Skip to content

Commit dc70a2c

Browse files
author
FolderView Plus Test
committed
Refine docker command view member actions
1 parent 9d16bac commit dc70a2c

8 files changed

Lines changed: 260 additions & 23 deletions

File tree

14 MB
Binary file not shown.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
6f8d4c5dbafa92fd48be3b6272c7ba5808362c0c64a39fb25eb530af68dfdc55 folderview.plus-2026.04.15.19.txz

docs/releases/2026.04.15.19.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
- UX: Docker command-view member tiles now keep a consistent fixed width by default so folders do not stretch or compress container cards differently based on row count.
2+
- UX: Command-view container tiles now expose visible WebUI, console, and log quick-action icons, and clicking a tile opens a lightweight per-container action menu for start, stop, pause, resume, and restart.
3+
- Fix: The command-view folder-level `Open WebUIs` action now opens directly from the rendered member targets first, which avoids the broken no-op behavior seen in the experimental surface.

folderview.plus.plg

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,19 @@
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.18">
10-
<!ENTITY md5 "1c868cfd5fef54417e7f30d8c4d39b45">
9+
<!ENTITY version "2026.04.15.19">
10+
<!ENTITY md5 "914c1a027767564a695bd0b5508376cd">
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.19
17+
- UX: Docker command-view member tiles now keep a consistent fixed width by default so folders do not stretch or compress container cards differently based on row count.
18+
- UX: Command-view container tiles now expose visible WebUI, console, and log quick-action icons, and clicking a tile opens a lightweight per-container action menu for start, stop, pause, resume, and restart.
19+
- Fix: The command-view folder-level `Open WebUIs` action now opens directly from the rendered member targets first, which avoids the broken no-op behavior seen in the experimental surface.
20+
21+
1622
###2026.04.15.18
1723
- Fix: Docker runtime rows, folder state, and container interactions.
1824
- UX: Settings workspace layout, section flows, and table behavior.

