Skip to content

Commit 9d16bac

Browse files
author
FolderView Plus Test
committed
Improve docker command view tiles and button chrome
1 parent cf208ef commit 9d16bac

9 files changed

Lines changed: 193 additions & 38 deletions

File tree

archive/folderview.plus-2026.04.14.06.txz.sha256

Lines changed: 0 additions & 1 deletion
This file was deleted.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
21d9133f0ea5e43bb5ad42aac6da06ebcdd8a64684da3f6c7f54c55cc1b906f4 folderview.plus-2026.04.15.18.txz

docs/releases/2026.04.15.18.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
- UX: Docker `Command view (experimental)` now uses the shared FolderView Plus button chrome instead of a one-off button skin, so its action rows match the rest of the plugin surfaces more closely.
2+
- UX: Command-view folder cards now render visible container tiles with app icons, name pills, state badges, and update badges across the row instead of limiting members to a few small text chips.
3+
- Quality: Added regression coverage to keep the isolated command-view module on the shared button variables and the richer member-tile rendering path, and hardened `pkg_build.sh` to use a relative build lock path so dev packaging does not stall on the Windows/WSL lockfile path.

folderview.plus.plg

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,20 @@
66
<!ENTITY launch "Settings/FolderViewPlus">
77
<!ENTITY plugdir "/usr/local/emhttp/plugins/&name;">
88
<!ENTITY pluginURL "https://raw.githubusercontent.com/&github;/dev/folderview.plus.plg">
9-
<!ENTITY version "2026.04.15.16">
10-
<!ENTITY md5 "e5ac4572a2ff30dfdca16df70243ab58">
9+
<!ENTITY version "2026.04.15.18">
10+
<!ENTITY md5 "1c868cfd5fef54417e7f30d8c4d39b45">
1111
]>
1212

1313
<PLUGIN name="&name;" author="&author;" version="&version;" launch="&launch;" pluginURL="&pluginURL;" icon="folder-icon.png" support="https://forums.unraid.net/topic/197631-plugin-folderview-plus/" min="7.0.0">
1414
<CHANGES>
1515

16+
###2026.04.15.18
17+
- Fix: Docker runtime rows, folder state, and container interactions.
18+
- UX: Settings workspace layout, section flows, and table behavior.
19+
- Quality: Release automation, CI smoke coverage, and packaging guards.
20+
- Docs: Project documentation and support guidance.
21+
22+
1623
###2026.04.15.16
1724
- Feature: Added an isolated Docker `Command view (experimental)` mode that loads from its own runtime module and stylesheet, reuses the existing folder/runtime data model, and keeps FolderView and host-list modes on separate paths.
1825
- UX: Docker command view renders a dedicated folder command surface with branch status summaries, member chips, and quick actions for start, stop, update, WebUI, edit, pin, and lock.

pkg_build.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ validate_after_build=true
2020
dry_run=false
2121
run_install_smoke=false
2222
tmpdir=""
23-
lockfile="$CWD/tmp/pkg_build.lock"
23+
lockfile="tmp/pkg_build.lock"
2424
lockdir=""
2525
branch_override="${FVPLUS_BUILD_BRANCH:-}"
2626

@@ -269,7 +269,7 @@ sync_ca_template_metadata() {
269269
}
270270

