diff --git a/src/webui/proto/service_pb2.py b/src/webui/proto/service_pb2.py index 806ec46386848e727cd6bb43f73501d0ce9dce69..7a006915b8be39710a17faab075e382e322d918f 100644 --- a/src/webui/proto/service_pb2.py +++ b/src/webui/proto/service_pb2.py @@ -20,7 +20,7 @@ DESCRIPTOR = _descriptor.FileDescriptor( syntax='proto3', serialized_options=None, create_key=_descriptor._internal_create_key, - serialized_pb=b'\n\rservice.proto\x12\x07service\x1a\rcontext.proto2\xed\x02\n\x0eServiceService\x12\x38\n\x0eGetServiceList\x12\x0e.context.Empty\x1a\x14.context.ServiceList\"\x00\x12\x37\n\rCreateService\x12\x10.context.Service\x1a\x12.context.ServiceId\"\x00\x12\x37\n\rUpdateService\x12\x10.context.Service\x1a\x12.context.ServiceId\"\x00\x12\x35\n\rDeleteService\x12\x12.context.ServiceId\x1a\x0e.context.Empty\"\x00\x12\x38\n\x0eGetServiceById\x12\x12.context.ServiceId\x1a\x10.context.Service\"\x00\x12>\n\x11GetConnectionList\x12\x0e.context.Empty\x1a\x17.context.ConnectionList\"\x00\x62\x06proto3' + serialized_pb=b'\n\rservice.proto\x12\x07service\x1a\rcontext.proto2\xfd\x01\n\x0eServiceService\x12\x37\n\rCreateService\x12\x10.context.Service\x1a\x12.context.ServiceId\"\x00\x12\x37\n\rUpdateService\x12\x10.context.Service\x1a\x12.context.ServiceId\"\x00\x12\x35\n\rDeleteService\x12\x12.context.ServiceId\x1a\x0e.context.Empty\"\x00\x12\x42\n\x11GetConnectionList\x12\x12.context.ServiceId\x1a\x17.context.ConnectionList\"\x00\x62\x06proto3' , dependencies=[context__pb2.DESCRIPTOR,]) @@ -38,22 +38,12 @@ _SERVICESERVICE = _descriptor.ServiceDescriptor( serialized_options=None, create_key=_descriptor._internal_create_key, serialized_start=42, - serialized_end=407, + serialized_end=295, methods=[ - _descriptor.MethodDescriptor( - name='GetServiceList', - full_name='service.ServiceService.GetServiceList', - index=0, - containing_service=None, - input_type=context__pb2._EMPTY, - output_type=context__pb2._SERVICELIST, - serialized_options=None, - create_key=_descriptor._internal_create_key, - ), _descriptor.MethodDescriptor( name='CreateService', full_name='service.ServiceService.CreateService', - index=1, + index=0, containing_service=None, input_type=context__pb2._SERVICE, output_type=context__pb2._SERVICEID, @@ -63,7 +53,7 @@ _SERVICESERVICE = _descriptor.ServiceDescriptor( _descriptor.MethodDescriptor( name='UpdateService', full_name='service.ServiceService.UpdateService', - index=2, + index=1, containing_service=None, input_type=context__pb2._SERVICE, output_type=context__pb2._SERVICEID, @@ -73,29 +63,19 @@ _SERVICESERVICE = _descriptor.ServiceDescriptor( _descriptor.MethodDescriptor( name='DeleteService', full_name='service.ServiceService.DeleteService', - index=3, + index=2, containing_service=None, input_type=context__pb2._SERVICEID, output_type=context__pb2._EMPTY, serialized_options=None, create_key=_descriptor._internal_create_key, ), - _descriptor.MethodDescriptor( - name='GetServiceById', - full_name='service.ServiceService.GetServiceById', - index=4, - containing_service=None, - input_type=context__pb2._SERVICEID, - output_type=context__pb2._SERVICE, - serialized_options=None, - create_key=_descriptor._internal_create_key, - ), _descriptor.MethodDescriptor( name='GetConnectionList', full_name='service.ServiceService.GetConnectionList', - index=5, + index=3, containing_service=None, - input_type=context__pb2._EMPTY, + input_type=context__pb2._SERVICEID, output_type=context__pb2._CONNECTIONLIST, serialized_options=None, create_key=_descriptor._internal_create_key, diff --git a/src/webui/service/device/forms.py b/src/webui/service/device/forms.py index ba3d05ea76731fca85c7f5cfb45b0f705dd81935..6dda2cbba946a583c55b13d03d396fbec58e8c67 100644 --- a/src/webui/service/device/forms.py +++ b/src/webui/service/device/forms.py @@ -1,16 +1,41 @@ # external imports from flask_wtf import FlaskForm -from wtforms import StringField, SelectField, SubmitField -from wtforms.validators import DataRequired, Length, NumberRange +from wtforms import StringField, SelectField, TextAreaField, SubmitField +from wtforms.validators import DataRequired, Length, NumberRange, Regexp, ValidationError +from webui.utils.form_validators import key_value_validator +from webui.proto.context_pb2 import (DeviceDriverEnum, DeviceOperationalStatusEnum) class AddDeviceForm(FlaskForm): - device_id = StringField('Device ID', + device_id = StringField('ID', validators=[DataRequired(), Length(min=5)]) - device_type = StringField('Device Type', + device_type = StringField('Type', validators=[DataRequired(), Length(min=5)]) + device_config = TextAreaField('Configurations', validators=[key_value_validator()]) operational_status = SelectField('Operational Status', - choices=[(-1, 'Select...'), (0, 'Undefined'), (1, 'Disabled'), (2, 'Enabled')], + # choices=[(-1, 'Select...'), (0, 'Undefined'), (1, 'Disabled'), (2, 'Enabled')], coerce=int, - validators=[DataRequired(), NumberRange(min=0)]) + validators=[NumberRange(min=0)]) + device_drivers = TextAreaField('Drivers', validators=[DataRequired(), Regexp('^\d+(,\d+)*$')]) submit = SubmitField('Add') + + def validate_operational_status(form, field): + if field.data not in DeviceOperationalStatusEnum.DESCRIPTOR.values_by_number: + raise ValidationError('The operational status value selected is incorrect!') + + def validate_device_drivers(form, field): + if ',' not in field.data: + data = str(field.data) + ',' + else: + data = field.data + for value in data.split(','): + value = value.strip() + if len(value) == 0: + continue + try: + value_int = int(value) + except: + raise ValidationError(f'The value "{value}" is not a valid driver identified.') + if value_int not in DeviceDriverEnum.DESCRIPTOR.values_by_number: + values = ', '.join([str(x) for x in DeviceDriverEnum.DESCRIPTOR.values_by_number]) + raise ValidationError(f'The device driver {value_int} is not correct. Allowed values are: {values}.') diff --git a/src/webui/service/device/routes.py b/src/webui/service/device/routes.py index 2773d088017e4f9459d936a5fee27f733016eb82..4d95faff877b2afa721261688a018b9970d003cd 100644 --- a/src/webui/service/device/routes.py +++ b/src/webui/service/device/routes.py @@ -1,4 +1,5 @@ from flask import render_template, Blueprint, flash, session +from context.proto.context_pb2 import ConfigActionEnum, ConfigRule, TopologyIdList, TopologyList from webui.Config import CONTEXT_SERVICE_ADDRESS, CONTEXT_SERVICE_PORT from context.client.ContextClient import ContextClient from webui.proto.context_pb2 import (ContextId, DeviceList, DeviceId, @@ -21,10 +22,63 @@ def home(): @device.route('add', methods=['GET', 'POST']) def add(): form = AddDeviceForm() + + request: ContextId = ContextId() + request.context_uuid.uuid = session['context_uuid'] + client: ContextClient = ContextClient(CONTEXT_SERVICE_ADDRESS, CONTEXT_SERVICE_PORT) + response: TopologyIdList = client.ListTopologyIds(request) + client.close() + + # listing enum values + form.operational_status.choices = [(-1, 'Select...')] + for key, value in DeviceOperationalStatusEnum.DESCRIPTOR.values_by_name.items(): + form.operational_status.choices.append((DeviceOperationalStatusEnum.Value(key), key.replace('DEVICEOPERATIONALSTATUS_', ''))) + + # device driver ids + device_driver_ids = [] + for key in DeviceDriverEnum.DESCRIPTOR.values_by_name: + device_driver_ids.append(f"{DeviceDriverEnum.Value(key)}={key.replace('DEVICEDRIVER_', '')}") + device_driver_ids = ', '.join(device_driver_ids) + if form.validate_on_submit(): - pass + device: Device = Device() + device.device_id.device_uuid.uuid = form.device_id.data + device.device_type = form.device_type.data + if '\n' not in form.device_config.data: + data = form.device_config.data.strip() + '\n' + else: + data = form.device_config.data.strip() + + for config in data.split('\n'): + if len(config.strip()) > 0: + parts = config.strip().split('=') + config_rule: ConfigRule = ConfigRule() + config_rule.action = ConfigActionEnum.CONFIGACTION_SET + config_rule.resource_key = parts[0].strip() + config_rule.resource_value = parts[1].strip() + device.device_config.config_rules.extend([config_rule]) + + device.device_operational_status = form.operational_status.data + + if ',' not in form.device_drivers.data: + data = form.device_drivers.data.strip() + ',' + else: + data = form.device_drivers.data.strip() + + for driver in data.split(','): + driver = driver.strip() + if len(driver) == 0: + continue + device.device_drivers.extend([int(driver)]) + client: ContextClient = ContextClient(CONTEXT_SERVICE_ADDRESS, CONTEXT_SERVICE_PORT) + response: DeviceId = client.SetDevice(request) + client.close() + + flash(f'New device was created with ID "{response.device_uuid.uuid}".', 'success') + return render_template('device/add.html', form=form, - submit_text='Add New Device') + submit_text='Add New Device', + device_driver_ids=device_driver_ids) @device.route('detail/<device_uuid>', methods=['GET', 'POST']) def detail(device_uuid: str): @@ -32,6 +86,5 @@ def detail(device_uuid: str): request.device_uuid.uuid = device_uuid client: ContextClient = ContextClient(CONTEXT_SERVICE_ADDRESS, CONTEXT_SERVICE_PORT) response: Device = client.GetDevice(request) - print(response) client.close() return render_template('device/detail.html', device=response) diff --git a/src/webui/service/static/site.js b/src/webui/service/static/site.js new file mode 100644 index 0000000000000000000000000000000000000000..e06cea8b0bed68602ad91ed5dc52229b7b7aad75 --- /dev/null +++ b/src/webui/service/static/site.js @@ -0,0 +1,4 @@ +var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')) +var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) { + return new bootstrap.Tooltip(tooltipTriggerEl) +}) diff --git a/src/webui/service/templates/base.html b/src/webui/service/templates/base.html index e4a9e58d6201594ecfaacfdf1e91fbdb29c532a6..862ae6b44f745baf3f787634f95421fb2f0459bc 100644 --- a/src/webui/service/templates/base.html +++ b/src/webui/service/templates/base.html @@ -108,6 +108,7 @@ <!-- Option 1: Bootstrap Bundle with Popper --> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-kQtW33rZJAHjgefvhyyzcGF3C5TFyBQBA13V1RKPf4uH+bwyzQxZ6CmMZHmNBEfJ" crossorigin="anonymous"></script> + <!-- <script src="{{ url_for('static', filename='site.js') }}"/> --> <!-- Option 2: Separate Popper and Bootstrap JS --> <!-- diff --git a/src/webui/service/templates/device/add.html b/src/webui/service/templates/device/add.html index e26bf0c36ab14b4cb925da87907f2384ada3e470..dae31a37689e9c7e9704401c38e3182aaed8c311 100644 --- a/src/webui/service/templates/device/add.html +++ b/src/webui/service/templates/device/add.html @@ -21,6 +21,70 @@ {% endif %} </div> </div> + <div class="row mb-3"> + {{ form.device_type.label(class="col-sm-2 col-form-label") }} + <div class="col-sm-10"> + {% if form.device_type.errors %} + {{ form.device_type(class="form-control is-invalid") }} + <div class="invalid-feedback"> + {% for error in form.device_type.errors %} + <span>{{ error }}</span> + {% endfor %} + </div> + {% else %} + {{ form.device_type(class="form-control") }} + {% endif %} + </div> + </div> + <div class="row mb-3"> + {{ form.operational_status.label(class="col-sm-2 col-form-label") }} + <div class="col-sm-10"> + {% if form.operational_status.errors %} + {{ form.operational_status(class="form-control is-invalid") }} + <div class="invalid-feedback"> + {% for error in form.operational_status.errors %} + <span>{{ error }}</span> + {% endfor %} + </div> + {% else %} + {{ form.operational_status(class="form-control") }} + {% endif %} + </div> + </div> + <div class="row mb-3"> + {{ form.device_config.label(class="col-sm-2 col-form-label") }} + <div class="col-sm-10"> + {% if form.device_config.errors %} + {{ form.device_config(class="form-control is-invalid", rows=5) }} + <div class="invalid-feedback"> + {% for error in form.device_config.errors %} + <span>{{ error }}</span> + {% endfor %} + </div> + {% else %} + {{ form.device_config(class="form-control", rows=5) }} + {% endif %} + </div> + <div id="device_config_help" class="form-text">The device configurations should follow a <i>key=value</i> format, one configuration per line.</div> + </div> + <div class="row mb-3"> + {{ form.device_drivers.label(class="col-sm-2 col-form-label") }} + <div class="col-sm-10"> + {% if form.device_drivers.errors %} + {{ form.device_drivers(class="form-control is-invalid", rows=5) }} + <div class="invalid-feedback"> + {% for error in form.device_drivers.errors %} + <span>{{ error }}</span> + {% endfor %} + </div> + {% else %} + {{ form.device_drivers(class="form-control", rows=5) }} + {% endif %} + </div> + <div id="device_drivers_help" class="form-text"> + List the device drivers by their numerical ID, separated by commas, without spaces between them. Numerical IDs: {{ device_driver_ids }}. + </div> + </div> <div class="row mb-3"> <button type="submit" class="btn btn-primary">{{ submit_text }}</button> </div> diff --git a/src/webui/tests/test_unitary.py b/src/webui/tests/test_unitary.py index f73621f29d98bf9bdfd7ec3c498dd62455475c23..3096e66127c33c1ef5e45d927056646998a85bf2 100644 --- a/src/webui/tests/test_unitary.py +++ b/src/webui/tests/test_unitary.py @@ -68,7 +68,7 @@ def test_device_detail_page(client, device_id): assert rw.status_code == 200 assert b'Device' in rw.data assert device_id in rw.data.decode() - assert b'Endpoints' in rw.data, 'Missing information on the device detail page.' + assert b'Endpoints' in rw.data, 'Missing endpoint information on the device detail page.' # assert b'Add New Device' in rw.data def test_device_add_page(client): @@ -78,3 +78,6 @@ def test_device_add_page(client): assert rw.status_code == 200 assert b'Add New Device' in rw.data assert b'Operational Status' in rw.data, 'Form is not correctly implemented.' + assert b'Type' in rw.data, 'Form is not correctly implemented.' + assert b'Configurations' in rw.data, 'Form is not correctly implemented.' + assert b'Drivers' in rw.data, 'Form is not correctly implemented.' diff --git a/src/webui/utils/__init__.py b/src/webui/utils/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/webui/utils/form_validators.py b/src/webui/utils/form_validators.py new file mode 100644 index 0000000000000000000000000000000000000000..e339fdcc096ae8994ff0eb61a30f93d778fbe41e --- /dev/null +++ b/src/webui/utils/form_validators.py @@ -0,0 +1,14 @@ +from wtforms.validators import ValidationError + +def key_value_validator(): + def _validate(form, field): + if len(field.data) > 0: + if '\n' not in field.data: # case in which there is only one configuration + if '=' not in field.data: + raise ValidationError(f'Configuration "{field.data}" does not follow the key=value pattern.') + else: # case in which there are several configurations + configurations = field.data.split('\n') + for configutation in configurations: + if '=' not in configutation: + raise ValidationError(f'Configuration "{configutation}" does not follow the key=value pattern.') + return _validate