Merge ~ines-almeida/launchpad:project-tokens/add-new-access-token-tables-to-view into launchpad:master

Proposed by Ines Almeida
Status: Merged
Approved by: Ines Almeida
Approved revision: a14cea12f3860be2f26a355a2d61ab4fe3ba3d00
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~ines-almeida/launchpad:project-tokens/add-new-access-token-tables-to-view
Merge into: launchpad:master
Diff against target: 335 lines (+152/-50)
7 files modified
lib/canonical/launchpad/icing/css/base.scss (+8/-1)
lib/canonical/launchpad/icing/css/components/_index.scss (+2/-1)
lib/canonical/launchpad/icing/css/components/access_tokens.css (+3/-0)
lib/lp/services/auth/browser.py (+27/-1)
lib/lp/services/auth/javascript/tokens.js (+1/-0)
lib/lp/services/auth/templates/accesstokentarget-access-tokens.pt (+72/-42)
lib/lp/services/auth/tests/test_browser.py (+39/-5)
Reviewer Review Type Date Requested Status
Peter Makowski (community) Approve
Colin Watson (community) Approve
Review via email: mp+452109@code.launchpad.net

Commit message

Add 'project tokens' table to git repo access tokens view

Description of the change

This is a small update but I think it will improve the UX for these tokens, since we will be able to have a better view of the tokens that grant access to a repository within that repository "+access-tokens" view.

Regarding the CSS change, it is quite small and in line with Vanilla. It improves slightly the tables look. You can see a diff in the Jira ticket: https://warthogs.atlassian.net/browse/LP-1315. This bit is in a separate commit - happy to move it into a another new MP if you prefer.

To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) :
review: Approve
Revision history for this message
Ines Almeida (ines-almeida) :
Revision history for this message
Peter Makowski (petermakowski) wrote :

Nice. Appreciate you trying to follow vanilla styles as closely as possible.

Revision history for this message
Ines Almeida (ines-almeida) wrote :

Thank you for the review! I addressed your comments

Revision history for this message
Peter Makowski (petermakowski) wrote :

No problem, this looks great! Just one minor thing.

Revision history for this message
Peter Makowski (petermakowski) :
review: Approve
Revision history for this message
Colin Watson (cjwatson) :
b1b04f8... by Ines Almeida

Add parent tokens table to access tokens view

Original table was moved into a reusable macro, and an 'owner' column was added

a14cea1... by Ines Almeida

Update style of access tokens tables

Revision history for this message
Ines Almeida (ines-almeida) wrote :

