Policies: Bulk-Zuweisung von Gruppen inkl. Alle Geräte/Alle Benutzer

- Neuer Toolbar-Button "Gruppen zuweisen" im Policy-Tab: mehreren
  ausgewählten Policies (typübergreifend) in einem Schritt dieselben
  Include-/Exclude-Gruppen zuweisen.
- Zwei Modi: "Hinzufügen" (bestehende Zuweisungen werden gelesen und
  gemergt, da Graph /assign Full-Replace ist) und "Ersetzen".
- Integrierte Include-Ziele "Alle Geräte" (allDevicesAssignmentTarget)
  und "Alle Benutzer" (allLicensedUsersAssignmentTarget), dedupliziert
  und mit Exclude-Gruppen kombinierbar.
- Invoke-PolicyAssignEndpoint um mode/allDevices/allUsers erweitert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-09-16 09:18:55 +02:00
co-authored by Claude Opus 4.8
parent af301925e9
commit 7bfcfc18ad
4 changed files with 322 additions and 2 deletions
+168
View File
@@ -6409,6 +6409,12 @@ function polUpdateSelCount() {
const n = keys.length;
document.getElementById('polSelCount').textContent = n;
document.getElementById('btnPolExport').disabled = n === 0;
const btnA = document.getElementById('btnPolAssignGroups');
if (btnA) {
btnA.disabled = n === 0;
const c = document.getElementById('polAssignSelCount');
if (c) c.textContent = n;
}
// Konsolidieren nur bei >=2 Policies UND alle vom Typ Settings Catalog.
const btnC = document.getElementById('btnPolConsolidate');
if (btnC) {
@@ -6843,6 +6849,168 @@ document.getElementById('polAssignApply')?.addEventListener('click', async () =>
}
});
// =============================================================
// Bulk: mehreren ausgewählten Policies dieselben Gruppen zuweisen
// =============================================================
const POL_TYPE_TO_EXPORT = {
settingscatalog: 'SettingsCatalog',
compliance: 'CompliancePolicy',
configuration: 'ConfigurationProfile',
administrativetemplate: 'AdministrativeTemplate',
};
const PolBulkAssign = { policies: [], include: [], exclude: [] };
let _polBulkSearchTimer = null;
function openPolBulkAssign() {
const sel = PolState.items.filter(p => PolState.selected.has(polKey(p)));
if (!sel.length) return;
PolBulkAssign.policies = sel.map(p => ({
id: p.id,
name: p.name || '(ohne Name)',
exportType: POL_TYPE_TO_EXPORT[p.type] || '',
}));
PolBulkAssign.include = [];
PolBulkAssign.exclude = [];
document.getElementById('polBulkCount').textContent = sel.length;
document.getElementById('polBulkNames').innerHTML = PolBulkAssign.policies.map(p =>
`<span class="pol-bulk-name">${escapeHtml(p.name)} <span class="pol-type-badge">${escapeHtml(POL_TYPE_LABELS[p.exportType] || 'Policy')}</span></span>`
).join('');
const addRadio = document.querySelector('input[name="polBulkMode"][value="add"]');
if (addRadio) addRadio.checked = true;
const ad = document.getElementById('polBulkAllDevices'); if (ad) ad.checked = false;
const au = document.getElementById('polBulkAllUsers'); if (au) au.checked = false;
renderPolBulkChips('include');
renderPolBulkChips('exclude');
updatePolBulkApply();
openModal('modalPolBulkAssign');
}
function renderPolBulkChips(kind) {
const box = document.querySelector(`.pol-assign-chips[data-scope="bulk"][data-kind="${kind}"]`);
if (!box) return;
box.innerHTML = PolBulkAssign[kind].map((g, j) => `
<span class="pol-assign-chip ${kind === 'exclude' ? 'exclude' : ''}">
${escapeHtml(g.name)}
<button type="button" data-kind="${kind}" data-j="${j}" aria-label="Entfernen">×</button>
</span>`).join('');
updatePolBulkApply();
}
function updatePolBulkApply() {
const inc = PolBulkAssign.include.length, exc = PolBulkAssign.exclude.length;
const allDev = document.getElementById('polBulkAllDevices')?.checked || false;
const allUsr = document.getElementById('polBulkAllUsers')?.checked || false;
const has = inc + exc > 0 || allDev || allUsr;
const btn = document.getElementById('polBulkApply');
if (btn) btn.disabled = !has;
const info = document.getElementById('polBulkInfo');
if (info) {
if (!has) { info.textContent = 'Mindestens eine Gruppe oder ein integriertes Ziel wählen.'; }
else {
const parts = [];
if (allDev) parts.push('Alle Geräte');
if (allUsr) parts.push('Alle Benutzer');
if (inc) parts.push(`${inc} Include`);
if (exc) parts.push(`${exc} Exclude`);
info.textContent = parts.join(' · ');
}
}
}
function polBulkHideDropdowns() {
document.querySelectorAll('.pol-bulk-dropdown').forEach(d => { d.classList.add('hidden'); d.innerHTML = ''; });
}
// Gruppensuche (debounced) im Bulk-Modal
document.getElementById('modalPolBulkAssign')?.addEventListener('input', e => {
const input = e.target.closest('.pol-bulk-search');
if (!input) return;
const kind = input.dataset.kind;
const dd = document.querySelector(`.pol-bulk-dropdown[data-kind="${kind}"]`);
const q = input.value.trim();
clearTimeout(_polBulkSearchTimer);
if (q.length < 2) { if (dd) { dd.classList.add('hidden'); dd.innerHTML = ''; } return; }
if (dd) { dd.classList.remove('hidden'); dd.innerHTML = '<div class="msg">Suche…</div>'; }
_polBulkSearchTimer = setTimeout(async () => {
try {
const data = await api(`/api/groups/search?q=${encodeURIComponent(q)}`);
const items = (data.items || []).slice(0, 25);
if (!dd) return;
if (!items.length) { dd.innerHTML = '<div class="msg">Keine Gruppen gefunden.</div>'; return; }
dd.innerHTML = items.map(g => `
<div class="opt" data-id="${escapeHtml(g.Id)}" data-name="${escapeHtml(g.DisplayName)}">
<div>${escapeHtml(g.DisplayName)}</div>
${g.Description ? `<div class="desc">${escapeHtml(g.Description)}</div>` : ''}
</div>`).join('');
} catch (err) {
if (dd) dd.innerHTML = `<div class="msg">Suche fehlgeschlagen: ${escapeHtml(err.message)}</div>`;
}
}, 300);
});
// Option wählen / Chip entfernen im Bulk-Modal
document.getElementById('modalPolBulkAssign')?.addEventListener('click', e => {
const opt = e.target.closest('.pol-bulk-dropdown .opt');
if (opt) {
const dd = opt.closest('.pol-bulk-dropdown');
const kind = dd.dataset.kind;
const id = opt.dataset.id, name = opt.dataset.name;
const other = kind === 'include' ? 'exclude' : 'include';
if (!PolBulkAssign[kind].some(g => g.id === id) && !PolBulkAssign[other].some(g => g.id === id)) {
PolBulkAssign[kind].push({ id, name });
renderPolBulkChips(kind);
}
const input = document.querySelector(`.pol-bulk-search[data-kind="${kind}"]`);
if (input) input.value = '';
dd.classList.add('hidden'); dd.innerHTML = '';
return;
}
const rm = e.target.closest('.pol-assign-chip button');
if (rm) {
const kind = rm.dataset.kind, j = +rm.dataset.j;
PolBulkAssign[kind].splice(j, 1);
renderPolBulkChips(kind);
}
});
document.addEventListener('click', e => {
if (!e.target.closest('#modalPolBulkAssign .pol-assign-search-wrap')) polBulkHideDropdowns();
});
document.getElementById('polBulkAllDevices')?.addEventListener('change', updatePolBulkApply);
document.getElementById('polBulkAllUsers')?.addEventListener('change', updatePolBulkApply);
document.getElementById('polBulkApply')?.addEventListener('click', async () => {
const inc = PolBulkAssign.include.map(g => g.id);
const exc = PolBulkAssign.exclude.map(g => g.id);
const allDevices = document.getElementById('polBulkAllDevices')?.checked || false;
const allUsers = document.getElementById('polBulkAllUsers')?.checked || false;
if (!inc.length && !exc.length && !allDevices && !allUsers) return;
const mode = (document.querySelector('input[name="polBulkMode"]:checked')?.value) === 'replace' ? 'replace' : 'add';
const items = PolBulkAssign.policies.map(p => ({
exportType: p.exportType, id: p.id, policyName: p.name,
include: inc, exclude: exc, allDevices, allUsers, mode,
}));
setLoading('Weise Gruppen zu…');
try {
const res = await api('/api/policies/assign', { method: 'POST', body: { items }, timeoutMs: 180000 });
const results = res.results || [];
const fail = results.filter(r => !r.success);
if (fail.length) {
const first = fail[0];
toast(`${res.assignedCount || 0} zugewiesen, ${fail.length} fehlgeschlagen. z.B. "${first.policyName || '?'}": ${first.error || 'Fehler'}`, 'err', 'Zuweisung');
} else {
toast(`Gruppen ${mode === 'add' ? 'hinzugefügt' : 'ersetzt'}: ${res.assignedCount || 0} Policy(s).`, 'ok', 'Zuweisung');
}
closeModal('modalPolBulkAssign');
} catch (e) {
toast('Zuweisung fehlgeschlagen: ' + e.message, 'err');
} finally {
clearLoading();
}
});
document.getElementById('btnPolAssignGroups')?.addEventListener('click', openPolBulkAssign);
// Alle Policies als Snapshot in den konfigurierten Git-Ordner schreiben + committen.
async function polGitSnapshot() {
setLoading('Erstelle Policy-Snapshot & committe…');