diff --git a/proto/context.proto b/proto/context.proto index 9f06d32ee04b5102ce2af511f45f8de34f984599..4d61572df1db6c95b64e9ce1cbfdd9e0db111078 100644 --- a/proto/context.proto +++ b/proto/context.proto @@ -258,6 +258,14 @@ message LinkId { Uuid link_uuid = 1; } +enum LinkTypeEnum { + LINKTYPE_UNKNOWN = 0; + LINKTYPE_COPPER = 1; + LINKTYPE_FIBER = 2; + LINKTYPE_RADIO = 3; + LINKTYPE_VIRTUAL = 4; +} + message LinkAttributes { float total_capacity_gbps = 1; float used_capacity_gbps = 2; @@ -266,9 +274,9 @@ message LinkAttributes { message Link { LinkId link_id = 1; string name = 2; - repeated EndPointId link_endpoint_ids = 3; - LinkAttributes attributes = 4; - LinkTypeEnum link_type = 5; + LinkTypeEnum link_type = 3; + repeated EndPointId link_endpoint_ids = 4; + LinkAttributes attributes = 5; } message LinkIdList { @@ -284,14 +292,6 @@ message LinkEvent { LinkId link_id = 2; } -enum LinkTypeEnum { - LINKTYPE_UNKNOWN = 0; - LINKTYPE_COPPER = 1; - LINKTYPE_VIRTUAL_COPPER = 2; - LINKTYPE_OPTICAL = 3; - LINKTYPE_VIRTUAL_OPTICAL = 4; -} - // ----- Service ------------------------------------------------------------------------------------------------------- message ServiceId { ContextId context_id = 1; diff --git a/src/context/service/database/Connection.py b/src/context/service/database/Connection.py index de45151a5ca95e974e940503aaade83a47faf46c..529c02d6e29b6434b3da0ec03593634b3ab87f08 100644 --- a/src/context/service/database/Connection.py +++ b/src/context/service/database/Connection.py @@ -74,7 +74,7 @@ def connection_set(db_engine : Engine, messagebroker : MessageBroker, request : _,service_uuid = service_get_uuid(request.service_id, allow_random=False) settings = grpc_message_to_json_string(request.settings), - now = datetime.datetime.utcnow() + now = datetime.datetime.now(datetime.timezone.utc) connection_data = [{ 'connection_uuid': connection_uuid, diff --git a/src/context/service/database/Context.py b/src/context/service/database/Context.py index f0cd36f83fb3588976a3fcf515e2e97bd9613f4c..f3ef214c3715ac8c6110e3d5a18c2fb744d2d6db 100644 --- a/src/context/service/database/Context.py +++ b/src/context/service/database/Context.py @@ -82,7 +82,8 @@ def context_set(db_engine : Engine, messagebroker : MessageBroker, request : Con if len(request.slice_ids) > 0: # pragma: no cover LOGGER.warning('Items in field "slice_ids" ignored. This field is used for retrieval purposes only.') - now = datetime.datetime.utcnow() + now = datetime.datetime.now(datetime.timezone.utc) + context_data = [{ 'context_uuid': context_uuid, 'context_name': context_name, diff --git a/src/context/service/database/Device.py b/src/context/service/database/Device.py index 930b5299f9c77243e5d26c453d051e3ecb455e27..7515f8d68227bd26f5d5384756186394492e5e53 100644 --- a/src/context/service/database/Device.py +++ b/src/context/service/database/Device.py @@ -92,7 +92,7 @@ def device_set(db_engine : Engine, messagebroker : MessageBroker, request : Devi oper_status = grpc_to_enum__device_operational_status(request.device_operational_status) device_drivers = [grpc_to_enum__device_driver(d) for d in request.device_drivers] - now = datetime.datetime.utcnow() + now = datetime.datetime.now(datetime.timezone.utc) topology_uuids : Set[str] = set() related_topologies : List[Dict] = list() diff --git a/src/context/service/database/Link.py b/src/context/service/database/Link.py index deef3769cb64e34ae6c300f562c31983a8807286..6244a8517f66280893acb10944a235666beb80d4 100644 --- a/src/context/service/database/Link.py +++ b/src/context/service/database/Link.py @@ -22,11 +22,12 @@ from common.proto.context_pb2 import Empty, EventTypeEnum, Link, LinkId, LinkIdL from common.message_broker.MessageBroker import MessageBroker from common.method_wrappers.ServiceExceptions import NotFoundException from common.tools.object_factory.Link import json_link_id -from context.service.database.uuids.Topology import topology_get_uuid +from .models.enums.LinkType import grpc_to_enum__link_type_enum from .models.LinkModel import LinkModel, LinkEndPointModel from .models.TopologyModel import TopologyLinkModel, TopologyModel from .uuids.EndPoint import endpoint_get_uuid from .uuids.Link import link_get_uuid +from .uuids.Topology import topology_get_uuid from .Events import notify_event_context, notify_event_link, notify_event_topology LOGGER = logging.getLogger(__name__) @@ -68,7 +69,9 @@ def link_set(db_engine : Engine, messagebroker : MessageBroker, request : Link) link_name = raw_link_uuid if len(raw_link_name) == 0 else raw_link_name link_uuid = link_get_uuid(request.link_id, link_name=link_name, allow_random=True) - now = datetime.datetime.utcnow() + link_type = grpc_to_enum__link_type_enum(request.link_type) + + now = datetime.datetime.now(datetime.timezone.utc) topology_uuids : Set[str] = set() related_topologies : List[Dict] = list() @@ -117,6 +120,7 @@ def link_set(db_engine : Engine, messagebroker : MessageBroker, request : Link) link_data = [{ 'link_uuid' : link_uuid, 'link_name' : link_name, + 'link_type' : link_type, 'total_capacity_gbps' : total_capacity_gbps, 'used_capacity_gbps' : used_capacity_gbps, 'created_at' : now, @@ -129,6 +133,7 @@ def link_set(db_engine : Engine, messagebroker : MessageBroker, request : Link) index_elements=[LinkModel.link_uuid], set_=dict( link_name = stmt.excluded.link_name, + link_type = stmt.excluded.link_type, total_capacity_gbps = stmt.excluded.total_capacity_gbps, used_capacity_gbps = stmt.excluded.used_capacity_gbps, updated_at = stmt.excluded.updated_at, diff --git a/src/context/service/database/OpticalLink.py b/src/context/service/database/OpticalLink.py index 53fd7bdb5a5964376c6ffb94c6b130b6744f7215..1f45daf43bb805e750b1742e6e4ea7e18039cc43 100644 --- a/src/context/service/database/OpticalLink.py +++ b/src/context/service/database/OpticalLink.py @@ -64,7 +64,7 @@ def optical_link_set(db_engine : Engine, messagebroker : MessageBroker, request link_name = raw_link_uuid if len(raw_link_name) == 0 else raw_link_name link_uuid = link_get_uuid(request.link_id, link_name=link_name, allow_random=True) - now = datetime.datetime.utcnow() + now = datetime.datetime.now(datetime.timezone.utc) # By default, always add link to default Context/Topology topology_uuids : Set[str] = set() diff --git a/src/context/service/database/PolicyRule.py b/src/context/service/database/PolicyRule.py index 3d59c59b3ade828450924404d081e237b7a37c1f..ad38838fc127aaee1927f9e44ac0791cef105300 100644 --- a/src/context/service/database/PolicyRule.py +++ b/src/context/service/database/PolicyRule.py @@ -84,7 +84,7 @@ def policyrule_set(db_engine : Engine, messagebroker : MessageBroker, request : 'actionList': json_policyrule_basic.get('actionList', []), }, sort_keys=True) - now = datetime.datetime.utcnow() + now = datetime.datetime.now(datetime.timezone.utc) policyrule_data = [{ 'policyrule_uuid' : policyrule_uuid, diff --git a/src/context/service/database/Service.py b/src/context/service/database/Service.py index c789b291bd15bda1a49d8dd68e78d1875adccfb7..62f07e4fbe4f90af7834358cc79d4c8cb82934f4 100644 --- a/src/context/service/database/Service.py +++ b/src/context/service/database/Service.py @@ -91,7 +91,7 @@ def service_set(db_engine : Engine, messagebroker : MessageBroker, request : Ser service_status = grpc_to_enum__service_status(request.service_status.service_status) - now = datetime.datetime.utcnow() + now = datetime.datetime.now(datetime.timezone.utc) service_endpoints_data : List[Dict] = list() for i,endpoint_id in enumerate(request.service_endpoint_ids): @@ -180,7 +180,8 @@ def service_unset(db_engine : Engine, messagebroker : MessageBroker, request : S ['should be == request.service_id.context_id.context_uuid.uuid({:s})'.format(raw_context_uuid)]) service_endpoint_uuids.add(endpoint_get_uuid(endpoint_id, allow_random=False)[2]) - now = datetime.datetime.utcnow() + now = datetime.datetime.now(datetime.timezone.utc) + constraints = compose_constraints_data(request.service_constraints, now, service_uuid=service_uuid) config_rules = compose_config_rules_data(request.service_config.config_rules, now, service_uuid=service_uuid) diff --git a/src/context/service/database/Slice.py b/src/context/service/database/Slice.py index 18620e6fc76bbbbf6f445767d92a108f84c20a06..84e210e025e750613387a18c4c7eef4618cdfcdb 100644 --- a/src/context/service/database/Slice.py +++ b/src/context/service/database/Slice.py @@ -89,7 +89,7 @@ def slice_set(db_engine : Engine, messagebroker : MessageBroker, request : Slice slice_status = grpc_to_enum__slice_status(request.slice_status.slice_status) - now = datetime.datetime.utcnow() + now = datetime.datetime.now(datetime.timezone.utc) slice_endpoints_data : List[Dict] = list() for i,endpoint_id in enumerate(request.slice_endpoint_ids): @@ -222,7 +222,8 @@ def slice_unset(db_engine : Engine, messagebroker : MessageBroker, request : Sli for subslice_id in request.slice_subslice_ids } - now = datetime.datetime.utcnow() + now = datetime.datetime.now(datetime.timezone.utc) + constraints = compose_constraints_data(request.slice_constraints, now, slice_uuid=slice_uuid) config_rules = compose_config_rules_data(request.slice_config.config_rules, now, slice_uuid=slice_uuid) diff --git a/src/context/service/database/Topology.py b/src/context/service/database/Topology.py index ba2649c69abb025988e576f4978479bc05a5d657..1ee5d16422e94d2b9a2ed4dede461a14e97434ec 100644 --- a/src/context/service/database/Topology.py +++ b/src/context/service/database/Topology.py @@ -134,7 +134,8 @@ def topology_set(db_engine : Engine, messagebroker : MessageBroker, request : To MSG += 'Items in field "link_ids" ignored. This field is used for retrieval purposes only.' LOGGER.warning(MSG) - now = datetime.datetime.utcnow() + now = datetime.datetime.now(datetime.timezone.utc) + topology_data = [{ 'context_uuid' : context_uuid, 'topology_uuid': topology_uuid, diff --git a/src/context/service/database/models/LinkModel.py b/src/context/service/database/models/LinkModel.py index 423e39832201cc19d98a106b136fb545f4e24b7d..1bfa532d843bce4e078ef087fa4659b6fec75ceb 100644 --- a/src/context/service/database/models/LinkModel.py +++ b/src/context/service/database/models/LinkModel.py @@ -13,17 +13,22 @@ # limitations under the License. import operator -from sqlalchemy import CheckConstraint, Column, DateTime, Float, ForeignKey, Integer, String +from sqlalchemy import ( + CheckConstraint, Column, DateTime, Enum, Float, ForeignKey, Integer, String +) from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import relationship from typing import Dict +from common.proto.context_pb2 import LinkTypeEnum from ._Base import _Base +from .enums.LinkType import ORM_LinkTypeEnum class LinkModel(_Base): __tablename__ = 'link' link_uuid = Column(UUID(as_uuid=False), primary_key=True) link_name = Column(String, nullable=False) + link_type = Column(Enum(ORM_LinkTypeEnum), nullable=False) total_capacity_gbps = Column(Float, nullable=True) used_capacity_gbps = Column(Float, nullable=True) created_at = Column(DateTime, nullable=False) @@ -44,11 +49,14 @@ class LinkModel(_Base): result = { 'link_id' : self.dump_id(), 'name' : self.link_name, + 'link_type' : self.link_type.value, 'link_endpoint_ids': [ link_endpoint.endpoint.dump_id() for link_endpoint in sorted(self.link_endpoints, key=operator.attrgetter('position')) ], } + if self.link_type is None: + self.link_type = LinkTypeEnum.LINKTYPE_UNKNOWN if self.total_capacity_gbps is not None: attributes : Dict = result.setdefault('attributes', dict()) attributes.setdefault('total_capacity_gbps', self.total_capacity_gbps) diff --git a/src/context/service/database/models/enums/LinkType.py b/src/context/service/database/models/enums/LinkType.py new file mode 100644 index 0000000000000000000000000000000000000000..68624af845ea813aa5ca886de97861852a294516 --- /dev/null +++ b/src/context/service/database/models/enums/LinkType.py @@ -0,0 +1,33 @@ +# Copyright 2022-2024 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# +# 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 enum, functools +from common.proto.context_pb2 import LinkTypeEnum +from ._GrpcToEnum import grpc_to_enum + +# IMPORTANT: Entries of enum class ORM_LinkTypeEnum should be named as in +# the proto files removing the prefixes. For example, proto item +# LinkTypeEnum.DEVICEDRIVER_COPPER should be included as COPPER. +# If item name does not match, automatic mapping of proto enums +# to database enums will fail. +class ORM_LinkTypeEnum(enum.Enum): + UNKNOWN = LinkTypeEnum.LINKTYPE_UNKNOWN + COPPER = LinkTypeEnum.LINKTYPE_COPPER + FIBER = LinkTypeEnum.LINKTYPE_FIBER + RADIO = LinkTypeEnum.LINKTYPE_RADIO + VIRTUAL = LinkTypeEnum.LINKTYPE_VIRTUAL + +grpc_to_enum__link_type_enum = functools.partial( + grpc_to_enum, LinkTypeEnum, ORM_LinkTypeEnum +) diff --git a/src/nbi/service/rest_server/nbi_plugins/tfs_api/Resources.py b/src/nbi/service/rest_server/nbi_plugins/tfs_api/Resources.py index f934624696a6c24b24d5d2f095decc62425d5c47..ab608f2d746a2faffc819f25cc026ce9b949aff7 100644 --- a/src/nbi/service/rest_server/nbi_plugins/tfs_api/Resources.py +++ b/src/nbi/service/rest_server/nbi_plugins/tfs_api/Resources.py @@ -306,16 +306,20 @@ class Link(_Resource): def put(self, link_uuid : str): link_json = request.get_json() link = grpc_link(link_json) - virtual_types = {LinkTypeEnum.LINKTYPE_VIRTUAL_COPPER, LinkTypeEnum.LINKTYPE_VIRTUAL_OPTICAL} if link_uuid != link.link_id.link_uuid.uuid: raise BadRequest('Mismatching link_uuid') - elif link.link_type in virtual_types: - link = grpc_link(link_json) - return format_grpc_to_json(self.vntmanager_client.SetVirtualLink(link)) - return format_grpc_to_json(self.context_client.SetLink(grpc_link(link))) + if link.link_type == LinkTypeEnum.LINKTYPE_VIRTUAL: + return format_grpc_to_json(self.vntmanager_client.SetVirtualLink(link)) + else: + return format_grpc_to_json(self.context_client.SetLink(link)) def delete(self, link_uuid : str): - return format_grpc_to_json(self.context_client.RemoveLink(grpc_link_id(link_uuid))) + link_id = grpc_link_id(link_uuid) + link = self.context_client.GetLink(link_id) + if link.link_type == LinkTypeEnum.LINKTYPE_VIRTUAL: + return format_grpc_to_json(self.vntmanager_client.RemoveVirtualLink(link_id)) + else: + return format_grpc_to_json(self.context_client.RemoveLink(link_id)) class ConnectionIds(_Resource): def get(self, context_uuid : str, service_uuid : str): diff --git a/src/webui/service/device/routes.py b/src/webui/service/device/routes.py index f3896bdd8f80ddcbcc55f91707caa5e462801cab..16b86c769c5af1b69db4b669f5689043d03536bd 100644 --- a/src/webui/service/device/routes.py +++ b/src/webui/service/device/routes.py @@ -13,10 +13,14 @@ # limitations under the License. import json -from flask import current_app, render_template, Blueprint, flash, session, redirect, url_for +from flask import ( + current_app, render_template, Blueprint, flash, session, redirect, url_for +) from common.DeviceTypes import DeviceTypeEnum from common.proto.context_pb2 import ( - ConfigActionEnum, Device, DeviceDriverEnum, DeviceId, DeviceList, DeviceOperationalStatusEnum, Empty) + ConfigActionEnum, Device, DeviceDriverEnum, DeviceId, DeviceList, + DeviceOperationalStatusEnum, Empty +) from common.tools.context_queries.Device import get_device from common.tools.context_queries.Topology import get_topology from context.client.ContextClient import ContextClient diff --git a/src/webui/service/link/routes.py b/src/webui/service/link/routes.py index dacf77534711c97237ca1afd54c3646fe9809a28..42f5984a3c0957a0740690ad6e37bed7438449b7 100644 --- a/src/webui/service/link/routes.py +++ b/src/webui/service/link/routes.py @@ -13,8 +13,10 @@ # limitations under the License. -from flask import current_app, render_template, Blueprint, flash, session, redirect, url_for -from common.proto.context_pb2 import Empty, Link, LinkId, LinkList +from flask import ( + current_app, render_template, Blueprint, flash, session, redirect, url_for +) +from common.proto.context_pb2 import Empty, Link, LinkId, LinkList, LinkTypeEnum from common.tools.context_queries.EndPoint import get_endpoint_names from common.tools.context_queries.Link import get_link from common.tools.context_queries.Topology import get_topology @@ -50,7 +52,10 @@ def home(): device_names, endpoints_data = get_endpoint_names(context_client, endpoint_ids) context_client.close() - return render_template('link/home.html', links=links, device_names=device_names, endpoints_data=endpoints_data) + return render_template( + 'link/home.html', links=links, device_names=device_names, + endpoints_data=endpoints_data, lte=LinkTypeEnum + ) @link.route('detail/<path:link_uuid>', methods=('GET', 'POST')) @@ -64,7 +69,10 @@ def detail(link_uuid: str): else: device_names, endpoints_data = get_endpoint_names(context_client, link_obj.link_endpoint_ids) context_client.close() - return render_template('link/detail.html',link=link_obj, device_names=device_names, endpoints_data=endpoints_data) + return render_template( + 'link/detail.html', link=link_obj, device_names=device_names, + endpoints_data=endpoints_data, lte=LinkTypeEnum + ) @link.get('<path:link_uuid>/delete') def delete(link_uuid): diff --git a/src/webui/service/templates/link/detail.html b/src/webui/service/templates/link/detail.html index 7c5f732e33a209b6acbd36b4982da90d7f148f38..775e5392d3dad06eaf13414cda2c9cde2586fe12 100644 --- a/src/webui/service/templates/link/detail.html +++ b/src/webui/service/templates/link/detail.html @@ -39,6 +39,7 @@ <div class="col-sm-4"> <b>UUID: </b>{{ link.link_id.link_uuid.uuid }}<br> <b>Name: </b>{{ link.name }}<br> + <b>Type: </b>{{ lte.Name(link.link_type).replace('LINKTYPE_', '') }}<br> </div> <div class="col-sm-8"> <table class="table table-striped table-hover"> diff --git a/src/webui/service/templates/link/home.html b/src/webui/service/templates/link/home.html index d96da78147d344533bf94fcc1adecbecf8fe8dd2..6632898793c8601bf30f8d824e9090e3a2f7552e 100644 --- a/src/webui/service/templates/link/home.html +++ b/src/webui/service/templates/link/home.html @@ -12,87 +12,89 @@ 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>Links</h1> - - <div class="row"> - <div class="col"> - <!-- <a href="#" class="btn btn-primary" style="margin-bottom: 10px;"> - <i class="bi bi-plus"></i> - Add New Link - </a> --> - </div> - <div class="col"> - {{ links | length }} links found in context <i>{{ session['context_uuid'] }}</i> - </div> - <!-- <div class="col"> - <form> - <div class="input-group"> - <input type="text" aria-label="Search" placeholder="Search..." class="form-control"/> - <button type="submit" class="btn btn-primary">Search</button> - </div> - </form> - </div> --> - </div> - - <table class="table table-striped table-hover"> - <thead> - <tr> - <th scope="col">UUID</th> - <th scope="col">Name</th> - <th scope="col">Endpoints</th> - <th scope="col"></th> - </tr> - </thead> - <tbody> - {% if links %} - {% for link in links %} - <tr> - <td> +--> + +{% extends 'base.html' %} + +{% block content %} + <h1>Links</h1> + + <div class="row"> + <div class="col"> + <!-- <a href="#" class="btn btn-primary" style="margin-bottom: 10px;"> + <i class="bi bi-plus"></i> + Add New Link + </a> --> + </div> + <div class="col"> + {{ links | length }} links found in context <i>{{ session['context_uuid'] }}</i> + </div> + <!-- <div class="col"> + <form> + <div class="input-group"> + <input type="text" aria-label="Search" placeholder="Search..." class="form-control"/> + <button type="submit" class="btn btn-primary">Search</button> + </div> + </form> + </div> --> + </div> + + <table class="table table-striped table-hover"> + <thead> + <tr> + <th scope="col">UUID</th> + <th scope="col">Name</th> + <th scope="col">Type</th> + <th scope="col">Endpoints</th> + <th scope="col"></th> + </tr> + </thead> + <tbody> + {% if links %} + {% for link in links %} + <tr> + <td> {{ link.link_id.link_uuid.uuid }} - </td> - <td> + </td> + <td> {{ link.name }} </td> - - <td> - <ul> - {% for endpoint in link.link_endpoint_ids %} - <li> - {{ endpoints_data.get(endpoint.endpoint_uuid.uuid, (endpoint.endpoint_uuid.uuid, ''))[0] }} / - Device: - <a href="{{ url_for('device.detail', device_uuid=endpoint.device_id.device_uuid.uuid) }}"> - {{ device_names.get(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> - </li> - {% endfor %} - </ul> - </td> + <td> + {{ lte.Name(link.link_type).replace('LINKTYPE_', '') }} + </td> + <td> + <ul> + {% for endpoint in link.link_endpoint_ids %} + <li> + {{ endpoints_data.get(endpoint.endpoint_uuid.uuid, (endpoint.endpoint_uuid.uuid, ''))[0] }} / + Device: + <a href="{{ url_for('device.detail', device_uuid=endpoint.device_id.device_uuid.uuid) }}"> + {{ device_names.get(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> + </li> + {% endfor %} + </ul> + </td> - <td> + <td> <a href="{{ url_for('link.detail', link_uuid=link.link_id.link_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 links found</td> - </tr> - {% endif %} - </tbody> - </table> - - {% endblock %} \ No newline at end of file + <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 links found</td> + </tr> + {% endif %} + </tbody> + </table> +{% endblock %}