mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-09 02:29:40 +00:00
Added admin dialog for tool configuration exports (#949)
This commit is contained in:
1 parent
4141e7c504
commit
88bf475847
24 files changed
+921
-102
No files matched your search
@@ -48,27 +48,22 @@
|
||||
<MudTooltip Text="@T("Edit")">
|
||||
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Edit" OnClick="@(() => this.EditChatTemplate(context))"/>
|
||||
</MudTooltip>
|
||||
@if (this.SettingsManager.ConfigurationData.App.ShowAdminSettings)
|
||||
@if (context.FileAttachments.Count == 0)
|
||||
{
|
||||
@if (context.FileAttachments.Count == 0)
|
||||
{
|
||||
<MudTooltip Text="@T("Export configuration")">
|
||||
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Dataset" OnClick="@(() => this.ExportChatTemplateWithSharedAttachmentPaths(context))"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudTooltip Text="@T("Export configuration")">
|
||||
<MudMenu Icon="@Icons.Material.Filled.Dataset" Color="Color.Info" Variant="Variant.Text">
|
||||
<MudMenuItem Icon="@Icons.Material.Filled.Link" OnClick="@(() => this.ExportChatTemplateWithSharedAttachmentPaths(context))">
|
||||
@T("Use shared attachment paths")
|
||||
</MudMenuItem>
|
||||
<MudMenuItem Icon="@Icons.Material.Filled.Folder" Disabled="@this.isPluginDirectoryDialogOpen" OnClick="@(() => this.ExportChatTemplateWithPackagedAttachments(context))">
|
||||
@T("Copy attachments into plugin")
|
||||
</MudMenuItem>
|
||||
</MudMenu>
|
||||
</MudTooltip>
|
||||
}
|
||||
<AdminExportButton OnClick="@(() => this.ExportChatTemplateWithSharedAttachmentPaths(context))" />
|
||||
}
|
||||
else if (this.SettingsManager.ConfigurationData.App.ShowAdminSettings)
|
||||
{
|
||||
<MudTooltip Text="@T("Export configuration")">
|
||||
<MudMenu Icon="@Icons.Material.Filled.Dataset" Color="Color.Info" Variant="Variant.Text">
|
||||
<MudMenuItem Icon="@Icons.Material.Filled.Link" OnClick="@(() => this.ExportChatTemplateWithSharedAttachmentPaths(context))">
|
||||
@T("Use shared attachment paths")
|
||||
</MudMenuItem>
|
||||
<MudMenuItem Icon="@Icons.Material.Filled.Folder" Disabled="@this.isPluginDirectoryDialogOpen" OnClick="@(() => this.ExportChatTemplateWithPackagedAttachments(context))">
|
||||
@T("Copy attachments into plugin")
|
||||
</MudMenuItem>
|
||||
</MudMenu>
|
||||
</MudTooltip>
|
||||
}
|
||||
<MudTooltip Text="@T("Delete")">
|
||||
<MudIconButton Color="Color.Error" Icon="@Icons.Material.Filled.Delete" OnClick="@(() => this.DeleteChatTemplate(context))"/>
|
||||
|
||||
@@ -49,11 +49,9 @@
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Edit" OnClick="() => this.EditDataSource(context)">
|
||||
@T("Edit")
|
||||
</MudButton>
|
||||
@if (this.SettingsManager.ConfigurationData.App.ShowAdminSettings && context is DataSourceERI_V1)
|
||||
@if (context is DataSourceERI_V1)
|
||||
{
|
||||
<MudTooltip Text="@T("Export configuration")">
|
||||
<MudIconButton Variant="Variant.Filled" Color="Color.Info" Icon="@Icons.Material.Filled.Dataset" OnClick="() => this.ExportDataSource(context)"/>
|
||||
</MudTooltip>
|
||||
<AdminExportButton Variant="Variant.Filled" OnClick="@(() => this.ExportDataSource(context))" />
|
||||
}
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Error" StartIcon="@Icons.Material.Filled.Delete" OnClick="() => this.DeleteDataSource(context)">
|
||||
@T("Delete")
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Business" Disabled="true"/>
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="@T("View")">
|
||||
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Visibility" OnClick="() => this.ViewProfile(context)"/>
|
||||
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Visibility" OnClick="@(() => this.ViewProfile(context))"/>
|
||||
</MudTooltip>
|
||||
</MudStack>
|
||||
}
|
||||
@@ -45,16 +45,11 @@
|
||||
{
|
||||
<MudStack Row="true" Class="mb-2 mt-2" Wrap="Wrap.Wrap">
|
||||
<MudTooltip Text="@T("Edit")">
|
||||
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Edit" OnClick="() => this.EditProfile(context)"/>
|
||||
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Edit" OnClick="@(() => this.EditProfile(context))"/>
|
||||
</MudTooltip>
|
||||
@if (this.SettingsManager.ConfigurationData.App.ShowAdminSettings)
|
||||
{
|
||||
<MudTooltip Text="@T("Export configuration")">
|
||||
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Dataset" OnClick="() => this.ExportProfile(context)"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
<AdminExportButton OnClick="@(() => this.ExportProfile(context))" />
|
||||
<MudTooltip Text="@T("Delete")">
|
||||
<MudIconButton Color="Color.Error" Icon="@Icons.Material.Filled.Delete" OnClick="() => this.DeleteProfile(context)"/>
|
||||
<MudIconButton Color="Color.Error" Icon="@Icons.Material.Filled.Delete" OnClick="@(() => this.DeleteProfile(context))"/>
|
||||
</MudTooltip>
|
||||
</MudStack>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
@using AIStudio.Tools.ToolCallingSystem
|
||||
@using AIStudio.Tools.PluginSystem
|
||||
@inherits SettingsDialogBase
|
||||
|
||||
<MudDialog>
|
||||
<TitleContent>
|
||||
<MudText Typo="Typo.h6" Class="d-flex align-center">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Dataset" Class="mr-2" />
|
||||
@T("Export tool configuration")
|
||||
</MudText>
|
||||
</TitleContent>
|
||||
<DialogContent>
|
||||
@if (this.IsAdmin)
|
||||
{
|
||||
@if (!string.IsNullOrWhiteSpace(this.message))
|
||||
{
|
||||
<MudAlert Severity="@this.messageSeverity" Class="mb-4">@this.message</MudAlert>
|
||||
}
|
||||
|
||||
@if (this.isLoading)
|
||||
{
|
||||
<MudProgressCircular Indeterminate="@true" Size="Size.Small" />
|
||||
<MudText>@T("Loading tool configuration...")</MudText>
|
||||
}
|
||||
else if (this.toolDefinition is null || this.implementation is null)
|
||||
{
|
||||
@if (string.IsNullOrWhiteSpace(this.message))
|
||||
{
|
||||
<MudText>@T("The selected tool could not be loaded.")</MudText>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText Typo="Typo.subtitle1" Class="mb-2">@this.implementation.GetDisplayName()</MudText>
|
||||
<MudText Typo="Typo.body2" Class="mb-4">
|
||||
@T("Export saved settings as Lua code for your configuration plugin. You can combine exports and adapt the code before deploying it.")
|
||||
</MudText>
|
||||
|
||||
@if (this.areas.Count > 0)
|
||||
{
|
||||
<MudPaper Class="pa-3 mb-4 border-dashed border rounded-lg">
|
||||
<MudText Typo="Typo.subtitle2">@T("Settings to include")</MudText>
|
||||
@if (this.areas.Count > 1)
|
||||
{
|
||||
<MudCheckBox T="bool" Label="@T("Select all")" Value="@this.AllAreasSelected" ValueChanged="@this.SelectAllAreas" Disabled="@this.isExporting" />
|
||||
}
|
||||
@foreach (var area in this.areas)
|
||||
{
|
||||
<MudCheckBox @key="area.Id" T="bool" Label="@area.Label" Value="@this.selectedAreaIds.Contains(area.Id)" ValueChanged="@(selected => this.SelectArea(area.Id, selected))" Disabled="@this.isExporting" />
|
||||
}
|
||||
<MudText Typo="Typo.caption">@T("Each area is independent. Select general settings separately if you want to include them.")</MudText>
|
||||
</MudPaper>
|
||||
}
|
||||
|
||||
<MudSelect T="ToolSettingsExportMode" Label="@T("Export mode")" @bind-Value="this.mode" Variant="Variant.Outlined" Class="mb-4" Disabled="@this.isExporting" HelperText="@T("This choice applies to settings other than secrets and the minimum provider confidence.")">
|
||||
<MudSelectItem T="ToolSettingsExportMode" Value="@ToolSettingsExportMode.LOCKED">@T("Locked settings")</MudSelectItem>
|
||||
<MudSelectItem T="ToolSettingsExportMode" Value="@ToolSettingsExportMode.DEFAULT">@T("Editable defaults")</MudSelectItem>
|
||||
</MudSelect>
|
||||
|
||||
@if (this.WarnAboutEmptyLockedSettings)
|
||||
{
|
||||
<MudAlert Severity="Severity.Warning" Class="mb-4">
|
||||
@string.Format(T("{0} of the selected settings are empty and are exported as empty locked values. Users cannot change a locked setting, so an empty required one leaves the tool unusable. Deselect the areas you have not configured, or export them as editable defaults."), this.EmptySelectedFieldCount)
|
||||
</MudAlert>
|
||||
}
|
||||
|
||||
<MudPaper Class="pa-3 mb-4 border-dashed border rounded-lg">
|
||||
<MudCheckBox T="bool" Label="@T("Include encrypted API keys and other secrets")" @bind-Value="this.includeSecrets" Disabled="@(this.isExporting || !this.CanIncludeSecrets)" />
|
||||
@if (PluginFactory.EnterpriseEncryption?.IsAvailable is not true)
|
||||
{
|
||||
<MudText Typo="Typo.body2">@T("No enterprise encryption secret is configured. API keys and other secrets cannot be exported.")</MudText>
|
||||
}
|
||||
else if (!this.HasSelectedSecrets)
|
||||
{
|
||||
<MudText Typo="Typo.body2">@T("The selected areas contain no configured API keys or other secrets.")</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText Typo="Typo.body2">@T("Secrets are always exported as locked settings. Recipients need the same enterprise encryption secret to use them.")</MudText>
|
||||
}
|
||||
</MudPaper>
|
||||
|
||||
<MudPaper Class="pa-3 mb-4 border-dashed border rounded-lg">
|
||||
<MudCheckBox T="bool" Label="@T("Include minimum provider confidence")" @bind-Value="this.includeMinimumProviderConfidence" Disabled="@this.isExporting" />
|
||||
<MudText Typo="Typo.body2" Class="mb-2">@string.Format(T("Current requirement: {0}"), this.GetMinimumProviderConfidenceName())</MudText>
|
||||
<MudText Typo="Typo.body2">@T("This setting is always locked and applies to the entire tool. The configuration plugin locks minimum provider confidence levels together for all tools in its confidence table.")</MudText>
|
||||
</MudPaper>
|
||||
}
|
||||
}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Close" Variant="Variant.Text">@T("Cancel")</MudButton>
|
||||
<MudButton OnClick="@this.Export" Variant="Variant.Filled" StartIcon="@Icons.Material.Filled.ContentCopy" Disabled="@(!this.CanExport)">
|
||||
@T("Export to clipboard")
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
@@ -0,0 +1,204 @@
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
using AIStudio.Tools.ToolCallingSystem;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Dialogs.Settings;
|
||||
|
||||
public partial class ToolSettingsExportDialog : SettingsDialogBase
|
||||
{
|
||||
[Parameter]
|
||||
public string ToolId { get; set; } = string.Empty;
|
||||
|
||||
[Inject]
|
||||
private ToolRegistry ToolRegistry { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private ToolSettingsService ToolSettingsService { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private ILogger<ToolSettingsExportDialog> Logger { get; init; } = null!;
|
||||
|
||||
private ToolDefinition? toolDefinition;
|
||||
private IToolImplementation? implementation;
|
||||
private IReadOnlyList<ExportableSettings> areas = [];
|
||||
private HashSet<string> selectedAreaIds = new(StringComparer.Ordinal);
|
||||
private HashSet<string> configuredSecretFields = new(StringComparer.Ordinal);
|
||||
private HashSet<string> emptyFieldNames = new(StringComparer.Ordinal);
|
||||
private ToolSettingsExportMode mode = ToolSettingsExportMode.LOCKED;
|
||||
private bool includeSecrets;
|
||||
private bool includeMinimumProviderConfidence = true;
|
||||
private bool isLoading = true;
|
||||
private bool isExporting;
|
||||
private bool isDisposed;
|
||||
private string message = string.Empty;
|
||||
private Severity messageSeverity = Severity.Error;
|
||||
|
||||
private bool IsAdmin => this.SettingsManager.ConfigurationData.App.ShowAdminSettings;
|
||||
|
||||
private bool AllAreasSelected => this.areas.Count > 0 && this.areas.All(area => this.selectedAreaIds.Contains(area.Id));
|
||||
|
||||
private bool HasSelectedSecrets => this.areas.Any(area => this.selectedAreaIds.Contains(area.Id) && area.FieldNames.Any(this.configuredSecretFields.Contains));
|
||||
|
||||
private bool CanIncludeSecrets => this.HasSelectedSecrets && PluginFactory.EnterpriseEncryption?.IsAvailable is true;
|
||||
|
||||
/// <summary>
|
||||
/// How many of the selected settings hold no value, counting a field shared by two areas once.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Saving a tool's settings writes every field of its schema, empty ones included, so an area
|
||||
/// the administrator never filled in still exports. Locked, those empty values are what the
|
||||
/// recipient is left with and cannot change, which is worth saying before the export.
|
||||
/// </remarks>
|
||||
private int EmptySelectedFieldCount => this.areas
|
||||
.Where(area => this.selectedAreaIds.Contains(area.Id))
|
||||
.SelectMany(area => area.FieldNames)
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.Count(this.emptyFieldNames.Contains);
|
||||
|
||||
private bool WarnAboutEmptyLockedSettings => this.mode is ToolSettingsExportMode.LOCKED && this.EmptySelectedFieldCount > 0;
|
||||
|
||||
private bool CanExport => this.IsAdmin && !this.isLoading && !this.isExporting && !this.isDisposed &&
|
||||
this.toolDefinition is not null && this.implementation is not null && (this.selectedAreaIds.Count > 0 || this.includeMinimumProviderConfidence);
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
if (!this.IsAdmin)
|
||||
{
|
||||
this.Close();
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
this.toolDefinition = this.ToolRegistry.GetDefinition(this.ToolId);
|
||||
if (this.toolDefinition is null)
|
||||
return;
|
||||
|
||||
this.implementation = this.ToolRegistry.GetImplementation(this.toolDefinition.ImplementationKey);
|
||||
if (this.implementation is null)
|
||||
return;
|
||||
|
||||
this.areas = this.implementation.GetExportableSettings(this.toolDefinition);
|
||||
this.selectedAreaIds = this.areas.Select(area => area.Id).ToHashSet(StringComparer.Ordinal);
|
||||
|
||||
// Retain only field names, never the values themselves, so no plaintext secret lives
|
||||
// in this component. ExportAsync reads effective settings again when the
|
||||
// administrator exports.
|
||||
var values = await this.ToolSettingsService.GetSettingsAsync(this.toolDefinition);
|
||||
this.configuredSecretFields = this.toolDefinition.SettingsSchema.Properties
|
||||
.Where(property => property.Value.Secret && values.TryGetValue(property.Key, out var value) && !string.IsNullOrWhiteSpace(value))
|
||||
.Select(property => property.Key)
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
|
||||
// A field the export writes as an empty value: it has to be present, because a
|
||||
// missing one is skipped rather than exported, and it has to be a non-secret,
|
||||
// because an empty secret is skipped as well.
|
||||
this.emptyFieldNames = this.toolDefinition.SettingsSchema.Properties
|
||||
.Where(property => !property.Value.Secret && values.TryGetValue(property.Key, out var value) && string.IsNullOrWhiteSpace(value))
|
||||
.Select(property => property.Key)
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// A runtime error may contain secret data, so it goes to the log for diagnosis but
|
||||
// never into the dialog:
|
||||
this.Logger.LogError(e, "Failed to load the configuration of the tool '{ToolId}' for export.", this.ToolId);
|
||||
this.toolDefinition = null;
|
||||
this.message = T("The tool configuration could not be loaded. Please close this dialog and try again.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void SelectArea(string areaId, bool selected)
|
||||
{
|
||||
if (selected)
|
||||
this.selectedAreaIds.Add(areaId);
|
||||
else
|
||||
this.selectedAreaIds.Remove(areaId);
|
||||
|
||||
this.SelectionChanged();
|
||||
}
|
||||
|
||||
private void SelectAllAreas(bool selected)
|
||||
{
|
||||
this.selectedAreaIds = selected ? this.areas.Select(area => area.Id).ToHashSet(StringComparer.Ordinal) : new(StringComparer.Ordinal);
|
||||
this.SelectionChanged();
|
||||
}
|
||||
|
||||
private void SelectionChanged()
|
||||
{
|
||||
// A new selection must not keep an invisible opt-in to secrets it no longer contains.
|
||||
if (!this.CanIncludeSecrets)
|
||||
this.includeSecrets = false;
|
||||
|
||||
this.message = string.Empty;
|
||||
}
|
||||
|
||||
private string GetMinimumProviderConfidenceName()
|
||||
{
|
||||
var confidence = this.toolDefinition is null ? ConfidenceLevel.NONE : this.ToolRegistry.GetMinimumProviderConfidence(this.toolDefinition);
|
||||
return confidence is ConfidenceLevel.NONE ? T("No minimum confidence level chosen") : confidence.GetName();
|
||||
}
|
||||
|
||||
private async Task Export()
|
||||
{
|
||||
if (!this.CanExport || this.toolDefinition is null || this.implementation is null)
|
||||
return;
|
||||
|
||||
this.isExporting = true;
|
||||
this.message = string.Empty;
|
||||
this.messageSeverity = Severity.Error;
|
||||
try
|
||||
{
|
||||
var options = new ToolSettingsExportOptions
|
||||
{
|
||||
SelectedAreaIds = new HashSet<string>(this.selectedAreaIds, StringComparer.Ordinal),
|
||||
Mode = this.mode,
|
||||
IncludeSecrets = this.includeSecrets,
|
||||
IncludeMinimumProviderConfidence = this.includeMinimumProviderConfidence,
|
||||
};
|
||||
|
||||
var result = await this.ToolSettingsService.ExportAsync(this.toolDefinition, this.implementation, options);
|
||||
if (this.isDisposed || !this.IsAdmin)
|
||||
return;
|
||||
|
||||
if (!result.Success)
|
||||
{
|
||||
this.message = result.ErrorMessage;
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(result.LuaCode))
|
||||
{
|
||||
this.messageSeverity = Severity.Info;
|
||||
this.message = T("The selected areas contain no settings to export.");
|
||||
return;
|
||||
}
|
||||
|
||||
// The runtime reports clipboard success or failure. Keep the dialog open so that
|
||||
// administrators can retry or export another selection from the same tool.
|
||||
await this.RustService.CopyText2Clipboard(result.LuaCode);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.Logger.LogError(e, "Failed to export the configuration of the tool '{ToolId}'.", this.ToolId);
|
||||
this.message = T("The tool configuration could not be exported. Please try again.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.isExporting = false;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void DisposeResources()
|
||||
{
|
||||
this.isDisposed = true;
|
||||
base.DisposeResources();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user