Added enterprise support to the embedding and transcription provider dialogs

This commit is contained in:
Thorsten Sommer 2026-08-15 10:56:17 +02:00
parent 26c470f70e
commit 96f70fdf06
Signed by untrusted user who does not match committer: tsommer
GPG Key ID: 371BBA77A02C0108
4 changed files with 104 additions and 18 deletions

View File

@ -4,10 +4,16 @@
<MudDialog>
<DialogContent>
@if (this.IsEnterpriseConfiguration)
{
<MudAlert Severity="Severity.Info" Class="mb-4">
@T("This embedding provider is managed by your organization. Host, model, and other settings are locked. You can set your own API key below.")
</MudAlert>
}
<MudForm @ref="@this.form" @bind-IsValid="@this.dataIsValid" @bind-Errors="@this.dataIssues">
<MudStack Row="@true" AlignItems="AlignItems.Center">
@* ReSharper disable once CSharpWarnings::CS8974 *@
<MudSelect @bind-Value="@this.DataLLMProvider" Label="@T("Provider")" Class="mb-3" OpenIcon="@Icons.Material.Filled.AccountBalance" AdornmentColor="Color.Info" Adornment="Adornment.Start" Validation="@this.providerValidation.ValidatingProvider">
<MudSelect @bind-Value="@this.DataLLMProvider" Label="@T("Provider")" Class="mb-3" OpenIcon="@Icons.Material.Filled.AccountBalance" AdornmentColor="Color.Info" Adornment="Adornment.Start" Disabled="@this.IsEnterpriseConfiguration" Validation="@this.providerValidation.ValidatingProvider">
@foreach (LLMProviders provider in Enum.GetValues(typeof(LLMProviders)))
{
if (provider.ProvideEmbeddingAPI() || provider is LLMProviders.NONE)
@ -38,13 +44,14 @@
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Dns"
AdornmentColor="Color.Info"
Disabled="@this.IsEnterpriseConfiguration"
Validation="@this.providerValidation.ValidatingHostname"
UserAttributes="@SPELLCHECK_ATTRIBUTES"/>
}
@if (this.DataLLMProvider.IsHostNeeded())
{
<MudSelect T="Host" Value="@this.DataHost" ValueChanged="@this.OnHostChanged" Label="@T("Host")" Class="mb-3" OpenIcon="@Icons.Material.Filled.ExpandMore" AdornmentColor="Color.Info" Adornment="Adornment.Start" Validation="@this.providerValidation.ValidatingHost">
<MudSelect T="Host" Value="@this.DataHost" ValueChanged="@this.OnHostChanged" Label="@T("Host")" Class="mb-3" OpenIcon="@Icons.Material.Filled.ExpandMore" AdornmentColor="Color.Info" Adornment="Adornment.Start" Disabled="@this.IsEnterpriseConfiguration" Validation="@this.providerValidation.ValidatingHost">
@foreach (Host host in Enum.GetValues(typeof(Host)))
{
if (host.IsEmbeddingSupported())
@ -69,6 +76,7 @@
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Dns"
AdornmentColor="Color.Info"
Disabled="@this.IsEnterpriseConfiguration"
Validation="@this.ValidateManuallyModel"
UserAttributes="@SPELLCHECK_ATTRIBUTES"
HelperText="@T("Currently, we cannot query the embedding models for the selected provider and/or host. Therefore, please enter the model name manually.")"
@ -76,7 +84,7 @@
}
else
{
<MudButton Disabled="@(!this.DataLLMProvider.CanLoadModels(this.DataHost, this.dataAPIKey))" Variant="Variant.Filled" Size="Size.Small" StartIcon="@Icons.Material.Filled.Refresh" OnClick="@this.ReloadModels">
<MudButton Disabled="@(this.IsEnterpriseConfiguration || !this.DataLLMProvider.CanLoadModels(this.DataHost, this.dataAPIKey))" Variant="Variant.Filled" Size="Size.Small" StartIcon="@Icons.Material.Filled.Refresh" OnClick="@this.ReloadModels">
@T("Load")
</MudButton>
@if(this.availableModels.Count is 0)
@ -87,7 +95,7 @@
}
else
{
<MudSelect Disabled="@this.IsNoneProvider" @bind-Value="@this.DataModel" Label="@T("Model")"
<MudSelect Disabled="@(this.IsEnterpriseConfiguration || this.IsNoneProvider)" @bind-Value="@this.DataModel" Label="@T("Model")"
OpenIcon="@Icons.Material.Filled.FaceRetouchingNatural"
AdornmentColor="Color.Info" Adornment="Adornment.Start"
Validation="@this.providerValidation.ValidatingModel">
@ -121,6 +129,7 @@
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Lightbulb"
AdornmentColor="Color.Info"
Disabled="@this.IsEnterpriseConfiguration"
Validation="@this.providerValidation.ValidatingInstanceName"
UserAttributes="@SPELLCHECK_ATTRIBUTES"
/>

View File

@ -68,7 +68,14 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId
/// </summary>
[Parameter]
public bool IsEditing { get; init; }
/// <summary>
/// Whether this embedding provider is managed by an enterprise configuration plugin. When true,
/// every field except the API key is locked, matching <see cref="Settings.EmbeddingProvider.IsEnterpriseConfiguration"/>.
/// </summary>
[Parameter]
public bool IsEnterpriseConfiguration { get; set; }
[Inject]
private RustService RustService { get; init; } = null!;
@ -85,6 +92,7 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId
private bool dataIsValid;
private string[] dataIssues = [];
private string dataAPIKey = string.Empty;
private bool dataHadStoredAPIKeyOnLoad;
private string dataManuallyModel = string.Empty;
private string dataAPIKeyStorageIssue = string.Empty;
private string dataEditingPreviousInstanceName = string.Empty;
@ -134,7 +142,7 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId
IsSelfHosted = this.DataLLMProvider is LLMProviders.SELF_HOSTED,
Hostname = cleanedHostname.EndsWith('/') ? cleanedHostname[..^1] : cleanedHostname,
Host = this.DataHost,
IsEnterpriseConfiguration = false,
IsEnterpriseConfiguration = this.IsEnterpriseConfiguration,
EnterpriseConfigurationPluginId = Guid.Empty,
};
}
@ -174,11 +182,17 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId
// Load the API key:
var requestedSecret = await this.RustService.GetAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER, isTrying: this.DataLLMProvider is LLMProviders.SELF_HOSTED);
if (requestedSecret.Success)
{
this.dataAPIKey = await requestedSecret.Secret.Decrypt(this.encryption);
this.dataHadStoredAPIKeyOnLoad = !string.IsNullOrWhiteSpace(this.dataAPIKey);
}
else
{
this.dataAPIKey = string.Empty;
if (this.DataLLMProvider is not LLMProviders.SELF_HOSTED)
// For an enterprise-managed provider, having no key yet is the expected first-run
// state, not a storage failure -- the user is just about to set their own key:
if (this.DataLLMProvider is not LLMProviders.SELF_HOSTED && !this.IsEnterpriseConfiguration)
{
this.dataAPIKeyStorageIssue = string.Format(T("Failed to load the API key from the operating system. The message was: {0}. You might ignore this message and provide the API key again."), requestedSecret.Issue);
await this.form.Validate();
@ -203,8 +217,12 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId
#region Implementation of ISecretId
public string SecretId => this.DataLLMProvider.ToSecretId();
// Must mirror Settings.EmbeddingProvider.SecretId exactly: when editing an enterprise-managed
// provider, the key has to be stored under the same "ENT::"-prefixed keyring row that the
// app reads from at runtime (see BaseProvider.SecretId). Otherwise, a key entered here would
// silently end up in the wrong keyring row and never be found again.
public string SecretId => this.IsEnterpriseConfiguration ? $"{ISecretId.ENTERPRISE_KEY_PREFIX}::{this.DataLLMProvider.ToSecretId()}" : this.DataLLMProvider.ToSecretId();
public string SecretName => this.DataName;
#endregion
@ -240,6 +258,22 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId
await this.form.Validate();
return;
}
this.dataHadStoredAPIKeyOnLoad = true;
}
else if (this.dataHadStoredAPIKeyOnLoad)
{
// The user cleared a previously stored key. Without this, the old key would simply
// stay in the OS keyring untouched and keep being used:
var deleteResponse = await this.RustService.DeleteAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER);
if (!deleteResponse.Success)
{
this.dataAPIKeyStorageIssue = string.Format(T("Failed to remove the API key from the operating system. The message was: {0}. Please try again."), deleteResponse.Issue);
await this.form.Validate();
return;
}
this.dataHadStoredAPIKeyOnLoad = false;
}
this.MudDialog.Close(DialogResult.Ok(addedProviderSettings));

