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:
Brian Warner 2016-12-08 15:15:49 -08:00
parent 48fc14bd30
commit 77fd41b66e
8 changed files with 127 additions and 93 deletions

View File

@ -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])

View File

@ -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)

View File

@ -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):

View File

@ -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()

View File

@ -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

View File

@ -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)

View File

@ -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;
}

View File

@ -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&nbsp;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&nbsp;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>