Thank you for the tips Colin!

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/canonical/launchpad/icing/css/base.scss b/lib/canonical/launchpad/icing/css/base.scss
2index f86bbf0..ea976cc 100644
3--- a/lib/canonical/launchpad/icing/css/base.scss
4+++ b/lib/canonical/launchpad/icing/css/base.scss
5@@ -366,9 +366,15 @@ body {
6 table {
7 &.listing {
8 margin: 0;
9+ margin-bottom: 1.5rem;
10 width: 100%;
11 border-bottom: 1px solid #d2d2d2;
12
13+ form {
14+ /* Remove margin from forms when they are embedded within a table */
15+ margin-bottom: 0;
16+ }
17+
18 tbody, td.end-of-section {
19 border-bottom: 1px solid #d2d2d2;
20 }
21@@ -440,7 +446,8 @@ body {
22 }
23
24 th, td {
25- padding: 0.25em;
26+ padding: 0.5rem;
27+ padding-top: calc(.5rem - 1px);
28 }
29
30 table {
31diff --git a/lib/canonical/launchpad/icing/css/components/_index.scss b/lib/canonical/launchpad/icing/css/components/_index.scss
32index e1623c8..3facac8 100644
33--- a/lib/canonical/launchpad/icing/css/components/_index.scss
34+++ b/lib/canonical/launchpad/icing/css/components/_index.scss
35@@ -1,4 +1,5 @@
36-@import 'batch_navigation',
37+@import 'access_tokens',
38+ 'batch_navigation',
39 'bug_picker',
40 'profiling_info',
41 'sidebar_portlets',
42diff --git a/lib/canonical/launchpad/icing/css/components/access_tokens.css b/lib/canonical/launchpad/icing/css/components/access_tokens.css
43new file mode 100644
44index 0000000..351ae1c
45--- /dev/null
46+++ b/lib/canonical/launchpad/icing/css/components/access_tokens.css
47@@ -0,0 +1,3 @@
48+.access-tokens-table {
49+ max-width: 80rem;
50+}
51diff --git a/lib/lp/services/auth/browser.py b/lib/lp/services/auth/browser.py
52index b10d64b..21ccfcb 100644
53--- a/lib/lp/services/auth/browser.py
54+++ b/lib/lp/services/auth/browser.py
55@@ -16,7 +16,13 @@ from lp.app.browser.launchpadform import LaunchpadFormView, action
56 from lp.app.errors import UnexpectedFormData
57 from lp.app.widgets.date import DateTimeWidget
58 from lp.app.widgets.itemswidgets import LabeledMultiCheckBoxWidget
59-from lp.services.auth.interfaces import IAccessToken, IAccessTokenSet
60+from lp.code.interfaces.gitrepository import IGitRepository
61+from lp.registry.interfaces.product import IProduct
62+from lp.services.auth.interfaces import (
63+ IAccessToken,
64+ IAccessTokenSet,
65+ IAccessTokenTarget,
66+)
67 from lp.services.propertycache import cachedproperty
68 from lp.services.webapp.publisher import canonical_url
69
70@@ -49,6 +55,14 @@ class AccessTokensView(LaunchpadFormView):
71
72 page_title = "Personal access tokens"
73
74+ @property
75+ def context_type(self):
76+ if IGitRepository.providedBy(self.context):
77+ return "Git repository"
78+ elif IProduct.providedBy(self.context):
79+ return "project"
80+ return "target"
81+
82 @cachedproperty
83 def access_tokens(self):
84 return list(
85@@ -57,6 +71,18 @@ class AccessTokensView(LaunchpadFormView):
86 )
87 )
88
89+ @cachedproperty
90+ def parent_tokens(self):
91+ if IGitRepository.providedBy(
92+ self.context
93+ ) and IAccessTokenTarget.providedBy(self.context.target):
94+ return list(
95+ getUtility(IAccessTokenSet).findByTarget(
96+ self.context.target, visible_by_user=self.user
97+ )
98+ )
99+ return []
100+
101 @action("Revoke", name="revoke")
102 def revoke_action(self, action, data):
103 form = self.request.form
104diff --git a/lib/lp/services/auth/javascript/tokens.js b/lib/lp/services/auth/javascript/tokens.js
105index 81b4d4b..ebc6039 100644
106--- a/lib/lp/services/auth/javascript/tokens.js
107+++ b/lib/lp/services/auth/javascript/tokens.js
108@@ -103,6 +103,7 @@ YUI.add('lp.services.auth.tokens', function(Y) {
109 ? Y.lp.ui.CSS_ODD : Y.lp.ui.CSS_EVEN)
110 .append(Y.Node.create('<td />')
111 .set('text', description))
112+ .append(Y.Node.create('<td />'))
113 .append(Y.Node.create('<td />')
114 .set('text', scopes.join(', ')))
115 .append(Y.Node.create('<td />')
116diff --git a/lib/lp/services/auth/templates/accesstokentarget-access-tokens.pt b/lib/lp/services/auth/templates/accesstokentarget-access-tokens.pt
117index 6d85df0..d6e3563 100644
118--- a/lib/lp/services/auth/templates/accesstokentarget-access-tokens.pt
119+++ b/lib/lp/services/auth/templates/accesstokentarget-access-tokens.pt
120@@ -32,6 +32,60 @@
121 </script>
122 </metal:block>
123
124+
125+<metal:macros fill-slot="bogus">
126+ <metal:access-tokens-table define-macro="access-tokens-table">
127+ <tal:comment condition="nothing">
128+ This macro requires a string value for 'access_tokens_attribute', which
129+ will be the name of the view function used to fetch the access tokens.
130+ </tal:comment>
131+ <table tal:attributes="id access_tokens_attribute"
132+ class="listing access-tokens-table">
133+ <thead>
134+ <tr>
135+ <th>Description</th>
136+ <th>Owner</th>
137+ <th>Scopes</th>
138+ <th>Created</th>
139+ <th>Last used</th>
140+ <th>Expires</th>
141+ <th><tal:comment condition="nothing">Revoke button</tal:comment></th>
142+ </tr>
143+ </thead>
144+ <tbody id="access-tokens-tbody">
145+ <tr tal:repeat="token python:getattr(view, access_tokens_attribute)"
146+ tal:attributes="
147+ class python: 'yui3-lazr-even' if repeat['token'].even()
148+ else 'yui3-lazr-odd';
149+ token-id token/id">
150+ <td tal:content="token/description" />
151+ <td tal:content="structure token/owner/fmt:link" />
152+ <td tal:content="
153+ python: ', '.join(scope.title for scope in token.scopes)" />
154+ <td tal:content="
155+ structure token/date_created/fmt:approximatedatetitle" />
156+ <td tal:content="
157+ structure token/date_last_used/fmt:approximatedatetitle" />
158+ <td>
159+ <tal:expires
160+ condition="token/date_expires"
161+ replace="
162+ structure token/date_expires/fmt:approximatedatetitle" />
163+ <span tal:condition="not: token/date_expires">Never</span>
164+ </td>
165+ <td>
166+ <form method="post" tal:attributes="name string:revoke-${token/id}">
167+ <input type="hidden" name="token_id"
168+ tal:attributes="value token/id" />
169+ <input type="submit" name="field.actions.revoke" value="Revoke" />
170+ </form>
171+ </td>
172+ </tr>
173+ </tbody>
174+ </table>
175+ </metal:access-tokens-table>
176+</metal:macros>
177+
178 <div metal:fill-slot="main">
179 <p>Personal access tokens allow using certain parts of the Launchpad API.</p>
180
181@@ -74,48 +128,24 @@
182 </div>
183
184 <h2>Active tokens</h2>
185- <table class="listing access-tokens-table"
186- style="max-width: 80em;">
187- <thead>
188- <tr>
189- <th>Description</th>
190- <th>Scopes</th>
191- <th>Created</th>
192- <th>Last used</th>
193- <th>Expires</th>
194- <th><tal:comment condition="nothing">Revoke button</tal:comment></th>
195- </tr>
196- </thead>
197- <tbody id="access-tokens-tbody">
198- <tr tal:repeat="token view/access_tokens"
199- tal:attributes="
200- class python: 'yui3-lazr-even' if repeat['token'].even()
201- else 'yui3-lazr-odd';
202- token-id token/id">
203- <td tal:content="token/description" />
204- <td tal:content="
205- python: ', '.join(scope.title for scope in token.scopes)" />
206- <td tal:content="
207- structure token/date_created/fmt:approximatedatetitle" />
208- <td tal:content="
209- structure token/date_last_used/fmt:approximatedatetitle" />
210- <td>
211- <tal:expires
212- condition="token/date_expires"
213- replace="
214- structure token/date_expires/fmt:approximatedatetitle" />
215- <span tal:condition="not: token/date_expires">Never</span>
216- </td>
217- <td>
218- <form method="post" tal:attributes="name string:revoke-${token/id}">
219- <input type="hidden" name="token_id"
220- tal:attributes="value token/id" />
221- <input type="submit" name="field.actions.revoke" value="Revoke" />
222- </form>
223- </td>
224- </tr>
225- </tbody>
226- </table>
227+ <p>Active personal access tokens for
228+ <a tal:replace="structure context/fmt:link" />
229+ <span tal:content="view/context_type" />.
230+ </p>
231+ <tal:access-tokens-table define="access_tokens_attribute string:access_tokens">
232+ <metal:access-tokens-table use-macro="template/macros/access-tokens-table" />
233+ </tal:access-tokens-table>
234+ <div tal:condition="view/parent_tokens">
235+ <h2>Project tokens</h2>
236+ <p>Your project access tokens for this Git repository's project:
237+ <a tal:replace="structure context/target/fmt:link" />.
238+ </p>
239+ <p>Project tokens grant access to all the project's Git repositories
240+ accessible to a user.</p>
241+ <tal:access-tokens-table define="access_tokens_attribute string:parent_tokens">
242+ <metal:access-tokens-table use-macro="template/macros/access-tokens-table" />
243+ </tal:access-tokens-table>
244+ </div>
245 </div>
246
247 </body>
248diff --git a/lib/lp/services/auth/tests/test_browser.py b/lib/lp/services/auth/tests/test_browser.py
249index 76e1221..721ef8b 100644
250--- a/lib/lp/services/auth/tests/test_browser.py
251+++ b/lib/lp/services/auth/tests/test_browser.py
252@@ -17,6 +17,7 @@ from testtools.matchers import (
253 from zope.component import getUtility
254 from zope.security.proxy import removeSecurityProxy
255
256+from lp.code.interfaces.gitrepository import IGitRepository
257 from lp.services.webapp.interfaces import IPlacelessAuthUtility
258 from lp.services.webapp.publisher import canonical_url
259 from lp.testing import TestCaseWithFactory, login_person
260@@ -33,7 +34,13 @@ token_listing_constants = soupmatchers.HTMLContains(
261 soupmatchers.Within(breadcrumbs_tag, tokens_page_crumb_tag)
262 )
263 token_listing_tag = soupmatchers.Tag(
264- "tokens table", "table", attrs={"class": "listing"}
265+ "tokens table", "table", attrs={"id": "access_tokens"}
266+)
267+
268+project_token_listing_tag = soupmatchers.Tag(
269+ "project tokens table",
270+ "table",
271+ attrs={"id": "parent_tokens"},
272 )
273
274
275@@ -82,9 +89,11 @@ class TestAccessTokenViewBase:
276 tokens_link = browser.getLink("Manage access tokens")
277 self.assertEqual(expected_tokens_url, tokens_link.url)
278
279- def makeTokensAndMatchers(self, count):
280+ def makeTokensAndMatchers(self, count, target=None):
281+ if target is None:
282+ target = self.target
283 tokens = [
284- self.factory.makeAccessToken(target=self.target)[1]
285+ self.factory.makeAccessToken(target=target)[1]
286 for _ in range(count)
287 ]
288 # There is a row for each token.
289@@ -124,6 +133,23 @@ class TestAccessTokenViewBase:
290 MatchesAll(*self.getPageContent(token_matchers)),
291 )
292
293+ def test_extra_tokens(self):
294+ if not IGitRepository.providedBy(self.target):
295+ self.skipTest("Test only relevant for Git Repository tests.")
296+
297+ project = self.factory.makeProduct(owner=self.owner)
298+ self.target.setTarget(target=project, user=self.owner)
299+ token_matchers = self.makeTokensAndMatchers(5)
300+ extra_matchers = self.makeTokensAndMatchers(5, target=project)
301+ self.assertThat(
302+ self.makeView("+access-tokens")(),
303+ MatchesAll(
304+ *self.getPageContent(
305+ token_matchers, extra_matchers=extra_matchers
306+ )
307+ ),
308+ )
309+
310 def test_revoke(self):
311 tokens = [
312 self.factory.makeAccessToken(target=self.target)[1]
313@@ -192,12 +218,20 @@ class TestAccessTokenViewGitRepository(
314 def getTraversalStack(self, obj):
315 return [obj.target, obj]
316
317- def getPageContent(self, token_matchers):
318- return [
319+ def getPageContent(self, token_matchers, extra_matchers=None):
320+ contents = [
321 token_listing_constants,
322 soupmatchers.HTMLContains(token_listing_tag, *token_matchers),
323 ]
324
325+ if extra_matchers is not None:
326+ contents.append(
327+ soupmatchers.HTMLContains(
328+ project_token_listing_tag, *extra_matchers
329+ )
330+ )
331+ return contents
332+
333
334 class TestAccessTokenViewProject(TestAccessTokenViewBase, TestCaseWithFactory):
335 rootsite = None

Subscribers

People subscribed via source and target branches

to status/vote changes: