Skip to content

Commit d6029c9

Browse files
committed
Settings is split into tabs
The page had grown to nineteen sections on one long scroll. They are now in five tabs: Scanning, What it looks for, Sign in, Reports, and Web and external sites. Each tab shows only its sections, with Save right below. It is still one form, so saving from any tab keeps everything on the others. A link to a section, such as /settings#external, opens the tab it is on, the last tab used comes back after a save, and a field the browser refuses on a tab that is not showing opens that tab instead of stopping the save without a word. Without script every section shows, as before. The page title also had the settings script inside it, which some browsers showed as text in the tab.
1 parent d4d97c3 commit d6029c9

5 files changed

Lines changed: 131 additions & 7 deletions

File tree

‎app/web/help_text.py‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,3 +305,24 @@
305305
def sections() -> list:
306306
"""The sections, in page order, as plain dictionaries for the template."""
307307
return [{"id": key, "nav": nav, **body} for key, nav, body in SECTIONS]
308+
309+
310+
# The tabs on the Settings page, and which sections each one holds. Every
311+
# section is in exactly one, which a test checks, so a new section cannot be
312+
# left on no tab and quietly never shown.
313+
TABS = [
314+
("scanning", "Scanning", ["accounts", "scan", "branches", "people", "limits"]),
315+
("findings", "What it looks for", ["credentials", "code", "dependencies",
316+
"intel", "fidelity", "sensitivity"]),
317+
("signin", "Sign in", ["signin", "sessions"]),
318+
("reports", "Reports", ["output", "reports", "display"]),
319+
("web", "Web and external sites", ["domains", "webtest", "external"]),
320+
]
321+
322+
323+
def tabs() -> list:
324+
"""-> [{id, label, sections: [{id, nav}]}] in page order."""
325+
names = {key: nav for key, nav, _body in SECTIONS}
326+
return [{"id": key, "label": label,
327+
"sections": [{"id": s, "nav": names.get(s, s)} for s in ids]}
328+
for key, label, ids in TABS]

