Rewrite web handler with server-side rendering - no jQuery dependency
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
using Disco.Plugins.ADCompare.Features;
|
||||
using Disco.Services.Plugins;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
@@ -19,8 +18,8 @@ namespace Disco.Plugins.ADCompare.WebHandler
|
||||
case "":
|
||||
case "index":
|
||||
return Index();
|
||||
case "compare":
|
||||
return Compare();
|
||||
case "run":
|
||||
return Run();
|
||||
case "export":
|
||||
return ExportCsv();
|
||||
default:
|
||||
@@ -30,24 +29,14 @@ namespace Disco.Plugins.ADCompare.WebHandler
|
||||
|
||||
private ActionResult Index()
|
||||
{
|
||||
return new ContentResult
|
||||
{
|
||||
Content = BuildDashboardHtml(),
|
||||
ContentType = "text/html",
|
||||
ContentEncoding = Encoding.UTF8
|
||||
};
|
||||
return HtmlResult(BuildPage(null));
|
||||
}
|
||||
|
||||
private ActionResult Compare()
|
||||
private ActionResult Run()
|
||||
{
|
||||
var service = new DeviceCompareService(Database);
|
||||
var summary = service.CompareAllDevices();
|
||||
return new ContentResult
|
||||
{
|
||||
Content = JsonConvert.SerializeObject(summary, Formatting.Indented),
|
||||
ContentType = "application/json",
|
||||
ContentEncoding = Encoding.UTF8
|
||||
};
|
||||
return HtmlResult(BuildPage(summary));
|
||||
}
|
||||
|
||||
private ActionResult ExportCsv()
|
||||
@@ -59,14 +48,13 @@ namespace Disco.Plugins.ADCompare.WebHandler
|
||||
foreach (var r in summary.Results.Where(r => !r.IsMatch))
|
||||
{
|
||||
sb.AppendLine(string.Join(",",
|
||||
CsvEscape(r.SerialNumber), CsvEscape(r.ComputerName),
|
||||
CsvEscape(r.DiscoAssignedUserId), CsvEscape(r.DiscoAssignedUserDisplayName),
|
||||
CsvEscape(r.ADManagedByUserId), CsvEscape(r.ADManagedByDisplayName),
|
||||
r.IsMatch.ToString(), CsvEscape(r.MismatchReason)));
|
||||
CsvEsc(r.SerialNumber), CsvEsc(r.ComputerName),
|
||||
CsvEsc(r.DiscoAssignedUserId), CsvEsc(r.DiscoAssignedUserDisplayName),
|
||||
CsvEsc(r.ADManagedByUserId), CsvEsc(r.ADManagedByDisplayName),
|
||||
r.IsMatch.ToString(), CsvEsc(r.MismatchReason)));
|
||||
}
|
||||
var fileName = "AD_ManagedBy_Compare_" + DateTime.Now.ToString("yyyyMMdd_HHmmss") + ".csv";
|
||||
HostController.Response.Headers.Add("Content-Disposition",
|
||||
"attachment; filename=\"" + fileName + "\"");
|
||||
HostController.Response.Headers.Add("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
|
||||
return new ContentResult
|
||||
{
|
||||
Content = sb.ToString(),
|
||||
@@ -75,75 +63,134 @@ namespace Disco.Plugins.ADCompare.WebHandler
|
||||
};
|
||||
}
|
||||
|
||||
private string CsvEscape(string v)
|
||||
private ActionResult HtmlResult(string html)
|
||||
{
|
||||
return new ContentResult
|
||||
{
|
||||
Content = html,
|
||||
ContentType = "text/html",
|
||||
ContentEncoding = Encoding.UTF8
|
||||
};
|
||||
}
|
||||
|
||||
private string CsvEsc(string v)
|
||||
{
|
||||
if (v == null) v = "";
|
||||
return "\"" + v.Replace("\"", "\"\"") + "\"";
|
||||
}
|
||||
|
||||
private string BuildDashboardHtml()
|
||||
private string H(string v)
|
||||
{
|
||||
// Build the plugin URL manually (T4MVC's MVC.API class isn't available outside the Disco solution)
|
||||
var pluginUrl = "/Plugin/" + HttpUtility.UrlEncode(Manifest.Id);
|
||||
if (string.IsNullOrEmpty(v)) return "";
|
||||
return HttpUtility.HtmlEncode(v);
|
||||
}
|
||||
|
||||
return @"
|
||||
<div id='adcompare'>
|
||||
<h2><i class='fa fa-exchange'></i> AD Compare — Device Managed By</h2>
|
||||
<p>Compares the AD computer <strong>Managed By</strong> field against the Disco <strong>Assigned User</strong>.</p>
|
||||
<div style='margin:15px 0;'>
|
||||
<button id='btnRun' class='btn btn-primary' onclick='runCompare()'><i class='fa fa-refresh'></i> Run Comparison</button>
|
||||
<button id='btnExport' class='btn btn-default' onclick='location.href=""" + pluginUrl + @"/Export""' style='margin-left:10px;' disabled><i class='fa fa-download'></i> Export Mismatches CSV</button>
|
||||
<span id='loading' style='display:none;margin-left:15px;'><i class='fa fa-spinner fa-spin'></i> Querying AD for all devices…</span>
|
||||
</div>
|
||||
<div id='summary' style='display:none;margin:15px 0;padding:15px;background:#f5f5f5;border-radius:4px;'>
|
||||
<div class='row'>
|
||||
<div class='col-md-2'><strong>Total Devices</strong><br/><span id='sTotal' style='font-size:24px;'>-</span></div>
|
||||
<div class='col-md-2 text-success'><strong>Matched</strong><br/><span id='sMatch' style='font-size:24px;'>-</span></div>
|
||||
<div class='col-md-2 text-warning'><strong>Mismatched</strong><br/><span id='sMismatch' style='font-size:24px;'>-</span></div>
|
||||
<div class='col-md-2 text-danger'><strong>Not in AD</strong><br/><span id='sNotAD' style='font-size:24px;'>-</span></div>
|
||||
<div class='col-md-2 text-info'><strong>No ManagedBy</strong><br/><span id='sNoMgr' style='font-size:24px;'>-</span></div>
|
||||
<div class='col-md-2 text-muted'><strong>No Assignment</strong><br/><span id='sNoAssign' style='font-size:24px;'>-</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div style='margin:10px 0;'>
|
||||
<label><input type='checkbox' id='chkMismatchOnly' checked onchange='renderTable()'> Show mismatches only</label>
|
||||
<input type='text' id='txtFilter' placeholder='Filter by serial, computer name, or user...' style='margin-left:15px;padding:4px 8px;width:350px;' oninput='renderTable()'>
|
||||
</div>
|
||||
<table id='tbl' class='table table-striped table-condensed' style='display:none;'>
|
||||
<thead><tr>
|
||||
<th></th><th>Serial Number</th><th>Computer Name</th>
|
||||
<th>Disco Assigned User</th><th>AD Managed By</th><th>Reason</th>
|
||||
</tr></thead>
|
||||
<tbody id='tbody'></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<script>
|
||||
var data=[];
|
||||
function runCompare(){
|
||||
$('#loading').show();$('#btnRun').prop('disabled',true);$('#tbl').hide();
|
||||
$.getJSON('" + pluginUrl + @"/Compare',function(d){
|
||||
data=d.Results;
|
||||
$('#sTotal').text(d.TotalDevices);$('#sMatch').text(d.DevicesMatched);
|
||||
$('#sMismatch').text(d.DevicesMismatched);$('#sNotAD').text(d.DevicesNotInAD);
|
||||
$('#sNoMgr').text(d.DevicesNoManagedBy);$('#sNoAssign').text(d.DevicesNoAssignment);
|
||||
$('#summary').show();$('#btnExport').prop('disabled',false);
|
||||
renderTable();$('#tbl').show();
|
||||
}).fail(function(x){alert('Error: '+x.statusText);})
|
||||
.always(function(){$('#loading').hide();$('#btnRun').prop('disabled',false);});
|
||||
}
|
||||
function renderTable(){
|
||||
var mo=$('#chkMismatchOnly').is(':checked'),q=$('#txtFilter').val().toLowerCase(),tb=$('#tbody');tb.empty();
|
||||
data.forEach(function(r){
|
||||
if(mo&&r.IsMatch)return;
|
||||
if(q){var hay=(r.SerialNumber+' '+r.ComputerName+' '+r.DiscoAssignedUserId+' '+r.ADManagedByUserId+' '+(r.DiscoAssignedUserDisplayName||'')+' '+(r.ADManagedByDisplayName||'')).toLowerCase();if(hay.indexOf(q)<0)return;}
|
||||
var icon=r.IsMatch?'<i class=""fa fa-check text-success""></i>':(!r.FoundInAD?'<i class=""fa fa-times text-danger""></i>':'<i class=""fa fa-exclamation-triangle text-warning""></i>');
|
||||
var discoUser=r.DiscoAssignedUserId?(esc(r.DiscoAssignedUserId)+(r.DiscoAssignedUserDisplayName?'<br/><small class=""text-muted"">'+esc(r.DiscoAssignedUserDisplayName)+'</small>':'')):'<em class=""text-muted"">Not assigned</em>';
|
||||
var adUser=r.ADManagedByUserId?(esc(r.ADManagedByUserId)+(r.ADManagedByDisplayName?'<br/><small class=""text-muted"">'+esc(r.ADManagedByDisplayName)+'</small>':'')):(r.FoundInAD?'<em class=""text-muted"">Empty</em>':'<em class=""text-muted"">N/A</em>');
|
||||
tb.append('<tr class=""'+(r.IsMatch?'':'warning')+'"">'+'<td>'+icon+'</td><td>'+esc(r.SerialNumber)+'</td><td>'+esc(r.ComputerName||'')+'</td><td>'+discoUser+'</td><td>'+adUser+'</td><td>'+esc(r.MismatchReason||'')+'</td></tr>');
|
||||
});
|
||||
}
|
||||
function esc(t){if(!t)return'';var d=document.createElement('div');d.appendChild(document.createTextNode(t));return d.innerHTML;}
|
||||
</script>";
|
||||
private string BuildPage(Models.DeviceComparisonSummary summary)
|
||||
{
|
||||
var pluginUrl = "/Plugin/Disco.Plugins.ADCompare";
|
||||
var sb = new StringBuilder();
|
||||
|
||||
sb.Append("<!DOCTYPE html><html><head><meta charset='utf-8'/>");
|
||||
sb.Append("<title>AD Compare - Device Managed By</title>");
|
||||
sb.Append("<style>");
|
||||
sb.Append("body{font-family:Segoe UI,Arial,sans-serif;margin:20px;background:#fff;color:#333;}");
|
||||
sb.Append("h2{color:#333;border-bottom:2px solid #337ab7;padding-bottom:10px;}");
|
||||
sb.Append(".btn{display:inline-block;padding:8px 16px;font-size:14px;border:none;border-radius:4px;cursor:pointer;text-decoration:none;color:#fff;margin-right:8px;}");
|
||||
sb.Append(".btn-primary{background:#337ab7;} .btn-primary:hover{background:#286090;}");
|
||||
sb.Append(".btn-default{background:#777;} .btn-default:hover{background:#555;}");
|
||||
sb.Append(".summary{display:flex;gap:20px;margin:15px 0;padding:15px;background:#f5f5f5;border-radius:4px;flex-wrap:wrap;}");
|
||||
sb.Append(".stat{text-align:center;min-width:100px;} .stat .num{font-size:28px;font-weight:bold;} .stat .lbl{font-size:12px;color:#666;}");
|
||||
sb.Append(".green{color:#5cb85c;} .orange{color:#f0ad4e;} .red{color:#d9534f;} .blue{color:#5bc0de;} .grey{color:#999;}");
|
||||
sb.Append("table{width:100%;border-collapse:collapse;margin-top:10px;} th{background:#f5f5f5;text-align:left;padding:8px;border-bottom:2px solid #ddd;}");
|
||||
sb.Append("td{padding:8px;border-bottom:1px solid #eee;} tr:hover{background:#f9f9f9;}");
|
||||
sb.Append(".warn-row{background:#fcf8e3;} .danger-row{background:#f2dede;}");
|
||||
sb.Append(".badge{display:inline-block;padding:2px 8px;border-radius:10px;font-size:11px;color:#fff;}");
|
||||
sb.Append(".badge-warn{background:#f0ad4e;} .badge-err{background:#d9534f;}");
|
||||
sb.Append(".muted{color:#999;font-style:italic;} .small{font-size:12px;color:#888;}");
|
||||
sb.Append("</style></head><body>");
|
||||
|
||||
sb.Append("<h2>AD Compare — Device Managed By</h2>");
|
||||
sb.Append("<p>Compares the AD computer <strong>Managed By</strong> field against the Disco <strong>Assigned User</strong>.</p>");
|
||||
|
||||
sb.Append("<div style='margin:15px 0;'>");
|
||||
sb.Append("<a href='" + pluginUrl + "/Run' class='btn btn-primary'>↻ Run Comparison</a>");
|
||||
if (summary != null)
|
||||
{
|
||||
sb.Append("<a href='" + pluginUrl + "/Export' class='btn btn-default'>⬇ Export Mismatches CSV</a>");
|
||||
}
|
||||
sb.Append("</div>");
|
||||
|
||||
if (summary != null)
|
||||
{
|
||||
sb.Append("<div class='summary'>");
|
||||
sb.Append("<div class='stat'><div class='num'>" + summary.TotalDevices + "</div><div class='lbl'>Total Devices</div></div>");
|
||||
sb.Append("<div class='stat'><div class='num green'>" + summary.DevicesMatched + "</div><div class='lbl'>Matched</div></div>");
|
||||
sb.Append("<div class='stat'><div class='num orange'>" + summary.DevicesMismatched + "</div><div class='lbl'>Mismatched</div></div>");
|
||||
sb.Append("<div class='stat'><div class='num red'>" + summary.DevicesNotInAD + "</div><div class='lbl'>Not in AD</div></div>");
|
||||
sb.Append("<div class='stat'><div class='num blue'>" + summary.DevicesNoManagedBy + "</div><div class='lbl'>No ManagedBy</div></div>");
|
||||
sb.Append("<div class='stat'><div class='num grey'>" + summary.DevicesNoAssignment + "</div><div class='lbl'>No Assignment</div></div>");
|
||||
sb.Append("</div>");
|
||||
|
||||
var mismatches = summary.Results.Where(r => !r.IsMatch).ToList();
|
||||
|
||||
if (mismatches.Count == 0)
|
||||
{
|
||||
sb.Append("<div style='padding:20px;background:#dff0d8;border-radius:4px;margin-top:15px;'>");
|
||||
sb.Append("<strong>✓ All devices match!</strong> Every device's Disco assigned user matches the AD Managed By field.");
|
||||
sb.Append("</div>");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append("<h3 style='margin-top:20px;'>Mismatches (" + mismatches.Count + ")</h3>");
|
||||
sb.Append("<table><thead><tr>");
|
||||
sb.Append("<th>Status</th><th>Serial Number</th><th>Computer Name</th>");
|
||||
sb.Append("<th>Disco Assigned User</th><th>AD Managed By</th><th>Reason</th>");
|
||||
sb.Append("</tr></thead><tbody>");
|
||||
|
||||
foreach (var r in mismatches)
|
||||
{
|
||||
string rowClass = r.FoundInAD ? "warn-row" : "danger-row";
|
||||
string badge = r.FoundInAD
|
||||
? "<span class='badge badge-warn'>Mismatch</span>"
|
||||
: "<span class='badge badge-err'>Not in AD</span>";
|
||||
|
||||
string discoUser = !string.IsNullOrEmpty(r.DiscoAssignedUserId)
|
||||
? H(r.DiscoAssignedUserId) + (r.DiscoAssignedUserDisplayName != null
|
||||
? "<br/><span class='small'>" + H(r.DiscoAssignedUserDisplayName) + "</span>" : "")
|
||||
: "<span class='muted'>Not assigned</span>";
|
||||
|
||||
string adUser;
|
||||
if (!string.IsNullOrEmpty(r.ADManagedByUserId))
|
||||
{
|
||||
adUser = H(r.ADManagedByUserId);
|
||||
if (r.ADManagedByDisplayName != null)
|
||||
adUser = adUser + "<br/><span class='small'>" + H(r.ADManagedByDisplayName) + "</span>";
|
||||
}
|
||||
else if (r.FoundInAD)
|
||||
{
|
||||
adUser = "<span class='muted'>Empty</span>";
|
||||
}
|
||||
else
|
||||
{
|
||||
adUser = "<span class='muted'>N/A</span>";
|
||||
}
|
||||
|
||||
sb.Append("<tr class='" + rowClass + "'>");
|
||||
sb.Append("<td>" + badge + "</td>");
|
||||
sb.Append("<td>" + H(r.SerialNumber) + "</td>");
|
||||
sb.Append("<td>" + H(r.ComputerName) + "</td>");
|
||||
sb.Append("<td>" + discoUser + "</td>");
|
||||
sb.Append("<td>" + adUser + "</td>");
|
||||
sb.Append("<td>" + H(r.MismatchReason) + "</td>");
|
||||
sb.Append("</tr>");
|
||||
}
|
||||
|
||||
sb.Append("</tbody></table>");
|
||||
}
|
||||
}
|
||||
|
||||
sb.Append("</body></html>");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user