UI : éléments répliqués choisis dans la fenêtre de dry-run, menu de la barre retiré

Le choix Répliquer / Ne pas répliquer de la fenêtre de dry-run devient la
liste enregistrée des éléments répliqués (administrateurs) : tous les
éléments y figurent, avec leurs dépendances, et un changement relance le
dry-run. Le menu « Éléments répliqués » de la barre du haut et l'exclusion
par exécution (skip) côté interface sont retirés.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
cedricandClaude Opus 5.5 committed 2026-10-23 19:36:23 +02:00
1 parent 993fe2d8a7
commit 4980cd4c7f
3 files changed
+40 -71

No files matched your search

+36 -61
View File
@@ -13,10 +13,7 @@
refreshTitle: 'Relire le primaire et les cibles', settings: 'Paramètres', theme: 'Thème clair / sombre',
logout: 'Se déconnecter', viewAria: 'Affichage', viewDiff: 'Écarts', viewClusters: 'Clusters', viewRuns: 'Journal', viewAudit: 'Audit',
kindAria: "Type d'objet", statusAria: 'Statut', modeAria: 'Mode', dryAll: 'Dry-run', syncAll: 'Répliquer',
kindMenu: 'Éléments répliqués', targetMenu: 'Cibles', allTargets: 'Toutes les cibles', noTarget: 'Aucune cible sélectionnée.',
kindMenuRO: 'Seuls les administrateurs changent les éléments répliqués.',
kindMenuHelp: 'Les comptes du realm pve sont répliqués avec leurs mots de passe et ont besoin de leurs groupes : ils sont cochés ensemble.',
kindGroups: { dc: 'Datacenter', access: 'Comptes et droits (realm pve)', sdn: 'SDN' },
targetMenu: 'Cibles', allTargets: 'Toutes les cibles', noTarget: 'Aucune cible sélectionnée.',
setupClusters: 'Base vide : ajoutez vos clusters (URL et mot de passe root), puis choisissez le primaire.', setupClustersBtn: 'Ajouter des clusters',
setupPrimary: 'Aucun cluster primaire : choisissez la source de la réplication dans la vue Clusters.', setupPrimaryBtn: 'Choisir le primaire',
stripAria: 'Clusters', colKind: 'Type', colObject: 'Objet', colStatus: 'Statut', colFields: 'Champs',
@@ -100,8 +97,9 @@
language: 'Langue', themeTitle: 'Thème', themeSystem: 'Système', themeLight: 'Clair', themeDark: 'Sombre',
density: 'Densité', densityNormal: 'Normale', densityCompact: 'Compacte',
noRights: 'Votre rôle ne permet pas de répliquer.', errorPrefix: 'Erreur : ',
planKinds: 'Éléments de cette exécution', kindOn: 'Répliquer', kindOff: 'Ne pas répliquer',
planKindsHelp: "Un élément exclu n'est pas écrit, sauf ce dont les autres ont besoin (un groupe à créer pour un compte). Changer un choix relance le dry-run.",
planKinds: 'Éléments répliqués', kindOn: 'Répliquer', kindOff: 'Ne pas répliquer',
planKindsHelp: 'Choix enregistré pour toutes les réplications. Les comptes du realm pve vont avec leurs mots de passe et ont besoin de leurs groupes. Changer un choix relance le dry-run.',
planKindsRO: 'Seuls les administrateurs changent les éléments répliqués.',
redacted: 'masqué',
polDef: {
'datacenter-options': 'tag-style, registered-tags, user-tag-access, next-id : sync ; le reste : ignore ; webauthn, u2f, http_proxy : jamais',
@@ -117,10 +115,7 @@
refreshTitle: 'Read the primary and the targets again', settings: 'Settings', theme: 'Light / dark theme',
logout: 'Sign out', viewAria: 'View', viewDiff: 'Differences', viewClusters: 'Clusters', viewRuns: 'Journal', viewAudit: 'Audit',
kindAria: 'Object kind', statusAria: 'Status', modeAria: 'Mode', dryAll: 'Dry-run', syncAll: 'Replicate',
kindMenu: 'Replicated items', targetMenu: 'Targets', allTargets: 'All targets', noTarget: 'No target selected.',
kindMenuRO: 'Only administrators change the replicated items.',
kindMenuHelp: 'The accounts of the pve realm are replicated with their passwords and need their groups: they are checked together.',
kindGroups: { dc: 'Datacenter', access: 'Accounts and permissions (pve realm)', sdn: 'SDN' },
targetMenu: 'Targets', allTargets: 'All targets', noTarget: 'No target selected.',
setupClusters: 'Empty database: add your clusters (URL and root password), then choose the primary.', setupClustersBtn: 'Add clusters',
setupPrimary: 'No primary cluster: choose the source of the replication in the Clusters view.', setupPrimaryBtn: 'Choose the primary',
stripAria: 'Clusters', colKind: 'Kind', colObject: 'Object', colStatus: 'Status', colFields: 'Fields',
@@ -204,8 +199,9 @@
language: 'Language', themeTitle: 'Theme', themeSystem: 'System', themeLight: 'Light', themeDark: 'Dark',
density: 'Density', densityNormal: 'Normal', densityCompact: 'Compact',
noRights: 'Your role cannot replicate.', errorPrefix: 'Error: ',
planKinds: 'Items of this run', kindOn: 'Replicate', kindOff: 'Do not replicate',
planKindsHelp: 'An excluded item is not written, except what the others need (a group to create for an account). Changing a choice runs the dry-run again.',
planKinds: 'Replicated items', kindOn: 'Replicate', kindOff: 'Do not replicate',
planKindsHelp: 'Choice saved for every replication. The accounts of the pve realm go with their passwords and need their groups. Changing a choice runs the dry-run again.',
planKindsRO: 'Only administrators change the replicated items.',
redacted: 'hidden',
polDef: {
'datacenter-options': 'tag-style, registered-tags, user-tag-access, next-id: sync; the rest: ignore; webauthn, u2f, http_proxy: never',
@@ -218,7 +214,6 @@
},
};
const KINDS = ['datacenter-options', 'pve-group', 'pve-role', 'pve-user', 'pve-password', 'sdn-zone', 'sdn-vnet', 'sdn-subnet'];
const KIND_GROUPS = [['dc', ['datacenter-options']], ['access', ['pve-group', 'pve-role', 'pve-user']], ['sdn', ['sdn-zone', 'sdn-vnet', 'sdn-subnet']]];
// kinds chosen as one item: the accounts of the pve realm with their passwords
const BUNDLES = { 'pve-user': ['pve-password'] };
const ITEMS = KINDS.filter((k) => !Object.values(BUNDLES).some((b) => b.includes(k)));
@@ -548,7 +543,7 @@
$('#setupBtn').textContent = t(S.setup === 'clusters' ? 'setupClustersBtn' : 'setupPrimaryBtn');
}
$$('[data-admin-only]').forEach((e) => { e.hidden = !can.admin(); });
if (S.view === 'diff') { renderDiff(); renderKindMenu(); renderTargetMenu(); }
if (S.view === 'diff') { renderDiff(); renderTargetMenu(); }
if (S.view === 'clusters') renderClusters();
if (S.view === 'runs') renderRuns();
if (S.view === 'audit') renderAudit();
@@ -802,22 +797,19 @@
}
// renderPlanKinds shows, above the plan, one "replicate / do not replicate"
// choice per replicated item: a change runs the dry-run again without the
// items left out, so the reviewed plan is the one applied.
// choice per item: it is the saved list of replicated items (administrators),
// and a change runs the dry-run again, so the reviewed plan is the one applied.
function renderPlanKinds(sel, plans, intentSync) {
const box = $('#planKinds');
const kinds = (S.settings && S.settings.kinds) || KINDS;
const items = ITEMS.filter((k) => kinds.includes(k));
box.hidden = sel.scope === 'object' || items.length < 2;
box.hidden = sel.scope === 'object' || !S.settings;
if (box.hidden) return;
const skip = new Set(sel.skip || []);
const kinds = S.settings.kinds || KINDS;
const admin = can.admin();
const count = (k) => plans.reduce((n, p) => n + ((p.plan && p.plan.changes) || []).filter((c) => bundled(k).includes(c.kind)).length, 0);
const rows = items.map((k) => {
const off = skip.has(k);
const set = (v) => {
const next = new Set(skip);
bundled(k).forEach((x) => (v ? next.delete(x) : next.add(x)));
openPlan(Object.assign({}, sel, { skip: KINDS.filter((x) => next.has(x)) }), intentSync);
const rows = ITEMS.map((k) => {
const off = !kinds.includes(k);
const set = async (v) => {
if (await saveKinds(withDeps(kinds, k, v))) openPlan(sel, intentSync);
};
const n = off ? '—' : String(count(k));
return h('div', { class: 'pk-row' + (off ? ' off' : '') },
@@ -825,10 +817,23 @@
h('span', { class: 'pk-name', text: t('kinds.' + k) }),
h('span', { class: 'pk-n', text: n, title: off ? '' : t('changesN', { n }) }),
h('div', { class: 'seg', role: 'radiogroup', 'aria-label': t('kinds.' + k) },
h('button', { type: 'button', role: 'radio', class: off ? '' : 'on', 'aria-checked': String(!off), onclick: () => off && set(true) }, t('kindOn')),
h('button', { type: 'button', role: 'radio', class: off ? 'on' : '', 'aria-checked': String(off), onclick: () => !off && set(false) }, t('kindOff'))));
h('button', { type: 'button', role: 'radio', class: off ? '' : 'on', 'aria-checked': String(!off), disabled: !admin, onclick: () => off && set(true) }, t('kindOn')),
h('button', { type: 'button', role: 'radio', class: off ? 'on' : '', 'aria-checked': String(off), disabled: !admin, onclick: () => !off && set(false) }, t('kindOff'))));
});
box.replaceChildren(h('div', { class: 'pk-head' }, h('strong', { text: t('planKinds') }), h('span', { class: 'muted small', text: t('planKindsHelp') })), ...rows);
box.replaceChildren(h('div', { class: 'pk-head' }, h('strong', { text: t('planKinds') }), h('span', { class: 'muted small', text: t(admin ? 'planKindsHelp' : 'planKindsRO') })), ...rows);
}
// saveKinds saves the list of replicated items (at least one).
async function saveKinds(kinds) {
if (!kinds.length) return false;
const body = Object.assign({}, S.settings, { kinds });
try {
await api('PUT', 'api/settings', body);
S.settings = body;
return true;
} catch (e) {
if (e.message !== 'auth') toast(e.message, true);
return false;
}
}
function changeList(changes) {
@@ -1506,7 +1511,7 @@
}
}
// ---------- menus of the filter bar: replicated items, targets ----------
// ---------- replicated items, targets of the global actions ----------
// withDeps checks or unchecks a kind with the kinds it needs (or that need it).
function withDeps(kinds, k, on) {
const out = new Set(kinds);
@@ -1515,36 +1520,6 @@
if (on) need(k); else bundled(k).forEach(drop);
return KINDS.filter((x) => out.has(x));
}
function renderKindMenu() {
const pop = $('#kindMenuPop');
const kinds = (S.settings && S.settings.kinds) || KINDS;
$('#kindMenuN').textContent = ITEMS.filter((k) => kinds.includes(k)).length + '/' + ITEMS.length;
const admin = can.admin();
pop.replaceChildren();
for (const [g, ks] of KIND_GROUPS) {
pop.appendChild(h('div', { class: 'grp', text: t('kindGroups.' + g) }));
for (const k of ks) {
pop.appendChild(h('label', { class: 'opt' },
h('input', { type: 'checkbox', checked: kinds.includes(k), disabled: !admin || !S.settings,
onchange: (ev) => saveKinds(withDeps(kinds, k, ev.target.checked)) }),
h('span', { class: 'kind ' + k, text: t('kindShort.' + k) }), h('span', { text: t('kinds.' + k) })));
}
}
pop.appendChild(h('div', { class: 'foot' }, h('span', { class: 'note-s', text: admin ? t('kindMenuHelp') : t('kindMenuRO') })));
}
async function saveKinds(kinds) {
if (!kinds.length) { renderKindMenu(); return; }
const body = Object.assign({}, S.settings, { kinds });
try {
await api('PUT', 'api/settings', body);
S.settings = body;
renderKindMenu();
loadState(true);
} catch (e) {
if (e.message !== 'auth') toast(e.message, true);
renderKindMenu();
}
}
// targets of the global dry-run / replication (all by default; the ones
// unchecked are remembered in this browser)
function selectedTargets() {
@@ -1727,7 +1702,7 @@
const d = await api('GET', 'api/settings');
S.settings = d.settings;
S.deps = d.deps || {};
} catch (e) { /* the menu stays read only */ }
} catch (e) { /* the replicated items stay read only */ }
if (S.view === 'clusters') loadClusters();
await loadState(false);
if (S.view === 'runs') loadRuns(true);
-8
View File
@@ -67,14 +67,6 @@
<span data-i18n="viewAudit">Audit</span>
</button>
</div>
<details id="kindMenu" class="menu" data-diff>
<summary>
<!-- Lucide "list-checks" (ISC license) -->
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m3 17 2 2 4-4M3 7l2 2 4-4M13 6h8M13 12h8M13 18h8"/></svg>
<span data-i18n="kindMenu">Éléments répliqués</span><span class="n" id="kindMenuN"></span>
</summary>
<div class="pop" id="kindMenuPop"></div>
</details>
<select id="kindSel" data-diff data-i18n-aria="kindAria"></select>
<select id="statusSel" data-diff data-i18n-aria="statusAria"></select>
<select id="runModeSel" data-runs data-i18n-aria="modeAria" hidden></select>
+4 -2
View File
@@ -697,7 +697,7 @@ body.login-page { min-height: 100vh; padding: 16px; display: grid; place-items:
.erow .acts { display: flex; gap: 2px; }
.import-res { margin: 10px 0 0; }
/* ---------- drop-down menus of the filter bar (replicated kinds, targets) ---------- */
/* ---------- drop-down menu of the filter bar (targets) ---------- */
.menu { position: relative; }
.menu > summary {
list-style: none; display: inline-flex; align-items: center; gap: 6px; height: 32px; padding: 0 10px;
@@ -746,11 +746,13 @@ body.login-page { min-height: 100vh; padding: 16px; display: grid; place-items:
.setup-form h2 { margin: 0 0 4px; font-size: 15px; text-align: center; }
.setup-form p { margin: 0 0 6px; font-size: 12.5px; color: var(--muted); }
/* dry-run: replicate / do not replicate, per item */
/* dry-run: replicated items (replicate / do not replicate) */
.plan-kinds { display: grid; gap: 6px; padding: 12px 20px; border-bottom: 1px solid var(--border); background: var(--panel-2); }
.plan-kinds .pk-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px; margin-bottom: 2px; }
.plan-kinds .pk-row { display: grid; grid-template-columns: 6.5rem minmax(0, 1fr) 2.5rem auto; align-items: center; gap: 10px; }
.plan-kinds .pk-row.off .pk-name, .plan-kinds .pk-row.off .kind { opacity: .55; }
.plan-kinds .pk-n { text-align: right; font-variant-numeric: tabular-nums; color: var(--muted); }
.plan-kinds .seg button { padding: 3px 10px; font-size: 12px; }
.plan-kinds .seg button:disabled { cursor: default; }
.plan-kinds .seg button:disabled:not([aria-checked="true"]) { opacity: .55; }
@media (max-width: 560px) { .plan-kinds .pk-row { grid-template-columns: minmax(0, 1fr) auto; } .plan-kinds .pk-row .kind, .plan-kinds .pk-n { display: none; } }