From 9fabb924867e164e1c6d4d805761db6c39652cf7 Mon Sep 17 00:00:00 2001 From: Leif Ryge Date: Mon, 4 Jan 2016 19:58:55 +0000 Subject: [PATCH] wui: improved columns in welcome page server list As discussed at https://tahoe-lafs.org/trac/tahoe-lafs/ticket/1973 and in previous pull request #129. - replace lengthy timestamps with human-readable deltas (eg 1h 2m 3s) - replace "announced" column with "Last RX" column - remove service column (it always said the same thing, "storage") - fix colspan on 'You are not presently connected' message Previous versions, some with github comments: https://github.com/leif/tahoe-lafs/commit/3fe9053134b2429904f673df561e602a50f83c7e , https://github.com/leif/tahoe-lafs/commit/486dbfc7bd3c0bbba42a6df8e4564601120aec0e , and https://github.com/tahoe-lafs/tahoe-lafs/commit/c89ea625803be36a18bce1af4eef95dcd78bba2b Thanks to an anonymous contributor who wrote some of the tests. --- src/allmydata/interfaces.py | 1 - src/allmydata/storage_client.py | 9 ++-- src/allmydata/test/test_util.py | 49 ++++++++++++++++++++++ src/allmydata/test/test_web.py | 33 +++++++++++---- src/allmydata/util/time_format.py | 26 +++++++++++- src/allmydata/web/common.py | 15 +++++++ src/allmydata/web/root.py | 40 +++++++++++------- src/allmydata/web/static/css/new-tahoe.css | 9 ++++ src/allmydata/web/welcome.xhtml | 16 ++++--- src/allmydata/webish.py | 4 +- 10 files changed, 162 insertions(+), 40 deletions(-) diff --git a/src/allmydata/interfaces.py b/src/allmydata/interfaces.py index e495c759..31fc6144 100644 --- a/src/allmydata/interfaces.py +++ b/src/allmydata/interfaces.py @@ -417,7 +417,6 @@ class IStorageBroker(Interface): public attributes:: service_name: the type of service provided, like 'storage' - announcement_time: when we first heard about this service last_connect_time: when we last established a connection last_loss_time: when we last lost a connection diff --git a/src/allmydata/storage_client.py b/src/allmydata/storage_client.py index dd9780f2..81dc0042 100644 --- a/src/allmydata/storage_client.py +++ b/src/allmydata/storage_client.py @@ -168,7 +168,6 @@ class NativeStorageServer: the their version information. I remember information about when we were last connected too, even if we aren't currently connected. - @ivar announcement_time: when we first heard about this service @ivar last_connect_time: when we last established a connection @ivar last_loss_time: when we last lost a connection @@ -216,7 +215,6 @@ class NativeStorageServer: self._long_description = tubid_s self._short_description = tubid_s[:6] - self.announcement_time = time.time() self.last_connect_time = None self.last_loss_time = None self.remote_host = None @@ -267,8 +265,11 @@ class NativeStorageServer: return self.last_connect_time def get_last_loss_time(self): return self.last_loss_time - def get_announcement_time(self): - return self.announcement_time + def get_last_received_data_time(self): + if self.rref is None: + return None + else: + return self.rref.getDataLastReceivedAt() def get_available_space(self): version = self.get_version() diff --git a/src/allmydata/test/test_util.py b/src/allmydata/test/test_util.py index db64bf19..eb0bac4a 100644 --- a/src/allmydata/test/test_util.py +++ b/src/allmydata/test/test_util.py @@ -1006,6 +1006,55 @@ class TimeFormat(unittest.TestCase): def test_parse_date(self): self.failUnlessEqual(time_format.parse_date("2010-02-21"), 1266710400) + def test_format_time(self): + self.failUnlessEqual(time_format.format_time(time.gmtime(0)), '1970-01-01 00:00:00') + self.failUnlessEqual(time_format.format_time(time.gmtime(60)), '1970-01-01 00:01:00') + self.failUnlessEqual(time_format.format_time(time.gmtime(60*60)), '1970-01-01 01:00:00') + seconds_per_day = 60*60*24 + leap_years_1970_to_2014_inclusive = ((2012 - 1968) // 4) + self.failUnlessEqual(time_format.format_time(time.gmtime(seconds_per_day*((2015 - 1970)*365+leap_years_1970_to_2014_inclusive))), '2015-01-01 00:00:00') + + def test_format_time_y2038(self): + seconds_per_day = 60*60*24 + leap_years_1970_to_2047_inclusive = ((2044 - 1968) // 4) + self.failUnlessEqual(time_format.format_time(time.gmtime(seconds_per_day*((2048 - 1970)*365+leap_years_1970_to_2047_inclusive))), '2048-01-01 00:00:00') + + test_format_time_y2038.todo = "one day we'll move beyond 32-bit time" + + def test_format_delta(self): + time_1 = 1389812723 + time_5s_delta = 1389812728 + time_28m7s_delta = 1389814410 + time_1h_delta = 1389816323 + time_1d21h46m49s_delta = 1389977532 + + self.failUnlessEqual( + time_format.format_delta(time_1, time_1), '0s') + + self.failUnlessEqual( + time_format.format_delta(time_1, time_5s_delta), '5s') + self.failUnlessEqual( + time_format.format_delta(time_1, time_28m7s_delta), '28m 7s') + self.failUnlessEqual( + time_format.format_delta(time_1, time_1h_delta), '1h 0m 0s') + self.failUnlessEqual( + time_format.format_delta(time_1, time_1d21h46m49s_delta), '1d 21h 46m 49s') + + self.failUnlessEqual( + time_format.format_delta(time_1d21h46m49s_delta, time_1), '-') + + # time_1 with a decimal fraction will make the delta 1s less + time_1decimal = 1389812723.383963 + + self.failUnlessEqual( + time_format.format_delta(time_1decimal, time_5s_delta), '4s') + self.failUnlessEqual( + time_format.format_delta(time_1decimal, time_28m7s_delta), '28m 6s') + self.failUnlessEqual( + time_format.format_delta(time_1decimal, time_1h_delta), '59m 59s') + self.failUnlessEqual( + time_format.format_delta(time_1decimal, time_1d21h46m49s_delta), '1d 21h 46m 48s') + class CacheDir(unittest.TestCase): def test_basic(self): basedir = "test_util/CacheDir/test_basic" diff --git a/src/allmydata/test/test_web.py b/src/allmydata/test/test_web.py index 723ae7ac..5420400e 100644 --- a/src/allmydata/test/test_web.py +++ b/src/allmydata/test/test_web.py @@ -172,21 +172,28 @@ class FakeHistory: return [] class FakeDisplayableServer(StubServer): - def __init__(self, serverid, nickname): + def __init__(self, serverid, nickname, connected, + last_connect_time, last_lost_time, last_rx_time): StubServer.__init__(self, serverid) self.announcement = {"my-version": "allmydata-tahoe-fake", "service-name": "storage", "nickname": nickname} + self.connected = connected + self.last_lost_time = last_lost_time + self.last_rx_time = last_rx_time + self.last_connect_time = last_connect_time def is_connected(self): - return True + return self.connected def get_permutation_seed(self): return "" def get_remote_host(self): return "" def get_last_loss_time(self): - return None - def get_announcement_time(self): - return None + return self.last_lost_time + def get_last_received_data_time(self): + return self.last_rx_time + def get_last_connect_time(self): + return self.last_connect_time def get_announcement(self): return self.announcement def get_nickname(self): @@ -242,7 +249,13 @@ class FakeClient(Client): self.storage_broker = StorageFarmBroker(None, permute_peers=True) # fake knowledge of another server self.storage_broker.test_add_server("other_nodeid", - FakeDisplayableServer("other_nodeid", u"other_nickname \u263B")) + FakeDisplayableServer( + serverid="other_nodeid", nickname=u"other_nickname \u263B", connected = True, + last_connect_time = 10, last_lost_time = 20, last_rx_time = 30)) + self.storage_broker.test_add_server("disconnected_nodeid", + FakeDisplayableServer( + serverid="other_nodeid", nickname=u"disconnected_nickname \u263B", connected = False, + last_connect_time = 15, last_lost_time = 25, last_rx_time = 35)) self.introducer_client = None self.history = FakeHistory() self.uploader = FakeUploader() @@ -274,8 +287,9 @@ class WebMixin(object): self.s.startService() self.staticdir = self.mktemp() self.clock = Clock() + self.fakeTime = 86460 # 1d 0h 1m 0s self.ws = webish.WebishServer(self.s, "0", staticdir=self.staticdir, - clock=self.clock) + clock=self.clock, now_fn=lambda:self.fakeTime) self.ws.setServiceParent(self.s) self.webish_port = self.ws.getPortnum() self.webish_url = self.ws.getURL() @@ -619,6 +633,11 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, testutil.ReallyEqualMixi res_u = res.decode('utf-8') self.failUnlessIn(u'fake_nickname \u263A', res_u) self.failUnlessIn(u'
other_nickname \u263B
', res_u) + self.failUnlessIn(u'Connected to 1\n of 2 known storage servers', res_u) + self.failUnlessIn(u'
Connected
\n 1d\u00A00h\u00A00m\u00A050s', res_u) + self.failUnlessIn(u'
Disconnected
\n 1d\u00A00h\u00A00m\u00A035s', res_u) + self.failUnlessIn(u'1d\u00A00h\u00A00m\u00A030s', res_u) + self.failUnlessIn(u'1d\u00A00h\u00A00m\u00A025s', res_u) self.failUnlessIn(u'\u00A9 Tahoe-LAFS Software Foundation', res_u) self.failUnlessIn('