View File

@ -4,10 +4,16 @@
<MudDialog>
<DialogContent>
@if (this.IsEnterpriseConfiguration)
{
<MudAlert Severity="Severity.Info" Class="mb-4">
@T("This transcription provider is managed by your organization. Host, model, and other settings are locked. You can set your own API key below.")
</MudAlert>
}
<MudForm @ref="@this.form" @bind-IsValid="@this.dataIsValid" @bind-Errors="@this.dataIssues">
<MudStack Row="@true" AlignItems="AlignItems.Center">
@* ReSharper disable once CSharpWarnings::CS8974 *@
<MudSelect @bind-Value="@this.DataLLMProvider" Label="@T("Provider")" Class="mb-3" OpenIcon="@Icons.Material.Filled.AccountBalance" AdornmentColor="Color.Info" Adornment="Adornment.Start" Validation="@this.providerValidation.ValidatingProvider">
<MudSelect @bind-Value="@this.DataLLMProvider" Label="@T("Provider")" Class="mb-3" OpenIcon="@Icons.Material.Filled.AccountBalance" AdornmentColor="Color.Info" Adornment="Adornment.Start" Disabled="@this.IsEnterpriseConfiguration" Validation="@this.providerValidation.ValidatingProvider">
@foreach (LLMProviders provider in Enum.GetValues(typeof(LLMProviders)))
{
if (provider.ProvideTranscriptionAPI() || provider is LLMProviders.NONE)
@ -38,13 +44,14 @@
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Dns"
AdornmentColor="Color.Info"
Disabled="@this.IsEnterpriseConfiguration"
Validation="@this.providerValidation.ValidatingHostname"
UserAttributes="@SPELLCHECK_ATTRIBUTES"/>
}
@if (this.DataLLMProvider.IsHostNeeded())
{
<MudSelect T="Host" Value="@this.DataHost" ValueChanged="@this.OnHostChanged" Label="@T("Host")" Class="mb-3" OpenIcon="@Icons.Material.Filled.ExpandMore" AdornmentColor="Color.Info" Adornment="Adornment.Start" Validation="@this.providerValidation.ValidatingHost">
<MudSelect T="Host" Value="@this.DataHost" ValueChanged="@this.OnHostChanged" Label="@T("Host")" Class="mb-3" OpenIcon="@Icons.Material.Filled.ExpandMore" AdornmentColor="Color.Info" Adornment="Adornment.Start" Disabled="@this.IsEnterpriseConfiguration" Validation="@this.providerValidation.ValidatingHost">
@foreach (Host host in Enum.GetValues(typeof(Host)))
{
if (host.IsTranscriptionSupported())
@ -71,6 +78,7 @@
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Dns"
AdornmentColor="Color.Info"
Disabled="@this.IsEnterpriseConfiguration"
Validation="@this.ValidateManuallyModel"
UserAttributes="@SPELLCHECK_ATTRIBUTES"
HelperText="@T("Currently, we cannot query the transcription models for the selected provider and/or host. Therefore, please enter the model name manually.")"
@ -78,7 +86,7 @@
}
else
{
<MudButton Disabled="@(!this.DataLLMProvider.CanLoadModels(this.DataHost, this.dataAPIKey))" Variant="Variant.Filled" Size="Size.Small" StartIcon="@Icons.Material.Filled.Refresh" OnClick="@this.ReloadModels">
<MudButton Disabled="@(this.IsEnterpriseConfiguration || !this.DataLLMProvider.CanLoadModels(this.DataHost, this.dataAPIKey))" Variant="Variant.Filled" Size="Size.Small" StartIcon="@Icons.Material.Filled.Refresh" OnClick="@this.ReloadModels">
@T("Load")
</MudButton>
@if(this.availableModels.Count is 0)
@ -89,7 +97,7 @@
}
else
{
<MudSelect Disabled="@this.IsNoneProvider" @bind-Value="@this.DataModel" Label="@T("Model")"
<MudSelect Disabled="@(this.IsEnterpriseConfiguration || this.IsNoneProvider)" @bind-Value="@this.DataModel" Label="@T("Model")"
OpenIcon="@Icons.Material.Filled.FaceRetouchingNatural"
AdornmentColor="Color.Info" Adornment="Adornment.Start"
Validation="@this.providerValidation.ValidatingModel">
@ -132,6 +140,7 @@
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Lightbulb"
AdornmentColor="Color.Info"
Disabled="@this.IsEnterpriseConfiguration"
Validation="@this.providerValidation.ValidatingInstanceName"
UserAttributes="@SPELLCHECK_ATTRIBUTES"
/>

View File

@ -68,7 +68,14 @@ public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId
/// </summary>
[Parameter]
public bool IsEditing { get; init; }
/// <summary>
/// Whether this transcription provider is managed by an enterprise configuration plugin. When
/// true, every field except the API key is locked, matching <see cref="Settings.TranscriptionProvider.IsEnterpriseConfiguration"/>.
/// </summary>
[Parameter]
public bool IsEnterpriseConfiguration { get; set; }
[Inject]
private RustService RustService { get; init; } = null!;
@ -85,6 +92,7 @@ public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId
private bool dataIsValid;
private string[] dataIssues = [];
private string dataAPIKey = string.Empty;
private bool dataHadStoredAPIKeyOnLoad;
private string dataManuallyModel = string.Empty;
private string dataAPIKeyStorageIssue = string.Empty;
private string dataEditingPreviousInstanceName = string.Empty;
@ -149,7 +157,7 @@ public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId
IsSelfHosted = this.DataLLMProvider is LLMProviders.SELF_HOSTED,
Hostname = cleanedHostname.EndsWith('/') ? cleanedHostname[..^1] : cleanedHostname,
Host = this.DataHost,
IsEnterpriseConfiguration = false,
IsEnterpriseConfiguration = this.IsEnterpriseConfiguration,
EnterpriseConfigurationPluginId = Guid.Empty,
};
}
@ -189,11 +197,17 @@ public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId
// Load the API key:
var requestedSecret = await this.RustService.GetAPIKey(this, SecretStoreType.TRANSCRIPTION_PROVIDER, isTrying: this.DataLLMProvider is LLMProviders.SELF_HOSTED);
if (requestedSecret.Success)
{
this.dataAPIKey = await requestedSecret.Secret.Decrypt(this.encryption);
this.dataHadStoredAPIKeyOnLoad = !string.IsNullOrWhiteSpace(this.dataAPIKey);
}
else
{
this.dataAPIKey = string.Empty;
if (this.DataLLMProvider is not LLMProviders.SELF_HOSTED)
// For an enterprise-managed provider, having no key yet is the expected first-run
// state, not a storage failure -- the user is just about to set their own key:
if (this.DataLLMProvider is not LLMProviders.SELF_HOSTED && !this.IsEnterpriseConfiguration)
{
this.dataAPIKeyStorageIssue = string.Format(T("Failed to load the API key from the operating system. The message was: {0}. You might ignore this message and provide the API key again."), requestedSecret.Issue);
await this.form.Validate();
@ -218,8 +232,12 @@ public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId
#region Implementation of ISecretId
public string SecretId => this.DataLLMProvider.ToSecretId();
// Must mirror Settings.TranscriptionProvider.SecretId exactly: when editing an enterprise-managed
// provider, the key has to be stored under the same "ENT::"-prefixed keyring row that the
// app reads from at runtime (see BaseProvider.SecretId). Otherwise, a key entered here would
// silently end up in the wrong keyring row and never be found again.
public string SecretId => this.IsEnterpriseConfiguration ? $"{ISecretId.ENTERPRISE_KEY_PREFIX}::{this.DataLLMProvider.ToSecretId()}" : this.DataLLMProvider.ToSecretId();
public string SecretName => this.DataName;
#endregion
@ -255,6 +273,22 @@ public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId
await this.form.Validate();
return;
}
this.dataHadStoredAPIKeyOnLoad = true;
}
else if (this.dataHadStoredAPIKeyOnLoad)
{
// The user cleared a previously stored key. Without this, the old key would simply
// stay in the OS keyring untouched and keep being used:
var deleteResponse = await this.RustService.DeleteAPIKey(this, SecretStoreType.TRANSCRIPTION_PROVIDER);
if (!deleteResponse.Success)
{
this.dataAPIKeyStorageIssue = string.Format(T("Failed to remove the API key from the operating system. The message was: {0}. Please try again."), deleteResponse.Issue);
await this.form.Validate();
return;
}
this.dataHadStoredAPIKeyOnLoad = false;
}
this.MudDialog.Close(DialogResult.Ok(addedProviderSettings));