Skip to content

Commit 6979a73

Browse files
Improved enterprise service (#493)
1 parent fcf511c commit 6979a73

File tree

12 files changed

+189
-116
lines changed

12 files changed

+189
-116
lines changed

app/MindWork AI Studio/Components/MSGComponentBase.cs

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,6 @@ public abstract class MSGComponentBase : ComponentBase, IDisposable, IMessageBus
1313
[Inject]
1414
protected MessageBus MessageBus { get; init; } = null!;
1515

16-
[Inject]
17-
// ReSharper disable once UnusedAutoPropertyAccessor.Local
18-
private ILogger<MSGComponentBase> Logger { get; init; } = null!;
19-
2016
private ILanguagePlugin Lang { get; set; } = PluginFactory.BaseLanguage;
2117

2218
#region Overrides of ComponentBase
@@ -45,19 +41,22 @@ protected override async Task OnInitializedAsync()
4541

4642
public async Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
4743
{
48-
switch (triggeredEvent)
44+
await this.InvokeAsync(async () =>
4945
{
50-
case Event.COLOR_THEME_CHANGED:
51-
this.StateHasChanged();
52-
break;
46+
switch (triggeredEvent)
47+
{
48+
case Event.COLOR_THEME_CHANGED:
49+
this.StateHasChanged();
50+
break;
5351

54-
case Event.PLUGINS_RELOADED:
55-
this.Lang = await this.SettingsManager.GetActiveLanguagePlugin();
56-
await this.InvokeAsync(this.StateHasChanged);
57-
break;
58-
}
52+
case Event.PLUGINS_RELOADED:
53+
this.Lang = await this.SettingsManager.GetActiveLanguagePlugin();
54+
await this.InvokeAsync(this.StateHasChanged);
55+
break;
56+
}
5957

60-
await this.ProcessIncomingMessage(sendingComponent, triggeredEvent, data);
58+
await this.ProcessIncomingMessage(sendingComponent, triggeredEvent, data);
59+
});
6160
}
6261

6362
public async Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data)

app/MindWork AI Studio/Layout/MainLayout.razor.cs

Lines changed: 89 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using Microsoft.AspNetCore.Components.Routing;
1010

1111
using DialogOptions = AIStudio.Dialogs.DialogOptions;
12+
using EnterpriseEnvironment = AIStudio.Tools.EnterpriseEnvironment;
1213

1314
namespace AIStudio.Layout;
1415

@@ -138,90 +139,100 @@ private void LoadNavItems()
138139

