Compare commits
246 Commits
forcegraph
...
develop
Author | SHA1 | Date | |
---|---|---|---|
8a756f0ecb | |||
b1a1df1a56 | |||
35cbfad9b0 | |||
68de16e73b | |||
9fe1d0fab8 | |||
538e47ed0e | |||
fb12482833 | |||
f5317e4559 | |||
07fbfe6a63 | |||
0fc62b342e | |||
b6a4d02507 | |||
4d046c2c8e | |||
|
71da7ebd8c | ||
|
50188b9b1a | ||
|
d29919556d | ||
|
0cd0af750d | ||
|
7429c5dc58 | ||
a37f26c256 | |||
a7cfaa8b0a | |||
fa9d990428 | |||
78ab3c0796 | |||
e65544a31b | |||
6563c9073e | |||
d1e9c75c0a | |||
ab67d494c9 | |||
cf3d47bdd7 | |||
441e1e61be | |||
b4b9d42bf4 | |||
a768496858 | |||
d987c63461 | |||
536a6c920f | |||
f14ba5d954 | |||
1347254bb2 | |||
614093d2af | |||
|
3c0cd2a36d | ||
|
4fd562b84b | ||
|
e448b02bb4 | ||
|
4d47587278 | ||
|
1fa034e1e8 | ||
|
1bcc6bfd10 | ||
|
35a79ba4f6 | ||
|
d28e834cd5 | ||
|
f40179535f | ||
|
1dfe8951ac | ||
|
3a3a3e014c | ||
|
e083ed13a7 | ||
|
8a89c0f5db | ||
|
ec0e0e5445 | ||
|
157da0aeba | ||
|
8959e1e7cb | ||
|
ddb1482a67 | ||
|
6c8c12be1e | ||
|
22af6d1bce | ||
|
90a19a4e74 | ||
|
b6f1dd066e | ||
|
d021d312d7 | ||
|
98e7a55f6f | ||
|
2736461604 | ||
|
c0ea13a8fa | ||
|
2200c4bc19 | ||
|
c99e4d8c42 | ||
|
aeb849aca8 | ||
|
3dc4d10082 | ||
|
97cda8cb18 | ||
|
5ad56139ac | ||
|
c45f367fcd | ||
|
29ea71a4d0 | ||
|
d2033af7d7 | ||
|
69e37f779f | ||
|
a34812f2a9 | ||
|
c02cf3be95 | ||
|
84085727a6 | ||
|
8fdc8dcabb | ||
|
77c94a1f2e | ||
|
2604b2b731 | ||
|
31d0209cc2 | ||
|
bc82e07354 | ||
|
fd9eab724b | ||
|
5d1d72d95d | ||
|
8bf3498744 | ||
|
2192500d05 | ||
|
712c3f21ce | ||
|
3d113c6247 | ||
|
9c596531e2 | ||
|
e1aa152055 | ||
|
d29bb31311 | ||
|
2c6303d820 | ||
|
2a7e1cdaa6 | ||
|
3311e70296 | ||
|
e1d3a3d7b2 | ||
|
25212adb81 | ||
|
4fd4e27a8b | ||
|
bfb1111744 | ||
|
81a26b5560 | ||
|
02e02f9219 | ||
|
20a8c4583a | ||
|
51be472ce4 | ||
|
e75d865fbf | ||
|
4f078f4b56 | ||
|
15f8f4bd74 | ||
|
3c72e12c3a | ||
|
28df47c15f | ||
|
ded730f36c | ||
|
ad4ee19344 | ||
|
48aa3a486b | ||
|
ad89c910ab | ||
|
7aab2f1462 | ||
|
61b9e9d351 | ||
|
3085c14c3b | ||
|
100cf626a5 | ||
|
6d57ad35c3 | ||
|
0c03efbfff | ||
|
ef9b9c3c3f | ||
|
1b89f86d74 | ||
|
b294a5d405 | ||
|
957cc892a9 | ||
|
6c0e667259 | ||
|
13f84d5dbc | ||
|
7bd2883732 | ||
|
5343b5dfaf | ||
|
82c9309a1d | ||
|
3714795e2b | ||
|
300a73213b | ||
|
4dd8dc9c45 | ||
|
87eb98f542 | ||
|
8e8cdd63af | ||
|
75c650b107 | ||
|
f115600e5b | ||
|
c32e951cf1 | ||
|
4bc2256f2f | ||
|
76fb09326e | ||
|
9212b0e427 | ||
|
fc596ee45f | ||
|
380b13d04b | ||
|
66127de355 | ||
|
bd670c85fd | ||
|
3c33dd63ac | ||
|
dc81543bc2 | ||
|
5c6f478fe6 | ||
|
198b97ad18 | ||
|
0aa3473a62 | ||
|
e0cef88beb | ||
|
71ab6e65e4 | ||
|
1673fe1248 | ||
|
c3cda56fe9 | ||
|
bac2977b3d | ||
|
b48b4f41f0 | ||
|
201c74d29b | ||
|
5702b5f21b | ||
|
b1a5e472e4 | ||
|
f8bf473666 | ||
|
8da8154edd | ||
|
c9dd968c92 | ||
|
ed80c7657e | ||
|
f729349a1f | ||
|
a30d12312a | ||
|
7fa0c5e522 | ||
|
6af7ba6796 | ||
|
bdac3c01e2 | ||
|
a71eff2d39 | ||
|
ad02f7789d | ||
|
0e20617435 | ||
|
2aadc39022 | ||
|
87e4c1d2f9 | ||
|
e99c38970a | ||
|
0c9860192b | ||
|
7bfbb1b909 | ||
|
72674fd6da | ||
|
338c90a2d1 | ||
|
844bf99641 | ||
|
dabfbfba83 | ||
|
1c14ec79ab | ||
|
18a2a17d97 | ||
|
d0b6031d5f | ||
|
2c7500f1bb | ||
|
cc18e53430 | ||
|
6add4f0916 | ||
|
b89e99b79e | ||
|
fd6c7c7f1e | ||
|
6091a8b82c | ||
|
c407f2e334 | ||
|
9a4836257f | ||
|
4315f18efe | ||
|
ed93a202d6 | ||
|
aa89f06342 | ||
|
1887a3270c | ||
|
9f95fa9c95 | ||
|
1ec81fd45c | ||
|
31e8667658 | ||
|
4caf38e990 | ||
|
a0378348b5 | ||
|
519f37cd14 | ||
|
1909eb291e | ||
|
8d6d508bba | ||
|
da029735cf | ||
|
f060884b04 | ||
|
57ee21f8ec | ||
|
af589ee227 | ||
|
d6b84eba22 | ||
|
fb857717fd | ||
|
b4bd941197 | ||
|
13eacf5fa8 | ||
|
e0630808e3 | ||
|
35fd75e4f6 | ||
|
914f6a344b | ||
|
10ab1ead2c | ||
|
b3ef2460b3 | ||
|
a9630ffa78 | ||
|
2a3dbc9842 | ||
|
4787aa7f62 | ||
|
77ac4ca3f5 | ||
|
db16ea8375 | ||
|
7c8456b18a | ||
|
24abeb74bd | ||
|
ec4732610b | ||
|
cb0aa1317c | ||
|
f0789392b5 | ||
|
93fb72788a | ||
|
def55bad15 | ||
|
e2a6200d75 | ||
|
a5d4140bda | ||
|
9aff29a634 | ||
|
f9892e5401 | ||
|
329a0c44fc | ||
|
779fa0a630 | ||
|
c24cf3cfb5 | ||
|
375627ab00 | ||
|
1995855693 | ||
|
1bd72654b8 | ||
|
ea057cf90f | ||
|
0cbf619c13 | ||
|
94b4a10bd4 | ||
|
b37ac2ddce | ||
|
717ba27992 | ||
|
374e73998a | ||
|
9c3f57dd3f | ||
|
6b67e4f714 | ||
|
2fba8c1ac9 | ||
|
8df5c2ce80 | ||
|
3e63b6432e | ||
|
0987b4b39a | ||
|
d0c5fb2dab | ||
|
ecf73dd7ab | ||
|
62c9c1c830 | ||
|
6e9d30445b | ||
|
69c6a75409 |
26
.bithoundrc
@ -1,26 +0,0 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"mute": [
|
||||
],
|
||||
"unused-ignores": [
|
||||
"almond",
|
||||
"d3-*",
|
||||
"leaflet",
|
||||
"moment",
|
||||
"navigo",
|
||||
"node-polyglot",
|
||||
"promise-polyfill",
|
||||
"rbush",
|
||||
"requirejs",
|
||||
"snabbdom"
|
||||
]
|
||||
},
|
||||
"critics": {
|
||||
"wc": {
|
||||
"limit": 5000
|
||||
}
|
||||
},
|
||||
"ignore": [
|
||||
"polyfill.js"
|
||||
]
|
||||
}
|
14
.drone.yml
Normal file
@ -0,0 +1,14 @@
|
||||
kind: pipeline
|
||||
name: meshviewer build
|
||||
|
||||
steps:
|
||||
- name: docker
|
||||
image: plugins/docker
|
||||
settings:
|
||||
repo: fftdf/meshviewer
|
||||
target: meshviewer
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
|
@ -2,15 +2,19 @@
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
charset = utf-8
|
||||
|
||||
# Get rid of whitespace to avoid diffs with a bunch of EOL changes
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
|
||||
[*.{js,html,scss,json,yml,md}]
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
|
||||
|
||||
[assets/favicon/manifest.json]
|
||||
indent_size = 4
|
||||
|
@ -9,5 +9,6 @@ rules:
|
||||
"func-names": 0
|
||||
"guard-for-in": 0
|
||||
"no-undefined": 0
|
||||
"consistent-return": 0
|
||||
"no-nested-ternary": 0
|
||||
"no-extend-native": ["error", { "exceptions": ["String"] }]
|
||||
|
7
.github/CONTRIBUTING.md
vendored
@ -1,5 +1,6 @@
|
||||
## Contributing is welcome
|
||||
|
||||
Pull requests are welcome without an opening an issue. If you unsure about the feature or implementation open an issue and
|
||||
discuss your suggested changes. Meshviewer is a frontend application and the code needs to be loaded and
|
||||
perform on slow mobile devices with many nodes and clients.
|
||||
Pull requests are welcome without the need of opening an issue. If you're unsure
|
||||
about your feature or your implementation open an issue and discuss your
|
||||
suggested changes. Meshviewer is a frontend application and the code needs to be
|
||||
loaded fast and perform with many nodes and clients on slow mobile devices.
|
||||
|
@ -1,3 +1,9 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
|
||||
---
|
||||
|
||||
<!--- Provide a general summary of the issue in the Title above -->
|
||||
<!--- This template should help to improve the report, unneeded parts can be remvoed -->
|
||||
|
||||
@ -13,7 +19,7 @@
|
||||
<!--- Not obligatory, but suggest a fix/reason for the bug, -->
|
||||
<!--- or ideas how to implement the addition or change -->
|
||||
|
||||
## Steps to Reproduce (for bugs)
|
||||
## Steps to Reproduce
|
||||
<!--- Provide a link to a live example, or an unambiguous set of steps to -->
|
||||
<!--- reproduce this bug. Include code to reproduce, if relevant -->
|
||||
1.
|
||||
@ -31,3 +37,6 @@
|
||||
* Browser Name and version:
|
||||
* Operating System and version (desktop or mobile):
|
||||
* Link to your project:
|
||||
|
||||
## Screenshots
|
||||
<!--- If applicable, add screenshots to help explain your problem. -->
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
|
||||
---
|
||||
|
||||
<!--- Provide a general summary of the issue in the Title above -->
|
||||
<!--- This template should help to improve the report, unneeded parts can be remvoed -->
|
||||
|
||||
## Is your feature request related to a problem? Please describe.
|
||||
<!--- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
|
||||
|
||||
## Describe the solution you'd like
|
||||
<!--- A clear and concise description of what you want to happen. -->
|
||||
|
||||
## Describe alternatives you've considered
|
||||
<!--- A clear and concise description of any alternative solutions or features you've considered. -->
|
||||
|
||||
## Additional context
|
||||
<!--- Add any other context or screenshots about the feature request here. -->
|
17
.travis.yml
@ -1,31 +1,24 @@
|
||||
sudo: false
|
||||
dist: trusty
|
||||
|
||||
language: node_js
|
||||
|
||||
node_js:
|
||||
- "7"
|
||||
- "12"
|
||||
|
||||
os:
|
||||
- linux
|
||||
- macosx
|
||||
- windows
|
||||
- osx
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- node_js: 7
|
||||
- node_js: "8"
|
||||
os: linux
|
||||
env: USE_NPM=true
|
||||
- node_js: 6
|
||||
- node_js: "10"
|
||||
os: linux
|
||||
- node_js: 4
|
||||
os: linux
|
||||
|
||||
cache:
|
||||
yarn: true
|
||||
|
||||
before_install:
|
||||
- if [ "$USE_NPM" == "true" ]; then rm yarn.lock; fi
|
||||
|
||||
before_script:
|
||||
- if git status | grep -q "modified. \.travis\.yml"; then echo "Dirty yarn.lock"; exit 1; fi
|
||||
|
||||
|
66
CHANGELOG.md
@ -1,66 +0,0 @@
|
||||
# Change Log
|
||||
|
||||
## Switched to rolling release
|
||||
|
||||
- All major changes can be found in README.md and everything else in git history https://github.com/ffrgb/meshviewer
|
||||
- Lot of parts of codebase have been changed
|
||||
|
||||
## v4
|
||||
|
||||
- add a legend (map)
|
||||
- new graph theme
|
||||
- performance improvements in graph view
|
||||
- various UI changes
|
||||
- various map fixes
|
||||
- moved config from config.js to config.json
|
||||
- online/offline statistics
|
||||
- define layers for map in config
|
||||
- graph: zoom by keyboard (+ and - keys)
|
||||
- direct links to graph and map views
|
||||
|
||||
### Bugfixes
|
||||
|
||||
- map works with little or no nodes
|
||||
|
||||
## v3
|
||||
|
||||
### Implemented enhancements:
|
||||
|
||||
- Make clients in map start at a random angle
|
||||
- On statistics page: show how many nodes supply geoinformation
|
||||
- Allow additional statistics (global and per node) configured in config.js
|
||||
- Improve node count information (total, online, clients, ...)
|
||||
- Show hardware model in link infobox
|
||||
- Introduce maxAge setting
|
||||
- Graph: show VPN links in grayscale
|
||||
|
||||
### Removed features:
|
||||
|
||||
- Don't show contact information in node lists
|
||||
|
||||
### Fixed bugs:
|
||||
|
||||
- Fixed off-by-one when drawing clients
|
||||
- Match labels order to node order in map
|
||||
- Statistics: count only nodes that are present
|
||||
|
||||
## v2
|
||||
|
||||
### General changes:
|
||||
|
||||
- License change from GPL 3 to AGPL 3
|
||||
|
||||
### Implemented enhancements:
|
||||
|
||||
- Improved performance on Firefox
|
||||
- Labels in graph view
|
||||
- infobox: link to geouri with node's coordinates
|
||||
- infobox: show node id
|
||||
- map: locate user
|
||||
- map: adding custom layers from leaflet.providers
|
||||
- nodelist: sort by uptime fixed
|
||||
- graph: circles for clients
|
||||
|
||||
### Fixed bugs:
|
||||
|
||||
- Links disappeared on graph on refresh
|
21
Dockerfile
Normal file
@ -0,0 +1,21 @@
|
||||
# builder
|
||||
FROM node:12.13.1-stretch as builder
|
||||
COPY . /mesh
|
||||
WORKDIR /mesh
|
||||
|
||||
# show versions
|
||||
RUN node --version && npm --version && yarn --version
|
||||
|
||||
# install gulp
|
||||
RUN npm i gulp-cli -g
|
||||
RUN npm i gulp -g
|
||||
|
||||
# run yarn for prerequisits
|
||||
RUN yarn
|
||||
|
||||
# run gulp to build app
|
||||
RUN gulp
|
||||
|
||||
# build docker container
|
||||
FROM nginx:1.17.6-alpine as meshviewer
|
||||
COPY --from=builder /mesh/build /usr/share/nginx/html/
|
52
README.md
@ -1,64 +1,20 @@
|
||||
# Meshviewer
|
||||
[![Build Status](https://img.shields.io/travis/ffrgb/meshviewer/develop.svg?style=flat-square)](https://travis-ci.org/ffrgb/meshviewer)
|
||||
[![Build Status](https://ci.freifunk-rhein-sieg.net/api/badges/Freifunk-Troisdorf/meshviewer/status.svg)](https://ci.freifunk-rhein-sieg.net/Freifunk-Troisdorf/meshviewer)
|
||||
[![Scrutinizer Code Quality](https://img.shields.io/scrutinizer/g/ffrgb/meshviewer/develop.svg?style=flat-square)](https://scrutinizer-ci.com/g/ffrgb/meshviewer/?branch=develop)
|
||||
[![Documentation](https://img.shields.io/badge/gitbooks.io-documentation-brightgreen.svg?style=flat-square)](https://meshviewer.gitbooks.io/documentation/content/)
|
||||
[![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg?style=flat-square)](https://www.gnu.org/licenses/agpl-3.0)
|
||||
[![License: AGPL v3](https://img.shields.io/github/license/ffrgb/meshviewer.svg?style=flat-square)](https://www.gnu.org/licenses/agpl-3.0)
|
||||
|
||||
Meshviewer is an online visualization app to represent nodes and links on a map for Freifunk open mesh network.
|
||||
|
||||
#### Main differences to https://github.com/ffnord/meshviewer
|
||||
_Some similar features might have been implemented/merged_
|
||||
|
||||
- Replaced router - including language, mode, node, link, location
|
||||
- Leaflet upgraded to v1 - faster on mobile
|
||||
- Forcegraph rewrite with d3.js v4
|
||||
- Map layer modes (Allow to set a default layer based on time combined with a stylesheet)
|
||||
- Automatic updates for selected node or list (incl. image stats cache-breaker)
|
||||
- Node filter
|
||||
- Zoom level for clicking on a node (`nodeZoom`) is definable independently from the maximum zoom level 22
|
||||
- Formatted Code
|
||||
- Translation support - https://crowdin.com/project/meshviewer - Contact us for new languages
|
||||
- Currently available: en, de, fr & ru
|
||||
- Gulp inline for some css and js - fewer requests and instant load indicator
|
||||
- Icon font with needed icons only
|
||||
- Switch to Gulp (Tested with Node.js 4/6 LTS, 7 on Linux, 7 OSX & W**)
|
||||
- css and some js moved inline
|
||||
- Yarn/npm in favour of bower
|
||||
- Load only moment.js without languages (Languages are included in translations)
|
||||
- unneeded components removed (es6-shim, tablesort, numeraljs, leaflet-providers, leaflet-label jshashes, chroma-js)
|
||||
- RBush v2 - performance boost in last versions (positions, labels and clients on the map)
|
||||
- Ruby dependency removed
|
||||
- FixedCenter is required
|
||||
- Sass-lint, scss and variables rewritten for easy customizations/adjustments
|
||||
- Cross browser/device support improved (THX@BrowserStack)
|
||||
- Yarn package manager in favour of npm (npm still works)
|
||||
- Configurable reverse geocoding server
|
||||
- [A lot more in the commit history](https://github.com/ffrgb/meshviewer/commits/develop)
|
||||
|
||||
### Demo
|
||||
|
||||
Embedded: https://regensburg.freifunk.net/netz/karte/
|
||||
Standalone: https://regensburg.freifunk.net/meshviewer/
|
||||
|
||||
## Documentation
|
||||
|
||||
Documentation moved to [meshviewer.gitbooks.io](https://meshviewer.gitbooks.io/documentation/content/).
|
||||
|
||||
- Read: https://meshviewer.gitbooks.io/documentation/content/
|
||||
- PDF, Mobi, ePub & edit: https://www.gitbook.com/book/meshviewer/documentation/details
|
||||
|
||||
#### Why move the documentation?
|
||||
|
||||
- Search available
|
||||
- Multiple pages
|
||||
- Less doc commits, faster changes
|
||||
- Export as PDF, Mobi, ePub
|
||||
|
||||
## Sponsoring / Supporting
|
||||
|
||||
- [BrowserStack](https://www.browserstack.com/) for providing an awesome testing service for hundreds of browsers
|
||||
- [Travis CI](https://travis-ci.org/) for building meshviewer on every push and pull request
|
||||
- [Travis CI](https://travis-ci.com/) for building meshviewer on every push and pull request
|
||||
- [Scrutinizer CI](https://scrutinizer-ci.com/g/ffrgb/meshviewer/) for testing code quality on every push and pull request
|
||||
- [Crowdin](https://crowdin.com/) for providing an easy non-developer translation environment
|
||||
- [POEditor](https://poeditor.com/join/project/VZBjPNNic9) for providing an easy non-developer translation environment
|
||||
|
||||
These tools need a lot of infrastructures and provide a free account for open source software.
|
||||
|
26
app.js
@ -9,21 +9,21 @@ require.config({
|
||||
'moment': '../node_modules/moment/moment',
|
||||
// d3 modules indirect dependencies
|
||||
// by d3-zoom: d3-drag
|
||||
'd3-ease': '../node_modules/d3-ease/build/d3-ease',
|
||||
'd3-transition': '../node_modules/d3-transition/build/d3-transition',
|
||||
'd3-color': '../node_modules/d3-color/build/d3-color',
|
||||
'd3-interpolate': '../node_modules/d3-interpolate/build/d3-interpolate',
|
||||
'd3-ease': '../node_modules/d3-ease/dist/d3-ease',
|
||||
'd3-transition': '../node_modules/d3-transition/dist/d3-transition',
|
||||
'd3-color': '../node_modules/d3-color/dist/d3-color',
|
||||
'd3-interpolate': '../node_modules/d3-interpolate/dist/d3-interpolate',
|
||||
// by d3-force
|
||||
'd3-collection': '../node_modules/d3-collection/build/d3-collection',
|
||||
'd3-dispatch': '../node_modules/d3-dispatch/build/d3-dispatch',
|
||||
'd3-quadtree': '../node_modules/d3-quadtree/build/d3-quadtree',
|
||||
'd3-timer': '../node_modules/d3-timer/build/d3-timer',
|
||||
'd3-collection': '../node_modules/d3-collection/dist/d3-collection',
|
||||
'd3-dispatch': '../node_modules/d3-dispatch/dist/d3-dispatch',
|
||||
'd3-quadtree': '../node_modules/d3-quadtree/dist/d3-quadtree',
|
||||
'd3-timer': '../node_modules/d3-timer/dist/d3-timer',
|
||||
// by d3-drag: d3-selection
|
||||
// d3 modules dependencies
|
||||
'd3-selection': '../node_modules/d3-selection/build/d3-selection',
|
||||
'd3-force': '../node_modules/d3-force/build/d3-force',
|
||||
'd3-zoom': '../node_modules/d3-zoom/build/d3-zoom',
|
||||
'd3-drag': '../node_modules/d3-drag/build/d3-drag',
|
||||
'd3-selection': '../node_modules/d3-selection/dist/d3-selection',
|
||||
'd3-force': '../node_modules/d3-force/dist/d3-force',
|
||||
'd3-zoom': '../node_modules/d3-zoom/dist/d3-zoom',
|
||||
'd3-drag': '../node_modules/d3-drag/dist/d3-drag',
|
||||
'snabbdom': '../node_modules/snabbdom/dist/snabbdom-patch',
|
||||
'rbush': '../node_modules/rbush/rbush',
|
||||
'helper': 'utils/helper'
|
||||
@ -37,5 +37,5 @@ require.config({
|
||||
});
|
||||
|
||||
require(['main'], function (main) {
|
||||
main(jsonData);
|
||||
main();
|
||||
});
|
||||
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 9.1 KiB |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 624 B After Width: | Height: | Size: 485 B |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 886 B |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 9.3 KiB |
@ -1,5 +1,6 @@
|
||||
{
|
||||
"name": "Meshviewer",
|
||||
"short_name": "Meshviewer",
|
||||
"icons": [
|
||||
{
|
||||
"src": "./android-chrome-192x192.png",
|
||||
@ -14,5 +15,7 @@
|
||||
],
|
||||
"theme_color": "#dc0067",
|
||||
"background_color": "#dc0067",
|
||||
"display": "standalone"
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"orientation": "portrait"
|
||||
}
|
@ -1 +1 @@
|
||||
{"result":{"status":"success"},"favicon":{"package_url":"https://realfavicongenerator.net/files/03dc81277d21a8ed4bb836b4c05ada2ee75b9e3c/favicons.zip","files_urls":["https://realfavicongenerator.net/files/03dc81277d21a8ed4bb836b4c05ada2ee75b9e3c/package_files/android-chrome-192x192.png","https://realfavicongenerator.net/files/03dc81277d21a8ed4bb836b4c05ada2ee75b9e3c/package_files/android-chrome-512x512.png","https://realfavicongenerator.net/files/03dc81277d21a8ed4bb836b4c05ada2ee75b9e3c/package_files/apple-touch-icon.png","https://realfavicongenerator.net/files/03dc81277d21a8ed4bb836b4c05ada2ee75b9e3c/package_files/browserconfig.xml","https://realfavicongenerator.net/files/03dc81277d21a8ed4bb836b4c05ada2ee75b9e3c/package_files/favicon-16x16.png","https://realfavicongenerator.net/files/03dc81277d21a8ed4bb836b4c05ada2ee75b9e3c/package_files/favicon-32x32.png","https://realfavicongenerator.net/files/03dc81277d21a8ed4bb836b4c05ada2ee75b9e3c/package_files/favicon.ico","https://realfavicongenerator.net/files/03dc81277d21a8ed4bb836b4c05ada2ee75b9e3c/package_files/manifest.json","https://realfavicongenerator.net/files/03dc81277d21a8ed4bb836b4c05ada2ee75b9e3c/package_files/mstile-144x144.png","https://realfavicongenerator.net/files/03dc81277d21a8ed4bb836b4c05ada2ee75b9e3c/package_files/mstile-150x150.png","https://realfavicongenerator.net/files/03dc81277d21a8ed4bb836b4c05ada2ee75b9e3c/package_files/mstile-310x150.png","https://realfavicongenerator.net/files/03dc81277d21a8ed4bb836b4c05ada2ee75b9e3c/package_files/mstile-310x310.png","https://realfavicongenerator.net/files/03dc81277d21a8ed4bb836b4c05ada2ee75b9e3c/package_files/mstile-70x70.png","https://realfavicongenerator.net/files/03dc81277d21a8ed4bb836b4c05ada2ee75b9e3c/package_files/safari-pinned-tab.svg"],"html_code":"<link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"./apple-touch-icon.png\">\n<link rel=\"icon\" type=\"image/png\" href=\"./favicon-32x32.png\" sizes=\"32x32\">\n<link rel=\"icon\" type=\"image/png\" href=\"./favicon-16x16.png\" sizes=\"16x16\">\n<link rel=\"manifest\" href=\"./manifest.json\">\n<link rel=\"mask-icon\" href=\"./safari-pinned-tab.svg\" color=\"#dc0067\">\n<link rel=\"shortcut icon\" href=\"./favicon.ico\">\n<meta name=\"apple-mobile-web-app-title\" content=\"Meshviewer\">\n<meta name=\"application-name\" content=\"Meshviewer\">\n<meta name=\"msapplication-TileColor\" content=\"#dc0067\">\n<meta name=\"msapplication-TileImage\" content=\"./mstile-144x144.png\">\n<meta name=\"msapplication-config\" content=\"./browserconfig.xml\">\n<meta name=\"theme-color\" content=\"#dc0067\">","compression":"true","overlapping_markups":["link[rel=\"apple-touch-icon\"]","meta[name=\"apple-mobile-web-app-title\"]","link[rel=\"shortcut\"]","link[rel=\"shortcut icon\"]","link[rel=\"icon\",sizes=\"16x16\"]","link[rel=\"icon\",sizes=\"32x32\"]","meta[name=\"msapplication-TileColor\"]","meta[name=\"msapplication-TileImage\"]","meta[name=\"msapplication-config\"]","meta[name=\"application-name\"]","link[rel=\"manifest\"]","meta[name=\"theme-color\"]","link[rel=\"mask-icon\"]"]},"files_location":{"type":"path","path":"."},"preview_picture_url":"https://realfavicongenerator.net/files/03dc81277d21a8ed4bb836b4c05ada2ee75b9e3c/favicon_preview.png","version":"0.14"}
|
||||
{"result":{"status":"success"},"favicon":{"package_url":"https://realfavicongenerator.net/files/ed9ef5a59ae048602fb9a5b74436696e43a575ce/favicon_package_v0.16.zip","files_urls":["https://realfavicongenerator.net/files/ed9ef5a59ae048602fb9a5b74436696e43a575ce/package_files/android-chrome-192x192.png","https://realfavicongenerator.net/files/ed9ef5a59ae048602fb9a5b74436696e43a575ce/package_files/android-chrome-512x512.png","https://realfavicongenerator.net/files/ed9ef5a59ae048602fb9a5b74436696e43a575ce/package_files/apple-touch-icon.png","https://realfavicongenerator.net/files/ed9ef5a59ae048602fb9a5b74436696e43a575ce/package_files/browserconfig.xml","https://realfavicongenerator.net/files/ed9ef5a59ae048602fb9a5b74436696e43a575ce/package_files/favicon-16x16.png","https://realfavicongenerator.net/files/ed9ef5a59ae048602fb9a5b74436696e43a575ce/package_files/favicon-32x32.png","https://realfavicongenerator.net/files/ed9ef5a59ae048602fb9a5b74436696e43a575ce/package_files/favicon.ico","https://realfavicongenerator.net/files/ed9ef5a59ae048602fb9a5b74436696e43a575ce/package_files/mstile-144x144.png","https://realfavicongenerator.net/files/ed9ef5a59ae048602fb9a5b74436696e43a575ce/package_files/mstile-150x150.png","https://realfavicongenerator.net/files/ed9ef5a59ae048602fb9a5b74436696e43a575ce/package_files/mstile-310x150.png","https://realfavicongenerator.net/files/ed9ef5a59ae048602fb9a5b74436696e43a575ce/package_files/mstile-310x310.png","https://realfavicongenerator.net/files/ed9ef5a59ae048602fb9a5b74436696e43a575ce/package_files/mstile-70x70.png","https://realfavicongenerator.net/files/ed9ef5a59ae048602fb9a5b74436696e43a575ce/package_files/safari-pinned-tab.svg","https://realfavicongenerator.net/files/ed9ef5a59ae048602fb9a5b74436696e43a575ce/package_files/site.webmanifest"],"html_code":"<link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"./apple-touch-icon.png\">\n<link rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"./favicon-32x32.png\">\n<link rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"./favicon-16x16.png\">\n<link rel=\"manifest\" href=\"./site.webmanifest\">\n<link rel=\"mask-icon\" href=\"./safari-pinned-tab.svg\" color=\"#dc0067\">\n<link rel=\"shortcut icon\" href=\"./favicon.ico\">\n<meta name=\"apple-mobile-web-app-title\" content=\"<!-- inject:title --><!-- endinject -->\">\n<meta name=\"application-name\" content=\"<!-- inject:title --><!-- endinject -->\">\n<meta name=\"msapplication-TileColor\" content=\"#dc0067\">\n<meta name=\"msapplication-TileImage\" content=\"./mstile-144x144.png\">\n<meta name=\"msapplication-config\" content=\"./browserconfig.xml\">\n<meta name=\"theme-color\" content=\"#dc0067\">","compression":"true","overlapping_markups":["link[rel=\"apple-touch-icon\"]","meta[name=\"apple-mobile-web-app-title\"]","link[rel=\"shortcut\"]","link[rel=\"shortcut icon\"]","link[rel=\"icon\",sizes=\"16x16\"]","link[rel=\"icon\",sizes=\"32x32\"]","meta[name=\"msapplication-TileColor\"]","meta[name=\"msapplication-TileImage\"]","meta[name=\"msapplication-config\"]","meta[name=\"application-name\"]","link[rel=\"manifest\"]","meta[name=\"theme-color\"]","link[rel=\"mask-icon\"]"]},"files_location":{"type":"path","path":"."},"preview_picture_url":"https://realfavicongenerator.net/files/ed9ef5a59ae048602fb9a5b74436696e43a575ce/favicon_preview.png","version":"0.16"}
|
||||
|
BIN
assets/icons/fonts/meshviewer.ttf
Normal file
BIN
assets/icons/fonts/meshviewer.woff
Normal file
BIN
assets/icons/fonts/meshviewer.woff2
Normal file
@ -7,9 +7,9 @@ $cache-breaker: unique-id();
|
||||
font-family: 'ionicons';
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
src: url('fonts/icon.woff2?rel=#{$cache-breaker}') format('woff2'),
|
||||
url('fonts/icon.woff?rel=#{$cache-breaker}') format('woff'),
|
||||
url('fonts/icon.ttf?rel=#{$cache-breaker}') format('truetype');
|
||||
src: url('fonts/meshviewer.woff2?rel=#{$cache-breaker}') format('woff2'),
|
||||
url('fonts/meshviewer.woff?rel=#{$cache-breaker}') format('woff'),
|
||||
url('fonts/meshviewer.ttf?rel=#{$cache-breaker}') format('truetype');
|
||||
}
|
||||
|
||||
[class^='ion-'],
|
||||
@ -49,3 +49,5 @@ $cache-breaker: unique-id();
|
||||
@include icon('arrow-resize', '\f264');
|
||||
@include icon('arrow-left-c', '\f108');
|
||||
@include icon('arrow-right-c', '\f10b');
|
||||
@include icon('full-enter', '\e901');
|
||||
@include icon('full-exit', '\e900');
|
||||
|
194
config.default.js
Normal file
@ -0,0 +1,194 @@
|
||||
module.exports = function () {
|
||||
return {
|
||||
'reverseGeocodingApi': 'https://nominatim.openstreetmap.org/reverse',
|
||||
'maxAge': 14,
|
||||
'maxAgeAlert': 3,
|
||||
'nodeZoom': 20,
|
||||
'labelZoom': 13,
|
||||
'clientZoom': 15,
|
||||
'fullscreen': true,
|
||||
'fullscreenFrame': true,
|
||||
'nodeAttr': [
|
||||
// value can be a node attribute (1 depth) or a a function in utils/node with prefix show
|
||||
{
|
||||
'name': 'node.status',
|
||||
'value': 'Status'
|
||||
},
|
||||
{
|
||||
'name': 'node.gateway',
|
||||
'value': 'Gateway'
|
||||
},
|
||||
{
|
||||
'name': 'node.coordinates',
|
||||
'value': 'GeoURI'
|
||||
},
|
||||
// {
|
||||
// "name": "node.contact",
|
||||
// "value": "owner"
|
||||
// },
|
||||
|
||||
// Examples for functions
|
||||
// {
|
||||
// // no name will remove first column
|
||||
// 'value': function (d) {
|
||||
// var moment = require('moment');
|
||||
// var V = require('snabbdom').default;
|
||||
// return V.h('td', { props: { colSpan: 2 }, style: { background: '#49a' } },
|
||||
// _.t('sidebar.nodeOnline') + ' translate, ' + moment(d.firstseen).get('month') +
|
||||
// ' Month require libs like moment, access config ' + config.siteName);
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// 'name': 'Neighbour first seen',
|
||||
// 'value': function (d, nodeDict) {
|
||||
// return nodeDict[d.gateway_nexthop].firstseen.format() + 'access node object';
|
||||
// }
|
||||
// },
|
||||
{
|
||||
'name': 'node.hardware',
|
||||
'value': 'model'
|
||||
},
|
||||
{
|
||||
'name': 'node.primaryMac',
|
||||
'value': 'mac'
|
||||
},
|
||||
{
|
||||
'name': 'node.firmware',
|
||||
'value': 'Firmware'
|
||||
},
|
||||
{
|
||||
'name': 'node.uptime',
|
||||
'value': 'Uptime'
|
||||
},
|
||||
{
|
||||
'name': 'node.firstSeen',
|
||||
'value': 'FirstSeen'
|
||||
},
|
||||
{
|
||||
'name': 'node.systemLoad',
|
||||
'value': 'Load'
|
||||
},
|
||||
{
|
||||
'name': 'node.ram',
|
||||
'value': 'RAM'
|
||||
},
|
||||
{
|
||||
'name': 'node.ipAddresses',
|
||||
'value': 'IPs'
|
||||
},
|
||||
{
|
||||
'name': 'node.update',
|
||||
'value': 'Autoupdate'
|
||||
},
|
||||
{
|
||||
'name': 'node.domain',
|
||||
'value': 'Domain'
|
||||
},
|
||||
{
|
||||
'name': 'node.clients',
|
||||
'value': 'Clients'
|
||||
}
|
||||
],
|
||||
'supportedLocale': [
|
||||
'en',
|
||||
'de',
|
||||
'cz',
|
||||
'fr',
|
||||
'tr',
|
||||
'ru'
|
||||
],
|
||||
// Color configs
|
||||
'icon': {
|
||||
'base': {
|
||||
'fillOpacity': 0.6,
|
||||
'opacity': 0.6,
|
||||
'weight': 2,
|
||||
'radius': 6,
|
||||
'className': 'stroke-first'
|
||||
},
|
||||
'online': {
|
||||
'color': '#1566A9',
|
||||
'fillColor': '#1566A9'
|
||||
},
|
||||
'offline': {
|
||||
'color': '#D43E2A',
|
||||
'fillColor': '#D43E2A',
|
||||
'radius': 3
|
||||
},
|
||||
'lost': {
|
||||
'color': '#D43E2A',
|
||||
'fillColor': '#D43E2A',
|
||||
'radius': 4
|
||||
},
|
||||
'alert': {
|
||||
'color': '#D43E2A',
|
||||
'fillColor': '#D43E2A',
|
||||
'radius': 5
|
||||
},
|
||||
'new': {
|
||||
'color': '#1566A9',
|
||||
'fillColor': '#93E929'
|
||||
}
|
||||
},
|
||||
'client': {
|
||||
'wifi24': 'rgba(220, 0, 103, 0.7)',
|
||||
'wifi5': 'rgba(10, 156, 146, 0.7)',
|
||||
'other': 'rgba(227, 166, 25, 0.7)'
|
||||
},
|
||||
'map': {
|
||||
'labelNewColor': '#459c18',
|
||||
'tqFrom': '#F02311',
|
||||
'tqTo': '#04C714',
|
||||
'highlightNode': {
|
||||
'color': '#ad2358',
|
||||
'weight': 8,
|
||||
'fillOpacity': 1,
|
||||
'opacity': 0.4,
|
||||
'className': 'stroke-first'
|
||||
},
|
||||
'highlightLink': {
|
||||
'weight': 4,
|
||||
'opacity': 1,
|
||||
'dashArray': '5, 10'
|
||||
}
|
||||
},
|
||||
'forceGraph': {
|
||||
'nodeColor': '#fff',
|
||||
'nodeOfflineColor': '#D43E2A',
|
||||
'highlightColor': 'rgba(255, 255, 255, 0.2)',
|
||||
'labelColor': '#fff',
|
||||
'tqFrom': '#770038',
|
||||
'tqTo': '#dc0067',
|
||||
'zoomModifier': 1
|
||||
},
|
||||
'locate': {
|
||||
'outerCircle': {
|
||||
'stroke': false,
|
||||
'color': '#4285F4',
|
||||
'opacity': 1,
|
||||
'fillOpacity': 0.3,
|
||||
'clickable': false,
|
||||
'radius': 16
|
||||
},
|
||||
'innerCircle': {
|
||||
'stroke:': true,
|
||||
'color': '#ffffff',
|
||||
'fillColor': '#4285F4',
|
||||
'weight': 1.5,
|
||||
'clickable': false,
|
||||
'opacity': 1,
|
||||
'fillOpacity': 1,
|
||||
'radius': 7
|
||||
},
|
||||
'accuracyCircle': {
|
||||
'stroke': true,
|
||||
'color': '#4285F4',
|
||||
'weight': 1,
|
||||
'clickable': false,
|
||||
'opacity': 0.7,
|
||||
'fillOpacity': 0.2
|
||||
}
|
||||
},
|
||||
'cacheBreaker': '<!-- inject:cache-breaker -->'
|
||||
};
|
||||
};
|
@ -1,52 +0,0 @@
|
||||
// Gulp will remove all comments
|
||||
{
|
||||
"reverseGeocodingApi": "https://nominatim.openstreetmap.org/reverse",
|
||||
"maxAge": 14,
|
||||
"maxAgeAlert": 3,
|
||||
"nodeZoom": 18,
|
||||
"labelZoom": 13,
|
||||
"clientZoom": 15,
|
||||
"nodeInfobox": {
|
||||
"contact": false,
|
||||
"hardwareUsage": true
|
||||
},
|
||||
"supportedLocale": [
|
||||
"en",
|
||||
"de",
|
||||
"fr",
|
||||
"ru"
|
||||
],
|
||||
"icon": {
|
||||
"base": {
|
||||
"fillOpacity": 0.6,
|
||||
"opacity": 0.6,
|
||||
"weight": 2,
|
||||
"radius": 6,
|
||||
"className": "stroke-first"
|
||||
},
|
||||
"online": {
|
||||
"color": "#1566A9",
|
||||
"fillColor": "#1566A9"
|
||||
},
|
||||
"offline": {
|
||||
"color": "#D43E2A",
|
||||
"fillColor": "#D43E2A",
|
||||
"radius": 3
|
||||
},
|
||||
"lost": {
|
||||
"color": "#D43E2A",
|
||||
"fillColor": "#D43E2A",
|
||||
"radius": 4
|
||||
},
|
||||
"alert": {
|
||||
"color": "#D43E2A",
|
||||
"fillColor": "#D43E2A",
|
||||
"radius": 5
|
||||
},
|
||||
"new": {
|
||||
"color": "#1566A9",
|
||||
"fillColor": "#93E929"
|
||||
}
|
||||
},
|
||||
"cacheBreaker": "<!-- inject:cache-breaker -->"
|
||||
}
|
89
config.js
Normal file
@ -0,0 +1,89 @@
|
||||
module.exports = function () {
|
||||
return {
|
||||
// Variables are NODE_ID and NODE_NAME (only a-z0-9\- other chars are replaced with _)
|
||||
'nodeInfos': [
|
||||
{
|
||||
'name': 'Clientstatistik',
|
||||
'href': 'https://statistik.freifunk-troisdorf.de/d/000000001/node-stats?orgId=1&var-node={NODE_ID}&var-saveinterval=60',
|
||||
'image': 'https://statistik.freifunk-troisdorf.de/render/d-solo/000000001/node-stats?orgId=1&var-node={NODE_ID}&var-saveinterval=60&theme=light&panelId=1&width=1000&height=500&tz=Europe%2FBerlin',
|
||||
'title': 'Knoten {NODE_ID}'
|
||||
},
|
||||
{
|
||||
'name': 'Traffic',
|
||||
'href': 'https://statistik.freifunk-troisdorf.de/d/000000001/node-stats?orgId=1&var-node={NODE_ID}&var-saveinterval=60',
|
||||
'image': 'https://statistik.freifunk-troisdorf.de/render/d-solo/000000001/node-stats?orgId=1&var-node={NODE_ID}&var-saveinterval=60&theme=light&panelId=2&width=1000&height=500&tz=Europe%2FBerlin',
|
||||
'title': 'Knoten {NODE_ID}'
|
||||
}
|
||||
],
|
||||
// Array of data provider are supported
|
||||
'dataPath': [
|
||||
// 'https://map.freifunk-troisdorf.de/data/tdf4/',
|
||||
// 'https://map.freifunk-troisdorf.de/data/tdf5/',
|
||||
// 'https://map.freifunk-troisdorf.de/data/tdf6/',
|
||||
// 'https://map.freifunk-troisdorf.de/data/rifu/'
|
||||
'https://map.freifunk-troisdorf.de/data/api/'
|
||||
],
|
||||
'siteName': 'Freifunk Troisdorf',
|
||||
'maxAge': 7,
|
||||
'mapLayers': [
|
||||
{
|
||||
'name': 'Freifunk Rhein-Sieg',
|
||||
// Please ask Freifunk Rhein Sieg before using its tile server c- example with retina tiles
|
||||
'url': 'https://tile.freifunk-rhein-sieg.net/tile/{z}/{x}/{y}.png',
|
||||
'config': {
|
||||
'maxZoom': 20,
|
||||
'attribution': '<a href="https://freifunk-rhein-sieg.net/" target="_blank">© Freifunk-Rhein-Sieg</a> <a href="http://www.openstreetmap.org/about/" target="_blank">© OpenStreetMap contributors</a>'
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'OSM',
|
||||
'url': 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
'config': {
|
||||
'maxZoom': 20,
|
||||
'attribution': '<a href="http://www.openmaptiles.org/" target="_blank">© OpenMapTiles</a> <a href="http://www.openstreetmap.org/about/" target="_blank">© OpenStreetMap contributors</a>'
|
||||
}
|
||||
}
|
||||
],
|
||||
// Set a visible frame
|
||||
'fixedCenter': [
|
||||
// Northwest
|
||||
[
|
||||
50.8428,
|
||||
7.0367
|
||||
],
|
||||
// Southeast
|
||||
[
|
||||
50.776,
|
||||
7.1919
|
||||
]
|
||||
],
|
||||
'domainNames': [
|
||||
{
|
||||
'site': 'tdf',
|
||||
'name': 'Troisdorf'
|
||||
},
|
||||
{
|
||||
'site': 'inn',
|
||||
'name': 'Innenstadt'
|
||||
},
|
||||
{
|
||||
'site': 'rifu',
|
||||
'name': 'Richtfunk'
|
||||
},
|
||||
{
|
||||
'site': 'flu',
|
||||
'name': 'Soziale Netze'
|
||||
}
|
||||
],
|
||||
'linkList': [
|
||||
{
|
||||
'title': 'Impressum',
|
||||
'href': 'http://freifunk-troisdorf.de/kontakt/impressum/'
|
||||
},
|
||||
{
|
||||
'title': 'Datenschutz',
|
||||
'href': 'http://freifunk-troisdorf.de/datenschutz/'
|
||||
}
|
||||
]
|
||||
};
|
||||
};
|
115
config.json
@ -1,115 +0,0 @@
|
||||
// Gulp will remove all comments
|
||||
{
|
||||
// Variables are NODE_ID and NODE_NAME (only a-z0-9\- other chars are replaced with _)
|
||||
"nodeInfos": [
|
||||
{
|
||||
"name": "Clientstatistik",
|
||||
"href": "https://regensburg.freifunk.net/netz/statistik/node/{NODE_ID}/",
|
||||
"image": "https://grafana.regensburg.freifunk.net/render/dashboard-solo/db/ffrgb-all-nodes?panelId=1&from=now-7d&var-nodeid={NODE_ID}&var-host={NODE_NAME}&width=650&height=350&theme=light&_t={TIME}",
|
||||
"title": "Knoten {NODE_ID} - weiteren Statistiken"
|
||||
},
|
||||
{
|
||||
"name": "Trafficstatistik",
|
||||
"href": "https://regensburg.freifunk.net/netz/statistik/node/{NODE_ID}/",
|
||||
"image": "https://grafana.regensburg.freifunk.net/render/dashboard-solo/db/ffrgb-all-nodes?panelId=2&from=now-7d&var-nodeid={NODE_ID}&var-host={NODE_NAME}&width=650&height=350&theme=light&_t={TIME}",
|
||||
"title": "Knoten {NODE_ID} - weiteren Statistiken"
|
||||
}
|
||||
],
|
||||
"globalInfos": [
|
||||
{
|
||||
"name": "Statistik",
|
||||
"href": "https://regensburg.freifunk.net/netz/statistik/",
|
||||
"image": "https://grafana.regensburg.freifunk.net/render/dashboard-solo/db/ffrgb-network-wide-stats?panelId=11&from=now-1y&width=600&height=350&theme=light",
|
||||
"title": "Jahresstatistik - weiteren Statistiken"
|
||||
}
|
||||
],
|
||||
// String or array of data provider are supported
|
||||
"dataPath": "https://regensburg.freifunk.net/data/",
|
||||
"siteName": "Freifunk Regensburg",
|
||||
"mapLayers": [
|
||||
{
|
||||
"name": "Freifunk Regensburg",
|
||||
// Please ask Freifunk Regensburg before using its tile server c- example with retina tiles
|
||||
"url": "https://{s}.tiles.ffrgb.net/{z}/{x}/{y}{retina}.png",
|
||||
"config": {
|
||||
"maxZoom": 22,
|
||||
"subdomains": "1234",
|
||||
"attribution": "<a href=\"https://www.mapbox.com/about/maps/\" target=\"_blank\">© Mapbox</a> <a href=\"https://openstreetmap.org/about/\" target=\"_blank\">© OpenStreetMap</a> <a class=\"mapbox-improve-map\" href=\"https://www.mapbox.com/map-feedback/\" target=\"_blank\">Improve this map</a>",
|
||||
"start": 6
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Freifunk Regensburg Night",
|
||||
// Please ask Freifunk Regensburg before using its tile server - example with retina and dark tiles
|
||||
"url": "https://{s}.tiles.ffrgb.net/n/{z}/{x}/{y}{retina}.png",
|
||||
"config": {
|
||||
"maxZoom": 22,
|
||||
"subdomains": "1234",
|
||||
"attribution": "<a href=\"https://www.mapbox.com/about/maps/\" target=\"_blank\">© Mapbox</a> <a href=\"https://openstreetmap.org/about/\" target=\"_blank\">© OpenStreetMap</a> <a class=\"mapbox-improve-map\" href=\"https://www.mapbox.com/map-feedback/\" target=\"_blank\">Improve this map</a>",
|
||||
"mode": "night",
|
||||
"start": 19,
|
||||
"end": 7
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "OpenStreetMap.HOT",
|
||||
"url": "https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png",
|
||||
"config": {
|
||||
"maxZoom": 19,
|
||||
"attribution": "© Openstreetmap France | © <a href=\"http://www.openstreetmap.org/copyright\">OpenStreetMap</a>"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "HERE",
|
||||
// Please use your own API key - Free plan is on right side after the pay plans
|
||||
"url": "https://{s}.base.maps.api.here.com/maptile/2.1/maptile/newest/normal.day/{z}/{x}/{y}/256/png8?app_id=YOUR_KEY&app_code=YOUR_CODE&lg=deu",
|
||||
"config": {
|
||||
"attribution": "Map © 1987-2014 <a href=\"http://developer.here.com\">HERE</a>",
|
||||
"subdomains": "1234",
|
||||
"maxZoom": 20
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Esri.WorldImagery",
|
||||
"url": "//server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
|
||||
"config": {
|
||||
"maxZoom": 20,
|
||||
"attribution": "Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "HERE.hybridDay",
|
||||
// Please use your own API key - Free plan is on right side after the pay plans
|
||||
"url": "https://{s}.aerial.maps.api.here.com/maptile/2.1/maptile/newest/{variant}/{z}/{x}/{y}/256/png8?app_id=YOUR_KEY&app_code=YOUR_CODE&lg=deu",
|
||||
"config": {
|
||||
"attribution": "Map © 1987-2014 <a href=\"http://developer.here.com\">HERE</a>",
|
||||
"subdomains": "1234",
|
||||
"variant": "hybrid.day",
|
||||
"maxZoom": 20
|
||||
}
|
||||
}
|
||||
],
|
||||
// Set a visible frame
|
||||
"fixedCenter": [
|
||||
// Northwest
|
||||
[
|
||||
49.3522,
|
||||
11.7752
|
||||
],
|
||||
// Southeast
|
||||
[
|
||||
48.7480,
|
||||
12.8917
|
||||
]
|
||||
],
|
||||
"siteNames": [
|
||||
{
|
||||
"site": "ffrgb-bat15",
|
||||
"name": "Regensburg"
|
||||
},
|
||||
{
|
||||
"site": "ffrgb",
|
||||
"name": "Regensburg"
|
||||
}
|
||||
]
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
files:
|
||||
- source: /locale/en.json
|
||||
translation: /locale/%two_letters_code%.json
|
@ -8,11 +8,11 @@ module.exports = function () {
|
||||
sass: 'scss/**/*.scss',
|
||||
javascript: ['./app.js', 'lib/**/*.js'],
|
||||
json: 'locale/*.json',
|
||||
html: ['html/*.html', './config*.json']
|
||||
html: ['html/*.html', './config*.js']
|
||||
},
|
||||
clean: [build + '/*.map', build + '/vendor', build + '/main.css'],
|
||||
autoprefixer: ['> 1% in DE'],
|
||||
browsersync: {
|
||||
open: false,
|
||||
server: {
|
||||
baseDir: build
|
||||
},
|
||||
|
@ -2,10 +2,12 @@ module.exports = function (gulp, plugins, config) {
|
||||
return function copy() {
|
||||
gulp.src(['html/*.html', 'assets/favicon/*'])
|
||||
.pipe(gulp.dest(config.build));
|
||||
gulp.src(['assets/logo.svg'])
|
||||
gulp.src(['assets/logo.svg', 'service-worker.js'])
|
||||
.pipe(gulp.dest(config.build));
|
||||
gulp.src(['node_modules/promise-polyfill/promise.js', 'polyfill.js'])
|
||||
gulp.src(['polyfill.js'])
|
||||
.pipe(gulp.dest(config.build + '/vendor'));
|
||||
gulp.src(['node_modules/promise-polyfill/dist/polyfill.js'])
|
||||
.pipe(gulp.dest(config.build + '/vendor/promise'));
|
||||
return gulp.src(['assets/fonts/*', 'assets/icons/fonts/*'])
|
||||
.pipe(gulp.dest(config.build + '/fonts'));
|
||||
};
|
||||
|
@ -7,7 +7,7 @@ module.exports = function (gulp, plugins, config) {
|
||||
design: {
|
||||
ios: {
|
||||
pictureAspect: 'backgroundAndMargin',
|
||||
backgroundColor: '#000000',
|
||||
backgroundColor: '#ffffff',
|
||||
margin: '14%',
|
||||
assets: {
|
||||
ios6AndPriorIcons: false,
|
||||
@ -19,7 +19,7 @@ module.exports = function (gulp, plugins, config) {
|
||||
},
|
||||
desktopBrowser: {},
|
||||
windows: {
|
||||
pictureAspect: 'noChange',
|
||||
pictureAspect: 'whiteSilhouette',
|
||||
backgroundColor: '#dc0067',
|
||||
onConflict: 'override',
|
||||
assets: {
|
||||
@ -39,7 +39,7 @@ module.exports = function (gulp, plugins, config) {
|
||||
manifest: {
|
||||
name: 'Meshviewer',
|
||||
display: 'standalone',
|
||||
orientation: 'notSet',
|
||||
orientation: 'portrait',
|
||||
onConflict: 'override',
|
||||
declared: true
|
||||
},
|
||||
|
@ -1,25 +1,52 @@
|
||||
const fs = require('fs');
|
||||
|
||||
// stringify functions https://gist.github.com/cowboy/3749767
|
||||
var stringify = function (obj) {
|
||||
var placeholder = '____PLACEHOLDER____';
|
||||
var fns = [];
|
||||
var json = JSON.stringify(obj, function (key, value) {
|
||||
if (typeof value === 'function') {
|
||||
fns.push(value);
|
||||
return placeholder;
|
||||
}
|
||||
return value;
|
||||
}, 2);
|
||||
json = json.replace(new RegExp('"' + placeholder + '"', 'g'), function () {
|
||||
return fns.shift();
|
||||
});
|
||||
return json;
|
||||
};
|
||||
|
||||
module.exports = function (gulp, plugins, config, env) {
|
||||
return function html() {
|
||||
return gulp.src(env.production() ? config.build + '/*.html' : 'html/*.html')
|
||||
.pipe(plugins.inject(gulp.src(['config.json']), {
|
||||
.pipe(plugins.realFavicon.injectFaviconMarkups(JSON.parse(fs.readFileSync(config.faviconData)).favicon.html_code))
|
||||
.pipe(env.production(plugins.inlineSource({ compress: false })))
|
||||
.pipe(plugins.inject(gulp.src(['config.js']), {
|
||||
removeTags: true,
|
||||
starttag: '<!-- inject:config -->',
|
||||
transform: function (filePath, customConfig) {
|
||||
var defaultConfig = fs.readFileSync('config.default.json', 'utf8');
|
||||
var buildConfig = Object.assign(
|
||||
JSON.parse(JSON.minify(defaultConfig)),
|
||||
JSON.parse(JSON.minify(customConfig.contents.toString('utf8')))
|
||||
);
|
||||
return '<script>var jsonData =' +
|
||||
JSON.stringify(buildConfig)
|
||||
transform: function () {
|
||||
delete require.cache[require.resolve('../../config.default')];
|
||||
delete require.cache[require.resolve('../../config')];
|
||||
var buildConfig = Object.assign({}, require('../../config.default')(), require('../../config')());
|
||||
return '<title>' + buildConfig.siteName + ' - loading...</title>' +
|
||||
'<script>window.config =' +
|
||||
stringify(buildConfig)
|
||||
.replace('<!-- inject:cache-breaker -->',
|
||||
Math.random().toString(12).substring(7)) +
|
||||
';</script>'
|
||||
;
|
||||
';</script>';
|
||||
}
|
||||
}))
|
||||
.pipe(plugins.inject(gulp.src(['config.js']), {
|
||||
removeTags: true,
|
||||
starttag: '<!-- inject:title -->',
|
||||
transform: function () {
|
||||
delete require.cache[require.resolve('../../config.default')];
|
||||
delete require.cache[require.resolve('../../config')];
|
||||
var buildConfig = Object.assign({}, require('../../config.default')(), require('../../config')());
|
||||
return buildConfig.siteName;
|
||||
}
|
||||
}))
|
||||
.pipe(env.production(plugins.kyhInlineSource({ compress: false })))
|
||||
.pipe(plugins.realFavicon.injectFaviconMarkups(JSON.parse(fs.readFileSync(config.faviconData)).favicon.html_code))
|
||||
.pipe(plugins.cacheBust({
|
||||
type: 'timestamp'
|
||||
}))
|
||||
|
@ -3,7 +3,10 @@ module.exports = function (gulp, plugins, config, env) {
|
||||
return gulp.src('app.js')
|
||||
.pipe(env.development(plugins.sourcemaps.init()))
|
||||
.pipe(plugins.requirejsOptimize(env.production() ? config.requireJs.prod : config.requireJs.dev))
|
||||
.pipe(env.production(plugins.uglify({ preserveComments: 'license' })))
|
||||
.on('error', function () {
|
||||
this.emit('end');
|
||||
})
|
||||
.pipe(env.production(plugins.uglify({ output: { comments: 'all' } })))
|
||||
.pipe(env.development(plugins.sourcemaps.write('.', { addComment: true })))
|
||||
.pipe(gulp.dest(config.build));
|
||||
};
|
||||
|
@ -6,6 +6,9 @@ module.exports = function (gulp, plugins, config, env) {
|
||||
outputStyle: 'compressed',
|
||||
sourceMap: false
|
||||
}))
|
||||
.on('error', function () {
|
||||
this.emit('end');
|
||||
})
|
||||
.pipe(plugins.autoprefixer({
|
||||
browsers: config.autoprefixer
|
||||
}))
|
||||
|
@ -1,22 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<html itemscope itemtype="http://schema.org/WebPage">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<!--<meta name="image" content="https://regensburg.freifunk.net/meshviewer/apple-touch-icon.png">-->
|
||||
|
||||
<meta itemprop="name" content="<!-- inject:title --><!-- endinject --> Meshviewer">
|
||||
<meta name="description" itemprop="description" content="<!-- inject:title --><!-- endinject --> Knotenkarte - Zeigt alle Knoten, Statistiken und Verbindungen auf Karte oder Topologie">
|
||||
<!--Uncomment & adjust local urls-->
|
||||
<!--<meta itemprop="image" content="https://regensburg.freifunk.net/meshviewer/android-chrome-512x512.png">-->
|
||||
|
||||
<!--<meta property="business:contact_data:locality" content="Regensburg">-->
|
||||
<!--<meta property="business:contact_data:region" content="Bayern">-->
|
||||
<meta property="business:contact_data:country_name" content="Germany">
|
||||
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta name="twitter:site" content="@freifunk">
|
||||
|
||||
<meta name="og:title" content="<!-- inject:title --><!-- endinject -->">
|
||||
<meta name="og:description" content="<!-- inject:title --><!-- endinject --> Knotenkarte - Zeigt alle Knoten, Statistiken und Verbindungen auf Karte oder Topologie">
|
||||
<!--<meta name="og:image" content="https://regensburg.freifunk.net/meshviewer/android-chrome-512x512.png">-->
|
||||
<!--<meta name="og:url" content="https://regensburg.freifunk.net/meshviewer/">-->
|
||||
<meta name="og:site_name" content="<!-- inject:title --><!-- endinject -->">
|
||||
<meta name="og:type" content="website">
|
||||
|
||||
<link rel="stylesheet" href="main.css" inline>
|
||||
<link rel="stylesheet" class="css-mode night" media="not" href="night.css" inline>
|
||||
<!-- inject:config -->
|
||||
<!-- contents of html partials will be injected here -->
|
||||
<!-- endinject -->
|
||||
<script src="vendor/polyfill.js" inline></script>
|
||||
<script src="vendor/promise.js" inline></script>
|
||||
<script src="vendor/promise/polyfill.js" inline></script>
|
||||
<script src="app.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="loader">
|
||||
<p>
|
||||
Lade<br />
|
||||
<img inline src="logo.svg" class="spinner" />
|
||||
<img inline src="logo.svg" class="spinner" alt="Loading ..."/>
|
||||
<br />
|
||||
Karten & Knoten...
|
||||
</p>
|
||||
|
23
html/offline.html
Normal file
@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title><!-- inject:title --><!-- endinject --></title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<link rel="stylesheet" href="main.css" inline>
|
||||
</head>
|
||||
<body>
|
||||
<div class="loader">
|
||||
<p>
|
||||
You are Offline!<br />
|
||||
<img inline src="logo.svg" class="spinner" alt="Loading ..."/>
|
||||
<br />
|
||||
No connection available.
|
||||
<br /><br /><button onclick="location.reload(true)" class="btn text" aria-label="Try to reload">Try to reload</button><br />
|
||||
</p>
|
||||
<noscript>
|
||||
<strong>JavaScript required</strong>
|
||||
</noscript>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
13
lib/about.js
@ -4,7 +4,18 @@ define(function () {
|
||||
return function () {
|
||||
this.render = function render(d) {
|
||||
d.innerHTML = _.t('sidebar.aboutInfo') +
|
||||
|
||||
'<h4>' + _.t('node.nodes') + '</h4>' +
|
||||
'<p class="legend">' +
|
||||
'<span class="legend-new"><span class="symbol"></span> ' + _.t('sidebar.nodeNew') + '</span>' +
|
||||
'<span class="legend-online"><span class="symbol"></span> ' + _.t('sidebar.nodeOnline') + '</span>' +
|
||||
'<span class="legend-offline"><span class="symbol"></span> ' + _.t('sidebar.nodeOffline') + '</span>' +
|
||||
'</p>' +
|
||||
'<h4>' + _.t('node.clients') + '</h4>' +
|
||||
'<p class="legend">' +
|
||||
'<span class="legend-24ghz"><span class="symbol"></span> 2.4 GHz</span>' +
|
||||
'<span class="legend-5ghz"><span class="symbol"></span> 5 GHz</span>' +
|
||||
'<span class="legend-others"><span class="symbol"></span> ' + _.t('others') + '</span>' +
|
||||
'</p>' +
|
||||
'<h3>AGPL 3</h3>' +
|
||||
|
||||
'<p>Copyright (C) Milan Pässler</p>' +
|
||||
|
@ -97,8 +97,7 @@ define(['filters/nodefilter'], function (NodeFilter) {
|
||||
setData: setData,
|
||||
addFilter: addFilter,
|
||||
removeFilter: removeFilter,
|
||||
watchFilters: watchFilters,
|
||||
refresh: refresh
|
||||
watchFilters: watchFilters
|
||||
};
|
||||
};
|
||||
});
|
||||
|
@ -22,6 +22,7 @@ define(function () {
|
||||
|
||||
var button = document.createElement('button');
|
||||
button.classList.add('ion-close');
|
||||
button.setAttribute('aria-label', _.t('remove'));
|
||||
button.onclick = function onclick() {
|
||||
distributor.removeFilter(d);
|
||||
};
|
||||
|
@ -16,7 +16,7 @@ define(function () {
|
||||
}
|
||||
|
||||
function run(d) {
|
||||
return (d.nodeinfo !== undefined ? d.nodeinfo.hostname.toLowerCase().includes(input.value.toLowerCase()) : '');
|
||||
return d.hostname.toLowerCase().includes(input.value.toLowerCase());
|
||||
}
|
||||
|
||||
function setRefresh(f) {
|
||||
@ -26,6 +26,7 @@ define(function () {
|
||||
function render(el) {
|
||||
input.type = 'search';
|
||||
input.placeholder = _.t('sidebar.nodeFilter');
|
||||
input.setAttribute('aria-label', _.t('sidebar.nodeFilter'));
|
||||
input.addEventListener('input', refresh);
|
||||
el.classList.add('filter-node');
|
||||
el.classList.add('ion-filter');
|
||||
|
@ -12,26 +12,8 @@ define(function () {
|
||||
}
|
||||
}
|
||||
|
||||
var filteredIds = new Set();
|
||||
|
||||
n.graph = {};
|
||||
n.graph.nodes = data.graph.nodes.filter(function (d) {
|
||||
var r;
|
||||
if (d.node) {
|
||||
r = filter(d.node);
|
||||
} else {
|
||||
r = filter({});
|
||||
}
|
||||
|
||||
if (r) {
|
||||
filteredIds.add(d.id);
|
||||
}
|
||||
|
||||
return r;
|
||||
});
|
||||
|
||||
n.graph.links = data.graph.links.filter(function (d) {
|
||||
return filteredIds.has(d.source.id) && filteredIds.has(d.target.id);
|
||||
n.links = data.links.filter(function (d) {
|
||||
return filter(d.source) && filter(d.target);
|
||||
});
|
||||
|
||||
return n;
|
||||
|
@ -2,60 +2,62 @@ define(['d3-selection', 'd3-force', 'd3-zoom', 'd3-drag', 'd3-timer', 'd3-ease',
|
||||
function (d3Selection, d3Force, d3Zoom, d3Drag, d3Timer, d3Ease, d3Interpolate, math, draw) {
|
||||
'use strict';
|
||||
|
||||
return function (config, linkScale, sidebar, router) {
|
||||
return function (linkScale, sidebar) {
|
||||
var self = this;
|
||||
var el;
|
||||
var canvas;
|
||||
var ctx;
|
||||
var force;
|
||||
var forceLink;
|
||||
var animate = false;
|
||||
|
||||
var transform = d3Zoom.zoomIdentity;
|
||||
var intNodes = [];
|
||||
var dictNodes = {};
|
||||
var intLinks = [];
|
||||
var movetoTimer;
|
||||
var initial = 1.8;
|
||||
|
||||
var NODE_RADIUS_DRAG = 10;
|
||||
var NODE_RADIUS_SELECT = 15;
|
||||
var LINK_RADIUS_SELECT = 12;
|
||||
var ZOOM_ANIMATE_DURATION = 500;
|
||||
var ZOOM_ANIMATE_DURATION = 350;
|
||||
|
||||
var ZOOM_MIN = 1 / 8;
|
||||
var ZOOM_MAX = 3;
|
||||
|
||||
var FORCE_ALPHA = 0.3;
|
||||
|
||||
var FORCE_ALPHA = 0.01;
|
||||
|
||||
draw.setTransform(transform);
|
||||
|
||||
function sleep(time) {
|
||||
return new Promise(function (resolve) {
|
||||
setTimeout(resolve, time);
|
||||
});
|
||||
}
|
||||
|
||||
function resizeCanvas() {
|
||||
canvas.width = el.offsetWidth;
|
||||
canvas.height = el.offsetHeight;
|
||||
draw.setMaxArea(canvas.width, canvas.height);
|
||||
}
|
||||
|
||||
function moveTo(x, y, k) {
|
||||
function transformPosition(p) {
|
||||
transform.x = p.x;
|
||||
transform.y = p.y;
|
||||
transform.k = p.k;
|
||||
}
|
||||
|
||||
var end = {k: k};
|
||||
end.x = (canvas.width + sidebar()) / 2 - x * k;
|
||||
function moveTo(callback, forceMove) {
|
||||
clearTimeout(movetoTimer);
|
||||
if (!forceMove && force.alpha() > 0.3) {
|
||||
movetoTimer = setTimeout(function timerOfMoveTo() {
|
||||
moveTo(callback);
|
||||
}, 300);
|
||||
return;
|
||||
}
|
||||
var result = callback();
|
||||
var x = result[0];
|
||||
var y = result[1];
|
||||
var k = result[2];
|
||||
var end = { k: k };
|
||||
|
||||
end.x = (canvas.width + sidebar.getWidth()) / 2 - x * k;
|
||||
end.y = canvas.height / 2 - y * k;
|
||||
|
||||
if (!animate) {
|
||||
transformPosition(end);
|
||||
redraw();
|
||||
} else {
|
||||
var start = { x: transform.x, y: transform.y, k: transform.k };
|
||||
|
||||
var interpolate = d3Interpolate.interpolateObject(start, end);
|
||||
@ -71,7 +73,6 @@ define(['d3-selection', 'd3-force', 'd3-zoom', 'd3-drag', 'd3-timer', 'd3-ease',
|
||||
window.requestAnimationFrame(redraw);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function onClick() {
|
||||
if (d3Selection.event.defaultPrevented) {
|
||||
@ -82,7 +83,7 @@ define(['d3-selection', 'd3-force', 'd3-zoom', 'd3-drag', 'd3-timer', 'd3-ease',
|
||||
var n = force.find(e[0], e[1], NODE_RADIUS_SELECT);
|
||||
|
||||
if (n !== undefined) {
|
||||
router.fullUrl({ node: n.o.node.nodeinfo.node_id });
|
||||
router.fullUrl({ node: n.o.node_id });
|
||||
return;
|
||||
}
|
||||
|
||||
@ -121,16 +122,16 @@ define(['d3-selection', 'd3-force', 'd3-zoom', 'd3-drag', 'd3-timer', 'd3-ease',
|
||||
|
||||
forceLink = d3Force.forceLink()
|
||||
.distance(function (d) {
|
||||
if (d.o.vpn) {
|
||||
if (d.o.type.indexOf('vpn') === 0) {
|
||||
return 0;
|
||||
}
|
||||
return 75;
|
||||
})
|
||||
.strength(function (d) {
|
||||
if (d.o.vpn) {
|
||||
if (d.o.type.indexOf('vpn') === 0) {
|
||||
return 0.02;
|
||||
}
|
||||
return Math.max(0.5, 1 / d.o.tq);
|
||||
return Math.max(0.5, d.o.source_tq);
|
||||
});
|
||||
|
||||
var zoom = d3Zoom.zoom()
|
||||
@ -148,8 +149,7 @@ define(['d3-selection', 'd3-force', 'd3-zoom', 'd3-drag', 'd3-timer', 'd3-ease',
|
||||
.force('y', d3Force.forceY().strength(0.02))
|
||||
.force('collide', d3Force.forceCollide())
|
||||
.on('tick', redraw)
|
||||
.alphaDecay(0.01)
|
||||
.velocityDecay(0.1);
|
||||
.alphaDecay(0.025);
|
||||
|
||||
var drag = d3Drag.drag()
|
||||
.subject(function () {
|
||||
@ -198,13 +198,11 @@ define(['d3-selection', 'd3-force', 'd3-zoom', 'd3-drag', 'd3-timer', 'd3-ease',
|
||||
});
|
||||
|
||||
self.setData = function setData(data) {
|
||||
intNodes = data.graph.nodes.map(function (d) {
|
||||
var e;
|
||||
if (d.id in dictNodes) {
|
||||
e = dictNodes[d.id];
|
||||
} else {
|
||||
intNodes = data.nodes.all.map(function (d) {
|
||||
var e = dictNodes[d.node_id];
|
||||
if (!e) {
|
||||
e = {};
|
||||
dictNodes[d.id] = e;
|
||||
dictNodes[d.node_id] = e;
|
||||
}
|
||||
|
||||
e.o = d;
|
||||
@ -212,79 +210,67 @@ define(['d3-selection', 'd3-force', 'd3-zoom', 'd3-drag', 'd3-timer', 'd3-ease',
|
||||
return e;
|
||||
});
|
||||
|
||||
intLinks = data.graph.links.map(function (d) {
|
||||
var e = {};
|
||||
e.o = d;
|
||||
e.source = dictNodes[d.source.id];
|
||||
e.target = dictNodes[d.target.id];
|
||||
e.color = linkScale(1 / d.tq);
|
||||
|
||||
return e;
|
||||
intLinks = data.links.filter(function (d) {
|
||||
return data.nodeDict[d.source.node_id].is_online && data.nodeDict[d.target.node_id].is_online;
|
||||
}).map(function (d) {
|
||||
return {
|
||||
o: d,
|
||||
source: dictNodes[d.source.node_id],
|
||||
target: dictNodes[d.target.node_id],
|
||||
color: linkScale(d.source_tq),
|
||||
color_to: linkScale(d.target_tq)
|
||||
};
|
||||
});
|
||||
|
||||
force.nodes(intNodes);
|
||||
forceLink.links(intLinks);
|
||||
|
||||
force.alpha(1).restart();
|
||||
force.alphaTarget(1);
|
||||
force.alpha(initial).velocityDecay(0.15).restart();
|
||||
if (initial === 1.8) {
|
||||
initial = 0.5;
|
||||
}
|
||||
|
||||
resizeCanvas();
|
||||
sleep(1000).then(function () {
|
||||
force.alphaTarget(0);
|
||||
animate = true;
|
||||
});
|
||||
};
|
||||
|
||||
self.resetView = function resetView() {
|
||||
moveTo(function calcToReset() {
|
||||
draw.setHighlight(null);
|
||||
moveTo(0, 0, (ZOOM_MIN + 1) / 2);
|
||||
return [0, 0, (ZOOM_MIN + config.forceGraph.zoomModifier) / 2];
|
||||
}, true);
|
||||
};
|
||||
|
||||
self.gotoNode = function gotoNode(d) {
|
||||
function getNode() {
|
||||
for (var i = 0; i < intNodes.length; i++) {
|
||||
var n = intNodes[i];
|
||||
if (n.o.node.nodeinfo.node_id !== d.nodeinfo.node_id) {
|
||||
continue;
|
||||
}
|
||||
draw.setHighlight({ type: 'node', o: n.o.node });
|
||||
moveTo(n.x, n.y, (ZOOM_MAX + 1) / 2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!animate) {
|
||||
sleep(1500).then(getNode);
|
||||
} else {
|
||||
getNode();
|
||||
moveTo(function calcToNode() {
|
||||
draw.setHighlight({ type: 'node', id: d.node_id });
|
||||
var n = dictNodes[d.node_id];
|
||||
if (n) {
|
||||
return [n.x, n.y, (ZOOM_MAX + 1) / 2];
|
||||
}
|
||||
return self.resetView();
|
||||
});
|
||||
};
|
||||
|
||||
self.gotoLink = function gotoLink(d) {
|
||||
function getLink() {
|
||||
draw.setHighlight({ type: 'link', o: d });
|
||||
for (var i = 0; i < intLinks.length; i++) {
|
||||
var l = intLinks[i];
|
||||
if (l.o !== d) {
|
||||
continue;
|
||||
}
|
||||
moveTo((l.source.x + l.target.x) / 2, (l.source.y + l.target.y) / 2, transform.k);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!animate) {
|
||||
sleep(1500).then(getLink);
|
||||
} else {
|
||||
getLink();
|
||||
moveTo(function calcToLink() {
|
||||
draw.setHighlight({ type: 'link', id: d[0].id });
|
||||
var l = intLinks.find(function (link) {
|
||||
return link.o.id === d[0].id;
|
||||
});
|
||||
if (l) {
|
||||
return [(l.source.x + l.target.x) / 2, (l.source.y + l.target.y) / 2, (ZOOM_MAX / 2) + ZOOM_MIN];
|
||||
}
|
||||
return self.resetView();
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
self.gotoLocation = function gotoLocation() {
|
||||
// ignore
|
||||
};
|
||||
|
||||
self.destroy = function destroy() {
|
||||
force.stop();
|
||||
canvas.remove();
|
||||
canvas.parentNode.removeChild(canvas);
|
||||
force = null;
|
||||
|
||||
if (el.parentNode) {
|
||||
|
@ -4,50 +4,39 @@ define(['helper'], function (helper) {
|
||||
var ctx;
|
||||
var width;
|
||||
var height;
|
||||
|
||||
var transform;
|
||||
|
||||
var highlight;
|
||||
|
||||
var nodeColor = '#fff';
|
||||
var clientColor = '#e6324b';
|
||||
var highlightColor = 'rgba(255, 255, 255, 0.2)';
|
||||
|
||||
var labelColor = '#fff';
|
||||
|
||||
var NODE_RADIUS = 15;
|
||||
var LINE_RADIUS = 12;
|
||||
|
||||
function drawDetailNode(d) {
|
||||
if (transform.k > 1) {
|
||||
ctx.beginPath();
|
||||
helper.positionClients(ctx, d, Math.PI, d.o.node.statistics.clients, 15);
|
||||
ctx.fillStyle = clientColor;
|
||||
ctx.fill();
|
||||
if (transform.k > 1 && d.o.is_online) {
|
||||
helper.positionClients(ctx, d, Math.PI, d.o, 15);
|
||||
ctx.beginPath();
|
||||
var name = d.o.node_id;
|
||||
if (d.o.node && d.o.node.nodeinfo) {
|
||||
name = d.o.node.nodeinfo.hostname;
|
||||
if (d.o) {
|
||||
name = d.o.hostname;
|
||||
}
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillStyle = labelColor;
|
||||
ctx.fillStyle = config.forceGraph.labelColor;
|
||||
ctx.fillText(name, d.x, d.y + 20);
|
||||
}
|
||||
}
|
||||
|
||||
function drawHighlightNode(d) {
|
||||
if (highlight && highlight.type === 'node' && d.o.node === highlight.o) {
|
||||
if (highlight && highlight.type === 'node' && d.o.node_id === highlight.id) {
|
||||
ctx.arc(d.x, d.y, NODE_RADIUS * 1.5, 0, 2 * Math.PI);
|
||||
ctx.fillStyle = highlightColor;
|
||||
ctx.fillStyle = config.forceGraph.highlightColor;
|
||||
ctx.fill();
|
||||
ctx.beginPath();
|
||||
}
|
||||
}
|
||||
|
||||
function drawHighlightLink(d, to) {
|
||||
if (highlight && highlight.type === 'link' && d.o === highlight.o) {
|
||||
if (highlight && highlight.type === 'link' && d.o.id === highlight.id) {
|
||||
ctx.lineTo(to[0], to[1]);
|
||||
ctx.strokeStyle = highlightColor;
|
||||
ctx.strokeStyle = config.forceGraph.highlightColor;
|
||||
ctx.lineWidth = LINE_RADIUS * 2;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.stroke();
|
||||
@ -64,9 +53,17 @@ define(['helper'], function (helper) {
|
||||
|
||||
drawHighlightNode(d);
|
||||
|
||||
ctx.moveTo(d.x + 3, d.y);
|
||||
if (d.o.is_online) {
|
||||
ctx.arc(d.x, d.y, 8, 0, 2 * Math.PI);
|
||||
ctx.fillStyle = nodeColor;
|
||||
if (d.o.is_gateway) {
|
||||
ctx.rect(d.x - 9, d.y - 9, 18, 18);
|
||||
}
|
||||
ctx.fillStyle = config.forceGraph.nodeColor;
|
||||
} else {
|
||||
ctx.arc(d.x, d.y, 6, 0, 2 * Math.PI);
|
||||
ctx.fillStyle = config.forceGraph.nodeOfflineColor;
|
||||
}
|
||||
|
||||
ctx.fill();
|
||||
|
||||
drawDetailNode(d);
|
||||
@ -85,9 +82,13 @@ define(['helper'], function (helper) {
|
||||
|
||||
to = drawHighlightLink(d, to);
|
||||
|
||||
var grd = ctx.createLinearGradient(d.source.x, d.source.y, d.target.x, d.target.y);
|
||||
grd.addColorStop(0.45, d.color);
|
||||
grd.addColorStop(0.55, d.color_to);
|
||||
|
||||
ctx.lineTo(to[0], to[1]);
|
||||
ctx.strokeStyle = d.color;
|
||||
if (d.o.vpn) {
|
||||
ctx.strokeStyle = grd;
|
||||
if (d.o.type.indexOf('vpn') === 0) {
|
||||
ctx.globalAlpha = 0.2;
|
||||
ctx.lineWidth = 1.5;
|
||||
} else {
|
||||
|
46
lib/gui.js
@ -1,18 +1,18 @@
|
||||
define(['d3-interpolate', 'map', 'sidebar', 'tabs', 'container', 'legend',
|
||||
'linklist', 'nodelist', 'simplenodelist', 'infobox/main',
|
||||
'proportions', 'forcegraph', 'title', 'about', 'datadistributor',
|
||||
'filters/filtergui', 'filters/hostname'],
|
||||
function (d3Interpolate, Map, Sidebar, Tabs, Container, Legend, Linklist,
|
||||
'filters/filtergui', 'filters/hostname', 'helper'],
|
||||
function (d3Interpolate, Map, Sidebar, Tabs, Container, Legend, Linklist,
|
||||
Nodelist, SimpleNodelist, Infobox, Proportions, ForceGraph,
|
||||
Title, About, DataDistributor, FilterGUI, HostnameFilter) {
|
||||
Title, About, DataDistributor, FilterGUI, HostnameFilter, helper) {
|
||||
'use strict';
|
||||
|
||||
return function (config, router, language) {
|
||||
return function (language) {
|
||||
var self = this;
|
||||
var content;
|
||||
var contentDiv;
|
||||
|
||||
var linkScale = d3Interpolate.interpolate('#F02311', '#04C714');
|
||||
var linkScale = d3Interpolate.interpolate(config.map.tqFrom, config.map.tqTo);
|
||||
var sidebar;
|
||||
|
||||
var buttons = document.createElement('div');
|
||||
@ -38,7 +38,7 @@ define(['d3-interpolate', 'map', 'sidebar', 'tabs', 'container', 'legend',
|
||||
function addContent(K) {
|
||||
removeContent();
|
||||
|
||||
content = new K(config, linkScale, sidebar.getWidth, router, buttons);
|
||||
content = new K(linkScale, sidebar, buttons);
|
||||
content.render(contentDiv);
|
||||
|
||||
fanout.add(content);
|
||||
@ -63,8 +63,8 @@ define(['d3-interpolate', 'map', 'sidebar', 'tabs', 'container', 'legend',
|
||||
contentDiv.appendChild(buttons);
|
||||
|
||||
var buttonToggle = document.createElement('button');
|
||||
buttonToggle.classList.add('ion-eye', 'shadow');
|
||||
buttonToggle.setAttribute('data-tooltip', _.t('button.switchView'));
|
||||
buttonToggle.classList.add('ion-eye');
|
||||
buttonToggle.setAttribute('aria-label', _.t('button.switchView'));
|
||||
buttonToggle.onclick = function onclick() {
|
||||
var data;
|
||||
if (content.constructor === Map) {
|
||||
@ -77,23 +77,35 @@ define(['d3-interpolate', 'map', 'sidebar', 'tabs', 'container', 'legend',
|
||||
|
||||
buttons.appendChild(buttonToggle);
|
||||
|
||||
var title = new Title(config);
|
||||
if (config.fullscreen || config.fullscreenFrame && window.frameElement) {
|
||||
var buttonFullscreen = document.createElement('button');
|
||||
buttonFullscreen.classList.add('ion-full-enter');
|
||||
buttonFullscreen.setAttribute('aria-label', _.t('button.fullscreen'));
|
||||
buttonFullscreen.onclick = function onclick() {
|
||||
helper.fullscreen(buttonFullscreen);
|
||||
};
|
||||
|
||||
buttons.appendChild(buttonFullscreen);
|
||||
}
|
||||
|
||||
var title = new Title();
|
||||
|
||||
var header = new Container('header');
|
||||
var infobox = new Infobox(config, sidebar, router);
|
||||
var infobox = new Infobox(sidebar, linkScale);
|
||||
var tabs = new Tabs();
|
||||
var overview = new Container();
|
||||
var legend = new Legend(config, language);
|
||||
var newnodeslist = new SimpleNodelist('new', 'firstseen', router, _.t('node.new'));
|
||||
var lostnodeslist = new SimpleNodelist('lost', 'lastseen', router, _.t('node.missing'));
|
||||
var nodelist = new Nodelist(router);
|
||||
var linklist = new Linklist(linkScale, router);
|
||||
var statistics = new Proportions(config, fanout);
|
||||
var legend = new Legend(language);
|
||||
var newnodeslist = new SimpleNodelist('new', 'firstseen', _.t('node.new'));
|
||||
var lostnodeslist = new SimpleNodelist('lost', 'lastseen', _.t('node.missing'));
|
||||
var nodelist = new Nodelist();
|
||||
var linklist = new Linklist(linkScale);
|
||||
var statistics = new Proportions(fanout);
|
||||
var about = new About();
|
||||
|
||||
fanoutUnfiltered.add(legend);
|
||||
fanoutUnfiltered.add(newnodeslist);
|
||||
fanoutUnfiltered.add(lostnodeslist);
|
||||
fanoutUnfiltered.add(infobox);
|
||||
fanout.add(nodelist);
|
||||
fanout.add(linklist);
|
||||
fanout.add(statistics);
|
||||
@ -128,4 +140,4 @@ define(['d3-interpolate', 'map', 'sidebar', 'tabs', 'container', 'legend',
|
||||
|
||||
return self;
|
||||
};
|
||||
});
|
||||
});
|
||||
|
@ -1,53 +1,89 @@
|
||||
define(['helper'], function (helper) {
|
||||
define(['helper', 'snabbdom'], function (helper, V) {
|
||||
'use strict';
|
||||
V = V.default;
|
||||
|
||||
function showStatImg(o, d, time) {
|
||||
var subst = {};
|
||||
subst['{SOURCE_ID}'] = d.source.node_id;
|
||||
subst['{SOURCE_NAME}'] = d.source.node.nodeinfo.hostname.replace(/[^a-z0-9\-]/ig, '_');
|
||||
subst['{TARGET_ID}'] = d.target.node_id;
|
||||
subst['{TARGET_NAME}'] = d.target.node.nodeinfo.hostname.replace(/[^a-z0-9\-]/ig, '_');
|
||||
subst['{TIME}'] = time;
|
||||
subst['{LOCALE}'] = _.locale();
|
||||
return helper.showStat(o, subst);
|
||||
function showStatImg(img, o, d, time) {
|
||||
var subst = {
|
||||
'{SOURCE_ID}': d.source.node_id,
|
||||
'{SOURCE_NAME}': d.source.hostname.replace(/[^a-z0-9\-]/ig, '_'),
|
||||
'{SOURCE_ADDR}': d.source_addr,
|
||||
'{SOURCE_MAC}': d.source_mac ? d.source_mac : d.source_addr,
|
||||
'{TARGET_ID}': d.target.node_id,
|
||||
'{TARGET_NAME}': d.target.hostname.replace(/[^a-z0-9\-]/ig, '_'),
|
||||
'{TARGET_ADDR}': d.target_addr,
|
||||
'{TARGET_MAC}': d.target_mac ? d.target_mac : d.target_addr,
|
||||
'{TYPE}': d.type,
|
||||
'{TIME}': time,
|
||||
'{LOCALE}': _.locale()
|
||||
};
|
||||
|
||||
img.push(V.h('h4', helper.listReplace(o.name, subst)));
|
||||
img.push(helper.showStat(V, o, subst));
|
||||
}
|
||||
|
||||
return function (config, el, router, d) {
|
||||
var h2 = document.createElement('h2');
|
||||
var a1 = document.createElement('a');
|
||||
a1.href = router.generateLink({ node: d.source.node_id });
|
||||
a1.textContent = d.source.node.nodeinfo.hostname;
|
||||
h2.appendChild(a1);
|
||||
return function (el, d, linkScale) {
|
||||
var self = this;
|
||||
var header = document.createElement('div');
|
||||
var table = document.createElement('table');
|
||||
var images = document.createElement('div');
|
||||
el.appendChild(header);
|
||||
el.appendChild(table);
|
||||
el.appendChild(images);
|
||||
|
||||
var arrow = document.createElement('span');
|
||||
arrow.classList.add('ion-arrow-right-c');
|
||||
h2.appendChild(arrow);
|
||||
self.render = function render() {
|
||||
var children = [];
|
||||
var img = [];
|
||||
var time = d[0].target.lastseen.format('DDMMYYYYHmmss');
|
||||
|
||||
var a2 = document.createElement('a');
|
||||
a2.href = router.generateLink({ node: d.target.node_id });
|
||||
a2.textContent = d.target.node.nodeinfo.hostname;
|
||||
h2.appendChild(a2);
|
||||
el.appendChild(h2);
|
||||
header = V.patch(header, V.h('div', V.h('h2', [
|
||||
V.h('a', {
|
||||
props: { href: router.generateLink({ node: d[0].source.node_id }) }
|
||||
}, d[0].source.hostname),
|
||||
V.h('span', ' - '),
|
||||
V.h('a', {
|
||||
props: { href: router.generateLink({ node: d[0].target.node_id }) }
|
||||
}, d[0].target.hostname)
|
||||
])));
|
||||
|
||||
var attributes = document.createElement('table');
|
||||
attributes.classList.add('attributes');
|
||||
helper.attributeEntry(V, children, 'node.hardware', (d[0].source.model ? d[0].source.model + ' – ' : '') +
|
||||
(d[0].target.model ? d[0].target.model : ''));
|
||||
helper.attributeEntry(V, children, 'node.distance', helper.showDistance(d[0]));
|
||||
|
||||
helper.attributeEntry(attributes, 'node.tq', helper.showTq(d));
|
||||
helper.attributeEntry(attributes, 'node.distance', helper.showDistance(d));
|
||||
var hw1 = helper.dictGet(d.source.node.nodeinfo, ['hardware', 'model']);
|
||||
var hw2 = helper.dictGet(d.target.node.nodeinfo, ['hardware', 'model']);
|
||||
helper.attributeEntry(attributes, 'node.hardware', hw1 + ' – ' + hw2);
|
||||
d.forEach(function (link) {
|
||||
children.push(V.h('tr', { props: { className: 'header' } }, [
|
||||
V.h('th', _.t('node.connectionType')),
|
||||
V.h('th', link.type)
|
||||
]));
|
||||
helper.attributeEntry(V, children, 'node.tq', V.h('span',
|
||||
{ style: { color: linkScale((link.source_tq + link.target_tq) / 2) } },
|
||||
helper.showTq(link.source_tq) + ' - ' + helper.showTq(link.target_tq))
|
||||
);
|
||||
|
||||
el.appendChild(attributes);
|
||||
|
||||
if (config.linkInfos) {
|
||||
var time = d.target.node.lastseen.format('DDMMYYYYHmmss');
|
||||
config.linkInfos.forEach(function (linkInfo) {
|
||||
var h4 = document.createElement('h4');
|
||||
h4.textContent = linkInfo.name;
|
||||
el.appendChild(h4);
|
||||
el.appendChild(showStatImg(linkInfo, d, time));
|
||||
if (config.linkTypeInfos) {
|
||||
config.linkTypeInfos.forEach(function (o) {
|
||||
showStatImg(img, o, link, time);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (config.linkInfos) {
|
||||
config.linkInfos.forEach(function (o) {
|
||||
showStatImg(img, o, d[0], time);
|
||||
});
|
||||
}
|
||||
|
||||
var elNew = V.h('table', children);
|
||||
table = V.patch(table, elNew);
|
||||
table.elm.classList.add('attributes');
|
||||
images = V.patch(images, V.h('div', img));
|
||||
};
|
||||
|
||||
self.setData = function setData(data) {
|
||||
d = data.links.filter(function (a) {
|
||||
return a.id === d[0].id;
|
||||
});
|
||||
self.render();
|
||||
};
|
||||
return self;
|
||||
};
|
||||
});
|
||||
|
@ -1,7 +1,7 @@
|
||||
define(['helper'], function (helper) {
|
||||
'use strict';
|
||||
|
||||
return function (config, el, router, d) {
|
||||
return function (el, d) {
|
||||
var sidebarTitle = document.createElement('h2');
|
||||
sidebarTitle.textContent = _.t('location.location');
|
||||
el.appendChild(sidebarTitle);
|
||||
@ -14,16 +14,19 @@ define(['helper'], function (helper) {
|
||||
});
|
||||
|
||||
var editLat = document.createElement('input');
|
||||
editLat.setAttribute('aria-label', _.t('location.latitude'));
|
||||
editLat.type = 'text';
|
||||
editLat.value = d.lat.toFixed(9);
|
||||
el.appendChild(createBox('lat', _.t('location.latitude'), editLat));
|
||||
|
||||
var editLng = document.createElement('input');
|
||||
editLng.setAttribute('aria-label', _.t('location.longitude'));
|
||||
editLng.type = 'text';
|
||||
editLng.value = d.lng.toFixed(9);
|
||||
el.appendChild(createBox('lng', _.t('location.longitude'), editLng));
|
||||
|
||||
var editUci = document.createElement('textarea');
|
||||
editUci.setAttribute('aria-label', 'Uci');
|
||||
editUci.value =
|
||||
"uci set gluon-node-info.@location[0]='location'; " +
|
||||
"uci set gluon-node-info.@location[0].share_location='1';" +
|
||||
@ -41,6 +44,7 @@ define(['helper'], function (helper) {
|
||||
var btn = document.createElement('button');
|
||||
btn.classList.add('ion-clipboard');
|
||||
btn.title = _.t('location.copy');
|
||||
btn.setAttribute('aria-label', _.t('location.copy'));
|
||||
btn.onclick = function onclick() {
|
||||
copy2clip(inputElem.id);
|
||||
};
|
||||
|
@ -1,14 +1,16 @@
|
||||
define(['infobox/link', 'infobox/node', 'infobox/location'], function (link, node, location) {
|
||||
define(['infobox/link', 'infobox/node', 'infobox/location'], function (Link, Node, location) {
|
||||
'use strict';
|
||||
|
||||
return function (config, sidebar, router) {
|
||||
return function (sidebar, linkScale) {
|
||||
var self = this;
|
||||
var el;
|
||||
var node;
|
||||
var link;
|
||||
|
||||
function destroy() {
|
||||
if (el && el.parentNode) {
|
||||
el.parentNode.removeChild(el);
|
||||
el = undefined;
|
||||
node = link = el = undefined;
|
||||
sidebar.reveal();
|
||||
}
|
||||
}
|
||||
@ -28,6 +30,7 @@ define(['infobox/link', 'infobox/node', 'infobox/location'], function (link, nod
|
||||
var closeButton = document.createElement('button');
|
||||
closeButton.classList.add('close');
|
||||
closeButton.classList.add('ion-close');
|
||||
closeButton.setAttribute('aria-label', _.t('close'));
|
||||
closeButton.onclick = function () {
|
||||
router.fullUrl();
|
||||
};
|
||||
@ -36,19 +39,30 @@ define(['infobox/link', 'infobox/node', 'infobox/location'], function (link, nod
|
||||
|
||||
self.resetView = destroy;
|
||||
|
||||
self.gotoNode = function gotoNode(d) {
|
||||
self.gotoNode = function gotoNode(d, nodeDict) {
|
||||
create();
|
||||
node(config, el, router, d);
|
||||
node = new Node(el, d, linkScale, nodeDict);
|
||||
node.render();
|
||||
};
|
||||
|
||||
self.gotoLink = function gotoLink(d) {
|
||||
create();
|
||||
link(config, el, router, d);
|
||||
link = new Link(el, d, linkScale);
|
||||
link.render();
|
||||
};
|
||||
|
||||
self.gotoLocation = function gotoLocation(d) {
|
||||
create();
|
||||
location(config, el, router, d);
|
||||
location(el, d);
|
||||
};
|
||||
|
||||
self.setData = function setData(d) {
|
||||
if (typeof node === 'object') {
|
||||
node.setData(d);
|
||||
}
|
||||
if (typeof link === 'object') {
|
||||
link.setData(d);
|
||||
}
|
||||
};
|
||||
|
||||
return self;
|
||||
|
@ -1,272 +1,120 @@
|
||||
define(['sorttable', 'snabbdom', 'd3-interpolate', 'moment', 'helper'],
|
||||
function (SortTable, V, d3Interpolate, moment, helper) {
|
||||
define(['sorttable', 'snabbdom', 'd3-interpolate', 'helper', 'utils/node'],
|
||||
function (SortTable, V, d3Interpolate, helper, nodef) {
|
||||
'use strict';
|
||||
V = V.default;
|
||||
|
||||
function showGeoURI(d) {
|
||||
if (!helper.hasLocation(d)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return function (el) {
|
||||
var a = document.createElement('a');
|
||||
a.textContent = Number(d.nodeinfo.location.latitude.toFixed(6)) + ', ' + Number(d.nodeinfo.location.longitude.toFixed(6));
|
||||
a.href = 'geo:' + d.nodeinfo.location.latitude + ',' + d.nodeinfo.location.longitude;
|
||||
el.appendChild(a);
|
||||
};
|
||||
}
|
||||
|
||||
function showStatus(d) {
|
||||
return function (el) {
|
||||
el.classList.add(d.flags.unseen ? 'unseen' : (d.flags.online ? 'online' : 'offline'));
|
||||
el.textContent = _.t((d.flags.online ? 'node.lastOnline' : 'node.lastOffline'), {
|
||||
time: d.lastseen.fromNow(),
|
||||
date: d.lastseen.format('DD.MM.YYYY, H:mm:ss')
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function showFirmware(d) {
|
||||
var release = helper.dictGet(d.nodeinfo, ['software', 'firmware', 'release']);
|
||||
var base = helper.dictGet(d.nodeinfo, ['software', 'firmware', 'base']);
|
||||
|
||||
if (release === null || base === null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return release + ' / ' + base;
|
||||
}
|
||||
|
||||
function showSite(d, config) {
|
||||
var site = helper.dictGet(d.nodeinfo, ['system', 'site_code']);
|
||||
var rt = site;
|
||||
if (config.siteNames) {
|
||||
config.siteNames.forEach(function (t) {
|
||||
if (site === t.site) {
|
||||
rt = t.name;
|
||||
}
|
||||
});
|
||||
}
|
||||
return rt;
|
||||
}
|
||||
|
||||
function showUptime(d) {
|
||||
if (!('uptime' in d.statistics)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return moment.duration(d.statistics.uptime, 'seconds').humanize();
|
||||
}
|
||||
|
||||
function showFirstseen(d) {
|
||||
if (!('firstseen' in d)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return d.firstseen.fromNow(true);
|
||||
}
|
||||
|
||||
function showClients(d) {
|
||||
if (!d.flags.online) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return function (el) {
|
||||
el.appendChild(document.createTextNode(d.statistics.clients > 0 ? d.statistics.clients : _.t('none')));
|
||||
el.appendChild(document.createElement('br'));
|
||||
|
||||
var span = document.createElement('span');
|
||||
span.classList.add('clients');
|
||||
span.innerHTML = '<i class="ion-person"></i>'.repeat(d.statistics.clients);
|
||||
el.appendChild(span);
|
||||
};
|
||||
}
|
||||
|
||||
function showIPs(d) {
|
||||
var ips = helper.dictGet(d.nodeinfo, ['network', 'addresses']);
|
||||
if (ips === null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
ips.sort();
|
||||
|
||||
return function (el) {
|
||||
ips.forEach(function (ip, i) {
|
||||
var link = !ip.startsWith('fe80:');
|
||||
|
||||
if (i > 0) {
|
||||
el.appendChild(document.createElement('br'));
|
||||
}
|
||||
|
||||
if (link) {
|
||||
var a = document.createElement('a');
|
||||
a.href = 'http://[' + ip + ']/';
|
||||
a.textContent = ip;
|
||||
el.appendChild(a);
|
||||
} else {
|
||||
el.appendChild(document.createTextNode(ip));
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function showBar(v, width, warning) {
|
||||
var span = document.createElement('span');
|
||||
span.classList.add('bar');
|
||||
|
||||
var bar = document.createElement('span');
|
||||
bar.style.width = (width * 100) + '%';
|
||||
if (warning) {
|
||||
span.classList.add('warning');
|
||||
}
|
||||
span.appendChild(bar);
|
||||
|
||||
var label = document.createElement('label');
|
||||
label.textContent = v;
|
||||
span.appendChild(label);
|
||||
|
||||
return span;
|
||||
}
|
||||
|
||||
function showLoad(d) {
|
||||
if (!('loadavg' in d.statistics)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return function (el) {
|
||||
var value = d.statistics.loadavg.toFixed(2);
|
||||
var width = d.statistics.loadavg % 1;
|
||||
var warning = false;
|
||||
if (d.statistics.loadavg >= d.nodeinfo.hardware.nproc) {
|
||||
warning = true;
|
||||
}
|
||||
el.appendChild(showBar(value, width, warning));
|
||||
};
|
||||
}
|
||||
|
||||
function showRAM(d) {
|
||||
if (!('memory_usage' in d.statistics)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return function (el) {
|
||||
var value = Math.round(d.statistics.memory_usage * 100) + ' %';
|
||||
var width = d.statistics.memory_usage;
|
||||
var warning = false;
|
||||
if (d.statistics.memory_usage >= 0.8) {
|
||||
warning = true;
|
||||
}
|
||||
el.appendChild(showBar(value, width, warning));
|
||||
};
|
||||
}
|
||||
|
||||
function showAutoupdate(d) {
|
||||
var au = helper.dictGet(d.nodeinfo, ['software', 'autoupdater']);
|
||||
if (!au) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return au.enabled ? _.t('node.activated', { branch: au.branch }) : _.t('node.deactivated');
|
||||
}
|
||||
|
||||
function showStatImg(o, d) {
|
||||
var subst = {};
|
||||
subst['{NODE_ID}'] = d.nodeinfo.node_id;
|
||||
subst['{NODE_NAME}'] = d.nodeinfo.hostname.replace(/[^a-z0-9\-]/ig, '_');
|
||||
subst['{TIME}'] = d.lastseen.format('DDMMYYYYHmmss');
|
||||
subst['{LOCALE}'] = _.locale();
|
||||
return helper.showStat(o, subst);
|
||||
var subst = {
|
||||
'{NODE_ID}': d.node_id,
|
||||
'{NODE_NAME}': d.hostname.replace(/[^a-z0-9\-]/ig, '_'),
|
||||
'{TIME}': d.lastseen.format('DDMMYYYYHmmss'),
|
||||
'{LOCALE}': _.locale()
|
||||
};
|
||||
return helper.showStat(V, o, subst);
|
||||
}
|
||||
|
||||
return function (config, el, router, d) {
|
||||
var linkScale = d3Interpolate.interpolate('#F02311', '#04C714');
|
||||
|
||||
function renderNeighbourRow(n) {
|
||||
var icons = [];
|
||||
icons.push(V.h('span', { props: { className: n.incoming ? 'ion-arrow-left-c' : 'ion-arrow-right-c' } }));
|
||||
if (helper.hasLocation(n.node)) {
|
||||
icons.push(V.h('span', { props: { className: 'ion-location' } }));
|
||||
}
|
||||
|
||||
var name = V.h('a', {
|
||||
return function (el, d, linkScale, nodeDict) {
|
||||
function nodeLink(node) {
|
||||
return V.h('a', {
|
||||
props: {
|
||||
className: 'online',
|
||||
href: router.generateLink({ node: n.node.nodeinfo.node_id })
|
||||
className: node.is_online ? 'online' : 'offline',
|
||||
href: router.generateLink({ node: node.node_id })
|
||||
}, on: {
|
||||
click: function (e) {
|
||||
router.fullUrl({ node: n.node.nodeinfo.node_id }, e);
|
||||
router.fullUrl({ node: node.node_id }, e);
|
||||
}
|
||||
}
|
||||
}, n.node.nodeinfo.hostname);
|
||||
|
||||
var td1 = V.h('td', icons);
|
||||
var td2 = V.h('td', name);
|
||||
var td3 = V.h('td', (n.node.statistics.clients ? n.node.statistics.clients.toString() : '0'));
|
||||
var td4 = V.h('td', { style: { color: linkScale(1 / n.link.tq) } }, helper.showTq(n.link));
|
||||
var td5 = V.h('td', helper.showDistance(n.link));
|
||||
|
||||
return V.h('tr', [td1, td2, td3, td4, td5]);
|
||||
}, node.hostname);
|
||||
}
|
||||
|
||||
var h2 = document.createElement('h2');
|
||||
h2.textContent = d.nodeinfo.hostname;
|
||||
el.appendChild(h2);
|
||||
|
||||
var attributes = document.createElement('table');
|
||||
attributes.classList.add('attributes');
|
||||
|
||||
helper.attributeEntry(attributes, 'node.status', showStatus(d));
|
||||
helper.attributeEntry(attributes, 'node.gateway', d.flags.gateway ? 'ja' : null);
|
||||
helper.attributeEntry(attributes, 'node.coordinates', showGeoURI(d));
|
||||
|
||||
if (config.nodeInfobox && config.nodeInfobox.contact) {
|
||||
helper.attributeEntry(attributes, 'node.contact', helper.dictGet(d.nodeinfo, ['owner', 'contact']));
|
||||
function nodeIdLink(nodeId) {
|
||||
if (nodeDict[nodeId]) {
|
||||
return nodeLink(nodeDict[nodeId]);
|
||||
}
|
||||
return nodeId;
|
||||
}
|
||||
|
||||
helper.attributeEntry(attributes, 'node.hardware', helper.dictGet(d.nodeinfo, ['hardware', 'model']));
|
||||
helper.attributeEntry(attributes, 'node.primaryMac', helper.dictGet(d.nodeinfo, ['network', 'mac']));
|
||||
helper.attributeEntry(attributes, 'node.id', helper.dictGet(d.nodeinfo, ['node_id']));
|
||||
helper.attributeEntry(attributes, 'node.firmware', showFirmware(d));
|
||||
helper.attributeEntry(attributes, 'node.site', showSite(d, config));
|
||||
helper.attributeEntry(attributes, 'node.uptime', showUptime(d));
|
||||
helper.attributeEntry(attributes, 'node.firstSeen', showFirstseen(d));
|
||||
if (config.nodeInfobox && config.nodeInfobox.hardwareUsage) {
|
||||
helper.attributeEntry(attributes, 'node.systemLoad', showLoad(d));
|
||||
helper.attributeEntry(attributes, 'node.ram', showRAM(d));
|
||||
function showGateway(node) {
|
||||
var gatewayCols = [
|
||||
V.h('span', [
|
||||
nodeIdLink(node.gateway_nexthop),
|
||||
V.h('br'),
|
||||
_.t('node.nexthop')
|
||||
]),
|
||||
V.h('span', { props: { className: 'ion-arrow-right-c' } }),
|
||||
V.h('span', [
|
||||
nodeIdLink(node.gateway),
|
||||
V.h('br'),
|
||||
'IPv4'
|
||||
])
|
||||
];
|
||||
|
||||
if (node.gateway6 !== undefined) {
|
||||
gatewayCols.push(V.h('span', [
|
||||
nodeIdLink(node.gateway6),
|
||||
V.h('br'),
|
||||
'IPv6'
|
||||
]));
|
||||
}
|
||||
helper.attributeEntry(attributes, 'node.ipAddresses', showIPs(d));
|
||||
helper.attributeEntry(attributes, 'node.selectedGateway', helper.dictGet(d.statistics, ['gateway']));
|
||||
helper.attributeEntry(attributes, 'node.update', showAutoupdate(d));
|
||||
helper.attributeEntry(attributes, 'node.clients', showClients(d));
|
||||
|
||||
el.appendChild(attributes);
|
||||
return V.h('td', { props: { className: 'gateway' } }, gatewayCols);
|
||||
}
|
||||
|
||||
if (d.neighbours.length > 0) {
|
||||
var h3 = document.createElement('h3');
|
||||
h3.textContent = _.t('node.link', d.neighbours.length) + '(' + d.neighbours.length + ')';
|
||||
el.appendChild(h3);
|
||||
function renderNeighbourRow(n) {
|
||||
var icons = [V.h('span', { props: { className: 'icon ion-' + (n.link.type.indexOf('wifi') === 0 ? 'wifi' : 'share-alt'), title: _.t(n.link.type) } })];
|
||||
if (helper.hasLocation(n.node)) {
|
||||
icons.push(V.h('span', { props: { className: 'ion-location', title: _.t('location.location') } }));
|
||||
}
|
||||
|
||||
return V.h('tr', [
|
||||
V.h('td', icons),
|
||||
V.h('td', nodeLink(n.node)),
|
||||
V.h('td', n.node.clients),
|
||||
V.h('td', [V.h('a', {
|
||||
style: {
|
||||
color: linkScale((n.link.source_tq + n.link.target_tq) / 2)
|
||||
},
|
||||
props: {
|
||||
title: n.link.source.hostname + ' - ' + n.link.target.hostname,
|
||||
href: router.generateLink({ link: n.link.id })
|
||||
}, on: {
|
||||
click: function (e) {
|
||||
router.fullUrl({ link: n.link.id }, e);
|
||||
}
|
||||
}
|
||||
}, helper.showTq(n.link.source_tq) + ' - ' + helper.showTq(n.link.target_tq))]),
|
||||
V.h('td', helper.showDistance(n.link))
|
||||
]);
|
||||
}
|
||||
|
||||
var self = this;
|
||||
var header = document.createElement('h2');
|
||||
var table = document.createElement('table');
|
||||
var images = document.createElement('div');
|
||||
var neighbours = document.createElement('h3');
|
||||
var headings = [{
|
||||
name: ''
|
||||
name: '',
|
||||
sort: function (a, b) {
|
||||
return a.link.type.localeCompare(b.link.type);
|
||||
}
|
||||
}, {
|
||||
name: 'node.nodes',
|
||||
sort: function (a, b) {
|
||||
return a.node.nodeinfo.hostname.localeCompare(b.node.nodeinfo.hostname);
|
||||
return a.node.hostname.localeCompare(b.node.hostname);
|
||||
},
|
||||
reverse: false
|
||||
}, {
|
||||
name: 'node.clients',
|
||||
class: 'ion-people',
|
||||
sort: function (a, b) {
|
||||
return ('clients' in a.node.statistics ? a.node.statistics.clients : -1) -
|
||||
('clients' in b.node.statistics ? b.node.statistics.clients : -1);
|
||||
return a.node.clients - b.node.clients;
|
||||
},
|
||||
reverse: true
|
||||
}, {
|
||||
name: 'node.tq',
|
||||
class: 'ion-connection-bars',
|
||||
sort: function (a, b) {
|
||||
return a.link.tq - b.link.tq;
|
||||
return a.link.source_tq - b.link.source_tq;
|
||||
},
|
||||
reverse: true
|
||||
}, {
|
||||
@ -278,20 +126,69 @@ define(['sorttable', 'snabbdom', 'd3-interpolate', 'moment', 'helper'],
|
||||
},
|
||||
reverse: true
|
||||
}];
|
||||
var tableNeighbour = new SortTable(headings, 1, renderNeighbourRow);
|
||||
|
||||
var table = new SortTable(headings, 1, renderNeighbourRow);
|
||||
table.setData(d.neighbours);
|
||||
table.el.elm.classList.add('node-links');
|
||||
el.appendChild(table.el.elm);
|
||||
el.appendChild(header);
|
||||
el.appendChild(table);
|
||||
el.appendChild(neighbours);
|
||||
el.appendChild(tableNeighbour.el);
|
||||
el.appendChild(images);
|
||||
|
||||
self.render = function render() {
|
||||
V.patch(header, V.h('h2', d.hostname));
|
||||
|
||||
var children = [];
|
||||
|
||||
config.nodeAttr.forEach(function (row) {
|
||||
var field = d[row.value];
|
||||
if (typeof row.value === 'function') {
|
||||
field = row.value(d, nodeDict);
|
||||
} else if (nodef['show' + row.value] !== undefined) {
|
||||
field = nodef['show' + row.value](d);
|
||||
}
|
||||
|
||||
if (field) {
|
||||
if (typeof field !== 'object') {
|
||||
field = V.h('td', field);
|
||||
}
|
||||
children.push(V.h('tr', [
|
||||
row.name !== undefined ? V.h('th', _.t(row.name)) : null,
|
||||
field
|
||||
]));
|
||||
}
|
||||
});
|
||||
|
||||
children.push(V.h('tr', [
|
||||
V.h('th', _.t('node.gateway')),
|
||||
showGateway(d)
|
||||
]));
|
||||
|
||||
var elNew = V.h('table', children);
|
||||
table = V.patch(table, elNew);
|
||||
table.elm.classList.add('attributes');
|
||||
|
||||
V.patch(neighbours, V.h('h3', _.t('node.link', d.neighbours.length) + ' (' + d.neighbours.length + ')'));
|
||||
if (d.neighbours.length > 0) {
|
||||
tableNeighbour.setData(d.neighbours);
|
||||
tableNeighbour.el.elm.classList.add('node-links');
|
||||
}
|
||||
|
||||
if (config.nodeInfos) {
|
||||
var img = [];
|
||||
config.nodeInfos.forEach(function (nodeInfo) {
|
||||
var h4 = document.createElement('h4');
|
||||
h4.textContent = nodeInfo.name;
|
||||
el.appendChild(h4);
|
||||
el.appendChild(showStatImg(nodeInfo, d));
|
||||
img.push(V.h('h4', nodeInfo.name));
|
||||
img.push(showStatImg(nodeInfo, d));
|
||||
});
|
||||
images = V.patch(images, V.h('div', img));
|
||||
}
|
||||
};
|
||||
|
||||
self.setData = function setData(data) {
|
||||
if (data.nodeDict[d.node_id]) {
|
||||
d = data.nodeDict[d.node_id];
|
||||
}
|
||||
self.render();
|
||||
};
|
||||
return self;
|
||||
};
|
||||
});
|
||||
|
@ -1,46 +1,53 @@
|
||||
define(['helper'], function (helper) {
|
||||
'use strict';
|
||||
|
||||
return function (config, language) {
|
||||
return function (language) {
|
||||
var self = this;
|
||||
var stats = document.createTextNode('');
|
||||
var timestamp = document.createTextNode('');
|
||||
|
||||
self.setData = function setData(d) {
|
||||
var totalNodes = helper.sum(d.nodes.all.map(helper.one));
|
||||
var totalOnlineNodes = helper.sum(d.nodes.all.filter(helper.online).map(helper.one));
|
||||
var totalClients = helper.sum(d.nodes.all.filter(helper.online).map(function (n) {
|
||||
return n.statistics.clients ? n.statistics.clients : 0;
|
||||
var totalNodes = Object.keys(d.nodeDict).length;
|
||||
var totalOnlineNodes = d.nodes.online.length;
|
||||
var totalClients = helper.sum(d.nodes.online.map(function (n) {
|
||||
return n.clients;
|
||||
}));
|
||||
var totalGateways = helper.sum(d.nodes.all.filter(helper.online).filter(function (n) {
|
||||
return n.flags.gateway;
|
||||
var totalGateways = helper.sum(d.nodes.online.filter(function (n) {
|
||||
return n.is_gateway;
|
||||
}).map(helper.one));
|
||||
|
||||
stats.textContent = _.t('sidebar.nodes', { total: totalNodes, online: totalOnlineNodes }) + ' ' +
|
||||
_.t('sidebar.clients', { smart_count: totalClients }) + ' ' +
|
||||
_.t('sidebar.gateway', { smart_count: totalGateways });
|
||||
|
||||
timestamp.textContent = _.t('sidebar.lastUpdate') + ': ' + d.timestamp.format('DD.MM.Y HH:mm');
|
||||
timestamp.textContent = _.t('sidebar.lastUpdate') + ' ' + d.timestamp.fromNow();
|
||||
};
|
||||
|
||||
self.render = function render(el) {
|
||||
var h2 = document.createElement('h2');
|
||||
h2.textContent = config.siteName;
|
||||
el.appendChild(h2);
|
||||
var h1 = document.createElement('h1');
|
||||
h1.textContent = config.siteName;
|
||||
el.appendChild(h1);
|
||||
|
||||
language.languageSelect(el);
|
||||
|
||||
var p = document.createElement('p');
|
||||
p.classList.add('legend');
|
||||
p.innerHTML = '<span class="legend-new"><span class="symbol"></span> ' + _.t('sidebar.nodeNew') + '</span>' +
|
||||
'<span class="legend-online"><span class="symbol"></span> ' + _.t('sidebar.nodeOnline') + '</span>' +
|
||||
'<span class="legend-offline"><span class="symbol"></span> ' + _.t('sidebar.nodeOffline') + '</span>';
|
||||
el.appendChild(p);
|
||||
|
||||
p.appendChild(document.createElement('br'));
|
||||
p.appendChild(stats);
|
||||
p.appendChild(document.createElement('br'));
|
||||
p.appendChild(timestamp);
|
||||
|
||||
if (config.linkList) {
|
||||
p.appendChild(document.createElement('br'));
|
||||
config.linkList.forEach(function (link) {
|
||||
var a = document.createElement('a');
|
||||
a.innerText = link.title;
|
||||
a.href = link.href;
|
||||
p.appendChild(a);
|
||||
});
|
||||
}
|
||||
|
||||
el.appendChild(p);
|
||||
};
|
||||
|
||||
return self;
|
||||
|
@ -1,11 +1,17 @@
|
||||
define(['sorttable', 'snabbdom', 'helper'], function (SortTable, V, helper) {
|
||||
'use strict';
|
||||
V = V.default;
|
||||
|
||||
function linkName(d) {
|
||||
return (d.source.node ? d.source.node.nodeinfo.hostname : d.source.id) + ' – ' + d.target.node.nodeinfo.hostname;
|
||||
return (d.source ? d.source.hostname : d.source.id) + ' – ' + d.target.hostname;
|
||||
}
|
||||
|
||||
var headings = [{
|
||||
name: '',
|
||||
sort: function (a, b) {
|
||||
return a.type.localeCompare(b.type);
|
||||
}
|
||||
}, {
|
||||
name: 'node.nodes',
|
||||
sort: function (a, b) {
|
||||
return linkName(a).localeCompare(linkName(b));
|
||||
@ -15,7 +21,7 @@ define(['sorttable', 'snabbdom', 'helper'], function (SortTable, V, helper) {
|
||||
name: 'node.tq',
|
||||
class: 'ion-connection-bars',
|
||||
sort: function (a, b) {
|
||||
return a.tq - b.tq;
|
||||
return (a.source_tq + a.target_tq) / 2 - (b.source_tq + b.target_tq) / 2;
|
||||
},
|
||||
reverse: true
|
||||
}, {
|
||||
@ -28,9 +34,8 @@ define(['sorttable', 'snabbdom', 'helper'], function (SortTable, V, helper) {
|
||||
reverse: true
|
||||
}];
|
||||
|
||||
return function (linkScale, router) {
|
||||
var table = new SortTable(headings, 2, renderRow);
|
||||
V = V.default;
|
||||
return function (linkScale) {
|
||||
var table = new SortTable(headings, 3, renderRow);
|
||||
|
||||
function renderRow(d) {
|
||||
var td1Content = [V.h('a', {
|
||||
@ -43,11 +48,12 @@ define(['sorttable', 'snabbdom', 'helper'], function (SortTable, V, helper) {
|
||||
}
|
||||
}, linkName(d))];
|
||||
|
||||
var td1 = V.h('td', td1Content);
|
||||
var td2 = V.h('td', { style: { color: linkScale(1 / d.tq) } }, helper.showTq(d));
|
||||
var td3 = V.h('td', helper.showDistance(d));
|
||||
|
||||
return V.h('tr', [td1, td2, td3]);
|
||||
return V.h('tr', [
|
||||
V.h('td', V.h('span', { props: { className: 'icon ion-' + (d.type.indexOf('wifi') === 0 ? 'wifi' : 'share-alt'), title: _.t(d.type) } })),
|
||||
V.h('td', td1Content),
|
||||
V.h('td', { style: { color: linkScale((d.source_tq + d.target_tq) / 2) } }, helper.showTq(d.source_tq) + ' - ' + helper.showTq(d.target_tq)),
|
||||
V.h('td', helper.showDistance(d))
|
||||
]);
|
||||
}
|
||||
|
||||
this.render = function render(d) {
|
||||
@ -59,7 +65,7 @@ define(['sorttable', 'snabbdom', 'helper'], function (SortTable, V, helper) {
|
||||
};
|
||||
|
||||
this.setData = function setData(d) {
|
||||
table.setData(d.graph.links);
|
||||
table.setData(d.links);
|
||||
};
|
||||
};
|
||||
});
|
||||
|
158
lib/main.js
@ -2,108 +2,53 @@ define(['moment', 'utils/router', 'leaflet', 'gui', 'helper', 'utils/language'],
|
||||
function (moment, Router, L, GUI, helper, Language) {
|
||||
'use strict';
|
||||
|
||||
return function (config) {
|
||||
return function () {
|
||||
function handleData(data) {
|
||||
var dataNodes = {};
|
||||
dataNodes.nodes = [];
|
||||
var dataGraph = {};
|
||||
dataGraph.batadv = {};
|
||||
dataGraph.batadv.nodes = [];
|
||||
dataGraph.batadv.links = [];
|
||||
|
||||
function rearrangeLinks(d) {
|
||||
d.source += dataGraph.batadv.nodes.length;
|
||||
d.target += dataGraph.batadv.nodes.length;
|
||||
}
|
||||
var timestamp;
|
||||
var nodes = [];
|
||||
var links = [];
|
||||
var nodeDict = {};
|
||||
|
||||
for (var i = 0; i < data.length; ++i) {
|
||||
var vererr;
|
||||
if (i % 2) {
|
||||
if (data[i].version !== 1) {
|
||||
vererr = 'Unsupported graph version: ' + data[i].version;
|
||||
console.error(vererr); // silent fail
|
||||
} else {
|
||||
data[i].batadv.links.forEach(rearrangeLinks);
|
||||
dataGraph.batadv.nodes = dataGraph.batadv.nodes.concat(data[i].batadv.nodes);
|
||||
dataGraph.batadv.links = dataGraph.batadv.links.concat(data[i].batadv.links);
|
||||
dataGraph.timestamp = data[i].timestamp;
|
||||
nodes = nodes.concat(data[i].nodes);
|
||||
timestamp = data[i].timestamp;
|
||||
links = links.concat(data[i].links);
|
||||
}
|
||||
} else if (data[i].version !== 2) {
|
||||
vererr = 'Unsupported nodes version: ' + data[i].version;
|
||||
console.error(vererr); // silent fail
|
||||
} else {
|
||||
dataNodes.nodes = dataNodes.nodes.concat(data[i].nodes);
|
||||
dataNodes.timestamp = data[i].timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
var nodes = dataNodes.nodes.filter(function (d) {
|
||||
return 'firstseen' in d && 'lastseen' in d;
|
||||
});
|
||||
|
||||
nodes.forEach(function (node) {
|
||||
node.firstseen = moment.utc(node.firstseen).local();
|
||||
node.lastseen = moment.utc(node.lastseen).local();
|
||||
});
|
||||
|
||||
var now = moment();
|
||||
var age = moment(now).subtract(config.maxAge, 'days');
|
||||
var age = moment().subtract(config.maxAge, 'days');
|
||||
|
||||
var newnodes = helper.limit('firstseen', age, helper.sortByKey('firstseen', nodes).filter(helper.online));
|
||||
var lostnodes = helper.limit('lastseen', age, helper.sortByKey('lastseen', nodes).filter(helper.offline));
|
||||
|
||||
var graphnodes = {};
|
||||
|
||||
dataNodes.nodes.forEach(function (d) {
|
||||
graphnodes[d.nodeinfo.node_id] = d;
|
||||
var online = nodes.filter(function (d) {
|
||||
return d.is_online;
|
||||
});
|
||||
var offline = nodes.filter(function (d) {
|
||||
return !d.is_online;
|
||||
});
|
||||
|
||||
var graph = dataGraph.batadv;
|
||||
|
||||
graph.nodes.forEach(function (d) {
|
||||
if (d.node_id in graphnodes) {
|
||||
d.node = graphnodes[d.node_id];
|
||||
if (d.unseen) {
|
||||
d.node.flags.online = true;
|
||||
d.node.flags.unseen = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
graph.links.forEach(function (d) {
|
||||
d.source = graph.nodes[d.source];
|
||||
|
||||
if (graph.nodes[d.target].node) {
|
||||
d.target = graph.nodes[d.target];
|
||||
} else {
|
||||
d.target = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
var links = graph.links.filter(function (d) {
|
||||
return d.target !== undefined;
|
||||
});
|
||||
var newnodes = helper.limit('firstseen', age, helper.sortByKey('firstseen', online));
|
||||
var lostnodes = helper.limit('lastseen', age, helper.sortByKey('lastseen', offline));
|
||||
|
||||
nodes.forEach(function (d) {
|
||||
d.neighbours = [];
|
||||
nodeDict[d.node_id] = d;
|
||||
});
|
||||
|
||||
links.forEach(function (d) {
|
||||
var ids;
|
||||
d.source = nodeDict[d.source];
|
||||
d.target = nodeDict[d.target];
|
||||
|
||||
ids = [d.source.node.nodeinfo.node_id, d.target.node.nodeinfo.node_id];
|
||||
d.source.node.neighbours.push({ node: d.target.node, link: d, incoming: false });
|
||||
d.target.node.neighbours.push({ node: d.source.node, link: d, incoming: true });
|
||||
if (d.vpn) {
|
||||
d.source.node.meshlinks = d.source.node.meshlinks ? d.source.node.meshlinks + 1 : 1;
|
||||
}
|
||||
|
||||
d.id = ids.join('-');
|
||||
d.id = [d.source.node_id, d.target.node_id].join('-');
|
||||
d.source.neighbours.push({ node: d.target, link: d });
|
||||
d.target.neighbours.push({ node: d.source, link: d });
|
||||
|
||||
try {
|
||||
d.latlngs = [];
|
||||
d.latlngs.push(L.latLng(d.source.node.nodeinfo.location.latitude, d.source.node.nodeinfo.location.longitude));
|
||||
d.latlngs.push(L.latLng(d.target.node.nodeinfo.location.latitude, d.target.node.nodeinfo.location.longitude));
|
||||
d.latlngs.push(L.latLng(d.source.location.latitude, d.source.location.longitude));
|
||||
d.latlngs.push(L.latLng(d.target.location.latitude, d.target.location.longitude));
|
||||
|
||||
d.distance = d.latlngs[0].distanceTo(d.latlngs[1]);
|
||||
} catch (e) {
|
||||
@ -111,50 +56,53 @@ define(['moment', 'utils/router', 'leaflet', 'gui', 'helper', 'utils/language'],
|
||||
}
|
||||
});
|
||||
|
||||
links.sort(function (a, b) {
|
||||
return b.tq - a.tq;
|
||||
});
|
||||
|
||||
return {
|
||||
now: now,
|
||||
timestamp: moment.utc(dataNodes.timestamp).local(),
|
||||
now: moment(),
|
||||
timestamp: moment.utc(timestamp).local(),
|
||||
nodes: {
|
||||
all: nodes,
|
||||
online: online,
|
||||
offline: offline,
|
||||
new: newnodes,
|
||||
lost: lostnodes
|
||||
},
|
||||
graph: {
|
||||
links: links,
|
||||
nodes: graph.nodes
|
||||
}
|
||||
nodeDict: nodeDict
|
||||
};
|
||||
}
|
||||
|
||||
var language = new Language(config);
|
||||
var router = new Router(language);
|
||||
var language = new Language();
|
||||
window.router = new Router(language);
|
||||
|
||||
var urls = [];
|
||||
config.dataPath.forEach(function (d, i) {
|
||||
config.dataPath[i] += 'meshviewer.json';
|
||||
});
|
||||
|
||||
if (typeof config.dataPath === 'string' || config.dataPath instanceof String) {
|
||||
config.dataPath = [config.dataPath];
|
||||
}
|
||||
|
||||
for (var i in config.dataPath) {
|
||||
if (config.dataPath.hasOwnProperty(i)) {
|
||||
urls.push(config.dataPath[i] + 'nodes.json');
|
||||
urls.push(config.dataPath[i] + 'graph.json');
|
||||
}
|
||||
}
|
||||
language.init(router);
|
||||
|
||||
function update() {
|
||||
language.init(router);
|
||||
return Promise.all(urls.map(helper.getJSON))
|
||||
return Promise.all(config.dataPath.map(helper.getJSON))
|
||||
.then(handleData);
|
||||
}
|
||||
|
||||
update()
|
||||
.then(function (d) {
|
||||
var gui = new GUI(config, router, language);
|
||||
return new Promise(function (resolve, reject) {
|
||||
var count = 0;
|
||||
(function waitForLanguage() {
|
||||
if (Object.keys(_.phrases).length > 0) {
|
||||
resolve(d);
|
||||
} else if (count > 500) {
|
||||
reject(new Error('translation not loaded after 10 seconds'));
|
||||
} else {
|
||||
setTimeout(waitForLanguage.bind(this), 20);
|
||||
}
|
||||
count++;
|
||||
})();
|
||||
});
|
||||
})
|
||||
.then(function (d) {
|
||||
var gui = new GUI(language);
|
||||
gui.setData(d);
|
||||
router.setData(d);
|
||||
router.resolve();
|
||||
@ -168,7 +116,7 @@ define(['moment', 'utils/router', 'leaflet', 'gui', 'helper', 'utils/language'],
|
||||
})
|
||||
.catch(function (e) {
|
||||
document.querySelector('.loader').innerHTML += e.message
|
||||
+ '<br /><br /><button onclick="location.reload(true)" class="btn text">Try to reload</button><br /> or report to your community';
|
||||
+ '<br /><br /><button onclick="location.reload(true)" class="btn text" aria-label="Try to reload">Try to reload</button><br /> or report to your community';
|
||||
console.warn(e);
|
||||
});
|
||||
};
|
||||
|
59
lib/map.js
@ -1,4 +1,4 @@
|
||||
define(['map/clientlayer', 'map/labellayer', 'map/button', 'leaflet'],
|
||||
define(['map/clientlayer', 'map/labellayer', 'map/button', 'leaflet', 'map/activearea'],
|
||||
function (ClientLayer, LabelLayer, Button, L) {
|
||||
'use strict';
|
||||
|
||||
@ -8,7 +8,7 @@ define(['map/clientlayer', 'map/labellayer', 'map/button', 'leaflet'],
|
||||
minZoom: 0
|
||||
};
|
||||
|
||||
return function (config, linkScale, sidebar, router, buttons) {
|
||||
return function (linkScale, sidebar, buttons) {
|
||||
var self = this;
|
||||
var savedView;
|
||||
|
||||
@ -27,10 +27,26 @@ define(['map/clientlayer', 'map/labellayer', 'map/button', 'leaflet'],
|
||||
document.querySelector('.leaflet-control-layers').classList.add('leaflet-control-layers-expanded');
|
||||
}
|
||||
|
||||
function mapActiveArea() {
|
||||
map.setActiveArea({
|
||||
position: 'absolute',
|
||||
left: sidebar.getWidth() + 'px',
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0
|
||||
});
|
||||
}
|
||||
|
||||
function setActiveArea() {
|
||||
setTimeout(mapActiveArea, 300);
|
||||
}
|
||||
|
||||
var el = document.createElement('div');
|
||||
el.classList.add('map');
|
||||
|
||||
map = L.map(el, options);
|
||||
mapActiveArea();
|
||||
|
||||
var now = new Date();
|
||||
config.mapLayers.forEach(function (item, i) {
|
||||
if ((typeof item.config.start === 'number' && item.config.start <= now.getHours()) || (typeof item.config.end === 'number' && item.config.end > now.getHours())) {
|
||||
@ -47,7 +63,7 @@ define(['map/clientlayer', 'map/labellayer', 'map/button', 'leaflet'],
|
||||
var layers = config.mapLayers.map(function (d) {
|
||||
return {
|
||||
'name': d.name,
|
||||
'layer': 'url' in d ? L.tileLayer(d.url.replace('{retina}', L.Browser.retina ? '@2x' : ''), d.config) : console.warn('Missing map url')
|
||||
'layer': L.tileLayer(d.url.replace('{retina}', L.Browser.retina ? '@2x' : ''), d.config)
|
||||
};
|
||||
});
|
||||
|
||||
@ -57,13 +73,23 @@ define(['map/clientlayer', 'map/labellayer', 'map/button', 'leaflet'],
|
||||
baseLayers[d.name] = d.layer;
|
||||
});
|
||||
|
||||
var button = new Button(config, map, router, buttons);
|
||||
var button = new Button(map, buttons);
|
||||
|
||||
map.on('locationfound', button.locationFound);
|
||||
map.on('locationerror', button.locationError);
|
||||
map.on('dragend', saveView);
|
||||
map.on('contextmenu', contextMenuOpenLayerMenu);
|
||||
|
||||
if (config.geo) {
|
||||
[].forEach.call(config.geo, function (geo) {
|
||||
geo.json().then(function (result) {
|
||||
if (result) {
|
||||
L.geoJSON(result, geo.option).addTo(map);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
button.init();
|
||||
|
||||
layerControl = L.control.layers(baseLayers, [], { position: 'bottomright' });
|
||||
@ -79,6 +105,8 @@ define(['map/clientlayer', 'map/labellayer', 'map/button', 'leaflet'],
|
||||
labelLayer.addTo(map);
|
||||
labelLayer.setZIndex(6);
|
||||
|
||||
sidebar.button.addEventListener('visibility', setActiveArea);
|
||||
|
||||
map.on('zoom', function () {
|
||||
clientLayer.redraw();
|
||||
labelLayer.redraw();
|
||||
@ -105,6 +133,14 @@ define(['map/clientlayer', 'map/labellayer', 'map/button', 'leaflet'],
|
||||
}
|
||||
});
|
||||
|
||||
map.on('load', function () {
|
||||
var inputs = document.querySelectorAll('.leaflet-control-layers-selector');
|
||||
[].forEach.call(inputs, function (input) {
|
||||
input.setAttribute('role', 'radiogroup');
|
||||
input.setAttribute('aria-label', input.nextSibling.innerHTML.trim());
|
||||
});
|
||||
});
|
||||
|
||||
var nodeDict = {};
|
||||
var linkDict = {};
|
||||
var highlight;
|
||||
@ -120,7 +156,7 @@ define(['map/clientlayer', 'map/labellayer', 'map/button', 'leaflet'],
|
||||
}
|
||||
|
||||
function setView(bounds, zoom) {
|
||||
map.fitBounds(bounds, { paddingTopLeft: [sidebar(), 0], maxZoom: (zoom ? zoom : config.nodeZoom) });
|
||||
map.fitBounds(bounds, { maxZoom: (zoom ? zoom : config.nodeZoom) });
|
||||
}
|
||||
|
||||
function goto(m) {
|
||||
@ -142,12 +178,12 @@ define(['map/clientlayer', 'map/labellayer', 'map/button', 'leaflet'],
|
||||
var m;
|
||||
|
||||
if (highlight !== undefined) {
|
||||
if (highlight.type === 'node' && nodeDict[highlight.o.nodeinfo.node_id]) {
|
||||
m = nodeDict[highlight.o.nodeinfo.node_id];
|
||||
m.setStyle({ color: 'orange', weight: 20, fillOpacity: 1, opacity: 0.7, className: 'stroke-first' });
|
||||
if (highlight.type === 'node' && nodeDict[highlight.o.node_id]) {
|
||||
m = nodeDict[highlight.o.node_id];
|
||||
m.setStyle(config.map.highlightNode);
|
||||
} else if (highlight.type === 'link' && linkDict[highlight.o.id]) {
|
||||
m = linkDict[highlight.o.id];
|
||||
m.setStyle({ weight: 4, opacity: 1, dashArray: '5, 10' });
|
||||
m.setStyle(config.map.highlightLink);
|
||||
}
|
||||
}
|
||||
|
||||
@ -167,7 +203,7 @@ define(['map/clientlayer', 'map/labellayer', 'map/button', 'leaflet'],
|
||||
linkDict = {};
|
||||
|
||||
clientLayer.setData(data);
|
||||
labelLayer.setData(data, map, nodeDict, linkDict, linkScale, router, config);
|
||||
labelLayer.setData(data, map, nodeDict, linkDict, linkScale);
|
||||
|
||||
updateView(true);
|
||||
};
|
||||
@ -186,7 +222,7 @@ define(['map/clientlayer', 'map/labellayer', 'map/button', 'leaflet'],
|
||||
|
||||
self.gotoLink = function gotoLink(d) {
|
||||
button.disableTracking();
|
||||
highlight = { type: 'link', o: d };
|
||||
highlight = { type: 'link', o: d[0] };
|
||||
updateView();
|
||||
};
|
||||
|
||||
@ -197,6 +233,7 @@ define(['map/clientlayer', 'map/labellayer', 'map/button', 'leaflet'],
|
||||
|
||||
self.destroy = function destroy() {
|
||||
button.clearButtons();
|
||||
sidebar.button.removeEventListener('visibility', setActiveArea);
|
||||
map.remove();
|
||||
|
||||
if (el.parentNode) {
|
||||
|
291
lib/map/activearea.js
Normal file
@ -0,0 +1,291 @@
|
||||
define(function () {
|
||||
/**
|
||||
* https://github.com/Mappy/Leaflet-active-area
|
||||
* Apache 2.0 license https://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
|
||||
var previousMethods = {
|
||||
getCenter: L.Map.prototype.getCenter,
|
||||
setView: L.Map.prototype.setView,
|
||||
setZoomAround: L.Map.prototype.setZoomAround,
|
||||
getBoundsZoom: L.Map.prototype.getBoundsZoom,
|
||||
RendererUpdate: L.Renderer.prototype._update
|
||||
};
|
||||
|
||||
L.Map.include({
|
||||
getBounds: function () {
|
||||
if (this._viewport) {
|
||||
return this.getViewportLatLngBounds();
|
||||
}
|
||||
var bounds = this.getPixelBounds();
|
||||
var sw = this.unproject(bounds.getBottomLeft());
|
||||
var ne = this.unproject(bounds.getTopRight());
|
||||
|
||||
return new L.LatLngBounds(sw, ne);
|
||||
},
|
||||
|
||||
getViewport: function () {
|
||||
return this._viewport;
|
||||
},
|
||||
|
||||
getViewportBounds: function () {
|
||||
var vp = this._viewport;
|
||||
var topleft = L.point(vp.offsetLeft, vp.offsetTop);
|
||||
var vpsize = L.point(vp.clientWidth, vp.clientHeight);
|
||||
|
||||
if (vpsize.x === 0 || vpsize.y === 0) {
|
||||
// Our own viewport has no good size - so we fallback to the container size:
|
||||
vp = this.getContainer();
|
||||
if (vp) {
|
||||
topleft = L.point(0, 0);
|
||||
vpsize = L.point(vp.clientWidth, vp.clientHeight);
|
||||
}
|
||||
}
|
||||
|
||||
return L.bounds(topleft, topleft.add(vpsize));
|
||||
},
|
||||
|
||||
getViewportLatLngBounds: function () {
|
||||
var bounds = this.getViewportBounds();
|
||||
return L.latLngBounds(this.containerPointToLatLng(bounds.min), this.containerPointToLatLng(bounds.max));
|
||||
},
|
||||
|
||||
getOffset: function () {
|
||||
var mCenter = this.getSize().divideBy(2);
|
||||
var vCenter = this.getViewportBounds().getCenter();
|
||||
|
||||
return mCenter.subtract(vCenter);
|
||||
},
|
||||
|
||||
getCenter: function (withoutViewport) {
|
||||
var center = previousMethods.getCenter.call(this);
|
||||
|
||||
if (this.getViewport() && !withoutViewport) {
|
||||
var zoom = this.getZoom();
|
||||
var point = this.project(center, zoom);
|
||||
point = point.subtract(this.getOffset());
|
||||
|
||||
center = this.unproject(point, zoom);
|
||||
}
|
||||
|
||||
return center;
|
||||
},
|
||||
|
||||
setView: function (center, zoom, options) {
|
||||
center = L.latLng(center);
|
||||
zoom = zoom === undefined ? this._zoom : this._limitZoom(zoom);
|
||||
|
||||
if (this.getViewport()) {
|
||||
var point = this.project(center, this._limitZoom(zoom));
|
||||
point = point.add(this.getOffset());
|
||||
center = this.unproject(point, this._limitZoom(zoom));
|
||||
}
|
||||
|
||||
return previousMethods.setView.call(this, center, zoom, options);
|
||||
},
|
||||
|
||||
setZoomAround: function (latlng, zoom, options) {
|
||||
var vp = this.getViewport();
|
||||
|
||||
if (vp) {
|
||||
var scale = this.getZoomScale(zoom);
|
||||
var viewHalf = this.getViewportBounds().getCenter();
|
||||
var containerPoint = latlng instanceof L.Point ? latlng : this.latLngToContainerPoint(latlng);
|
||||
|
||||
var centerOffset = containerPoint.subtract(viewHalf).multiplyBy(1 - 1 / scale);
|
||||
var newCenter = this.containerPointToLatLng(viewHalf.add(centerOffset));
|
||||
|
||||
return this.setView(newCenter, zoom, { zoom: options });
|
||||
}
|
||||
return previousMethods.setZoomAround.call(this, latlng, zoom, options);
|
||||
},
|
||||
|
||||
getBoundsZoom: function (bounds, inside, padding) { // (LatLngBounds[, Boolean, Point]) -> Number
|
||||
bounds = L.latLngBounds(bounds);
|
||||
padding = L.point(padding || [0, 0]);
|
||||
|
||||
var zoom = this.getZoom() || 0;
|
||||
var min = this.getMinZoom();
|
||||
var max = this.getMaxZoom();
|
||||
var nw = bounds.getNorthWest();
|
||||
var se = bounds.getSouthEast();
|
||||
var vp = this.getViewport();
|
||||
var size = (vp ? L.point(vp.clientWidth, vp.clientHeight) : this.getSize()).subtract(padding);
|
||||
var boundsSize = this.project(se, zoom).subtract(this.project(nw, zoom));
|
||||
var snap = L.Browser.any3d ? this.options.zoomSnap : 1;
|
||||
|
||||
var scale = Math.min(size.x / boundsSize.x, size.y / boundsSize.y);
|
||||
|
||||
zoom = this.getScaleZoom(scale, zoom);
|
||||
|
||||
if (snap) {
|
||||
zoom = Math.round(zoom / (snap / 100)) * (snap / 100); // don't jump if within 1% of a snap level
|
||||
zoom = inside ? Math.ceil(zoom / snap) * snap : Math.floor(zoom / snap) * snap;
|
||||
}
|
||||
|
||||
return Math.max(min, Math.min(max, zoom));
|
||||
}
|
||||
});
|
||||
|
||||
L.Map.include({
|
||||
setActiveArea: function (css, keepCenter, animate) {
|
||||
var center;
|
||||
if (keepCenter && this._zoom) {
|
||||
// save center if map is already initialized
|
||||
// and keepCenter is passed
|
||||
center = this.getCenter();
|
||||
}
|
||||
|
||||
if (!this._viewport) {
|
||||
// Make viewport if not already made
|
||||
var container = this.getContainer();
|
||||
this._viewport = L.DomUtil.create('div', '');
|
||||
container.insertBefore(this._viewport, container.firstChild);
|
||||
}
|
||||
|
||||
if (typeof css === 'string') {
|
||||
this._viewport.className = css;
|
||||
} else {
|
||||
L.extend(this._viewport.style, css);
|
||||
}
|
||||
|
||||
if (center) {
|
||||
this.setView(center, this.getZoom(), { animate: !!animate });
|
||||
}
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
L.Renderer.include({
|
||||
_onZoom: function () {
|
||||
this._updateTransform(this._map.getCenter(true), this._map.getZoom());
|
||||
},
|
||||
|
||||
_update: function () {
|
||||
previousMethods.RendererUpdate.call(this);
|
||||
this._center = this._map.getCenter(true);
|
||||
}
|
||||
});
|
||||
|
||||
L.GridLayer.include({
|
||||
_updateLevels: function () {
|
||||
var zoom = this._tileZoom;
|
||||
var maxZoom = this.options.maxZoom;
|
||||
|
||||
if (zoom === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
for (var z in this._levels) {
|
||||
if (this._levels[z].el.children.length || z === zoom) {
|
||||
this._levels[z].el.style.zIndex = maxZoom - Math.abs(zoom - z);
|
||||
} else {
|
||||
L.DomUtil.remove(this._levels[z].el);
|
||||
this._removeTilesAtZoom(z);
|
||||
delete this._levels[z];
|
||||
}
|
||||
}
|
||||
|
||||
var level = this._levels[zoom];
|
||||
var map = this._map;
|
||||
|
||||
if (!level) {
|
||||
level = this._levels[zoom] = {};
|
||||
|
||||
level.el = L.DomUtil.create('div', 'leaflet-tile-container leaflet-zoom-animated', this._container);
|
||||
level.el.style.zIndex = maxZoom;
|
||||
|
||||
level.origin = map.project(map.unproject(map.getPixelOrigin()), zoom).round();
|
||||
level.zoom = zoom;
|
||||
|
||||
this._setZoomTransform(level, map.getCenter(true), map.getZoom());
|
||||
|
||||
// force the browser to consider the newly added element for transition
|
||||
L.Util.falseFn(level.el.offsetWidth);
|
||||
}
|
||||
|
||||
this._level = level;
|
||||
|
||||
return level;
|
||||
},
|
||||
|
||||
_resetView: function (e) {
|
||||
var animating = e && (e.pinch || e.flyTo);
|
||||
this._setView(this._map.getCenter(true), this._map.getZoom(), animating, animating);
|
||||
},
|
||||
|
||||
_update: function (center) {
|
||||
var map = this._map;
|
||||
if (!map) {
|
||||
return;
|
||||
}
|
||||
var zoom = map.getZoom();
|
||||
|
||||
if (center === undefined) {
|
||||
center = map.getCenter(this);
|
||||
}
|
||||
if (this._tileZoom === undefined) {
|
||||
return;
|
||||
} // if out of minzoom/maxzoom
|
||||
|
||||
var pixelBounds = this._getTiledPixelBounds(center);
|
||||
var tileRange = this._pxBoundsToTileRange(pixelBounds);
|
||||
var tileCenter = tileRange.getCenter();
|
||||
var queue = [];
|
||||
|
||||
for (var key in this._tiles) {
|
||||
this._tiles[key].current = false;
|
||||
}
|
||||
|
||||
// _update just loads more tiles. If the tile zoom level differs too much
|
||||
// from the map's, let _setView reset levels and prune old tiles.
|
||||
if (Math.abs(zoom - this._tileZoom) > 1) {
|
||||
this._setView(center, zoom);
|
||||
return;
|
||||
}
|
||||
|
||||
// create a queue of coordinates to load tiles from
|
||||
for (var j = tileRange.min.y; j <= tileRange.max.y; j++) {
|
||||
for (var i = tileRange.min.x; i <= tileRange.max.x; i++) {
|
||||
var coords = new L.Point(i, j);
|
||||
coords.z = this._tileZoom;
|
||||
|
||||
if (!this._isValidTile(coords)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var tile = this._tiles[this._tileCoordsToKey(coords)];
|
||||
if (tile) {
|
||||
tile.current = true;
|
||||
} else {
|
||||
queue.push(coords);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sort tile queue to load tiles in order of their distance to center
|
||||
queue.sort(function (a, b) {
|
||||
return a.distanceTo(tileCenter) - b.distanceTo(tileCenter);
|
||||
});
|
||||
|
||||
if (queue.length !== 0) {
|
||||
// if its the first batch of tiles to load
|
||||
if (!this._loading) {
|
||||
this._loading = true;
|
||||
// @event loading: Event
|
||||
// Fired when the grid layer starts loading tiles
|
||||
this.fire('loading');
|
||||
}
|
||||
|
||||
// create DOM fragment to append tiles in one batch
|
||||
var fragment = document.createDocumentFragment();
|
||||
|
||||
for (i = 0; i < queue.length; i++) {
|
||||
this._addTile(queue[i], fragment);
|
||||
}
|
||||
|
||||
this._level.el.appendChild(fragment);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
@ -1,5 +1,5 @@
|
||||
define(['map/clientlayer', 'map/labellayer', 'leaflet', 'moment', 'map/locationmarker'],
|
||||
function (ClientLayer, LabelLayer, L, moment, LocationMarker) {
|
||||
define(['map/clientlayer', 'map/labellayer', 'leaflet', 'map/locationmarker'],
|
||||
function (ClientLayer, LabelLayer, L, LocationMarker) {
|
||||
'use strict';
|
||||
var self = {};
|
||||
|
||||
@ -28,8 +28,8 @@ define(['map/clientlayer', 'map/labellayer', 'leaflet', 'moment', 'map/locationm
|
||||
|
||||
var LocateButton = ButtonBase.extend({
|
||||
onAdd: function () {
|
||||
var button = L.DomUtil.create('button', 'ion-locate shadow');
|
||||
button.setAttribute('data-tooltip', _.t('button.tracking'));
|
||||
var button = L.DomUtil.create('button', 'ion-locate');
|
||||
button.setAttribute('aria-label', _.t('button.tracking'));
|
||||
L.DomEvent.disableClickPropagation(button);
|
||||
L.DomEvent.addListener(button, 'click', this.onClick, this);
|
||||
|
||||
@ -45,8 +45,8 @@ define(['map/clientlayer', 'map/labellayer', 'leaflet', 'moment', 'map/locationm
|
||||
|
||||
var CoordsPickerButton = ButtonBase.extend({
|
||||
onAdd: function () {
|
||||
var button = L.DomUtil.create('button', 'ion-pin shadow');
|
||||
button.setAttribute('data-tooltip', _.t('button.location'));
|
||||
var button = L.DomUtil.create('button', 'ion-pin');
|
||||
button.setAttribute('aria-label', _.t('button.location'));
|
||||
|
||||
// Click propagation isn't disabled as this causes problems with the
|
||||
// location picking mode; instead propagation is stopped in onClick().
|
||||
@ -63,7 +63,7 @@ define(['map/clientlayer', 'map/labellayer', 'leaflet', 'moment', 'map/locationm
|
||||
}
|
||||
});
|
||||
|
||||
return function (config, map, router, buttons) {
|
||||
return function (map, buttons) {
|
||||
var userLocation;
|
||||
|
||||
var locateUserButton = new LocateButton(function (d) {
|
||||
|
@ -1,23 +1,23 @@
|
||||
define(['leaflet', 'rbush', 'helper'],
|
||||
function (L, rbush, helper) {
|
||||
function (L, RBush, helper) {
|
||||
'use strict';
|
||||
|
||||
return L.GridLayer.extend({
|
||||
mapRTree: function mapRTree(d) {
|
||||
return {
|
||||
minX: d.nodeinfo.location.latitude, minY: d.nodeinfo.location.longitude,
|
||||
maxX: d.nodeinfo.location.latitude, maxY: d.nodeinfo.location.longitude,
|
||||
minX: d.location.latitude, minY: d.location.longitude,
|
||||
maxX: d.location.latitude, maxY: d.location.longitude,
|
||||
node: d
|
||||
};
|
||||
},
|
||||
setData: function (data) {
|
||||
var rtreeOnlineAll = rbush(9);
|
||||
var rtreeOnlineAll = new RBush(9);
|
||||
|
||||
this.data = rtreeOnlineAll.load(data.nodes.all.filter(helper.online).filter(helper.hasLocation).map(this.mapRTree));
|
||||
this.data = rtreeOnlineAll.load(data.nodes.online.filter(helper.hasLocation).map(this.mapRTree));
|
||||
|
||||
// pre-calculate start angles
|
||||
this.data.all().forEach(function (n) {
|
||||
n.startAngle = (parseInt(n.node.nodeinfo.node_id.substr(10, 2), 16) / 255) * 2 * Math.PI;
|
||||
n.startAngle = (parseInt(n.node.node_id.substr(10, 2), 16) / 255) * 2 * Math.PI;
|
||||
});
|
||||
this.redraw();
|
||||
},
|
||||
@ -45,21 +45,17 @@ define(['leaflet', 'rbush', 'helper'],
|
||||
return tile;
|
||||
}
|
||||
|
||||
var startDistance = 12;
|
||||
var startDistance = 10;
|
||||
|
||||
ctx.beginPath();
|
||||
nodes.forEach(function (d) {
|
||||
var p = map.project([d.node.nodeinfo.location.latitude, d.node.nodeinfo.location.longitude]);
|
||||
var p = map.project([d.node.location.latitude, d.node.location.longitude]);
|
||||
|
||||
p.x -= s.x;
|
||||
p.y -= s.y;
|
||||
|
||||
helper.positionClients(ctx, p, d.startAngle, d.node.statistics.clients, startDistance);
|
||||
helper.positionClients(ctx, p, d.startAngle, d.node, startDistance);
|
||||
});
|
||||
|
||||
ctx.fillStyle = 'rgba(220, 0, 103, 0.7)';
|
||||
ctx.fill();
|
||||
|
||||
return tile;
|
||||
}
|
||||
});
|
||||
|
@ -1,5 +1,5 @@
|
||||
define(['leaflet', 'rbush', 'helper', 'moment'],
|
||||
function (L, rbush, helper, moment) {
|
||||
function (L, RBush, helper, moment) {
|
||||
'use strict';
|
||||
|
||||
var groupOnline;
|
||||
@ -35,14 +35,14 @@ define(['leaflet', 'rbush', 'helper', 'moment'],
|
||||
return function (d) {
|
||||
var font = fontSize + 'px ' + bodyStyle.fontFamily;
|
||||
return {
|
||||
position: L.latLng(d.nodeinfo.location.latitude, d.nodeinfo.location.longitude),
|
||||
label: d.nodeinfo.hostname,
|
||||
position: L.latLng(d.location.latitude, d.location.longitude),
|
||||
label: d.hostname,
|
||||
offset: offset,
|
||||
fillStyle: fillStyle,
|
||||
height: fontSize * 1.2,
|
||||
font: font,
|
||||
stroke: stroke,
|
||||
width: measureText(font, d.nodeinfo.hostname).width
|
||||
width: measureText(font, d.hostname).width
|
||||
};
|
||||
};
|
||||
}
|
||||
@ -76,33 +76,33 @@ define(['leaflet', 'rbush', 'helper', 'moment'],
|
||||
return { minX: x, minY: y, maxX: x + width, maxY: y + height };
|
||||
}
|
||||
|
||||
function mkMarker(dict, iconFunc, router) {
|
||||
function mkMarker(dict, iconFunc) {
|
||||
return function (d) {
|
||||
var m = L.circleMarker([d.nodeinfo.location.latitude, d.nodeinfo.location.longitude], iconFunc(d));
|
||||
var m = L.circleMarker([d.location.latitude, d.location.longitude], iconFunc(d));
|
||||
|
||||
m.resetStyle = function resetStyle() {
|
||||
m.setStyle(iconFunc(d));
|
||||
};
|
||||
|
||||
m.on('click', function () {
|
||||
router.fullUrl({ node: d.nodeinfo.node_id });
|
||||
router.fullUrl({ node: d.node_id });
|
||||
});
|
||||
m.bindTooltip(d.nodeinfo.hostname);
|
||||
m.bindTooltip(helper.escape(d.hostname));
|
||||
|
||||
dict[d.nodeinfo.node_id] = m;
|
||||
dict[d.node_id] = m;
|
||||
|
||||
return m;
|
||||
};
|
||||
}
|
||||
|
||||
function addLinksToMap(dict, linkScale, graph, router) {
|
||||
function addLinksToMap(dict, linkScale, graph) {
|
||||
graph = graph.filter(function (d) {
|
||||
return 'distance' in d && !d.vpn;
|
||||
return 'distance' in d && d.type.indexOf('vpn') !== 0;
|
||||
});
|
||||
|
||||
return graph.map(function (d) {
|
||||
var opts = {
|
||||
color: linkScale(1 / d.tq),
|
||||
color: linkScale((d.source_tq + d.target_tq) / 2),
|
||||
weight: 4,
|
||||
opacity: 0.5,
|
||||
dashArray: 'none'
|
||||
@ -114,7 +114,9 @@ define(['leaflet', 'rbush', 'helper', 'moment'],
|
||||
line.setStyle(opts);
|
||||
};
|
||||
|
||||
line.bindTooltip(d.source.node.nodeinfo.hostname + ' – ' + d.target.node.nodeinfo.hostname + '<br><strong>' + helper.showDistance(d) + ' / ' + helper.showTq(d) + '</strong>');
|
||||
line.bindTooltip(helper.escape(d.source.hostname + ' – ' + d.target.hostname) +
|
||||
'<br><strong>' + helper.showDistance(d) + ' / ' + helper.showTq(d.source_tq) + ' - ' + helper.showTq(d.target_tq) + '<br>' + d.type + '</strong>');
|
||||
|
||||
line.on('click', function () {
|
||||
router.fullUrl({ link: d.id });
|
||||
});
|
||||
@ -125,7 +127,7 @@ define(['leaflet', 'rbush', 'helper', 'moment'],
|
||||
});
|
||||
}
|
||||
|
||||
function getIcon(config, color) {
|
||||
function getIcon(color) {
|
||||
return Object.assign({}, config.icon.base, config.icon[color]);
|
||||
}
|
||||
|
||||
@ -136,12 +138,12 @@ define(['leaflet', 'rbush', 'helper', 'moment'],
|
||||
this.prepareLabels();
|
||||
}
|
||||
},
|
||||
setData: function (data, map, nodeDict, linkDict, linkScale, router, config) {
|
||||
var iconOnline = getIcon(config, 'online');
|
||||
var iconOffline = getIcon(config, 'offline');
|
||||
var iconLost = getIcon(config, 'lost');
|
||||
var iconAlert = getIcon(config, 'alert');
|
||||
var iconNew = getIcon(config, 'new');
|
||||
setData: function (data, map, nodeDict, linkDict, linkScale) {
|
||||
var iconOnline = getIcon('online');
|
||||
var iconOffline = getIcon('offline');
|
||||
var iconLost = getIcon('lost');
|
||||
var iconAlert = getIcon('alert');
|
||||
var iconNew = getIcon('new');
|
||||
// Check if init or data is already set
|
||||
if (groupLines) {
|
||||
groupOffline.clearLayers();
|
||||
@ -151,38 +153,36 @@ define(['leaflet', 'rbush', 'helper', 'moment'],
|
||||
groupLines.clearLayers();
|
||||
}
|
||||
|
||||
var lines = addLinksToMap(linkDict, linkScale, data.graph.links, router);
|
||||
var lines = addLinksToMap(linkDict, linkScale, data.links);
|
||||
groupLines = L.featureGroup(lines).addTo(map);
|
||||
|
||||
var nodesOnline = helper.subtract(data.nodes.all.filter(helper.online), data.nodes.new);
|
||||
var nodesOffline = helper.subtract(data.nodes.all.filter(helper.offline), data.nodes.lost);
|
||||
var nodesOnline = helper.subtract(data.nodes.online, data.nodes.new).filter(helper.hasLocation);
|
||||
var nodesOffline = helper.subtract(data.nodes.offline, data.nodes.lost).filter(helper.hasLocation);
|
||||
var nodesNew = data.nodes.new.filter(helper.hasLocation);
|
||||
var nodesLost = data.nodes.lost.filter(helper.hasLocation);
|
||||
|
||||
var markersOnline = nodesOnline.filter(helper.hasLocation)
|
||||
.map(mkMarker(nodeDict, function () {
|
||||
var markersOnline = nodesOnline.map(mkMarker(nodeDict, function () {
|
||||
return iconOnline;
|
||||
}, router));
|
||||
}));
|
||||
|
||||
var markersOffline = nodesOffline.filter(helper.hasLocation)
|
||||
.map(mkMarker(nodeDict, function () {
|
||||
var markersOffline = nodesOffline.map(mkMarker(nodeDict, function () {
|
||||
return iconOffline;
|
||||
}, router));
|
||||
}));
|
||||
|
||||
var markersNew = data.nodes.new.filter(helper.hasLocation)
|
||||
.map(mkMarker(nodeDict, function () {
|
||||
var markersNew = nodesNew.map(mkMarker(nodeDict, function () {
|
||||
return iconNew;
|
||||
}, router));
|
||||
}));
|
||||
|
||||
var markersLost = data.nodes.lost.filter(helper.hasLocation)
|
||||
.map(mkMarker(nodeDict, function (d) {
|
||||
if (d.lastseen.isAfter(moment(data.now).subtract(config.maxAgeAlert, 'days'))) {
|
||||
var markersLost = nodesLost.map(mkMarker(nodeDict, function (d) {
|
||||
var age = moment(data.now).diff(d.lastseen, 'days', true);
|
||||
if (age <= config.maxAgeAlert) {
|
||||
return iconAlert;
|
||||
}
|
||||
|
||||
if (d.lastseen.isAfter(moment(data.now).subtract(config.maxAge, 'days'))) {
|
||||
if (age <= config.maxAge) {
|
||||
return iconLost;
|
||||
}
|
||||
return null;
|
||||
}, router));
|
||||
}));
|
||||
|
||||
groupOffline = L.featureGroup(markersOffline).addTo(map);
|
||||
groupLost = L.featureGroup(markersLost).addTo(map);
|
||||
@ -190,10 +190,10 @@ define(['leaflet', 'rbush', 'helper', 'moment'],
|
||||
groupNew = L.featureGroup(markersNew).addTo(map);
|
||||
|
||||
this.data = {
|
||||
online: nodesOnline.filter(helper.hasLocation),
|
||||
offline: nodesOffline.filter(helper.hasLocation),
|
||||
new: data.nodes.new.filter(helper.hasLocation),
|
||||
lost: data.nodes.lost.filter(helper.hasLocation)
|
||||
online: nodesOnline,
|
||||
offline: nodesOffline,
|
||||
new: nodesNew,
|
||||
lost: nodesLost
|
||||
};
|
||||
this.updateLayer();
|
||||
},
|
||||
@ -214,9 +214,9 @@ define(['leaflet', 'rbush', 'helper', 'moment'],
|
||||
// - color (string)
|
||||
|
||||
var labelsOnline = d.online.map(prepareLabel(null, 11, 8, true));
|
||||
var labelsOffline = d.offline.map(prepareLabel('rgba(212, 62, 42, 0.9)', 9, 5, false));
|
||||
var labelsNew = d.new.map(prepareLabel('rgba(48, 99, 20, 0.9)', 11, 8, true));
|
||||
var labelsLost = d.lost.map(prepareLabel('rgba(212, 62, 42, 0.9)', 11, 8, true));
|
||||
var labelsOffline = d.offline.map(prepareLabel(config.icon.offline.color, 9, 5, false));
|
||||
var labelsNew = d.new.map(prepareLabel(config.map.labelNewColor, 11, 8, true));
|
||||
var labelsLost = d.lost.map(prepareLabel(config.icon.lost.color, 11, 8, true));
|
||||
|
||||
var labels = []
|
||||
.concat(labelsNew)
|
||||
@ -239,7 +239,7 @@ define(['leaflet', 'rbush', 'helper', 'moment'],
|
||||
}
|
||||
|
||||
for (var z = minZoom; z <= maxZoom; z++) {
|
||||
trees[z] = rbush(9);
|
||||
trees[z] = new RBush(9);
|
||||
trees[z].load(labels.map(nodeToRect(z)));
|
||||
}
|
||||
|
||||
@ -291,7 +291,7 @@ define(['leaflet', 'rbush', 'helper', 'moment'],
|
||||
}).sort().reverse()[0];
|
||||
}
|
||||
|
||||
this.labels = rbush(9);
|
||||
this.labels = new RBush(9);
|
||||
this.labels.load(labels.map(mapRTree));
|
||||
|
||||
this.redraw();
|
||||
|
@ -2,39 +2,10 @@ define(['leaflet'], function (L) {
|
||||
'use strict';
|
||||
|
||||
return L.CircleMarker.extend({
|
||||
outerCircle: {
|
||||
stroke: false,
|
||||
color: '#4285F4',
|
||||
opacity: 1,
|
||||
fillOpacity: 0.3,
|
||||
clickable: false,
|
||||
radius: 16
|
||||
},
|
||||
|
||||
innerCircle: {
|
||||
stroke: true,
|
||||
color: '#ffffff',
|
||||
fillColor: '#4285F4',
|
||||
weight: 1.5,
|
||||
clickable: false,
|
||||
opacity: 1,
|
||||
fillOpacity: 1,
|
||||
radius: 7
|
||||
},
|
||||
|
||||
accuracyCircle: {
|
||||
stroke: true,
|
||||
color: '#4285F4',
|
||||
weight: 1,
|
||||
clickable: false,
|
||||
opacity: 0.7,
|
||||
fillOpacity: 0.2
|
||||
},
|
||||
|
||||
initialize: function (latlng) {
|
||||
this.accuracyCircle = L.circle(latlng, 0, this.accuracyCircle);
|
||||
this.outerCircle = L.circleMarker(latlng, this.outerCircle);
|
||||
L.CircleMarker.prototype.initialize.call(this, latlng, this.innerCircle);
|
||||
this.accuracyCircle = L.circle(latlng, 0, config.locate.accuracyCircle);
|
||||
this.outerCircle = L.circleMarker(latlng, config.locate.outerCircle);
|
||||
L.CircleMarker.prototype.initialize.call(this, latlng, config.locate.innerCircle);
|
||||
|
||||
this.on('remove', function () {
|
||||
this._map.removeLayer(this.accuracyCircle);
|
||||
|
@ -2,28 +2,18 @@ define(['sorttable', 'snabbdom', 'helper'], function (SortTable, V, helper) {
|
||||
'use strict';
|
||||
V = V.default;
|
||||
|
||||
function getUptime(now, d) {
|
||||
if (d.flags.online && 'uptime' in d.statistics) {
|
||||
return Math.round(d.statistics.uptime);
|
||||
} else if (!d.flags.online && 'lastseen' in d) {
|
||||
return Math.round(-(now.unix() - d.lastseen.unix()));
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function showUptime(uptime) {
|
||||
var s = '';
|
||||
uptime /= 3600;
|
||||
|
||||
if (uptime !== undefined) {
|
||||
if (Math.abs(uptime) >= 24) {
|
||||
s = Math.round(uptime / 24) + 'd';
|
||||
} else {
|
||||
s = Math.round(uptime) + 'h';
|
||||
// 1000ms are 1 second and 60 second are 1min: 60 * 1000 = 60000
|
||||
var s = uptime / 60000;
|
||||
if (Math.abs(s) < 60) {
|
||||
return Math.round(s) + ' m';
|
||||
}
|
||||
s /= 60;
|
||||
if (Math.abs(s) < 24) {
|
||||
return Math.round(s) + ' h';
|
||||
}
|
||||
|
||||
return s;
|
||||
s /= 24;
|
||||
return Math.round(s) + ' d';
|
||||
}
|
||||
|
||||
var headings = [{
|
||||
@ -31,7 +21,7 @@ define(['sorttable', 'snabbdom', 'helper'], function (SortTable, V, helper) {
|
||||
}, {
|
||||
name: 'node.nodes',
|
||||
sort: function (a, b) {
|
||||
return a.nodeinfo.hostname.localeCompare(b.nodeinfo.hostname);
|
||||
return a.hostname.localeCompare(b.hostname);
|
||||
},
|
||||
reverse: false
|
||||
}, {
|
||||
@ -45,47 +35,43 @@ define(['sorttable', 'snabbdom', 'helper'], function (SortTable, V, helper) {
|
||||
name: 'node.links',
|
||||
class: 'ion-share-alt',
|
||||
sort: function (a, b) {
|
||||
return a.meshlinks - b.meshlinks;
|
||||
return a.neighbours.length - b.neighbours.length;
|
||||
},
|
||||
reverse: true
|
||||
}, {
|
||||
name: 'node.clients',
|
||||
class: 'ion-people',
|
||||
sort: function (a, b) {
|
||||
return ('clients' in a.statistics ? a.statistics.clients : -1) -
|
||||
('clients' in b.statistics ? b.statistics.clients : -1);
|
||||
return a.clients - b.clients;
|
||||
},
|
||||
reverse: true
|
||||
}];
|
||||
|
||||
return function (router) {
|
||||
return function () {
|
||||
function renderRow(d) {
|
||||
var td0Content = [];
|
||||
var td1Content = [];
|
||||
var aClass = ['hostname', d.flags.online ? 'online' : 'offline'];
|
||||
var td0Content = '';
|
||||
if (helper.hasLocation(d)) {
|
||||
td0Content = V.h('span', { props: { className: 'icon ion-location', title: _.t('location.location') } });
|
||||
}
|
||||
|
||||
td1Content.push(V.h('a', {
|
||||
var td1Content = V.h('a', {
|
||||
props: {
|
||||
className: aClass.join(' '),
|
||||
href: router.generateLink({ node: d.nodeinfo.node_id })
|
||||
className: ['hostname', d.is_online ? 'online' : 'offline'].join(' '),
|
||||
href: router.generateLink({ node: d.node_id })
|
||||
}, on: {
|
||||
click: function (e) {
|
||||
router.fullUrl({ node: d.nodeinfo.node_id }, e);
|
||||
router.fullUrl({ node: d.node_id }, e);
|
||||
}
|
||||
}
|
||||
}, d.nodeinfo.hostname));
|
||||
}, d.hostname);
|
||||
|
||||
if (helper.hasLocation(d)) {
|
||||
td0Content.push(V.h('span', { props: { className: 'icon ion-location' } }));
|
||||
}
|
||||
|
||||
var td0 = V.h('td', td0Content);
|
||||
var td1 = V.h('td', td1Content);
|
||||
var td2 = V.h('td', showUptime(d.uptime));
|
||||
var td3 = V.h('td', d.meshlinks.toString());
|
||||
var td4 = V.h('td', Number('clients' in d.statistics ? d.statistics.clients : 0).toFixed(0));
|
||||
|
||||
return V.h('tr', [td0, td1, td2, td3, td4]);
|
||||
return V.h('tr', [
|
||||
V.h('td', td0Content),
|
||||
V.h('td', td1Content),
|
||||
V.h('td', showUptime(d.uptime)),
|
||||
V.h('td', d.neighbours.length),
|
||||
V.h('td', d.clients)
|
||||
]);
|
||||
}
|
||||
|
||||
var table = new SortTable(headings, 1, renderRow);
|
||||
@ -101,8 +87,11 @@ define(['sorttable', 'snabbdom', 'helper'], function (SortTable, V, helper) {
|
||||
this.setData = function setData(d) {
|
||||
var data = d.nodes.all.map(function (e) {
|
||||
var n = Object.create(e);
|
||||
n.uptime = getUptime(d.now, e);
|
||||
n.meshlinks = e.meshlinks || 0;
|
||||
if (e.is_online) {
|
||||
n.uptime = d.now - new Date(e.uptime).getTime();
|
||||
} else {
|
||||
n.uptime = e.lastseen - d.now;
|
||||
}
|
||||
return n;
|
||||
});
|
||||
|
||||
|
@ -1,22 +1,21 @@
|
||||
define(['d3-interpolate', 'snabbdom', 'filters/genericnode', 'helper'],
|
||||
function (d3Interpolate, V, Filter, helper) {
|
||||
define(['d3-interpolate', 'snabbdom', 'utils/version', 'filters/genericnode', 'helper'],
|
||||
function (d3Interpolate, V, versionCompare, Filter, helper) {
|
||||
'use strict';
|
||||
|
||||
return function (config, filterManager) {
|
||||
var self = this;
|
||||
var scale = d3Interpolate.interpolate('#770038', '#dc0067');
|
||||
V = V.default;
|
||||
|
||||
return function (filterManager) {
|
||||
var self = this;
|
||||
var scale = d3Interpolate.interpolate(config.forceGraph.tqFrom, config.forceGraph.tqTo);
|
||||
var time;
|
||||
|
||||
var statusTable;
|
||||
var fwTable;
|
||||
var hwTable;
|
||||
var geoTable;
|
||||
var autoTable;
|
||||
var siteTable;
|
||||
|
||||
function showStatGlobal(o) {
|
||||
return helper.showStat(o);
|
||||
}
|
||||
var gatewayTable;
|
||||
var gateway6Table;
|
||||
var domainTable;
|
||||
|
||||
function count(nodes, key, f) {
|
||||
var dict = {};
|
||||
@ -61,14 +60,13 @@ define(['d3-interpolate', 'snabbdom', 'filters/genericnode', 'helper'],
|
||||
|
||||
var filter = new Filter(_.t(name), d[2], d[0], d[3]);
|
||||
|
||||
var a = V.h('a', { props: { href: '#' }, on: { click: addFilter(filter) } }, d[0]);
|
||||
var a = V.h('a', { on: { click: addFilter(filter) } }, d[0]);
|
||||
|
||||
var th = V.h('th', a);
|
||||
var td = V.h('td', V.h('span', {
|
||||
style: {
|
||||
width: Math.round(v * 100) + '%',
|
||||
backgroundColor: scale(v),
|
||||
color: 'white'
|
||||
width: 'calc(25px + ' + Math.round(v * 90) + '%)',
|
||||
backgroundColor: scale(v)
|
||||
}
|
||||
}, d[1].toFixed(0)));
|
||||
|
||||
@ -79,56 +77,53 @@ define(['d3-interpolate', 'snabbdom', 'filters/genericnode', 'helper'],
|
||||
}
|
||||
|
||||
self.setData = function setData(data) {
|
||||
var onlineNodes = data.nodes.all.filter(helper.online);
|
||||
var onlineNodes = data.nodes.online;
|
||||
var nodes = onlineNodes.concat(data.nodes.lost);
|
||||
var nodeDict = {};
|
||||
time = data.timestamp;
|
||||
|
||||
data.nodes.all.forEach(function (d) {
|
||||
nodeDict[d.nodeinfo.node_id] = d;
|
||||
});
|
||||
function hostnameOfNodeID(nodeid) {
|
||||
var gateway = data.nodeDict[nodeid];
|
||||
if (gateway) {
|
||||
return gateway.hostname;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
var statusDict = count(nodes, ['flags', 'online'], function (d) {
|
||||
var gatewayDict = count(nodes, ['gateway'], hostnameOfNodeID);
|
||||
var gateway6Dict = count(nodes, ['gateway6'], hostnameOfNodeID);
|
||||
|
||||
var statusDict = count(nodes, ['is_online'], function (d) {
|
||||
return d ? 'online' : 'offline';
|
||||
});
|
||||
var fwDict = count(nodes, ['nodeinfo', 'software', 'firmware', 'release']);
|
||||
var hwDict = count(nodes, ['nodeinfo', 'hardware', 'model']);
|
||||
var geoDict = count(nodes, ['nodeinfo', 'location'], function (d) {
|
||||
var fwDict = count(nodes, ['firmware', 'release']);
|
||||
var hwDict = count(nodes, ['model']);
|
||||
var geoDict = count(nodes, ['location'], function (d) {
|
||||
return d && d.longitude && d.latitude ? _.t('yes') : _.t('no');
|
||||
});
|
||||
|
||||
var autoDict = count(nodes, ['nodeinfo', 'software', 'autoupdater'], function (d) {
|
||||
if (d === null) {
|
||||
return null;
|
||||
} else if (d.enabled) {
|
||||
var autoDict = count(nodes, ['autoupdater'], function (d) {
|
||||
if (d.enabled) {
|
||||
return d.branch;
|
||||
}
|
||||
return _.t('node.deactivated');
|
||||
});
|
||||
|
||||
var siteDict = count(nodes, ['nodeinfo', 'system', 'site_code'], function (d) {
|
||||
var rt = d;
|
||||
if (config.siteNames) {
|
||||
config.siteNames.forEach(function (t) {
|
||||
if (d === t.site) {
|
||||
rt = t.name;
|
||||
var domainDict = count(nodes, ['domain'], function (d) {
|
||||
if (config.domainNames) {
|
||||
config.domainNames.some(function (t) {
|
||||
if (d === t.domain) {
|
||||
d = t.name;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
return rt;
|
||||
return d;
|
||||
});
|
||||
|
||||
statusTable = fillTable('node.status', statusTable, statusDict.sort(function (a, b) {
|
||||
return b[1] - a[1];
|
||||
}));
|
||||
fwTable = fillTable('node.firmware', fwTable, fwDict.sort(function (a, b) {
|
||||
if (b[0] < a[0]) {
|
||||
return -1;
|
||||
}
|
||||
if (b[0] > a[0]) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}));
|
||||
fwTable = fillTable('node.firmware', fwTable, fwDict.sort(versionCompare));
|
||||
hwTable = fillTable('node.hardware', hwTable, hwDict.sort(function (a, b) {
|
||||
return b[1] - a[1];
|
||||
}));
|
||||
@ -138,39 +133,54 @@ define(['d3-interpolate', 'snabbdom', 'filters/genericnode', 'helper'],
|
||||
autoTable = fillTable('node.update', autoTable, autoDict.sort(function (a, b) {
|
||||
return b[1] - a[1];
|
||||
}));
|
||||
siteTable = fillTable('node.site', siteTable, siteDict.sort(function (a, b) {
|
||||
gatewayTable = fillTable('node.selectedGatewayIPv4', gatewayTable, gatewayDict.sort(function (a, b) {
|
||||
return b[1] - a[1];
|
||||
}));
|
||||
gateway6Table = fillTable('node.selectedGatewayIPv6', gateway6Table, gateway6Dict.sort(function (a, b) {
|
||||
return b[1] - a[1];
|
||||
}));
|
||||
domainTable = fillTable('node.domain', domainTable, domainDict.sort(function (a, b) {
|
||||
return b[1] - a[1];
|
||||
}));
|
||||
};
|
||||
|
||||
self.render = function render(el) {
|
||||
var h2;
|
||||
self.renderSingle(el, 'node.status', statusTable);
|
||||
self.renderSingle(el, 'node.firmware', fwTable);
|
||||
self.renderSingle(el, 'node.hardware', hwTable);
|
||||
self.renderSingle(el, 'node.visible', geoTable);
|
||||
self.renderSingle(el, 'node.update', autoTable);
|
||||
self.renderSingle(el, 'node.site', siteTable);
|
||||
self.renderSingle(el, 'node.selectedGatewayIPv4', gatewayTable);
|
||||
self.renderSingle(el, 'node.selectedGatewayIPv6', gateway6Table);
|
||||
self.renderSingle(el, 'node.domain', domainTable);
|
||||
|
||||
if (config.globalInfos) {
|
||||
var images = document.createElement('div');
|
||||
el.appendChild(images);
|
||||
var img = [];
|
||||
var subst = {
|
||||
'{TIME}': time,
|
||||
'{LOCALE}': _.locale()
|
||||
};
|
||||
config.globalInfos.forEach(function (globalInfo) {
|
||||
h2 = document.createElement('h2');
|
||||
h2.textContent = globalInfo.name;
|
||||
el.appendChild(h2);
|
||||
el.appendChild(showStatGlobal(globalInfo));
|
||||
img.push(V.h('h2', globalInfo.name));
|
||||
img.push(helper.showStat(V, globalInfo, subst));
|
||||
});
|
||||
V.patch(images, V.h('div', img));
|
||||
}
|
||||
};
|
||||
|
||||
self.renderSingle = function renderSingle(el, heading, table) {
|
||||
if (table.children.length > 0) {
|
||||
var h2 = document.createElement('h2');
|
||||
h2.classList.add('proportion-header');
|
||||
h2.textContent = _.t(heading);
|
||||
h2.onclick = function onclick() {
|
||||
table.classList.toggle('hide');
|
||||
table.elm.classList.toggle('hide');
|
||||
};
|
||||
el.appendChild(h2);
|
||||
el.appendChild(table.elm);
|
||||
}
|
||||
};
|
||||
return self;
|
||||
};
|
||||
|
@ -15,10 +15,13 @@ define(function () {
|
||||
el.appendChild(sidebar);
|
||||
|
||||
var button = document.createElement('button');
|
||||
var visibility = new CustomEvent('visibility');
|
||||
sidebar.appendChild(button);
|
||||
|
||||
button.classList.add('sidebarhandle', 'shadow');
|
||||
button.classList.add('sidebarhandle');
|
||||
button.setAttribute('aria-label', _.t('sidebar.toggle'));
|
||||
button.onclick = function onclick() {
|
||||
button.dispatchEvent(visibility);
|
||||
sidebar.classList.toggle('hidden');
|
||||
};
|
||||
|
||||
@ -27,7 +30,7 @@ define(function () {
|
||||
sidebar.appendChild(container);
|
||||
|
||||
self.getWidth = function getWidth() {
|
||||
if (gridBreakpoints.lg[0] > window.innerWidth) {
|
||||
if (gridBreakpoints.lg[0] > window.innerWidth || sidebar.classList.contains('hidden')) {
|
||||
return 0;
|
||||
} else if (gridBreakpoints.xl[0] > window.innerWidth) {
|
||||
return gridBreakpoints.lg[1];
|
||||
@ -54,6 +57,7 @@ define(function () {
|
||||
};
|
||||
|
||||
self.container = sidebar;
|
||||
self.button = button;
|
||||
|
||||
return self;
|
||||
};
|
||||
|
@ -2,7 +2,7 @@ define(['moment', 'snabbdom', 'helper'], function (moment, V, helper) {
|
||||
'use strict';
|
||||
V = V.default;
|
||||
|
||||
return function (nodes, field, router, title) {
|
||||
return function (nodes, field, title) {
|
||||
var self = this;
|
||||
var el;
|
||||
var tbody;
|
||||
@ -34,32 +34,27 @@ define(['moment', 'snabbdom', 'helper'], function (moment, V, helper) {
|
||||
}
|
||||
|
||||
var items = list.map(function (d) {
|
||||
var time = moment(d[field]).from(data.now);
|
||||
var td0Content = [];
|
||||
var td1Content = [];
|
||||
var td0Content = '';
|
||||
if (helper.hasLocation(d)) {
|
||||
td0Content = V.h('span', { props: { className: 'icon ion-location', title: _.t('location.location') } });
|
||||
}
|
||||
|
||||
var aClass = ['hostname', d.flags.online ? 'online' : 'offline'];
|
||||
|
||||
td1Content.push(V.h('a', {
|
||||
var td1Content = V.h('a', {
|
||||
props: {
|
||||
className: aClass.join(' '),
|
||||
href: router.generateLink({ node: d.nodeinfo.node_id })
|
||||
className: ['hostname', d.is_online ? 'online' : 'offline'].join(' '),
|
||||
href: router.generateLink({ node: d.node_id })
|
||||
}, on: {
|
||||
click: function (e) {
|
||||
router.fullUrl({ node: d.nodeinfo.node_id }, e);
|
||||
router.fullUrl({ node: d.node_id }, e);
|
||||
}
|
||||
}
|
||||
}, d.nodeinfo.hostname));
|
||||
}, d.hostname);
|
||||
|
||||
if (helper.hasLocation(d)) {
|
||||
td0Content.push(V.h('span', { props: { className: 'icon ion-location' } }));
|
||||
}
|
||||
|
||||
var td0 = V.h('td', td0Content);
|
||||
var td1 = V.h('td', td1Content);
|
||||
var td2 = V.h('td', time);
|
||||
|
||||
return V.h('tr', [td0, td1, td2]);
|
||||
return V.h('tr', [
|
||||
V.h('td', td0Content),
|
||||
V.h('td', td1Content),
|
||||
V.h('td', moment(d[field]).from(data.now))
|
||||
]);
|
||||
});
|
||||
|
||||
var tbodyNew = V.h('tbody', items);
|
||||
|
@ -1,7 +1,7 @@
|
||||
define(function () {
|
||||
'use strict';
|
||||
|
||||
return function (config) {
|
||||
return function () {
|
||||
function setTitle(d) {
|
||||
var title = [config.siteName];
|
||||
|
||||
@ -17,11 +17,11 @@ define(function () {
|
||||
};
|
||||
|
||||
this.gotoNode = function gotoNode(d) {
|
||||
setTitle(d.nodeinfo.hostname);
|
||||
setTitle(d.hostname);
|
||||
};
|
||||
|
||||
this.gotoLink = function gotoLink(d) {
|
||||
setTitle((d.source.node ? d.source.node.nodeinfo.hostname : d.source.id) + ' \u21D4 ' + d.target.node.nodeinfo.hostname);
|
||||
setTitle(d[0].source.hostname + ' \u21D4 ' + d[0].target.hostname);
|
||||
};
|
||||
|
||||
this.gotoLocation = function gotoLocation() {
|
||||
|
@ -27,9 +27,9 @@ define({
|
||||
},
|
||||
|
||||
sortByKey: function sortByKey(key, d) {
|
||||
return d.slice().sort(function (a, b) {
|
||||
return a[key] - b[key];
|
||||
}).reverse();
|
||||
return d.sort(function (a, b) {
|
||||
return b[key] - a[key];
|
||||
});
|
||||
},
|
||||
|
||||
limit: function limit(key, m, d) {
|
||||
@ -48,10 +48,6 @@ define({
|
||||
return 1;
|
||||
},
|
||||
|
||||
trueDefault: function trueDefault(d) {
|
||||
return d === undefined ? true : d;
|
||||
},
|
||||
|
||||
dictGet: function dictGet(dict, key) {
|
||||
var k = key.shift();
|
||||
|
||||
@ -76,31 +72,21 @@ define({
|
||||
return s;
|
||||
},
|
||||
|
||||
/* Helpers working with nodes */
|
||||
|
||||
offline: function offline(d) {
|
||||
return !d.flags.online;
|
||||
},
|
||||
|
||||
online: function online(d) {
|
||||
return d.flags.online;
|
||||
},
|
||||
|
||||
hasLocation: function hasLocation(d) {
|
||||
return 'location' in d.nodeinfo &&
|
||||
Math.abs(d.nodeinfo.location.latitude) < 90 &&
|
||||
Math.abs(d.nodeinfo.location.longitude) < 180;
|
||||
return 'location' in d &&
|
||||
Math.abs(d.location.latitude) < 90 &&
|
||||
Math.abs(d.location.longitude) < 180;
|
||||
},
|
||||
|
||||
subtract: function subtract(a, b) {
|
||||
var ids = {};
|
||||
|
||||
b.forEach(function (d) {
|
||||
ids[d.nodeinfo.node_id] = true;
|
||||
ids[d.node_id] = true;
|
||||
});
|
||||
|
||||
return a.filter(function (d) {
|
||||
return !(d.nodeinfo.node_id in ids);
|
||||
return !ids[d.node_id];
|
||||
});
|
||||
},
|
||||
|
||||
@ -115,59 +101,35 @@ define({
|
||||
},
|
||||
|
||||
showTq: function showTq(d) {
|
||||
return (1 / d.tq * 100).toFixed(0) + '%';
|
||||
return (d * 100).toFixed(0) + '%';
|
||||
},
|
||||
|
||||
attributeEntry: function attributeEntry(el, label, value) {
|
||||
if (value === null || value === undefined) {
|
||||
return '';
|
||||
attributeEntry: function attributeEntry(V, children, label, value) {
|
||||
if (value !== undefined) {
|
||||
if (typeof value !== 'object') {
|
||||
value = V.h('td', value);
|
||||
}
|
||||
|
||||
var tr = document.createElement('tr');
|
||||
var th = document.createElement('th');
|
||||
th.textContent = _.t(label);
|
||||
tr.appendChild(th);
|
||||
|
||||
var td = document.createElement('td');
|
||||
|
||||
if (typeof value === 'function') {
|
||||
value(td);
|
||||
} else {
|
||||
td.appendChild(document.createTextNode(value));
|
||||
children.push(V.h('tr', [
|
||||
V.h('th', _.t(label)),
|
||||
value
|
||||
]));
|
||||
}
|
||||
|
||||
tr.appendChild(td);
|
||||
|
||||
el.appendChild(tr);
|
||||
|
||||
return td;
|
||||
},
|
||||
|
||||
showStat: function showStat(o, subst) {
|
||||
var content;
|
||||
subst = typeof subst !== 'undefined' ? subst : {};
|
||||
|
||||
content = document.createElement('img');
|
||||
content.src = require('helper').listReplace(o.image, subst);
|
||||
|
||||
var p = document.createElement('p');
|
||||
showStat: function showStat(V, o, subst) {
|
||||
var content = V.h('img', { attrs: { src: require('helper').listReplace(o.image, subst) } });
|
||||
|
||||
if (o.href) {
|
||||
var link = document.createElement('a');
|
||||
link.target = '_blank';
|
||||
link.href = require('helper').listReplace(o.href, subst);
|
||||
link.appendChild(content);
|
||||
|
||||
if (o.title) {
|
||||
link.title = require('helper').listReplace(o.title, subst);
|
||||
return V.h('p', V.h('a', {
|
||||
attrs:
|
||||
{
|
||||
href: require('helper').listReplace(o.href, subst),
|
||||
target: '_blank',
|
||||
title: require('helper').listReplace(o.title, subst)
|
||||
}
|
||||
|
||||
p.appendChild(link);
|
||||
} else {
|
||||
p.appendChild(content);
|
||||
}, content));
|
||||
}
|
||||
|
||||
return p;
|
||||
return V.h('p', content);
|
||||
},
|
||||
|
||||
getTileBBox: function getTileBBox(s, map, tileSize, margin) {
|
||||
@ -176,20 +138,35 @@ define({
|
||||
|
||||
return { minX: br.lat, minY: tl.lng, maxX: tl.lat, maxY: br.lng };
|
||||
},
|
||||
positionClients: function positionClients(ctx, p, startAngle, clients, startDistance) {
|
||||
if (clients === 0) {
|
||||
positionClients: function positionClients(ctx, p, startAngle, node, startDistance) {
|
||||
if (node.clients === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var radius = 3;
|
||||
var a = 1.2;
|
||||
var mode = 0;
|
||||
|
||||
for (var orbit = 0, i = 0; i < clients; orbit++) {
|
||||
ctx.beginPath();
|
||||
ctx.fillStyle = config.client.wifi24;
|
||||
|
||||
for (var orbit = 0, i = 0; i < node.clients; orbit++) {
|
||||
var distance = startDistance + orbit * 2 * radius * a;
|
||||
var n = Math.floor((Math.PI * distance) / (a * radius));
|
||||
var delta = clients - i;
|
||||
var delta = node.clients - i;
|
||||
|
||||
for (var j = 0; j < Math.min(delta, n); i++, j++) {
|
||||
if (mode !== 1 && i >= (node.clients_wifi24 + node.clients_wifi5)) {
|
||||
mode = 1;
|
||||
ctx.fill();
|
||||
ctx.beginPath();
|
||||
ctx.fillStyle = config.client.wifi5;
|
||||
} else if (mode === 0 && i >= node.clients_wifi24) {
|
||||
mode = 2;
|
||||
ctx.fill();
|
||||
ctx.beginPath();
|
||||
ctx.fillStyle = config.client.other;
|
||||
}
|
||||
var angle = 2 * Math.PI / n * j;
|
||||
var x = p.x + distance * Math.cos(angle + startAngle);
|
||||
var y = p.y + distance * Math.sin(angle + startAngle);
|
||||
@ -198,5 +175,32 @@ define({
|
||||
ctx.arc(x, y, radius, 0, 2 * Math.PI);
|
||||
}
|
||||
}
|
||||
ctx.fill();
|
||||
},
|
||||
fullscreen: function fullscreen(btn) {
|
||||
if (!document.fullscreenElement && !document.webkitFullscreenElement && !document.mozFullScreenElement) {
|
||||
var fel = document.firstElementChild;
|
||||
var func = fel.requestFullscreen
|
||||
|| fel.webkitRequestFullScreen
|
||||
|| fel.mozRequestFullScreen;
|
||||
func.call(fel);
|
||||
btn.classList.remove('ion-full-enter');
|
||||
btn.classList.add('ion-full-exit');
|
||||
} else {
|
||||
func = document.exitFullscreen
|
||||
|| document.webkitExitFullscreen
|
||||
|| document.mozCancelFullScreen;
|
||||
if (func) {
|
||||
func.call(document);
|
||||
btn.classList.remove('ion-full-exit');
|
||||
btn.classList.add('ion-full-enter');
|
||||
}
|
||||
}
|
||||
},
|
||||
escape: function escape(string) {
|
||||
return string.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
});
|
||||
|
@ -1,11 +1,12 @@
|
||||
define(['polyglot', 'moment', 'helper'], function (Polyglot, moment, helper) {
|
||||
'use strict';
|
||||
return function (config) {
|
||||
return function () {
|
||||
var router;
|
||||
|
||||
function languageSelect(el) {
|
||||
var select = document.createElement('select');
|
||||
select.className = 'language-switch';
|
||||
select.setAttribute('aria-label', 'Language');
|
||||
select.addEventListener('change', setSelectLocale);
|
||||
el.appendChild(select);
|
||||
|
||||
@ -20,13 +21,8 @@ define(['polyglot', 'moment', 'helper'], function (Polyglot, moment, helper) {
|
||||
router.fullUrl({ lang: event.target.value }, false, true);
|
||||
}
|
||||
|
||||
function setLocale(lang) {
|
||||
localStorage.setItem('language', getLocale(lang));
|
||||
location.reload();
|
||||
}
|
||||
|
||||
function getLocale(input) {
|
||||
var language = input || localStorage.getItem('language') || navigator.languages && navigator.languages[0] || navigator.language || navigator.userLanguage;
|
||||
var language = input || navigator.languages && navigator.languages[0] || navigator.language;
|
||||
var locale = config.supportedLocale[0];
|
||||
config.supportedLocale.some(function (item) {
|
||||
if (language.indexOf(item) !== -1) {
|
||||
@ -59,6 +55,7 @@ define(['polyglot', 'moment', 'helper'], function (Polyglot, moment, helper) {
|
||||
|
||||
function init(r) {
|
||||
router = r;
|
||||
/** global: _ */
|
||||
window._ = new Polyglot({ locale: getLocale(router.getLang()), allowMissing: true });
|
||||
helper.getJSON('locale/' + _.locale() + '.json?' + config.cacheBreaker).then(setTranslation);
|
||||
document.querySelector('html').setAttribute('lang', _.locale());
|
||||
@ -67,7 +64,6 @@ define(['polyglot', 'moment', 'helper'], function (Polyglot, moment, helper) {
|
||||
return {
|
||||
init: init,
|
||||
getLocale: getLocale,
|
||||
setLocale: setLocale,
|
||||
languageSelect: languageSelect
|
||||
};
|
||||
};
|
||||
|
@ -2,7 +2,7 @@ define(function () {
|
||||
var self = {};
|
||||
|
||||
self.distance = function distance(a, b) {
|
||||
return Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2);
|
||||
return (a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y);
|
||||
};
|
||||
|
||||
self.distancePoint = function distancePoint(a, b) {
|
||||
|
144
lib/utils/node.js
Normal file
@ -0,0 +1,144 @@
|
||||
define(['snabbdom', 'helper', 'moment'], function (V, helper, moment) {
|
||||
'use strict';
|
||||
V = V.default;
|
||||
|
||||
var self = {};
|
||||
|
||||
function showBar(v, width, warning) {
|
||||
return V.h('span',
|
||||
{ props: { className: 'bar' + (warning ? ' warning' : '') } },
|
||||
[
|
||||
V.h('span',
|
||||
{
|
||||
style: { width: (width * 100) + '%' }
|
||||
}),
|
||||
V.h('label', v)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
self.showStatus = function showStatus(d) {
|
||||
return V.h('td',
|
||||
{ props: { className: d.is_online ? 'online' : 'offline' } },
|
||||
_.t((d.is_online ? 'node.lastOnline' : 'node.lastOffline'), {
|
||||
time: d.lastseen.fromNow(),
|
||||
date: d.lastseen.format('DD.MM.YYYY, H:mm:ss')
|
||||
}));
|
||||
};
|
||||
|
||||
self.showGeoURI = function showGeoURI(d) {
|
||||
if (!helper.hasLocation(d)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return V.h('td',
|
||||
V.h('a',
|
||||
{ props: { href: 'geo:' + d.location.latitude + ',' + d.location.longitude } },
|
||||
Number(d.location.latitude.toFixed(6)) + ', ' + Number(d.location.longitude.toFixed(6))
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
self.showGateway = function showGateway(d) {
|
||||
return d.is_gateway ? _.t('yes') : undefined;
|
||||
};
|
||||
|
||||
self.showFirmware = function showFirmware(d) {
|
||||
return [
|
||||
helper.dictGet(d, ['firmware', 'release']),
|
||||
helper.dictGet(d, ['firmware', 'base'])
|
||||
].filter(function (n) {
|
||||
return n !== null;
|
||||
}).join(' / ') || undefined;
|
||||
};
|
||||
|
||||
self.showUptime = function showUptime(d) {
|
||||
return moment.utc(d.uptime).local().fromNow(true);
|
||||
};
|
||||
|
||||
self.showFirstSeen = function showFirstSeen(d) {
|
||||
return d.firstseen.fromNow(true);
|
||||
};
|
||||
|
||||
self.showLoad = function showLoad(d) {
|
||||
return showBar(d.loadavg.toFixed(2), d.loadavg / (d.nproc || 1), d.loadavg >= d.nproc);
|
||||
};
|
||||
|
||||
self.showRAM = function showRAM(d) {
|
||||
return showBar(Math.round(d.memory_usage * 100) + ' %', d.memory_usage, d.memory_usage >= 0.8);
|
||||
};
|
||||
|
||||
self.showDomain = function showDomain(d) {
|
||||
var rt = d.domain;
|
||||
if (config.domainNames) {
|
||||
config.domainNames.some(function (t) {
|
||||
if (rt === t.domain) {
|
||||
rt = t.name;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
return rt;
|
||||
};
|
||||
|
||||
self.showClients = function showClients(d) {
|
||||
if (!d.is_online) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
var clients = [
|
||||
V.h('span', [
|
||||
d.clients > 0 ? d.clients : _.t('none'),
|
||||
V.h('br'),
|
||||
V.h('i', { props: { className: 'ion-people', title: _.t('node.clients') } })
|
||||
]),
|
||||
V.h('span',
|
||||
{ props: { className: 'legend-24ghz' } },
|
||||
[
|
||||
d.clients_wifi24,
|
||||
V.h('br'),
|
||||
V.h('span', { props: { className: 'symbol', title: '2,4 GHz' } })
|
||||
]),
|
||||
V.h('span',
|
||||
{ props: { className: 'legend-5ghz' } },
|
||||
[
|
||||
d.clients_wifi5,
|
||||
V.h('br'),
|
||||
V.h('span', { props: { className: 'symbol', title: '5 GHz' } })
|
||||
]),
|
||||
V.h('span',
|
||||
{ props: { className: 'legend-others' } },
|
||||
[
|
||||
d.clients_other,
|
||||
V.h('br'),
|
||||
V.h('span', { props: { className: 'symbol', title: _.t('others') } })
|
||||
])
|
||||
];
|
||||
|
||||
return V.h('td', { props: { className: 'clients' } }, clients);
|
||||
};
|
||||
|
||||
self.showIPs = function showIPs(d) {
|
||||
var string = [];
|
||||
var ips = d.addresses;
|
||||
ips.sort();
|
||||
ips.forEach(function (ip, i) {
|
||||
if (i > 0) {
|
||||
string.push(V.h('br'));
|
||||
}
|
||||
|
||||
if (ip.indexOf('fe80:') !== 0) {
|
||||
string.push(V.h('a', { props: { href: 'http://[' + ip + ']/', target: '_blank' } }, ip));
|
||||
} else {
|
||||
string.push(ip);
|
||||
}
|
||||
});
|
||||
return V.h('td', string);
|
||||
};
|
||||
|
||||
self.showAutoupdate = function showAutoupdate(d) {
|
||||
return d.autoupdater.enabled ? _.t('node.activated', { branch: d.autoupdater.branch }) : _.t('node.deactivated');
|
||||
};
|
||||
|
||||
return self;
|
||||
});
|
@ -3,7 +3,7 @@ define(['Navigo'], function (Navigo) {
|
||||
|
||||
return function (language) {
|
||||
var init = false;
|
||||
var objects = { nodes: {}, links: {} };
|
||||
var objects = {};
|
||||
var targets = [];
|
||||
var views = {};
|
||||
var current = {};
|
||||
@ -16,17 +16,20 @@ define(['Navigo'], function (Navigo) {
|
||||
}
|
||||
|
||||
function gotoNode(d) {
|
||||
if (d.nodeId in objects.nodes) {
|
||||
if (objects.nodeDict[d.nodeId]) {
|
||||
targets.forEach(function (t) {
|
||||
t.gotoNode(objects.nodes[d.nodeId]);
|
||||
t.gotoNode(objects.nodeDict[d.nodeId], objects.nodeDict);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function gotoLink(d) {
|
||||
if (d.linkId in objects.links) {
|
||||
var link = objects.links.filter(function (value) {
|
||||
return value.id === d.linkId;
|
||||
});
|
||||
if (link) {
|
||||
targets.forEach(function (t) {
|
||||
t.gotoLink(objects.links[d.linkId]);
|
||||
t.gotoLink(link);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -51,7 +54,7 @@ define(['Navigo'], function (Navigo) {
|
||||
};
|
||||
|
||||
if (lang && lang !== state.lang && lang === language.getLocale(lang)) {
|
||||
language.setLocale(lang);
|
||||
location.reload();
|
||||
}
|
||||
|
||||
if (!init || viewValue && viewValue !== state.view) {
|
||||
@ -79,10 +82,10 @@ define(['Navigo'], function (Navigo) {
|
||||
}
|
||||
}
|
||||
|
||||
var router = new Navigo(null, true);
|
||||
var router = new Navigo(null, true, '#!');
|
||||
|
||||
router
|
||||
.on(/^\/?#?\/([\w]{2})?\/?(map|graph)?\/?([a-f\d]{12})?([a-f\d\-]{25})?\/?(?:(\d+)\/([\d.]+)\/([\d.]+))?$/, customRoute)
|
||||
.on(/^\/?#?!?\/([\w]{2})?\/?(map|graph)?\/?([a-f\d]{12})?([a-f\d\-]{25})?\/?(?:(\d+)\/(-?[\d.]+)\/(-?[\d.]+))?$/, customRoute)
|
||||
.on({
|
||||
'*': function () {
|
||||
router.fullUrl();
|
||||
@ -90,7 +93,7 @@ define(['Navigo'], function (Navigo) {
|
||||
});
|
||||
|
||||
router.generateLink = function generateLink(data, full, deep) {
|
||||
var result = '#';
|
||||
var result = '#!';
|
||||
|
||||
if (full) {
|
||||
data = Object.assign({}, state, data);
|
||||
@ -116,7 +119,7 @@ define(['Navigo'], function (Navigo) {
|
||||
};
|
||||
|
||||
router.getLang = function getLang() {
|
||||
var lang = location.hash.match(/^\/?#\/([\w]{2})\//);
|
||||
var lang = location.hash.match(/^\/?#!?\/([\w]{2})\//);
|
||||
if (lang) {
|
||||
state.lang = language.getLocale(lang[1]);
|
||||
return lang[1];
|
||||
@ -139,16 +142,7 @@ define(['Navigo'], function (Navigo) {
|
||||
};
|
||||
|
||||
router.setData = function setData(data) {
|
||||
objects.nodes = {};
|
||||
objects.links = {};
|
||||
|
||||
data.nodes.all.forEach(function (d) {
|
||||
objects.nodes[d.nodeinfo.node_id] = d;
|
||||
});
|
||||
|
||||
data.graph.links.forEach(function (d) {
|
||||
objects.links[d.id] = d;
|
||||
});
|
||||
objects = data;
|
||||
};
|
||||
|
||||
return router;
|
||||
|
99
lib/utils/version.js
Normal file
@ -0,0 +1,99 @@
|
||||
define(function () {
|
||||
'use strict';
|
||||
|
||||
/*
|
||||
reimplate after node-deb-version-compare under MIT
|
||||
(https://github.com/sdumetz/node-deb-version-compare)
|
||||
*/
|
||||
|
||||
function Version(v) {
|
||||
var version = /^[a-zA-Z]?([0-9]*(?=:))?:(.*)/.exec(v);
|
||||
this.epoch = (version) ? version[1] : 0;
|
||||
version = (version && version[2]) ? version[2] : v;
|
||||
version = version.split('-');
|
||||
this.debian = (version.length > 1) ? version.pop() : '';
|
||||
this.upstream = version.join('-');
|
||||
}
|
||||
|
||||
Version.prototype.compare = function (b) {
|
||||
if ((this.epoch > 0 || b.epoch > 0) && Math.sign(this.epoch - b.epoch) !== 0) {
|
||||
return Math.sign(this.epoch - b.epoch);
|
||||
}
|
||||
if (this.compareStrings(this.upstream, b.upstream) !== 0) {
|
||||
return this.compareStrings(this.upstream, b.upstream);
|
||||
}
|
||||
return this.compareStrings(this.debian, b.debian);
|
||||
};
|
||||
|
||||
Version.prototype.charCode = function (c) { // the lower the charcode the lower the version.
|
||||
// if (c === '~') {return 0;} // tilde sort before anything
|
||||
// else
|
||||
if (/[a-zA-Z]/.test(c)) {
|
||||
return c.charCodeAt(0) - 'A'.charCodeAt(0) + 1;
|
||||
} else if (/[.:+-:]/.test(c)) {
|
||||
return c.charCodeAt(0) + 'z'.charCodeAt(0) + 1;
|
||||
} // charcodes are 46..58
|
||||
return 0;
|
||||
};
|
||||
|
||||
// find index of "val" in "ar".
|
||||
Version.prototype.findIndex = function (ar, fn) {
|
||||
for (var i = 0; i < ar.length; i++) {
|
||||
if (fn(ar[i], i)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
|
||||
Version.prototype.compareChunk = function (a, b) {
|
||||
var ca = a.split('');
|
||||
var cb = b.split('');
|
||||
var diff = this.findIndex(ca, function (c, index) {
|
||||
return !(cb[index] && c === cb[index]);
|
||||
});
|
||||
if (diff === -1) {
|
||||
if (cb.length > ca.length) {
|
||||
if (cb[ca.length] === '~') {
|
||||
return 1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
return 0; // no diff found and same length
|
||||
} else if (!cb[diff]) {
|
||||
return (ca[diff] === '~') ? -1 : 1;
|
||||
}
|
||||
return (this.charCode(ca[diff]) > this.charCode(cb[diff])) ? 1 : -1;
|
||||
};
|
||||
|
||||
Version.prototype.compareStrings = function (a, b) {
|
||||
if (a === b) {
|
||||
return 0;
|
||||
}
|
||||
var parseA = /([^0-9]+|[0-9]+)/g;
|
||||
var parseB = /([^0-9]+|[0-9]+)/g;
|
||||
var ra = parseA.exec(a);
|
||||
var rb = parseB.exec(b);
|
||||
while (ra !== null && rb !== null) {
|
||||
if ((isNaN(ra[1]) || isNaN(rb[1])) && ra[1] !== rb[1]) { // a or b is not a number and they're not equal. Note : "" IS a number so both null is impossible
|
||||
return this.compareChunk(ra[1], rb[1]);
|
||||
} // both are numbers
|
||||
if (ra[1] !== rb[1]) {
|
||||
return (parseInt(ra[1], 10) > parseInt(rb[1], 10)) ? 1 : -1;
|
||||
}
|
||||
ra = parseA.exec(a);
|
||||
rb = parseB.exec(b);
|
||||
}
|
||||
if (!ra && rb) { // rb doesn't get exec-ed when ra == null
|
||||
return (rb.length > 0 && rb[1].split('')[0] === '~') ? 1 : -1;
|
||||
} else if (ra && !rb) {
|
||||
return (ra[1].split('')[0] === '~') ? -1 : 1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
return function compare(a, b) {
|
||||
var va = new Version(a[0]);
|
||||
var vb = new Version(b[0]);
|
||||
return vb.compare(va);
|
||||
};
|
||||
});
|
96
locale/cz.json
Normal file
@ -0,0 +1,96 @@
|
||||
{
|
||||
"node": {
|
||||
"all": "Všechny uzly",
|
||||
"nodes": "Uzly",
|
||||
"uptime": "Celková doba provozu",
|
||||
"links": "Odkazy",
|
||||
"clients": "Klienti",
|
||||
"distance": "Vzdálenost",
|
||||
"connectionType": "typ připojení",
|
||||
"tq": "tq",
|
||||
"lastOnline": "poslední on-line %{time} (%{date})",
|
||||
"lastOffline": "lastOffline %{time} (%{date})",
|
||||
"activated": "aktivováno (%{branch})",
|
||||
"deactivated": "deaktivováno",
|
||||
"status": "Stav",
|
||||
"firmware": "Verze firmwaru",
|
||||
"hardware": "Model hardwaru",
|
||||
"visible": "Visible on the map",
|
||||
"update": "Automatický update",
|
||||
"domain": "Domain",
|
||||
"gateway": "Brána",
|
||||
"coordinates": "Souřadnice",
|
||||
"contact": "Kontakt",
|
||||
"primaryMac": "Hlavní MAC",
|
||||
"id": "Identifikace uzlu",
|
||||
"firstSeen": "firstSeen",
|
||||
"systemLoad": "Průměrné zatížení",
|
||||
"ram": "Využití paměti",
|
||||
"ipAddresses": "IP adresa",
|
||||
"nexthop": "Další skok",
|
||||
"selectedGatewayIPv4": "vybranýGatewayIPv4",
|
||||
"selectedGatewayIPv6": "vybranýGatewayIPv6",
|
||||
"link": "Odkaz ||| Odkazy",
|
||||
"node": "Uzel ||| Uzly",
|
||||
"new": "Nové uzly",
|
||||
"missing": "Zmizelé uzly"
|
||||
},
|
||||
"location": {
|
||||
"location": "Poloha",
|
||||
"latitude": "Zeměpisná šířka",
|
||||
"longitude": "Zeměpisná délka",
|
||||
"copy": "Kopírovat"
|
||||
},
|
||||
"sidebar": {
|
||||
"nodeFilter": "nodeFilter",
|
||||
"nodes": "%{total} uzly, %{online} uzly on-line",
|
||||
"clients": "%{smart_count} klienti |||| %{smart_count} klienti",
|
||||
"gateway": " %{smart_count} gateway |||| %{smart_count} gateways",
|
||||
"lastUpdate": "Poslední update",
|
||||
"nodeNew": "nodeNew",
|
||||
"nodeOnline": "Uzel je online",
|
||||
"nodeOffline": "Uzel je offline",
|
||||
"aboutInfo": "aboutInfo",
|
||||
"actual": "aktuální",
|
||||
"stats": "Statistika",
|
||||
"about": "O produktu",
|
||||
"toggle": "přepínat"
|
||||
},
|
||||
"button": {
|
||||
"switchView": "Přepnout zobrazení",
|
||||
"location": "Vybrat souřadnice",
|
||||
"tracking": "Lokalizace"
|
||||
},
|
||||
"momentjs": {
|
||||
"calendar": {
|
||||
"sameDay": "[Today at] LT",
|
||||
"nextDay": "[Tomorrow at] LT",
|
||||
"nextWeek": "dddd [at] LT",
|
||||
"lastDay": "[Yesterday at] LT",
|
||||
"lastWeek": "[Last] dddd [at] LT",
|
||||
"sameElse": "L"
|
||||
},
|
||||
"relativeTime": {
|
||||
"future": "in %s",
|
||||
"past": "%s ago",
|
||||
"s": "Několik sekund",
|
||||
"m": "minuta",
|
||||
"mm": "%d minut",
|
||||
"h": "an hour",
|
||||
"hh": "%d hodin",
|
||||
"d": "den",
|
||||
"dd": "%d dnů",
|
||||
"M": "měsíc",
|
||||
"MM": "%d měsíců",
|
||||
"y": "rok",
|
||||
"yy": "%d let"
|
||||
}
|
||||
},
|
||||
"yes": "ano",
|
||||
"no": "ne",
|
||||
"unknown": "neznámý",
|
||||
"others": "ostatní",
|
||||
"none": "žádný",
|
||||
"remove": "odstranit",
|
||||
"close": "zavřít"
|
||||
}
|
@ -6,6 +6,7 @@
|
||||
"links": "Verbindungen",
|
||||
"clients": "Nutzer",
|
||||
"distance": "Entfernung",
|
||||
"connectionType": "Verbindungsart",
|
||||
"tq": "Übertragungsqualität",
|
||||
"lastOnline": "online, letzte Nachricht %{time} (%{date})",
|
||||
"lastOffline": "offline, letzte Nachricht %{time} (%{date})",
|
||||
@ -16,17 +17,19 @@
|
||||
"hardware": "Geräte-Modell",
|
||||
"visible": "Auf der Karte sichtbar",
|
||||
"update": "Auto-Update",
|
||||
"site": "Site",
|
||||
"domain": "Domain",
|
||||
"gateway": "Gateway",
|
||||
"coordinates": "Koordinaten",
|
||||
"contact": "Kontakt",
|
||||
"primaryMac": "Primäre MAC",
|
||||
"id": "Knoten ID",
|
||||
"firstSeen": "Erstmals gesehen",
|
||||
"systemLoad": "Load average",
|
||||
"systemLoad": "Systemlast",
|
||||
"ram": "Speicherauslastung",
|
||||
"ipAddresses": "IP Adressen",
|
||||
"selectedGateway": "Gewähltes Gateway",
|
||||
"nexthop": "Nächster Sprung",
|
||||
"selectedGatewayIPv4": "Gewähltes ipv4 Gateway",
|
||||
"selectedGatewayIPv6": "Gewähltes ipv6 Gateway",
|
||||
"link": "Verbindung |||| Verbindungen",
|
||||
"node": "Knoten",
|
||||
"new": "Neue Knoten",
|
||||
@ -44,18 +47,20 @@
|
||||
"clients": "mit %{smart_count} Nutzer |||| mit %{smart_count} Nutzern",
|
||||
"gateway": "auf %{smart_count} Gateway |||| auf %{smart_count} Gateways",
|
||||
"lastUpdate": "Letzte Aktualisierung",
|
||||
"nodeNew": "Knoten ist neu",
|
||||
"nodeOnline": "Knoten ist online",
|
||||
"nodeOffline": "Knoten ist offline",
|
||||
"nodeNew": "neu",
|
||||
"nodeOnline": "online",
|
||||
"nodeOffline": "offline",
|
||||
"aboutInfo": "<h2>Über Meshviewer</h2><p>Mit Doppelklick kann man in die Karte hinein zoomen und Shift+Doppelklick heraus zoomen.</p>",
|
||||
"actual": "Aktuell",
|
||||
"stats": "Statistiken",
|
||||
"about": "Über"
|
||||
"about": "Über",
|
||||
"toggle": "Seitenleiste anzeigen/ausblenden"
|
||||
},
|
||||
"button": {
|
||||
"switchView": "Ansicht wechseln",
|
||||
"location": "Koordinaten wählen",
|
||||
"tracking": "Lokalisierung"
|
||||
"tracking": "Lokalisierung",
|
||||
"fullscreen": "Vollbildmodus wechseln"
|
||||
},
|
||||
"momentjs": {
|
||||
"calendar": {
|
||||
@ -85,5 +90,8 @@
|
||||
"yes": "ja",
|
||||
"no": "nein",
|
||||
"unknown": "unbekannt",
|
||||
"none": "keine"
|
||||
"others": "andere",
|
||||
"none": "keine",
|
||||
"remove": "entfernen",
|
||||
"close": "schließen"
|
||||
}
|
||||
|
@ -6,6 +6,7 @@
|
||||
"links": "Links",
|
||||
"clients": "Clients",
|
||||
"distance": "Distance",
|
||||
"connectionType": "Connection type",
|
||||
"tq": "Transmit quality",
|
||||
"lastOnline": "online, last message %{time} (%{date})",
|
||||
"lastOffline": "offline, last message %{time} (%{date})",
|
||||
@ -16,7 +17,7 @@
|
||||
"hardware": "Hardware model",
|
||||
"visible": "Visible on the map",
|
||||
"update": "Auto update",
|
||||
"site": "Site",
|
||||
"domain": "Domain",
|
||||
"gateway": "Gateway",
|
||||
"coordinates": "Coordinates",
|
||||
"contact": "Contact",
|
||||
@ -26,7 +27,9 @@
|
||||
"systemLoad": "Load average",
|
||||
"ram": "Memory usage",
|
||||
"ipAddresses": "IP addresses",
|
||||
"selectedGateway": "Selected gateway",
|
||||
"nexthop": "Nexthop",
|
||||
"selectedGatewayIPv4": "Selected ipv4-gateway",
|
||||
"selectedGatewayIPv6": "Selected ipv6-gateway",
|
||||
"link": "Link |||| Links",
|
||||
"node": "Node |||| Nodes",
|
||||
"new": "New nodes",
|
||||
@ -44,18 +47,20 @@
|
||||
"clients": "with %{smart_count} client |||| with %{smart_count} clients",
|
||||
"gateway": "on %{smart_count} gateway |||| on %{smart_count} gateways",
|
||||
"lastUpdate": "Last update",
|
||||
"nodeNew": "Node is new",
|
||||
"nodeOnline": "Node is online",
|
||||
"nodeOffline": "Node is offline",
|
||||
"nodeNew": "new",
|
||||
"nodeOnline": "online",
|
||||
"nodeOffline": "offline",
|
||||
"aboutInfo": "<h2>About Meshviewer</h2> <p>You can zoom in with double-click and zoom out with shift+double-click</p>",
|
||||
"actual": "Current",
|
||||
"stats": "Statistics",
|
||||
"about": "About"
|
||||
"about": "About",
|
||||
"toggle": "Toggle Sidebar"
|
||||
},
|
||||
"button": {
|
||||
"switchView": "Switch view",
|
||||
"location": "Pick coordinates",
|
||||
"tracking": "Localisation"
|
||||
"tracking": "Localisation",
|
||||
"fullscreen": "Toggle fullscreen"
|
||||
},
|
||||
"momentjs": {
|
||||
"calendar": {
|
||||
@ -85,5 +90,8 @@
|
||||
"yes": "yes",
|
||||
"no": "no",
|
||||
"unknown": "unknown",
|
||||
"none": "none"
|
||||
"others": "other",
|
||||
"none": "none",
|
||||
"remove": "remove",
|
||||
"close": "close"
|
||||
}
|
||||
|
@ -6,6 +6,7 @@
|
||||
"links": "Connexion",
|
||||
"clients": "Clients",
|
||||
"distance": "Distance",
|
||||
"connectionType": "Type de connexion",
|
||||
"tq": "Qualité de transmission",
|
||||
"lastOnline": "en ligne, dernier message %{time} (%{date})",
|
||||
"lastOffline": "hors ligne, dernier message %{time} (%{date})",
|
||||
@ -16,7 +17,7 @@
|
||||
"hardware": "Modèle matériel",
|
||||
"visible": "Visible sur la carte",
|
||||
"update": "Mise à jour automatique",
|
||||
"site": "Site",
|
||||
"domain": "Domain",
|
||||
"gateway": "Passerelle",
|
||||
"coordinates": "Coordonnées",
|
||||
"contact": "Contact",
|
||||
@ -26,7 +27,9 @@
|
||||
"systemLoad": "Charge moyenne",
|
||||
"ram": "Utilisation de la mémoire",
|
||||
"ipAddresses": "Adresse IP",
|
||||
"selectedGateway": "Passerelle sélectionné",
|
||||
"nexthop": "Nexthop",
|
||||
"selectedGatewayIPv4": "Selected ipv4-gateway",
|
||||
"selectedGatewayIPv6": "Selected ipv6-gateway",
|
||||
"link": "Connexion |||| Connexions",
|
||||
"node": "Nœud |||| Nœuds",
|
||||
"new": "Nouveaux nœuds",
|
||||
@ -50,7 +53,8 @@
|
||||
"aboutInfo": "<h2>Sur Meshviewer</h2> <p>Vous pouvez zoomer avec double-clic et effectuer un zoom arrière avec shift + double-clic</p>",
|
||||
"actual": "Actuel",
|
||||
"stats": "Statistiques",
|
||||
"about": "À propros"
|
||||
"about": "À propros",
|
||||
"toggle": "Toggle Sidebar"
|
||||
},
|
||||
"button": {
|
||||
"switchView": "Basculer l’affichage",
|
||||
@ -85,5 +89,8 @@
|
||||
"yes": "oui",
|
||||
"no": "non",
|
||||
"unknown": "inconnu",
|
||||
"none": "aucun"
|
||||
"others": "autres",
|
||||
"none": "aucun",
|
||||
"remove": "supprimer",
|
||||
"close": "fermer"
|
||||
}
|
||||
|
@ -6,6 +6,7 @@
|
||||
"links": "Ссылки",
|
||||
"clients": "Клиенты",
|
||||
"distance": "Расстояние",
|
||||
"connectionType": "Тип подключения",
|
||||
"tq": "Качество связи",
|
||||
"lastOnline": "в сети, последнее сообщение %{time} (%{date})",
|
||||
"lastOffline": "не в сети, последнее сообщение %{time} (%{date})",
|
||||
@ -16,7 +17,7 @@
|
||||
"hardware": "Тип оборудования",
|
||||
"visible": "Видно на карте",
|
||||
"update": "Автообновление",
|
||||
"site": "Сайт",
|
||||
"domain": "Сайт",
|
||||
"gateway": "Шлюз",
|
||||
"coordinates": "Координаты",
|
||||
"contact": "Контакты",
|
||||
@ -26,7 +27,9 @@
|
||||
"systemLoad": "Средняя загрузка",
|
||||
"ram": "Используемая память",
|
||||
"ipAddresses": "IP адреса",
|
||||
"selectedGateway": "Выбранный шлюз",
|
||||
"nexthop": "Следующий скачок",
|
||||
"selectedGatewayIPv4": "Выбранный шлюз ipv4",
|
||||
"selectedGatewayIPv6": "Выбранный шлюз ipv6",
|
||||
"link": "Ссылка |||| Ссылки",
|
||||
"node": "Узел |||| Узлы",
|
||||
"new": "Новые узлы",
|
||||
@ -50,7 +53,8 @@
|
||||
"aboutInfo": "<h2>О Meshviewer</h2> <p>Вы можете увеличить масштаб двойным щелчком мыши и уменьшить с shift + двойной щелчок</p>",
|
||||
"actual": "Текущее",
|
||||
"stats": "Статистика",
|
||||
"about": "О продукте"
|
||||
"about": "О продукте",
|
||||
"toggle": "Включить панель"
|
||||
},
|
||||
"button": {
|
||||
"switchView": "Переключить вид",
|
||||
@ -85,5 +89,8 @@
|
||||
"yes": "да",
|
||||
"no": "нет",
|
||||
"unknown": "неизвестно",
|
||||
"none": "нет"
|
||||
"others": "другие",
|
||||
"none": "нет",
|
||||
"remove": "убрать",
|
||||
"close": "закрыть"
|
||||
}
|
||||
|
96
locale/tr.json
Normal file
@ -0,0 +1,96 @@
|
||||
{
|
||||
"node": {
|
||||
"all": "Bütün düğümler",
|
||||
"nodes": "Düğümler",
|
||||
"uptime": "Çalışma süresi",
|
||||
"links": "Bağlantılar",
|
||||
"clients": "Müşteriler",
|
||||
"distance": "Mesafe",
|
||||
"connectionType": "Bağlantı türü",
|
||||
"tq": "İletim kalitesi",
|
||||
"lastOnline": "çevrimiçi, son mesaj %{time} (%{date})",
|
||||
"lastOffline": "çevrimdışı, son mesaj %{time} (%{date})",
|
||||
"activated": "aktif (%{branch})",
|
||||
"deactivated": "devredışı bırakıldı",
|
||||
"status": "Durum",
|
||||
"firmware": "Yazılım versiyonu",
|
||||
"hardware": "Donanım modeli",
|
||||
"visible": "Harita üzerinde görünür",
|
||||
"update": "Otomatik güncelleme",
|
||||
"domain": "Domain",
|
||||
"gateway": "Geçit",
|
||||
"coordinates": "Koordinatlar",
|
||||
"contact": "İlişki",
|
||||
"primaryMac": "Birincil MAC",
|
||||
"id": "Düğüm kimliği",
|
||||
"firstSeen": "İlk görülme",
|
||||
"systemLoad": "Ortalama yük",
|
||||
"ram": "Bellek kullanımı",
|
||||
"ipAddresses": "IP adresleri",
|
||||
"nexthop": "Bir sonraki atlama",
|
||||
"selectedGatewayIPv4": "Seçili Ipv4-ağ geçidi",
|
||||
"selectedGatewayIPv6": "Seçili Ipv6-ağ geçidi",
|
||||
"link": "Bağlantı ||| Bağlantılar",
|
||||
"node": "Düğüm ||| Düğümler",
|
||||
"new": "Yeni düğümler",
|
||||
"missing": "Kaybolan düğümler"
|
||||
},
|
||||
"location": {
|
||||
"location": "Konum",
|
||||
"latitude": "Enlem",
|
||||
"longitude": "Boylam",
|
||||
"copy": "Kopya"
|
||||
},
|
||||
"sidebar": {
|
||||
"nodeFilter": "Düğüm Filtresi",
|
||||
"nodes": "%{total} düğümler, %{online} çevrimiçi düğümler dahil",
|
||||
"clients": "%{smart_count} müşteri ile |||| %{smart_count} müşteriler ile",
|
||||
"gateway": "%{smart_count} geçit üzerinde |||| %{smart_count} geçitler üzerinde",
|
||||
"lastUpdate": "Son güncelleme",
|
||||
"nodeNew": "yeni",
|
||||
"nodeOnline": "çevrimiçi",
|
||||
"nodeOffline": "çevrimdışı",
|
||||
"aboutInfo": "<h2>Meshviewer Hakkında</h2> <p>Çift tıklayarak yakınlaştırabilir ve Shift tuşuna basıp+çift tıklayarak uzaklaştırabilirsiniz</p>",
|
||||
"actual": "Mevcut",
|
||||
"stats": "İstatistikler",
|
||||
"about": "Hakkında",
|
||||
"toggle": "Kenar çubuğunu değiştir"
|
||||
},
|
||||
"button": {
|
||||
"switchView": "Görünümü Değiştir",
|
||||
"location": "Koordinatları seç",
|
||||
"tracking": "Yerelleştirme"
|
||||
},
|
||||
"momentjs": {
|
||||
"calendar": {
|
||||
"sameDay": "[Bugün] LT",
|
||||
"nextDay": "[Yarın] LT",
|
||||
"nextWeek": "dddd [at] LT",
|
||||
"lastDay": "[Dün] LT",
|
||||
"lastWeek": "[Last] dddd [at] LT",
|
||||
"sameElse": "L"
|
||||
},
|
||||
"relativeTime": {
|
||||
"future": "%s içinde",
|
||||
"past": "%s önce",
|
||||
"s": "birkaç saniye",
|
||||
"m": "bir dakika",
|
||||
"mm": "%d dakikalar",
|
||||
"h": "bir saat",
|
||||
"hh": "%d saatler",
|
||||
"d": "bir gün",
|
||||
"dd": "%d günler",
|
||||
"M": "bir ay",
|
||||
"MM": "%d aylar",
|
||||
"y": "bir yıl",
|
||||
"yy": "%d yıllar"
|
||||
}
|
||||
},
|
||||
"yes": "evet",
|
||||
"no": "hayır",
|
||||
"unknown": "bilinmeyen",
|
||||
"others": "diğer",
|
||||
"none": "hiçbiri",
|
||||
"remove": "kaldır",
|
||||
"close": "kapat"
|
||||
}
|
77
package.json
@ -1,5 +1,6 @@
|
||||
{
|
||||
"name": "ffrgb-meshviewer",
|
||||
"name": "meshviewer",
|
||||
"version": "11.1.0",
|
||||
"license": "AGPL-3.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -9,28 +10,30 @@
|
||||
"url": "https://github.com/ffrgb/meshviewer/issues"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-eslint": "^7.2.2",
|
||||
"browser-sync": "^2.18.8",
|
||||
"eslint": "^3.19.0",
|
||||
"eslint-config-airbnb-es5": "^1.1.0",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"browser-sync": "^2.26.5",
|
||||
"del": "^5.1.0",
|
||||
"eslint": "^6.5.1",
|
||||
"eslint-config-airbnb-es5": "^1.2.0",
|
||||
"eslint-config-defaults": "^9.0.0",
|
||||
"eslint-plugin-react": "^6.10.3",
|
||||
"gulp": "github:gulpjs/gulp#4.0",
|
||||
"gulp-autoprefixer": "^3.1.1",
|
||||
"gulp-cache-bust": "^1.1.0",
|
||||
"eslint-plugin-react": "^7.12.4",
|
||||
"gulp": "^4.0.1",
|
||||
"gulp-autoprefixer": "^7.0.1",
|
||||
"gulp-cache-bust": "^1.4.0",
|
||||
"gulp-cli": "^2.2.0",
|
||||
"gulp-environments": "^0.1.2",
|
||||
"gulp-eslint": "^3.0.1",
|
||||
"gulp-htmlmin": "^3.0.0",
|
||||
"gulp-inject": "^4.2.0",
|
||||
"gulp-jsonminify": "^1.0.0",
|
||||
"gulp-kyh-inline-source": "^3.0.2",
|
||||
"gulp-load-plugins": "^1.5.0",
|
||||
"gulp-real-favicon": "^0.2.2",
|
||||
"gulp-requirejs-optimize": "^1.2.0",
|
||||
"gulp-sass": "^3.1.0",
|
||||
"gulp-sass-lint": "^1.3.2",
|
||||
"gulp-sourcemaps": "^2.6.0",
|
||||
"gulp-uglify": "^2.1.2"
|
||||
"gulp-eslint": "^6.0.0",
|
||||
"gulp-htmlmin": "^5.0.1",
|
||||
"gulp-inject": "^5.0.2",
|
||||
"gulp-inline-source": "^4.0.0",
|
||||
"gulp-jsonminify": "^1.1.0",
|
||||
"gulp-load-plugins": "^2.0.1",
|
||||
"gulp-real-favicon": "^0.3.2",
|
||||
"gulp-requirejs-optimize": "^1.3.0",
|
||||
"gulp-sass": "^4.0.2",
|
||||
"gulp-sass-lint": "^1.4.0",
|
||||
"gulp-sourcemaps": "^2.6.5",
|
||||
"gulp-uglify": "^3.0.2"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"env": {
|
||||
@ -42,17 +45,23 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"almond": "^0.3.3",
|
||||
"d3-drag": "^1.0.4",
|
||||
"d3-force": "^1.0.6",
|
||||
"d3-selection": "^1.0.5",
|
||||
"d3-zoom": "^1.1.3",
|
||||
"leaflet": "^1.0.3",
|
||||
"moment": "^2.17.1",
|
||||
"navigo": "^4.6.0",
|
||||
"node-polyglot": "^2.2.2",
|
||||
"promise-polyfill": "^6.0.2",
|
||||
"rbush": "^2.0.1",
|
||||
"requirejs": "^2.3.2",
|
||||
"snabbdom": "^0.6.4"
|
||||
}
|
||||
"d3-drag": "^1.2.4",
|
||||
"d3-force": "^1.2.1",
|
||||
"d3-selection": "^1.4.0",
|
||||
"d3-zoom": "^1.8.3",
|
||||
"leaflet": "^1.5.1",
|
||||
"moment": "^2.24.0",
|
||||
"navigo": "^7.1.2",
|
||||
"node-polyglot": "2.2.2",
|
||||
"promise-polyfill": "^8.1.3",
|
||||
"rbush": "^3.0.1",
|
||||
"requirejs": "^2.3.6",
|
||||
"snabbdom": "^0.7.3"
|
||||
},
|
||||
"scripts": {
|
||||
"gulp": "./node_modules/gulp-cli/bin/gulp.js"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1% in DE"
|
||||
]
|
||||
}
|
||||
|
72
polyfill.js
@ -7,56 +7,8 @@ if (!String.prototype.includes) {
|
||||
};
|
||||
}
|
||||
|
||||
if (!String.prototype.startsWith) {
|
||||
String.prototype.startsWith = function (searchString, position) {
|
||||
position = position || 0;
|
||||
return this.substr(position, searchString.length) === searchString;
|
||||
};
|
||||
}
|
||||
|
||||
if (!String.prototype.repeat) {
|
||||
String.prototype.repeat = function (count) {
|
||||
'use strict';
|
||||
if (this === null) {
|
||||
throw new TypeError('can\'t convert ' + this + ' to object');
|
||||
}
|
||||
var str = '' + this;
|
||||
count = +count;
|
||||
if (count < 0) {
|
||||
throw new RangeError('repeat count must be non-negative');
|
||||
}
|
||||
if (count === Infinity) {
|
||||
throw new RangeError('repeat count must be less than infinity');
|
||||
}
|
||||
count = Math.floor(count);
|
||||
if (str.length === 0 || count === 0) {
|
||||
return '';
|
||||
}
|
||||
// Ensuring count is a 31-bit integer allows us to heavily optimize the
|
||||
// main part. But anyway, most current (August 2014) browsers can't handle
|
||||
// strings 1 << 28 chars or longer, so:
|
||||
if (str.length * count >= 1 << 28) {
|
||||
throw new RangeError('repeat count must not overflow maximum string size');
|
||||
}
|
||||
var rpt = '';
|
||||
for (; ;) {
|
||||
if ((count & 1) === 1) {
|
||||
rpt += str;
|
||||
}
|
||||
count >>>= 1;
|
||||
if (count === 0) {
|
||||
break;
|
||||
}
|
||||
str += str;
|
||||
}
|
||||
// Could we try:
|
||||
// return Array(count + 1).join(this);
|
||||
return rpt;
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof Object.assign !== 'function') {
|
||||
Object.assign = function(target, varArgs) { // .length of function is 2
|
||||
Object.assign = function (target, varArgs) { // .length of function is 2
|
||||
if (target == null) { // TypeError if undefined or null
|
||||
throw new TypeError('Cannot convert undefined or null to object');
|
||||
}
|
||||
@ -78,3 +30,25 @@ if (typeof Object.assign !== 'function') {
|
||||
return to;
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
(function () {
|
||||
if (typeof window.CustomEvent === 'function') {
|
||||
return false;
|
||||
}
|
||||
|
||||
function CustomEvent(event, params) {
|
||||
params = params || { bubbles: false, cancelable: false, detail: undefined };
|
||||
var evt = document.createEvent('CustomEvent');
|
||||
evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
|
||||
return evt;
|
||||
}
|
||||
|
||||
CustomEvent.prototype = window.Event.prototype;
|
||||
|
||||
window.CustomEvent = CustomEvent;
|
||||
})();
|
||||
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.register('service-worker.js');
|
||||
}
|
||||
|
@ -3,7 +3,6 @@
|
||||
@import 'custom/variables';
|
||||
|
||||
// Mixins
|
||||
@import 'mixins/shadow';
|
||||
@import 'mixins/icon';
|
||||
@import 'mixins/font';
|
||||
|
||||
|
@ -1,14 +0,0 @@
|
||||
// Original is in LESS and can be found here: https://gist.github.com/gefangenimnetz/3ef3e18364edf105c5af
|
||||
@mixin shadow($level: 1) {
|
||||
@if $level == 1 {
|
||||
box-shadow: 0 1px 3px transparentize($color-black, .88), 0 1px 2px transparentize($color-black, .76);
|
||||
} @else if $level == 2 {
|
||||
box-shadow: 0 3px 6px transparentize($color-black, .84), 0 3px 6px transparentize($color-black, .77);
|
||||
} @else if $level == 3 {
|
||||
box-shadow: 0 10px 20px transparentize($color-black, .81), 0 6px 6px transparentize($color-black, .77);
|
||||
} @else if $level == 4 {
|
||||
box-shadow: 0 14px 28px transparentize($color-black, .75), 0 10px 10px transparentize($color-black, .78);
|
||||
} @else if $level == 5 {
|
||||
box-shadow: 0 19px 38px transparentize($color-black, .7), 0 15px 12px transparentize($color-black, .78);
|
||||
}
|
||||
}
|
@ -13,6 +13,12 @@ header {
|
||||
border-bottom: 1px solid darken($color-white, 10%);
|
||||
}
|
||||
|
||||
textarea,
|
||||
input {
|
||||
background: transparent;
|
||||
color: $color-black, 100;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
@ -22,11 +28,7 @@ h6 {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
padding: .67em 0;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
font-size: 1.5em;
|
||||
padding: .83em 0;
|
||||
@ -37,6 +39,7 @@ h3 {
|
||||
padding: 1em 0;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
padding-left: $button-distance;
|
||||
@ -57,6 +60,10 @@ img {
|
||||
a {
|
||||
color: $color-online;
|
||||
text-decoration: none;
|
||||
|
||||
&:focus {
|
||||
color: darken($color-online, 15%);
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
@ -70,3 +77,15 @@ strong {
|
||||
.hide {
|
||||
display: none !important; // sass-lint:disable-line no-important
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
border: 0;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
clip-path: inset(50%);
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
white-space: nowrap;
|
||||
width: 1px;
|
||||
}
|
||||
|
@ -29,7 +29,8 @@ button {
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
&.active,
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 2px $color-primary;
|
||||
}
|
||||
|
||||
@ -37,20 +38,6 @@ button {
|
||||
color: $color-primary;
|
||||
}
|
||||
|
||||
@if $shadows == 1 {
|
||||
&.shadow {
|
||||
@include shadow(1);
|
||||
|
||||
&:hover {
|
||||
@include shadow(2);
|
||||
}
|
||||
|
||||
&:active {
|
||||
box-shadow: inset 0 5px 20px transparentize($color-black, .81), inset 0 3px 6px transparentize($color-black, .77);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tooltip
|
||||
&[data-tooltip] {
|
||||
&::after {
|
||||
@ -78,9 +65,6 @@ button {
|
||||
&.close {
|
||||
background-color: transparent;
|
||||
border-radius: 0;
|
||||
@if $shadows == 1 {
|
||||
box-shadow: none;
|
||||
}
|
||||
color: transparentize($color-black, .5);
|
||||
float: right;
|
||||
font-size: $button-font-size;
|
||||
@ -90,3 +74,33 @@ button {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
// Tooltip
|
||||
// sass-lint:disable-block nesting-depth
|
||||
.content,
|
||||
.sidebar > {
|
||||
button {
|
||||
&[aria-label] {
|
||||
&::after {
|
||||
background: $color-black;
|
||||
border-radius: 3px;
|
||||
color: $color-white;
|
||||
content: attr(aria-label);
|
||||
font-family: $font-family;
|
||||
font-size: $font-size;
|
||||
padding: 0 12px;
|
||||
position: absolute;
|
||||
transform: translate(45px, 52px);
|
||||
visibility: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&::after {
|
||||
transition: visibility 0s linear .3s;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -57,6 +57,10 @@
|
||||
outline: none;
|
||||
padding: 0 2px;
|
||||
width: 100%;
|
||||
|
||||
&:focus {
|
||||
background: transparentize($color-primary, .95);
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
|
@ -1,7 +1,30 @@
|
||||
.infobox {
|
||||
.clients {
|
||||
color: $color-online;
|
||||
font-family: $font-family-icons;
|
||||
.clients,
|
||||
.gateway {
|
||||
display: flex;
|
||||
flex-flow: wrap;
|
||||
|
||||
span {
|
||||
flex-grow: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ion-people,
|
||||
.ion-arrow-right-c {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.node-links {
|
||||
table-layout: fixed;
|
||||
|
||||
th,
|
||||
td {
|
||||
&:nth-child(3),
|
||||
&:nth-child(5) {
|
||||
width: 12%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input,
|
||||
|
@ -1,5 +1,5 @@
|
||||
header {
|
||||
h2 {
|
||||
h1 {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
@ -17,13 +17,23 @@ header {
|
||||
}
|
||||
|
||||
.legend {
|
||||
.symbol {
|
||||
a {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
span {
|
||||
&:not(:first-child) {
|
||||
margin-left: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.symbol {
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
height: 1em;
|
||||
vertical-align: -5%;
|
||||
width: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
// Dot looks compared to thin font a bit darker - lighten it 10%
|
||||
@ -45,7 +55,20 @@ header {
|
||||
}
|
||||
}
|
||||
|
||||
.legend-online,
|
||||
.legend-offline {
|
||||
margin-left: 1em;
|
||||
.legend-24ghz {
|
||||
.symbol {
|
||||
background-color: $color-24ghz;
|
||||
}
|
||||
}
|
||||
|
||||
.legend-5ghz {
|
||||
.symbol {
|
||||
background-color: $color-5ghz;
|
||||
}
|
||||
}
|
||||
|
||||
.legend-others {
|
||||
.symbol {
|
||||
background-color: $color-others;
|
||||
}
|
||||
}
|
||||
|
@ -16,9 +16,10 @@
|
||||
}
|
||||
|
||||
@media screen and (max-width: map-get($grid-breakpoints, lg) - 1) {
|
||||
right: -1rem;
|
||||
right: .1rem;
|
||||
top: 0;
|
||||
transform: scale(.8);
|
||||
transform-origin: right;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -16,6 +16,7 @@
|
||||
background: $color-new;
|
||||
display: inline-block;
|
||||
height: 1.4em;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
label {
|
||||
|
@ -16,9 +16,14 @@
|
||||
|
||||
span {
|
||||
box-sizing: border-box;
|
||||
color: $color-white;
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
min-width: 1.5em;
|
||||
padding: .25em .5em;
|
||||
}
|
||||
|
||||
a {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,13 @@
|
||||
.sidebarhandle {
|
||||
left: $button-distance;
|
||||
transform: scale(-1, 1);
|
||||
|
||||
// sass-lint:disable-block nesting-depth
|
||||
&[aria-label] {
|
||||
&::after {
|
||||
transform: scale(-1, 1) translate(105px, 52px) !important; // sass-lint:disable-line no-important
|
||||
}
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: map-get($grid-breakpoints, lg) - 1) {
|
||||
width: auto;
|
||||
@ -23,7 +30,8 @@
|
||||
}
|
||||
|
||||
.node-list,
|
||||
.node-links {
|
||||
.node-links,
|
||||
.link-list {
|
||||
th,
|
||||
td {
|
||||
&:first-child {
|
||||
@ -40,36 +48,29 @@
|
||||
}
|
||||
}
|
||||
|
||||
.link-list {
|
||||
th,
|
||||
td {
|
||||
&:nth-child(2) {
|
||||
width: 60%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.node-links {
|
||||
padding-bottom: 15px;
|
||||
|
||||
th,
|
||||
td {
|
||||
&:first-child {
|
||||
width: 50px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.link-list {
|
||||
th,
|
||||
td {
|
||||
&:nth-child(1) {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
width: 66%;
|
||||
width: 35px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
@if $shadows == 1 {
|
||||
@include shadow(2);
|
||||
} @else {
|
||||
border-right: 1px solid darken($color-white, 10%);
|
||||
}
|
||||
background: transparentize($color-white, .03);
|
||||
border-right: 1px solid darken($color-white, 10%);
|
||||
min-height: 100vh;
|
||||
overflow-y: visible;
|
||||
|
||||
@ -91,9 +92,6 @@
|
||||
.container,
|
||||
.infobox {
|
||||
border-radius: 0;
|
||||
@if $shadows == 1 {
|
||||
box-shadow: none;
|
||||
}
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
@ -120,13 +118,19 @@
|
||||
left: $sidebar-width + 2 * $button-distance;
|
||||
position: fixed;
|
||||
top: $button-distance;
|
||||
transition: left .5s, box-shadow .5s, color .5s, transform .5s;
|
||||
transition: left .5s, color .5s, transform .5s;
|
||||
z-index: 1010;
|
||||
|
||||
&::after {
|
||||
&::before {
|
||||
content: '\f124';
|
||||
padding-right: .125em;
|
||||
}
|
||||
|
||||
&[aria-label] {
|
||||
&::after {
|
||||
transform: translate(-45px, 52px) !important; // sass-lint:disable-line no-important
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.online {
|
||||
@ -136,7 +140,3 @@
|
||||
.offline {
|
||||
color: $color-offline;
|
||||
}
|
||||
|
||||
.unseen {
|
||||
color: $color-unseen;
|
||||
}
|
||||
|
@ -22,6 +22,16 @@ table {
|
||||
}
|
||||
}
|
||||
|
||||
tr {
|
||||
&.header {
|
||||
font-size: 1.2em;
|
||||
|
||||
th {
|
||||
padding-top: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
line-height: 1.41em;
|
||||
|
@ -1,11 +1,7 @@
|
||||
.tabs {
|
||||
@if $shadows == 1 {
|
||||
@include shadow(1);
|
||||
} @else {
|
||||
background: transparentize($color-black, .98);
|
||||
border: 0 solid darken($color-white, 10%);
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
background: transparentize($color-black, .98);
|
||||
display: flex;
|
||||
display: -webkit-flex; // sass-lint:disable-line no-vendor-prefixes no-duplicate-properties
|
||||
list-style: none;
|
||||
|
@ -9,7 +9,10 @@ $color-primary: #dc0067 !default;
|
||||
$color-new: #459c18 !default;
|
||||
$color-online: #1566a9 !default;
|
||||
$color-offline: #cf3e2a !default;
|
||||
$color-unseen: #d89100 !default;
|
||||
|
||||
$color-24ghz: $color-primary !default;
|
||||
$color-5ghz: #e3a619 !default;
|
||||
$color-others: #0a9c92 !default;
|
||||
|
||||
$color-map-background: #f8f4f0 !default;
|
||||
|
||||
@ -43,8 +46,5 @@ $grid-breakpoints: (
|
||||
$sidebar-width: map-get($grid-breakpoints, xl) * .45 !default;
|
||||
$sidebar-width-small: map-get($grid-breakpoints, lg) * .45 !default;
|
||||
|
||||
// En/disable box-shadows
|
||||
$shadows: 0 !default;
|
||||
|
||||
// En/disable included font
|
||||
$use-included-font: 1 !default;
|
||||
|
@ -1,14 +1,18 @@
|
||||
// Overwrite normal style (colors) - shadows are ignored
|
||||
// Overwrite normal style (colors)
|
||||
@import 'modules/variables';
|
||||
@import 'custom/variables';
|
||||
|
||||
$color-white: #111;
|
||||
$color-white: #1c1c13;
|
||||
$color-black: #fefefe;
|
||||
$color-map-background: #0d151c;
|
||||
|
||||
$color-online: lighten($color-online, 25%);
|
||||
|
||||
html {
|
||||
//@import 'modules/base';
|
||||
body {
|
||||
body,
|
||||
textarea,
|
||||
input {
|
||||
background: $color-white;
|
||||
color: lighten($color-black, 100);
|
||||
}
|
||||
@ -18,6 +22,15 @@ html {
|
||||
border-bottom-color: lighten($color-white, 10%);
|
||||
}
|
||||
|
||||
a {
|
||||
color: $color-online;
|
||||
text-decoration: none;
|
||||
|
||||
&:focus {
|
||||
color: darken($color-online, 15%);
|
||||
}
|
||||
}
|
||||
|
||||
//@import 'modules/leaflet';
|
||||
.leaflet-container {
|
||||
background: $color-map-background;
|
||||
|