From: Leif Ryge Date: Mon, 4 Jan 2016 19:58:55 +0000 (+0000) Subject: wui: improved columns in welcome page server list X-Git-Url: https://git.rkrishnan.org/%5B/%5D%20/uri/%22doc.html/%5B%5E?a=commitdiff_plain;h=5b33f1fdf3964db3a7475df36d5e83db46f6b002;p=tahoe-lafs%2Ftahoe-lafs.git 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, https://github.com/tahoe-lafs/tahoe-lafs/commit/9fabb924867e164e1c6d4d805761db6c39652cf7, https://github.com/tahoe-lafs/tahoe-lafs/commit/bbd8b42a25f8617c43b8293f3b654b3d060e27b9 Unlike previous attempts, the tests on this one should pass in any timezone. (But like current master, will fail with Nevow >=0.12...) Thanks to an anonymous contributor who wrote some of the tests. --- 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/common_util.py b/src/allmydata/test/common_util.py index e51ab8b0..47dbe59a 100644 --- a/src/allmydata/test/common_util.py +++ b/src/allmydata/test/common_util.py @@ -173,6 +173,24 @@ class TestMixin(SignalMixin): if required_to_quiesce and active: self.fail("Reactor was still active when it was required to be quiescent.") + +class TimezoneMixin(object): + + def setTimezone(self, timezone): + unset = object() + originalTimezone = os.environ.get('TZ', unset) + def restoreTimezone(): + if originalTimezone is unset: + del os.environ['TZ'] + time.tzset() + else: + os.environ['TZ'] = originalTimezone + time.tzset() + os.environ['TZ'] = timezone + time.tzset() + self.addCleanup(restoreTimezone) + + try: import win32file import win32con diff --git a/src/allmydata/test/test_util.py b/src/allmydata/test/test_util.py index adaec89f..5fe2fbd0 100644 --- a/src/allmydata/test/test_util.py +++ b/src/allmydata/test/test_util.py @@ -1021,6 +1021,40 @@ class TimeFormat(unittest.TestCase): test_format_time_y2038.todo = "This test is known to fail on systems with 32-bit time_t." + 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..5b151ecb 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_loss_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_loss_time = last_loss_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_loss_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_loss_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_loss_time = 25, last_rx_time = 35)) self.introducer_client = None self.history = FakeHistory() self.uploader = FakeUploader() @@ -268,14 +281,16 @@ class FakeClient(Client): MUTABLE_SIZELIMIT = FakeMutableFileNode.MUTABLE_SIZELIMIT -class WebMixin(object): +class WebMixin(testutil.TimezoneMixin): def setUp(self): + self.setTimezone('UTC-13:00') self.s = FakeClient() 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() @@ -601,7 +616,6 @@ class WebMixin(object): self.fail("%s was supposed to Error(302), not get '%s'" % (which, res)) - class Web(WebMixin, WebErrorMixin, testutil.StallMixin, testutil.ReallyEqualMixin, unittest.TestCase): def test_create(self): pass @@ -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 c481adc3..a6fe3ec7 100644 --- a/src/allmydata/util/time_format.py +++ b/src/allmydata/util/time_format.py @@ -66,3 +66,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 b8e6d5b4..5cfe84d9 100644 --- a/src/allmydata/web/common.py +++ b/src/allmydata/web/common.py @@ -15,7 +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 +from allmydata.util.time_format import format_time, format_delta from allmydata.util.encodingutil import to_str, quote_output @@ -213,9 +213,15 @@ def text_plain(text, ctx): 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 c46a6dd5..d8d789cf 100644 --- a/src/allmydata/web/root.py +++ b/src/allmydata/web/root.py @@ -14,7 +14,7 @@ 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, render_time + get_arg, RenderMixin, get_format, get_mutable_type, render_time_delta, render_time, render_time_attr class URIHandler(RenderMixin, rend.Page): @@ -138,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: @@ -282,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): @@ -290,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", render_time(since)) - ctx.fillSlots("announced", render_time(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)