139140
public async Task ProcessMessage<TMessage>(ComponentBase? sendingComponent, Event triggeredEvent, TMessage? data)
140141
{
141-
switch (triggeredEvent)
142+
await this.InvokeAsync(async () =>
142143
{
143-
case Event.UPDATE_AVAILABLE:
144-
if (data is UpdateResponse updateResponse)
145-
{
146-
this.currentUpdateResponse = updateResponse;
147-
var message = string.Format(T("An update to version {0} is available."), updateResponse.NewVersion);
148-
this.Snackbar.Add(message, Severity.Info, config =>
144+
switch (triggeredEvent)
145+
{
146+
case Event.UPDATE_AVAILABLE:
147+
if (data is UpdateResponse updateResponse)
149148
{
150-
config.Icon = Icons.Material.Filled.Update;
151-
config.IconSize = Size.Large;
152-
config.HideTransitionDuration = 600;
153-
config.VisibleStateDuration = 32_000;
154-
config.OnClick = async _ =>
149+
this.currentUpdateResponse = updateResponse;
150+
var message = string.Format(T("An update to version {0} is available."), updateResponse.NewVersion);
151+
this.Snackbar.Add(message, Severity.Info, config =>
155152
{
156-
await this.ShowUpdateDialog();
157-
};
158-
config.Action = T("Show details");
159-
config.ActionVariant = Variant.Filled;
160-
});
161-
}
162-
163-
break;
164-
165-
case Event.CONFIGURATION_CHANGED:
166-
if(this.SettingsManager.ConfigurationData.App.NavigationBehavior is NavBehavior.ALWAYS_EXPAND)
167-
this.navBarOpen = true;
168-
else
169-
this.navBarOpen = false;
170-
171-
await this.UpdateThemeConfiguration();
172-
this.LoadNavItems();
173-
this.StateHasChanged();
174-
break;
175-
176-
case Event.COLOR_THEME_CHANGED:
177-
this.StateHasChanged();
178-
break;
179-
180-
case Event.SHOW_SUCCESS:
181-
if (data is DataSuccessMessage success)
182-
success.Show(this.Snackbar);
183-
184-
break;
185-
186-
case Event.SHOW_ERROR:
187-
if (data is DataErrorMessage error)
188-
error.Show(this.Snackbar);
189-
190-
break;
191-
192-
case Event.SHOW_WARNING:
193-
if (data is DataWarningMessage warning)
194-
warning.Show(this.Snackbar);
195-
196-
break;
197-
198-
case Event.STARTUP_PLUGIN_SYSTEM:
199-
_ = Task.Run(async () =>
200-
{
201-
// Set up the plugin system:
202-
if (PluginFactory.Setup())
153+
config.Icon = Icons.Material.Filled.Update;
154+
config.IconSize = Size.Large;
155+
config.HideTransitionDuration = 600;
156+
config.VisibleStateDuration = 32_000;
157+
config.OnClick = async _ =>
158+
{
159+
await this.ShowUpdateDialog();
160+
};
161+
config.Action = T("Show details");
162+
config.ActionVariant = Variant.Filled;
163+
});
164+
}
165+
166+
break;
167+
168+
case Event.CONFIGURATION_CHANGED:
169+
if (this.SettingsManager.ConfigurationData.App.NavigationBehavior is NavBehavior.ALWAYS_EXPAND)
170+
this.navBarOpen = true;
171+
else
172+
this.navBarOpen = false;
173+
174+
await this.UpdateThemeConfiguration();
175+
this.LoadNavItems();
176+
this.StateHasChanged();
177+
break;
178+
179+
case Event.COLOR_THEME_CHANGED:
180+
this.StateHasChanged();
181+
break;
182+
183+
case Event.SHOW_SUCCESS:
184+
if (data is DataSuccessMessage success)
185+
success.Show(this.Snackbar);
186+
187+
break;
188+
189+
case Event.SHOW_ERROR:
190+
if (data is DataErrorMessage error)
191+
error.Show(this.Snackbar);
192+
193+
break;
194+
195+
case Event.SHOW_WARNING:
196+
if (data is DataWarningMessage warning)
197+
warning.Show(this.Snackbar);
198+
199+
break;
200+
201+
case Event.STARTUP_PLUGIN_SYSTEM:
202+
_ = Task.Run(async () =>
203203
{
204-
// Ensure that all internal plugins are present:
205-
await PluginFactory.EnsureInternalPlugins();
204+
// Set up the plugin system:
205+
if (PluginFactory.Setup())
206+
{
207+
// Ensure that all internal plugins are present:
208+
await PluginFactory.EnsureInternalPlugins();
209+
210+
//
211+
// Check if there is an enterprise configuration plugin to download:
212+
//
213+
var enterpriseEnvironment = this.MessageBus.CheckDeferredMessages<EnterpriseEnvironment>(Event.STARTUP_ENTERPRISE_ENVIRONMENT).FirstOrDefault();
214+
if (enterpriseEnvironment != default)
215+
await PluginFactory.TryDownloadingConfigPluginAsync(enterpriseEnvironment.ConfigurationId, enterpriseEnvironment.ConfigurationServerUrl);
216+
217+
// Load (but not start) all plugins without waiting for them:
218+
var pluginLoadingTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
219+
await PluginFactory.LoadAll(pluginLoadingTimeout.Token);
220+
221+
// Set up hot reloading for plugins:
222+
PluginFactory.SetUpHotReloading();
223+
}
224+
});
225+
break;
206226

207-
// Load (but not start) all plugins, without waiting for them:
208-
var pluginLoadingTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
209-
await PluginFactory.LoadAll(pluginLoadingTimeout.Token);
227+
case Event.PLUGINS_RELOADED:
228+
this.Lang = await this.SettingsManager.GetActiveLanguagePlugin();
229+
I18N.Init(this.Lang);
230+
this.LoadNavItems();
210231

211-
// Set up hot reloading for plugins:
212-
PluginFactory.SetUpHotReloading();
213-
}
214-
});
215-
break;
216-
217-
case Event.PLUGINS_RELOADED:
218-
this.Lang = await this.SettingsManager.GetActiveLanguagePlugin();
219-
I18N.Init(this.Lang);
220-
this.LoadNavItems();
221-
222-
await this.InvokeAsync(this.StateHasChanged);
223-
break;
224-
}
232+
await this.InvokeAsync(this.StateHasChanged);
233+
break;
234+
}
235+
});
225236
}
226237

