Skip to content

Commit 80f2c3f

Browse files
committed
Add collapsible Import section
Move the Import UI to the top of the left panel and add a collapsible Import section with help, bulk-import controls, session tools, and export output. Add CSS for section layout, collapse behavior, and styling (.section-title-row, .section-actions, .import-section-top, .collapse-btn). Update app.js to manage importSectionCollapsed state, render/toggle the collapsed view, and persist the setting in saved sessions (loadState/buildStatePayload); ensure loadExample resets the section to expanded. Update ARCHITECTURE.md to document the top-of-panel Import section, subnet mapping export script, collapse behavior, and add a Phase M: Testing note.
1 parent df614dd commit 80f2c3f

4 files changed

Lines changed: 127 additions & 45 deletions

File tree

ARCHITECTURE.md

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ openwrt-firewall-visualiser/
3030
├── README.md # Placeholder project documentation
3131
├── ARCHITECTURE.md # This architecture guide
3232
├── scripts/
33-
│ └── openwrt_export_hosts.sh
33+
│ ├── openwrt_export_hosts.sh
34+
│ └── openwrt_export_subnet_mappings.sh
3435
└── public/
3536
├── index.html # Application markup and external asset references
3637
└── assets/
@@ -484,7 +485,7 @@ Objective: allow users to populate devices from existing network inventories ins
484485

485486
Requirements:
486487

487-
- Add a clearly labelled "Import" section with a "Bulk Import Hosts" subsection.
488+
- Add a clearly labelled, top-of-panel "Import" section with a "Bulk Import Hosts" subsection.
488489
- Support device inventory exports.
489490
- Support host lists.
490491
- Support DHCP exports.
@@ -518,6 +519,7 @@ Supported Linux neighbour table format:
518519
Implemented details:
519520

520521
- Import controls are grouped separately from manual Devices and Subnet Mappings controls.
522+
- The Import section can be collapsed when the user is done with first-run setup or file imports.
521523
- `renderImportChecklist()` shows first-time users which setup/import steps are currently populated.
522524
- `importBulkHosts()` imports the textarea content.
523525
- `parseBulkHosts()` parses host data line-by-line.
@@ -569,6 +571,7 @@ Implemented details:
569571

570572
- The `subnetMappings` textarea stores subnet-to-zone mappings.
571573
- The Subnet Mappings help box explains when the mappings are used, the manual `CIDR zone` format, and the exact UCI output expected by the UCI import field.
574+
- The Subnet Mappings help box links to `scripts/openwrt_export_subnet_mappings.sh`, which exports the matching OpenWrt UCI lines from a router.
572575
- `parseSubnetMappings()` parses CIDR mappings.
573576
- `inferZoneForIp()` assigns zones during import when a host line has no explicit zone.
574577
- Unresolved imported hosts are counted in the import result panel.
@@ -652,6 +655,7 @@ Implemented improvements:
652655
- Export Session uses a distinct positive-action button.
653656
- Import Session / Devices is a dedicated file-import control.
654657
- Import status is persisted with saved sessions so the checklist reflects restored state.
658+
- Import section collapsed/expanded state is persisted with saved sessions.
655659

656660
### Phase L: Usability
657661

@@ -660,12 +664,19 @@ Status: implemented.
660664
Implementation improvements:
661665

662666
- Loading the example now prompts before replacing the current state.
663-
- Devices, Subnet Mappings, and Import are separate left-panel sections.
667+
- Import, Devices, and Subnet Mappings are separate left-panel sections.
668+
- Import is positioned before manual configuration entry so first-time users can start from existing exports.
664669
- Subnet Mappings has its own help button, with less ambiguous guidance about when subnet mappings are used.
665670
- Graph Visualiser has an expand/collapse button.
666671
- Export Graph PNG moved into the Graph Visualiser toolbar.
667672
- Device Relationship Map shows the first 15 relationships by default and adds a show-all/show-fewer control when needed.
668673

674+
## Phase M: Testing
675+
676+
Phase M provides unit testing for CI/CD
677+
678+
679+
669680
## Development Notes
670681

671682
The current implementation is intentionally simple:

public/assets/css/styles.css

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,13 +138,35 @@ input {
138138
margin-bottom: 0.5rem;
139139
}
140140

