diff --git a/manifests/webuiservice.yaml b/manifests/webuiservice.yaml index 52fc75a9868001d50f7380cfe238fa344de27f6e..1390e99dafcdae86e36aa542af567f0df8b1bf0b 100644 --- a/manifests/webuiservice.yaml +++ b/manifests/webuiservice.yaml @@ -60,43 +60,43 @@ spec: limits: cpu: 700m memory: 1024Mi - - name: grafana - image: grafana/grafana:8.2.6 - imagePullPolicy: IfNotPresent - ports: - - containerPort: 3000 - name: http-grafana - protocol: TCP - env: - - name: GF_SERVER_ROOT_URL - value: "http://0.0.0.0:3000/grafana/" - - name: GF_SERVER_SERVE_FROM_SUB_PATH - value: "true" - readinessProbe: - failureThreshold: 3 - httpGet: - path: /robots.txt - port: 3000 - scheme: HTTP - initialDelaySeconds: 10 - periodSeconds: 30 - successThreshold: 1 - timeoutSeconds: 2 - livenessProbe: - failureThreshold: 3 - initialDelaySeconds: 30 - periodSeconds: 10 - successThreshold: 1 - tcpSocket: - port: 3000 - timeoutSeconds: 1 - resources: - requests: - cpu: 250m - memory: 750Mi - limits: - cpu: 700m - memory: 1024Mi +# - name: grafana +# image: grafana/grafana:8.2.6 +# imagePullPolicy: IfNotPresent +# ports: +# - containerPort: 3000 +# name: http-grafana +# protocol: TCP +# env: +# - name: GF_SERVER_ROOT_URL +# value: "http://0.0.0.0:3000/grafana/" +# - name: GF_SERVER_SERVE_FROM_SUB_PATH +# value: "true" +# readinessProbe: +# failureThreshold: 3 +# httpGet: +# path: /robots.txt +# port: 3000 +# scheme: HTTP +# initialDelaySeconds: 10 +# periodSeconds: 30 +# successThreshold: 1 +# timeoutSeconds: 2 +# livenessProbe: +# failureThreshold: 3 +# initialDelaySeconds: 30 +# periodSeconds: 10 +# successThreshold: 1 +# tcpSocket: +# port: 3000 +# timeoutSeconds: 1 +# resources: +# requests: +# cpu: 250m +# memory: 750Mi +# limits: +# cpu: 700m +# memory: 1024Mi --- apiVersion: v1 kind: Service @@ -110,6 +110,6 @@ spec: - name: webui port: 8004 targetPort: 8004 - - name: grafana - port: 3000 - targetPort: 3000 +# - name: grafana +# port: 3000 +# targetPort: 3000 diff --git a/my_deploy.sh b/my_deploy.sh index 67a2e0558c25d767e14b635e6dd9174433827156..eead186b22a05e46601af8a1511b56994c789bec 100644 --- a/my_deploy.sh +++ b/my_deploy.sh @@ -1,22 +1,6 @@ -# Set the URL of your local Docker registry where the images will be uploaded to. export TFS_REGISTRY_IMAGE="http://localhost:32000/tfs/" - -# Set the list of components, separated by comas, you want to build images for, and deploy. -# Supported components are: -# context device automation policy service compute monitoring webui -# interdomain slice pathcomp dlt -# dbscanserving opticalattackmitigator opticalcentralizedattackdetector -# l3_attackmitigator l3_centralizedattackdetector l3_distributedattackdetector -export TFS_COMPONENTS="context device automation service compute monitoring webui" - -# Set the tag you want to use for your images. +export TFS_COMPONENTS="context device service webui" export TFS_IMAGE_TAG="dev" - -# Set the name of the Kubernetes namespace to deploy to. export TFS_K8S_NAMESPACE="tfs" - -# Set additional manifest files to be applied after the deployment export TFS_EXTRA_MANIFESTS="manifests/nginx_ingress_http.yaml" - -# Set the neew Grafana admin password export TFS_GRAFANA_PASSWORD="admin123+" diff --git a/src/webui/Dockerfile b/src/webui/Dockerfile index 7760416be32b893ed5f2408b70e874fb89721e17..a17d2bd9aea9c6948262dcf17776f75c0be351b8 100644 --- a/src/webui/Dockerfile +++ b/src/webui/Dockerfile @@ -79,6 +79,8 @@ COPY --chown=webui:webui src/device/__init__.py device/__init__.py COPY --chown=webui:webui src/device/client/. device/client/ COPY --chown=webui:webui src/service/__init__.py service/__init__.py COPY --chown=webui:webui src/service/client/. service/client/ +COPY --chown=webui:webui src/slice/__init__.py slice/__init__.py +COPY --chown=webui:webui src/slice/client/. slice/client/ COPY --chown=webui:webui src/webui/. webui/ # Start the service diff --git a/src/webui/service/__init__.py b/src/webui/service/__init__.py index 9187d90e76acd256bcac752ce7e7be025889e133..75e1036420d0bc88a790fb7b65f4f4900abaaadd 100644 --- a/src/webui/service/__init__.py +++ b/src/webui/service/__init__.py @@ -72,11 +72,15 @@ def create_app(use_config=None, web_app_root=None): from webui.service.service.routes import service app.register_blueprint(service) + from webui.service.slice.routes import slice + app.register_blueprint(slice) + from webui.service.device.routes import device app.register_blueprint(device) from webui.service.link.routes import link app.register_blueprint(link) + app.jinja_env.filters['from_json'] = from_json diff --git a/src/webui/service/main/routes.py b/src/webui/service/main/routes.py index 893f0854361081325305289f95b1471279384c7e..e9545ade40949a1ad772b35b669e02a1fa39d64d 100644 --- a/src/webui/service/main/routes.py +++ b/src/webui/service/main/routes.py @@ -11,32 +11,43 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + import copy, json, logging from flask import jsonify, redirect, render_template, Blueprint, flash, session, url_for, request -from common.proto.context_pb2 import Context, Device, Empty, Link, Service, Topology, ContextIdList +from common.proto.context_pb2 import Connection, Context, Device, Empty, Link, Service, Slice, Topology, ContextIdList from common.tools.grpc.Tools import grpc_message_to_json_string from context.client.ContextClient import ContextClient from device.client.DeviceClient import DeviceClient from service.client.ServiceClient import ServiceClient +from slice.client.SliceClient import SliceClient from webui.service.main.forms import ContextForm, DescriptorForm + main = Blueprint('main', __name__) + context_client = ContextClient() device_client = DeviceClient() service_client = ServiceClient() +slice_client = SliceClient() + logger = logging.getLogger(__name__) + ENTITY_TO_TEXT = { # name => singular, plural - 'context' : ('Context', 'Contexts' ), - 'topology': ('Topology', 'Topologies'), - 'device' : ('Device', 'Devices' ), - 'link' : ('Link', 'Links' ), - 'service' : ('Service', 'Services' ), + 'context' : ('Context', 'Contexts' ), + 'topology' : ('Topology', 'Topologies' ), + 'device' : ('Device', 'Devices' ), + 'link' : ('Link', 'Links' ), + 'service' : ('Service', 'Services' ), + 'slice' : ('Slice', 'Slices' ), + 'connection': ('Connection', 'Connections'), } + ACTION_TO_TEXT = { # action => infinitive, past 'add' : ('Add', 'Added'), 'update' : ('Update', 'Updated'), } + def process_descriptor(entity_name, action_name, grpc_method, grpc_class, entities): entity_name_singluar,entity_name_plural = ENTITY_TO_TEXT[entity_name] action_infinitive, action_past = ACTION_TO_TEXT[action_name] @@ -50,25 +61,56 @@ def process_descriptor(entity_name, action_name, grpc_method, grpc_class, entiti num_err += 1 if num_ok : flash(f'{str(num_ok)} {entity_name_plural} {action_past}', 'success') if num_err: flash(f'{str(num_err)} {entity_name_plural} failed', 'danger') + def process_descriptors(descriptors): - logger.warning(str(descriptors.data)) - logger.warning(str(descriptors.name)) try: - logger.warning(str(request.files)) descriptors_file = request.files[descriptors.name] - logger.warning(str(descriptors_file)) descriptors_data = descriptors_file.read() - logger.warning(str(descriptors_data)) descriptors = json.loads(descriptors_data) - logger.warning(str(descriptors)) except Exception as e: # pylint: disable=broad-except flash(f'Unable to load descriptor file: {str(e)}', 'danger') return - contexts = descriptors.get('contexts' , []) - topologies = descriptors.get('topologies', []) - devices = descriptors.get('devices' , []) - links = descriptors.get('links' , []) - services = descriptors.get('services' , []) + + dummy_mode = descriptors.get('dummy_mode' , False) + contexts = descriptors.get('contexts' , []) + topologies = descriptors.get('topologies' , []) + devices = descriptors.get('devices' , []) + links = descriptors.get('links' , []) + services = descriptors.get('services' , []) + slices = descriptors.get('slices' , []) + connections = descriptors.get('connections', []) + + if dummy_mode: + # Dummy Mode: used to pre-load databases (WebUI debugging purposes) with no smart or automated tasks. + context_client.connect() + + contexts_add = copy.deepcopy(contexts) + for context in contexts_add: + context['topology_ids'] = [] + context['service_ids'] = [] + + topologies_add = copy.deepcopy(topologies) + for topology in topologies_add: + topology['device_ids'] = [] + topology['link_ids'] = [] + + process_descriptor('context', 'add', context_client.SetContext, Context, contexts_add ) + process_descriptor('topology', 'add', context_client.SetTopology, Topology, topologies_add) + process_descriptor('device', 'add', context_client.SetDevice, Device, devices ) + process_descriptor('link', 'add', context_client.SetLink, Link, links ) + process_descriptor('service', 'add', context_client.SetService, Service, services ) + process_descriptor('context', 'update', context_client.SetContext, Context, contexts ) + process_descriptor('topology', 'update', context_client.SetTopology, Topology, topologies ) + process_descriptor('slice', 'add', context_client.SetSlice, Slice, slices ) + process_descriptor('connection', 'add', context_client.SetConnection, Connection, connections ) + context_client.close() + return + + # Normal mode: follows the automated workflows in the different components + + # in normal mode, connections should not be set + assert len(connections) == 0 + services_add = [] for service in services: service_copy = copy.deepcopy(service) @@ -76,18 +118,34 @@ def process_descriptors(descriptors): service_copy['service_constraints'] = [] service_copy['service_config'] = {'config_rules': []} services_add.append(service_copy) + + slices_add = [] + for slice in slices: + slice_copy = copy.deepcopy(slice) + slice_copy['slice_endpoint_ids'] = [] + slice_copy['slice_constraints'] = [] + slice_copy['slice_config'] = {'config_rules': []} + slices_add.append(slice_copy) + context_client.connect() device_client.connect() service_client.connect() + slice_client.connect() + process_descriptor('context', 'add', context_client.SetContext, Context, contexts ) process_descriptor('topology', 'add', context_client.SetTopology, Topology, topologies ) process_descriptor('device', 'add', device_client .AddDevice, Device, devices ) process_descriptor('link', 'add', context_client.SetLink, Link, links ) process_descriptor('service', 'add', service_client.CreateService, Service, services_add) process_descriptor('service', 'update', service_client.UpdateService, Service, services ) + process_descriptor('slice', 'add', slice_client.CreateSlice, Slice, slices_add ) + process_descriptor('slice', 'update', slice_client.UpdateSlice, Slice, slices ) + + slice_client.close() service_client.close() device_client.close() context_client.close() + @main.route('/', methods=['GET', 'POST']) def home(): context_client.connect() @@ -95,14 +153,18 @@ def home(): response: ContextIdList = context_client.ListContextIds(Empty()) context_form: ContextForm = ContextForm() context_form.context.choices.append(('', 'Select...')) + for context in response.context_ids: context_form.context.choices.append((context.context_uuid.uuid, context.context_uuid)) + if context_form.validate_on_submit(): session['context_uuid'] = context_form.context.data flash(f'The context was successfully set to `{context_form.context.data}`.', 'success') return redirect(url_for("main.home")) + if 'context_uuid' in session: context_form.context.data = session['context_uuid'] + descriptor_form: DescriptorForm = DescriptorForm() try: if descriptor_form.validate_on_submit(): @@ -114,7 +176,9 @@ def home(): finally: context_client.close() device_client.close() + return render_template('main/home.html', context_form=context_form, descriptor_form=descriptor_form) + @main.route('/topology', methods=['GET']) def topology(): context_client.connect() @@ -125,6 +189,7 @@ def topology(): 'name': device.device_id.device_uuid.uuid, 'type': device.device_type, } for device in response.devices] + response = context_client.ListLinks(Empty()) links = [] for link in response.links: @@ -137,18 +202,22 @@ def topology(): 'source': link.link_endpoint_ids[0].device_id.device_uuid.uuid, 'target': link.link_endpoint_ids[1].device_id.device_uuid.uuid, }) + return jsonify({'devices': devices, 'links': links}) except: logger.exception('Error retrieving topology') finally: context_client.close() + @main.get('/about') def about(): return render_template('main/about.html') + @main.get('/debug') def debug(): return render_template('main/debug.html') + @main.get('/resetsession') def reset_session(): session.clear() - return redirect(url_for("main.home")) \ No newline at end of file + return redirect(url_for("main.home")) diff --git a/src/webui/service/slice/__init__.py b/src/webui/service/slice/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..70a33251242c51f49140e596b8208a19dd5245f7 --- /dev/null +++ b/src/webui/service/slice/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2021-2023 H2020 TeraFlow (https://www.teraflow-h2020.eu/) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/src/webui/service/slice/routes.py b/src/webui/service/slice/routes.py new file mode 100644 index 0000000000000000000000000000000000000000..b6818be8af9434296efd80f09d97a4baac14cfb0 --- /dev/null +++ b/src/webui/service/slice/routes.py @@ -0,0 +1,101 @@ +# Copyright 2021-2023 H2020 TeraFlow (https://www.teraflow-h2020.eu/) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +import grpc +from flask import current_app, redirect, render_template, Blueprint, flash, session, url_for +from common.proto.context_pb2 import ContextId, Slice, SliceId, SliceList, Connection +from context.client.ContextClient import ContextClient +#from slice.client.SliceClient import SliceClient + + + +slice = Blueprint('slice', __name__, url_prefix='/slice') + +context_client = ContextClient() +#slice_client = SliceClient() + +@slice.get('/') +def home(): + # flash('This is an info message', 'info') + # flash('This is a danger message', 'danger') + + context_uuid = session.get('context_uuid', '-') + if context_uuid == "-": + flash("Please select a context!", "warning") + return redirect(url_for("main.home")) + request = ContextId() + request.context_uuid.uuid = context_uuid + context_client.connect() + try: + slice_list = context_client.ListSlices(request) + # print(slice_list) + slices = slice_list.slices + context_not_found = False + except grpc.RpcError as e: + if e.code() != grpc.StatusCode.NOT_FOUND: raise + if e.details() != 'Context({:s}) not found'.format(context_uuid): raise + slices = [] + context_not_found = True + context_client.close() + return render_template('slice/home.html',slices=slices, context_not_found=context_not_found) + +# +#@slice.route('add', methods=['GET', 'POST']) +#def add(): +# flash('Add slice route called', 'danger') +# raise NotImplementedError() +# return render_template('slice/home.html') +# +# +@slice.get('<path:slice_uuid>/detail') +def detail(slice_uuid: str): + context_uuid = session.get('context_uuid', '-') + if context_uuid == "-": + flash("Please select a context!", "warning") + return redirect(url_for("main.home")) + + request: SliceId = SliceId() + request.slice_uuid.uuid = slice_uuid + request.context_id.context_uuid.uuid = context_uuid + try: + context_client.connect() + response: Slice = context_client.GetSlice(request) + #services: Service = context_client.ListServices(request) + context_client.close() + except Exception as e: + flash('The system encountered an error and cannot show the details of this slice.', 'warning') + current_app.logger.exception(e) + return redirect(url_for('slice.home')) + return render_template('slice/detail.html', slice=response) +# +#@slice.get('<path:slice_uuid>/delete') +#def delete(slice_uuid: str): +# context_uuid = session.get('context_uuid', '-') +# if context_uuid == "-": +# flash("Please select a context!", "warning") +# return redirect(url_for("main.home")) +# +# try: +# request = SliceId() +# request.slice_uuid.uuid = slice_uuid +# request.context_id.context_uuid.uuid = context_uuid +# slice_client.connect() +# response = slice_client.DeleteSlice(request) +# slice_client.close() +# +# flash('Slice "{:s}" deleted successfully!'.format(slice_uuid), 'success') +# except Exception as e: +# flash('Problem deleting slice "{:s}": {:s}'.format(slice_uuid, str(e.details())), 'danger') +# current_app.logger.exception(e) +# return redirect(url_for('slice.home')) \ No newline at end of file diff --git a/src/webui/service/templates/base.html b/src/webui/service/templates/base.html index cc245819ee60c76c018d03aca05e7f9e576cb63a..5d7801d11880e89869120985307c6b43416f5a05 100644 --- a/src/webui/service/templates/base.html +++ b/src/webui/service/templates/base.html @@ -76,7 +76,13 @@ <a class="nav-link" href="{{ url_for('service.home') }}">Service</a> {% endif %} </li> - + <li class="nav-item"> + {% if '/slice/' in request.path %} + <a class="nav-link active" aria-current="page" href="{{ url_for('slice.home') }}">Slice</a> + {% else %} + <a class="nav-link" href="{{ url_for('slice.home') }}">Slice</a> + {% endif %} + </li> <li class="nav-item"> <a class="nav-link" href="/grafana" id="grafana_link" target="grafana">Grafana</a> </li> diff --git a/src/webui/service/templates/slice/detail.html b/src/webui/service/templates/slice/detail.html new file mode 100644 index 0000000000000000000000000000000000000000..6d8595c881664a50c98d413ee79f35062eff84db --- /dev/null +++ b/src/webui/service/templates/slice/detail.html @@ -0,0 +1,114 @@ +<!-- + Copyright 2021-2023 H2020 TeraFlow (https://www.teraflow-h2020.eu/) + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +{% extends 'base.html' %} + +{% block content %} +<h1>Slice {{ slice.slice_id.slice_uuid.uuid }}</h1> + +<div class="row mb-3"> + <div class="col-sm-4"> + <b>UUID: </b> {{ slice.slice_id.slice_uuid.uuid }}<br><br> + <b>Status: </b> {{ slice.slice_status.slice_status }}<br><br> + </div> + <div class="col-sm-8"> + <table class="table table-striped table-hover"> + <thead> + <tr> + <th scope="col">Endpoints</th> + <th scope="col">Device</th> + </tr> + </thead> + <tbody> + {% for endpoint in slice.slice_endpoint_ids %} + <tr> + <td> + {{ endpoint.endpoint_uuid.uuid }} + </td> + <td> + <a href="{{ url_for('device.detail', device_uuid=endpoint.device_id.device_uuid.uuid) }}"> + {{ endpoint.device_id.device_uuid.uuid }} + <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" + class="bi bi-eye" viewBox="0 0 16 16"> + <path + d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8zM1.173 8a13.133 13.133 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.133 13.133 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5c-2.12 0-3.879-1.168-5.168-2.457A13.134 13.134 0 0 1 1.172 8z" /> + <path + d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z" /> + </svg> + </a> + </td> + </tr> + {% endfor %} + </tbody> + </table> + </div> +</div> + +<b>Constraints:</b> +<table class="table table-striped table-hover"> + <thead> + <tr> + <th scope="col">Type</th> + <th scope="col">Value</th> + </tr> + </thead> + <tbody> + {% for constraint in slice.slice_constraints %} + <tr> + <td> + {{ constraint.custom.constraint_type }} + </td> + <td> + <ul> + {{ constraint.custom.constraint_value }} + </ul> + </td> + </tr> + {% endfor %} + </tbody> +</table> +<!-- +<table class="table table-striped table-hover"> + <thead> + <tr> + <th scope="col">Service Id</th> + <th scope="col">Sub-slice</th> + </tr> + </thead> + <tbody> + {% for connections in connections.connections %} + <tr> + <td> + {{ connections.connection_id.connection_uuid.uuid }} + </td> + <td> + {{ connections.sub_service_ids|map(attribute='service_uuid')|map(attribute='uuid')|join(', ') }} + </td> + + {% for i in range(connections.path_hops_endpoint_ids|length) %} + <td> + {{ connections.path_hops_endpoint_ids[i].device_id.device_uuid.uuid }} / {{ + connections.path_hops_endpoint_ids[i].endpoint_uuid.uuid }} + </td> + {% endfor %} + + + </tr> + {% endfor %} + </tbody> + </table> --> + +{% endblock %} \ No newline at end of file diff --git a/src/webui/service/templates/slice/home.html b/src/webui/service/templates/slice/home.html new file mode 100644 index 0000000000000000000000000000000000000000..d43710797b8949e9b8b3dba86a7cb25519b23fd7 --- /dev/null +++ b/src/webui/service/templates/slice/home.html @@ -0,0 +1,67 @@ +<!-- + Copyright 2021-2023 H2020 TeraFlow (https://www.teraflow-h2020.eu/) + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +{% extends 'base.html' %} + +{% block content %} + <h1>Slice</h1> + + + <table class="table table-striped table-hover"> + <thead> + <tr> + <th scope="col">#</th> + <th scope="col">End points</th> + <th scope="col">Status</th> + <th scope="col"></th> + + </tr> + </thead> + <tbody> + {% if slices %} + {% for slice in slices %} + <tr> + <td> + <!-- <a href="{{ url_for('slice.detail', slice_uuid=slice.slice_id.slice_uuid.uuid) }}"> --> + {{ slice.slice_id.slice_uuid.uuid }} + <!-- </a> --> + </td> + <td> + {{ slice.slice_endpoint_ids }} + </td> + <td> + {{ slice.slice_status }} + </td> + <td> + <a href="{{ url_for('slice.detail', slice_uuid=slice.slice_id.slice_uuid.uuid) }}"> + <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-eye" viewBox="0 0 16 16"> + <path d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8zM1.173 8a13.133 13.133 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.133 13.133 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5c-2.12 0-3.879-1.168-5.168-2.457A13.134 13.134 0 0 1 1.172 8z"/> + <path d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z"/> + </svg> + </a> + </td> + </tr> + {% endfor %} + {% else %} + <tr> + <td colspan="7">No slices found</td> + </tr> + + {% endif %} + </tbody> + </table> + +{% endblock %} \ No newline at end of file diff --git a/src/webui/tests/test_unitary.py b/src/webui/tests/test_unitary.py index 945a60186e04cc1bd3ee7678b340e9321646df97..11cc77a460a94707c6226cdfc4ca747563e95f45 100644 --- a/src/webui/tests/test_unitary.py +++ b/src/webui/tests/test_unitary.py @@ -68,6 +68,7 @@ class TestWebUI(ClientTestCase): with self.app.app_context(): url_for('main.home') url_for('service.home') + url_for('slice.home') url_for('device.home') url_for('link.home') #url_for('main.debug')