271271
acquire_build_lock() {
272-
mkdir -p "$CWD/tmp"
272+
mkdir -p "$(dirname "$lockfile")"
273273
if command -v flock >/dev/null 2>&1; then
274274
exec 9>"$lockfile"
275275
if ! flock -n 9; then

src/folderview.plus/usr/local/emhttp/plugins/folderview.plus/scripts/docker.runtime.command-view.js

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
: (typeof window !== 'undefined' ? window : null);
1313
const ROOT_ID = 'fvplus-docker-command-view-root';
1414
const BODY_ATTR = 'data-fvplus-docker-command-view-mounted';
15+
const DOCKER_ICON_FALLBACK = '/plugins/dynamix.docker.manager/images/question.png';
1516

1617
const createApi = (deps = {}) => {
1718
const win = deps.window || fallbackWindow;
@@ -47,6 +48,15 @@
4748
const normalizePrefs = typeof utils.normalizePrefs === 'function'
4849
? utils.normalizePrefs
4950
: ((prefs = {}) => (prefs && typeof prefs === 'object' ? prefs : {}));
51+
const sanitizeImageSrc = typeof utils.sanitizeImageSrc === 'function'
52+
? utils.sanitizeImageSrc
53+
: ((value, fallback = DOCKER_ICON_FALLBACK) => {
54+
const raw = String(value || '').trim();
55+
if (!raw || /^javascript:/i.test(raw)) {
56+
return fallback;
57+
}
58+
return escapeHtml(raw);
59+
});
5060
const normalizeRuntimeInfoMap = typeof deps.normalizeDockerRuntimeInfoMap === 'function'
5161
? deps.normalizeDockerRuntimeInfoMap
5262
: ((value) => (value && typeof value === 'object' ? value : {}));
@@ -197,6 +207,17 @@
197207
|| entry?.info?.State?.Updated === true
198208
);
199209

210+
const getContainerStateMeta = (entry = {}) => {
211+
const state = resolveContainerState(entry);
212+
if (state === 'paused') {
213+
return { state, label: 'paused', icon: 'fa-pause' };
214+
}
215+
if (state === 'running') {
216+
return { state, label: 'running', icon: 'fa-play' };
217+
}
218+
return { state, label: 'stopped', icon: 'fa-stop' };
219+
};
220+
200221
const computeOrderedFolderIds = (folders, prefs, hostOrder, unraidOrder) => {
201222
const folderMap = folders && typeof folders === 'object' ? folders : {};
202223
const baseOrder = reorderFolderSlotsInBaseOrder(unraidOrder, folderMap, prefs);
@@ -240,8 +261,10 @@
240261
const branchContainers = getScopedRuntimeContainersForFolder(folderId, true) || {};
241262
const members = Object.entries(branchContainers).map(([name, entry]) => ({
242263
name,
243-
state: resolveContainerState(entry),
244-
updateReady: containerHasUpdate(entry)
264+
icon: sanitizeImageSrc(entry?.icon || DOCKER_ICON_FALLBACK, DOCKER_ICON_FALLBACK),
265+
stateMeta: getContainerStateMeta(entry),
266+
updateReady: containerHasUpdate(entry),
267+
managed: entry?.managed === true || entry?.manager === 'dockerman'
245268
})).sort((left, right) => left.name.localeCompare(right.name));
246269
const actionCounts = summarizeFolderActionCounts(branchContainers);
247270
const directMatches = snapshot.matchCache[folderId] || {};
@@ -253,9 +276,9 @@
253276
let stopped = 0;
254277
let updates = 0;
255278
members.forEach((member) => {
256-
if (member.state === 'running') {
279+
if (member.stateMeta.state === 'running') {
257280
running += 1;
258-
} else if (member.state === 'paused') {
281+
} else if (member.stateMeta.state === 'paused') {
259282
paused += 1;
260283
} else {
261284
stopped += 1;
@@ -333,13 +356,23 @@
333356
`${card.branchMemberCount} in branch`,
334357
card.childCount > 0 ? `${card.childCount} child folders` : ''
335358
].filter(Boolean).join(' • ');
336-
const memberChips = card.members.slice(0, 6).map((member) => `
337-
<span class="fv-docker-command-member ${escapeHtml(member.state)}">
338-
<span class="fv-docker-command-member-name">${escapeHtml(member.name)}</span>
339-
${member.updateReady ? '<span class="fv-docker-command-member-update">update</span>' : ''}
340-
</span>
359+
const memberTiles = card.members.map((member) => `
360+
<div class="fv-docker-command-member-tile ${escapeHtml(member.stateMeta.state)}${member.updateReady ? ' has-update' : ''}">
361+
<span class="fv-docker-command-member-icon-wrap">
362+
<img src="${member.icon}" class="fv-docker-command-member-icon" alt="" loading="lazy" onerror='this.src="${DOCKER_ICON_FALLBACK}"'>
363+
</span>
364+
<span class="fv-docker-command-member-content">
365+
<span class="fv-docker-command-member-pill">${escapeHtml(member.name)}</span>
366+
<span class="fv-docker-command-member-meta">
367+
<span class="fv-docker-command-member-state ${escapeHtml(member.stateMeta.state)}">
368+
<i class="fa ${escapeHtml(member.stateMeta.icon)}" aria-hidden="true"></i>
369+
<span>${escapeHtml(member.stateMeta.label)}</span>
370+
</span>
371+
${member.updateReady ? '<span class="fv-docker-command-member-update">update ready</span>' : ''}
372+
</span>
373+
</span>
374+
</div>
341375
`).join('');
342-
const hiddenCount = Math.max(0, card.members.length - 6);
343376
return `
344377
<article class="fv-docker-command-card" data-folder-id="${escapeHtml(card.folderId)}" style="--fv-docker-command-depth:${card.depth};">
345378
<div class="fv-docker-command-card-head">
@@ -360,7 +393,7 @@
360393
<span class="fv-docker-command-stat update"><i class="fa fa-cloud-download"></i> ${card.updates} updates</span>
361394
</div>
362395
<div class="fv-docker-command-members">
363-
${memberChips}${hiddenCount > 0 ? `<span class="fv-docker-command-member more">+${hiddenCount} more</span>` : ''}
396+
${memberTiles || '<span class="fv-docker-command-member-empty">No containers matched this folder yet.</span>'}
364397
</div>
365398
<div class="fv-docker-command-actions">
366399
<button type="button" class="fv-docker-command-button" data-fv-command-action="start-branch">Start branch</button>

src/folderview.plus/usr/local/emhttp/plugins/folderview.plus/styles/docker.command-view.css

Lines changed: 124 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,7 @@ body[data-fvplus-docker-command-view-mounted="true"] table#docker_containers {
7777
.fv-docker-command-toolbar,
7878
.fv-docker-command-actions,
7979
.fv-docker-command-card-flags,
80-
.fv-docker-command-stats,
81-
.fv-docker-command-members {
80+
.fv-docker-command-stats {
8281
display: flex;
8382
flex-wrap: wrap;
8483
gap: 0.5rem;
@@ -90,29 +89,35 @@ body[data-fvplus-docker-command-view-mounted="true"] table#docker_containers {
9089

9190
.fv-docker-command-button {
9291
appearance: none;
93-
border: 1px solid rgba(255, 154, 60, 0.56);
94-
border-radius: 10px;
95-
background: linear-gradient(180deg, rgba(255, 154, 60, 0.18), rgba(255, 154, 60, 0.08));
96-
color: #ffb56d;
92+
border: 0 !important;
93+
border-radius: 8px !important;
94+
background: linear-gradient(180deg, var(--fvplus-settings-button-bg-top), var(--fvplus-settings-button-bg-bottom)) !important;
95+
color: var(--fvplus-settings-button-fg) !important;
96+
box-shadow: var(--fvplus-settings-button-shadow) !important;
97+
text-shadow: none !important;
9798
font-size: 0.82rem;
9899
font-weight: 700;
99100
letter-spacing: 0.08em;
100101
text-transform: uppercase;
101102
padding: 0.5rem 0.8rem;
102103
cursor: pointer;
103-
transition: transform 120ms ease, border-color 120ms ease, background 120ms ease;
104+
transition: background 120ms ease, box-shadow 120ms ease, color 120ms ease;
104105
}
105106

106107
.fv-docker-command-button:hover,
107108
.fv-docker-command-button:focus-visible {
108-
transform: translateY(-1px);
109-
border-color: rgba(255, 183, 120, 0.92);
110-
background: linear-gradient(180deg, rgba(255, 154, 60, 0.28), rgba(255, 154, 60, 0.12));
109+
background: linear-gradient(180deg, var(--fvplus-settings-button-hover-top), var(--fvplus-settings-button-hover-bottom)) !important;
110+
box-shadow: var(--fvplus-settings-button-shadow-hover) !important;
111+
}
112+
113+
.fv-docker-command-button:active {
114+
background: linear-gradient(180deg, var(--fvplus-settings-button-active-top), var(--fvplus-settings-button-active-bottom)) !important;
115+
box-shadow: var(--fvplus-settings-button-shadow-active) !important;
111116
}
112117

113-
.fv-docker-command-button.is-primary {
114-
color: #17100a;
115-
background: linear-gradient(180deg, rgba(255, 176, 96, 0.98), rgba(255, 149, 50, 0.92));
118+
.fv-docker-command-button:disabled {
119+
opacity: 0.56;
120+
box-shadow: none !important;
116121
}
117122

118123
.fv-docker-command-overview {
@@ -179,8 +184,7 @@ body[data-fvplus-docker-command-view-mounted="true"] table#docker_containers {
179184
}
180185

181186
.fv-docker-command-flag,
182-
.fv-docker-command-stat,
183-
.fv-docker-command-member {
187+
.fv-docker-command-stat {
184188
display: inline-flex;
185189
align-items: center;
186190
gap: 0.35rem;
@@ -206,17 +210,17 @@ body[data-fvplus-docker-command-view-mounted="true"] table#docker_containers {
206210
}
207211

208212
.fv-docker-command-stat.running,
209-
.fv-docker-command-member.running {
213+
.fv-docker-command-member-state.running {
210214
color: #7ad05a;
211215
}
212216

213217
.fv-docker-command-stat.paused,
214-
.fv-docker-command-member.paused {
218+
.fv-docker-command-member-state.paused {
215219
color: #d7a418;
216220
}
217221

218222
.fv-docker-command-stat.stopped,
219-
.fv-docker-command-member.stopped {
223+
.fv-docker-command-member-state.stopped {
220224
color: #ff6363;
221225
}
222226

@@ -226,20 +230,114 @@ body[data-fvplus-docker-command-view-mounted="true"] table#docker_containers {
226230
margin-top: 0.75rem;
227231
}
228232

229-
.fv-docker-command-member-name {
230-
max-width: 18rem;
233+
.fv-docker-command-members {
234+
display: grid;
235+
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
236+
gap: 0.6rem;
237+
}
238+
239+
.fv-docker-command-member-tile {
240+
display: flex;
241+
align-items: center;
242+
gap: 0.7rem;
243+
min-width: 0;
244+
padding: 0.62rem 0.72rem;
245+
border-radius: 12px;
246+
border: 1px solid rgba(255, 255, 255, 0.08);
247+
background: rgba(255, 255, 255, 0.04);
248+
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.02);
249+
}
250+
251+
.fv-docker-command-member-tile.running {
252+
border-color: rgba(122, 208, 90, 0.26);
253+
}
254+
255+
.fv-docker-command-member-tile.paused {
256+
border-color: rgba(215, 164, 24, 0.24);
257+
}
258+
259+
.fv-docker-command-member-tile.stopped {
260+
border-color: rgba(255, 99, 99, 0.22);
261+
}
262+
263+
.fv-docker-command-member-tile.has-update {
264+
box-shadow: inset 0 0 0 1px rgba(255, 154, 60, 0.18);
265+
}
266+
267+
.fv-docker-command-member-icon-wrap {
268+
flex: 0 0 auto;
269+
width: 36px;
270+
height: 36px;
271+
display: inline-flex;
272+
align-items: center;
273+
justify-content: center;
274+
border-radius: 10px;
275+
background: rgba(255, 255, 255, 0.05);
276+
overflow: hidden;
277+
}
278+
279+
.fv-docker-command-member-icon {
280+
display: block;
281+
width: 28px;
282+
height: 28px;
283+
object-fit: contain;
284+
}
285+
286+
.fv-docker-command-member-content {
287+
min-width: 0;
288+
display: flex;
289+
flex-direction: column;
290+
gap: 0.32rem;
291+
}
292+
293+
.fv-docker-command-member-pill {
294+
display: inline-flex;
295+
align-items: center;
296+
min-width: 0;
297+
max-width: 100%;
298+
width: fit-content;
299+
padding: 0.2rem 0.56rem;
300+
border-radius: 999px;
301+
background: rgba(255, 255, 255, 0.05);
302+
border: 1px solid rgba(255, 255, 255, 0.08);
303+
color: #f3f5f7;
304+
font-size: 0.82rem;
305+
font-weight: 600;
231306
overflow: hidden;
232307
text-overflow: ellipsis;
233308
white-space: nowrap;
234309
}
235310

311+
.fv-docker-command-member-meta {
312+
display: flex;
313+
flex-wrap: wrap;
314+
gap: 0.35rem;
315+
min-width: 0;
316+
}
317+
318+
.fv-docker-command-member-state,
319+
.fv-docker-command-member-update {
320+
display: inline-flex;
321+
align-items: center;
322+
gap: 0.3rem;
323+
padding: 0.14rem 0.44rem;
324+
border-radius: 999px;
325+
background: rgba(255, 255, 255, 0.04);
326+
border: 1px solid rgba(255, 255, 255, 0.06);
327+
font-size: 0.73rem;
328+
}
329+
236330
.fv-docker-command-member-update {
237-
font-size: 0.68rem;
238331
text-transform: uppercase;
239332
letter-spacing: 0.06em;
333+
color: #ff9a3c;
240334
}
241335

242-
.fv-docker-command-member.more {
336+
.fv-docker-command-member-empty {
337+
display: inline-flex;
338+
align-items: center;
339+
min-height: 52px;
340+
padding: 0 0.2rem;
243341
color: rgba(255, 255, 255, 0.66);
244342
}
245343

@@ -276,4 +374,8 @@ body[data-fvplus-docker-command-view-mounted="true"] table#docker_containers {
276374
.fv-docker-command-card-head {
277375
flex-direction: column;
278376
}
377+
378+
.fv-docker-command-members {
379+
grid-template-columns: 1fr;
380+
}
279381
}

tests/docker-runtime-shared-architecture.test.mjs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,9 +231,19 @@ test('docker runtime consumes shared state store and guarded async action wrappe
231231
test('docker command-view stylesheet only hides the host table when the isolated command module is mounted', () => {
232232
assert.match(dockerCommandViewCss, /body\[data-fvplus-docker-command-view-mounted="true"\] table#docker_containers/);
233233
assert.match(dockerCommandViewCss, /\.fv-docker-command-shell/);
234+
assert.match(dockerCommandViewCss, /var\(--fvplus-settings-button-bg-top\)/);
235+
assert.match(dockerCommandViewCss, /\.fv-docker-command-member-tile/);
234236
assert.doesNotMatch(dockerCommandViewCss, /body\[data-fvplus-docker-page-view="command"\] table#docker_containers/);
235237
});
236238

239+
test('docker command-view renders visible member tiles instead of name-only chips', () => {
240+
assert.match(dockerCommandViewJs, /const sanitizeImageSrc = typeof utils\.sanitizeImageSrc === 'function'/);
241+
assert.match(dockerCommandViewJs, /class="fv-docker-command-member-tile/);
242+
assert.match(dockerCommandViewJs, /class="fv-docker-command-member-pill"/);
243+
assert.match(dockerCommandViewJs, /class="fv-docker-command-member-icon"/);
244+
assert.doesNotMatch(dockerCommandViewJs, /class="fv-docker-command-member more"/);
245+
});
246+
237247
test('docker CSS keeps docker-specific layout tokens while shared stylesheet owns shared dropdown geometry', () => {
238248
assert.match(dockerCss, /--fvplus-docker-folder-right-gutter:\s*28px/);
239249
assert.match(dockerCss, /--fvplus-docker-folder-outer-reserved-width:\s*106px/);

0 commit comments

Comments
 (0)