141+
.section-title-row {
142+
display: flex;
143+
align-items: center;
144+
justify-content: space-between;
145+
gap: 0.75rem;
146+
margin-bottom: 0.5rem;
147+
}
148+
149+
.section-title-row h2 {
150+
margin: 0;
151+
}
152+
153+
.section-actions {
154+
display: flex;
155+
align-items: center;
156+
gap: 0.5rem;
157+
}
158+
141159
.help-btn {
142160
width: 1.8rem;
143161
height: 1.8rem;
144162
padding: 0;
145163
border-radius: 999px;
146164
}
147165

166+
.collapse-btn {
167+
min-width: 5.25rem;
168+
}
169+
148170
.help-box {
149171
margin: 0.75rem 0;
150172
padding: 0.75rem;
@@ -329,6 +351,19 @@ pre {
329351
border-top-color: rgba(56, 189, 248, 0.55);
330352
}
331353

354+
.import-section-top {
355+
margin-top: 0;
356+
margin-bottom: 1rem;
357+
padding-top: 0;
358+
padding-bottom: 1rem;
359+
border-top: 0;
360+
border-bottom: 1px solid rgba(56, 189, 248, 0.55);
361+
}
362+
363+
.import-section.collapsed .collapsible-body {
364+
display: none;
365+
}
366+
332367
.checklist {
333368
display: grid;
334369
gap: 0.45rem;

public/assets/js/app.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ let relationshipMapExpanded = false;
100100
let hostImportHasRun = false;
101101
let subnetImportHasRun = false;
102102
let sessionImportHasRun = false;
103+
let importSectionCollapsed = false;
103104
let storageAvailable = true;
104105

105106
const autoParse = debounce(() => {
@@ -110,6 +111,7 @@ function main() {
110111
setCurrentYear();
111112
loadState();
112113
bindReactiveControls();
114+
renderImportSectionCollapsed();
113115
renderDeviceInputs();
114116
renderSubnetMappings();
115117
renderPathCriteria();
@@ -128,6 +130,26 @@ function toggleImportHelp() {
128130
document.getElementById("importHelpBox").classList.toggle("hidden");
129131
}
130132

133+
function toggleImportSectionCollapsed() {
134+
importSectionCollapsed = !importSectionCollapsed;
135+
renderImportSectionCollapsed();
136+
saveState();
137+
}
138+
139+
function renderImportSectionCollapsed() {
140+
const section = document.getElementById("importSection");
141+
const button = document.getElementById("importCollapseButton");
142+
143+
if (section) {
144+
section.classList.toggle("collapsed", importSectionCollapsed);
145+
}
146+
147+
if (button) {
148+
button.textContent = importSectionCollapsed ? "Expand" : "Collapse";
149+
button.setAttribute("aria-expanded", String(!importSectionCollapsed));
150+
}
151+
}
152+
131153
function loadExample() {
132154
if (!confirm("Load the example config and reset devices? Current unsaved page state will be replaced.")) {
133155
return;
@@ -142,6 +164,8 @@ function loadExample() {
142164
hostImportHasRun = false;
143165
subnetImportHasRun = false;
144166
sessionImportHasRun = false;
167+
importSectionCollapsed = false;
168+
renderImportSectionCollapsed();
145169
renderDeviceInputs();
146170
renderSubnetMappings();
147171
renderPathCriteria();
@@ -400,6 +424,7 @@ function loadState() {
400424
hostImportHasRun = Boolean(savedState.importState?.hostImportHasRun);
401425
subnetImportHasRun = Boolean(savedState.importState?.subnetImportHasRun);
402426
sessionImportHasRun = Boolean(savedState.importState?.sessionImportHasRun);
427+
importSectionCollapsed = Boolean(savedState.uiState?.importSectionCollapsed);
403428
graphPathHighlightRequested = false;
404429

405430
if (graphFilter) {
@@ -898,6 +923,9 @@ function buildStatePayload() {
898923
hostImportHasRun,
899924
subnetImportHasRun,
900925
sessionImportHasRun
926+
},
927+
uiState: {
928+
importSectionCollapsed
901929
}
902930
};
903931
}

public/index.html

Lines changed: 50 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,51 @@ <h1>🔥 OpenWrt Firewall Relationship Visualiser</h1>
1818

1919
<main>
2020
<section class="panel">
21+
<section id="importSection" class="tool-section import-section import-section-top">
22+
<div class="section-title-row">
23+
<h2>📥 Import</h2>
24+
<div class="section-actions">
25+
<button class="help-btn" type="button" onclick="toggleImportHelp()">?</button>
26+
<button id="importCollapseButton" class="collapse-btn" type="button" aria-expanded="true" onclick="toggleImportSectionCollapsed()">Collapse</button>
27+
</div>
28+
</div>
29+
<div id="importHelpBox" class="help-box hidden">
30+
<p><strong>Supported inputs:</strong> host lists, <code>ip neighbour</code>, <code>arp -a</code>, <code>cat /tmp/dhcp.leases</code>, and CSV from <a href="https://github.com/safesploitOrg/openwrt-firewall-visualiser/blob/main/scripts/openwrt_export_hosts.sh" target="_blank" rel="noopener noreferrer">scripts/openwrt_export_hosts.sh</a>.</p>
31+
<pre>scp scripts/openwrt_export_hosts.sh root@172.16.0.1:/tmp/
32+
ssh root@172.16.0.1 sh /tmp/openwrt_export_hosts.sh</pre>
33+
<p class="small">The script output format is <code>ip,hostname,zone,mac</code>.</p>
34+
</div>
35+
<div id="importSectionBody" class="collapsible-body">
36+
<div id="importChecklist" class="checklist"></div>
37+
<h3>Bulk Import Hosts</h3>
38+
<select id="bulkImportFormat">
39+
<option value="auto">Auto detect</option>
40+
<option value="host-list">Host list</option>
41+
<option value="neighbour">Neighbour / ARP table</option>
42+
<option value="dhcp">OpenWrt DHCP leases</option>
43+
<option value="openwrt-export">OpenWrt export CSV</option>
44+
</select>
45+
<textarea id="bulkHostsInput" class="compact-textarea" spellcheck="false" placeholder="Host list: 172.16.20.10 Alexa-Kitchen iot&#10;Neighbour: 172.16.20.10 dev br-iot lladdr aa:bb:cc:dd:ee:ff REACHABLE&#10;DHCP: 1710000000 aa:bb:cc:dd:ee:ff 172.16.20.10 Alexa-Kitchen *&#10;Export CSV: ip,hostname,zone,mac"></textarea>
46+
<div class="controls">
47+
<button onclick="importBulkHosts()">Import Hosts</button>
48+
<button onclick="clearBulkImport()">Clear Import</button>
49+
</div>
50+
<div id="bulkImportResult" class="result small import-result">No hosts imported yet.</div>
51+
52+
<h3>Session Tools</h3>
53+
<div class="controls">
54+
<button onclick="exportDevicesJson()">Export Devices JSON</button>
55+
<button onclick="exportDevicesCsv()">Export Devices CSV</button>
56+
<button class="button-warn" onclick="exportSessionJson()">Export Full Session JSON</button>
57+
<label class="file-label">
58+
Import Session / Devices
59+
<input type="file" accept=".json,.csv,.txt" onchange="importDataFile(event)" />
60+
</label>
61+
</div>
62+
<textarea id="exportOutput" class="compact-textarea export-output" spellcheck="false" readonly></textarea>
63+
</div>
64+
</section>
65+
2166
<div class="help-row">
2267
<h2>📥 Firewall Config</h2>
2368
<button class="help-btn" type="button" onclick="toggleHelp()">?</button>
@@ -54,7 +99,7 @@ <h2>📥 Firewall Config</h2>
5499

55100
<h2>📡 Devices</h2>
56101
<p class="small">
57-
Map real devices to firewall zones, or use the import section below to populate them in bulk.
102+
Map real devices to firewall zones, or use the import section above to populate them in bulk.
58103
</p>
59104

60105
<div class="controls">
@@ -79,6 +124,10 @@ <h2>🧭 Subnet Mappings</h2>
79124
<p>For OpenWrt UCI import, paste the output of these commands into the UCI box:</p>
80125
<pre>uci show firewall | grep -E "\.(name|network)="
81126
uci show network | grep -E "\.(ipaddr|netmask)="</pre>
127+
<p>Helper script:</p>
128+
<pre>scp scripts/openwrt_export_subnet_mappings.sh root@172.16.0.1:/tmp/
129+
ssh root@172.16.0.1 sh /tmp/openwrt_export_subnet_mappings.sh</pre>
130+
<p class="small">Script source: <a href="https://github.com/safesploitOrg/openwrt-firewall-visualiser/blob/main/scripts/openwrt_export_subnet_mappings.sh" target="_blank" rel="noopener noreferrer">scripts/openwrt_export_subnet_mappings.sh</a>.</p>
82131
<p class="small">This is separate from the firewall config parser. The main firewall box still expects <code>/etc/config/firewall</code>.</p>
83132
</div>
84133
<textarea id="subnetMappings" class="compact-textarea" spellcheck="false" onchange="handleSubnetMappingsChange()"></textarea>
@@ -90,47 +139,6 @@ <h2>🧭 Subnet Mappings</h2>
90139
<div id="uciSubnetResult" class="result small import-result">No UCI subnet import run yet.</div>
91140
</section>
92141

93-
<!-- Import Section -->
94-
<!-- Maybe move this to the top with a collapse button -->
95-
<section class="tool-section import-section">
96-
<div class="help-row">
97-
<h2>📥 Import</h2>
98-
<button class="help-btn" type="button" onclick="toggleImportHelp()">?</button>
99-
</div>
100-
<div id="importHelpBox" class="help-box hidden">
101-
<p><strong>Supported inputs:</strong> host lists, <code>ip neighbour</code>, <code>arp -a</code>, <code>cat /tmp/dhcp.leases</code>, and CSV from <a href="https://github.com/safesploitOrg/openwrt-firewall-visualiser/blob/main/scripts/openwrt_export_hosts.sh" target="_blank" rel="noopener noreferrer">scripts/openwrt_export_hosts.sh</a>.</p>
102-
<pre>scp scripts/openwrt_export_hosts.sh root@172.16.0.1:/tmp/
103-
ssh root@172.16.0.1 sh /tmp/openwrt_export_hosts.sh</pre>
104-
<p class="small">The script output format is <code>ip,hostname,zone,mac</code>.</p>
105-
</div>
106-
<div id="importChecklist" class="checklist"></div>
107-
<h3>Bulk Import Hosts</h3>
108-
<select id="bulkImportFormat">
109-
<option value="auto">Auto detect</option>
110-
<option value="host-list">Host list</option>
111-
<option value="neighbour">Neighbour / ARP table</option>
112-
<option value="dhcp">OpenWrt DHCP leases</option>
113-
<option value="openwrt-export">OpenWrt export CSV</option>
114-
</select>
115-
<textarea id="bulkHostsInput" class="compact-textarea" spellcheck="false" placeholder="Host list: 172.16.20.10 Alexa-Kitchen iot&#10;Neighbour: 172.16.20.10 dev br-iot lladdr aa:bb:cc:dd:ee:ff REACHABLE&#10;DHCP: 1710000000 aa:bb:cc:dd:ee:ff 172.16.20.10 Alexa-Kitchen *&#10;Export CSV: ip,hostname,zone,mac"></textarea>
116-
<div class="controls">
117-
<button onclick="importBulkHosts()">Import Hosts</button>
118-
<button onclick="clearBulkImport()">Clear Import</button>
119-
</div>
120-
<div id="bulkImportResult" class="result small import-result">No hosts imported yet.</div>
121-
122-
<h3>Session Tools</h3>
123-
<div class="controls">
124-
<button onclick="exportDevicesJson()">Export Devices JSON</button>
125-
<button onclick="exportDevicesCsv()">Export Devices CSV</button>
126-
<button class="button-warn" onclick="exportSessionJson()">Export Full Session JSON</button>
127-
<label class="file-label">
128-
Import Session / Devices
129-
<input type="file" accept=".json,.csv,.txt" onchange="importDataFile(event)" />
130-
</label>
131-
</div>
132-
<textarea id="exportOutput" class="compact-textarea export-output" spellcheck="false" readonly></textarea>
133-
</section>
134142
</section>
135143

136144
<section class="panel">

0 commit comments

Comments
 (0)