Commit 5e99cf85 authored by jan.koester's avatar jan.koester
Browse files

test

parent 7b332ff4
Loading
Loading
Loading
Loading
+24 −0
Changes for www/app.css: 24 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -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; }
+65 −21
Changes for www/modules/raid.js: 65 added lines, 21 removed lines.
Original line number Diff line number Diff line
@@ -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.',
  }});
@@ -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() {
@@ -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');