src/folderview.plus/usr/local/emhttp/plugins/folderview.plus/scripts/docker.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -721,8 +721,12 @@ const getDockerCommandViewApi = () => {
721721
actionFolder: (id, action, options = {}) => actionFolder(id, action, options),
722722
updateFolder: (id, options = {}) => updateFolder(id, options),
723723
forceUpdateFolder: (id, options = {}) => forceUpdateFolder(id, options),
724+
getSafeWebuiUrl: (value) => getSafeWebuiUrl(value),
724725
openFolderWebuisFromMenu: (id, runningOnly = true, includeDescendants = false) =>
725726
openFolderWebuisFromMenu(id, runningOnly, includeDescendants),
727+
openWebuiInNewTab: (url) => openWebuiInNewTab(url),
728+
openWebuiPopupWindow: (url, targetName = '_blank') => openWebuiPopupWindow(url, targetName),
729+
openTerminal: (type, containerName, shellValue) => openTerminal(type, containerName, shellValue),
726730
toggleFolderPin: (folderId) => toggleDockerFolderPin(folderId),
727731
toggleFolderLock: (folderId) => toggleDockerFolderLock(folderId),
728732
queueLoadlistRefresh: (options = {}) => queueLoadlistRefresh(options),

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

Lines changed: 143 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@
6060
const normalizeRuntimeInfoMap = typeof deps.normalizeDockerRuntimeInfoMap === 'function'
6161
? deps.normalizeDockerRuntimeInfoMap
6262
: ((value) => (value && typeof value === 'object' ? value : {}));
63+
const getSafeWebuiUrl = typeof deps.getSafeWebuiUrl === 'function'
64+
? deps.getSafeWebuiUrl
65+
: ((value) => {
66+
const raw = String(value || '').trim();
67+
return raw && !/^javascript:/i.test(raw) ? raw : '';
68+
});
6369
const getPrefsOrderedFolderMap = typeof deps.getPrefsOrderedFolderMap === 'function'
6470
? deps.getPrefsOrderedFolderMap
6571
: ((folders, prefs) => (
@@ -118,6 +124,9 @@
118124
const openFolderWebuisFromMenu = typeof deps.openFolderWebuisFromMenu === 'function'
119125
? deps.openFolderWebuisFromMenu
120126
: (() => {});
127+
const openWebuiInNewTab = typeof deps.openWebuiInNewTab === 'function' ? deps.openWebuiInNewTab : (() => false);
128+
const openWebuiPopupWindow = typeof deps.openWebuiPopupWindow === 'function' ? deps.openWebuiPopupWindow : (() => false);
129+
const openTerminal = typeof deps.openTerminal === 'function' ? deps.openTerminal : (() => {});
121130
const toggleFolderPin = typeof deps.toggleFolderPin === 'function' ? deps.toggleFolderPin : (() => Promise.resolve());
122131
const toggleFolderLock = typeof deps.toggleFolderLock === 'function' ? deps.toggleFolderLock : (() => {});
123132
const queueLoadlistRefresh = typeof deps.queueLoadlistRefresh === 'function'
@@ -127,6 +136,7 @@
127136
let rootNode = null;
128137
let clickHandler = null;
129138
let renderToken = 0;
139+
let expandedMemberKey = '';
130140

131141
const getHostTable = () => doc?.querySelector('table#docker_containers') || null;
132142
const isFolderToken = (value) => String(value || '').trim().startsWith('folder-');
@@ -218,6 +228,62 @@
218228
return { state, label: 'stopped', icon: 'fa-stop' };
219229
};
220230

231+
const getMemberKey = (folderId, memberName) => `${String(folderId || '').trim()}::${String(memberName || '').trim()}`;
232+
233+
const dispatchContainerControl = (action, containerId) => {
234+
const safeAction = String(action || '').trim();
235+
const safeContainerId = String(containerId || '').trim();
236+
if (!safeAction || !safeContainerId || typeof win?.eventControl !== 'function') {
237+
return false;
238+
}
239+
win.eventControl({ action: safeAction, container: safeContainerId }, 'loadlist');
240+
return true;
241+
};
242+
243+
const openFolderCardWebuis = (folderCard) => {
244+
if (!(folderCard instanceof HTMLElement)) {
245+
return false;
246+
}
247+
const urls = Array.from(new Set(
248+
Array.from(folderCard.querySelectorAll('[data-member-webui-url]'))
249+
.map((node) => getSafeWebuiUrl(node.getAttribute('data-member-webui-url') || ''))
250+
.filter(Boolean)
251+
));
252+
if (!urls.length) {
253+
return false;
254+
}
255+
const stamp = Date.now();
256+
urls.forEach((url, index) => {
257+
if (index === 0) {
258+
openWebuiInNewTab(url);
259+
return;
260+
}
261+
openWebuiPopupWindow(url, `fvw-${stamp}-${index}`);
262+
});
263+
return true;
264+
};
265+
266+
const buildMemberMenuButtons = (member) => {
267+
const actions = [];
268+
if (member.stateMeta.state === 'stopped') {
269+
actions.push({ action: 'start', label: 'Start' });
270+
} else if (member.stateMeta.state === 'paused') {
271+
actions.push({ action: 'resume', label: 'Resume' });
272+
actions.push({ action: 'stop', label: 'Stop' });
273+
} else {
274+
actions.push({ action: 'stop', label: 'Stop' });
275+
actions.push({ action: 'pause', label: 'Pause' });
276+
}
277+
if (member.id) {
278+
actions.push({ action: 'restart', label: 'Restart' });
279+
}
280+
return actions.map((entry) => `
281+
<button type="button" class="fv-docker-command-member-menu-button" data-fv-command-member-action="${escapeHtml(entry.action)}" data-member-name="${escapeHtml(member.name)}">
282+
${escapeHtml(entry.label)}
283+
</button>
284+
`).join('');
285+
};
286+
221287
const computeOrderedFolderIds = (folders, prefs, hostOrder, unraidOrder) => {
222288
const folderMap = folders && typeof folders === 'object' ? folders : {};
223289
const baseOrder = reorderFolderSlotsInBaseOrder(unraidOrder, folderMap, prefs);
@@ -261,7 +327,10 @@
261327
const branchContainers = getScopedRuntimeContainersForFolder(folderId, true) || {};
262328
const members = Object.entries(branchContainers).map(([name, entry]) => ({
263329
name,
330+
id: String(entry?.id || entry?.shortId || '').trim(),
264331
icon: sanitizeImageSrc(entry?.icon || DOCKER_ICON_FALLBACK, DOCKER_ICON_FALLBACK),
332+
webuiUrl: getSafeWebuiUrl(entry?.webui || entry?.info?.State?.WebUi || entry?.info?.State?.TSWebUi || ''),
333+
shell: String(entry?.shell || entry?.info?.Shell || '/bin/sh').trim() || '/bin/sh',
265334
stateMeta: getContainerStateMeta(entry),
266335
updateReady: containerHasUpdate(entry),
267336
managed: entry?.managed === true || entry?.manager === 'dockerman'
@@ -357,20 +426,30 @@
357426
card.childCount > 0 ? `${card.childCount} child folders` : ''
358427
].filter(Boolean).join(' • ');
359428
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>
429+
<div class="fv-docker-command-member-tile ${escapeHtml(member.stateMeta.state)}${member.updateReady ? ' has-update' : ''}${expandedMemberKey === getMemberKey(card.folderId, member.name) ? ' is-expanded' : ''}" data-member-name="${escapeHtml(member.name)}" data-member-id="${escapeHtml(member.id)}" data-member-webui-url="${escapeHtml(member.webuiUrl)}" data-member-shell="${escapeHtml(member.shell)}">
430+
<button type="button" class="fv-docker-command-member-surface" data-fv-command-member-action="toggle-menu" data-member-name="${escapeHtml(member.name)}">
431+
<span class="fv-docker-command-member-icon-wrap">
432+
<img src="${member.icon}" class="fv-docker-command-member-icon" alt="" loading="lazy" onerror='this.src="${DOCKER_ICON_FALLBACK}"'>
433+
</span>
434+
<span class="fv-docker-command-member-content">
435+
<span class="fv-docker-command-member-pill">${escapeHtml(member.name)}</span>
436+
<span class="fv-docker-command-member-meta">
437+
<span class="fv-docker-command-member-state ${escapeHtml(member.stateMeta.state)}">
438+
<i class="fa ${escapeHtml(member.stateMeta.icon)}" aria-hidden="true"></i>
439+
<span>${escapeHtml(member.stateMeta.label)}</span>
440+
</span>
441+
${member.updateReady ? '<span class="fv-docker-command-member-update">update ready</span>' : ''}
370442
</span>
371-
${member.updateReady ? '<span class="fv-docker-command-member-update">update ready</span>' : ''}
372443
</span>
373-
</span>
444+
<span class="fv-docker-command-member-quick-actions">
445+
${member.webuiUrl ? `<button type="button" class="fv-docker-command-member-icon-button" title="Open WebUI" aria-label="Open WebUI" data-fv-command-member-action="webui" data-member-name="${escapeHtml(member.name)}"><i class="fa fa-globe" aria-hidden="true"></i></button>` : ''}
446+
<button type="button" class="fv-docker-command-member-icon-button" title="Open console" aria-label="Open console" data-fv-command-member-action="console" data-member-name="${escapeHtml(member.name)}"><i class="fa fa-terminal" aria-hidden="true"></i></button>
447+
<button type="button" class="fv-docker-command-member-icon-button" title="Open logs" aria-label="Open logs" data-fv-command-member-action="logs" data-member-name="${escapeHtml(member.name)}"><i class="fa fa-bars" aria-hidden="true"></i></button>
448+
</span>
449+
</button>
450+
<div class="fv-docker-command-member-menu">
451+
${buildMemberMenuButtons(member)}
452+
</div>
374453
</div>
375454
`).join('');
376455
return `
@@ -427,6 +506,55 @@
427506
rootNode.removeEventListener('click', clickHandler);
428507
}
429508
clickHandler = (event) => {
509+
const memberButton = event.target instanceof Element
510+
? event.target.closest('[data-fv-command-member-action]')
511+
: null;
512+
if (memberButton instanceof HTMLElement) {
513+
event.preventDefault();
514+
event.stopPropagation();
515+
const memberAction = String(memberButton.getAttribute('data-fv-command-member-action') || '').trim();
516+
const memberTile = memberButton.closest('.fv-docker-command-member-tile');
517+
const folderCard = memberButton.closest('[data-folder-id]');
518+
const folderId = folderCard instanceof HTMLElement ? String(folderCard.getAttribute('data-folder-id') || '').trim() : '';
519+
const memberName = memberTile instanceof HTMLElement ? String(memberTile.getAttribute('data-member-name') || '').trim() : '';
520+
const memberId = memberTile instanceof HTMLElement ? String(memberTile.getAttribute('data-member-id') || '').trim() : '';
521+
const memberWebuiUrl = memberTile instanceof HTMLElement ? String(memberTile.getAttribute('data-member-webui-url') || '').trim() : '';
522+
const memberShell = memberTile instanceof HTMLElement ? String(memberTile.getAttribute('data-member-shell') || '').trim() || '/bin/sh' : '/bin/sh';
523+
if (!folderId || !memberName) {
524+
return;
525+
}
526+
const memberKey = getMemberKey(folderId, memberName);
527+
if (memberAction === 'toggle-menu') {
528+
expandedMemberKey = expandedMemberKey === memberKey ? '' : memberKey;
529+
rootNode.querySelectorAll('.fv-docker-command-member-tile.is-expanded').forEach((node) => {
530+
if (node !== memberTile) {
531+
node.classList.remove('is-expanded');
532+
}
533+
});
534+
if (memberTile instanceof HTMLElement) {
535+
memberTile.classList.toggle('is-expanded', expandedMemberKey === memberKey);
536+
}
537+
return;
538+
}
539+
if (memberAction === 'webui') {
540+
openWebuiInNewTab(memberWebuiUrl);
541+
return;
542+
}
543+
if (memberAction === 'console') {
544+
openTerminal('docker', memberName, memberShell);
545+
return;
546+
}
547+
if (memberAction === 'logs') {
548+
openTerminal('docker', memberName, '.log');
549+
return;
550+
}
551+
if (memberAction === 'start' || memberAction === 'stop' || memberAction === 'pause' || memberAction === 'resume' || memberAction === 'restart') {
552+
if (dispatchContainerControl(memberAction, memberId)) {
553+
queueLoadlistRefresh({ suppressLoadingUi: true });
554+
}
555+
return;
556+
}
557+
}
430558
const button = event.target instanceof Element
431559
? event.target.closest('[data-fv-command-action]')
432560
: null;
@@ -466,7 +594,9 @@
466594
return;
467595
}
468596
if (action === 'open-webui') {
469-
openFolderWebuisFromMenu(folderId, true, true);
597+
if (!openFolderCardWebuis(folderCard)) {
598+
openFolderWebuisFromMenu(folderId, true, true);
599+
}
470600
return;
471601
}
472602
if (action === 'edit-folder') {

0 commit comments

Comments
 (0)