diff --git a/BootstrapBlazor.Extensions.sln b/BootstrapBlazor.Extensions.sln
index 1f62ca72..7dbcbca8 100644
--- a/BootstrapBlazor.Extensions.sln
+++ b/BootstrapBlazor.Extensions.sln
@@ -1,7 +1,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 18
-VisualStudioVersion = 18.0.10828.68 main
+VisualStudioVersion = 18.0.10828.68
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "components", "components", "{FF1089BE-C704-4374-B629-C57C08E1798F}"
EndProject
@@ -200,6 +200,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BootstrapBlazor.OpcDa", "sr
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTestOpcDa", "test\UnitTestOpcDa\UnitTestOpcDa.csproj", "{835C8BA9-A9CC-4EA0-9002-34A20F8B2E86}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BootstrapBlazor.Tasks.Dashboard", "src\components\BootstrapBlazor.Tasks.Dashboard\BootstrapBlazor.Tasks.Dashboard.csproj", "{30C57119-C564-401C-AE3A-6203E2733E1A}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -546,6 +548,10 @@ Global
{835C8BA9-A9CC-4EA0-9002-34A20F8B2E86}.Debug|Any CPU.Build.0 = Debug|Any CPU
{835C8BA9-A9CC-4EA0-9002-34A20F8B2E86}.Release|Any CPU.ActiveCfg = Release|Any CPU
{835C8BA9-A9CC-4EA0-9002-34A20F8B2E86}.Release|Any CPU.Build.0 = Release|Any CPU
+ {30C57119-C564-401C-AE3A-6203E2733E1A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {30C57119-C564-401C-AE3A-6203E2733E1A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {30C57119-C564-401C-AE3A-6203E2733E1A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {30C57119-C564-401C-AE3A-6203E2733E1A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -640,6 +646,7 @@ Global
{98373A64-E224-4715-AE02-A8C6DAFF3338} = {B6A98ADE-D26A-4D0B-8978-AB7AC915F5AE}
{01007B10-7C3C-4136-83FF-981CA39AD3D4} = {7B29E81D-92DE-46C8-8EDC-1B48C8F12BC2}
{835C8BA9-A9CC-4EA0-9002-34A20F8B2E86} = {B6A98ADE-D26A-4D0B-8978-AB7AC915F5AE}
+ {30C57119-C564-401C-AE3A-6203E2733E1A} = {FF1089BE-C704-4374-B629-C57C08E1798F}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D5EB1960-6F30-4CE1-B375-EAE1F787D6FF}
diff --git a/src/components/BootstrapBlazor.Tasks.Dashboard/BootstrapBlazor.Tasks.Dashboard.csproj b/src/components/BootstrapBlazor.Tasks.Dashboard/BootstrapBlazor.Tasks.Dashboard.csproj
new file mode 100644
index 00000000..bdf99fae
--- /dev/null
+++ b/src/components/BootstrapBlazor.Tasks.Dashboard/BootstrapBlazor.Tasks.Dashboard.csproj
@@ -0,0 +1,29 @@
+
+
+
+ 9.0.0
+
+
+
+ Bootstrap Blazor WebAssembly wasm UI Components Task Dashboard
+ Bootstrap UI components extensions of Task
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/BootstrapBlazor.Tasks.Dashboard/Locales/en-US.json b/src/components/BootstrapBlazor.Tasks.Dashboard/Locales/en-US.json
new file mode 100644
index 00000000..d4873ebe
--- /dev/null
+++ b/src/components/BootstrapBlazor.Tasks.Dashboard/Locales/en-US.json
@@ -0,0 +1,26 @@
+{
+ "BootstrapBlazor.Components.Tasks.TaskDashboard": {
+ "Name": "Name",
+ "Status": "Status",
+ "NextRuntime": "NextRuntime",
+ "LastRuntime": "LastRuntime",
+ "LastRunResult": "LastRunResult",
+ "Exception": "Exception",
+ "Operation": "Operation",
+ "SchedulerStatus.Running": "Running",
+ "SchedulerStatus.Ready": "Ready",
+ "SchedulerStatus.Disabled": "Disabled",
+ "TriggerResult.Running": "Running",
+ "TriggerResult.Success": "Success",
+ "TriggerResult.Cancelled": "Cancelled",
+ "TriggerResult.Timeout": "Timeout",
+ "TriggerResult.Error": "Error",
+ "ButtonPause": "Pause",
+ "ButtonRun": "Run",
+ "ButtonLog": "Log",
+ "LogDialogTitle": "{0} - Log(Latest 20 Items)",
+ "LogDialogConsoleHeaderText": "Action Log",
+ "ExceptionDialogTitle": "{0} - Exception Detail",
+ "None": "None"
+ }
+}
diff --git a/src/components/BootstrapBlazor.Tasks.Dashboard/Locales/zh-CN.json b/src/components/BootstrapBlazor.Tasks.Dashboard/Locales/zh-CN.json
new file mode 100644
index 00000000..908fdab1
--- /dev/null
+++ b/src/components/BootstrapBlazor.Tasks.Dashboard/Locales/zh-CN.json
@@ -0,0 +1,26 @@
+{
+ "BootstrapBlazor.Components.Tasks.TaskDashboard": {
+ "Name": "名称",
+ "Status": "调度状态",
+ "NextRuntime": "下次运行时间",
+ "LastRuntime": "上次运行时间",
+ "LastRunResult": "上次结果",
+ "Exception": "异常",
+ "Operation": "操作",
+ "SchedulerStatus.Running": "运行中",
+ "SchedulerStatus.Ready": "已停止",
+ "SchedulerStatus.Disabled": "禁用",
+ "TriggerResult.Running": "运行",
+ "TriggerResult.Success": "成功",
+ "TriggerResult.Cancelled": "取消",
+ "TriggerResult.Timeout": "超时",
+ "TriggerResult.Error": "故障",
+ "ButtonPause": "暂停",
+ "ButtonRun": "运行",
+ "ButtonLog": "日志",
+ "LogDialogTitle": "{0} - 日志窗口(最新 20 条)",
+ "LogDialogConsoleHeaderText": "执行日志",
+ "ExceptionDialogTitle": "{0} - 异常明细",
+ "None": "无"
+ }
+}
diff --git a/src/components/BootstrapBlazor.Tasks.Dashboard/TaskDashboard.razor b/src/components/BootstrapBlazor.Tasks.Dashboard/TaskDashboard.razor
new file mode 100644
index 00000000..0b5467ac
--- /dev/null
+++ b/src/components/BootstrapBlazor.Tasks.Dashboard/TaskDashboard.razor
@@ -0,0 +1,59 @@
+@namespace BootstrapBlazor.Components.Tasks
+@inherits BootstrapComponentBase
+
+
+
+
+
+
+ @v.Row.Name
+
+
+
+
+ @FormatStatus(v.Row.Status)
+
+
+
+
+ @FormatDateTime(v.Row.LastRuntime)
+
+
+
+
+ @FormatDateTime(v.Row.NextRuntime)
+
+
+
+
+ @FormatResult(v.Row.LastRunResult)
+
+
+
+
+ @if (v.Row.Exception != null)
+ {
+
+ }
+ else
+ {
+ @Localizer["None"]
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/BootstrapBlazor.Tasks.Dashboard/TaskDashboard.razor.cs b/src/components/BootstrapBlazor.Tasks.Dashboard/TaskDashboard.razor.cs
new file mode 100644
index 00000000..343b3b8c
--- /dev/null
+++ b/src/components/BootstrapBlazor.Tasks.Dashboard/TaskDashboard.razor.cs
@@ -0,0 +1,122 @@
+// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+// Website: https://www.blazor.zone or https://argozhang.github.io/
+
+using Microsoft.AspNetCore.Components;
+using Microsoft.Extensions.Localization;
+
+namespace BootstrapBlazor.Components.Tasks;
+
+///
+/// Task Dashboard Component
+///
+public partial class TaskDashboard
+{
+ [Inject, NotNull]
+ private IStringLocalizer? Localizer { get; set; }
+
+ [Inject, NotNull]
+ private DialogService? DialogService { get; set; }
+
+ private string? ClassString => CssBuilder.Default("bb-tasks-dashboard")
+ .AddClassFromAttributes(AdditionalAttributes)
+ .Build();
+
+ private IEnumerable _schedulers = [];
+
+ ///
+ ///
+ ///
+ protected override void OnParametersSet()
+ {
+ base.OnParametersSet();
+
+ _schedulers = TaskServicesManager.ToList();
+ }
+
+ private static Color GetStatusColor(SchedulerStatus status) => status switch
+ {
+ SchedulerStatus.Running => Color.Success,
+ SchedulerStatus.Ready => Color.Primary,
+ _ => Color.Danger
+ };
+
+ private string FormatStatus(SchedulerStatus status) => status switch
+ {
+ SchedulerStatus.Running => Localizer["SchedulerStatus.Running"],
+ SchedulerStatus.Ready => Localizer["SchedulerStatus.Ready"],
+ _ => Localizer["SchedulerStatus.Disabled"]
+ };
+
+ private static string GetStatusIcon(SchedulerStatus status) => status switch
+ {
+ SchedulerStatus.Running => "fa-solid fa-play-circle",
+ SchedulerStatus.Ready => "fa-solid fa-stop-circle",
+ _ => "fa-solid fa-times-circle"
+ };
+
+ private static Color GetResultColor(TriggerResult result) => result switch
+ {
+ TriggerResult.Running => Color.Primary,
+ TriggerResult.Success => Color.Success,
+ TriggerResult.Cancelled => Color.Dark,
+ TriggerResult.Timeout => Color.Warning,
+ _ => Color.Danger,
+ };
+
+ private string FormatResult(TriggerResult result) => result switch
+ {
+ TriggerResult.Running => Localizer["TriggerResult.Running"],
+ TriggerResult.Success => Localizer["TriggerResult.Success"],
+ TriggerResult.Cancelled => Localizer["TriggerResult.Timeout"],
+ TriggerResult.Timeout => Localizer["TriggerResult.Timeout"],
+ _ => Localizer["TriggerResult.Error"],
+ };
+
+ private static Task OnPause(IScheduler scheduler)
+ {
+ scheduler.Status = SchedulerStatus.Ready;
+ return Task.CompletedTask;
+ }
+
+ private static Task OnRun(IScheduler scheduler)
+ {
+ scheduler.Status = SchedulerStatus.Running;
+ return Task.CompletedTask;
+ }
+
+ private async Task OnLog(IScheduler scheduler)
+ {
+ var option = new DialogOption()
+ {
+ Class = "modal-dialog-task-log",
+ Title = Localizer["LogDialogTitle", scheduler.Name],
+ Component = BootstrapDynamicComponent.CreateComponent(new Dictionary
+ {
+ [nameof(TaskInfo.Scheduler)] = scheduler,
+ [nameof(TaskInfo.HeaderText)] = Localizer["LogDialogConsoleHeaderText"].Value
+ })
+ };
+ await DialogService.Show(option);
+ }
+
+ private static bool OnCheckPauseTaskStatus(IScheduler model) => model.Status != SchedulerStatus.Running;
+
+ private static bool OnCheckRunTaskStatus(IScheduler model) => model.Status == SchedulerStatus.Running;
+
+ private static string? FormatDateTime(DateTimeOffset? dateTime) => dateTime?.ToString("yyyy-MM-dd HH:mm:ss");
+
+ private async Task OnShowException(IScheduler scheduler, Exception ex)
+ {
+ var option = new DialogOption()
+ {
+ Class = "modal-dialog-task-ex",
+ IsScrolling = true,
+ Title = Localizer["ExceptionDialogTitle", scheduler.Name],
+ BodyTemplate = RenderException(ex)
+ };
+ await DialogService.Show(option);
+ }
+
+ private static RenderFragment RenderException(Exception ex) => builder => builder.AddContent(0, ex.FormatMarkupString());
+}
diff --git a/src/components/BootstrapBlazor.Tasks.Dashboard/TaskDashboard.razor.css b/src/components/BootstrapBlazor.Tasks.Dashboard/TaskDashboard.razor.css
new file mode 100644
index 00000000..c6afca40
--- /dev/null
+++ b/src/components/BootstrapBlazor.Tasks.Dashboard/TaskDashboard.razor.css
@@ -0,0 +1,6 @@
+.my-component {
+ border: 2px dashed red;
+ padding: 1em;
+ margin: 1em 0;
+ background-image: url('background.png');
+}
diff --git a/src/components/BootstrapBlazor.Tasks.Dashboard/TaskInfo.razor b/src/components/BootstrapBlazor.Tasks.Dashboard/TaskInfo.razor
new file mode 100644
index 00000000..d7003186
--- /dev/null
+++ b/src/components/BootstrapBlazor.Tasks.Dashboard/TaskInfo.razor
@@ -0,0 +1,3 @@
+@namespace BootstrapBlazor.Components.Tasks
+
+
diff --git a/src/components/BootstrapBlazor.Tasks.Dashboard/TaskInfo.razor.cs b/src/components/BootstrapBlazor.Tasks.Dashboard/TaskInfo.razor.cs
new file mode 100644
index 00000000..004b6cee
--- /dev/null
+++ b/src/components/BootstrapBlazor.Tasks.Dashboard/TaskInfo.razor.cs
@@ -0,0 +1,94 @@
+// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
+// Licensed under the LGPL License, Version 3.0. See License.txt in the project root for license information.
+// Website: https://admin.blazor.zone
+
+using Microsoft.AspNetCore.Components;
+
+namespace BootstrapBlazor.Components.Tasks;
+
+///
+/// TaskInfo 组件
+///
+public partial class TaskInfo : IDisposable
+{
+ ///
+ /// 获得/设置 实例
+ ///
+ [Parameter]
+ [NotNull]
+ [EditorRequired]
+ public IScheduler? Scheduler { get; set; }
+
+ ///
+ /// 获得/设置 日志窗口标题文本
+ ///
+ [Parameter]
+ public string? HeaderText { get; set; }
+
+ private List Messages { get; } = new(24);
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ protected override async Task OnAfterRenderAsync(bool firstRender)
+ {
+ await base.OnAfterRenderAsync(firstRender);
+
+ if (firstRender)
+ {
+ var scheduler = TaskServicesManager.Get(Scheduler.Name);
+ if (scheduler != null)
+ {
+ scheduler.Triggers.First().PulseCallback += DispatchMessageCallback;
+ await DispatchMessage(scheduler.Triggers.First());
+ }
+ }
+ }
+
+ private void DispatchMessageCallback(ITrigger trigger)
+ {
+ _ = DispatchMessage(trigger);
+ }
+
+ private async Task DispatchMessage(ITrigger trigger)
+ {
+ if (trigger.LastRuntime == null)
+ {
+ return;
+ }
+
+ var message = $"Trigger({trigger.GetType().Name}) LastRuntime: {trigger.LastRuntime} Run({trigger.LastResult}) NextRuntime: {trigger.NextRuntime} Elapsed: {trigger.LastRunElapsedTime.TotalSeconds}";
+ Messages.Add(new ConsoleMessageItem()
+ {
+ Message = message
+ });
+ if (Messages.Count > 20)
+ {
+ Messages.RemoveAt(0);
+ }
+ await InvokeAsync(StateHasChanged);
+ }
+
+ private void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ var scheduler = TaskServicesManager.Get(Scheduler.Name);
+ if (scheduler != null)
+ {
+ scheduler.Triggers.First().PulseCallback -= DispatchMessageCallback;
+ }
+ }
+ }
+
+ ///
+ ///
+ ///
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+}
diff --git a/src/components/BootstrapBlazor.Tasks.Dashboard/_Imports.razor b/src/components/BootstrapBlazor.Tasks.Dashboard/_Imports.razor
new file mode 100644
index 00000000..ea98a2fd
--- /dev/null
+++ b/src/components/BootstrapBlazor.Tasks.Dashboard/_Imports.razor
@@ -0,0 +1,3 @@
+@using BootstrapBlazor.Components
+@using Microsoft.AspNetCore.Components.Web
+@using Longbow.Tasks