227238
public Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data)

app/MindWork AI Studio/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ public static async Task Main()
160160

161161
// Get the logging factory for e.g., static classes:
162162
LOGGER_FACTORY = app.Services.GetRequiredService<ILoggerFactory>();
163+
MessageBus.INSTANCE.Initialize(LOGGER_FACTORY.CreateLogger<MessageBus>());
163164

164165
// Get a program logger:
165166
var programLogger = app.Services.GetRequiredService<ILogger<Program>>();
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
using System.Net.Http.Headers;
2+
13
namespace AIStudio.Tools;
24

3-
public readonly record struct EnterpriseEnvironment(string ConfigurationServerUrl, Guid ConfigurationId)
5+
public readonly record struct EnterpriseEnvironment(string ConfigurationServerUrl, Guid ConfigurationId, EntityTagHeaderValue? ETag)
46
{
57
public bool IsActive => !string.IsNullOrEmpty(this.ConfigurationServerUrl) && this.ConfigurationId != Guid.Empty;
68
}

app/MindWork AI Studio/Tools/Event.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ public enum Event
99
CONFIGURATION_CHANGED,
1010
COLOR_THEME_CHANGED,
1111
STARTUP_PLUGIN_SYSTEM,
12+
STARTUP_ENTERPRISE_ENVIRONMENT,
1213
PLUGINS_RELOADED,
1314
SHOW_ERROR,
1415
SHOW_WARNING,

app/MindWork AI Studio/Tools/MessageBus.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,17 @@ public sealed class MessageBus
1515
private readonly ConcurrentQueue<Message> messageQueue = new();
1616
private readonly SemaphoreSlim sendingSemaphore = new(1, 1);
1717

18+
private static ILogger<MessageBus>? LOG;
19+
1820
private MessageBus()
1921
{
2022
}
23+
24+
public void Initialize(ILogger<MessageBus> logger)
25+
{
26+
LOG = logger;
27+
LOG.LogInformation("Message bus initialized.");
28+
}
2129

