├── .gitignore ├── LICENSE ├── README.md ├── app.config.json ├── backend ├── __init__.py ├── config.ini ├── connections.py ├── data.py ├── devices.py ├── error.py ├── inventory.py ├── schemas.py └── socketio.py ├── config.json ├── docs └── screenshots │ ├── configuration.png │ ├── devices.png │ └── yang.png ├── frontend-assets ├── icons │ ├── add.svg │ ├── add_active.svg │ ├── back.svg │ ├── close.svg │ ├── close_active.svg │ ├── collapse.svg │ ├── collapse_active.svg │ ├── confirm.svg │ ├── confirm_active.svg │ ├── container.svg │ ├── edit.svg │ ├── edit_active.svg │ ├── info.svg │ ├── info_active.svg │ ├── key.svg │ ├── leaf.svg │ ├── leaflist.svg │ ├── menu.svg │ ├── menu_active.svg │ ├── module.svg │ ├── reload.svg │ ├── reload_active.svg │ ├── show.svg │ ├── show_active.svg │ ├── show_all.svg │ ├── show_all_active.svg │ ├── show_children.svg │ ├── show_children_active.svg │ ├── tree_branch.svg │ ├── tree_cont.svg │ ├── tree_empty.svg │ ├── tree_last_branch.svg │ ├── tree_root.svg │ └── yangfile.svg ├── logo.png └── starthere.svg ├── frontend ├── _netopeer-common.scss ├── common │ ├── loading │ │ ├── loading.component.html │ │ ├── loading.component.scss │ │ └── loading.component.ts │ └── pipes.ts ├── config │ ├── config.component.html │ ├── config.component.scss │ ├── config.component.ts │ ├── modifications.service.ts │ ├── ordering.directive.ts │ ├── session.ts │ ├── sessions.service.ts │ ├── tree-create.html │ ├── tree-edit.html │ ├── tree-indent.html │ ├── tree-node.html │ ├── tree.component.html │ ├── tree.component.scss │ ├── tree.component.ts │ └── tree.service.ts ├── dashboard.component.html ├── dashboard.component.ts ├── inventory │ ├── device.ts │ ├── devices.component.html │ ├── devices.component.ts │ ├── devices.service.ts │ ├── inventory.component.html │ ├── inventory.component.scss │ ├── inventory.component.ts │ ├── schema.ts │ ├── schemas.component.html │ └── schemas.component.ts ├── monitoring │ ├── monitoring.component.html │ ├── monitoring.component.scss │ └── monitoring.component.ts ├── netopeer.component.html ├── netopeer.component.ts ├── netopeer.module.ts ├── netopeer.scss ├── package.json ├── plugins │ ├── plugins.component.html │ ├── plugins.component.scss │ └── plugins.component.ts └── yang │ ├── schemas.service.ts │ ├── yang.component.html │ ├── yang.component.scss │ ├── yang.component.ts │ ├── yang.feature.html │ ├── yang.identity.html │ ├── yang.module.html │ ├── yang.node.html │ ├── yang.restriction.html │ ├── yang.type.html │ └── yang.typedef.html ├── schema.svg └── vagrant ├── OpenSUSE └── Vagrantfile ├── README.md ├── Ubuntu-release ├── Vagrantfile ├── lgui-config.ini ├── ncgui.conf ├── ncgui.service ├── netopeer-config.ini ├── run.sh └── setvenv.sh └── Ubuntu └── Vagrantfile /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Netopeer2GUI 2 | Web-based NETCONF management center 3 | 4 | This tool is currently under development and not intended for production use. 5 | However, we welcome your feedback provided via the [issue tracker](https://github.com/CESNET/Netopeer2GUI/issues). 6 | 7 | 8 | 9 | Several screenshots can be found in the [`docs`](./docs/). 10 | 11 | # Features List 12 | 13 | - [x] manage devices to connect to 14 | - [ ] manage devices groupings for bulk configuration 15 | - [x] manage YANG schemas stored in GUI to represent received data 16 | - [ ] interaction with user by asking unknown module used by the connected device 17 | - [x] display configuration and data of the connected device (data tree view) 18 | - [x] edit configuration data of the device 19 | - [ ] bulk configuration (set configuration of multiple device at once) 20 | - [x] YANG explorer to display/browse YANG schema (currently just basic textual information) 21 | - [ ] receive NETCONF notifications and present them to user 22 | - [ ] accept NETCONF Call Home connections 23 | - [ ] plugin interface for schema-specific applications 24 | 25 | # Dependencies 26 | The application is created as a module to the [liberouter-gui](https://github.com/CESNET/liberouter-gui) 27 | framework, so to install it, follow the [liberouter-gui instructions](https://github.com/CESNET/liberouter-gui/wiki/Deploying-LiberouterGUI). When you decide to deploy production version, there is pre-built Netopeer2GUI as a [release package](https://github.com/CESNET/Netopeer2GUI/releases). To configure your web server, please follow the mentioned liberouter-gui instructions or have a look at [`*-release` vagrant image(s)](./vagrant/). 28 | 29 | The backend is a Flask server written in Python 3 and utilizing [libyang](https://github.com/CESNET/libyang) 30 | and [libnetconf2](https://github.com/CESNET/libnetconf2) Python bindings. 31 | Unfortunatelly, the code of the bindings is not yet finished, so please use 32 | the devel branches of the mentioned libraries: 33 | ``` 34 | $ git clone -b devel https://github.com/CESNET/libyang 35 | $ mkdir -p libyang/build && cd libyang/build 36 | $ cmake -DGEN_LANGUAGE_BINDINGS=ON .. 37 | $ make 38 | # make install 39 | ``` 40 | ``` 41 | $ git clone -b devel https://github.com/CESNET/libnetconf2 42 | $ mkdir -p libnetconf2/build && cd libnetconf2/build 43 | $ cmake -DENABLE_PYTHON=ON .. 44 | $ make 45 | # make install 46 | ``` 47 | 48 | Or alternatively install binary packages of [libyang](https://software.opensuse.org//download.html?project=home%3Aliberouter&package=libyang-experimental) and [libnetconf2](https://software.opensuse.org//download.html?project=home%3Aliberouter&package=libnetconf2-experimental). 49 | 50 | # Vagrant 51 | For fast and simple testing/development deployment, you can use the prepared 52 | Vagrantfiles for instantiating virtual machine. More information can be found 53 | [here](./vagrant/). 54 | 55 | -------------------------------------------------------------------------------- /app.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "logo" : "assets/netopeer/logo.png", 3 | "name" : "Netopeer", 4 | "colorTheme" : { 5 | "colorMain" : "#354b68", 6 | "colorHighlight" : "#3d5676", 7 | "colorSelected" : "#3d5676", 8 | "colorSelected2" : "#6888b1" 9 | }, 10 | "api" : { 11 | "url" : "/libapi", 12 | "host" : null, 13 | "port" : null, 14 | "proto" : null 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /backend/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Netopeer2 GUI backend 3 | File: __init__.py 4 | Author: Radek Krejci 5 | 6 | Backend initialization via liberouter GUI. 7 | """ 8 | 9 | from liberouterapi import config, modules 10 | 11 | # Get Netopeer backend config 12 | config.load(path = __path__[0] + '/config.ini') 13 | 14 | # Register a blueprint 15 | module_bp = modules.module.Module('netopeer', __name__, url_prefix = '/netopeer', no_version = True) 16 | 17 | from .schemas import * 18 | from .devices import * 19 | from .connections import * 20 | 21 | module_bp.add_url_rule('/inventory/schemas', view_func = schemas_list, methods = ['GET']) 22 | module_bp.add_url_rule('/inventory/schemas', view_func = schemas_add, methods=['POST']) 23 | module_bp.add_url_rule('/inventory/schemas', view_func = schemas_rm, methods = ['DELETE']) 24 | module_bp.add_url_rule('/inventory/schema', view_func = schema_get, methods = ['GET']) 25 | module_bp.add_url_rule('/inventory/devices/list', view_func = devices_list, methods=['GET']) 26 | module_bp.add_url_rule('/inventory/devices', view_func = devices_add, methods=['POST']) 27 | module_bp.add_url_rule('/inventory/devices', view_func = devices_rm, methods = ['DELETE']) 28 | module_bp.add_url_rule('/session', view_func = connect, methods=['POST']) 29 | module_bp.add_url_rule('/session', view_func = session_close, methods = ['DELETE']) 30 | module_bp.add_url_rule('/session/alive', view_func = session_alive, methods=['GET']) 31 | module_bp.add_url_rule('/session/capabilities', view_func = session_get_capabilities, methods=['GET']) 32 | module_bp.add_url_rule('/session/rpcGet', view_func = session_get, methods=['GET']) 33 | module_bp.add_url_rule('/session/commit', view_func = session_commit, methods = ['POST']) 34 | module_bp.add_url_rule('/session/element/checkvalue', view_func = data_checkvalue, methods = ['GET']) 35 | module_bp.add_url_rule('/session/schema', view_func = schema_info, methods = ['GET']) 36 | module_bp.add_url_rule('/session/schema/checkvalue', view_func = schema_checkvalue, methods = ['GET']) 37 | module_bp.add_url_rule('/session/schema/values', view_func = schema_values, methods = ['GET']) 38 | -------------------------------------------------------------------------------- /backend/config.ini: -------------------------------------------------------------------------------- 1 | [netopeer] 2 | usersdata_path=./ -------------------------------------------------------------------------------- /backend/data.py: -------------------------------------------------------------------------------- 1 | """ 2 | NETCONF data helper functions 3 | File: data.py 4 | Author: Radek Krejci 5 | """ 6 | 7 | import json 8 | import os 9 | 10 | import yang 11 | import netconf2 as nc 12 | from .schemas import make_schema_key 13 | 14 | 15 | def infoBuiltInType(base): 16 | return { 17 | - 1 : 'error', 18 | 0 : 'derived', 19 | 1 : 'binary', 20 | 2 : 'bits', 21 | 3 : 'boolean', 22 | 4 : 'decimal64', 23 | 5 : 'empty', 24 | 6 : 'enumeration', 25 | 7 : 'identityref', 26 | 8 : 'instance-identifier', 27 | 9 : 'leafref', 28 | 10: 'string', 29 | 11: 'union', 30 | 12: 'int8', 31 | 13: 'uint8', 32 | 14: 'int16', 33 | 15: 'uint16', 34 | 16: 'int32', 35 | 17: 'uint32', 36 | 18: 'int64', 37 | 19: 'uint64', 38 | }[base] 39 | 40 | 41 | def schemaInfoType(schema, info): 42 | info["datatype"] = schema.type().der().name() 43 | info["datatypebase"] = infoBuiltInType(schema.type().base()) 44 | 45 | 46 | def typeValues(type, result): 47 | while type.der(): 48 | if type.base() == 2: 49 | # bits 50 | if type.info().bits().count(): 51 | for bit in type.info().bits().bit(): 52 | result.append(bit.name()) 53 | elif type.base() == 6: 54 | # enumeration 55 | if type.info().enums().count(): 56 | for enm in type.info().enums().enm(): 57 | result.append(enm.name()) 58 | else: 59 | return result 60 | type = type.der().type() 61 | 62 | return result 63 | 64 | def schemaInfoNode(schema): 65 | info = {} 66 | 67 | info["type"] = schema.nodetype() 68 | if schema.module().rev_size(): 69 | info["module"] = schema.module().name() + '@' + schema.module().rev().date() 70 | else: 71 | info["module"] = schema.module().name() 72 | info["name"] = schema.name() 73 | info["dsc"] = schema.dsc() 74 | info["config"] = True if schema.flags() & yang.LYS_CONFIG_W else False 75 | if info["type"] == 1: 76 | info["presence"] = schema.subtype().presence() 77 | info["path"] = schema.path() 78 | 79 | if info["type"] == yang.LYS_LEAF: 80 | schemaInfoType(schema.subtype(), info) 81 | info["key"] = False if schema.subtype().is_key() == None else True 82 | dflt = schema.subtype().dflt() 83 | if dflt: 84 | info["default"] = dflt 85 | else: 86 | tpdf = schema.subtype().type().der() 87 | while tpdf and not tpdf.dflt(): 88 | tpdf = tpdf.type().der() 89 | if tpdf: 90 | info["default"] = tpdf.dflt() 91 | elif info["type"] == yang.LYS_LEAFLIST: 92 | schemaInfoType(schema.subtype(), info) 93 | if schema.flags() & yang.LYS_USERORDERED: 94 | info["ordered"] = True; 95 | elif info["type"] == yang.LYS_LIST: 96 | if schema.flags() & yang.LYS_USERORDERED: 97 | info["ordered"] = True; 98 | info["keys"] = [] 99 | for key in schema.subtype().keys(): 100 | info["keys"].append(key.name()) 101 | 102 | return info 103 | 104 | 105 | def _sortChildren(node): 106 | sorted = [] 107 | for index, item in enumerate(node["children"]): 108 | sorted.append(item) 109 | if item["info"]["type"] == yang.LYS_LIST: 110 | removed = 0 111 | if "ordered" in item["info"]: 112 | item["order"] = removed 113 | for i, instance in enumerate(node["children"][index + 1:]): 114 | if item["info"]["name"] == instance["info"]["name"] and item["info"]["module"] == instance["info"]["module"]: 115 | sorted.append(node["children"].pop(index + 1 + i - removed)) 116 | removed += 1; 117 | if "ordered" in item["info"]: 118 | instance["order"] = removed 119 | if item["info"]["type"] == yang.LYS_LEAFLIST: 120 | lastLeafList = len(sorted) - 1 121 | item["first"] = True 122 | removed = 0 123 | if "ordered" in item["info"]: 124 | item["order"] = removed 125 | for i, instance in enumerate(node["children"][index + 1:]): 126 | if item["info"]["name"] == instance["info"]["name"] and item["info"]["module"] == instance["info"]["module"]: 127 | instance["first"] = False 128 | sorted.append(node["children"].pop(index + 1 + i - removed)) 129 | removed += 1; 130 | if "ordered" in item["info"]: 131 | instance["order"] = removed 132 | node["children"] = sorted 133 | last = node["children"][len(node["children"]) - 1] 134 | if last["info"]["type"] == yang.LYS_LEAFLIST: 135 | node["children"][lastLeafList]["last"] = True 136 | for item in node["children"][lastLeafList + 1:]: 137 | item["lastLeafList"] = True; 138 | else: 139 | last["last"] = True 140 | 141 | 142 | def dataInfoNode(node, parent=None, recursion=False): 143 | schema = node.schema() 144 | casted = node.subtype() 145 | 146 | if node.dflt(): 147 | return None 148 | 149 | info = schemaInfoNode(schema); 150 | 151 | result = {} 152 | if info["type"] == yang.LYS_LEAF or info["type"] == yang.LYS_LEAFLIST: 153 | result["value"] = casted.value_str() 154 | if info["datatypebase"] == "identityref": 155 | info["refmodule"] = make_schema_key(casted.value().ident().module()) 156 | elif recursion: 157 | result["children"] = [] 158 | if node.child(): 159 | for child in node.child().tree_for(): 160 | childNode = dataInfoNode(child, result, True) 161 | if not childNode: 162 | continue 163 | result["children"].append(childNode) 164 | # sort list instances 165 | _sortChildren(result) 166 | if info["type"] == yang.LYS_LIST: 167 | result["keys"] = [] 168 | index = 0 169 | for key in schema.subtype().keys(): 170 | if len(result["children"]) <= index: 171 | break 172 | if key.subtype().name() == result["children"][index]["info"]["name"]: 173 | result["keys"].append(result["children"][index]["value"]) 174 | index = index + 1 175 | result["info"] = info 176 | result["path"] = node.path() 177 | 178 | return result 179 | 180 | def dataInfoSubtree(data, path, recursion=False): 181 | try: 182 | node = data.find_path(path).data()[0] 183 | except: 184 | return(json.dumps({'success': False, 'error-msg': 'Invalid data path.'})) 185 | 186 | result = dataInfoNode(node) 187 | if not result: 188 | return(json.dumps({'success': False, 'error-msg': 'Path refers to a default node.'})) 189 | 190 | result["children"] = [] 191 | if node.child(): 192 | for child in node.child().tree_for(): 193 | childNode = dataInfoNode(child, result, recursion) 194 | if not childNode: 195 | continue 196 | result["children"].append(childNode) 197 | _sortChildren(result) 198 | 199 | return(json.dumps({'success': True, 'data': result})) 200 | 201 | 202 | def dataInfoRoots(data, recursion=False): 203 | top = {} 204 | top["children"] = [] 205 | for root in data.tree_for(): 206 | rootNode = dataInfoNode(root, top, recursion) 207 | if not rootNode: 208 | continue 209 | if not recursion: 210 | rootNode['subtreeRoot'] = True 211 | top["children"].append(rootNode) 212 | _sortChildren(top) 213 | return(json.dumps({'success': True, 'data': top["children"]})) 214 | -------------------------------------------------------------------------------- /backend/devices.py: -------------------------------------------------------------------------------- 1 | """ 2 | Manipulation with the devices to connect. 3 | File: devices.py 4 | Author: Radek Krejci 5 | """ 6 | 7 | import json 8 | import os 9 | import errno 10 | 11 | from liberouterapi import auth 12 | from flask import request 13 | 14 | from .inventory import INVENTORY, inventory_check 15 | from .error import NetopeerException 16 | 17 | __DEVICES_EMPTY = '{"device":[]}' 18 | 19 | 20 | def __devices_init(): 21 | return json.loads(__DEVICES_EMPTY) 22 | 23 | def __devices_inv_load(path): 24 | devicesinv_path = os.path.join(path, 'devices.json') 25 | try: 26 | with open(devicesinv_path, 'r') as devices_file: 27 | devices = json.load(devices_file) 28 | except OSError as e: 29 | if e.errno == errno.ENOENT: 30 | devices = __devices_init() 31 | else: 32 | raise NetopeerException('Unable to use user\'s devices inventory ' + devicesinv_path + ' (' + str(e) + ').') 33 | except ValueError: 34 | devices = __devices_init() 35 | 36 | return devices 37 | 38 | def __devices_inv_save(path, devices): 39 | devicesinv_path = os.path.join(path, 'devices.json') 40 | 41 | #store the list 42 | try: 43 | with open(devicesinv_path, 'w') as devices_file: 44 | json.dump(devices, devices_file) 45 | except Exception: 46 | pass 47 | 48 | return devices 49 | 50 | @auth.required() 51 | def devices_list(): 52 | session = auth.lookup(request.headers.get('lgui-Authorization', None)) 53 | user = session['user'] 54 | path = os.path.join(INVENTORY, user.username) 55 | 56 | inventory_check(path) 57 | devices = __devices_inv_load(path) 58 | 59 | for dev in devices['device']: 60 | if 'password' in dev: 61 | del dev['password'] 62 | 63 | return(json.dumps(devices['device'])) 64 | 65 | @auth.required() 66 | def devices_add(): 67 | session = auth.lookup(request.headers.get('lgui-Authorization', None)) 68 | user = session['user'] 69 | path = os.path.join(INVENTORY, user.username) 70 | 71 | device = request.get_json() 72 | if not device or not device['id']: 73 | raise NetopeerException('Invalid device remove request.') 74 | 75 | devices = __devices_inv_load(path) 76 | for dev in devices['device']: 77 | if dev['id'] == device['id']: 78 | return (json.dumps({'success': False})) 79 | 80 | device_json = {'id':device['id'], 81 | 'name':device['name'], 82 | 'hostname':device['hostname'], 83 | 'port':device['port'], 84 | 'autoconnect':device['autoconnect'], 85 | 'username':device['username']} 86 | if 'password' in device and device['password']: 87 | device_json['password'] = device['password'] 88 | devices['device'].append(device_json) 89 | 90 | #store the list 91 | __devices_inv_save(path, devices) 92 | 93 | return(json.dumps({'success': True})) 94 | 95 | @auth.required() 96 | def devices_rm(): 97 | session = auth.lookup(request.headers.get('lgui-Authorization', None)) 98 | user = session['user'] 99 | path = os.path.join(INVENTORY, user.username) 100 | 101 | rm_id = request.get_json()['id'] 102 | if not rm_id: 103 | raise NetopeerException('Invalid device remove request.') 104 | 105 | devices = __devices_inv_load(path) 106 | for i in range(len(devices['device'])): 107 | device = devices['device'][i] 108 | if device['id'] == rm_id: 109 | devices['device'].pop(i) 110 | device = None 111 | break 112 | 113 | if device: 114 | # device not in inventory 115 | return (json.dumps({'success': False})) 116 | 117 | # update the inventory database 118 | __devices_inv_save(path, devices) 119 | 120 | return(json.dumps({'success': True})) 121 | 122 | def devices_get(device_id, username): 123 | path = os.path.join(INVENTORY, username) 124 | devices = __devices_inv_load(path) 125 | 126 | for i in range(len(devices['device'])): 127 | device = devices['device'][i] 128 | if device['id'] == device_id: 129 | return device 130 | 131 | return None 132 | 133 | 134 | def devices_replace(device_id, username, device): 135 | path = os.path.join(INVENTORY, username) 136 | devices = __devices_inv_load(path) 137 | 138 | for i in range(len(devices['device'])): 139 | if devices['device'][i]['id'] == device_id: 140 | devices['device'][i] = device 141 | break 142 | 143 | # update the inventory database 144 | __devices_inv_save(path, devices) 145 | -------------------------------------------------------------------------------- /backend/error.py: -------------------------------------------------------------------------------- 1 | #!/bin/python3 2 | 3 | from liberouterapi.error import ApiException 4 | 5 | class NetopeerException(ApiException): 6 | status_code = 500 7 | -------------------------------------------------------------------------------- /backend/inventory.py: -------------------------------------------------------------------------------- 1 | """ 2 | Manipulation with the YANG schemas. 3 | File: schemas.py 4 | Author: Radek Krejci 5 | """ 6 | 7 | import os 8 | import errno 9 | 10 | from liberouterapi import config 11 | 12 | from .error import NetopeerException 13 | 14 | INVENTORY = config['netopeer'].get('usersdata_path', './') 15 | 16 | def inventory_check(path): 17 | try: 18 | os.makedirs(path, mode=0o750) 19 | except OSError as e: 20 | if e.errno == errno.EEXIST and os.path.isdir(path): 21 | pass 22 | elif e.errno == errno.EEXIST: 23 | raise NetopeerException('User\'s inventory (' + path + ') already exists and it\'s not a directory.') 24 | else: 25 | raise NetopeerException('Unable to use inventory path ' + path +' (' + str(e) + ').') 26 | 27 | -------------------------------------------------------------------------------- /backend/socketio.py: -------------------------------------------------------------------------------- 1 | """ 2 | Socket IO helper functions 3 | File: socketio.py 4 | Author: Radek Krejci 5 | """ 6 | 7 | from eventlet import event 8 | 9 | from liberouterapi import socketio 10 | 11 | sio_data = {} 12 | 13 | 14 | def sio_send(data): 15 | try: 16 | e = sio_data[data['id']] 17 | e.send(data) 18 | except KeyError: 19 | pass 20 | 21 | 22 | def sio_emit(name, params): 23 | socketio.emit(name, params, callback = sio_send) 24 | 25 | 26 | def sio_wait(id): 27 | e = sio_data[id] = event.Event() 28 | return e.wait() 29 | 30 | 31 | def sio_clean(id): 32 | sio_data.pop(id, None) 33 | 34 | 35 | @socketio.on('device_auth_password') 36 | @socketio.on('hostcheck_result') 37 | @socketio.on('getschema_result') 38 | def process_answer(data): 39 | sio_send(data) 40 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "npm" : "frontend/package.json" 4 | }, 5 | "module": { 6 | "name" : "netopeer", 7 | "file" : "netopeer.module.ts", 8 | "class" : "NetopeerModule", 9 | "hooks" : "NetopeerModuleHooks", 10 | "frontend" : "frontend", 11 | "backend" : "backend", 12 | "assets" : "frontend-assets" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /docs/screenshots/configuration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CESNET/Netopeer2GUI/6f533486bfa3cb5e29ffc01a62e60ef940a5945e/docs/screenshots/configuration.png -------------------------------------------------------------------------------- /docs/screenshots/devices.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CESNET/Netopeer2GUI/6f533486bfa3cb5e29ffc01a62e60ef940a5945e/docs/screenshots/devices.png -------------------------------------------------------------------------------- /docs/screenshots/yang.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CESNET/Netopeer2GUI/6f533486bfa3cb5e29ffc01a62e60ef940a5945e/docs/screenshots/yang.png -------------------------------------------------------------------------------- /frontend-assets/icons/add.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 22 | 24 | image/svg+xml 25 | 27 | 28 | 29 | 30 | 31 | 33 | 55 | 58 | 59 | 64 | 69 | 70 | -------------------------------------------------------------------------------- /frontend-assets/icons/add_active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 22 | 24 | image/svg+xml 25 | 27 | 28 | 29 | 30 | 31 | 33 | 55 | 58 | 59 | 64 | 69 | 70 | -------------------------------------------------------------------------------- /frontend-assets/icons/back.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 22 | 25 | 30 | 31 | 39 | 44 | 50 | 51 | 59 | 64 | 68 | 69 | 71 | 76 | 77 | 79 | 84 | 85 | 87 | 93 | 100 | 101 | 102 | 124 | 129 | 130 | 132 | 133 | 135 | image/svg+xml 136 | 138 | 139 | 140 | 141 | 142 | 147 | 150 | 153 | 156 | 159 | 162 | 165 | 168 | 171 | 174 | 177 | 180 | 183 | 186 | 189 | 192 | 197 | 198 | 215 | 216 | -------------------------------------------------------------------------------- /frontend-assets/icons/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 22 | 24 | image/svg+xml 25 | 27 | 28 | 29 | 30 | 31 | 33 | 55 | 58 | 59 | 64 | 69 | 70 | -------------------------------------------------------------------------------- /frontend-assets/icons/close_active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 22 | 24 | image/svg+xml 25 | 27 | 28 | 29 | 30 | 31 | 33 | 55 | 58 | 59 | 64 | 69 | 70 | -------------------------------------------------------------------------------- /frontend-assets/icons/collapse.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 22 | 24 | image/svg+xml 25 | 27 | 28 | 29 | 30 | 31 | 33 | 55 | 58 | 59 | 64 | 69 | 70 | -------------------------------------------------------------------------------- /frontend-assets/icons/collapse_active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 22 | 24 | image/svg+xml 25 | 27 | 28 | 29 | 30 | 31 | 33 | 55 | 58 | 59 | 64 | 69 | 70 | -------------------------------------------------------------------------------- /frontend-assets/icons/container.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 22 | 24 | image/svg+xml 25 | 27 | 28 | 29 | 30 | 31 | 33 | 55 | 58 | 59 | 62 | 68 | 74 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /frontend-assets/icons/edit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 22 | 24 | image/svg+xml 25 | 27 | 28 | 29 | 30 | 31 | 33 | 36 | 42 | 43 | 44 | 66 | 69 | 70 | 74 | 78 | 82 | 86 | 89 | 92 | 95 | 98 | 101 | 104 | 107 | 110 | 113 | 116 | 119 | 122 | 125 | 128 | 131 | 132 | -------------------------------------------------------------------------------- /frontend-assets/icons/edit_active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 22 | 24 | image/svg+xml 25 | 27 | 28 | 29 | 30 | 31 | 33 | 36 | 42 | 43 | 44 | 66 | 69 | 70 | 75 | 80 | 86 | 91 | 94 | 97 | 100 | 103 | 106 | 109 | 112 | 115 | 118 | 121 | 124 | 127 | 130 | 133 | 136 | 142 | 143 | -------------------------------------------------------------------------------- /frontend-assets/icons/info.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 22 | 24 | image/svg+xml 25 | 27 | 28 | 29 | 30 | 31 | 33 | 55 | 58 | 59 | 66 | i 77 | 78 | -------------------------------------------------------------------------------- /frontend-assets/icons/info_active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 22 | 24 | image/svg+xml 25 | 27 | 28 | 29 | 30 | 31 | 33 | 55 | 58 | 59 | 66 | i 77 | 78 | -------------------------------------------------------------------------------- /frontend-assets/icons/key.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 22 | 24 | image/svg+xml 25 | 27 | 28 | 29 | 30 | 31 | 33 | 55 | 58 | 59 | 62 | 68 | 73 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /frontend-assets/icons/leaf.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 22 | 24 | image/svg+xml 25 | 27 | 28 | 29 | 30 | 31 | 33 | 55 | 58 | 59 | 65 | 66 | -------------------------------------------------------------------------------- /frontend-assets/icons/leaflist.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 22 | 24 | image/svg+xml 25 | 27 | 28 | 29 | 30 | 31 | 33 | 55 | 58 | 59 | 62 | 68 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /frontend-assets/icons/menu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 22 | 24 | image/svg+xml 25 | 27 | 28 | 29 | 30 | 31 | 33 | 55 | 58 | 59 | 64 | 69 | 74 | 75 | -------------------------------------------------------------------------------- /frontend-assets/icons/menu_active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 22 | 24 | image/svg+xml 25 | 27 | 28 | 29 | 30 | 31 | 33 | 55 | 58 | 59 | 64 | 69 | 74 | 75 | -------------------------------------------------------------------------------- /frontend-assets/icons/module.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 22 | 25 | 30 | 31 | 39 | 44 | 50 | 51 | 59 | 64 | 68 | 69 | 71 | 76 | 77 | 79 | 84 | 85 | 87 | 93 | 100 | 101 | 102 | 124 | 129 | 130 | 132 | 133 | 135 | image/svg+xml 136 | 138 | 139 | 140 | 141 | 142 | 147 | 150 | 153 | 156 | 159 | 162 | 165 | 168 | 171 | 174 | 177 | 180 | 183 | 186 | 189 | 192 | 195 | 201 | 207 | 212 | 213 | 214 | 231 | 232 | -------------------------------------------------------------------------------- /frontend-assets/icons/show.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 22 | 24 | image/svg+xml 25 | 27 | 28 | 29 | 30 | 31 | 33 | 55 | 58 | 59 | 65 | 71 | 83 | 84 | -------------------------------------------------------------------------------- /frontend-assets/icons/show_active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 22 | 24 | image/svg+xml 25 | 27 | 28 | 29 | 30 | 31 | 33 | 55 | 58 | 59 | 65 | 71 | 83 | 84 | -------------------------------------------------------------------------------- /frontend-assets/icons/show_all.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 22 | 24 | image/svg+xml 25 | 27 | 28 | 29 | 30 | 31 | 33 | 55 | 58 | 59 | 64 | 69 | 70 | -------------------------------------------------------------------------------- /frontend-assets/icons/show_all_active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 22 | 24 | image/svg+xml 25 | 27 | 28 | 29 | 30 | 31 | 33 | 55 | 58 | 59 | 64 | 69 | 70 | -------------------------------------------------------------------------------- /frontend-assets/icons/show_children.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 22 | 24 | image/svg+xml 25 | 27 | 28 | 29 | 30 | 31 | 33 | 55 | 58 | 59 | 64 | 65 | -------------------------------------------------------------------------------- /frontend-assets/icons/show_children_active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 22 | 24 | image/svg+xml 25 | 27 | 28 | 29 | 30 | 31 | 33 | 55 | 58 | 59 | 64 | 65 | -------------------------------------------------------------------------------- /frontend-assets/icons/tree_branch.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 21 | 23 | image/svg+xml 24 | 26 | 27 | 28 | 29 | 30 | 32 | 54 | 57 | 58 | 65 | 71 | 77 | 78 | -------------------------------------------------------------------------------- /frontend-assets/icons/tree_cont.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 22 | 23 | 25 | image/svg+xml 26 | 28 | 29 | 30 | 31 | 32 | 34 | 56 | 59 | 60 | 67 | 73 | 74 | -------------------------------------------------------------------------------- /frontend-assets/icons/tree_empty.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 22 | 24 | image/svg+xml 25 | 27 | 28 | 29 | 30 | 31 | 33 | 55 | 58 | 59 | 66 | 67 | -------------------------------------------------------------------------------- /frontend-assets/icons/tree_last_branch.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 21 | 23 | image/svg+xml 24 | 26 | 27 | 28 | 29 | 30 | 32 | 54 | 57 | 58 | 65 | 71 | 72 | -------------------------------------------------------------------------------- /frontend-assets/icons/tree_root.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 21 | 23 | image/svg+xml 24 | 26 | 27 | 28 | 29 | 30 | 32 | 54 | 57 | 58 | 65 | 71 | 72 | -------------------------------------------------------------------------------- /frontend-assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CESNET/Netopeer2GUI/6f533486bfa3cb5e29ffc01a62e60ef940a5945e/frontend-assets/logo.png -------------------------------------------------------------------------------- /frontend/_netopeer-common.scss: -------------------------------------------------------------------------------- 1 | @import "./colors"; 2 | 3 | $colorSuccess: #def2de; 4 | $colorSuccessBorder: green; 5 | $colorFailure: #f2dede; 6 | $colorFailureBorder: red; 7 | 8 | a:not([href]) { 9 | cursor: pointer; 10 | text-decoration: underline; 11 | font-weight: bold; 12 | } 13 | 14 | .netopeer-content { 15 | display: block; 16 | padding: 0.7em 1em 1em 2em; 17 | background-color: $colorBackground; 18 | color: $black; 19 | } 20 | 21 | .msg-rounded { 22 | border-radius: 5px; 23 | } 24 | 25 | .msg-close { 26 | padding-right: 10px; 27 | padding-left: 10px; 28 | font-weight: bold; 29 | font-family: monospace; 30 | font-size: large; 31 | cursor: pointer; 32 | height: 1em; 33 | } 34 | 35 | .msg-success { 36 | background-color: $colorSuccess; 37 | color: $colorSuccessBorder; 38 | padding: 5px 1em 5px 0em; 39 | 40 | .msg-close { 41 | color: $colorSuccessBorder; 42 | } 43 | } 44 | 45 | .msg-failure { 46 | background-color: $colorFailure; 47 | color: $colorFailureBorder; 48 | padding: 5px 1em 5px 0em; 49 | 50 | .msg-close { 51 | color: $colorFailureBorder; 52 | } 53 | } 54 | 55 | .tab-add, 56 | .tab-reload, 57 | .tab-close { 58 | cursor: pointer; 59 | font-weight: bold; 60 | font-family: monospace; 61 | font-size: large; 62 | } 63 | .tab-action-first { 64 | padding-left: 0.3em; 65 | } 66 | .tab-action-last { 67 | padding-right: 0.5em; 68 | } 69 | 70 | .tab-icon { 71 | vertical-align: middle; 72 | height: 1em; 73 | } 74 | 75 | .tab-add { 76 | filter: invert(100%); 77 | 78 | &:hover { 79 | color: $green; 80 | } 81 | } 82 | 83 | .keyword { 84 | font-weight: bold; 85 | } 86 | 87 | .modal-header, .modal-footer { 88 | background-color: $colorMain; 89 | color: $colorTextInverse; 90 | } 91 | .modal-header { 92 | border-radius: 3px 3px 0 0; 93 | } 94 | .modal-footer { 95 | border-radius: 0 0 3px 3px; 96 | } 97 | .modal-title { 98 | color: $colorTextInverse; 99 | } 100 | 101 | .tab-close:hover { 102 | color: $colorFailureBorder; 103 | } 104 | .tab-reload:hover { 105 | color: blue; 106 | } 107 | 108 | #subnav { 109 | position: fixed; 110 | width: 100%; 111 | background-color: $colorMain; 112 | padding-left: 1em; 113 | 114 | a { 115 | cursor: pointer; 116 | text-decoration: none; 117 | display: inline-block; 118 | padding: 0.2em 0 0.1em 1em; 119 | color: $colorTextInverse; 120 | 121 | &:visited, 122 | &:link { 123 | color: inherit; 124 | } 125 | &:hover, 126 | &.active { 127 | background-color: $colorBackground; 128 | color: $colorText; 129 | } 130 | &active:hover { 131 | cursor: default; 132 | } 133 | } 134 | } -------------------------------------------------------------------------------- /frontend/common/loading/loading.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /frontend/common/loading/loading.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../netopeer-common'; 2 | 3 | .mat-spinner circle { 4 | stroke:$colorMain; 5 | } 6 | 7 | .mat-progress-bar-background { 8 | background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20version%3D%271.1%27%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20xmlns%3Axlink%3D%27http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%27%20x%3D%270px%27%20y%3D%270px%27%20enable-background%3D%27new%200%200%205%202%27%20xml%3Aspace%3D%27preserve%27%20viewBox%3D%270%200%205%202%27%20preserveAspectRatio%3D%27none%20slice%27%3E%3Ccircle%20cx%3D%271%27%20cy%3D%271%27%20r%3D%271%27%20fill%3D%27%23B2EBF2%27%2F%3E%3C%2Fsvg%3E") 9 | } 10 | .mat-progress-bar-buffer{ 11 | background-color:$lightGrey; 12 | } 13 | .mat-progress-bar-fill::after{ 14 | background-color:$colorMain; 15 | } 16 | -------------------------------------------------------------------------------- /frontend/common/loading/loading.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewEncapsulation, Input} from '@angular/core'; 2 | 3 | @Component({ 4 | selector : 'netopeer-loading', 5 | templateUrl : './loading.component.html', 6 | styleUrls : ['./loading.component.scss'], 7 | encapsulation: ViewEncapsulation.None 8 | }) 9 | 10 | export class LoadingComponent { 11 | @Input() spinner = false; 12 | @Input() diameter = 50; 13 | @Input() strokeWidth = 7; 14 | } 15 | -------------------------------------------------------------------------------- /frontend/common/pipes.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; 3 | 4 | @Pipe({name: 'noPrefix'}) 5 | export class NoPrefixPipe implements PipeTransform { 6 | transform(value: string): string { 7 | return value.slice(value.indexOf(':') + 1); 8 | } 9 | } 10 | 11 | @Pipe({name: 'prefixOnly'}) 12 | export class PrefixOnlyPipe implements PipeTransform { 13 | transform(value: string): string { 14 | return value.slice(0, value.indexOf(':')); 15 | } 16 | } 17 | 18 | @Pipe({name: 'patternHighlight'}) 19 | export class PatternHighlightPipe implements PipeTransform { 20 | constructor(private _sanitizer:DomSanitizer) {} 21 | 22 | transform(value: string): SafeHtml { 23 | let result = ''; 24 | for(let i = 0; i < value.length; i++) { 25 | if (value[i] == '(' || value[i] == '[' || value[i] == '{') { 26 | result = result.concat(`` + value[i] + ``); 27 | } else if (value[i] == ')' || value[i] == ']' || value[i] == '}') { 28 | let data = value[i]; 29 | if (i + 1 < value.length && (value[i+1] == '?' || value[i+1] == '+' || value[i+1] == '*')) { 30 | i; 31 | data = value.slice(i, i + 2); 32 | i++; 33 | } 34 | result = result.concat(`` + data + ``); 35 | } else { 36 | result = result.concat(value[i]); 37 | } 38 | } 39 | return this._sanitizer.bypassSecurityTrustHtml(result); 40 | } 41 | } -------------------------------------------------------------------------------- /frontend/config/config.component.html: -------------------------------------------------------------------------------- 1 | 17 | 18 |
19 | Start here with that + sign. 20 |
21 |

x{{err_msg}}

22 | 23 | 24 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 |
25 | v 29 | x 33 | Capability / ModuleVersion
 {{parseCapabilityName(cap)}}{{parseCapabilityRevision(cap)}}
45 | 46 | 47 | 48 | 49 | 50 | 51 |
 Data
52 |
53 | Retrieving data ... 54 |
55 |
56 | 57 |
58 |
59 | hide status data 60 | show status data 61 |
62 | 63 | 64 |
65 |
  66 |
67 |
Configuration data were changed. Do you wish to 68 | / 69 | 70 | all changes? 71 |
72 | 73 |
x{{err['message']}}
74 |
75 |
76 |
77 |
78 |
79 | 80 |
-------------------------------------------------------------------------------- /frontend/config/config.component.scss: -------------------------------------------------------------------------------- 1 | @import '../netopeer-common'; 2 | @import '../inventory/inventory.component'; 3 | @import './tree.component'; 4 | 5 | #config-data { 6 | cursor: default; 7 | width: 100%; 8 | } 9 | 10 | .item_action_expand, 11 | .item_action_collapse { 12 | height: 1em; 13 | } 14 | 15 | .item_action_collapse:hover { 16 | color: $red; 17 | } 18 | 19 | .item_action_expand:hover { 20 | color: $green; 21 | } 22 | 23 | .modifications-status { 24 | margin-bottom: 2em; 25 | margin-right: 2em; 26 | padding: 0.5em 1em; 27 | 28 | >div { 29 | position: fixed; 30 | bottom: 2em; 31 | right: 2em; 32 | background-color: $colorChanged; 33 | border: 2px solid $colorChangedBorder; 34 | padding: 0 1em; 35 | div { 36 | padding: 0.5em 0; 37 | } 38 | } 39 | } 40 | 41 | .loading { 42 | text-align: center; 43 | margin: auto; 44 | width: 10em; 45 | div { 46 | margin: auto; 47 | width: 50px; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /frontend/config/session.ts: -------------------------------------------------------------------------------- 1 | import { Device } from '../inventory/device'; 2 | 3 | import { SessionsService } from './sessions.service'; 4 | 5 | export enum NodeType { 6 | container = 1, 7 | leaf = 4, 8 | leaflist = 8, 9 | list = 16 10 | } 11 | 12 | export class Session { 13 | constructor ( 14 | public key: string, 15 | public device: Device, 16 | public loading = false, 17 | public data: Node = null, 18 | public treeFilters = [], 19 | public modifications = null, 20 | public cpblts: string = "", 21 | public dataPresence: string = 'none', 22 | public statusVisibility: boolean = true, 23 | public cpbltsVisibility: boolean = false, 24 | ) {} 25 | } 26 | 27 | export class NodeSchema { 28 | /* 29 | * type: NodeType; 30 | * path: string; 31 | */ 32 | } 33 | 34 | export class Node { 35 | /* 36 | * path: string; 37 | * info: NodeSchema; 38 | * 39 | * === container === 40 | * children: Node[] 41 | * newChildren: Node[] 42 | * 43 | * === leaf === 44 | * value: string; 45 | * 46 | * === leaf-list === 47 | * value: string; 48 | * 49 | * === list === 50 | * children: Node[] 51 | * newChildren: Node[] 52 | */ 53 | } 54 | -------------------------------------------------------------------------------- /frontend/config/tree-create.html: -------------------------------------------------------------------------------- 1 | 2 |
5 | 6 | 7 | 13 | 14 | 15 | 16 |
17 | xThere is no element to create at {{node['path']}}. 18 |
19 |
20 |
-------------------------------------------------------------------------------- /frontend/config/tree-edit.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | cancel 9 | done 14 | 22 | 27 |
28 | -------------------------------------------------------------------------------- /frontend/config/tree-indent.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 16 | 17 | 18 | 19 | 24 | 29 | 30 | 31 | 32 | 33 | 36 |
37 |
42 | 43 | cancel reordering 44 |
45 |
49 | 50 | delete 51 |
52 |
56 | 57 | create sibling 58 |
59 |
64 | 65 | create children 66 |
67 |
68 |
69 |
70 |
71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /frontend/config/tree-node.html: -------------------------------------------------------------------------------- 1 |
9 | 10 | info 13 | 14 | 15 | 16 | 17 | 18 | key 19 | 20 | 21 | edit 26 | edit 27 | 28 | 29 | 30 | 31 | 32 | show-all 36 | show-children 40 | collapse 44 | 45 | 46 |
{{node['info']['name']}} 47 | 48 | * [{{node['keys']}}] 49 | 50 | 51 | : 52 | 53 | 54 | 55 | {{node['value']}} 57 | info 59 | {{node['info']['datatype']}} 60 | ({{node['info']['datatypebase']}}) 61 | 62 | 63 | 64 | {{node['info']['datatype']}} 65 | ({{node['info']['datatypebase']}}) 66 | 67 | 68 |
69 | 70 | 71 |
{{treeService.moduleName(node)}}
72 |
73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 |
81 | 82 |
83 |
84 | 85 | 86 | 87 |
88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /frontend/config/tree.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |
6 | 7 | w 12 | x 17 |
18 |
19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 |
27 |
29 | 30 |
31 |
32 |
33 | 34 |
35 |
36 | 37 | 38 | 39 | 40 |
41 |
42 | 43 |
44 |
45 |
46 |
47 | 48 | 49 | 51 | 52 | 53 | 54 | 55 |
56 | -------------------------------------------------------------------------------- /frontend/config/tree.component.scss: -------------------------------------------------------------------------------- 1 | @import '../netopeer-common'; 2 | 3 | $colorChanged: #fafad2; 4 | $colorChangedBorder: #e4e4a4; 5 | $colorLineHover: #e1e1e1; 6 | 7 | .subtree { 8 | display: flex; 9 | flex-direction: column; 10 | width: 100%; 11 | 12 | .indentation{ 13 | height: 100%; 14 | } 15 | } 16 | 17 | .node { 18 | width: 100%; 19 | overflow: hidden; 20 | 21 | &:hover { 22 | background-color: $colorLineHover; 23 | } 24 | 25 | > div { 26 | display: inline-block; 27 | } 28 | } 29 | 30 | .node_edit { 31 | display: flex; 32 | width: 100%; 33 | overflow: hidden; 34 | 35 | img, 36 | input { 37 | display: inline-table; 38 | margin-right: 0.25em; 39 | } 40 | input { 41 | height: 2em; 42 | padding-left: 0.3em; 43 | background-color: $colorSuccess; 44 | 45 | &.invalid { 46 | background-color: $colorFailure; 47 | } 48 | } 49 | 50 | &.indentation { 51 | height:2.1em; 52 | } 53 | 54 | > .msg-failure { 55 | margin: 0.5em 0; 56 | } 57 | 58 | } 59 | .node_edit.dialog { 60 | background-color: $colorChanged;; 61 | } 62 | 63 | .editable { 64 | cursor: pointer; 65 | } 66 | 67 | .status, 68 | .keys { 69 | color: grey; 70 | } 71 | 72 | .dirty { 73 | background-color: $colorChanged; 74 | } 75 | 76 | .moved { 77 | background-color: #d8e8f8; 78 | } 79 | 80 | .deleted { 81 | background-color: $colorFailure; 82 | color: $colorFailureBorder; 83 | } 84 | 85 | .icon, 86 | .icon_action { 87 | font-size: xx-small; 88 | height: 2em; 89 | } 90 | 91 | .icon_action { 92 | cursor: pointer; 93 | } 94 | 95 | .icon_hidden { 96 | visibility: hidden; 97 | } 98 | 99 | .module_name { 100 | cursor: pointer; 101 | float: right; 102 | text-align: right; 103 | max-width: 25em; 104 | word-wrap: break-word; 105 | } 106 | 107 | .children { 108 | padding-left: 0; 109 | } 110 | 111 | .node_info { 112 | display: inline-block; 113 | word-wrap: break-word; 114 | margin-left: 3px; 115 | flex-grow: 1; 116 | 117 | .node_name { 118 | cursor: pointer; 119 | text-decoration: none; 120 | font-weight: normal; 121 | } 122 | } 123 | 124 | .value_inline { 125 | margin-left: 1.5em; 126 | } 127 | 128 | .indentation.value { 129 | width: 0.8em; 130 | height: 100%; 131 | } 132 | 133 | .indentation { 134 | width: 1.7em; 135 | height: 100%; 136 | } 137 | 138 | 139 | .editmenu { 140 | visibility: hidden; 141 | background-color: $colorBackground; 142 | border: 1px solid $black; 143 | 144 | position: fixed; 145 | z-index: 1; 146 | } 147 | 148 | .button_action { 149 | cursor: pointer; 150 | padding: 0.3em 1em 0.3em 0.5em; 151 | 152 | &:hover { 153 | background-color: $colorHighlight; 154 | color: $colorTextInverse; 155 | } 156 | } 157 | 158 | .loading { 159 | margin-top: 0.5em; 160 | } 161 | 162 | .yang-leaflist { 163 | display: flex; 164 | align-items: center; 165 | flex-direction: row; 166 | } 167 | 168 | .yang-leaflist-value { 169 | display: flex; 170 | } 171 | 172 | .yang-container, 173 | .yang-list { 174 | display:flex; 175 | align-items: center; 176 | } 177 | 178 | .yang-leaf { 179 | display: flex; 180 | align-items: center; 181 | } 182 | 183 | 184 | .value_standalone { 185 | flex-grow:1; 186 | word-break: break-all; 187 | } 188 | 189 | .ordered { 190 | cursor: move; 191 | } 192 | 193 | tree-indent { 194 | 195 | display: inline-flex; 196 | align-items: stretch; 197 | flex-direction: row; 198 | 199 | img{ 200 | display: inline-block; 201 | } 202 | } -------------------------------------------------------------------------------- /frontend/dashboard.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 14 | 15 | 17 | 18 |
 Currently connected devicesautoconnect
10 | x 13 | {{session.device.name}}
19 |
20 | -------------------------------------------------------------------------------- /frontend/dashboard.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import {Router} from '@angular/router'; 3 | 4 | import {SessionsService} from './config/sessions.service'; 5 | 6 | @Component({ 7 | selector : 'netopeer-dashboard', 8 | templateUrl : './dashboard.component.html', 9 | styleUrls : ['./netopeer.scss', 'inventory/inventory.component.scss'] 10 | }) 11 | 12 | export class DashboardComponent implements OnInit { 13 | 14 | constructor(public sessionsService: SessionsService, 15 | private router: Router) {} 16 | 17 | gotoConfig(session) { 18 | this.sessionsService.changeActiveSession(session.key); 19 | this.router.navigateByUrl('/netopeer/config'); 20 | } 21 | 22 | ngOnInit(): void { 23 | this.sessionsService.checkSessions(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /frontend/inventory/device.ts: -------------------------------------------------------------------------------- 1 | export class Device { 2 | constructor ( 3 | public id: number, 4 | public name:string = '', 5 | public hostname: string = '', 6 | public port: number = 830, 7 | public autoconnect: boolean = false, 8 | public username: string = '', 9 | public password: string = '', 10 | public fingerprint: string = '', 11 | ) {} 12 | } 13 | /* 14 | export class Device { 15 | id: number; 16 | hostname: string; 17 | port: number = 830; 18 | username: string; 19 | password: string; 20 | } 21 | */ 22 | -------------------------------------------------------------------------------- /frontend/inventory/devices.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 |
6 |
7 |
8 |
9 | 10 | 11 |
12 |
13 | 14 | 16 |
Hostname or IP address is required
17 |
18 |
19 | 20 | 22 |
Port must be in range 1 - 65535
23 |
24 |
25 | 26 | 28 |
User login is required to connect to the device.
29 |
30 |
31 | 32 | 34 |
User password must be specified to connect to the device.
35 |
36 |
37 | 38 | 40 |
41 |
42 | 43 | 48 |
49 | 50 |
51 | 52 | 53 | xfailed. 54 | xsuccessfully added. 55 | 56 |
57 |
58 |
59 |
60 |
61 | 62 |

x{{err_msg}}

63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 79 | 80 | 81 | 82 | 83 | 85 | 86 |
 namehostname : portfingerprintusernameautoconnect
74 | x 78 | {{device.name}}{{device.hostname}} : {{device.port}}{{device.fingerprint}}{{device.username}}
87 | 88 |
89 | -------------------------------------------------------------------------------- /frontend/inventory/devices.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { Observable } from 'rxjs/Observable'; 4 | import { catchError } from 'rxjs/operators'; 5 | 6 | import { Device } from './device'; 7 | 8 | @Injectable() 9 | export class DevicesService { 10 | constructor(private http: HttpClient) {} 11 | 12 | getDevices(): Observable { 13 | return this.http.get('/netopeer/inventory/devices/list') 14 | .pipe( 15 | catchError(err => Observable.throw(err)) 16 | ); 17 | } 18 | 19 | addDevice(device: Device) { 20 | // let options = new HttpOptions({ body: JSON.stringify(device) }); 21 | return this.http.post('/netopeer/inventory/devices', device) 22 | .pipe( 23 | catchError(err => Observable.throw(err)) 24 | ); 25 | } 26 | 27 | rmDevice(device_id: number) { 28 | // We need to use generic HTTP request, because HttpClient does not support body in DELETE requests. 29 | return this.http.request('DELETE', '/netopeer/inventory/devices', { body: JSON.stringify({'id':device_id}) }) 30 | .pipe( 31 | catchError(err => Observable.throw(err)) 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /frontend/inventory/inventory.component.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /frontend/inventory/inventory.component.scss: -------------------------------------------------------------------------------- 1 | @import '../netopeer-common'; 2 | 3 | nav { 4 | background-color: $colorMain; 5 | padding-left: 1em; 6 | 7 | a { 8 | text-decoration: none; 9 | display: inline-block; 10 | padding: 0.2em 1em 0.1em 1em; 11 | color: $colorTextInverse; 12 | 13 | &:visited, 14 | &:link { 15 | color: $colorTextInverse; 16 | } 17 | 18 | &:hover, 19 | &.active { 20 | background-color: $colorBackground; 21 | color: $colorText; 22 | } 23 | &.active:hover { 24 | cursor: default; 25 | } 26 | } 27 | } 28 | 29 | .items { 30 | width: 100%; 31 | border-radius: 3px; 32 | margin-bottom: 0.5em; 33 | } 34 | 35 | .item { 36 | cursor: pointer; 37 | margin-right: 0.5em; 38 | padding: 0.3em 1em 0.3em 0.7em; 39 | left: 0.5em; 40 | top: 0.5em; 41 | color: black; 42 | 43 | &:hover, 44 | &.selected { 45 | td { 46 | background-color: $colorMain; 47 | } 48 | color: $colorTextInverse; 49 | } 50 | &.selected:hover { 51 | cursor: default; 52 | } 53 | } 54 | 55 | .item_header { 56 | color: $colorTextInverse; 57 | th { 58 | background-color: $colorMain; 59 | } 60 | } 61 | 62 | .item_left { 63 | border-top-left-radius: 5px; 64 | border-bottom-left-radius: 5px; 65 | } 66 | 67 | .item_right { 68 | border-top-right-radius: 5px; 69 | border-bottom-right-radius: 5px; 70 | } 71 | 72 | .item_actions { 73 | cursor: pointer; 74 | width: 2em; 75 | padding-right: 10px; 76 | padding-left: 10px; 77 | font-weight: bold; 78 | font-family: monospace; 79 | font-size: large; 80 | color: $grey; 81 | } 82 | 83 | .item_action_delete { 84 | height: 1.3em; 85 | vertical-align: middle; 86 | 87 | &:hover { 88 | color: $red; 89 | } 90 | } 91 | 92 | .schema-revision { 93 | font-family: monospace; 94 | font-size: x-small; 95 | } 96 | 97 | .device-login { 98 | } 99 | 100 | .input_line { 101 | display: table-row; 102 | 103 | label, .input_label { 104 | display: table-cell; 105 | padding-right: 1em; 106 | } 107 | 108 | input, select, .input_data { 109 | display: table-cell; 110 | padding-left: 0.5em; 111 | margin-right: 0.5em; 112 | } 113 | } 114 | 115 | .input_line_alert { 116 | display: inline-block; 117 | background-color: $colorFailure; 118 | color: $red; 119 | } 120 | 121 | .input_control { 122 | padding-top: 1em; 123 | } 124 | 125 | form { 126 | input, 127 | select { 128 | border-left: 5px solid $colorSuccessBorder; 129 | &.invalid { 130 | border-left: 5px solid $colorSuccessBorder; 131 | } 132 | } 133 | } 134 | 135 | .input_switch { 136 | display: inline-block; 137 | text-align: center; 138 | margin-right: 1.5em; 139 | } -------------------------------------------------------------------------------- /frontend/inventory/inventory.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input } from '@angular/core'; 2 | import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; 3 | 4 | @Component({ 5 | selector : 'netopeer-inventory', 6 | templateUrl : './inventory.component.html', 7 | styleUrls : ['./inventory.component.scss'] 8 | }) 9 | 10 | export class InventoryComponent { 11 | title = 'Inventory'; 12 | inventoryComponents = [ 13 | 'devices', 14 | 'schemas' 15 | ]; 16 | 17 | constructor() { } 18 | } 19 | 20 | @Component({ 21 | selector: 'ngbd-modal-content', 22 | styleUrls: ['../netopeer.scss'], 23 | template: ` 26 | 33 | ` 36 | }) 37 | export class DialogueSchema implements OnInit { 38 | @Input() info; 39 | password = ''; 40 | 41 | constructor(public activeModal: NgbActiveModal) { } 42 | 43 | upload(schema: File) { 44 | let reader = new FileReader(); 45 | 46 | console.log(schema); 47 | reader.onloadend = () => { 48 | //console.log(reader.result); 49 | this.activeModal.close({'filename': schema.name, 'data': reader.result}); 50 | }; 51 | reader.readAsText(schema); 52 | } 53 | 54 | ngOnInit(): void { 55 | document.getElementById('uploadSchema').focus(); 56 | } 57 | } -------------------------------------------------------------------------------- /frontend/inventory/schema.ts: -------------------------------------------------------------------------------- 1 | export class Schema { 2 | constructor ( 3 | public key: string, 4 | public name: string = '', 5 | public revision: string = '', 6 | public type: string = '', 7 | public path: string = '', 8 | public data: any = null, 9 | public sections: string[] = [] 10 | ) {} 11 | } 12 | -------------------------------------------------------------------------------- /frontend/inventory/schemas.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | 6 |
7 |
8 |
9 | 10 | 11 | xParsing {{uploadSchema.value.replace("C:\\fakepath\\","")}} failed. 12 | xSchema {{uploadSchema.value.replace("C:\\fakepath\\","")}} successfully added. 13 | 14 |
15 |
16 |
17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 32 | 33 | 34 | 35 |
 namerevision
27 | x 31 | {{schema.name}}{{schema.revision}}
36 | 37 |
38 | -------------------------------------------------------------------------------- /frontend/inventory/schemas.component.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Schemas Inventory 3 | */ 4 | import { Component, Input, OnInit } from '@angular/core'; 5 | import { Router } from '@angular/router'; 6 | import { Schema } from './schema'; 7 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; 8 | 9 | import {DialogueSchema} from './inventory.component'; 10 | 11 | import { SchemasService } from '../yang/schemas.service' 12 | import {SocketService} from 'app/services/socket.service'; 13 | 14 | @Component( { 15 | selector: 'inventorySchemas', 16 | templateUrl: './schemas.component.html', 17 | styleUrls: ['./inventory.component.scss'] 18 | } ) 19 | 20 | export class InventorySchemasComponent implements OnInit { 21 | schemas; 22 | addingSchema = false; 23 | addingResult = -1; 24 | constructor(private schemasService: SchemasService, 25 | private socketService: SocketService, 26 | private modalService: NgbModal, 27 | private router: Router ) { 28 | this.schemas = []; 29 | } 30 | 31 | getSchemas(): void { 32 | this.schemasService.getSchemas().subscribe( result => {this.schemas = result;}); 33 | } 34 | 35 | showAddSchema() { 36 | this.addingSchema = !this.addingSchema; 37 | this.addingResult = -1; 38 | } 39 | 40 | socketAnswer(event: string, id:string, item: string, value: any, item2: string, value2: any) { 41 | let data = {'id': id}; 42 | data[item] = value; 43 | data[item2] = value2 44 | this.socketService.send(event, data); 45 | } 46 | 47 | upload(schema: File) { 48 | if (!schema) { 49 | /* do nothing */ 50 | return; 51 | } 52 | 53 | this.socketService.subscribe('getschema').subscribe((message: any) => { 54 | let modalRef = this.modalService.open(DialogueSchema, {centered: true, backdrop: 'static', keyboard: false}); 55 | modalRef.componentInstance.info = message; 56 | modalRef.result.then((result) => { 57 | this.socketAnswer('getschema_result', message['id'], 'filename', result['filename'], 'data', result['data']); 58 | }, (reason) => { 59 | this.socketAnswer('getschema_result', message['id'], 'filename', '', 'data', ''); 60 | }); 61 | }); 62 | 63 | /* upload the schema file to the server, if success the schema list is refreshed */ 64 | this.schemasService.addSchema(schema).subscribe(result => { 65 | this.socketService.unsubscribe('getschema'); 66 | this.addingResult = result['success'] ? 1 : 0; 67 | this.getSchemas(); 68 | }); 69 | } 70 | 71 | remove(key: string) { 72 | this.schemasService.rmSchema(key).subscribe( 73 | result => { if ( result['success'] ) { this.getSchemas() } } ); 74 | } 75 | 76 | ngOnInit(): void { 77 | this.getSchemas(); 78 | } 79 | 80 | onSelect(key: string): void { 81 | this.schemasService.show(key) 82 | .subscribe((result: object) => { 83 | if (result['success']) { 84 | this.router.navigateByUrl( '/netopeer/yang' ); 85 | } 86 | }); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /frontend/monitoring/monitoring.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |

Monitoring (TBD)

4 |

Work with NETCONF notifications from the connected devices.

5 |
    6 |
  • subscribing for notifications
  • 7 |
  • setting up time windows, filters, search in the received data
  • 8 |
  • background monitoring (receiving notifications) with alerts sent via email
  • 9 |
10 | 11 |
-------------------------------------------------------------------------------- /frontend/monitoring/monitoring.component.scss: -------------------------------------------------------------------------------- 1 | @import '../netopeer-common'; -------------------------------------------------------------------------------- /frontend/monitoring/monitoring.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector : 'netopeer-config', 5 | templateUrl : './monitoring.component.html', 6 | styleUrls : ['./monitoring.component.scss'] 7 | }) 8 | 9 | export class MonitoringComponent { 10 | title = 'Monitoring'; 11 | } 12 | -------------------------------------------------------------------------------- /frontend/netopeer.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Netopeer {{componentTitle}}

3 | 4 | 8 | 9 |
10 |
11 | 12 | 13 |
14 | -------------------------------------------------------------------------------- /frontend/netopeer.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | import { SessionsService } from './config/sessions.service'; 4 | import { DevicesService } from './inventory/devices.service'; 5 | import { Device } from './inventory/device'; 6 | 7 | class NComponent { 8 | route: string; 9 | name: string; 10 | } 11 | 12 | const NCOMPONENTS: NComponent[] = [ 13 | { route : 'inventory', name: 'Inventory' }, 14 | { route : 'config', name: 'Configuration' }, 15 | { route : 'yang', name: 'YANG Explorer' }, 16 | { route : 'monitoring', name: 'Monitoring' }, 17 | { route : 'plugins', name: 'Plugins' } 18 | ]; 19 | 20 | @Component({ 21 | selector : 'netopeer', 22 | templateUrl : './netopeer.component.html', 23 | styleUrls : ['./netopeer.scss'], 24 | }) 25 | 26 | export class NetopeerComponent implements OnInit { 27 | componentTitle = ''; 28 | netopeerComponents = NCOMPONENTS; 29 | 30 | constructor(private sessionsService: SessionsService, 31 | private devicesService: DevicesService) { } 32 | 33 | ngOnInit() { 34 | /* autoconnect selected devices if needed */ 35 | if (localStorage.getItem('netopeer-autoconnect') == 'enabled') { 36 | let ac_sessions: number[] = []; /* currently connected autoconnect devices' ids */ 37 | for (let session of this.sessionsService.sessions) { 38 | if (session.device.autoconnect) { 39 | ac_sessions.push(session.device.id); 40 | } 41 | } 42 | let ac_devices: Device[] = []; /* devices with enabled autoconnect */ 43 | this.devicesService.getDevices().subscribe(devices => { 44 | for (let device of devices) { 45 | if (!device['autoconnect']) { 46 | continue; 47 | } 48 | let i = ac_sessions.indexOf(device.id); 49 | if (i != -1) { 50 | ac_sessions.splice(i, 1); 51 | continue; 52 | } 53 | /* we have not connected autoconnect device */ 54 | ac_devices.push(device); 55 | } 56 | for (let device of ac_devices) { 57 | this.sessionsService.connect(device).subscribe(); 58 | } 59 | localStorage.setItem('netopeer-autoconnect', 'done'); 60 | }); 61 | } 62 | } 63 | 64 | onActivate(componentRef) { 65 | this.componentTitle = componentRef.title; 66 | } 67 | onDeactivate(componentRef) { 68 | this.componentTitle = ''; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /frontend/netopeer.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, OnInit } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { Routes, RouterModule } from '@angular/router'; 4 | import { FormsModule } from '@angular/forms'; 5 | import { ReactiveFormsModule } from '@angular/forms'; 6 | import { HttpClientModule } from '@angular/common/http'; 7 | 8 | import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; 9 | 10 | import { AuthGuard } from 'app/utils/auth.guard'; 11 | 12 | import {MatProgressSpinnerModule} from '@angular/material/progress-spinner'; 13 | import {MatProgressBarModule} from '@angular/material/progress-bar'; 14 | 15 | import { LoadingComponent } from './common/loading/loading.component'; 16 | import { OrderingDirective } from './config/ordering.directive'; 17 | 18 | import { NetopeerComponent } from './netopeer.component'; 19 | import { DashboardComponent } from './dashboard.component'; 20 | import { InventoryComponent, DialogueSchema } from './inventory/inventory.component'; 21 | import { InventorySchemasComponent } from './inventory/schemas.component'; 22 | import { InventoryDevicesComponent, DialogueHostcheck, DialoguePassword } from './inventory/devices.component'; 23 | import { ConfigComponent } from './config/config.component'; 24 | import { TreeView, TreeNode, TreeLeaflistValue, TreeIndent, TreeCreate, TreeEdit, TreeScrollTo, CheckLeafValue } from './config/tree.component'; 25 | import { YANGComponent, YANGModule, YANGIdentity, YANGFeature, YANGTypedef, YANGType, YANGRestriction, YANGNode, YANGIffeature } from './yang/yang.component'; 26 | import { MonitoringComponent } from './monitoring/monitoring.component'; 27 | import { PluginsComponent } from './plugins/plugins.component'; 28 | 29 | import { SessionsService } from './config/sessions.service'; 30 | import { SchemasService } from './yang/schemas.service'; 31 | import { DevicesService } from './inventory/devices.service'; 32 | import { TreeService } from './config/tree.service'; 33 | 34 | import { NoPrefixPipe, PrefixOnlyPipe, PatternHighlightPipe } from './common/pipes'; 35 | 36 | const routes: Routes = [ 37 | { path : 'netopeer', component : NetopeerComponent, canActivate : [AuthGuard], 38 | data : { role : 10, name : 'Netopeer', description : 'Network Management Center', icon : 'fa-gears' }, 39 | children: [{ 40 | path : 'dashboard', 41 | component : DashboardComponent, 42 | canActivate : [AuthGuard], 43 | data : { role : 10, name : 'Netopeer Dashboard'} 44 | }, { 45 | path : 'inventory', 46 | component : InventoryComponent, 47 | canActivate : [AuthGuard], 48 | data : { role : 10, name : 'Netopeer Items Inventories'}, 49 | children : [{ 50 | path : '', 51 | redirectTo: 'devices', 52 | pathMatch: 'full', 53 | }, { 54 | path : 'devices', 55 | component : InventoryDevicesComponent, 56 | canActivate : [AuthGuard], 57 | data : { role : 10, name : 'NETCONF Devices Inventory'} 58 | }, { 59 | path : 'schemas', 60 | component : InventorySchemasComponent, 61 | canActivate : [AuthGuard], 62 | data : { role : 10, name : 'YANG Schemas Inventory'} 63 | }] 64 | }, { 65 | path : 'config', 66 | component : ConfigComponent, 67 | canActivate : [AuthGuard], 68 | data : { role : 10, name : 'Netopeer Device Configuration'}, 69 | }, { 70 | path : 'yang', 71 | component : YANGComponent, 72 | canActivate : [AuthGuard], 73 | data : { role : 10, name : 'Netopeer YANG Explorer'}, 74 | }, { 75 | path : 'monitoring', 76 | component : MonitoringComponent, 77 | canActivate : [AuthGuard], 78 | data : { role : 10, name : 'Netopeer Device Monitoring'}, 79 | }, { 80 | path : 'plugins', 81 | component : PluginsComponent, 82 | canActivate : [AuthGuard], 83 | data : { role : 10, name : 'Netopeer Plugins'}, 84 | }] 85 | } 86 | ] 87 | 88 | @NgModule({ 89 | imports: [ 90 | CommonModule, 91 | FormsModule, 92 | ReactiveFormsModule, 93 | HttpClientModule, 94 | NgbModule.forRoot(), 95 | RouterModule.forChild(routes), 96 | MatProgressSpinnerModule, 97 | MatProgressBarModule, 98 | ], 99 | declarations: [ 100 | NetopeerComponent, 101 | DashboardComponent, 102 | InventoryComponent, 103 | InventorySchemasComponent, 104 | InventoryDevicesComponent, 105 | ConfigComponent, 106 | LoadingComponent, 107 | OrderingDirective, 108 | CheckLeafValue, 109 | TreeScrollTo, 110 | TreeIndent, 111 | TreeEdit, 112 | TreeCreate, 113 | TreeLeaflistValue, 114 | TreeNode, 115 | TreeView, 116 | YANGComponent, 117 | YANGModule, 118 | YANGIdentity, 119 | YANGFeature, 120 | YANGTypedef, 121 | YANGType, 122 | YANGRestriction, 123 | YANGNode, 124 | YANGIffeature, 125 | MonitoringComponent, 126 | PluginsComponent, 127 | DialogueHostcheck, 128 | DialoguePassword, 129 | DialogueSchema, 130 | NoPrefixPipe, 131 | PrefixOnlyPipe, 132 | PatternHighlightPipe 133 | ], 134 | providers: [ 135 | SessionsService, 136 | SchemasService, 137 | DevicesService, 138 | TreeService 139 | ], 140 | entryComponents : [ 141 | NetopeerComponent, 142 | DialogueHostcheck, 143 | DialoguePassword, 144 | DialogueSchema 145 | ] 146 | }) 147 | export class NetopeerModule { } 148 | 149 | export class NetopeerModuleHooks { 150 | constructor () { } 151 | 152 | login() { 153 | localStorage.setItem('netopeer-autoconnect', 'enabled'); 154 | } 155 | 156 | logout() { 157 | localStorage.removeItem('netopeer-autoconnect'); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /frontend/netopeer.scss: -------------------------------------------------------------------------------- 1 | @import 'netopeer-common'; 2 | 3 | #netopeer-header { 4 | position: fixed; 5 | width: 100%; 6 | margin: -0.5em -1em 0em -1em; 7 | padding-top: 1em; 8 | background-color: $colorMain; 9 | color: $colorTextInverse; 10 | display: block; 11 | } 12 | 13 | #netopeer-header h1 { 14 | margin-left: 1em; 15 | color: $colorTextInverse; 16 | } 17 | 18 | #netopeer-component { 19 | margin: 0em -1em 0em -1em; 20 | } 21 | 22 | #mainnav { 23 | with: 100%; 24 | padding-left: 1em; 25 | } 26 | 27 | #mainnav a:visited, 28 | #mainnav a:link { 29 | color: inherit; 30 | } 31 | 32 | #mainnav a:hover { 33 | border-top-color: $colorHighlight; 34 | background-color: $colorHighlight; 35 | } 36 | 37 | #mainnav a.active { 38 | border-top-color: $colorSelected2; 39 | background-color: $colorSelected; 40 | } 41 | #mainnav a.active:hover { 42 | cursor: default; 43 | } 44 | 45 | #mainnav a { 46 | text-decoration: none; 47 | display: inline-block; 48 | padding: 0.5em 1em 0.5em 0.5em; 49 | color: $colorTextInverse; 50 | border-top: 0.2em solid $colorMain; 51 | } 52 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Netopeer2GUI", 3 | "version": "0.1.0", 4 | "description": "NETCONF management center", 5 | "main": "index.html", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/CESNET/Netopeer2GUI.git" 9 | }, 10 | "author": "", 11 | "license": "BSD", 12 | "bugs": { 13 | "url": "https://github.com/CESNET/Netopeer2GUI/issues" 14 | }, 15 | "homepage": "https://github.com/CESNET/Netopeer2GUI", 16 | "dependencies" : { 17 | "@angular/material": "^6.0.0", 18 | "@angular/cdk": "^6.0.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /frontend/plugins/plugins.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |

Plugins (TBD)

4 |

Framework for schema(s) specific applications - simplified/more user friendly configuration approach than the generic configuration tree in the configuration tab, for example:

5 |
    6 |
  • visual network connections configuration
  • 7 |
  • connected devices time synchronisation checking
  • 8 |
  • ...
  • 9 |
10 | {{text}} 11 |
-------------------------------------------------------------------------------- /frontend/plugins/plugins.component.scss: -------------------------------------------------------------------------------- 1 | @import '../netopeer-common'; -------------------------------------------------------------------------------- /frontend/plugins/plugins.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector : 'netopeer-plugins', 5 | templateUrl : './plugins.component.html', 6 | styleUrls : ['./plugins.component.scss'] 7 | }) 8 | 9 | export class PluginsComponent implements OnInit { 10 | title = 'Plugins'; 11 | text = "test... ignore"; 12 | 13 | sleep(ms) { 14 | return new Promise(resolve => setTimeout(resolve, ms)); 15 | } 16 | 17 | async ngOnInit() { 18 | await this.sleep(2000); 19 | this.text = "still testing... still ignore"; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /frontend/yang/yang.component.html: -------------------------------------------------------------------------------- 1 | 15 |
16 | Start here with that + sign. 17 |
18 | back 20 | 21 | interactive 23 | text 25 | 26 | module 28 |
29 | 30 |
{{activeSchema.data}}
31 | 32 | 33 | 34 | 35 | 36 | 37 |
{{activeSchema.data | json}}
38 |
39 |
40 | 41 |
42 | -------------------------------------------------------------------------------- /frontend/yang/yang.component.scss: -------------------------------------------------------------------------------- 1 | @import '../netopeer-common'; 2 | @import '../inventory/inventory.component'; 3 | 4 | $colorLineHover: #e1e1e1; 5 | $colorLineSelected: #999999; 6 | 7 | .nav-button { 8 | height: 2em; 9 | cursor: pointer; 10 | margin: 0.3em; 11 | } 12 | 13 | .loading { 14 | text-align: center; 15 | margin: auto; 16 | width: 10em; 17 | div { 18 | margin: auto; 19 | width: 50px; 20 | } 21 | } 22 | 23 | .yang-infobox { 24 | display: block; 25 | cursor: default; 26 | } 27 | 28 | .yang-info-section-label, 29 | .yang-info-subsection-label, 30 | .yang-info-label { 31 | color: black; 32 | font-weight: 100; 33 | text-transform: uppercase; 34 | } 35 | 36 | .yang-info-section { 37 | display: flex; 38 | flex-direction: column; 39 | flex-wrap: wrap; 40 | width: 100%; 41 | padding-left: 2em; 42 | } 43 | 44 | .yang-info-subsection { 45 | .yang-info { 46 | padding-left: 2em; 47 | } 48 | } 49 | 50 | .yang-info, 51 | .yang-info-label, 52 | .yang-info-value { 53 | border: none; 54 | a { 55 | font-weight: normal; 56 | } 57 | } 58 | 59 | .yang-info { 60 | display: flex; 61 | padding: 5px; 62 | } 63 | .yang-info:hover { 64 | background-color: $colorLineHover; 65 | } 66 | 67 | .yang-info:nth-of-type(even), .yang-info-subsection:nth-of-type(even) { 68 | background: lighten($colorLineHover, 5%); 69 | } 70 | 71 | .yang-info-value { 72 | display: inline-block; 73 | } 74 | 75 | .yang-info-section-label, 76 | .yang-info-subsection-label, 77 | .yang-info-label { 78 | min-width: 10em; 79 | overflow: hidden; 80 | text-overflow: ellipsis; 81 | display: inline-block; 82 | } 83 | .yang-revision-label { 84 | font-style: italic; 85 | } 86 | 87 | .yang-subsection-container { 88 | margin-left: 2em; 89 | } 90 | 91 | .pattern { 92 | .selectedGroup { 93 | background-color: $colorLineSelected; 94 | } 95 | .bracket { 96 | } 97 | } 98 | 99 | -------------------------------------------------------------------------------- /frontend/yang/yang.feature.html: -------------------------------------------------------------------------------- 1 |
2 |

Feature {{name}}

3 |
4 |
5 | 6 |
7 |
8 | 9 |
10 |
11 | status{{data.status.value}} 12 |
13 |
14 | description 15 |
16 |
17 | reference 18 |
19 |
20 | 21 |
22 | 23 | 24 | -------------------------------------------------------------------------------- /frontend/yang/yang.identity.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |

Identity {{name}}

4 |
5 |
6 | 7 |
8 |
9 | 10 |
11 |
12 | base{{value | noPrefix}} 13 |
14 |
15 | 16 |
17 |
18 | status{{data.status.value}} 19 |
20 |
21 | description 22 |
23 |
24 | reference 25 |
26 |
27 | 28 |
29 | 30 | 31 | -------------------------------------------------------------------------------- /frontend/yang/yang.restriction.html: -------------------------------------------------------------------------------- 1 |
2 | {{name}} 3 | 4 |
5 |
6 | {{name}} 7 | {{data.value}} 8 |
9 |
10 |
11 | modifier 12 | {{data.modifier.value}} 13 |
14 |
15 | error-message 16 | {{data['error-message'].value}} 17 |
18 |
19 | error-app-tag 20 | {{data['error-app-tag'].value}} 21 |
22 |
23 | description 24 |
{{data.description.text}}
25 |
26 |
27 | reference 28 |
{{data.reference.text}}
29 |
30 |
-------------------------------------------------------------------------------- /frontend/yang/yang.type.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | base 5 | 6 | {{data['derived-from'] | noPrefix}} ({{data['basetype']}}) 7 | 8 | 9 | {{data['derived-from'] | noPrefix}} 10 | 11 |
12 |
13 | base{{data['basetype']}} 14 |
15 |
16 | 17 | 18 |
19 |
20 | fraction-digits{{data['fraction-digits'].value}} 21 |
22 | 23 | 24 | 25 | 26 | 27 |
28 | path 29 | 30 | 31 | /{{parent.id}} 32 | 33 | 34 | {{data.path.value}} 35 |
36 |
37 | require-instance{{data['require-instance'].value}} 38 |
39 |
40 | 41 | 42 |
43 | 44 |
45 | bit {{bit.position}}{{bit.name}} 46 |
47 |
48 | 49 |
50 | 51 |
52 |
53 |
54 | status{{bit.status.value}} 55 |
56 |
57 | description 58 |
59 |
60 | reference 61 |
62 |
63 |
64 |
65 | 66 | 67 |
68 | 69 |
70 | enum {{enum.name}}value {{enum.value}} 71 |
72 |
73 | 74 |
75 | 76 |
77 |
78 |
79 | status{{enum.status.value}} 80 |
81 |
82 | description 83 |
84 |
85 | reference 86 |
87 |
88 |
89 |
90 | 91 | 92 |
93 |
94 | base{{value | noPrefix}} 95 |
96 |
97 | 98 | 99 |
100 | 101 |
102 | type 103 |
104 |
105 | 106 |
107 |
108 |
109 |
110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /frontend/yang/yang.typedef.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |

Typedef {{name}}

4 | 5 | 6 | 7 |
8 |
9 | units{{data.units.name}} 10 |
11 |
12 | default{{data.default.value}} 13 |
14 |
15 | status{{data.status.value}} 16 |
17 |
18 | description 19 |
20 |
21 | reference 22 |
23 |
24 | 25 |
26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /vagrant/OpenSUSE/Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | $tools = <