Compare commits
7
Commits
eb974422a0
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c97f2d498 | ||
|
|
d7d80f9d00 | ||
|
|
0ff20b9573 | ||
|
|
9b17e661e4 | ||
|
|
cbb6dcec8a | ||
|
|
c3a08cf0c2 | ||
|
|
a76f4b11c3 |
Vendored
BIN
Binary file not shown.
@@ -0,0 +1,78 @@
|
|||||||
|
# import_User_to_group.ps1
|
||||||
|
|
||||||
|
PowerShell GUI-Tool zum Importieren von Benutzern aus einer CSV-Datei in eine Active Directory Gruppe.
|
||||||
|
|
||||||
|
## Voraussetzungen
|
||||||
|
|
||||||
|
| Anforderung | Details |
|
||||||
|
|---|---|
|
||||||
|
| PowerShell | 5.1 oder neuer |
|
||||||
|
| ActiveDirectory-Modul | Wird automatisch installiert (RSAT), falls nicht vorhanden |
|
||||||
|
| Berechtigungen | Normaler Benutzer reicht aus – Adminrechte werden **nur** benötigt, wenn das ActiveDirectory-Modul noch nicht installiert ist |
|
||||||
|
|
||||||
|
## CSV-Format
|
||||||
|
|
||||||
|
Die CSV-Datei muss eine Spalte namens `Username` **oder** `SamAccountName` enthalten:
|
||||||
|
|
||||||
|
```csv
|
||||||
|
Username
|
||||||
|
jdoe
|
||||||
|
mwende
|
||||||
|
asmith
|
||||||
|
```
|
||||||
|
|
||||||
|
Weitere Spalten in der CSV werden ignoriert. Leere Zeilen werden automatisch übersprungen.
|
||||||
|
|
||||||
|
## Verwendung
|
||||||
|
|
||||||
|
### GUI starten
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\import_User_to_group.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Optionaler Parameter
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Eigenen Log-Pfad angeben
|
||||||
|
.\import_User_to_group.ps1 -LogPath "C:\Logs\mein_import.txt"
|
||||||
|
```
|
||||||
|
|
||||||
|
### GUI-Bedienung
|
||||||
|
|
||||||
|
1. **Browse** – CSV-Datei auswählen
|
||||||
|
2. **Load Groups** – alle AD-Gruppen laden und in der Dropdown-Liste anzeigen (mit Autovervollständigung); alternativ Gruppenname direkt eintippen
|
||||||
|
3. **Import Users** – Import starten
|
||||||
|
|
||||||
|
## Verhalten beim Import
|
||||||
|
|
||||||
|
| Situation | Aktion |
|
||||||
|
|---|---|
|
||||||
|
| Benutzer erfolgreich hinzugefügt | Eintrag in der Ausgabe (weiss) |
|
||||||
|
| Benutzer bereits Mitglied der Gruppe | Wird übersprungen (gelb) |
|
||||||
|
| Benutzer nicht in AD gefunden | Wird übersprungen (gelb) |
|
||||||
|
| Fehler beim Hinzufügen | Fehlermeldung (rot), restliche Benutzer werden weiter verarbeitet |
|
||||||
|
|
||||||
|
Am Ende wird eine Zusammenfassung angezeigt:
|
||||||
|
```
|
||||||
|
Done. Added: 5 | Skipped: 2 | Not found: 1 | Errors: 0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Logging
|
||||||
|
|
||||||
|
Jeder Lauf erzeugt automatisch eine Log-Datei im selben Verzeichnis wie das Skript:
|
||||||
|
|
||||||
|
```
|
||||||
|
import_log_20260618_143022.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Der Pfad zur Log-Datei wird in der Statusleiste der GUI angezeigt.
|
||||||
|
|
||||||
|
## Automatische RSAT-Installation
|
||||||
|
|
||||||
|
Falls das ActiveDirectory-Modul fehlt, versucht das Skript die automatische Installation:
|
||||||
|
|
||||||
|
- **Windows Server** – `Add-WindowsFeature RSAT-AD-PowerShell`
|
||||||
|
- **Windows 10/11** – `Add-WindowsCapability` (RSAT optionales Feature)
|
||||||
|
|
||||||
|
Dafür sind Adminrechte erforderlich. Ohne Adminrechte bricht das Skript mit einem klaren Hinweis ab.
|
||||||
@@ -0,0 +1,400 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
GUI tool to export all members of an AD group (including nested groups) to a CSV file.
|
||||||
|
.PARAMETER LogPath
|
||||||
|
Optional path for the log file. Defaults to script directory with a timestamp.
|
||||||
|
#>
|
||||||
|
|
||||||
|
param (
|
||||||
|
[string]$LogPath = (Join-Path $PSScriptRoot ("export_log_" + (Get-Date -Format 'yyyyMMdd_HHmmss') + ".txt"))
|
||||||
|
)
|
||||||
|
|
||||||
|
Add-Type -AssemblyName System.Windows.Forms
|
||||||
|
Add-Type -AssemblyName System.Drawing
|
||||||
|
[System.Windows.Forms.Application]::EnableVisualStyles()
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Ensure AD module is loaded — called lazily before first AD operation
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
function Initialize-ADModule {
|
||||||
|
if (Get-Module -Name ActiveDirectory) { return $true }
|
||||||
|
|
||||||
|
if (-not (Get-Module -Name ActiveDirectory -ListAvailable)) {
|
||||||
|
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole(
|
||||||
|
[Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||||
|
if (-not $isAdmin) {
|
||||||
|
[System.Windows.Forms.MessageBox]::Show(
|
||||||
|
"Das ActiveDirectory-Modul wurde nicht gefunden.`nBitte das Skript als Administrator starten.",
|
||||||
|
"Modul fehlt", "OK", "Error") | Out-Null
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$os = (Get-CimInstance Win32_OperatingSystem).Caption
|
||||||
|
if ($os -match "Server") {
|
||||||
|
Import-Module ServerManager -ErrorAction Stop
|
||||||
|
Add-WindowsFeature RSAT-AD-PowerShell -ErrorAction Stop | Out-Null
|
||||||
|
} else {
|
||||||
|
$feature = Get-WindowsCapability -Name "Rsat.ActiveDirectory*" -Online -ErrorAction Stop |
|
||||||
|
Where-Object State -ne Installed | Select-Object -First 1
|
||||||
|
if ($feature) {
|
||||||
|
Add-WindowsCapability -Name $feature.Name -Online -ErrorAction Stop | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
[System.Windows.Forms.MessageBox]::Show(
|
||||||
|
"RSAT konnte nicht installiert werden:`n$_", "Fehler", "OK", "Error") | Out-Null
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Import-Module ActiveDirectory -ErrorAction Stop -WarningAction SilentlyContinue
|
||||||
|
return $true
|
||||||
|
} catch {
|
||||||
|
[System.Windows.Forms.MessageBox]::Show(
|
||||||
|
"ActiveDirectory-Modul konnte nicht geladen werden:`n$_", "Fehler", "OK", "Error") | Out-Null
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Script-scope state for the recursive export
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
$script:visitedGroups = $null
|
||||||
|
$script:exportResults = $null
|
||||||
|
$script:exportAdParams = $null
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
function Write-Log {
|
||||||
|
param([string]$Message, [string]$Level = 'INFO')
|
||||||
|
$entry = "[{0}] [{1}] {2}" -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Level, $Message
|
||||||
|
$entry | Out-File -FilePath $LogPath -Append -Encoding UTF8
|
||||||
|
|
||||||
|
$color = switch ($Level) {
|
||||||
|
'ERROR' { [System.Drawing.Color]::Salmon }
|
||||||
|
'WARN' { [System.Drawing.Color]::Goldenrod }
|
||||||
|
default { [System.Drawing.Color]::White }
|
||||||
|
}
|
||||||
|
Write-OutputLine $entry $color
|
||||||
|
}
|
||||||
|
|
||||||
|
function Write-OutputLine {
|
||||||
|
param([string]$Text, [System.Drawing.Color]$Color = [System.Drawing.Color]::White)
|
||||||
|
$script:rtbOutput.SelectionStart = $script:rtbOutput.TextLength
|
||||||
|
$script:rtbOutput.SelectionLength = 0
|
||||||
|
$script:rtbOutput.SelectionColor = $Color
|
||||||
|
$script:rtbOutput.AppendText("$Text`n")
|
||||||
|
$script:rtbOutput.ScrollToCaret()
|
||||||
|
[System.Windows.Forms.Application]::DoEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-NestedMembers {
|
||||||
|
param([string]$Group, [string]$SourceGroup)
|
||||||
|
|
||||||
|
if (-not $script:visitedGroups.Add($Group)) { return }
|
||||||
|
|
||||||
|
try {
|
||||||
|
$members = Get-ADGroupMember -Identity $Group -ErrorAction Stop @script:exportAdParams
|
||||||
|
} catch {
|
||||||
|
Write-Log "Gruppe '$Group' konnte nicht abgerufen werden: $_" -Level WARN
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($member in $members) {
|
||||||
|
switch ($member.objectClass) {
|
||||||
|
'user' {
|
||||||
|
try {
|
||||||
|
$user = Get-ADUser -Identity $member.distinguishedName -Properties `
|
||||||
|
DisplayName, EmailAddress, Title, Department, Company,
|
||||||
|
Enabled, LastLogonDate, PasswordLastSet, PasswordNeverExpires,
|
||||||
|
Manager, telephoneNumber, DistinguishedName `
|
||||||
|
-ErrorAction Stop @script:exportAdParams
|
||||||
|
|
||||||
|
$managerName = ''
|
||||||
|
if ($user.Manager) {
|
||||||
|
try {
|
||||||
|
$mgr = Get-ADUser -Identity $user.Manager -Properties DisplayName `
|
||||||
|
-ErrorAction Stop @script:exportAdParams
|
||||||
|
$managerName = $mgr.DisplayName
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
$script:exportResults.Add([PSCustomObject]@{
|
||||||
|
SourceGroup = $SourceGroup
|
||||||
|
MemberOfGroup = $Group
|
||||||
|
SamAccountName = $user.SamAccountName
|
||||||
|
DisplayName = $user.DisplayName
|
||||||
|
GivenName = $user.GivenName
|
||||||
|
Surname = $user.Surname
|
||||||
|
EmailAddress = $user.EmailAddress
|
||||||
|
Title = $user.Title
|
||||||
|
Department = $user.Department
|
||||||
|
Company = $user.Company
|
||||||
|
TelephoneNumber = $user.telephoneNumber
|
||||||
|
Enabled = $user.Enabled
|
||||||
|
LastLogonDate = $user.LastLogonDate
|
||||||
|
PasswordLastSet = $user.PasswordLastSet
|
||||||
|
PasswordNeverExpires = $user.PasswordNeverExpires
|
||||||
|
Manager = $managerName
|
||||||
|
DistinguishedName = $user.DistinguishedName
|
||||||
|
})
|
||||||
|
Write-Log " Benutzer: $($user.SamAccountName) ($($user.DisplayName))"
|
||||||
|
} catch {
|
||||||
|
Write-Log "Benutzer '$($member.SamAccountName)' konnte nicht abgerufen werden: $_" -Level WARN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'group' {
|
||||||
|
Write-Log "Verschachtelte Gruppe: $($member.Name)"
|
||||||
|
Get-NestedMembers -Group $member.distinguishedName -SourceGroup $SourceGroup
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Start-Export {
|
||||||
|
param([string]$GroupName, [string]$OutputPath, [string]$Server)
|
||||||
|
|
||||||
|
$script:btnExport.Enabled = $false
|
||||||
|
$script:rtbOutput.Clear()
|
||||||
|
|
||||||
|
if (-not (Initialize-ADModule)) {
|
||||||
|
$script:btnExport.Enabled = $true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$script:exportAdParams = @{}
|
||||||
|
if ($Server) { $script:exportAdParams['Server'] = $Server }
|
||||||
|
|
||||||
|
try {
|
||||||
|
$null = Get-ADGroup -Identity $GroupName -ErrorAction Stop @script:exportAdParams
|
||||||
|
Write-Log "Gruppe gefunden: $GroupName"
|
||||||
|
} catch {
|
||||||
|
Write-Log "Gruppe '$GroupName' nicht gefunden: $_" -Level ERROR
|
||||||
|
$script:btnExport.Enabled = $true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$script:visitedGroups = [System.Collections.Generic.HashSet[string]]::new(
|
||||||
|
[System.StringComparer]::OrdinalIgnoreCase)
|
||||||
|
$script:exportResults = [System.Collections.Generic.List[PSCustomObject]]::new()
|
||||||
|
|
||||||
|
Write-Log "Starte Export für '$GroupName' (inkl. verschachtelter Gruppen)..."
|
||||||
|
Get-NestedMembers -Group $GroupName -SourceGroup $GroupName
|
||||||
|
|
||||||
|
if ($script:exportResults.Count -eq 0) {
|
||||||
|
Write-Log "Keine Benutzer in Gruppe '$GroupName' gefunden." -Level WARN
|
||||||
|
$script:btnExport.Enabled = $true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$unique = $script:exportResults | Sort-Object SamAccountName -Unique
|
||||||
|
|
||||||
|
try {
|
||||||
|
$unique | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8 -Delimiter ';' -ErrorAction Stop
|
||||||
|
$msg = "Export abgeschlossen. Benutzer: {0} | Gruppen: {1} | Datei: {2}" -f `
|
||||||
|
$unique.Count, $script:visitedGroups.Count, $OutputPath
|
||||||
|
Write-Log $msg
|
||||||
|
Write-OutputLine $msg ([System.Drawing.Color]::LightGreen)
|
||||||
|
} catch {
|
||||||
|
Write-Log "Fehler beim Schreiben der CSV: $_" -Level ERROR
|
||||||
|
}
|
||||||
|
|
||||||
|
$script:btnExport.Enabled = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Form
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
$form = New-Object System.Windows.Forms.Form
|
||||||
|
$form.Text = "AD Group Export"
|
||||||
|
$form.Size = New-Object System.Drawing.Size(680, 600)
|
||||||
|
$form.StartPosition = "CenterScreen"
|
||||||
|
$form.FormBorderStyle = "FixedDialog"
|
||||||
|
$form.MaximizeBox = $false
|
||||||
|
$form.BackColor = [System.Drawing.Color]::FromArgb(30, 30, 30)
|
||||||
|
$form.ForeColor = [System.Drawing.Color]::White
|
||||||
|
$form.Font = New-Object System.Drawing.Font('Segoe UI', 9)
|
||||||
|
|
||||||
|
$pad = 16
|
||||||
|
|
||||||
|
# --- Group row ---
|
||||||
|
$lblGroup = New-Object System.Windows.Forms.Label
|
||||||
|
$lblGroup.Text = "AD Gruppe:"
|
||||||
|
$lblGroup.Location = New-Object System.Drawing.Point($pad, 20)
|
||||||
|
$lblGroup.Size = New-Object System.Drawing.Size(84, 22)
|
||||||
|
$form.Controls.Add($lblGroup)
|
||||||
|
|
||||||
|
$cmbGroup = New-Object System.Windows.Forms.ComboBox
|
||||||
|
$cmbGroup.Location = New-Object System.Drawing.Point(104, 18)
|
||||||
|
$cmbGroup.Size = New-Object System.Drawing.Size(436, 22)
|
||||||
|
$cmbGroup.BackColor = [System.Drawing.Color]::FromArgb(45, 45, 45)
|
||||||
|
$cmbGroup.ForeColor = [System.Drawing.Color]::White
|
||||||
|
$cmbGroup.FlatStyle = "Flat"
|
||||||
|
$cmbGroup.AutoCompleteMode = "SuggestAppend"
|
||||||
|
$cmbGroup.AutoCompleteSource = "ListItems"
|
||||||
|
$form.Controls.Add($cmbGroup)
|
||||||
|
|
||||||
|
$btnLoadGroups = New-Object System.Windows.Forms.Button
|
||||||
|
$btnLoadGroups.Text = "Gruppen laden"
|
||||||
|
$btnLoadGroups.Location = New-Object System.Drawing.Point(550, 16)
|
||||||
|
$btnLoadGroups.Size = New-Object System.Drawing.Size(96, 26)
|
||||||
|
$btnLoadGroups.BackColor = [System.Drawing.Color]::FromArgb(0, 122, 204)
|
||||||
|
$btnLoadGroups.ForeColor = [System.Drawing.Color]::White
|
||||||
|
$btnLoadGroups.FlatStyle = "Flat"
|
||||||
|
$btnLoadGroups.FlatAppearance.BorderSize = 0
|
||||||
|
$form.Controls.Add($btnLoadGroups)
|
||||||
|
|
||||||
|
$btnLoadGroups.Add_Click({
|
||||||
|
if (-not (Initialize-ADModule)) { return }
|
||||||
|
$btnLoadGroups.Text = "Lädt..."
|
||||||
|
$btnLoadGroups.Enabled = $false
|
||||||
|
[System.Windows.Forms.Application]::DoEvents()
|
||||||
|
try {
|
||||||
|
$adP = @{}
|
||||||
|
if ($txtServer.Tag) { $adP['Server'] = $txtServer.Tag }
|
||||||
|
$groups = Get-ADGroup -Filter * @adP | Select-Object -ExpandProperty Name | Sort-Object
|
||||||
|
$cmbGroup.Items.Clear()
|
||||||
|
$cmbGroup.Items.AddRange($groups)
|
||||||
|
Write-OutputLine "$($groups.Count) Gruppen geladen." ([System.Drawing.Color]::LightGreen)
|
||||||
|
} catch {
|
||||||
|
Write-OutputLine "Fehler beim Laden der Gruppen: $_" ([System.Drawing.Color]::Salmon)
|
||||||
|
}
|
||||||
|
$btnLoadGroups.Text = "Gruppen laden"
|
||||||
|
$btnLoadGroups.Enabled = $true
|
||||||
|
})
|
||||||
|
|
||||||
|
# --- CSV output row ---
|
||||||
|
$lblOut = New-Object System.Windows.Forms.Label
|
||||||
|
$lblOut.Text = "CSV Datei:"
|
||||||
|
$lblOut.Location = New-Object System.Drawing.Point($pad, 58)
|
||||||
|
$lblOut.Size = New-Object System.Drawing.Size(84, 22)
|
||||||
|
$form.Controls.Add($lblOut)
|
||||||
|
|
||||||
|
$txtOut = New-Object System.Windows.Forms.TextBox
|
||||||
|
$txtOut.Location = New-Object System.Drawing.Point(104, 56)
|
||||||
|
$txtOut.Size = New-Object System.Drawing.Size(436, 22)
|
||||||
|
$txtOut.BackColor = [System.Drawing.Color]::FromArgb(45, 45, 45)
|
||||||
|
$txtOut.ForeColor = [System.Drawing.Color]::White
|
||||||
|
$txtOut.BorderStyle = "FixedSingle"
|
||||||
|
$txtOut.Text = if ($PSScriptRoot) { Join-Path $PSScriptRoot "export.csv" } else { "export.csv" }
|
||||||
|
$form.Controls.Add($txtOut)
|
||||||
|
|
||||||
|
$btnBrowse = New-Object System.Windows.Forms.Button
|
||||||
|
$btnBrowse.Text = "Speichern..."
|
||||||
|
$btnBrowse.Location = New-Object System.Drawing.Point(550, 54)
|
||||||
|
$btnBrowse.Size = New-Object System.Drawing.Size(96, 26)
|
||||||
|
$btnBrowse.BackColor = [System.Drawing.Color]::FromArgb(0, 122, 204)
|
||||||
|
$btnBrowse.ForeColor = [System.Drawing.Color]::White
|
||||||
|
$btnBrowse.FlatStyle = "Flat"
|
||||||
|
$btnBrowse.FlatAppearance.BorderSize = 0
|
||||||
|
$form.Controls.Add($btnBrowse)
|
||||||
|
|
||||||
|
$btnBrowse.Add_Click({
|
||||||
|
$sfd = New-Object System.Windows.Forms.SaveFileDialog
|
||||||
|
$sfd.Filter = "CSV files (*.csv)|*.csv|All files (*.*)|*.*"
|
||||||
|
$sfd.Title = "CSV-Datei speichern"
|
||||||
|
$sfd.FileName = "export.csv"
|
||||||
|
if ($sfd.ShowDialog() -eq "OK") { $txtOut.Text = $sfd.FileName }
|
||||||
|
})
|
||||||
|
|
||||||
|
# --- Server row ---
|
||||||
|
$lblServer = New-Object System.Windows.Forms.Label
|
||||||
|
$lblServer.Text = "DC (optional):"
|
||||||
|
$lblServer.Location = New-Object System.Drawing.Point($pad, 96)
|
||||||
|
$lblServer.Size = New-Object System.Drawing.Size(84, 22)
|
||||||
|
$form.Controls.Add($lblServer)
|
||||||
|
|
||||||
|
$txtServer = New-Object System.Windows.Forms.TextBox
|
||||||
|
$txtServer.Location = New-Object System.Drawing.Point(104, 94)
|
||||||
|
$txtServer.Size = New-Object System.Drawing.Size(436, 22)
|
||||||
|
$txtServer.BackColor = [System.Drawing.Color]::FromArgb(45, 45, 45)
|
||||||
|
$txtServer.BorderStyle = "FixedSingle"
|
||||||
|
$txtServer.Tag = ''
|
||||||
|
$txtServer.Text = 'Domänencontroller (leer = Standard)'
|
||||||
|
$txtServer.ForeColor = [System.Drawing.Color]::Gray
|
||||||
|
$form.Controls.Add($txtServer)
|
||||||
|
|
||||||
|
$txtServer.Add_GotFocus({
|
||||||
|
if ($txtServer.Tag -eq '') {
|
||||||
|
$txtServer.Text = ''
|
||||||
|
$txtServer.ForeColor = [System.Drawing.Color]::White
|
||||||
|
}
|
||||||
|
})
|
||||||
|
$txtServer.Add_LostFocus({
|
||||||
|
$txtServer.Tag = $txtServer.Text.Trim()
|
||||||
|
if ($txtServer.Tag -eq '') {
|
||||||
|
$txtServer.Text = 'Domänencontroller (leer = Standard)'
|
||||||
|
$txtServer.ForeColor = [System.Drawing.Color]::Gray
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
# --- Separator ---
|
||||||
|
$sep = New-Object System.Windows.Forms.Panel
|
||||||
|
$sep.Location = New-Object System.Drawing.Point($pad, 130)
|
||||||
|
$sep.Size = New-Object System.Drawing.Size(632, 1)
|
||||||
|
$sep.BackColor = [System.Drawing.Color]::FromArgb(70, 70, 70)
|
||||||
|
$form.Controls.Add($sep)
|
||||||
|
|
||||||
|
# --- Export button ---
|
||||||
|
$script:btnExport = New-Object System.Windows.Forms.Button
|
||||||
|
$script:btnExport.Text = "Export starten"
|
||||||
|
$script:btnExport.Location = New-Object System.Drawing.Point($pad, 140)
|
||||||
|
$script:btnExport.Size = New-Object System.Drawing.Size(632, 34)
|
||||||
|
$script:btnExport.BackColor = [System.Drawing.Color]::FromArgb(16, 124, 16)
|
||||||
|
$script:btnExport.ForeColor = [System.Drawing.Color]::White
|
||||||
|
$script:btnExport.FlatStyle = "Flat"
|
||||||
|
$script:btnExport.FlatAppearance.BorderSize = 0
|
||||||
|
$script:btnExport.Font = New-Object System.Drawing.Font('Segoe UI', 10, [System.Drawing.FontStyle]::Bold)
|
||||||
|
$form.Controls.Add($script:btnExport)
|
||||||
|
|
||||||
|
$script:btnExport.Add_Click({
|
||||||
|
$groupName = $cmbGroup.Text.Trim()
|
||||||
|
$outputPath = $txtOut.Text.Trim()
|
||||||
|
$server = $txtServer.Tag
|
||||||
|
|
||||||
|
if (-not $groupName) {
|
||||||
|
[System.Windows.Forms.MessageBox]::Show(
|
||||||
|
"Bitte eine AD-Gruppe eingeben oder auswählen.", "Validierung", "OK", "Warning") | Out-Null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (-not $outputPath) {
|
||||||
|
[System.Windows.Forms.MessageBox]::Show(
|
||||||
|
"Bitte einen Speicherpfad für die CSV angeben.", "Validierung", "OK", "Warning") | Out-Null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Start-Export -GroupName $groupName -OutputPath $outputPath -Server $server
|
||||||
|
})
|
||||||
|
|
||||||
|
# --- Output panel ---
|
||||||
|
$lblOutput = New-Object System.Windows.Forms.Label
|
||||||
|
$lblOutput.Text = "Output:"
|
||||||
|
$lblOutput.Location = New-Object System.Drawing.Point($pad, 186)
|
||||||
|
$lblOutput.Size = New-Object System.Drawing.Size(80, 18)
|
||||||
|
$form.Controls.Add($lblOutput)
|
||||||
|
|
||||||
|
$script:rtbOutput = New-Object System.Windows.Forms.RichTextBox
|
||||||
|
$script:rtbOutput.Location = New-Object System.Drawing.Point($pad, 206)
|
||||||
|
$script:rtbOutput.Size = New-Object System.Drawing.Size(632, 320)
|
||||||
|
$script:rtbOutput.BackColor = [System.Drawing.Color]::FromArgb(12, 12, 12)
|
||||||
|
$script:rtbOutput.ForeColor = [System.Drawing.Color]::White
|
||||||
|
$script:rtbOutput.Font = New-Object System.Drawing.Font('Consolas', 9)
|
||||||
|
$script:rtbOutput.ReadOnly = $true
|
||||||
|
$script:rtbOutput.BorderStyle = "None"
|
||||||
|
$script:rtbOutput.ScrollBars = "Vertical"
|
||||||
|
$form.Controls.Add($script:rtbOutput)
|
||||||
|
|
||||||
|
# --- Status bar ---
|
||||||
|
$lblStatus = New-Object System.Windows.Forms.Label
|
||||||
|
$lblStatus.Text = "Log: $LogPath"
|
||||||
|
$lblStatus.Location = New-Object System.Drawing.Point($pad, 536)
|
||||||
|
$lblStatus.Size = New-Object System.Drawing.Size(632, 18)
|
||||||
|
$lblStatus.ForeColor = [System.Drawing.Color]::Gray
|
||||||
|
$lblStatus.Font = New-Object System.Drawing.Font('Segoe UI', 8)
|
||||||
|
$form.Controls.Add($lblStatus)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
$form.ShowDialog() | Out-Null
|
||||||
|
$form.Dispose()
|
||||||
Vendored
BIN
Binary file not shown.
@@ -1094,6 +1094,14 @@ function renderAppRow(app) {
|
|||||||
<span>+</span>
|
<span>+</span>
|
||||||
</button>`;
|
</button>`;
|
||||||
|
|
||||||
|
const removableReq = app.RequiredGroups.filter(g => !g.IsNative);
|
||||||
|
const removableAvail = app.AvailableGroups.filter(g => !g.IsNative);
|
||||||
|
const mkRemoveBtn = (groups, type, label) => groups.length > 0
|
||||||
|
? `<button class="type-btn type-remove type-remove-${type}" data-remove-group data-remove-type="${type}" data-app-id="${escapeHtml(app.AppId)}" data-app-name="${escapeHtml(app.AppName)}" title="${label}-Gruppe von App entfernen"><span>−${label}</span></button>`
|
||||||
|
: `<span class="type-btn type-remove type-remove--empty"></span>`;
|
||||||
|
const removeReqBtn = mkRemoveBtn(removableReq, 'required', 'R');
|
||||||
|
const removeAvailBtn = mkRemoveBtn(removableAvail, 'available', 'A');
|
||||||
|
|
||||||
const hasNoAssignments = app.AvailableGroups.length === 0 && app.RequiredGroups.length === 0;
|
const hasNoAssignments = app.AvailableGroups.length === 0 && app.RequiredGroups.length === 0;
|
||||||
// Direkt-Link in das Intune Admin Center fuer diese App (dezentes Icon, NICHT der Name selbst)
|
// Direkt-Link in das Intune Admin Center fuer diese App (dezentes Icon, NICHT der Name selbst)
|
||||||
const intuneIcon = intuneAppLink(app.AppId, app.AppName);
|
const intuneIcon = intuneAppLink(app.AppId, app.AppName);
|
||||||
@@ -1137,6 +1145,8 @@ function renderAppRow(app) {
|
|||||||
<div class="col col-avail">${availBtn || '<span class="cell-empty">—</span>'}</div>
|
<div class="col col-avail">${availBtn || '<span class="cell-empty">—</span>'}</div>
|
||||||
<div class="col col-req">${reqBtn || '<span class="cell-empty">—</span>'}</div>
|
<div class="col col-req">${reqBtn || '<span class="cell-empty">—</span>'}</div>
|
||||||
<div class="col col-create">${createBtn}</div>
|
<div class="col col-create">${createBtn}</div>
|
||||||
|
<div class="col col-remove-req">${removeReqBtn}</div>
|
||||||
|
<div class="col col-remove-avail">${removeAvailBtn}</div>
|
||||||
<div class="col col-delete">${deleteBtn}</div>
|
<div class="col col-delete">${deleteBtn}</div>
|
||||||
</div>
|
</div>
|
||||||
${detailHtml}
|
${detailHtml}
|
||||||
@@ -1688,6 +1698,26 @@ function attachAppRowEvents(wrap) {
|
|||||||
openCreateGroupModal(btn.dataset.appId, btn.dataset.appName);
|
openCreateGroupModal(btn.dataset.appId, btn.dataset.appName);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
wrap.querySelectorAll('[data-remove-group]').forEach(btn => {
|
||||||
|
btn.addEventListener('click', e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const app = State.apps.find(a => a.AppId === btn.dataset.appId);
|
||||||
|
if (!app) return;
|
||||||
|
const type = btn.dataset.removeType; // 'required' | 'available'
|
||||||
|
const src = type === 'required' ? app.RequiredGroups : app.AvailableGroups;
|
||||||
|
const groups = src.filter(g => !g.IsNative).map(g => ({ ...g, _type: type }));
|
||||||
|
if (groups.length === 0) return;
|
||||||
|
if (groups.length === 1) {
|
||||||
|
removeAssignmentWithConfirm({
|
||||||
|
appId: app.AppId, appName: app.AppName,
|
||||||
|
groupId: groups[0].GroupId, groupName: groups[0].GroupName,
|
||||||
|
type,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showRemoveGroupPicker(btn, app, groups);
|
||||||
|
});
|
||||||
|
});
|
||||||
wrap.querySelectorAll('[data-delete-app]').forEach(btn => {
|
wrap.querySelectorAll('[data-delete-app]').forEach(btn => {
|
||||||
btn.addEventListener('click', e => {
|
btn.addEventListener('click', e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -1893,6 +1923,62 @@ function showGroupPicker(anchorEl, app, groups, type) {
|
|||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showRemoveGroupPicker(anchorEl, app, groups) {
|
||||||
|
closeGroupPicker();
|
||||||
|
|
||||||
|
pickerEl = document.createElement('div');
|
||||||
|
pickerEl.className = 'group-picker picker-remove';
|
||||||
|
pickerEl.innerHTML = `
|
||||||
|
<div class="picker-head">Gruppe entfernen — ${escapeHtml(app.AppName)}</div>
|
||||||
|
<div class="picker-list">
|
||||||
|
${groups.map(g => {
|
||||||
|
const badge = g._type === 'required'
|
||||||
|
? `<span class="asg-badge req" style="font-size:10px;padding:1px 5px">Req</span>`
|
||||||
|
: `<span class="asg-badge avail" style="font-size:10px;padding:1px 5px">Avail</span>`;
|
||||||
|
return `
|
||||||
|
<div class="picker-row">
|
||||||
|
<button class="picker-item picker-remove-item"
|
||||||
|
data-group-id="${escapeHtml(g.GroupId)}"
|
||||||
|
data-group-name="${escapeHtml(g.GroupName)}"
|
||||||
|
data-type="${escapeHtml(g._type)}">
|
||||||
|
${badge}
|
||||||
|
<span class="picker-name">${escapeHtml(g.GroupName)}</span>
|
||||||
|
</button>
|
||||||
|
</div>`;
|
||||||
|
}).join('')}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(pickerEl);
|
||||||
|
|
||||||
|
const r = anchorEl.getBoundingClientRect();
|
||||||
|
const pw = pickerEl.offsetWidth;
|
||||||
|
const ph = pickerEl.offsetHeight;
|
||||||
|
let left = r.left;
|
||||||
|
let top = r.bottom + 4;
|
||||||
|
if (left + pw > window.innerWidth - 8) left = window.innerWidth - pw - 8;
|
||||||
|
if (top + ph > window.innerHeight - 8) top = r.top - ph - 4;
|
||||||
|
pickerEl.style.left = left + 'px';
|
||||||
|
pickerEl.style.top = top + 'px';
|
||||||
|
|
||||||
|
pickerEl.querySelectorAll('.picker-remove-item').forEach(item => {
|
||||||
|
item.addEventListener('click', e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const ctx = {
|
||||||
|
appId: app.AppId, appName: app.AppName,
|
||||||
|
groupId: item.dataset.groupId, groupName: item.dataset.groupName,
|
||||||
|
type: item.dataset.type,
|
||||||
|
};
|
||||||
|
closeGroupPicker();
|
||||||
|
removeAssignmentWithConfirm(ctx);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
document.addEventListener('click', onPickerOutsideClick, true);
|
||||||
|
document.addEventListener('keydown', onPickerEscape);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
// =============================================================
|
// =============================================================
|
||||||
// Membership
|
// Membership
|
||||||
// =============================================================
|
// =============================================================
|
||||||
|
|||||||
@@ -1330,6 +1330,8 @@ html[data-theme="dark"] .entra-link.teams-link:hover {
|
|||||||
98px /* available */
|
98px /* available */
|
||||||
98px /* required */
|
98px /* required */
|
||||||
26px /* + */
|
26px /* + */
|
||||||
|
32px /* −R */
|
||||||
|
32px /* −A */
|
||||||
26px; /* delete */
|
26px; /* delete */
|
||||||
column-gap: 8px;
|
column-gap: 8px;
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
@@ -1610,7 +1612,9 @@ html[data-theme="dark"] .entra-link.teams-link:hover {
|
|||||||
.col-version { justify-content: flex-start; }
|
.col-version { justify-content: flex-start; }
|
||||||
.col-avail { justify-content: stretch; }
|
.col-avail { justify-content: stretch; }
|
||||||
.col-req { justify-content: stretch; }
|
.col-req { justify-content: stretch; }
|
||||||
.col-create { justify-content: stretch; }
|
.col-create { justify-content: stretch; }
|
||||||
|
.col-remove-req { justify-content: stretch; }
|
||||||
|
.col-remove-avail { justify-content: stretch; }
|
||||||
|
|
||||||
/* Buttons fuellen ihre Spalte komplett — saubere vertikale Linien.
|
/* Buttons fuellen ihre Spalte komplett — saubere vertikale Linien.
|
||||||
Available/Required: Label klebt links, Count-Badge rechts, damit die
|
Available/Required: Label klebt links, Count-Badge rechts, damit die
|
||||||
@@ -1621,7 +1625,11 @@ html[data-theme="dark"] .entra-link.teams-link:hover {
|
|||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
}
|
}
|
||||||
.col-create .type-btn {
|
.col-create .type-btn,
|
||||||
|
.col-remove-req .type-btn,
|
||||||
|
.col-remove-req .type-remove,
|
||||||
|
.col-remove-avail .type-btn,
|
||||||
|
.col-remove-avail .type-remove {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -1891,6 +1899,26 @@ a.app-name:hover {
|
|||||||
border-color: var(--ink);
|
border-color: var(--ink);
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
}
|
}
|
||||||
|
.type-remove {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--muted);
|
||||||
|
border-style: dashed;
|
||||||
|
justify-content: center;
|
||||||
|
letter-spacing: -0.5px;
|
||||||
|
}
|
||||||
|
.type-remove:hover:not(:disabled) {
|
||||||
|
color: var(--danger, #e55);
|
||||||
|
border-color: var(--danger, #e55);
|
||||||
|
border-style: solid;
|
||||||
|
}
|
||||||
|
.type-remove--empty {
|
||||||
|
visibility: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* Anzahl-Hinweis (z.B. "Available 3" wenn 3 Available-Gruppen existieren) */
|
/* Anzahl-Hinweis (z.B. "Available 3" wenn 3 Available-Gruppen existieren) */
|
||||||
.type-btn-count {
|
.type-btn-count {
|
||||||
|
|||||||
Reference in New Issue
Block a user