Moved the add and import buttons of the settings dialogs into their footer (#1060)

Co-authored-by: Thorsten Sommer <SommerEngineering@users.noreply.github.com>
This commit is contained in:
Paul KoudelkaandThorsten Sommer authored and GitHub committed 2026-10-20 08:35:44 +02:00
1 parent f26d132d56
commit 6b42af864c
13 files changed
+409 -320

No files matched your search

@@ -0,0 +1,29 @@
@using AIStudio.Settings.DataModel
@using AIStudio.Tools.PluginSystem
@inherits MSGComponentBase
<MudStack Row="true" Wrap="Wrap.Wrap">
<MudMenu EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="@T("Add Data Source")" Color="Color.Primary" Variant="Variant.Filled" AnchorOrigin="Origin.TopLeft" TransformOrigin="Origin.BottomLeft">
<MudMenuItem OnClick="@(() => this.AddDataSource(DataSourceType.ERI_V1))">
@T("External Data (ERI-Server v1)")
</MudMenuItem>
@*
Without an embedding provider, the dialogs of the local data sources could never be saved.
They stay listed with the reason, so nobody concludes that AI Studio cannot read local files:
*@
<MudMenuItem OnClick="@(() => this.AddDataSource(DataSourceType.LOCAL_DIRECTORY))" Disabled="@(!this.HasEmbeddingProvider)">
@(this.HasEmbeddingProvider ? T("Local Directory") : T("Local Directory, requires an embedding provider"))
</MudMenuItem>
<MudMenuItem OnClick="@(() => this.AddDataSource(DataSourceType.LOCAL_FILE))" Disabled="@(!this.HasEmbeddingProvider)">
@(this.HasEmbeddingProvider ? T("Local File") : T("Local File, requires an embedding provider"))
</MudMenuItem>
@if (PreviewFeatures.PRE_MAILBOXES_2026.IsEnabled(this.SettingsManager))
{
@* Shown with a lock rather than left out, like the locked buttons for adding providers: *@
<MudMenuItem OnClick="@this.AddMailbox" Disabled="@(!this.MayAddMailbox || !this.HasEmbeddingProvider)" Icon="@(this.MayAddMailbox ? null : Icons.Material.Filled.Lock)">
@this.MailboxMenuText
</MudMenuItem>
}
</MudMenu>
<LockableButton Text="@T("Import Data Source")" IsLocked="@(() => !this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet(PluginConfigurationObjectType.DATA_SOURCE))" Icon="@Icons.Material.Filled.FileDownload" OnClickAsync="@this.ImportERIDataSource" ButtonVariant="Variant.Outlined" NoMargin="true" />
</MudStack>
@@ -0,0 +1,209 @@
using AIStudio.Dialogs;
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
using AIStudio.Tools.PluginSystem;
using AIStudio.Tools.Services;
using Microsoft.AspNetCore.Components;
using DialogOptions = AIStudio.Dialogs.DialogOptions;
namespace AIStudio.Components;
/// <summary>
/// Adds and imports data sources and mailboxes. It is kept apart from DataSourceManagement, so that the
/// data source settings dialog can place these buttons in its footer, where they stay in reach however
/// long the table of data sources grows. The data source panel in the app settings places them below the table.
/// </summary>
public partial class DataSourceAddButtons : MSGComponentBase
{
[Inject]
private DataSourceEmbeddingService DataSourceEmbeddingService { get; init; } = null!;
[Inject]
private IDialogService DialogService { get; init; } = null!;
[Inject]
private RustService RustService { get; init; } = null!;
private bool MayAddMailbox => this.SettingsManager.ConfigurationData.App.AllowUserToAddMailbox;
/// <summary>
/// Whether an embedding provider exists, which every local data source needs.
/// </summary>
private bool HasEmbeddingProvider => !EmbeddingProviderSetup.IsMissing(this.SettingsManager);
/// <remarks>
/// The rule of the organization comes first: an embedding provider would not make the mailbox possible.
/// </remarks>
private string MailboxMenuText => !this.MayAddMailbox
? T("Mailbox (IMAP), not allowed by your organization")
: this.HasEmbeddingProvider
? T("Mailbox (IMAP)")
: T("Mailbox (IMAP), requires an embedding provider");
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
this.ApplyFilters([], [ Event.CONFIGURATION_CHANGED ]);
}
#endregion
#region Overrides of MSGComponentBase
protected override Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
{
switch (triggeredEvent)
{
// The embedding providers and the rules of the organization decide what the buttons offer:
case Event.CONFIGURATION_CHANGED:
this.StateHasChanged();
break;
}
return Task.CompletedTask;
}
#endregion
private async Task ImportERIDataSource()
{
var import = await ConfigurationSnippetImportDialog.ShowAsync(this.DialogService, PluginConfigurationObjectType.DATA_SOURCE, T("Import Data Source"));
if (import?.ConfigurationObject is not DataSourceERI_V1 dataSource)
return;
var eriDialogParameters = new DialogParameters<DataSourceERI_V1Dialog>
{
{ x => x.IsEditing, false },
{ x => x.IsImport, true },
{ x => x.DataSource, dataSource },
{ x => x.ImportedSecret, import.Secret },
};
var eriDialogReference = await this.DialogService.ShowAsync<DataSourceERI_V1Dialog>(T("Add ERI v1 Data Source"), eriDialogParameters, DialogOptions.FULLSCREEN);
var eriDialogResult = await eriDialogReference.Result;
if (eriDialogResult is null || eriDialogResult.Canceled)
return;
//
// An organization can lock the import while the user is still filling in the form, e.g.,
// with a new version of its configuration plugin. Hence, the permission is checked once
// more before the imported data source is stored:
//
if (!this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet(PluginConfigurationObjectType.DATA_SOURCE))
return;
var eriDataSource = (DataSourceERI_V1)eriDialogResult.Data!;
await this.StoreAddedDataSource(eriDataSource with { Num = this.SettingsManager.ConfigurationData.NextDataSourceNum++ });
}
private async Task AddDataSource(DataSourceType type)
{
// The menu offers no local data source without an embedding provider, but the last one may have gone since it was drawn:
if (type is DataSourceType.LOCAL_FILE or DataSourceType.LOCAL_DIRECTORY && !this.HasEmbeddingProvider)
return;
IDataSource? addedDataSource = null;
switch (type)
{
case DataSourceType.LOCAL_FILE:
var localFileDialogParameters = new DialogParameters<DataSourceLocalFileDialog>
{
{ x => x.IsEditing, false },
};
var localFileDialogReference = await this.DialogService.ShowAsync<DataSourceLocalFileDialog>(T("Add Local File as Data Source"), localFileDialogParameters, DialogOptions.FULLSCREEN);
var localFileDialogResult = await localFileDialogReference.Result;
if (localFileDialogResult is null || localFileDialogResult.Canceled)
return;
var localFile = (DataSourceLocalFile)localFileDialogResult.Data!;
localFile = localFile with { Num = this.SettingsManager.ConfigurationData.NextDataSourceNum++ };
addedDataSource = localFile;
break;
case DataSourceType.LOCAL_DIRECTORY:
var localDirectoryDialogParameters = new DialogParameters<DataSourceLocalDirectoryDialog>
{
{ x => x.IsEditing, false },
};
var localDirectoryDialogReference = await this.DialogService.ShowAsync<DataSourceLocalDirectoryDialog>(T("Add Local Directory as Data Source"), localDirectoryDialogParameters, DialogOptions.FULLSCREEN);
var localDirectoryDialogResult = await localDirectoryDialogReference.Result;
if (localDirectoryDialogResult is null || localDirectoryDialogResult.Canceled)
return;
var localDirectory = (DataSourceLocalDirectory)localDirectoryDialogResult.Data!;
localDirectory = localDirectory with { Num = this.SettingsManager.ConfigurationData.NextDataSourceNum++ };
addedDataSource = localDirectory;
break;
case DataSourceType.ERI_V1:
var eriDialogParameters = new DialogParameters<DataSourceERI_V1Dialog>
{
{ x => x.IsEditing, false },
};
var eriDialogReference = await this.DialogService.ShowAsync<DataSourceERI_V1Dialog>(T("Add ERI v1 Data Source"), eriDialogParameters, DialogOptions.FULLSCREEN);
var eriDialogResult = await eriDialogReference.Result;
if (eriDialogResult is null || eriDialogResult.Canceled)
return;
var eriDataSource = (DataSourceERI_V1)eriDialogResult.Data!;
eriDataSource = eriDataSource with { Num = this.SettingsManager.ConfigurationData.NextDataSourceNum++ };
addedDataSource = eriDataSource;
break;
}
if(addedDataSource is null)
return;
await this.StoreAddedDataSource(addedDataSource);
}
private async Task StoreAddedDataSource(IDataSource addedDataSource)
{
this.SettingsManager.ConfigurationData.DataSources.Add(addedDataSource);
await this.SettingsManager.StoreSettings();
await this.DataSourceEmbeddingService.QueueDataSourceAsync(addedDataSource);
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
}
private async Task AddMailbox()
{
if (!this.MayAddMailbox || !this.HasEmbeddingProvider)
return;
var dialogParameters = new DialogParameters<DataSourceMailboxDialog>
{
{ x => x.IsEditing, false },
};
var dialogReference = await this.DialogService.ShowAsync<DataSourceMailboxDialog>(T("Add Mailbox"), dialogParameters, DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result;
if (dialogResult is null || dialogResult.Canceled)
return;
var mailbox = (DataSourceMailbox)dialogResult.Data!;
//
// The dialog refuses to add a mailbox once the organization rules them out, but the rule
// may change while it stores the password. Then the password goes again, as far as possible:
//
if (!this.MayAddMailbox)
{
await this.RustService.DeleteSecret(mailbox, SecretStoreType.DATA_SOURCE);
return;
}
mailbox = mailbox with { Num = this.SettingsManager.ConfigurationData.NextDataSourceNum++ };
this.SettingsManager.ConfigurationData.Mailboxes.Add(mailbox);
await this.SettingsManager.StoreSettings();
await this.DataSourceEmbeddingService.QueueDataSourceAsync(mailbox);
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
}
}
@@ -1,6 +1,5 @@
@using AIStudio.Settings
@using AIStudio.Settings.DataModel
@using AIStudio.Tools.PluginSystem
@inherits MSGComponentBase
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
@@ -125,30 +124,4 @@
<MudText Typo="Typo.h6" Class="mt-3">
@T("No data sources configured yet.")
</MudText>
}
<MudStack Row="true" Wrap="Wrap.Wrap" Class="mt-3 mb-6">
<MudMenu EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="@T("Add Data Source")" Color="Color.Primary" Variant="Variant.Filled" AnchorOrigin="Origin.TopLeft" TransformOrigin="Origin.BottomLeft">
<MudMenuItem OnClick="@(() => this.AddDataSource(DataSourceType.ERI_V1))">
@T("External Data (ERI-Server v1)")
</MudMenuItem>
@*
Without an embedding provider, the dialogs of the local data sources could never be saved.
They stay listed with the reason, so nobody concludes that AI Studio cannot read local files:
*@
<MudMenuItem OnClick="@(() => this.AddDataSource(DataSourceType.LOCAL_DIRECTORY))" Disabled="@(!this.HasEmbeddingProvider)">
@(this.HasEmbeddingProvider ? T("Local Directory") : T("Local Directory, requires an embedding provider"))
</MudMenuItem>
<MudMenuItem OnClick="@(() => this.AddDataSource(DataSourceType.LOCAL_FILE))" Disabled="@(!this.HasEmbeddingProvider)">
@(this.HasEmbeddingProvider ? T("Local File") : T("Local File, requires an embedding provider"))
</MudMenuItem>
@if (PreviewFeatures.PRE_MAILBOXES_2026.IsEnabled(this.SettingsManager))
{
@* Shown with a lock rather than left out, like the locked buttons for adding providers: *@
<MudMenuItem OnClick="@this.AddMailbox" Disabled="@(!this.MayAddMailbox || !this.HasEmbeddingProvider)" Icon="@(this.MayAddMailbox ? null : Icons.Material.Filled.Lock)">
@this.MailboxMenuText
</MudMenuItem>
}
</MudMenu>
<LockableButton Text="@T("Import Data Source")" IsLocked="@(() => !this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet(PluginConfigurationObjectType.DATA_SOURCE))" Icon="@Icons.Material.Filled.FileDownload" OnClickAsync="@this.ImportERIDataSource" ButtonVariant="Variant.Outlined" NoMargin="true" />
</MudStack>
}
@@ -14,7 +14,8 @@ namespace AIStudio.Components;
/// <summary>
/// Manages the configured data sources. Used by the data source settings dialog, which the chat
/// opens, and by the data source panel in the app settings.
/// opens, and by the data source panel in the app settings. Adding and importing a data source is
/// up to DataSourceAddButtons, which each of them places on its own.
/// </summary>
public partial class DataSourceManagement : MSGComponentBase
{
@@ -38,22 +39,6 @@ public partial class DataSourceManagement : MSGComponentBase
private bool AreMailboxesEnabled => PreviewFeatures.PRE_MAILBOXES_2026.IsEnabled(this.SettingsManager);
private bool MayAddMailbox => this.SettingsManager.ConfigurationData.App.AllowUserToAddMailbox;
/// <summary>
/// Whether an embedding provider exists, which every local data source needs.
/// </summary>
private bool HasEmbeddingProvider => !EmbeddingProviderSetup.IsMissing(this.SettingsManager);
/// <remarks>
/// The rule of the organization comes first: an embedding provider would not make the mailbox possible.
/// </remarks>
private string MailboxMenuText => !this.MayAddMailbox
? T("Mailbox (IMAP), not allowed by your organization")
: this.HasEmbeddingProvider
? T("Mailbox (IMAP)")
: T("Mailbox (IMAP), requires an embedding provider");
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
@@ -189,109 +174,6 @@ public partial class DataSourceManagement : MSGComponentBase
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
}
private async Task ImportERIDataSource()
{
var import = await ConfigurationSnippetImportDialog.ShowAsync(this.DialogService, PluginConfigurationObjectType.DATA_SOURCE, T("Import Data Source"));
if (import?.ConfigurationObject is not DataSourceERI_V1 dataSource)
return;
var eriDialogParameters = new DialogParameters<DataSourceERI_V1Dialog>
{
{ x => x.IsEditing, false },
{ x => x.IsImport, true },
{ x => x.DataSource, dataSource },
{ x => x.ImportedSecret, import.Secret },
};
var eriDialogReference = await this.DialogService.ShowAsync<DataSourceERI_V1Dialog>(T("Add ERI v1 Data Source"), eriDialogParameters, DialogOptions.FULLSCREEN);
var eriDialogResult = await eriDialogReference.Result;
if (eriDialogResult is null || eriDialogResult.Canceled)
return;
//
// An organization can lock the import while the user is still filling in the form, e.g.,
// with a new version of its configuration plugin. Hence, the permission is checked once
// more before the imported data source is stored:
//
if (!this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet(PluginConfigurationObjectType.DATA_SOURCE))
return;
var eriDataSource = (DataSourceERI_V1)eriDialogResult.Data!;
await this.StoreAddedDataSource(eriDataSource with { Num = this.SettingsManager.ConfigurationData.NextDataSourceNum++ });
}
private async Task AddDataSource(DataSourceType type)
{
// The menu offers no local data source without an embedding provider, but the last one may have gone since it was drawn:
if (type is DataSourceType.LOCAL_FILE or DataSourceType.LOCAL_DIRECTORY && !this.HasEmbeddingProvider)
return;
IDataSource? addedDataSource = null;
switch (type)
{
case DataSourceType.LOCAL_FILE:
var localFileDialogParameters = new DialogParameters<DataSourceLocalFileDialog>
{
{ x => x.IsEditing, false },
};
var localFileDialogReference = await this.DialogService.ShowAsync<DataSourceLocalFileDialog>(T("Add Local File as Data Source"), localFileDialogParameters, DialogOptions.FULLSCREEN);
var localFileDialogResult = await localFileDialogReference.Result;
if (localFileDialogResult is null || localFileDialogResult.Canceled)
return;
var localFile = (DataSourceLocalFile)localFileDialogResult.Data!;
localFile = localFile with { Num = this.SettingsManager.ConfigurationData.NextDataSourceNum++ };
addedDataSource = localFile;
break;
case DataSourceType.LOCAL_DIRECTORY:
var localDirectoryDialogParameters = new DialogParameters<DataSourceLocalDirectoryDialog>
{
{ x => x.IsEditing, false },
};
var localDirectoryDialogReference = await this.DialogService.ShowAsync<DataSourceLocalDirectoryDialog>(T("Add Local Directory as Data Source"), localDirectoryDialogParameters, DialogOptions.FULLSCREEN);
var localDirectoryDialogResult = await localDirectoryDialogReference.Result;
if (localDirectoryDialogResult is null || localDirectoryDialogResult.Canceled)
return;
var localDirectory = (DataSourceLocalDirectory)localDirectoryDialogResult.Data!;
localDirectory = localDirectory with { Num = this.SettingsManager.ConfigurationData.NextDataSourceNum++ };
addedDataSource = localDirectory;
break;
case DataSourceType.ERI_V1:
var eriDialogParameters = new DialogParameters<DataSourceERI_V1Dialog>
{
{ x => x.IsEditing, false },
};
var eriDialogReference = await this.DialogService.ShowAsync<DataSourceERI_V1Dialog>(T("Add ERI v1 Data Source"), eriDialogParameters, DialogOptions.FULLSCREEN);
var eriDialogResult = await eriDialogReference.Result;
if (eriDialogResult is null || eriDialogResult.Canceled)
return;
var eriDataSource = (DataSourceERI_V1)eriDialogResult.Data!;
eriDataSource = eriDataSource with { Num = this.SettingsManager.ConfigurationData.NextDataSourceNum++ };
addedDataSource = eriDataSource;
break;
}
if(addedDataSource is null)
return;
await this.StoreAddedDataSource(addedDataSource);
}
private async Task StoreAddedDataSource(IDataSource addedDataSource)
{
this.SettingsManager.ConfigurationData.DataSources.Add(addedDataSource);
await this.SettingsManager.StoreSettings();
await this.DataSourceEmbeddingService.QueueDataSourceAsync(addedDataSource);
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
}
private async Task ExportDataSource(IDataSourceBase dataSource)
{
if (!this.SettingsManager.ConfigurationData.App.ShowAdminSettings)
@@ -537,41 +419,6 @@ public partial class DataSourceManagement : MSGComponentBase
_ => Task.CompletedTask,
};
private async Task AddMailbox()
{
if (!this.MayAddMailbox || !this.HasEmbeddingProvider)
return;
var dialogParameters = new DialogParameters<DataSourceMailboxDialog>
{
{ x => x.IsEditing, false },
};
var dialogReference = await this.DialogService.ShowAsync<DataSourceMailboxDialog>(T("Add Mailbox"), dialogParameters, DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result;
if (dialogResult is null || dialogResult.Canceled)
return;
var mailbox = (DataSourceMailbox)dialogResult.Data!;
//
// The dialog refuses to add a mailbox once the organization rules them out, but the rule
// may change while it stores the password. Then the password goes again, as far as possible:
//
if (!this.MayAddMailbox)
{
await this.RustService.DeleteSecret(mailbox, SecretStoreType.DATA_SOURCE);
return;
}
mailbox = mailbox with { Num = this.SettingsManager.ConfigurationData.NextDataSourceNum++ };
this.SettingsManager.ConfigurationData.Mailboxes.Add(mailbox);
await this.SettingsManager.StoreSettings();
await this.DataSourceEmbeddingService.QueueDataSourceAsync(mailbox);
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
}
private async Task EditMailbox(DataSourceMailbox mailbox)
{
if (await MailboxEditing.EditAsync(this.DialogService, this.SettingsManager, this.DataSourceEmbeddingService, mailbox.Id))
@@ -6,5 +6,8 @@
<ExpansionPanel HeaderIcon="@AppIcons.DATABASE" HeaderText="@T("Configure Data Sources")">
<PreviewBeta ApplyInnerScrollingFix="true"/>
<DataSourceManagement/>
<div class="mt-3 mb-6">
<DataSourceAddButtons/>
</div>
</ExpansionPanel>
}