‎app/web/routes_pages.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3565,6 +3565,7 @@ def settings_page(request: Request, msg: str = "", kind: str = "",
35653565
from app.core import rule_mutes, settings_store as store
35663566
from app.core.models import RuleMute
35673567
from app.web.help_text import sections as help_sections
3568+
from app.web.help_text import tabs as settings_tabs
35683569

35693570
with session_scope() as db:
35703571
context = _shared(db, user)
@@ -3573,7 +3574,7 @@ def settings_page(request: Request, msg: str = "", kind: str = "",
35733574
# The links across the top and the ? beside each heading come from
35743575
# the same list, so a section cannot end up in one and not the
35753576
# other.
3576-
help_sections=help_sections(),
3577+
help_sections=help_sections(), settings_tabs=settings_tabs(),
35773578
values=store.all_values(db),
35783579
rulesets=store.RULESETS,
35793580
muted_notes=store.MUTED_RULE_NOTES,

‎app/web/static/app.css‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1108,3 +1108,9 @@ th.num, td.num { text-align: right; font-variant-numeric: tabular-nums; }
11081108
.ext-grid { display: grid; grid-template-columns: minmax(0, 2fr) minmax(0, 3fr); gap: 1rem; }
11091109
@media (max-width: 1100px) { .ext-grid { grid-template-columns: 1fr; } }
11101110
.engine-list td { vertical-align: top; }
1111+
1112+
/* The Settings page tabs. The same pills as the section bar, with the
1113+
chosen one filled in. */
1114+
.settings-tabs a { font-size: .85rem; padding: .35rem .9rem; }
1115+
.settings-tabs a.active { color: var(--text); border-color: var(--border-strong);
1116+
background: var(--bg-sunk); font-weight: 600; }

‎app/web/static/settings.js‎

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,95 @@
2222
picker.addEventListener("change", apply);
2323
apply();
2424
})();
25+
26+
/* Tabs. The page is one long form, and nobody wants to scroll past sign in
27+
* to reach the reports. Only the cards on the chosen tab show; the rest are
28+
* hidden, not removed, so the form still sends every field when it is saved.
29+
*
30+
* The tab comes from the address (#tab-reports, or a section such as
31+
* #external), then from the last one used, then the first. A field the
32+
* browser refuses on a tab that is not showing would stop the save without
33+
* saying why, so its tab is opened first. */
34+
(function () {
35+
"use strict";
36+
37+
var links = Array.prototype.slice.call(
38+
document.querySelectorAll(".settings-tabs [data-tab]"));
39+
if (!links.length) { return; }
40+
var owner = {};
41+
links.forEach(function (link) {
42+
link.getAttribute("data-sections").split(" ").forEach(function (id) {
43+
owner[id] = link.getAttribute("data-tab");
44+
});
45+
});
46+
47+
function remember(tab) {
48+
try { window.sessionStorage.setItem("gcr-settings-tab", tab); } catch (e) { /* private mode */ }
49+
}
50+
51+
function recalled() {
52+
try { return window.sessionStorage.getItem("gcr-settings-tab"); } catch (e) { return null; }
53+
}
54+
55+
function show(tab) {
56+
links.forEach(function (link) {
57+
var on = link.getAttribute("data-tab") === tab;
58+
link.classList.toggle("active", on);
59+
link.setAttribute("aria-selected", on ? "true" : "false");
60+
});
61+
Object.keys(owner).forEach(function (id) {
62+
var card = document.getElementById(id);
63+
if (card) { card.hidden = owner[id] !== tab; }
64+
});
65+
document.querySelectorAll("[data-on-tab]").forEach(function (node) {
66+
node.hidden = node.getAttribute("data-on-tab") !== tab;
67+
});
68+
remember(tab);
69+
}
70+
71+
function fromHash() {
72+
var hash = (window.location.hash || "").replace(/^#/, "");
73+
if (hash.indexOf("tab-") === 0 && links.some(function (l) {
74+
return l.getAttribute("data-tab") === hash.slice(4); })) {
75+
return hash.slice(4);
76+
}
77+
return owner[hash] || null;
78+
}
79+
80+
links.forEach(function (link) {
81+
link.addEventListener("click", function (event) {
82+
event.preventDefault();
83+
var tab = link.getAttribute("data-tab");
84+
show(tab);
85+
if (window.history && window.history.replaceState) {
86+
window.history.replaceState(null, "", "#tab-" + tab);
87+
}
88+
});
89+
});
90+
91+
window.addEventListener("hashchange", function () {
92+
var tab = fromHash();
93+
if (tab) {
94+
show(tab);
95+
var target = document.getElementById(window.location.hash.slice(1));
96+
if (target && owner[target.id]) { target.scrollIntoView(); }
97+
}
98+
});
99+
100+
var form = document.getElementById("settings-form");
101+
if (form) {
102+
form.addEventListener("invalid", function (event) {
103+
var card = event.target.closest(".card[id]");
104+
if (card && owner[card.id] && card.hidden) { show(owner[card.id]); }
105+
}, true);
106+
}
107+
108+
var start = fromHash();
109+
var last = recalled();
110+
if (!start && last && links.some(function (l) { return l.getAttribute("data-tab") === last; })) {
111+
start = last;
112+
}
113+
show(start || links[0].getAttribute("data-tab"));
114+
var direct = document.getElementById((window.location.hash || "").slice(1));
115+
if (direct && owner[direct.id]) { direct.scrollIntoView(); }
116+
})();

‎app/web/templates/settings.html‎

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
{% extends "base.html" %}
2-
{% block title %}Settings &middot; Git Code Review<script src="{{ base }}/static/settings.js?v={{ asset_v('settings.js') }}"></script>
3-
{% endblock %}
2+
{% block title %}Settings &middot; Git Code Review{% endblock %}
43
{% block content %}
54
<div class="page-head">
65
<div><h1>Settings</h1>
76
<div class="faint small">Everything the scanner needs. It all lives in the
87
database, so nothing here is in a file anybody can clone.</div></div>
98
</div>
109

11-
<nav class="section-nav" aria-label="Sections of this page">
12-
{% for section in help_sections %}
13-
<a href="#{{ section.id }}">{{ section.nav }}</a>
10+
{# One tab per group of sections. Every section stays in the one form, so
11+
saving from any tab keeps what is on the others: a hidden field is still
12+
sent. Without script every section shows, the way the page used to. #}
13+
<nav class="section-nav settings-tabs" role="tablist" aria-label="Settings">
14+
{% for tab in settings_tabs %}
15+
<a href="#tab-{{ tab.id }}" role="tab" data-tab="{{ tab.id }}"
16+
data-sections="{{ tab.sections | map(attribute='id') | join(' ') }}"
17+
title="{{ tab.sections | map(attribute='nav') | join(', ') }}">{{ tab.label }}</a>
1418
{% endfor %}
1519
</nav>
1620

@@ -50,7 +54,7 @@
5054
</div>
5155
</div>
5256

53-
<div class="small faint" style="margin:1.2rem 0 .8rem">
57+
<div class="small faint" style="margin:1.2rem 0 .8rem" data-on-tab="scanning">
5458
Everything below applies to every account: what to look for, where the reports
5559
go, how people sign in. The defaults work as they are. Every branch, full
5660
history, live credential verification on, every detector on, reports in /out.

0 commit comments

Comments
 (0)