Loading www/app.css +24 −0 Changes for www/app.css: 24 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -130,6 +130,30 @@ tr:last-child td { border-bottom: 0; } tr:hover td { background: #f1f3f5; } td.actions { white-space: nowrap; text-align: right; } td.actions button { margin-left: 4px; } /* action menu: a button that opens a list of actions (fixed, so a scrolling table doesn't clip it; placed by the page's script) */ .menu { display: inline-block; } .menu > summary { list-style: none; cursor: pointer; white-space: nowrap; user-select: none; color: var(--text); background: var(--surface); border: 1px solid var(--line-strong); border-radius: var(--radius); padding: .2em .6em; font-size: .9em; } .menu > summary::-webkit-details-marker { display: none; } .menu > summary:hover, .menu[open] > summary { background: #eceef1; border-color: #a9aeb6; } .menu-items { position: fixed; z-index: 50; min-width: 15em; max-width: calc(100vw - 32px); padding: .3em 0; background: var(--surface); border: 1px solid var(--line-strong); border-radius: var(--radius); box-shadow: var(--shadow); text-align: left; white-space: normal; } .menu-items .menu-group { margin: .45em .9em .15em; font-size: .78em; color: var(--muted); text-transform: uppercase; letter-spacing: .03em; } .menu-items hr { border: 0; border-top: 1px solid var(--line); margin: .3em 0; } td.actions .menu-items button, .menu-items button { display: block; width: 100%; margin: 0; text-align: left; border: 0; border-radius: 0; background: transparent; padding: .35em .9em; } .menu-items button:hover:not(:disabled) { background: #eceef1; } .menu-items button.danger { color: var(--danger); } .mono { font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; font-size: .92em; } .muted { color: var(--muted); } .empty { color: var(--muted); text-align: center; padding: 1.5em; } Loading www/modules/raid.js +65 −21 Changes for www/modules/raid.js: 65 added lines, 21 removed lines. Original line number Diff line number Diff line Loading @@ -93,6 +93,7 @@ 'Was nach ihrem Ausfall geschrieben wurde, fehlt auf ihnen: danach das Dateisystem prüfen und eine Reparatur laufen lassen.', 'I know that data may be inconsistent.': 'Mir ist klar, dass Daten inkonsistent sein können.', 'Please confirm.': 'Bitte bestätigen.', 'Actions': 'Aktionen', 'Array': 'Array', '{0} is degraded: add a device first, it takes the missing place.': '{0} ist beeinträchtigt: zuerst ein Gerät hinzufügen, es übernimmt den fehlenden Platz.', }}); Loading Loading @@ -449,30 +450,72 @@ // ---- page ---- // one button that opens the actions, in groups ({title, items: [{label, // onclick, disabled?, title?, danger?}]}); empty groups are left out function menu(groups) { groups = groups.filter(g => g.items.length); const list = h('div', {class: 'menu-items', role: 'menu'}); const box = h('details', {class: 'menu'}, h('summary', {}, t('Actions') + ' ▾'), list); groups.forEach((g, i) => { if (i) list.append(h('hr')); if (g.title) list.append(h('div', {class: 'menu-group'}, g.title)); for (const it of g.items) list.append(h('button', {class: it.danger ? 'danger' : null, role: 'menuitem', disabled: !!it.disabled, title: it.title || null, onclick: () => { box.open = false; it.onclick(); }}, it.label)); }); const close = e => { if (!box.contains(e.target)) box.open = false; }; const place = () => { const r = box.firstChild.getBoundingClientRect(); list.style.right = Math.max(16, window.innerWidth - r.right) + 'px'; list.style.top = r.bottom + 4 + 'px'; list.style.bottom = 'auto'; // no room below: open upwards if (r.bottom + 4 + list.offsetHeight > window.innerHeight && r.top > list.offsetHeight + 4) { list.style.top = 'auto'; list.style.bottom = window.innerHeight - r.top + 4 + 'px'; } }; box.addEventListener('toggle', () => { if (box.open) { document.querySelectorAll('details.menu[open]').forEach(d => { if (d !== box) d.open = false; }); place(); document.addEventListener('click', close, true); window.addEventListener('scroll', place, true); window.addEventListener('resize', place); } else { document.removeEventListener('click', close, true); window.removeEventListener('scroll', place, true); window.removeEventListener('resize', place); } }); return box; } function arrayActions(a) { const running = a.state !== 'inactive' && a.level; const out = []; if (running && a.level !== 'raid0' && a.level !== 'linear') out.push(h('button', {class: 'small', disabled: !data.devices.length, title: data.devices.length ? null : t('No free device.'), onclick: () => addDialog(a)}, t('Add device'))); if (running && a.members.length) out.push(h('button', {class: 'small', onclick: () => removeDialog(a)}, t('Remove device'))); if (running && readdCandidates(a).length) out.push(h('button', {class: 'small', onclick: () => readdDialog(a)}, t('Re-add device'))); if (running && a.level !== 'raid0' && a.level !== 'linear') { const redundant = running && a.level !== 'raid0' && a.level !== 'linear'; const idle = a.syncAction === 'idle'; const devices = [], scrub = [], array = []; if (redundant) devices.push({label: t('Add device'), disabled: !data.devices.length, title: data.devices.length ? null : t('No free device.'), onclick: () => addDialog(a)}); if (running && readdCandidates(a).length) devices.push({label: t('Re-add device'), onclick: () => readdDialog(a)}); if (redundant) { const spare = a.members.some(m => m.slot < 0 && !m.state.includes('faulty')); const can = spare || data.devices.length; const why = a.degraded > 0 ? t('{0} is degraded: add a device first, it takes the missing place.', a.name) : !can ? t('No device to replace with: add a spare or free a device.') : null; out.push(h('button', {class: 'small', disabled: a.syncAction !== 'idle' || !!why, title: why, onclick: () => replaceDialog(a)}, t('Replace device'))); if (a.syncAction === 'check') out.push(h('button', {class: 'small', onclick: () => checkArray(a, true, false)}, t('Stop check'))); else if (a.syncAction === 'repair') out.push(h('button', {class: 'small', onclick: () => checkArray(a, true, true)}, t('Stop repair'))); else out.push(h('button', {class: 'small', disabled: a.syncAction !== 'idle', onclick: () => checkArray(a, false, false)}, t('Check')), h('button', {class: 'small', disabled: a.syncAction !== 'idle', onclick: () => checkArray(a, false, true)}, t('Repair'))); } out.push(h('button', {class: 'small', disabled: a.inUse, onclick: () => stopDialog(a)}, t('Stop'))); if (running) out.push(h('button', {class: 'small danger', disabled: a.inUse, onclick: () => deleteDialog(a)}, t('Delete'))); return out; : !(spare || data.devices.length) ? t('No device to replace with: add a spare or free a device.') : null; devices.push({label: t('Replace device'), disabled: !idle || !!why, title: why, onclick: () => replaceDialog(a)}); } if (running && a.members.length) devices.push({label: t('Remove device'), onclick: () => removeDialog(a)}); if (redundant) { if (a.syncAction === 'check') scrub.push({label: t('Stop check'), onclick: () => checkArray(a, true, false)}); else if (a.syncAction === 'repair') scrub.push({label: t('Stop repair'), onclick: () => checkArray(a, true, true)}); else scrub.push({label: t('Check'), disabled: !idle, onclick: () => checkArray(a, false, false)}, {label: t('Repair'), disabled: !idle, onclick: () => checkArray(a, false, true)}); } array.push({label: t('Stop'), disabled: a.inUse, onclick: () => stopDialog(a)}); if (running) array.push({label: t('Delete'), danger: true, disabled: a.inUse, onclick: () => deleteDialog(a)}); return menu([{title: t('Devices'), items: devices}, {title: t('Check'), items: scrub}, {title: t('Array'), items: array}]); } function render() { Loading Loading @@ -538,7 +581,8 @@ if (r) { data = r; jobs.report('raid', data.jobs); if (!document.querySelector('dialog')) render(); // not under an open dialog or menu if (!document.querySelector('dialog') && !document.querySelector('details.menu[open]')) render(); } // follow a resync or a job closely, otherwise now and then const busy = data.arrays.some(a => a.syncAction && a.syncAction !== 'idle'); Loading Loading
www/app.css +24 −0 Changes for www/app.css: 24 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -130,6 +130,30 @@ tr:last-child td { border-bottom: 0; } tr:hover td { background: #f1f3f5; } td.actions { white-space: nowrap; text-align: right; } td.actions button { margin-left: 4px; } /* action menu: a button that opens a list of actions (fixed, so a scrolling table doesn't clip it; placed by the page's script) */ .menu { display: inline-block; } .menu > summary { list-style: none; cursor: pointer; white-space: nowrap; user-select: none; color: var(--text); background: var(--surface); border: 1px solid var(--line-strong); border-radius: var(--radius); padding: .2em .6em; font-size: .9em; } .menu > summary::-webkit-details-marker { display: none; } .menu > summary:hover, .menu[open] > summary { background: #eceef1; border-color: #a9aeb6; } .menu-items { position: fixed; z-index: 50; min-width: 15em; max-width: calc(100vw - 32px); padding: .3em 0; background: var(--surface); border: 1px solid var(--line-strong); border-radius: var(--radius); box-shadow: var(--shadow); text-align: left; white-space: normal; } .menu-items .menu-group { margin: .45em .9em .15em; font-size: .78em; color: var(--muted); text-transform: uppercase; letter-spacing: .03em; } .menu-items hr { border: 0; border-top: 1px solid var(--line); margin: .3em 0; } td.actions .menu-items button, .menu-items button { display: block; width: 100%; margin: 0; text-align: left; border: 0; border-radius: 0; background: transparent; padding: .35em .9em; } .menu-items button:hover:not(:disabled) { background: #eceef1; } .menu-items button.danger { color: var(--danger); } .mono { font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; font-size: .92em; } .muted { color: var(--muted); } .empty { color: var(--muted); text-align: center; padding: 1.5em; } Loading
www/modules/raid.js +65 −21 Changes for www/modules/raid.js: 65 added lines, 21 removed lines. Original line number Diff line number Diff line Loading @@ -93,6 +93,7 @@ 'Was nach ihrem Ausfall geschrieben wurde, fehlt auf ihnen: danach das Dateisystem prüfen und eine Reparatur laufen lassen.', 'I know that data may be inconsistent.': 'Mir ist klar, dass Daten inkonsistent sein können.', 'Please confirm.': 'Bitte bestätigen.', 'Actions': 'Aktionen', 'Array': 'Array', '{0} is degraded: add a device first, it takes the missing place.': '{0} ist beeinträchtigt: zuerst ein Gerät hinzufügen, es übernimmt den fehlenden Platz.', }}); Loading Loading @@ -449,30 +450,72 @@ // ---- page ---- // one button that opens the actions, in groups ({title, items: [{label, // onclick, disabled?, title?, danger?}]}); empty groups are left out function menu(groups) { groups = groups.filter(g => g.items.length); const list = h('div', {class: 'menu-items', role: 'menu'}); const box = h('details', {class: 'menu'}, h('summary', {}, t('Actions') + ' ▾'), list); groups.forEach((g, i) => { if (i) list.append(h('hr')); if (g.title) list.append(h('div', {class: 'menu-group'}, g.title)); for (const it of g.items) list.append(h('button', {class: it.danger ? 'danger' : null, role: 'menuitem', disabled: !!it.disabled, title: it.title || null, onclick: () => { box.open = false; it.onclick(); }}, it.label)); }); const close = e => { if (!box.contains(e.target)) box.open = false; }; const place = () => { const r = box.firstChild.getBoundingClientRect(); list.style.right = Math.max(16, window.innerWidth - r.right) + 'px'; list.style.top = r.bottom + 4 + 'px'; list.style.bottom = 'auto'; // no room below: open upwards if (r.bottom + 4 + list.offsetHeight > window.innerHeight && r.top > list.offsetHeight + 4) { list.style.top = 'auto'; list.style.bottom = window.innerHeight - r.top + 4 + 'px'; } }; box.addEventListener('toggle', () => { if (box.open) { document.querySelectorAll('details.menu[open]').forEach(d => { if (d !== box) d.open = false; }); place(); document.addEventListener('click', close, true); window.addEventListener('scroll', place, true); window.addEventListener('resize', place); } else { document.removeEventListener('click', close, true); window.removeEventListener('scroll', place, true); window.removeEventListener('resize', place); } }); return box; } function arrayActions(a) { const running = a.state !== 'inactive' && a.level; const out = []; if (running && a.level !== 'raid0' && a.level !== 'linear') out.push(h('button', {class: 'small', disabled: !data.devices.length, title: data.devices.length ? null : t('No free device.'), onclick: () => addDialog(a)}, t('Add device'))); if (running && a.members.length) out.push(h('button', {class: 'small', onclick: () => removeDialog(a)}, t('Remove device'))); if (running && readdCandidates(a).length) out.push(h('button', {class: 'small', onclick: () => readdDialog(a)}, t('Re-add device'))); if (running && a.level !== 'raid0' && a.level !== 'linear') { const redundant = running && a.level !== 'raid0' && a.level !== 'linear'; const idle = a.syncAction === 'idle'; const devices = [], scrub = [], array = []; if (redundant) devices.push({label: t('Add device'), disabled: !data.devices.length, title: data.devices.length ? null : t('No free device.'), onclick: () => addDialog(a)}); if (running && readdCandidates(a).length) devices.push({label: t('Re-add device'), onclick: () => readdDialog(a)}); if (redundant) { const spare = a.members.some(m => m.slot < 0 && !m.state.includes('faulty')); const can = spare || data.devices.length; const why = a.degraded > 0 ? t('{0} is degraded: add a device first, it takes the missing place.', a.name) : !can ? t('No device to replace with: add a spare or free a device.') : null; out.push(h('button', {class: 'small', disabled: a.syncAction !== 'idle' || !!why, title: why, onclick: () => replaceDialog(a)}, t('Replace device'))); if (a.syncAction === 'check') out.push(h('button', {class: 'small', onclick: () => checkArray(a, true, false)}, t('Stop check'))); else if (a.syncAction === 'repair') out.push(h('button', {class: 'small', onclick: () => checkArray(a, true, true)}, t('Stop repair'))); else out.push(h('button', {class: 'small', disabled: a.syncAction !== 'idle', onclick: () => checkArray(a, false, false)}, t('Check')), h('button', {class: 'small', disabled: a.syncAction !== 'idle', onclick: () => checkArray(a, false, true)}, t('Repair'))); } out.push(h('button', {class: 'small', disabled: a.inUse, onclick: () => stopDialog(a)}, t('Stop'))); if (running) out.push(h('button', {class: 'small danger', disabled: a.inUse, onclick: () => deleteDialog(a)}, t('Delete'))); return out; : !(spare || data.devices.length) ? t('No device to replace with: add a spare or free a device.') : null; devices.push({label: t('Replace device'), disabled: !idle || !!why, title: why, onclick: () => replaceDialog(a)}); } if (running && a.members.length) devices.push({label: t('Remove device'), onclick: () => removeDialog(a)}); if (redundant) { if (a.syncAction === 'check') scrub.push({label: t('Stop check'), onclick: () => checkArray(a, true, false)}); else if (a.syncAction === 'repair') scrub.push({label: t('Stop repair'), onclick: () => checkArray(a, true, true)}); else scrub.push({label: t('Check'), disabled: !idle, onclick: () => checkArray(a, false, false)}, {label: t('Repair'), disabled: !idle, onclick: () => checkArray(a, false, true)}); } array.push({label: t('Stop'), disabled: a.inUse, onclick: () => stopDialog(a)}); if (running) array.push({label: t('Delete'), danger: true, disabled: a.inUse, onclick: () => deleteDialog(a)}); return menu([{title: t('Devices'), items: devices}, {title: t('Check'), items: scrub}, {title: t('Array'), items: array}]); } function render() { Loading Loading @@ -538,7 +581,8 @@ if (r) { data = r; jobs.report('raid', data.jobs); if (!document.querySelector('dialog')) render(); // not under an open dialog or menu if (!document.querySelector('dialog') && !document.querySelector('details.menu[open]')) render(); } // follow a resync or a job closely, otherwise now and then const busy = data.arrays.some(a => a.syncAction && a.syncAction !== 'idle'); Loading