feature: computer name template testing

This commit is contained in:
Gary Sharp
2023-11-24 11:57:13 +11:00
parent a2aaa4c913
commit b576aec641
16 changed files with 1036 additions and 364 deletions
@@ -2,16 +2,20 @@
using Disco.Services;
using Disco.Services.Authorization;
using Disco.Services.Devices.ManagedGroups;
using Disco.Services.Expressions;
using Disco.Services.Interop.ActiveDirectory;
using Disco.Services.Plugins;
using Disco.Services.Plugins.Features.CertificateAuthorityProvider;
using Disco.Services.Plugins.Features.CertificateProvider;
using Disco.Services.Plugins.Features.WirelessProfileProvider;
using Disco.Services.Tasks;
using Disco.Services.Users;
using Disco.Services.Web;
using Disco.Web.Areas.API.Models.DeviceModel;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Web.Mvc;
namespace Disco.Web.Areas.API.Controllers
@@ -27,7 +31,6 @@ namespace Disco.Web.Areas.API.Controllers
const string pWirelessProfileProviders = "wirelessprofileproviders";
const string pOrganisationalUnit = "organisationalunit";
const string pDefaultOrganisationAddress = "defaultorganisationaddress";
const string pComputerNameTemplate = "computernametemplate";
const string pEnforceComputerNameConvention = "enforcecomputernameconvention";
const string pEnforceOrganisationalUnit = "enforceorganisationalunit";
const string pProvisionADAccount = "provisionadaccount";
@@ -37,7 +40,7 @@ namespace Disco.Web.Areas.API.Controllers
const string pAssignedUsersLinkedGroup = "assigneduserslinkedgroup";
[DiscoAuthorize(Claims.Config.DeviceProfile.Configure)]
public virtual ActionResult Update(int id, string key, string value = null, Nullable<bool> redirect = null)
public virtual ActionResult Update(int id, string key, string value = null, bool? redirect = null)
{
Authorization.Require(Claims.Config.DeviceProfile.Configure);
@@ -79,10 +82,6 @@ namespace Disco.Web.Areas.API.Controllers
case pDefaultOrganisationAddress:
UpdateDefaultOrganisationAddress(deviceProfile, value);
break;
case pComputerNameTemplate:
Authorization.Require(Claims.Config.DeviceProfile.ConfigureComputerNameTemplate);
UpdateComputerNameTemplate(deviceProfile, value);
break;
case pEnforceComputerNameConvention:
UpdateEnforceComputerNameConvention(deviceProfile, value);
break;
@@ -129,91 +128,209 @@ namespace Disco.Web.Areas.API.Controllers
#region Update Shortcut Methods
[DiscoAuthorize(Claims.Config.DeviceProfile.Configure)]
public virtual ActionResult UpdateDescription(int id, string Description = null, Nullable<bool> redirect = null)
public virtual ActionResult UpdateDescription(int id, string Description = null, bool? redirect = null)
{
return Update(id, pDescription, Description, redirect);
}
[DiscoAuthorize(Claims.Config.DeviceProfile.Configure)]
public virtual ActionResult UpdateName(int id, string ProfileName = null, Nullable<bool> redirect = null)
public virtual ActionResult UpdateName(int id, string ProfileName = null, bool? redirect = null)
{
return Update(id, pName, ProfileName, redirect);
}
[DiscoAuthorize(Claims.Config.DeviceProfile.Configure)]
public virtual ActionResult UpdateShortName(int id, string ShortName = null, Nullable<bool> redirect = null)
public virtual ActionResult UpdateShortName(int id, string ShortName = null, bool? redirect = null)
{
return Update(id, pShortName, ShortName, redirect);
}
[DiscoAuthorize(Claims.Config.DeviceProfile.Configure)]
public virtual ActionResult UpdateDistributionType(int id, string DistributionType = null, Nullable<bool> redirect = null)
public virtual ActionResult UpdateDistributionType(int id, string DistributionType = null, bool? redirect = null)
{
return Update(id, pDistributionType, DistributionType, redirect);
}
[DiscoAuthorize(Claims.Config.DeviceProfile.Configure)]
public virtual ActionResult UpdateCertificateProviders(int id, string CertificateProviders = null, Nullable<bool> redirect = null)
public virtual ActionResult UpdateCertificateProviders(int id, string CertificateProviders = null, bool? redirect = null)
{
return Update(id, pCertificateProviders, CertificateProviders, redirect);
}
[DiscoAuthorize(Claims.Config.DeviceProfile.Configure)]
public virtual ActionResult UpdateCertificateAuthorityProviders(int id, string CertificateAuthorityProviders = null, Nullable<bool> redirect = null)
public virtual ActionResult UpdateCertificateAuthorityProviders(int id, string CertificateAuthorityProviders = null, bool? redirect = null)
{
return Update(id, pCertificateAuthorityProviders, CertificateAuthorityProviders, redirect);
}
[DiscoAuthorize(Claims.Config.DeviceProfile.Configure)]
public virtual ActionResult UpdateWirelessProfileProviders(int id, string WirelessProfileProviders = null, Nullable<bool> redirect = null)
public virtual ActionResult UpdateWirelessProfileProviders(int id, string WirelessProfileProviders = null, bool? redirect = null)
{
return Update(id, pWirelessProfileProviders, WirelessProfileProviders, redirect);
}
[DiscoAuthorize(Claims.Config.DeviceProfile.Configure)]
public virtual ActionResult UpdateOrganisationalUnit(int id, string OrganisationalUnit = null, Nullable<bool> redirect = null)
public virtual ActionResult UpdateOrganisationalUnit(int id, string OrganisationalUnit = null, bool? redirect = null)
{
return Update(id, pOrganisationalUnit, OrganisationalUnit, redirect);
}
[DiscoAuthorize(Claims.Config.DeviceProfile.Configure)]
public virtual ActionResult UpdateDefaultOrganisationAddress(int id, string DefaultOrganisationAddress = null, Nullable<bool> redirect = null)
public virtual ActionResult UpdateDefaultOrganisationAddress(int id, string DefaultOrganisationAddress = null, bool? redirect = null)
{
return Update(id, pDefaultOrganisationAddress, DefaultOrganisationAddress, redirect);
}
[DiscoAuthorizeAll(Claims.Config.DeviceProfile.Configure, Claims.Config.DeviceProfile.ConfigureComputerNameTemplate)]
public virtual ActionResult UpdateComputerNameTemplate(int id, string ComputerNameTemplate = null, Nullable<bool> redirect = null)
[ValidateAntiForgeryToken]
public virtual ActionResult UpdateComputerNameTemplate(int id, string ComputerNameTemplate = null, bool? redirect = null)
{
return Update(id, pComputerNameTemplate, ComputerNameTemplate, redirect);
var deviceProfile = Database.DeviceProfiles.Find(id);
if (deviceProfile == null)
throw new ArgumentException("Invalid Device Profile Id", nameof(id));
if (string.IsNullOrWhiteSpace(ComputerNameTemplate))
throw new Exception("ComputerNameTemplate is Required");
var expression = new EvaluateExpressionPart(ComputerNameTemplate);
if (expression.ParseError)
{
return this.JsonStatusCode(400, expression.ParseErrorMessage);
}
deviceProfile.ComputerNameTemplate = ComputerNameTemplate;
Database.SaveChanges();
deviceProfile.ComputerNameInvalidateCache();
if (redirect.GetValueOrDefault(false))
return RedirectToAction(MVC.Config.DeviceProfile.Index(deviceProfile.Id));
else
return Json("OK");
}
[DiscoAuthorizeAll(Claims.Config.DeviceProfile.Configure, Claims.Config.DeviceProfile.ConfigureComputerNameTemplate)]
[ValidateAntiForgeryToken]
public virtual ActionResult TestComputerNameTemplate(int id, string ComputerNameTemplate = null, string UserSpecifiedDeviceSerialNumber = null)
{
Database.Configuration.LazyLoadingEnabled = true;
var deviceProfile = Database.DeviceProfiles.Find(id);
if (deviceProfile == null)
throw new ArgumentException("Invalid Device Profile Id", nameof(id));
if (string.IsNullOrWhiteSpace(ComputerNameTemplate))
throw new Exception("ComputerNameTemplate is Required");
var expression = Expression.TokenizeSingleDynamic(null, ComputerNameTemplate, 0);
if (expression.First().ParseError)
{
return this.JsonStatusCode(400, expression.First().ParseErrorMessage);
}
var result = new TestComputerNameTemplateModel()
{
DeviceProfileId = deviceProfile.Id,
ComputerNameTemplate = ComputerNameTemplate,
};
TestComputerNameTemplateModel.TestComputerNameTemplateResultModel evaluateDevice(Disco.Models.Repository.Device device)
{
var evaluatorVariables = Expression.StandardVariables(null, Database, UserService.CurrentUser, DateTime.Now, null, device);
var deviceResult = new TestComputerNameTemplateModel.TestComputerNameTemplateResultModel()
{
DeviceSerialNumber = device.SerialNumber,
DeviceComputerName = device.ComputerName,
};
try
{
var rendered = expression.EvaluateFirst<string>(device, evaluatorVariables);
deviceResult.Url = Url.Action(MVC.Device.Show(device.SerialNumber));
deviceResult.RenderedComputerName = rendered;
deviceResult.Success = true;
if (string.IsNullOrWhiteSpace(rendered))
{
deviceResult.Success = false;
deviceResult.ErrorMessage = "Rendered computer name is null or blank";
}
else
{
if (rendered.Length > 15)
{
deviceResult.Success = false;
deviceResult.ErrorMessage = "Must be no more than 15 characters";
}
var invalidCharacters = Regex.Matches(rendered, @"[^a-z0-9\-]", RegexOptions.IgnoreCase);
if (invalidCharacters.Count > 0)
{
deviceResult.Success = false;
deviceResult.ErrorMessage = $"Invalid characters: {string.Join(" ", invalidCharacters.Cast<Match>().Select(m => m.Value.Replace(" ", "{space}")).Distinct())}";
}
}
}
catch (Exception ex)
{
deviceResult.Success = false;
deviceResult.ErrorMessage = $"{ex.Message} [{ex.GetType().Name}]";
}
return deviceResult;
}
if (!string.IsNullOrWhiteSpace(UserSpecifiedDeviceSerialNumber))
{
var device = Database.Devices.FirstOrDefault(d => d.SerialNumber == UserSpecifiedDeviceSerialNumber);
if (device == null)
return this.JsonStatusCode(400, "Invalid user-specified device serial number");
result.UserSpecifiedResult = evaluateDevice(device);
}
result.RandomDeviceResults = Database.Devices
.Where(d => d.DeviceProfileId == deviceProfile.Id && d.SerialNumber != UserSpecifiedDeviceSerialNumber)
.OrderBy(d => Guid.NewGuid())
.Take(6)
.ToList()
.Select(d => evaluateDevice(d))
.ToList();
if (result.UserSpecifiedResult == null && result.RandomDeviceResults.Count > 0)
{
result.UserSpecifiedResult = result.RandomDeviceResults.First();
result.RandomDeviceResults.RemoveAt(0);
}
return Json(result);
}
[DiscoAuthorize(Claims.Config.DeviceProfile.Configure)]
public virtual ActionResult UpdateEnforceComputerNameConvention(int id, string EnforceComputerNameConvention = null, Nullable<bool> redirect = null)
public virtual ActionResult UpdateEnforceComputerNameConvention(int id, string EnforceComputerNameConvention = null, bool? redirect = null)
{
return Update(id, pEnforceComputerNameConvention, EnforceComputerNameConvention, redirect);
}
[DiscoAuthorize(Claims.Config.DeviceProfile.Configure)]
public virtual ActionResult UpdateEnforceOrganisationalUnit(int id, string EnforceOrganisationalUnit = null, Nullable<bool> redirect = null)
public virtual ActionResult UpdateEnforceOrganisationalUnit(int id, string EnforceOrganisationalUnit = null, bool? redirect = null)
{
return Update(id, pEnforceOrganisationalUnit, EnforceOrganisationalUnit, redirect);
}
[DiscoAuthorize(Claims.Config.DeviceProfile.Configure)]
public virtual ActionResult UpdateProvisionADAccount(int id, string ProvisionADAccount = null, Nullable<bool> redirect = null)
public virtual ActionResult UpdateProvisionADAccount(int id, string ProvisionADAccount = null, bool? redirect = null)
{
return Update(id, pProvisionADAccount, ProvisionADAccount, redirect);
}
[DiscoAuthorize(Claims.Config.DeviceProfile.Configure)]
public virtual ActionResult UpdateAssignedUserLocalAdmin(int id, string AssignedUserLocalAdmin = null, Nullable<bool> redirect = null)
public virtual ActionResult UpdateAssignedUserLocalAdmin(int id, string AssignedUserLocalAdmin = null, bool? redirect = null)
{
return Update(id, pAssignedUserLocalAdmin, AssignedUserLocalAdmin, redirect);
}
[DiscoAuthorize(Claims.Config.DeviceProfile.Configure)]
public virtual ActionResult UpdateAllowUntrustedReimageJobEnrolment(int id, string AllowUntrustedReimageJobEnrolment = null, Nullable<bool> redirect = null)
public virtual ActionResult UpdateAllowUntrustedReimageJobEnrolment(int id, string AllowUntrustedReimageJobEnrolment = null, bool? redirect = null)
{
return Update(id, pAllowUntrustedReimageJobEnrolment, AllowUntrustedReimageJobEnrolment, redirect);
}
@@ -444,20 +561,6 @@ namespace Disco.Web.Areas.API.Controllers
}
}
private void UpdateComputerNameTemplate(DeviceProfile deviceProfile, string ComputerNameTemplate)
{
Authorization.Require(Claims.Config.DeviceProfile.ConfigureComputerNameTemplate);
if (string.IsNullOrWhiteSpace(ComputerNameTemplate))
throw new Exception("ComputerNameTemplate is Required");
deviceProfile.ComputerNameTemplate = ComputerNameTemplate;
Database.SaveChanges();
deviceProfile.ComputerNameInvalidateCache();
}
private void UpdateDefaultOrganisationAddress(DeviceProfile deviceProfile, string DefaultOrganisationAddress)
{
if (string.IsNullOrEmpty(DefaultOrganisationAddress))
@@ -593,7 +696,7 @@ namespace Disco.Web.Areas.API.Controllers
#region Actions
[DiscoAuthorize(Claims.Config.DeviceProfile.Delete)]
public virtual ActionResult Delete(int id, Nullable<bool> redirect = false)
public virtual ActionResult Delete(int id, bool? redirect = false)
{
try
{
@@ -623,7 +726,7 @@ namespace Disco.Web.Areas.API.Controllers
#region Defaults
[DiscoAuthorize(Claims.Config.DeviceProfile.ConfigureDefaults)]
public virtual ActionResult Default(int id, Nullable<bool> redirect = null)
public virtual ActionResult Default(int id, bool? redirect = null)
{
try
{
@@ -649,7 +752,7 @@ namespace Disco.Web.Areas.API.Controllers
}
[DiscoAuthorize(Claims.Config.DeviceProfile.ConfigureDefaults)]
public virtual ActionResult DefaultAddDeviceOffline(int id, Nullable<bool> redirect = false)
public virtual ActionResult DefaultAddDeviceOffline(int id, bool? redirect = false)
{
try
{
@@ -69,5 +69,20 @@ namespace Disco.Web.Areas.API.Controllers
return Json(results, JsonRequestBehavior.AllowGet);
}
[DiscoAuthorize(Claims.Device.Search)]
public virtual ActionResult Devices(string Term, int Limit = 15)
{
if (string.IsNullOrWhiteSpace(Term))
throw new ArgumentNullException("Term", "The search query term is required");
if (Term.Length < 2)
throw new ArgumentException("The search query term must be at least two characters", "Term");
if (Limit < 1)
throw new ArgumentException("The search query limit cannot be less than 1", "Limit");
var results = Search.SearchDevices(Database, Term, Limit);
return Json(results, JsonRequestBehavior.AllowGet);
}
}
}
@@ -0,0 +1,23 @@
using System.Collections.Generic;
namespace Disco.Web.Areas.API.Models.DeviceModel
{
public class TestComputerNameTemplateModel
{
public int DeviceProfileId { get; set; }
public string ComputerNameTemplate { get; set; }
public TestComputerNameTemplateResultModel UserSpecifiedResult { get; set; }
public List<TestComputerNameTemplateResultModel> RandomDeviceResults { get; set; }
public class TestComputerNameTemplateResultModel
{
public string DeviceSerialNumber { get; set; }
public bool Success { get; set; }
public string ErrorMessage { get; set; }
public string DeviceComputerName { get; set; }
public string RenderedComputerName { get; set; }
public string Url { get; set; }
}
}
}
@@ -295,47 +295,228 @@
Template Expression:
</th>
<td>
<div id="displayComputerNameTemplate" class="code">
@if (string.IsNullOrWhiteSpace(Model.DeviceProfile.ComputerNameTemplate))
{
<span class="smallMessage">&lt;None Specified&gt;</span>
}
else
{
@Model.DeviceProfile.ComputerNameTemplate
}
</div>
@if (canConfig && canConfigExpression)
{
@Html.EditorFor(model => model.DeviceProfile.ComputerNameTemplate)
@AjaxHelpers.AjaxSave()
@AjaxHelpers.AjaxLoader()
<a id="expressionBrowserAnchor" href="@(Url.Action(MVC.Config.DocumentTemplate.ExpressionBrowser()))">&nbsp;</a>
<a id="changeComputerNameTemplate" href="#" class="button small">Change</a>@AjaxHelpers.AjaxLoader()
<div id="dialogComputerNameTemplate" title="Computer Name Template" class="dialog">
@using (Html.BeginForm(MVC.API.DeviceProfile.UpdateComputerNameTemplate(Model.DeviceProfile.Id, null, false)))
{
@Html.AntiForgeryToken()
@Html.TextArea("ComputerNameTemplate", Model.DeviceProfile.ComputerNameTemplate, new { spellcheck = "false", required = "required" })
}
<div class="hidden info-box error code whitespace-pre-wrap">
</div>
<div class="test hidden">
@using (Html.BeginForm(MVC.API.DeviceProfile.TestComputerNameTemplate(Model.DeviceProfile.Id)))
{
@Html.AntiForgeryToken();
<table class="genericData">
<thead>
<tr>
<th>Serial Number</th>
<th>Current</th>
<th>Result</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<input id="ComputerNameTemplateTestSearch" name="UserSpecifiedDeviceSerialNumber" type="text" placeholder="Device Serial Number" spellcheck="false" />
</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
</tr>
</tbody>
</table>
}
</div>
<button id="ComputerNameTemplateTest" class="button small" type="button">Test Template</button>@AjaxHelpers.AjaxLoader()
</div>
<script type="text/javascript">
$(function () {
var field = $('#DeviceProfile_ComputerNameTemplate');
var fieldOriginalWidth, fieldOriginalHeight;
$(() => {
let $dialog = null;
let $textarea = null;
let currentValue = null;
document.DiscoFunctions.PropertyChangeHelper(
field,
'None',
'@Url.Action(MVC.API.DeviceProfile.UpdateComputerNameTemplate(Model.DeviceProfile.Id))',
'ComputerNameTemplate'
);
$('#changeComputerNameTemplate').click(e => {
e.preventDefault();
field.focus(function () {
fieldOriginalWidth = field.width();
fieldOriginalHeight = field.height();
field.css('overflow', 'visible').animate({ width: field.parent().width() - 52, height: 75 }, 200);
}).blur(function () {
field.css('overflow', 'hidden').animate({ width: fieldOriginalWidth, height: fieldOriginalHeight }, 200);
}).attr('placeholder', 'None').attr('spellcheck', 'false');
if (!$dialog) {
$textarea = $('#ComputerNameTemplate');
currentValue = $textarea.val();
$dialog = $('#dialogComputerNameTemplate').dialog({
autoOpen: false,
buttons: null,
draggable: false,
modal: true,
resizable: false,
width: 700,
buttons: {
"Cancel": () => {
$textarea.val(currentValue);
$dialog.find('.error').addClass('hidden');
$dialog.dialog("close");
},
"Expression Browser": () => {
window.open('@(Url.Action(MVC.Config.DocumentTemplate.ExpressionBrowser()))', '_blank');
},
"Save": () => {
const $form = $textarea.closest('form');
$(this).find('.error').addClass('hidden');
if ($form[0].reportValidity()) {
const body = new FormData($form[0]);
const url = $form.attr('action');
const $ajaxLoading = $('#changeComputerNameTemplate').nextAll('.ajaxLoading').first();
$ajaxLoading.show();
$dialog.dialog('close');
fetch(url, {
method: 'POST',
body: body
}).then(response => {
if (response.ok) {
currentValue = $textarea.val();
const $display = $('#displayComputerNameTemplate');
if (currentValue) {
$display.text(currentValue);
} else {
$display.html('<span class="smallMessage">&lt;None Specified&gt;</span>');
}
$ajaxLoading.hide().next('.ajaxOk').show().delay('fast').fadeOut('slow');
} else if (response.status == 400) {
response.json().then(data => {
$ajaxLoading.hide();
$dialog.dialog('open');
$dialog.find('.error').text(data).removeClass('hidden');
console.error(data);
})
}
}).catch(error => {
alert('Unable to save computer name template');
$ajaxLoading.hide();
$dialog.dialog('open');
$dialog.find('.error').text('Unable to save computer name template: ' + error).removeClass('hidden');
console.error(error);
});
}
}
}
});
const $buttonTest = $dialog.find('#ComputerNameTemplateTest');
$buttonTest.click(e => {
e.preventDefault();
const $test = $dialog.find('.test');
const $ajaxLoading = $(e.currentTarget).nextAll('.ajaxLoading').first();
$ajaxLoading.show();
const $form = $test.find('form');
const url = $form.attr('action');
const body = new FormData($form[0]);
body.append('ComputerNameTemplate', $textarea.val());
fetch(url, {
method: 'POST',
body: body
}).then(response => {
if (response.ok) {
response.json().then(data => {
const $table = $test.find('table tbody');
if (data.UserSpecifiedResult) {
const result = data.UserSpecifiedResult;
const row = $table.find('tr').first();
row.find('input').val(result.DeviceSerialNumber);
const columns = row.find('td');
columns.eq(1).html('');
$('<span class="code">').text(result.DeviceComputerName)
.appendTo(columns.eq(1))
columns.eq(2).html('');
$('<span class="code">').text(result.RenderedComputerName)
.appendTo(columns.eq(2))
if (!result.Success) {
$('<div class="info-box error code whitespace-pre-wrap">').text(result.ErrorMessage).appendTo(columns[2]);
}
}
if (data.RandomDeviceResults) {
$table.find('tr').slice(1).remove();
for (var i = 0; i < data.RandomDeviceResults.length; i++) {
const result = data.RandomDeviceResults[i];
const row = $('<tr><td></td><td></td><td></td></tr>');
const columns = row.find('td');
$('<a>').attr({
href: result.Url,
target: '_blank'
}).text(result.DeviceSerialNumber)
.appendTo(columns.eq(0));
$('<span class="code">').text(result.DeviceComputerName)
.appendTo(columns.eq(1))
$('<span class="code">').text(result.RenderedComputerName)
.appendTo(columns.eq(2))
if (!result.Success) {
$('<div class="info-box error code whitespace-pre-wrap">').text(result.ErrorMessage).appendTo(columns.eq(2));
}
row.appendTo($table);
}
}
$test.removeClass('hidden');
$ajaxLoading.hide().next('.ajaxOk').show().delay('fast').fadeOut('slow');
});
} else if (response.status == 400) {
response.json().then(data => {
$ajaxLoading.hide();
$test.addClass('hidden');
$dialog.find('.error').text(data).removeClass('hidden');
console.error(data);
})
}
}).catch(error => {
$ajaxLoading.hide();
$test.addClass('hidden');
$dialog.find('.error').text('Unable to test computer name template: ' + error).removeClass('hidden');
console.error(error);
});
return false;
})
const $testSearchText = $dialog.find('#ComputerNameTemplateTestSearch');
$testSearchText.autocomplete({
source: '@(Url.Action(MVC.API.Search.Devices()))',
minLength: 2,
focus: function (e, ui) {
$testSearchText.val(ui.item.Id);
return false;
},
select: function (e, ui) {
$testSearchText.val(ui.item.Id).blur();
$buttonTest.click();
return false;
}
}).data('ui-autocomplete')._renderItem = function (ul, item) {
return $('<li>')
.data('item.autocomplete', item)
.append($('<a>').text(item.Description))
.appendTo(ul);
};
}
$dialog.dialog('open');
return false;
});
});
</script>
}
else
{
<div id="displayComputerNameTemplate" class="code">
@if (string.IsNullOrWhiteSpace(Model.DeviceProfile.ComputerNameTemplate))
{
<span class="smallMessage">&lt;None Specified&gt;</span>
}
else
{
@Model.DeviceProfile.ComputerNameTemplate
}
</div>
}
<div style="margin-top: 8px;">
@if (canConfig)
{
File diff suppressed because it is too large Load Diff