Available

', res) self.failUnlessIn('123.5kB', res) diff --git a/src/allmydata/util/time_format.py b/src/allmydata/util/time_format.py index f159ebec..f3cb486a 100644 --- a/src/allmydata/util/time_format.py +++ b/src/allmydata/util/time_format.py @@ -4,7 +4,7 @@ import calendar, datetime, re, time def format_time(t): - return time.strftime("%H:%M:%S %d-%b-%Y", t) + return time.strftime("%Y-%m-%d %H:%M:%S", t) def iso_utc_date(now=None, t=time.time): if now is None: @@ -71,3 +71,27 @@ def parse_date(s): # day return int(iso_utc_time_to_seconds(s + "T00:00:00")) +def format_delta(time_1, time_2): + if time_1 is None: + return "N/A" + if time_1 > time_2: + return '-' + delta = int(time_2 - time_1) + seconds = delta % 60 + delta -= seconds + minutes = (delta / 60) % 60 + delta -= minutes * 60 + hours = delta / (60*60) % 24 + delta -= hours * 24 + days = delta / (24*60*60) + if not days: + if not hours: + if not minutes: + return "%ss" % (seconds) + else: + return "%sm %ss" % (minutes, seconds) + else: + return "%sh %sm %ss" % (hours, minutes, seconds) + else: + return "%sd %sh %sm %ss" % (days, hours, minutes, seconds) + diff --git a/src/allmydata/web/common.py b/src/allmydata/web/common.py index f4229bb2..5cfe84d9 100644 --- a/src/allmydata/web/common.py +++ b/src/allmydata/web/common.py @@ -1,5 +1,7 @@ +import time import simplejson + from twisted.web import http, server from twisted.python import log from zope.interface import Interface @@ -13,6 +15,7 @@ from allmydata.interfaces import ExistingChildError, NoSuchChildError, \ MustBeReadonlyError, MustNotBeUnknownRWError, SDMF_VERSION, MDMF_VERSION from allmydata.mutable.common import UnrecoverableFileError from allmydata.util import abbreviate +from allmydata.util.time_format import format_time, format_delta from allmydata.util.encodingutil import to_str, quote_output @@ -207,6 +210,18 @@ def text_plain(text, ctx): req.setHeader("content-length", b"%d" % len(text)) return text +def spaces_to_nbsp(text): + return unicode(text).replace(u' ', u'\u00A0') + +def render_time_delta(time_1, time_2): + return spaces_to_nbsp(format_delta(time_1, time_2)) + +def render_time(t): + return spaces_to_nbsp(format_time(time.localtime(t))) + +def render_time_attr(t): + return format_time(time.localtime(t)) + class WebError(Exception): def __init__(self, text, code=http.BAD_REQUEST): self.text = text diff --git a/src/allmydata/web/root.py b/src/allmydata/web/root.py index 06c6d5b4..d8d789cf 100644 --- a/src/allmydata/web/root.py +++ b/src/allmydata/web/root.py @@ -10,12 +10,11 @@ from nevow.util import resource_filename import allmydata # to display import path from allmydata import get_package_versions_string from allmydata.util import log -from allmydata.util.time_format import format_time from allmydata.interfaces import IFileNode from allmydata.web import filenode, directory, unlinked, status, operations from allmydata.web import storage from allmydata.web.common import abbreviate_size, getxmlfile, WebError, \ - get_arg, RenderMixin, get_format, get_mutable_type + get_arg, RenderMixin, get_format, get_mutable_type, render_time_delta, render_time, render_time_attr class URIHandler(RenderMixin, rend.Page): @@ -139,12 +138,13 @@ class Root(rend.Page): "no": "Disconnected", } - def __init__(self, client, clock=None): + def __init__(self, client, clock=None, now_fn=None): rend.Page.__init__(self, client) self.client = client # If set, clock is a twisted.internet.task.Clock that the tests # use to test ophandle expiration. self.child_operations = operations.OphandleTable(clock) + self.now_fn = now_fn try: s = client.getServiceNamed("storage") except KeyError: @@ -172,7 +172,7 @@ class Root(rend.Page): # FIXME: This code is duplicated in root.py and introweb.py. def data_rendered_at(self, ctx, data): - return format_time(time.localtime()) + return render_time(time.time()) def data_version(self, ctx, data): return get_package_versions_string() def data_import_path(self, ctx, data): @@ -283,7 +283,7 @@ class Root(rend.Page): ctx.fillSlots("peerid", server.get_longname()) ctx.fillSlots("nickname", server.get_nickname()) rhost = server.get_remote_host() - if rhost: + if server.is_connected(): if nodeid == self.client.nodeid: rhost_s = "(loopback)" elif isinstance(rhost, address.IPv4Address): @@ -291,29 +291,37 @@ class Root(rend.Page): else: rhost_s = str(rhost) addr = rhost_s - connected = "yes" - since = server.get_last_connect_time() + 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" - connected = "no" - since = server.get_last_loss_time() - announced = server.get_announcement_time() + 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) + announcement = server.get_announcement() version = announcement["my-version"] - service_name = announcement["service-name"] available_space = server.get_available_space() if available_space is None: available_space = "N/A" else: available_space = abbreviate_size(available_space) ctx.fillSlots("address", addr) - ctx.fillSlots("connected", connected) - ctx.fillSlots("connected_alt", self._connectedalts[connected]) + ctx.fillSlots("service_connection_status", service_connection_status) + ctx.fillSlots("service_connection_status_alt", self._connectedalts[service_connection_status]) ctx.fillSlots("connected-bool", bool(rhost)) - ctx.fillSlots("since", format_time(time.localtime(since))) - ctx.fillSlots("announced", format_time(time.localtime(announced))) + 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("service_name", service_name) ctx.fillSlots("available_space", available_space) return ctx.tag diff --git a/src/allmydata/web/static/css/new-tahoe.css b/src/allmydata/web/static/css/new-tahoe.css index 175c3e33..8ab7f47d 100644 --- a/src/allmydata/web/static/css/new-tahoe.css +++ b/src/allmydata/web/static/css/new-tahoe.css @@ -77,3 +77,12 @@ body { float: left; margin: 5px; } + +.nickname-and-peerid .timestamp { + float: right; +} + +a.timestamp { + color: inherit; + text-decoration:none; +} diff --git a/src/allmydata/web/welcome.xhtml b/src/allmydata/web/welcome.xhtml index 3e23fc73..ca238767 100644 --- a/src/allmydata/web/welcome.xhtml +++ b/src/allmydata/web/welcome.xhtml @@ -1,4 +1,5 @@ - + @@ -169,27 +170,24 @@

Nickname

Address

-

Service

-

Since

-

Announced

+

Last RX

Version

Available

-
img/connected-.png
+
img/connected-.png
+
- - - + - You are not presently connected to any peers + You are not presently connected to any peers diff --git a/src/allmydata/webish.py b/src/allmydata/webish.py index e2029fee..b26b34da 100644 --- a/src/allmydata/webish.py +++ b/src/allmydata/webish.py @@ -129,14 +129,14 @@ class WebishServer(service.MultiService): name = "webish" def __init__(self, client, webport, nodeurl_path=None, staticdir=None, - clock=None): + clock=None, now_fn=time.time): service.MultiService.__init__(self) # the 'data' argument to all render() methods default to the Client # the 'clock' argument to root.Root is, if set, a # twisted.internet.task.Clock that is provided by the unit tests # so that they can test features that involve the passage of # time in a deterministic manner. - self.root = root.Root(client, clock) + self.root = root.Root(client, clock, now_fn) self.buildServer(webport, nodeurl_path, staticdir) if self.root.child_operations: self.site.remember(self.root.child_operations, IOpHandleTable) -- 2.37.2