2230
/// <summary>
2331
/// Define for which components and events you want to receive messages.
@@ -48,7 +56,7 @@ private record class Message(ComponentBase? SendingComponent, Event TriggeredEve
4856
public async Task SendMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data = default)
4957
{
5058
this.messageQueue.Enqueue(new Message(sendingComponent, triggeredEvent, data));
51-
59+
5260
try
5361
{
5462
await this.sendingSemaphore.WaitAsync();
@@ -61,11 +69,16 @@ public async Task SendMessage<T>(ComponentBase? sendingComponent, Event triggere
6169

6270
var eventFilter = this.componentEvents[receiver];
6371
if (eventFilter.Length == 0 || eventFilter.Contains(triggeredEvent))
72+
6473
// We don't await the task here because we don't want to block the message bus:
6574
_ = receiver.ProcessMessage(message.SendingComponent, message.TriggeredEvent, message.Data);
6675
}
6776
}
6877
}
78+
catch (Exception e)
79+
{
80+
LOG?.LogError(e, "Error while sending message.");
81+
}
6982
finally
7083
{
7184
this.sendingSemaphore.Release();

app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Download.cs

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,61 @@
11
using System.IO.Compression;
2+
using System.Net.Http.Headers;
23

34
namespace AIStudio.Tools.PluginSystem;
45

56
public static partial class PluginFactory
67
{
8+
public static async Task<EntityTagHeaderValue?> DetermineConfigPluginETagAsync(Guid configPlugId, string configServerUrl, CancellationToken cancellationToken = default)
9+
{
10+
try
11+
{
12+
var serverUrl = configServerUrl.EndsWith('/') ? configServerUrl[..^1] : configServerUrl;
13+
var downloadUrl = $"{serverUrl}/{configPlugId}.zip";
14+
15+
using var http = new HttpClient();
16+
using var request = new HttpRequestMessage(HttpMethod.Get, downloadUrl);
17+
var response = await http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
18+
return response.Headers.ETag;
19+
}
20+
catch (Exception e)
21+
{
22+
LOG.LogError(e, "An error occurred while determining the ETag for the configuration plugin.");
23+
return null;
24+
}
25+
}
26+
727
public static async Task<bool> TryDownloadingConfigPluginAsync(Guid configPlugId, string configServerUrl, CancellationToken cancellationToken = default)
828
{
9-
if (!IS_INITIALIZED)
29+
if(!IS_INITIALIZED)
30+
{
31+
LOG.LogWarning("Plugin factory is not yet initialized. Cannot download configuration plugin.");
1032
return false;
33+
}
1134

12-
LOG.LogInformation($"Downloading configuration plugin with ID: {configPlugId} from server: {configServerUrl}");
35+
var serverUrl = configServerUrl.EndsWith('/') ? configServerUrl[..^1] : configServerUrl;
36+
var downloadUrl = $"{serverUrl}/{configPlugId}.zip";
37+
38+
LOG.LogInformation($"Try to download configuration plugin with ID='{configPlugId}' from server='{configServerUrl}' (GET {downloadUrl})");
1339
var tempDownloadFile = Path.GetTempFileName();
1440
try
1541
{
1642
using var httpClient = new HttpClient();
17-
var response = await httpClient.GetAsync($"{configServerUrl}/{configPlugId}.zip", cancellationToken);
43+
var response = await httpClient.GetAsync(downloadUrl, cancellationToken);
1844
if (response.IsSuccessStatusCode)
1945
{
20-
await using var tempFileStream = File.Create(tempDownloadFile);
21-
await response.Content.CopyToAsync(tempFileStream, cancellationToken);
46+
await using(var tempFileStream = File.Create(tempDownloadFile))
47+
{
48+
await response.Content.CopyToAsync(tempFileStream, cancellationToken);
49+
}
2250

23-
var pluginDirectory = Path.Join(CONFIGURATION_PLUGINS_ROOT, configPlugId.ToString());
24-
if(Directory.Exists(pluginDirectory))
25-
Directory.Delete(pluginDirectory, true);
51+
var configDirectory = Path.Join(CONFIGURATION_PLUGINS_ROOT, configPlugId.ToString());
52+
if(Directory.Exists(configDirectory))
53+
Directory.Delete(configDirectory, true);
2654

27-
Directory.CreateDirectory(pluginDirectory);
28-
ZipFile.ExtractToDirectory(tempDownloadFile, pluginDirectory);
55+
Directory.CreateDirectory(configDirectory);
56+
ZipFile.ExtractToDirectory(tempDownloadFile, configDirectory);
2957

30-
LOG.LogInformation($"Configuration plugin with ID='{configPlugId}' downloaded and extracted successfully to '{pluginDirectory}'.");
58+
LOG.LogInformation($"Configuration plugin with ID='{configPlugId}' downloaded and extracted successfully to '{configDirectory}'.");
3159
}
3260
else
3361
LOG.LogError($"Failed to download the enterprise configuration plugin. HTTP Status: {response.StatusCode}");

app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.HotReload.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ private static async void HotReloadEventHandler(object _, FileSystemEventArgs ar
4646
try
4747
{
4848
LOG.LogInformation($"File changed ({changeType}): {args.FullPath}. Reloading plugins...");
49+
50+
// Wait for parallel writes to finish:
51+
await Task.Delay(TimeSpan.FromSeconds(3));
4952
await LoadAll();
5053
await MessageBus.INSTANCE.SendMessage<bool>(null, Event.PLUGINS_RELOADED);
5154
}

0 commit comments

Comments
 (0)