mirror of
https://github.com/tahoe-lafs/tahoe-lafs.git
synced 2025-04-08 03:14:21 +00:00
update WUI welcome page with new connection-status info
This shows current-connection info, and provides per-hint status details in a tooltip. The "Connection" section no longer shows seconds-since-loss when the server was not connected (previously it showed seconds-since-connect when connected, and flipped to seconds-since-loss when disconnected). We already have the "Last RX" column, which is arguably more meaningful (and I can't think of a good case when these would differ), so we don't really need seconds-since-loss, and the new ConnectionStatus doesn't track it anyways. So now the "Connection" timestamp for non-connected servers is just "N/A" (both the main text and the tooltip). The "Introducers" section was changed the same way. This moves the per-server connection timestamp out of the nickname/serverid box and over into the Connection box. It also right-floats all timestamps, regardless of which box they're in, which makes them share the box with connection_status more politely. Internally, this adds code to create ConnectionStatus objects when necessary.
This commit is contained in:
parent
48fc14bd30
commit
77fd41b66e
@ -610,7 +610,7 @@ class Client(node.Node, pollmixin.PollMixin):
|
||||
return self.encoding_params
|
||||
|
||||
def introducer_connection_statuses(self):
|
||||
return [ic.connected_to_introducer() for ic in self.introducer_clients]
|
||||
return [ic.connection_status() for ic in self.introducer_clients]
|
||||
|
||||
def connected_to_introducer(self):
|
||||
return any([ic.connected_to_introducer() for ic in self.introducer_clients])
|
||||
|
@ -8,7 +8,7 @@ from allmydata.introducer.interfaces import IIntroducerClient, \
|
||||
RIIntroducerSubscriberClient_v2
|
||||
from allmydata.introducer.common import sign_to_foolscap, unsign_from_foolscap,\
|
||||
get_tubid_string_from_ann
|
||||
from allmydata.util import log, yamlutil
|
||||
from allmydata.util import log, yamlutil, connection_status
|
||||
from allmydata.util.rrefutil import add_version_to_remote_reference
|
||||
from allmydata.util.keyutil import BadSignatureError
|
||||
from allmydata.util.assertutil import precondition
|
||||
@ -326,6 +326,14 @@ class IntroducerClient(service.Service, Referenceable):
|
||||
if service_name2 == service_name:
|
||||
eventually(cb, key_s, ann, *args, **kwargs)
|
||||
|
||||
def connection_status(self):
|
||||
assert self.running # startService builds _introducer_reconnector
|
||||
irc = self._introducer_reconnector
|
||||
last_received = (self._publisher.getDataLastReceivedAt()
|
||||
if self._publisher
|
||||
else None)
|
||||
return connection_status.from_foolscap_reconnector(irc, last_received)
|
||||
|
||||
def connected_to_introducer(self):
|
||||
return bool(self._publisher)
|
||||
|
||||
|
@ -36,7 +36,7 @@ from twisted.application import service
|
||||
|
||||
from foolscap.api import eventually
|
||||
from allmydata.interfaces import IStorageBroker, IDisplayableServer, IServer
|
||||
from allmydata.util import log, base32
|
||||
from allmydata.util import log, base32, connection_status
|
||||
from allmydata.util.assertutil import precondition
|
||||
from allmydata.util.observer import ObserverList
|
||||
from allmydata.util.rrefutil import add_version_to_remote_reference
|
||||
@ -364,6 +364,14 @@ class NativeStorageServer(service.MultiService):
|
||||
return self.announcement
|
||||
def get_remote_host(self):
|
||||
return self.remote_host
|
||||
|
||||
def get_connection_status(self):
|
||||
last_received = None
|
||||
if self.rref:
|
||||
last_received = self.rref.getDataLastReceivedAt()
|
||||
return connection_status.from_foolscap_reconnector(self._reconnector,
|
||||
last_received)
|
||||
|
||||
def is_connected(self):
|
||||
return self._is_connected
|
||||
def get_last_connect_time(self):
|
||||
|
@ -2,6 +2,7 @@ from twisted.trial import unittest
|
||||
|
||||
from ...storage_client import NativeStorageServer
|
||||
from ...web.root import Root
|
||||
from ...util.connection_status import ConnectionStatus
|
||||
|
||||
class FakeRoot(Root):
|
||||
def __init__(self):
|
||||
@ -26,6 +27,8 @@ class RenderServiceRow(unittest.TestCase):
|
||||
"permutation-seed-base32": "w2hqnbaa25yw4qgcvghl5psa3srpfgw3",
|
||||
}
|
||||
s = NativeStorageServer("server_id", ann, None, {})
|
||||
cs = ConnectionStatus(False, "summary", "description", 0, 0)
|
||||
s.get_connection_status = lambda: cs
|
||||
|
||||
r = FakeRoot()
|
||||
ctx = FakeContext()
|
||||
|
@ -20,6 +20,7 @@ from allmydata.web import status
|
||||
from allmydata.util import fileutil, base32, hashutil
|
||||
from allmydata.util.consumer import download_to_data
|
||||
from allmydata.util.encodingutil import to_str
|
||||
from ...util.connection_status import ConnectionStatus
|
||||
from ..common import FakeCHKFileNode, FakeMutableFileNode, \
|
||||
create_chk_filenode, WebErrorMixin, \
|
||||
make_mutable_file_uri, create_mutable_filenode
|
||||
@ -184,6 +185,9 @@ class FakeDisplayableServer(StubServer):
|
||||
return self.announcement["nickname"]
|
||||
def get_available_space(self):
|
||||
return 123456
|
||||
def get_connection_status(self):
|
||||
return ConnectionStatus(self.connected, "summary", "description",
|
||||
self.last_connect_time, self.last_rx_time)
|
||||
|
||||
class FakeBucketCounter(object):
|
||||
def get_state(self):
|
||||
@ -243,7 +247,7 @@ class FakeClient(Client):
|
||||
self.storage_broker.test_add_server("disconnected_nodeid",
|
||||
FakeDisplayableServer(
|
||||
serverid="other_nodeid", nickname=u"disconnected_nickname \u263B", connected = False,
|
||||
last_connect_time = 15, last_loss_time = 25, last_rx_time = 35))
|
||||
last_connect_time = None, last_loss_time = 25, last_rx_time = 35))
|
||||
self.introducer_client = None
|
||||
self.history = FakeHistory()
|
||||
self.uploader = FakeUploader()
|
||||
@ -611,6 +615,7 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, testutil.ReallyEqualMixi
|
||||
def test_welcome(self):
|
||||
d = self.GET("/")
|
||||
def _check(res):
|
||||
# TODO: replace this with a parser
|
||||
self.failUnlessIn('<title>Tahoe-LAFS - Welcome</title>', res)
|
||||
self.failUnlessIn(FAVICON_MARKUP, res)
|
||||
self.failUnlessIn('<a href="status">Recent and Active Operations</a>', res)
|
||||
@ -624,14 +629,27 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, testutil.ReallyEqualMixi
|
||||
self.failUnlessIn(u'Connected to <span>1</span>\n of <span>2</span> known storage servers', res_u)
|
||||
def timestamp(t):
|
||||
return (u'"%s"' % (t,)) if self.have_working_tzset() else u'"[^"]*"'
|
||||
|
||||
# TODO: use a real parser to make sure these two nodes are siblings
|
||||
self.failUnless(re.search(
|
||||
u'<div class="status-indicator"><img (src="img/connected-yes.png" |alt="Connected" ){2}/>'
|
||||
u'</div>\n <a( class="timestamp"| title=%s){2}>1d\u00A00h\u00A00m\u00A050s</a>'
|
||||
u'<div class="status-indicator"><img (src="img/connected-yes.png" |alt="Connected" ){2}/></div>'
|
||||
u'\s+'
|
||||
u'<div class="nickname">other_nickname \u263B</div>',
|
||||
res_u), repr(res_u))
|
||||
self.failUnless(re.search(
|
||||
u'<a( class="timestamp"| title=%s){2}>\s+1d\u00A00h\u00A00m\u00A050s\s+</a>'
|
||||
% timestamp(u'1970-01-01 13:00:10'), res_u), repr(res_u))
|
||||
|
||||
# same for these two nodes
|
||||
self.failUnless(re.search(
|
||||
u'<div class="status-indicator"><img (src="img/connected-no.png" |alt="Disconnected" ){2}/>'
|
||||
u'</div>\n <a( class="timestamp"| title=%s){2}>1d\u00A00h\u00A00m\u00A035s</a>'
|
||||
% timestamp(u'1970-01-01 13:00:25'), res_u), repr(res_u))
|
||||
u'<div class="status-indicator"><img (src="img/connected-no.png" |alt="Disconnected" ){2}/></div>'
|
||||
u'\s+'
|
||||
u'<div class="nickname">disconnected_nickname \u263B</div>',
|
||||
res_u), repr(res_u))
|
||||
self.failUnless(re.search(
|
||||
u'<a( class="timestamp"| title="N/A"){2}>\s+N/A\s+</a>',
|
||||
res_u), repr(res_u))
|
||||
|
||||
self.failUnless(re.search(
|
||||
u'<td class="service-last-received-data"><a( class="timestamp"| title=%s){2}>'
|
||||
u'1d\u00A00h\u00A00m\u00A030s</a></td>'
|
||||
@ -662,6 +680,9 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, testutil.ReallyEqualMixi
|
||||
return 0
|
||||
def get_last_received_data_time(self):
|
||||
return 0
|
||||
def connection_status(self):
|
||||
return ConnectionStatus(self.connected,
|
||||
"summary", "description", 0, 0)
|
||||
|
||||
d = defer.succeed(None)
|
||||
|
||||
@ -673,7 +694,7 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, testutil.ReallyEqualMixi
|
||||
d.addCallback(_set_introducer_not_connected_unguessable)
|
||||
def _check_introducer_not_connected_unguessable(res):
|
||||
html = res.replace('\n', ' ')
|
||||
self.failUnlessIn('<div class="furl">pb://someIntroducer/[censored]</div>', html)
|
||||
self.failUnlessIn('<div class="connection-status" title="description">summary</div>', html)
|
||||
self.failIfIn('pb://someIntroducer/secret', html)
|
||||
self.failUnless(re.search('<img (alt="Disconnected" |src="img/connected-no.png" ){2}/></div>[ ]*<div>No introducers connected</div>', html), res)
|
||||
|
||||
@ -687,7 +708,7 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, testutil.ReallyEqualMixi
|
||||
d.addCallback(_set_introducer_connected_unguessable)
|
||||
def _check_introducer_connected_unguessable(res):
|
||||
html = res.replace('\n', ' ')
|
||||
self.failUnlessIn('<div class="furl">pb://someIntroducer/[censored]</div>', html)
|
||||
self.failUnlessIn('<div class="connection-status" title="description">summary</div>', html)
|
||||
self.failIfIn('pb://someIntroducer/secret', html)
|
||||
self.failUnless(re.search('<img (src="img/connected-yes.png" |alt="Connected" ){2}/></div>[ ]*<div>1 introducer connected</div>', html), res)
|
||||
d.addCallback(_check_introducer_connected_unguessable)
|
||||
@ -700,7 +721,7 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, testutil.ReallyEqualMixi
|
||||
d.addCallback(_set_introducer_connected_guessable)
|
||||
def _check_introducer_connected_guessable(res):
|
||||
html = res.replace('\n', ' ')
|
||||
self.failUnlessIn('<div class="furl">pb://someIntroducer/introducer</div>', html)
|
||||
self.failUnlessIn('<div class="connection-status" title="description">summary</div>', html)
|
||||
self.failUnless(re.search('<img (src="img/connected-yes.png" |alt="Connected" ){2}/></div>[ ]*<div>1 introducer connected</div>', html), res)
|
||||
d.addCallback(_check_introducer_connected_guessable)
|
||||
return d
|
||||
|
@ -1,6 +1,5 @@
|
||||
import time, os
|
||||
|
||||
from twisted.internet import address
|
||||
from twisted.web import http
|
||||
from nevow import rend, url, tags as T
|
||||
from nevow.inevow import IRequest
|
||||
@ -240,18 +239,14 @@ class Root(rend.Page):
|
||||
return "%s introducers connected" % (connected_count,)
|
||||
|
||||
def data_total_introducers(self, ctx, data):
|
||||
return len(self.client.introducer_furls)
|
||||
return len(self.client.introducer_connection_statuses())
|
||||
|
||||
def data_connected_introducers(self, ctx, data):
|
||||
return self.client.introducer_connection_statuses().count(True)
|
||||
|
||||
def data_connected_to_introducer(self, ctx, data):
|
||||
if self.client.connected_to_introducer():
|
||||
return "yes"
|
||||
return "no"
|
||||
return len([1 for cs in self.client.introducer_connection_statuses()
|
||||
if cs.connected])
|
||||
|
||||
def data_connected_to_at_least_one_introducer(self, ctx, data):
|
||||
if True in self.client.introducer_connection_statuses():
|
||||
if self.data_connected_introducers(ctx, data):
|
||||
return "yes"
|
||||
return "no"
|
||||
|
||||
@ -260,42 +255,35 @@ class Root(rend.Page):
|
||||
|
||||
# In case we configure multiple introducers
|
||||
def data_introducers(self, ctx, data):
|
||||
connection_statuses = self.client.introducer_connection_statuses()
|
||||
s = []
|
||||
furls = self.client.introducer_furls
|
||||
for furl in furls:
|
||||
if connection_statuses:
|
||||
display_furl = furl
|
||||
# trim off the secret swissnum
|
||||
(prefix, _, swissnum) = furl.rpartition("/")
|
||||
if swissnum != "introducer":
|
||||
display_furl = "%s/[censored]" % (prefix,)
|
||||
i = furls.index(furl)
|
||||
ic = self.client.introducer_clients[i]
|
||||
s.append((display_furl, bool(connection_statuses[i]), ic))
|
||||
s.sort()
|
||||
return s
|
||||
return self.client.introducer_connection_statuses()
|
||||
|
||||
def render_introducers_row(self, ctx, s):
|
||||
(furl, connected, ic) = s
|
||||
service_connection_status = "yes" if connected else "no"
|
||||
|
||||
since = ic.get_since()
|
||||
service_connection_status_rel_time = render_time_delta(since, self.now_fn())
|
||||
service_connection_status_abs_time = render_time_attr(since)
|
||||
|
||||
last_received_data_time = ic.get_last_received_data_time()
|
||||
last_received_data_rel_time = render_time_delta(last_received_data_time, self.now_fn())
|
||||
last_received_data_abs_time = render_time_attr(last_received_data_time)
|
||||
|
||||
ctx.fillSlots("introducer_furl", "%s" % (furl))
|
||||
ctx.fillSlots("service_connection_status", "%s" % (service_connection_status,))
|
||||
def render_introducers_row(self, ctx, cs):
|
||||
connected = "yes" if cs.connected else "no"
|
||||
ctx.fillSlots("service_connection_status", connected)
|
||||
ctx.fillSlots("service_connection_status_alt",
|
||||
self._connectedalts[service_connection_status])
|
||||
ctx.fillSlots("service_connection_status_abs_time", service_connection_status_abs_time)
|
||||
ctx.fillSlots("service_connection_status_rel_time", service_connection_status_rel_time)
|
||||
ctx.fillSlots("last_received_data_abs_time", last_received_data_abs_time)
|
||||
ctx.fillSlots("last_received_data_rel_time", last_received_data_rel_time)
|
||||
self._connectedalts[connected])
|
||||
|
||||
since = cs.last_connection_time
|
||||
ctx.fillSlots("service_connection_status_rel_time",
|
||||
render_time_delta(since, self.now_fn())
|
||||
if since is not None
|
||||
else "N/A")
|
||||
ctx.fillSlots("service_connection_status_abs_time",
|
||||
render_time_attr(since)
|
||||
if since is not None
|
||||
else "N/A")
|
||||
|
||||
last_received_data_time = cs.last_received_time
|
||||
ctx.fillSlots("last_received_data_abs_time",
|
||||
render_time_attr(last_received_data_time)
|
||||
if last_received_data_time is not None
|
||||
else "N/A")
|
||||
ctx.fillSlots("last_received_data_rel_time",
|
||||
render_time_delta(last_received_data_time, self.now_fn())
|
||||
if last_received_data_time is not None
|
||||
else "N/A")
|
||||
ctx.fillSlots("summary", "%s" % cs.last_connection_summary)
|
||||
ctx.fillSlots("details", "%s" % cs.last_connection_description)
|
||||
return ctx.tag
|
||||
|
||||
def data_helper_furl_prefix(self, ctx, data):
|
||||
@ -344,33 +332,38 @@ class Root(rend.Page):
|
||||
return sorted(sb.get_known_servers(), key=lambda s: s.get_serverid())
|
||||
|
||||
def render_service_row(self, ctx, server):
|
||||
server_id = server.get_serverid()
|
||||
cs = server.get_connection_status()
|
||||
|
||||
ctx.fillSlots("peerid", server.get_longname())
|
||||
ctx.fillSlots("nickname", server.get_nickname())
|
||||
rhost = server.get_remote_host()
|
||||
if server.is_connected():
|
||||
if server_id == self.client.get_long_nodeid():
|
||||
rhost_s = "(loopback)"
|
||||
elif isinstance(rhost, address.IPv4Address):
|
||||
rhost_s = "%s:%d" % (rhost.host, rhost.port)
|
||||
else:
|
||||
rhost_s = str(rhost)
|
||||
addr = rhost_s
|
||||
service_connection_status = "yes"
|
||||
last_connect_time = server.get_last_connect_time()
|
||||
service_connection_status_rel_time = render_time_delta(last_connect_time, self.now_fn())
|
||||
service_connection_status_abs_time = render_time_attr(last_connect_time)
|
||||
else:
|
||||
addr = "N/A"
|
||||
service_connection_status = "no"
|
||||
last_loss_time = server.get_last_loss_time()
|
||||
service_connection_status_rel_time = render_time_delta(last_loss_time, self.now_fn())
|
||||
service_connection_status_abs_time = render_time_attr(last_loss_time)
|
||||
|
||||
last_received_data_time = server.get_last_received_data_time()
|
||||
last_received_data_rel_time = render_time_delta(last_received_data_time, self.now_fn())
|
||||
last_received_data_abs_time = render_time_attr(last_received_data_time)
|
||||
connected = "yes" if cs.connected else "no"
|
||||
ctx.fillSlots("service_connection_status", connected)
|
||||
ctx.fillSlots("service_connection_status_alt",
|
||||
self._connectedalts[connected])
|
||||
|
||||
since = cs.last_connection_time
|
||||
ctx.fillSlots("service_connection_status_rel_time",
|
||||
render_time_delta(since, self.now_fn())
|
||||
if since is not None
|
||||
else "N/A")
|
||||
ctx.fillSlots("service_connection_status_abs_time",
|
||||
render_time_attr(since)
|
||||
if since is not None
|
||||
else "N/A")
|
||||
|
||||
last_received_data_time = cs.last_received_time
|
||||
ctx.fillSlots("last_received_data_abs_time",
|
||||
render_time_attr(last_received_data_time)
|
||||
if last_received_data_time is not None
|
||||
else "N/A")
|
||||
ctx.fillSlots("last_received_data_rel_time",
|
||||
render_time_delta(last_received_data_time, self.now_fn())
|
||||
if last_received_data_time is not None
|
||||
else "N/A")
|
||||
|
||||
ctx.fillSlots("summary", "%s" % cs.last_connection_summary)
|
||||
ctx.fillSlots("details", "%s" % cs.last_connection_description)
|
||||
|
||||
announcement = server.get_announcement()
|
||||
version = announcement.get("my-version", "")
|
||||
@ -379,14 +372,6 @@ class Root(rend.Page):
|
||||
available_space = "N/A"
|
||||
else:
|
||||
available_space = abbreviate_size(available_space)
|
||||
ctx.fillSlots("address", addr)
|
||||
ctx.fillSlots("service_connection_status", service_connection_status)
|
||||
ctx.fillSlots("service_connection_status_alt",
|
||||
self._connectedalts[service_connection_status])
|
||||
ctx.fillSlots("service_connection_status_abs_time", service_connection_status_abs_time)
|
||||
ctx.fillSlots("service_connection_status_rel_time", service_connection_status_rel_time)
|
||||
ctx.fillSlots("last_received_data_abs_time", last_received_data_abs_time)
|
||||
ctx.fillSlots("last_received_data_rel_time", last_received_data_rel_time)
|
||||
ctx.fillSlots("version", version)
|
||||
ctx.fillSlots("available_space", available_space)
|
||||
|
||||
|
@ -50,6 +50,9 @@ body {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.connection-status {
|
||||
}
|
||||
|
||||
.furl {
|
||||
font-size: 0.8em;
|
||||
word-wrap: break-word;
|
||||
@ -78,7 +81,7 @@ body {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.nickname-and-peerid .timestamp {
|
||||
.timestamp {
|
||||
float: right;
|
||||
}
|
||||
|
||||
|
@ -176,7 +176,7 @@
|
||||
<thead>
|
||||
<tr n:pattern="header">
|
||||
<td><h3>Nickname</h3></td>
|
||||
<td><h3>Address</h3></td>
|
||||
<td><h3>Connection</h3></td>
|
||||
<td><h3>Last RX</h3></td>
|
||||
<td><h3>Version</h3></td>
|
||||
<td><h3>Available</h3></td>
|
||||
@ -185,16 +185,22 @@
|
||||
<tr n:pattern="item" n:render="service_row">
|
||||
<td class="nickname-and-peerid">
|
||||
<div class="status-indicator"><img><n:attr name="src">img/connected-<n:slot name="service_connection_status" />.png</n:attr><n:attr name="alt"><n:slot name="service_connection_status_alt" /></n:attr></img></div>
|
||||
<a class="timestamp"><n:attr name="title"><n:slot name="service_connection_status_abs_time"/></n:attr><n:slot name="service_connection_status_rel_time"/></a>
|
||||
<div class="nickname"><n:slot name="nickname"/></div>
|
||||
<div class="nodeid"><n:slot name="peerid"/></div>
|
||||
</td>
|
||||
<td class="address"><n:slot name="address"/></td>
|
||||
<td class="connection-status">
|
||||
<n:attr name="title"><n:slot name="details"/></n:attr>
|
||||
<n:slot name="summary"/>
|
||||
<a class="timestamp">
|
||||
<n:attr name="title"><n:slot name="service_connection_status_abs_time"/></n:attr>
|
||||
<n:slot name="service_connection_status_rel_time"/>
|
||||
</a>
|
||||
</td>
|
||||
<td class="service-last-received-data"><a class="timestamp"><n:attr name="title"><n:slot name="last_received_data_abs_time"/></n:attr><n:slot name="last_received_data_rel_time"/></a></td>
|
||||
<td class="service-version"><n:slot name="version"/></td>
|
||||
<td class="service-available-space"><n:slot name="available_space"/></td>
|
||||
</tr>
|
||||
<tr n:pattern="empty"><td colspan="5">You are not presently connected to any peers</td></tr>
|
||||
<tr n:pattern="empty"><td colspan="5">You are not presently connected to any servers.</td></tr>
|
||||
</table>
|
||||
<div class="row-fluid">
|
||||
<h2>Connected to <span n:render="string" n:data="connected_introducers" /> of <span n:render="string" n:data="total_introducers" /> introducers</h2>
|
||||
@ -202,7 +208,7 @@
|
||||
<table class="table table-striped table-bordered peer-status" n:render="sequence" n:data="introducers">
|
||||
<thead>
|
||||
<tr n:pattern="header">
|
||||
<td><h3>Address</h3></td>
|
||||
<td><h3>Connection</h3></td>
|
||||
<td><h3>Last RX</h3></td>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -210,7 +216,7 @@
|
||||
<td class="nickname-and-peerid">
|
||||
<div class="status-indicator"><img><n:attr name="src">img/connected-<n:slot name="service_connection_status" />.png</n:attr><n:attr name="alt"><n:slot name="service_connection_status_alt" /></n:attr></img></div>
|
||||
<a class="timestamp"><n:attr name="title"><n:slot name="service_connection_status_abs_time"/></n:attr><n:slot name="service_connection_status_rel_time"/></a>
|
||||
<div class="furl"><n:slot name="introducer_furl"/></div>
|
||||
<div class="connection-status"><n:attr name="title"><n:slot name="details"/></n:attr><n:slot name="summary"/></div>
|
||||
</td>
|
||||
<td class="service-last-received-data"><a class="timestamp"><n:attr name="title"><n:slot name="last_received_data_abs_time"/></n:attr><n:slot name="last_received_data_rel_time"/></a></td>
|
||||
</tr>
|
||||
|
Loading…
x
Reference in New Issue
Block a user