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:
+168
@@ -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…');
|
||||
|
||||
@@ -619,6 +619,10 @@
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><line x1="12" y1="3" x2="12" y2="9"/><line x1="12" y1="15" x2="12" y2="21"/><circle cx="12" cy="3" r="1"/><circle cx="12" cy="21" r="1"/></svg>
|
||||
Git-Snapshot
|
||||
</button>
|
||||
<button class="btn btn-secondary btn-sm pol-write" id="btnPolAssignGroups" disabled title="Ausgewählten Policies Gruppen zuweisen (Bulk)">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
|
||||
Gruppen zuweisen (<span id="polAssignSelCount">0</span>)
|
||||
</button>
|
||||
<button class="btn btn-secondary btn-sm pol-write" id="btnPolConsolidate" disabled title="Ausgewählte Settings-Catalog-Policies zu einer neuen Policy zusammenführen">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 3v6a3 3 0 0 0 3 3h4a3 3 0 0 1 3 3v6"/><polyline points="3 7 7 3 11 7"/><polyline points="13 17 17 21 21 17"/></svg>
|
||||
Konsolidieren
|
||||
@@ -984,6 +988,58 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bulk: mehreren ausgewählten Policies dieselben Gruppen zuweisen -->
|
||||
<div id="modalPolBulkAssign" class="modal hidden">
|
||||
<div class="modal-backdrop" data-close></div>
|
||||
<div class="modal-box modal-lg">
|
||||
<div class="modal-head">
|
||||
<h3>Gruppen zuweisen</h3>
|
||||
<button class="modal-close" data-close aria-label="Schließen">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-hint" style="margin-bottom:12px;">
|
||||
Die gewählten Include-/Exclude-Gruppen werden auf <b><span id="polBulkCount">0</span></b> ausgewählte Policy(s) angewendet.
|
||||
</div>
|
||||
<div id="polBulkNames" class="pol-bulk-names"></div>
|
||||
<div class="pol-assign-pickers" style="margin-top:12px;">
|
||||
<div class="pol-assign-picker" data-kind="include">
|
||||
<label class="lbl">Include-Gruppen</label>
|
||||
<div class="pol-assign-chips" data-scope="bulk" data-kind="include"></div>
|
||||
<div class="pol-assign-search-wrap">
|
||||
<input type="text" class="input pol-bulk-search" data-kind="include" placeholder="Gruppe suchen…" autocomplete="off">
|
||||
<div class="pol-assign-dropdown pol-bulk-dropdown hidden" data-kind="include"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pol-assign-picker" data-kind="exclude">
|
||||
<label class="lbl">Exclude-Gruppen</label>
|
||||
<div class="pol-assign-chips" data-scope="bulk" data-kind="exclude"></div>
|
||||
<div class="pol-assign-search-wrap">
|
||||
<input type="text" class="input pol-bulk-search" data-kind="exclude" placeholder="Gruppe suchen…" autocomplete="off">
|
||||
<div class="pol-assign-dropdown pol-bulk-dropdown hidden" data-kind="exclude"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pol-bulk-builtin" style="margin-top:14px;">
|
||||
<label class="lbl">Integrierte Ziele (Include)</label>
|
||||
<label class="pol-bulk-check"><input type="checkbox" id="polBulkAllDevices"> Alle Geräte</label>
|
||||
<label class="pol-bulk-check"><input type="checkbox" id="polBulkAllUsers"> Alle Benutzer</label>
|
||||
</div>
|
||||
<div class="pol-bulk-mode" style="margin-top:16px;">
|
||||
<label class="lbl">Modus</label>
|
||||
<label class="pol-bulk-radio"><input type="radio" name="polBulkMode" value="add" checked>
|
||||
<span><b>Hinzufügen</b> — bestehende Zuweisungen bleiben erhalten, neue Gruppen kommen dazu.</span></label>
|
||||
<label class="pol-bulk-radio"><input type="radio" name="polBulkMode" value="replace">
|
||||
<span><b>Ersetzen</b> — alle bisherigen Zuweisungen der Policy werden durch die gewählten Gruppen ersetzt.</span></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-foot">
|
||||
<span id="polBulkInfo" class="muted" style="margin-right:auto;"></span>
|
||||
<button class="btn btn-secondary" data-close>Abbrechen</button>
|
||||
<button class="btn btn-primary" id="polBulkApply" disabled>Zuweisen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MODAL: Geräte-Offboarding -->
|
||||
<div id="modalOffboard" class="modal hidden">
|
||||
<div class="modal-backdrop" data-close></div>
|
||||
|
||||
@@ -5552,6 +5552,47 @@ body.read-only .app-row {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* Gruppen-Zuweisungs-Picker (Post-Import & Bulk) */
|
||||
.pol-assign-pickers { display: flex; gap: 16px; flex-wrap: wrap; }
|
||||
.pol-assign-picker { flex: 1 1 240px; min-width: 220px; }
|
||||
.pol-assign-picker .lbl { display: block; font-size: 12px; font-weight: 600; margin-bottom: 6px; color: var(--text-dim, #888); }
|
||||
.pol-assign-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 6px; }
|
||||
.pol-assign-chip {
|
||||
display: inline-flex; align-items: center; gap: 4px;
|
||||
padding: 2px 4px 2px 9px; border-radius: var(--r-pill, 999px);
|
||||
font-size: 12px; background: rgba(96,165,250,.15); color: #60a5fa;
|
||||
}
|
||||
.pol-assign-chip.exclude { background: rgba(248,113,113,.15); color: #f87171; }
|
||||
.pol-assign-chip button {
|
||||
border: 0; background: transparent; color: inherit; cursor: pointer;
|
||||
font-size: 15px; line-height: 1; padding: 0 3px; border-radius: 50%;
|
||||
}
|
||||
.pol-assign-chip button:hover { background: rgba(0,0,0,.15); }
|
||||
.pol-assign-search-wrap { position: relative; }
|
||||
.pol-assign-dropdown {
|
||||
position: absolute; z-index: 20; left: 0; right: 0; top: calc(100% + 2px);
|
||||
max-height: 240px; overflow-y: auto;
|
||||
background: var(--canvas, #1b1b1b); border: 1px solid var(--hairline, #333);
|
||||
border-radius: 8px; box-shadow: 0 8px 24px rgba(0,0,0,.35);
|
||||
}
|
||||
.pol-assign-dropdown .opt { padding: 7px 10px; cursor: pointer; font-size: 13px; }
|
||||
.pol-assign-dropdown .opt:hover { background: var(--hover, rgba(255,255,255,.06)); }
|
||||
.pol-assign-dropdown .opt .desc { font-size: 11px; color: var(--text-dim, #888); margin-top: 1px; }
|
||||
.pol-assign-dropdown .msg { padding: 8px 10px; font-size: 12px; color: var(--text-dim, #888); }
|
||||
|
||||
/* Bulk-Zuweisung: Policy-Liste + Modus */
|
||||
.pol-bulk-names { display: flex; flex-wrap: wrap; gap: 6px; max-height: 120px; overflow-y: auto; }
|
||||
.pol-bulk-name {
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
padding: 3px 9px; border-radius: 6px; font-size: 12.5px;
|
||||
background: var(--bg3, var(--hover)); color: var(--ink, inherit);
|
||||
}
|
||||
.pol-bulk-mode .lbl { display: block; font-size: 12px; font-weight: 600; margin-bottom: 6px; color: var(--text-dim, #888); }
|
||||
.pol-bulk-radio { display: flex; gap: 8px; align-items: flex-start; margin-bottom: 8px; font-size: 13px; cursor: pointer; }
|
||||
.pol-bulk-radio input { margin-top: 2px; }
|
||||
.pol-bulk-builtin .lbl { display: block; font-size: 12px; font-weight: 600; margin-bottom: 6px; color: var(--text-dim, #888); }
|
||||
.pol-bulk-check { display: inline-flex; align-items: center; gap: 6px; margin-right: 18px; font-size: 13px; cursor: pointer; }
|
||||
|
||||
/* =============================================================
|
||||
Multi-Tenant: Topbar-Umschalter + Profil-Editor
|
||||
============================================================= */
|
||||
|
||||
Reference in New Issue
Block a user