Adds a new section "Scripting" in the server UI where PowerShell modules can be defined. These can be used for defining a library of functions that can be used in any script. These modules are automatically synced to the launcher and imported upon script execution.
388 lines
16 KiB
Text
388 lines
16 KiB
Text
@page "/Scripting/Modules/Add"
|
|
@page "/Scripting/Modules/{Name}"
|
|
@attribute [Authorize(Roles = RoleService.AdministratorRoleName)]
|
|
@inject ModuleService ModuleService
|
|
@inject NavigationManager NavigationManager
|
|
@inject IMessageService MessageService
|
|
@inject ILogger<Edit> Logger
|
|
|
|
<style>
|
|
.module-editor .monaco-editor,
|
|
.module-editor .monaco-editor-container {
|
|
min-height: 400px;
|
|
}
|
|
</style>
|
|
|
|
<PageHeader Title="@(IsNew ? "Add Module" : Module.Name)">
|
|
<PageHeaderExtra>
|
|
<Flex Gap="FlexGap.Small">
|
|
<Button OnClick="@(() => NavigationManager.NavigateTo("/Scripting/Modules"))">Back</Button>
|
|
<Button OnClick="Save" Type="@ButtonType.Primary">Save</Button>
|
|
</Flex>
|
|
</PageHeaderExtra>
|
|
</PageHeader>
|
|
|
|
<PageContent Class="module-editor">
|
|
<Form Model="@Module" Layout="@FormLayout.Vertical">
|
|
<Tabs ActiveKey="@_activeTab" ActiveKeyChanged="OnTabChanged">
|
|
<TabPane Key="functions" Tab="Functions">
|
|
@if (_activeTab == "functions")
|
|
{
|
|
<GridRow Gutter="16">
|
|
<GridCol Span="6">
|
|
<Flex Style="margin-bottom: 16px;">
|
|
<Button Type="@ButtonType.Primary" Icon="@IconType.Outline.Plus" OnClick="AddFunction" Block>Add Function</Button>
|
|
</Flex>
|
|
|
|
<Tree TItem="ModuleFunction" SelectedKeys="@_selectedFunctionKeys" OnClick="OnFunctionTreeClick">
|
|
<ChildContent>
|
|
<TreeNode Title="Public" Selectable="false" Expanded>
|
|
<ChildContent>
|
|
@foreach (var function in Module.Functions.Where(f => f.Visibility == FunctionVisibility.Public))
|
|
{
|
|
<TreeNode Key="@function.Id.ToString()" Title="@FunctionTitle(function)" DataItem="function" IsLeaf />
|
|
}
|
|
</ChildContent>
|
|
</TreeNode>
|
|
<TreeNode Title="Private" Selectable="false" Expanded>
|
|
<ChildContent>
|
|
@foreach (var function in Module.Functions.Where(f => f.Visibility == FunctionVisibility.Private))
|
|
{
|
|
<TreeNode Key="@function.Id.ToString()" Title="@FunctionTitle(function)" DataItem="function" IsLeaf />
|
|
}
|
|
</ChildContent>
|
|
</TreeNode>
|
|
</ChildContent>
|
|
</Tree>
|
|
</GridCol>
|
|
|
|
<GridCol Span="18">
|
|
@if (_selectedFunction != null)
|
|
{
|
|
<FormItem Label="Function Name">
|
|
<Flex Gap="FlexGap.Small" Align="FlexAlign.Center">
|
|
<Flex Gap="FlexGap.Small" Align="FlexAlign.Center" Style="flex-grow: 1;">
|
|
<Select TItem="VerbOption" TItemValue="string"
|
|
DataSource="_verbOptions"
|
|
@bind-Value="Verb"
|
|
ValueName="@nameof(VerbOption.Verb)"
|
|
LabelName="@nameof(VerbOption.Verb)"
|
|
GroupName="@nameof(VerbOption.Group)"
|
|
SortByGroup="SortDirection.Ascending"
|
|
SortByLabel="SortDirection.Ascending"
|
|
ShowSearch
|
|
Style="width: 160px;" />
|
|
<span>-</span>
|
|
<Input @bind-Value="Noun" Placeholder="Noun" Style="flex-grow: 1;" />
|
|
</Flex>
|
|
<Select @bind-Value="_selectedFunction.Visibility" TItemValue="FunctionVisibility" TItem="FunctionVisibility" Style="width: 140px;">
|
|
<SelectOptions>
|
|
<SelectOption TItemValue="FunctionVisibility" TItem="FunctionVisibility" Value="FunctionVisibility.Public" Label="Public" />
|
|
<SelectOption TItemValue="FunctionVisibility" TItem="FunctionVisibility" Value="FunctionVisibility.Private" Label="Private" />
|
|
</SelectOptions>
|
|
</Select>
|
|
<Popconfirm OnConfirm="() => DeleteFunction(_selectedFunction)" Title="Are you sure you want to delete this function?">
|
|
<Button Icon="@IconType.Outline.Delete" Danger />
|
|
</Popconfirm>
|
|
</Flex>
|
|
</FormItem>
|
|
|
|
@if (!string.IsNullOrWhiteSpace(_selectedFunction.Name) && !ModuleService.IsValidFunctionName(_selectedFunction.Name))
|
|
{
|
|
<Alert Type="@AlertType.Error" Message="Function names must use Verb-Noun format (e.g. Get-Thing)." ShowIcon="true" Style="margin-bottom: 16px;" />
|
|
}
|
|
else if (!string.IsNullOrWhiteSpace(_selectedFunction.Name) && !ModuleService.IsApprovedVerb(_selectedFunction.Name))
|
|
{
|
|
<Alert Type="@AlertType.Warning" Message="That verb is not an approved PowerShell verb. Run Get-Verb to see the approved list." ShowIcon="true" Style="margin-bottom: 16px;" />
|
|
}
|
|
|
|
<PowerShellScriptEditor @key="_selectedFunction" @bind-Value="_selectedFunction.Content" OnSave="Save" Height="400" />
|
|
}
|
|
</GridCol>
|
|
</GridRow>
|
|
}
|
|
</TabPane>
|
|
<TabPane Key="manifest" Tab="Manifest">
|
|
<FormItem Label="Module Name">
|
|
<Input @bind-Value="@context.Name" Disabled="@(!IsNew)" />
|
|
</FormItem>
|
|
|
|
<Flex Justify="FlexJustify.FlexEnd" Align="FlexAlign.Center" Gap="FlexGap.Small" Style="margin-bottom: 16px;">
|
|
<span>Advanced</span>
|
|
<Switch Checked="@_advancedManifest" OnChange="OnManifestModeChanged" />
|
|
</Flex>
|
|
|
|
@if (_activeTab == "manifest" && _advancedManifest)
|
|
{
|
|
<CodeInput @bind-Value="@context.Manifest" Language="Languages.PowerShell" OnSave="Save" Height="400" />
|
|
}
|
|
else if (_activeTab == "manifest")
|
|
{
|
|
<Form Model="@_manifest" Layout="@FormLayout.Vertical" Context="manifestContext">
|
|
<FormItem Label="Module Version">
|
|
<Input @bind-Value="@_manifest.ModuleVersion" />
|
|
</FormItem>
|
|
<FormItem Label="GUID">
|
|
<Input @bind-Value="@_manifest.Guid" />
|
|
</FormItem>
|
|
<FormItem Label="Author">
|
|
<Input @bind-Value="@_manifest.Author" />
|
|
</FormItem>
|
|
<FormItem Label="Description">
|
|
<TextArea @bind-Value="@_manifest.Description" />
|
|
</FormItem>
|
|
<FormItem Label="Exported Functions">
|
|
@if (ExportedFunctions.Count == 0)
|
|
{
|
|
<AntDesign.Text Type="@TextElementType.Secondary">None. Mark functions as Public to export them.</AntDesign.Text>
|
|
}
|
|
else
|
|
{
|
|
@foreach (var function in ExportedFunctions)
|
|
{
|
|
<Tag>@function</Tag>
|
|
}
|
|
}
|
|
</FormItem>
|
|
<FormItem Label="Exported Aliases">
|
|
@if (ExportedAliases.Count == 0)
|
|
{
|
|
<AntDesign.Text Type="@TextElementType.Secondary">None detected.</AntDesign.Text>
|
|
}
|
|
else
|
|
{
|
|
@foreach (var alias in ExportedAliases)
|
|
{
|
|
<Tag>@alias</Tag>
|
|
}
|
|
}
|
|
</FormItem>
|
|
</Form>
|
|
}
|
|
</TabPane>
|
|
</Tabs>
|
|
</Form>
|
|
</PageContent>
|
|
|
|
@code {
|
|
[Parameter] public string Name { get; set; }
|
|
|
|
Module Module { get; set; } = new();
|
|
ModuleManifest _manifest = new();
|
|
ModuleFunction _selectedFunction;
|
|
|
|
string _verb = string.Empty;
|
|
string _noun = string.Empty;
|
|
string[] _selectedFunctionKeys = Array.Empty<string>();
|
|
List<VerbOption> _verbOptions = new();
|
|
|
|
record VerbOption(string Verb, string Group);
|
|
|
|
string _activeTab = "functions";
|
|
bool _advancedManifest;
|
|
|
|
bool _isNew = true;
|
|
bool IsNew => _isNew;
|
|
|
|
IReadOnlyList<string> ExportedFunctions => ModuleService.GetExportedFunctions(Module);
|
|
IReadOnlyList<string> ExportedAliases => ModuleService.GetExportedAliases(Module);
|
|
|
|
protected override void OnInitialized()
|
|
{
|
|
if (!string.IsNullOrEmpty(Name))
|
|
{
|
|
var module = ModuleService.GetModule(Name);
|
|
|
|
if (module != null)
|
|
{
|
|
Module = module;
|
|
_isNew = false;
|
|
}
|
|
}
|
|
|
|
if (_isNew && Module.Functions.Count == 0)
|
|
{
|
|
Module.Functions.Add(new ModuleFunction
|
|
{
|
|
Name = "Get-Example",
|
|
Visibility = FunctionVisibility.Public,
|
|
Content = "function Get-Example {\n [CmdletBinding()]\n param()\n\n \"Hello from a LANCommander module\"\n}\n",
|
|
});
|
|
}
|
|
|
|
_verbOptions = ModuleService.GetApprovedVerbGroups()
|
|
.SelectMany(g => g.Verbs.Select(v => new VerbOption(v, g.Name)))
|
|
.ToList();
|
|
_manifest = ModuleService.ParseManifest(Module.Manifest);
|
|
SelectFunction(Module.Functions.FirstOrDefault());
|
|
}
|
|
|
|
void OnTabChanged(string key)
|
|
{
|
|
_activeTab = key;
|
|
}
|
|
|
|
string FunctionTitle(ModuleFunction function) =>
|
|
string.IsNullOrWhiteSpace(function.Name) ? "(unnamed)" : function.Name;
|
|
|
|
void AddFunction()
|
|
{
|
|
var function = new ModuleFunction
|
|
{
|
|
Name = string.Empty,
|
|
Visibility = FunctionVisibility.Public,
|
|
Content = "function Verb-Noun {\n [CmdletBinding()]\n param()\n\n}\n",
|
|
};
|
|
|
|
Module.Functions.Add(function);
|
|
SelectFunction(function);
|
|
}
|
|
|
|
void SelectFunction(ModuleFunction function)
|
|
{
|
|
_selectedFunction = function;
|
|
_selectedFunctionKeys = function == null ? Array.Empty<string>() : new[] { function.Id.ToString() };
|
|
LoadNameParts();
|
|
}
|
|
|
|
void DeleteFunction(ModuleFunction function)
|
|
{
|
|
Module.Functions.Remove(function);
|
|
|
|
if (_selectedFunction == function)
|
|
SelectFunction(Module.Functions.FirstOrDefault());
|
|
}
|
|
|
|
void LoadNameParts()
|
|
{
|
|
_verb = string.Empty;
|
|
_noun = string.Empty;
|
|
|
|
if (_selectedFunction == null || string.IsNullOrEmpty(_selectedFunction.Name))
|
|
return;
|
|
|
|
var dash = _selectedFunction.Name.IndexOf('-');
|
|
|
|
if (dash > 0)
|
|
{
|
|
_verb = _selectedFunction.Name.Substring(0, dash);
|
|
_noun = _selectedFunction.Name.Substring(dash + 1);
|
|
}
|
|
else
|
|
{
|
|
_verb = _selectedFunction.Name;
|
|
}
|
|
}
|
|
|
|
string Verb
|
|
{
|
|
get => _verb;
|
|
set { _verb = value; UpdateFunctionName(); }
|
|
}
|
|
|
|
string Noun
|
|
{
|
|
get => _noun;
|
|
set { _noun = value; UpdateFunctionName(); }
|
|
}
|
|
|
|
void OnFunctionTreeClick(TreeEventArgs<ModuleFunction> args)
|
|
{
|
|
if (args.Node.DataItem != null)
|
|
SelectFunction(args.Node.DataItem);
|
|
}
|
|
|
|
void UpdateFunctionName()
|
|
{
|
|
if (_selectedFunction == null)
|
|
return;
|
|
|
|
var name = string.IsNullOrEmpty(_noun) ? _verb : $"{_verb}-{_noun}";
|
|
|
|
_selectedFunction.Name = name;
|
|
|
|
if (!string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(_selectedFunction.Content))
|
|
{
|
|
var declaration = new System.Text.RegularExpressions.Regex(
|
|
@"function\s+[^\s{(]+", System.Text.RegularExpressions.RegexOptions.IgnoreCase);
|
|
|
|
if (declaration.IsMatch(_selectedFunction.Content))
|
|
_selectedFunction.Content = declaration.Replace(_selectedFunction.Content, $"function {name}", 1);
|
|
}
|
|
}
|
|
|
|
void OnManifestModeChanged(bool advanced)
|
|
{
|
|
if (advanced)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(_manifest.RootModule))
|
|
_manifest.RootModule = $"{Module.Name}.psm1";
|
|
|
|
Module.Manifest = ModuleService.GenerateManifest(_manifest, ExportedFunctions, ExportedAliases);
|
|
}
|
|
else
|
|
{
|
|
_manifest = ModuleService.ParseManifest(Module.Manifest);
|
|
}
|
|
|
|
_advancedManifest = advanced;
|
|
}
|
|
|
|
void Save()
|
|
{
|
|
try
|
|
{
|
|
if (string.IsNullOrWhiteSpace(Module.Name))
|
|
{
|
|
MessageService.Error("Name is required.");
|
|
return;
|
|
}
|
|
|
|
if (IsNew && ModuleService.ModuleExists(Module.Name))
|
|
{
|
|
MessageService.Error("A module with that name already exists.");
|
|
return;
|
|
}
|
|
|
|
foreach (var function in Module.Functions)
|
|
{
|
|
if (!ModuleService.IsValidFunctionName(function.Name))
|
|
{
|
|
MessageService.Error($"'{function.Name}' is not a valid function name. Use Verb-Noun format.");
|
|
return;
|
|
}
|
|
}
|
|
|
|
var duplicate = Module.Functions
|
|
.GroupBy(f => f.Name, StringComparer.OrdinalIgnoreCase)
|
|
.FirstOrDefault(g => g.Count() > 1);
|
|
|
|
if (duplicate != null)
|
|
{
|
|
MessageService.Error($"Duplicate function name '{duplicate.Key}'. Function names must be unique.");
|
|
return;
|
|
}
|
|
|
|
if (!_advancedManifest)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(_manifest.RootModule))
|
|
_manifest.RootModule = $"{Module.Name}.psm1";
|
|
|
|
Module.Manifest = ModuleService.GenerateManifest(_manifest, ExportedFunctions, ExportedAliases);
|
|
}
|
|
|
|
ModuleService.SaveModule(Module);
|
|
|
|
_isNew = false;
|
|
|
|
MessageService.Success("Module saved!");
|
|
|
|
NavigationManager.NavigateTo("/Scripting/Modules");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageService.Error("Could not save module!");
|
|
Logger?.LogError(ex, "Could not save module");
|
|
}
|
|
}
|
|
}
|