117 changed files with 4814 additions and 300 deletions
@ -0,0 +1,64 @@ |
|||||
|
@using System.Globalization |
||||
|
@using Microsoft.Extensions.Hosting |
||||
|
@using Volo.Abp.AspNetCore.Components.Server.MudBlazorBasicTheme.Bundling |
||||
|
@using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Bundling |
||||
|
@using Volo.Abp.Localization |
||||
|
@inject IHostEnvironment Env |
||||
|
@{ |
||||
|
var rtl = CultureHelper.IsRtl ? "rtl" : string.Empty; |
||||
|
} |
||||
|
|
||||
|
<!DOCTYPE html> |
||||
|
<html lang="@CultureInfo.CurrentCulture.Name" dir="@rtl"> |
||||
|
|
||||
|
<head> |
||||
|
<meta charset="utf-8" /> |
||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
||||
|
<title>Elsa Studio (Blazor Server)</title> |
||||
|
<base href="/" /> |
||||
|
|
||||
|
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet" /> |
||||
|
<AbpStyles BundleName="@BlazorMudBlazorBasicThemeBundles.Styles.Global" /> |
||||
|
|
||||
|
@* Elsa Studio UI styles (RCL static assets) + offline icon-font fallback. *@ |
||||
|
<link href="/_content/CodeBeam.MudBlazor.Extensions/MudExtensions.min.css" rel="stylesheet" /> |
||||
|
<link href="/_content/Radzen.Blazor/css/material-base.css" rel="stylesheet" /> |
||||
|
<link href="/_content/Elsa.Studio.Shell/css/shell.css" rel="stylesheet" /> |
||||
|
<link href="/_content/Elsa.Studio.Workflows.Designer/designer.css" rel="stylesheet" /> |
||||
|
<link href="/_content/Elsa.Studio.Workflows.Designer/designer.v1.css" rel="stylesheet" /> |
||||
|
<link href="/_content/Elsa.Studio.Workflows.Designer/designer.v2.css" rel="stylesheet" /> |
||||
|
<link href="/elsa-studio-icons.css" rel="stylesheet" /> |
||||
|
|
||||
|
<HeadOutlet @rendermode="InteractiveServer" /> |
||||
|
|
||||
|
</head> |
||||
|
<body> |
||||
|
|
||||
|
<Routes @rendermode="InteractiveServer" /> |
||||
|
|
||||
|
<AbpScripts BundleName="@BlazorMudBlazorBasicThemeBundles.Scripts.Global" /> |
||||
|
|
||||
|
@* Monaco + Blazor Server bootstrap (mirrors the official Elsa Studio Server host). *@ |
||||
|
<script src="/_content/MudBlazor/MudBlazor.min.js"></script> |
||||
|
<script src="/_content/CodeBeam.MudBlazor.Extensions/MudExtensions.min.js"></script> |
||||
|
<script src="/_content/Radzen.Blazor/Radzen.Blazor.js"></script> |
||||
|
<script src="/_content/BlazorMonaco/jsInterop.js"></script> |
||||
|
<script src="/_content/BlazorMonaco/lib/monaco-editor/min/vs/loader.js"></script> |
||||
|
<script src="/_content/BlazorMonaco/lib/monaco-editor/min/vs/editor/editor.main.js"></script> |
||||
|
<script src="/_framework/blazor.web.js"></script> |
||||
|
|
||||
|
<div id="blazor-error-ui"> |
||||
|
@if (Env.IsDevelopment()) |
||||
|
{ |
||||
|
<text>An unhandled exception has occurred. See browser dev tools for details.</text> |
||||
|
} |
||||
|
else if (Env.IsStaging() || Env.IsProduction()) |
||||
|
{ |
||||
|
<text>An error has occurred. This application may no longer respond until reloaded.</text> |
||||
|
} |
||||
|
<a href="" class="reload">Reload</a> |
||||
|
<a class="dismiss">✖</a> |
||||
|
</div> |
||||
|
|
||||
|
</body> |
||||
|
</html> |
||||
@ -0,0 +1,137 @@ |
|||||
|
@page "/" |
||||
|
@using Microsoft.AspNetCore.Components.Authorization |
||||
|
@inject AuthenticationStateProvider AuthenticationStateProvider |
||||
|
@inject NavigationManager Navigation |
||||
|
|
||||
|
<MudGrid Class="mb-3"> |
||||
|
<MudItem xs="12" md="6"> |
||||
|
<MudCard Style="height: 100%;"> |
||||
|
<MudCardContent> |
||||
|
@if (!IsAuthenticated) |
||||
|
{ |
||||
|
<MudText Typo="Typo.h5" Class="mb-2">欢迎使用 Elsa 工作室</MudText> |
||||
|
<MudText Typo="Typo.body1" Class="mud-text-secondary mb-2"> |
||||
|
以 Blazor Server 模式集成的 Elsa 工作流工作室:服务端渲染与交互,接口调用在服务端完成,凭据不出浏览器。 |
||||
|
</MudText> |
||||
|
<MudButton Href="/Account/Login" Variant="Variant.Filled" Color="Color.Primary" Class="mb-4" |
||||
|
StartIcon="@Icons.Material.Filled.Login">登录</MudButton> |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
<MudText Typo="Typo.h5" Class="mb-2">欢迎回来</MudText> |
||||
|
<MudText Typo="Typo.body1" Class="mud-text-secondary mb-3"> |
||||
|
通过左侧菜单进入 <strong>Elsa 工作室</strong>:管理工作流定义与实例,打开设计器进行可视化编排。 |
||||
|
</MudText> |
||||
|
<MudButton StartIcon="@Icons.Material.Filled.AccountTree" Variant="Variant.Text" Color="Color.Primary" |
||||
|
OnClick="GoDefinitions">工作流定义</MudButton> |
||||
|
} |
||||
|
</MudCardContent> |
||||
|
</MudCard> |
||||
|
</MudItem> |
||||
|
<MudItem xs="12" md="6"> |
||||
|
<MudCard Style="height: 100%;"> |
||||
|
<MudCardContent> |
||||
|
<MudText Typo="Typo.h6" Color="Color.Primary" Class="mb-2"> |
||||
|
<MudIcon Icon="@Icons.Material.Filled.Info" Class="me-1" />关于 |
||||
|
</MudText> |
||||
|
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-3"> |
||||
|
服务端 Blazor Server(SignalR)承载 Elsa Studio;认证走外部 OIDC AuthServer 授权码 + PKCE; |
||||
|
Studio 页面以 ABP 子页面呈现(路由在菜单中定义)。 |
||||
|
</MudText> |
||||
|
<MudDivider Class="my-2" /> |
||||
|
<MudText Typo="Typo.body2" Class="mud-text-secondary"> |
||||
|
集成模块:LINGYUN.Abp.ElsaNext.Studio.Blazor |
||||
|
</MudText> |
||||
|
</MudCardContent> |
||||
|
</MudCard> |
||||
|
</MudItem> |
||||
|
</MudGrid> |
||||
|
|
||||
|
<MudGrid> |
||||
|
<MudItem xs="12" lg="4"> |
||||
|
<MudCard Style="height: 100%;"> |
||||
|
<MudCardHeader> |
||||
|
<MudText Typo="Typo.h6">More from ABP.IO</MudText> |
||||
|
</MudCardHeader> |
||||
|
<MudCardContent Class="pt-0"> |
||||
|
<MudList T="string" Dense="true"> |
||||
|
<MudListItem Icon="@Icons.Material.Filled.Check" IconColor="Color.Primary" |
||||
|
Href="https://github.com/abpframework/abp/releases" Target="_blank"> |
||||
|
Latest Release Logs |
||||
|
</MudListItem> |
||||
|
<MudListItem Icon="@Icons.Material.Filled.Check" IconColor="Color.Primary" |
||||
|
Href="https://abp.io/video-courses/essentials" Target="_blank"> |
||||
|
Video Courses |
||||
|
</MudListItem> |
||||
|
<MudListItem Icon="@Icons.Material.Filled.Check" IconColor="Color.Primary" |
||||
|
Href="https://docs.abp.io/en/abp/latest/Samples/Index" Target="_blank"> |
||||
|
Samples |
||||
|
</MudListItem> |
||||
|
<MudListItem Icon="@Icons.Material.Filled.Check" IconColor="Color.Primary" |
||||
|
Href="https://abp.io/books" Target="_blank"> |
||||
|
Books |
||||
|
</MudListItem> |
||||
|
<MudListItem Icon="@Icons.Material.Filled.Check" IconColor="Color.Primary" |
||||
|
Href="https://abp.io/faq" Target="_blank"> |
||||
|
FAQ |
||||
|
</MudListItem> |
||||
|
</MudList> |
||||
|
</MudCardContent> |
||||
|
</MudCard> |
||||
|
</MudItem> |
||||
|
<MudItem xs="12" lg="8"> |
||||
|
<MudCard Style="height: 100%;"> |
||||
|
<MudCardHeader> |
||||
|
<MudText Typo="Typo.h6">Follow us on Social Media</MudText> |
||||
|
</MudCardHeader> |
||||
|
<MudCardContent Class="pt-0"> |
||||
|
<MudGrid Spacing="0"> |
||||
|
<MudItem xs="12" sm="6"> |
||||
|
<MudList T="string" Dense="true"> |
||||
|
<MudListItem Href="https://twitter.com/abpframework" Target="_blank" |
||||
|
Icon="@Icons.Custom.Brands.Twitter" IconColor="Color.Primary"> |
||||
|
X.com (Twitter) |
||||
|
</MudListItem> |
||||
|
<MudListItem Href="https://community.abp.io/discord" Target="_blank" |
||||
|
Icon="@Icons.Custom.Brands.Discord" IconColor="Color.Primary"> |
||||
|
Discord |
||||
|
</MudListItem> |
||||
|
<MudListItem Href="https://stackoverflow.com/questions/tagged/abp" Target="_blank" |
||||
|
Icon="@Icons.Custom.Brands.StackOverflow" IconColor="Color.Primary"> |
||||
|
Stack Overflow |
||||
|
</MudListItem> |
||||
|
</MudList> |
||||
|
</MudItem> |
||||
|
<MudItem xs="12" sm="6"> |
||||
|
<MudList T="string" Dense="true"> |
||||
|
<MudListItem Href="@("https://www.youtube.com/@Volosoft")" Target="_blank" |
||||
|
Icon="@Icons.Custom.Brands.YouTube" IconColor="Color.Error"> |
||||
|
YouTube |
||||
|
</MudListItem> |
||||
|
<MudListItem Href="https://www.instagram.com/abpframework/" Target="_blank" |
||||
|
Icon="@Icons.Custom.Brands.Instagram" IconColor="Color.Secondary"> |
||||
|
Instagram |
||||
|
</MudListItem> |
||||
|
<MudListItem Href="https://github.com/abpframework/abp" Target="_blank" |
||||
|
Icon="@Icons.Custom.Brands.GitHub" IconColor="Color.Dark"> |
||||
|
GitHub |
||||
|
</MudListItem> |
||||
|
</MudList> |
||||
|
</MudItem> |
||||
|
</MudGrid> |
||||
|
</MudCardContent> |
||||
|
</MudCard> |
||||
|
</MudItem> |
||||
|
</MudGrid> |
||||
|
|
||||
|
@code { |
||||
|
private bool IsAuthenticated; |
||||
|
|
||||
|
protected override async Task OnInitializedAsync() |
||||
|
{ |
||||
|
var state = await AuthenticationStateProvider.GetAuthenticationStateAsync(); |
||||
|
IsAuthenticated = state.User.Identity?.IsAuthenticated == true; |
||||
|
} |
||||
|
|
||||
|
private void GoDefinitions() => Navigation.NavigateTo("/elsa/workflows/definitions"); |
||||
|
} |
||||
@ -0,0 +1,10 @@ |
|||||
|
@inject NavigationManager Navigation |
||||
|
|
||||
|
@code |
||||
|
{ |
||||
|
protected override void OnInitialized() |
||||
|
{ |
||||
|
var returnUrl = Uri.EscapeDataString(Navigation.Uri); |
||||
|
Navigation.NavigateTo($"/Account/Login?returnUrl={returnUrl}", forceLoad: true); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,13 @@ |
|||||
|
@using Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme.Themes.Basic |
||||
|
@using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Routing |
||||
|
@using Microsoft.Extensions.Options |
||||
|
@inject IOptions<AbpRouterOptions> RouterOptions |
||||
|
<Router AppAssembly="typeof(Program).Assembly" AdditionalAssemblies="RouterOptions.Value.AdditionalAssemblies"> |
||||
|
<Found Context="routeData"> |
||||
|
<AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(MainLayout)"> |
||||
|
<NotAuthorized> |
||||
|
<LoginRedirect /> |
||||
|
</NotAuthorized> |
||||
|
</AuthorizeRouteView> |
||||
|
</Found> |
||||
|
</Router> |
||||
@ -0,0 +1,12 @@ |
|||||
|
@using System.Net.Http |
||||
|
@using Microsoft.AspNetCore.Authorization |
||||
|
@using Microsoft.AspNetCore.Components.Authorization |
||||
|
@using Microsoft.AspNetCore.Components.Forms |
||||
|
@using Microsoft.AspNetCore.Components.Routing |
||||
|
@using Microsoft.AspNetCore.Components.Web |
||||
|
@using Microsoft.AspNetCore.Components.Web.Virtualization |
||||
|
@using static Microsoft.AspNetCore.Components.Web.RenderMode |
||||
|
@using Microsoft.JSInterop |
||||
|
@using MudBlazor |
||||
|
@using Volo.Abp.MudBlazorUI |
||||
|
@using Volo.Abp.MudBlazorUI.Components |
||||
@ -0,0 +1,42 @@ |
|||||
|
using Microsoft.AspNetCore.Authorization; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace LINGYUN.Abp.MicroService.WorkflowService; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Root <see cref="IAuthorizationPolicyProvider"/> for Shells hosting. Elsa 3.8.0-rc2 endpoints
|
||||
|
/// declare FastEndpoints permission policies named "epPolicy:<EndpointType>" that CShells
|
||||
|
/// registers inside the shell's own container, so ASP.NET's root policy provider (ABP's) cannot
|
||||
|
/// resolve them. ABP authenticates /elsa/api with the AuthServer JWT and every validated token
|
||||
|
/// carries Elsa's "all permissions" claim (see <see cref="WorkflowManagementNextBlazorServerModule"/>),
|
||||
|
/// so epPolicy names are bridged to that grant; other policy names fall back to the defaults.
|
||||
|
/// </summary>
|
||||
|
public sealed class ElsaShellPermissionPolicyProvider : IAuthorizationPolicyProvider |
||||
|
{ |
||||
|
// Allow-all policy (always-true assertion; AuthorizationPolicy rejects zero requirements).
|
||||
|
private static readonly AuthorizationPolicy AllowAll = new AuthorizationPolicyBuilder().RequireAssertion(_ => true).Build(); |
||||
|
private readonly DefaultAuthorizationPolicyProvider _default = new(Options.Create(new AuthorizationOptions())); |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
public Task<AuthorizationPolicy?> GetPolicyAsync(string policyName) |
||||
|
{ |
||||
|
if (!policyName.StartsWith("epPolicy:", StringComparison.Ordinal)) |
||||
|
return _default.GetPolicyAsync(policyName); |
||||
|
|
||||
|
var policy = new AuthorizationPolicyBuilder() |
||||
|
.RequireAuthenticatedUser() |
||||
|
.RequireAssertion(context => |
||||
|
context.User.HasClaim(Elsa.PermissionNames.ClaimType, Elsa.PermissionNames.All)) |
||||
|
.Build(); |
||||
|
|
||||
|
return Task.FromResult<AuthorizationPolicy?>(policy); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
public Task<AuthorizationPolicy?> GetDefaultPolicyAsync() => Task.FromResult<AuthorizationPolicy?>(AllowAll); |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
public Task<AuthorizationPolicy?> GetFallbackPolicyAsync() => Task.FromResult<AuthorizationPolicy?>(AllowAll); |
||||
|
} |
||||
@ -0,0 +1,130 @@ |
|||||
|
using Consul; |
||||
|
using Elsa.Studio.Authentication.Abstractions.ComponentProviders; |
||||
|
using Elsa.Studio.Authentication.Abstractions.Contracts; |
||||
|
using Elsa.Studio.Authentication.Abstractions.Models; |
||||
|
using Elsa.Studio.Authentication.OpenIdConnect.BlazorServer.Components; |
||||
|
using Elsa.Studio.Authentication.OpenIdConnect.BlazorServer.Services; |
||||
|
using Elsa.Studio.Authentication.OpenIdConnect.Contracts; |
||||
|
using Elsa.Studio.Authentication.OpenIdConnect.Models; |
||||
|
using Elsa.Studio.Authentication.OpenIdConnect.Services; |
||||
|
using Elsa.Studio.Contracts; |
||||
|
using Microsoft.AspNetCore.Authentication.Cookies; |
||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer; |
||||
|
using Microsoft.AspNetCore.Authentication.OpenIdConnect; |
||||
|
using Microsoft.Extensions.DependencyInjection; |
||||
|
using Microsoft.Extensions.DependencyInjection.Extensions; |
||||
|
using Polly; |
||||
|
using System; |
||||
|
using System.Linq; |
||||
|
using System.Net.Http; |
||||
|
|
||||
|
namespace LINGYUN.Abp.MicroService.WorkflowService.Extensions; |
||||
|
|
||||
|
public static class ElsaStudioServiceCollectionExtensions |
||||
|
{ |
||||
|
internal const string AnonymousHttpClientName = "Elsa.Studio.Authentication.OpenIdConnect.BlazorServer.Anonymous"; |
||||
|
|
||||
|
public static IServiceCollection AddOpenIdConnectAuth( |
||||
|
this IServiceCollection services, |
||||
|
Action<OidcOptions> configure, |
||||
|
Func<IAsyncPolicy<HttpResponseMessage>> configureRetryPolicy = null) |
||||
|
{ |
||||
|
var options = new OidcOptions(); |
||||
|
configure(options); |
||||
|
services.AddSingleton<StudioAuthenticationProviderRegistration>(new StudioAuthenticationProviderRegistration(StudioAuthenticationProvider.OpenIdConnect)); |
||||
|
// Ensure we always request the minimal identity scopes.
|
||||
|
var configuredScopes = options.AuthenticationScopes?.Where(s => !string.IsNullOrWhiteSpace(s)).Distinct(StringComparer.OrdinalIgnoreCase).ToArray() ?? Array.Empty<string>(); |
||||
|
if (configuredScopes.Length == 0) |
||||
|
configuredScopes = ["openid", "profile", "offline_access"]; |
||||
|
options.AuthenticationScopes = configuredScopes; |
||||
|
|
||||
|
// Set Blazor Server defaults for callback paths if not explicitly specified.
|
||||
|
options.CallbackPath ??= "/signin-oidc"; |
||||
|
options.SignedOutCallbackPath ??= "/signout-callback-oidc"; |
||||
|
|
||||
|
// Register core services
|
||||
|
services.AddHttpContextAccessor(); |
||||
|
services.AddSingleton(options); |
||||
|
services.AddScoped<ITokenProvider, ServerTokenProvider>(); |
||||
|
services.AddScoped<IHttpConnectionOptionsConfigurator, OidcHttpConnectionOptionsConfigurator>(); |
||||
|
|
||||
|
// Shared token refresh service used by both session refresh and backend API token acquisition
|
||||
|
services.AddScoped<TokenRefreshService>(); |
||||
|
|
||||
|
// Cookie authentication events for automatic session token refresh (standard ASP.NET Core pattern)
|
||||
|
services.AddScoped<AuthCookieEvents>(); |
||||
|
|
||||
|
// Configure ASP.NET Core authentication with cookie and OIDC
|
||||
|
services.AddAuthentication(authOptions => |
||||
|
{ |
||||
|
authOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; |
||||
|
authOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; |
||||
|
}) |
||||
|
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, cookieOptions => |
||||
|
{ |
||||
|
cookieOptions.Cookie.Name = "ElsaStudio.Auth"; |
||||
|
cookieOptions.Cookie.HttpOnly = true; |
||||
|
cookieOptions.Cookie.SecurePolicy = Microsoft.AspNetCore.Http.CookieSecurePolicy.Always; |
||||
|
cookieOptions.Cookie.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Lax; |
||||
|
cookieOptions.ExpireTimeSpan = TimeSpan.FromHours(8); |
||||
|
cookieOptions.SlidingExpiration = true; |
||||
|
cookieOptions.LoginPath = "/Account/Login"; |
||||
|
cookieOptions.AccessDeniedPath = "/Account/Login"; |
||||
|
|
||||
|
// Use custom events for automatic token refresh on every request validation
|
||||
|
cookieOptions.EventsType = typeof(AuthCookieEvents); |
||||
|
}) |
||||
|
.AddAbpOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, oidcOptions => |
||||
|
{ |
||||
|
oidcOptions.Authority = options.Authority; |
||||
|
oidcOptions.ClientId = options.ClientId; |
||||
|
oidcOptions.ClientSecret = options.ClientSecret; |
||||
|
oidcOptions.ResponseType = options.ResponseType; |
||||
|
oidcOptions.UsePkce = options.UsePkce; |
||||
|
oidcOptions.SaveTokens = options.SaveTokens; |
||||
|
oidcOptions.CallbackPath = options.CallbackPath; |
||||
|
oidcOptions.SignedOutCallbackPath = options.SignedOutCallbackPath; |
||||
|
oidcOptions.RequireHttpsMetadata = options.RequireHttpsMetadata; |
||||
|
oidcOptions.GetClaimsFromUserInfoEndpoint = options.GetClaimsFromUserInfoEndpoint; |
||||
|
oidcOptions.PushedAuthorizationBehavior = PushedAuthorizationBehavior.Disable; |
||||
|
|
||||
|
// Configure scopes
|
||||
|
oidcOptions.Scope.Clear(); |
||||
|
foreach (var scope in options.AuthenticationScopes) |
||||
|
{ |
||||
|
oidcOptions.Scope.Add(scope); |
||||
|
} |
||||
|
|
||||
|
// Map token response properties to enable token refresh
|
||||
|
oidcOptions.MapInboundClaims = false; |
||||
|
|
||||
|
if (!string.IsNullOrWhiteSpace(options.MetadataAddress)) |
||||
|
{ |
||||
|
oidcOptions.MetadataAddress = options.MetadataAddress; |
||||
|
} |
||||
|
|
||||
|
// Configure token validation parameters
|
||||
|
oidcOptions.TokenValidationParameters = new() |
||||
|
{ |
||||
|
NameClaimType = options.NameClaimType, |
||||
|
RoleClaimType = options.RoleClaimType, |
||||
|
ValidateIssuer = true |
||||
|
}; |
||||
|
}); |
||||
|
|
||||
|
// Add authorization services
|
||||
|
services.AddAuthorizationCore(); |
||||
|
|
||||
|
// Use an OIDC-aware unauthorized component that initiates a challenge.
|
||||
|
services.AddScoped<IUnauthorizedComponentProvider, UnauthorizedComponentProvider<ChallengeToLogin>>(); |
||||
|
services.AddScoped<ILoginMethodCatalog, DirectOpenIdConnectLoginMethodCatalog>(); |
||||
|
services.AddScoped<ILoginMethodComponentProvider, DirectOpenIdConnectLoginMethodComponentProvider>(); |
||||
|
|
||||
|
// HTTP client for token refresh requests with retry policy
|
||||
|
var retryPolicy = configureRetryPolicy?.Invoke() ?? Elsa.Studio.Authentication.OpenIdConnect.BlazorServer.Extensions.ServiceCollectionExtensions.DefaultRetryPolicy; |
||||
|
services.AddHttpClient(AnonymousHttpClientName) |
||||
|
.AddPolicyHandler(retryPolicy); |
||||
|
|
||||
|
return services; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,6 @@ |
|||||
|
{ |
||||
|
"culture": "en", |
||||
|
"texts": { |
||||
|
"Menu:Logout": "Logout" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,6 @@ |
|||||
|
{ |
||||
|
"culture": "zh-Hans", |
||||
|
"texts": { |
||||
|
"Menu:Logout": "退出登录" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,22 @@ |
|||||
|
using Localization.Resources.AbpUi; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.UI.Navigation; |
||||
|
|
||||
|
namespace LINGYUN.Abp.MicroService.WorkflowService.Navigation; |
||||
|
|
||||
|
public class UserMenuContributor : IMenuContributor |
||||
|
{ |
||||
|
public virtual Task ConfigureMenuAsync(MenuConfigurationContext context) |
||||
|
{ |
||||
|
if (context.Menu.Name != StandardMenus.User) |
||||
|
{ |
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
var uiResource = context.GetLocalizer<AbpUiResource>(); |
||||
|
|
||||
|
context.Menu.AddItem(new ApplicationMenuItem("Auth.Logout", uiResource["Menu:Logout"], url: "~/Account/Logout", icon: "fa fa-power-off", order: int.MaxValue - 1000)); |
||||
|
|
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,6 @@ |
|||||
|
@page "/Account/Login" |
||||
|
@model LINGYUN.Abp.MicroService.WorkflowService.Pages.Auth.LoginModel |
||||
|
@{ |
||||
|
ViewData["Title"] = "Login"; |
||||
|
} |
||||
|
<p>Redirecting to the identity provider…</p> |
||||
@ -0,0 +1,21 @@ |
|||||
|
using Microsoft.AspNetCore.Authentication; |
||||
|
using Microsoft.AspNetCore.Authentication.OpenIdConnect; |
||||
|
using Microsoft.AspNetCore.Mvc; |
||||
|
using Microsoft.AspNetCore.Mvc.RazorPages; |
||||
|
|
||||
|
namespace LINGYUN.Abp.MicroService.WorkflowService.Pages.Auth; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Triggers the standard OIDC authorization-code flow: signing in challenges the "oidc" scheme so
|
||||
|
/// the user is redirected to the external AuthServer, then back to this host's cookie.
|
||||
|
/// </summary>
|
||||
|
public class LoginModel : PageModel |
||||
|
{ |
||||
|
public IActionResult OnGet(string returnUrl = null) |
||||
|
{ |
||||
|
var redirectUri = string.IsNullOrWhiteSpace(returnUrl) ? "/" : returnUrl; |
||||
|
return Challenge( |
||||
|
new AuthenticationProperties { RedirectUri = redirectUri }, |
||||
|
OpenIdConnectDefaults.AuthenticationScheme); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,6 @@ |
|||||
|
@page "/Account/Logout" |
||||
|
@model LINGYUN.Abp.MicroService.WorkflowService.Pages.Auth.LogoutModel |
||||
|
@{ |
||||
|
ViewData["Title"] = "Logout"; |
||||
|
} |
||||
|
<p>Signing out…</p> |
||||
@ -0,0 +1,22 @@ |
|||||
|
using Microsoft.AspNetCore.Authentication; |
||||
|
using Microsoft.AspNetCore.Authentication.Cookies; |
||||
|
using Microsoft.AspNetCore.Authentication.OpenIdConnect; |
||||
|
using Microsoft.AspNetCore.Mvc; |
||||
|
using Microsoft.AspNetCore.Mvc.RazorPages; |
||||
|
|
||||
|
namespace LINGYUN.Abp.MicroService.WorkflowService.Pages.Auth; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Signs out the local cookie and ends the OIDC session at the external AuthServer.
|
||||
|
/// </summary>
|
||||
|
public class LogoutModel : PageModel |
||||
|
{ |
||||
|
public IActionResult OnGet(string returnUrl = null) |
||||
|
{ |
||||
|
var redirectUri = string.IsNullOrWhiteSpace(returnUrl) ? "/" : returnUrl; |
||||
|
return SignOut( |
||||
|
new AuthenticationProperties { RedirectUri = redirectUri }, |
||||
|
CookieAuthenticationDefaults.AuthenticationScheme, |
||||
|
OpenIdConnectDefaults.AuthenticationScheme); |
||||
|
} |
||||
|
} |
||||
@ -1,42 +0,0 @@ |
|||||
@page "/" |
|
||||
@inject IConfiguration Configuration; |
|
||||
@{ |
|
||||
var serverUrl = Configuration["Elsa:Server:BaseUrl"]; |
|
||||
} |
|
||||
<!DOCTYPE html> |
|
||||
<html lang="en"> |
|
||||
<head> |
|
||||
<meta charset="utf-8"/> |
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> |
|
||||
<title>Elsa Workflows</title> |
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/_content/Elsa.Designer.Components.Web/elsa-workflows-studio/assets/images/favicon-32x32.png"> |
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/_content/Elsa.Designer.Components.Web/elsa-workflows-studio/assets/images/favicon-16x16.png"> |
|
||||
<link rel="stylesheet" href="/_content/Elsa.Designer.Components.Web/elsa-workflows-studio/assets/fonts/inter/inter.css"> |
|
||||
<link rel="stylesheet" href="/_content/Elsa.Designer.Components.Web/elsa-workflows-studio/elsa-workflows-studio.css"> |
|
||||
<script src="/_content/Elsa.Designer.Components.Web/monaco-editor/min/vs/loader.js"></script> |
|
||||
<script type="module" src="/_content/Elsa.Designer.Components.Web/elsa-workflows-studio/elsa-workflows-studio.esm.js"></script> |
|
||||
</head> |
|
||||
<body> |
|
||||
<elsa-studio-root server-url="@serverUrl" base-path="" monaco-lib-path="./_content/Elsa.Designer.Components.Web/monaco-editor/min"> |
|
||||
<elsa-studio-dashboard></elsa-studio-dashboard> |
|
||||
</elsa-studio-root> |
|
||||
</body> |
|
||||
|
|
||||
<script type="module"> |
|
||||
// Import publicly exposed services. |
|
||||
import { WebhooksPlugin, WorkflowSettingsPlugin } from "/_content/Elsa.Designer.Components.Web/elsa-workflows-studio/index.esm.js"; |
|
||||
|
|
||||
// Get a handle to the elsa-studio-root element. |
|
||||
const elsaStudioRoot = document.querySelector('elsa-studio-root'); |
|
||||
|
|
||||
// Configure Elsa during the 'initializing' event. |
|
||||
elsaStudioRoot.addEventListener('initializing', e => { |
|
||||
const elsa = e.detail; |
|
||||
elsa.pluginManager.registerPlugins([ |
|
||||
WebhooksPlugin, |
|
||||
WorkflowSettingsPlugin |
|
||||
]); |
|
||||
}); |
|
||||
</script> |
|
||||
|
|
||||
</html> |
|
||||
@ -1,2 +1,3 @@ |
|||||
@using Microsoft.Extensions.Configuration |
@using Microsoft.AspNetCore.Authorization |
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers |
@using Microsoft.AspNetCore.Http |
||||
|
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers |
||||
|
|||||
@ -0,0 +1,8 @@ |
|||||
|
{ |
||||
|
"version": "10.6.0", |
||||
|
"name": "aspire-workflow-service", |
||||
|
"private": true, |
||||
|
"dependencies": { |
||||
|
"@abp/aspnetcore.components.server.basictheme": "10.6.0" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,49 @@ |
|||||
|
using Elsa.Studio.Extensions; |
||||
|
using LINGYUN.Abp.ElsaNext.Agents.Blazor.Navigation; |
||||
|
using LINGYUN.Abp.ElsaNext.Studio.Blazor; |
||||
|
using LINGYUN.Abp.ElsaNext.Studio.Translations.Localization; |
||||
|
using Microsoft.Extensions.DependencyInjection; |
||||
|
using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Routing; |
||||
|
using Volo.Abp.Localization; |
||||
|
using Volo.Abp.Modularity; |
||||
|
using Volo.Abp.UI.Navigation; |
||||
|
using Volo.Abp.VirtualFileSystem; |
||||
|
|
||||
|
namespace LINGYUN.Abp.ElsaNext.Agents.Blazor; |
||||
|
|
||||
|
[DependsOn( |
||||
|
typeof(AbpElsaNextAgentsModule), |
||||
|
typeof(AbpElsaNextStudioBlazorModule))] |
||||
|
public class AbpElsaNextAgentsBlazorModule : AbpModule |
||||
|
{ |
||||
|
public override void ConfigureServices(ServiceConfigurationContext context) |
||||
|
{ |
||||
|
var configuration = context.Configuration.GetSection("Elsa:Studio"); |
||||
|
|
||||
|
Configure<AbpRouterOptions>(options => |
||||
|
{ |
||||
|
options.AdditionalAssemblies.Add(typeof(Elsa.Studio.Agents.Feature).Assembly); |
||||
|
}); |
||||
|
|
||||
|
Configure<AbpNavigationOptions>(options => |
||||
|
{ |
||||
|
options.MenuContributors.Add(new ElsaStudioAgentsMenuContributor()); |
||||
|
}); |
||||
|
|
||||
|
Configure<AbpVirtualFileSystemOptions>(options => |
||||
|
{ |
||||
|
options.FileSets.AddEmbedded<AbpElsaNextAgentsBlazorModule>("LINGYUN.Abp.ElsaNext.Agents.Blazor"); |
||||
|
}); |
||||
|
|
||||
|
Configure<AbpLocalizationOptions>(options => |
||||
|
{ |
||||
|
options.Resources |
||||
|
.Get<ElsaStudioResource>() |
||||
|
.AddVirtualJson("/Localization/Resources/AgentsBlazor"); |
||||
|
}); |
||||
|
|
||||
|
var elsaNextStudioBlazoeOptions = context.Services.ExecutePreConfiguredActions<AbpElsaNextStudioBlazorOptions>(); |
||||
|
|
||||
|
context.Services.AddAgentsModule(elsaNextStudioBlazoeOptions.BackendApiConfig); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,3 @@ |
|||||
|
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
||||
|
<ConfigureAwait ContinueOnCapturedContext="false" /> |
||||
|
</Weavers> |
||||
@ -0,0 +1,30 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
||||
|
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
||||
|
<xs:element name="Weavers"> |
||||
|
<xs:complexType> |
||||
|
<xs:all> |
||||
|
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
||||
|
<xs:complexType> |
||||
|
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
||||
|
</xs:complexType> |
||||
|
</xs:element> |
||||
|
</xs:all> |
||||
|
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
</xs:complexType> |
||||
|
</xs:element> |
||||
|
</xs:schema> |
||||
@ -0,0 +1,29 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk.Razor"> |
||||
|
|
||||
|
<Import Project="..\..\..\..\configureawait.props" /> |
||||
|
<Import Project="..\..\..\..\common.props" /> |
||||
|
|
||||
|
<PropertyGroup> |
||||
|
<TargetFramework>net10.0</TargetFramework> |
||||
|
<AssemblyName>LINGYUN.Abp.ElsaNext.Agents.Blazor</AssemblyName> |
||||
|
<PackageId>LINGYUN.Abp.ElsaNext.Agents.Blazor</PackageId> |
||||
|
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute> |
||||
|
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute> |
||||
|
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<Content Remove="Localization\Resources\AgentsBlazor\*.json" /> |
||||
|
<EmbeddedResource Include="Localization\Resources\AgentsBlazor\*.json" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<PackageReference Include="Elsa.Studio.Agents" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<ProjectReference Include="..\LINGYUN.Abp.ElsaNext.Agents\LINGYUN.Abp.ElsaNext.Agents.csproj" /> |
||||
|
<ProjectReference Include="..\LINGYUN.Abp.ElsaNext.Studio.Blazor\LINGYUN.Abp.ElsaNext.Studio.Blazor.csproj" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
</Project> |
||||
@ -0,0 +1,7 @@ |
|||||
|
{ |
||||
|
"culture": "en", |
||||
|
"texts": { |
||||
|
"Menu:Agents": "Agents", |
||||
|
"Agents": "Agents" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,7 @@ |
|||||
|
{ |
||||
|
"culture": "zh-Hans", |
||||
|
"texts": { |
||||
|
"Menu:Agents": "代理列表", |
||||
|
"Agents": "代理列表" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,28 @@ |
|||||
|
using LINGYUN.Abp.ElsaNext.Studio.Blazor.Navigation; |
||||
|
using LINGYUN.Abp.ElsaNext.Studio.Translations.Localization; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.UI.Navigation; |
||||
|
|
||||
|
namespace LINGYUN.Abp.ElsaNext.Agents.Blazor.Navigation; |
||||
|
|
||||
|
public class ElsaStudioAgentsMenuContributor : IMenuContributor |
||||
|
{ |
||||
|
public Task ConfigureMenuAsync(MenuConfigurationContext context) |
||||
|
{ |
||||
|
if (context.Menu.Name == StandardMenus.Main) |
||||
|
{ |
||||
|
var menu = context.Menu; |
||||
|
var l = context.GetLocalizer<ElsaStudioResource>(); |
||||
|
|
||||
|
var group = menu.GetMenuItemOrNull(ElsaStudioMenus.GroupName); |
||||
|
group?.AddItem(new ApplicationMenuItem( |
||||
|
ElsaStudioMenus.GroupName + ".Agents", |
||||
|
l["Menu:Agents"], |
||||
|
url: "/ai/agents", |
||||
|
icon: "fa fa-bots", |
||||
|
order: 5)); |
||||
|
} |
||||
|
|
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,5 @@ |
|||||
|
@using Microsoft.AspNetCore.Authorization |
||||
|
@using Microsoft.AspNetCore.Components.Web |
||||
|
@using Elsa.Studio.Components |
||||
|
@using Elsa.Studio.Localization |
||||
|
@using MudBlazor |
||||
@ -0,0 +1,3 @@ |
|||||
|
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
||||
|
<ConfigureAwait ContinueOnCapturedContext="false" /> |
||||
|
</Weavers> |
||||
@ -0,0 +1,30 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
||||
|
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
||||
|
<xs:element name="Weavers"> |
||||
|
<xs:complexType> |
||||
|
<xs:all> |
||||
|
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
||||
|
<xs:complexType> |
||||
|
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
||||
|
</xs:complexType> |
||||
|
</xs:element> |
||||
|
</xs:all> |
||||
|
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
</xs:complexType> |
||||
|
</xs:element> |
||||
|
</xs:schema> |
||||
@ -0,0 +1,27 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
|
||||
|
<Import Project="..\..\..\..\configureawait.props" /> |
||||
|
<Import Project="..\..\..\..\common.props" /> |
||||
|
|
||||
|
<PropertyGroup> |
||||
|
<TargetFramework>net10.0</TargetFramework> |
||||
|
<AssemblyName>LINGYUN.Abp.ElsaNext.Agents</AssemblyName> |
||||
|
<PackageId>LINGYUN.Abp.ElsaNext.Agents</PackageId> |
||||
|
<ImplicitUsings>enable</ImplicitUsings> |
||||
|
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute> |
||||
|
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute> |
||||
|
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute> |
||||
|
<RootNamespace /> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<PackageReference Include="Elsa.Agents" /> |
||||
|
<PackageReference Include="Elsa.Agents.Api" /> |
||||
|
<PackageReference Include="Elsa.Agents.OpenAI" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<ProjectReference Include="..\LINGYUN.Abp.ElsaNext\LINGYUN.Abp.ElsaNext.csproj" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
</Project> |
||||
@ -0,0 +1,22 @@ |
|||||
|
using Elsa.Agents; |
||||
|
using Elsa.Extensions; |
||||
|
using Elsa.Features.Services; |
||||
|
using Volo.Abp.Modularity; |
||||
|
|
||||
|
namespace LINGYUN.Abp.ElsaNext.Agents; |
||||
|
|
||||
|
[DependsOn( |
||||
|
typeof(AbpElsaNextModule))] |
||||
|
public class AbpElsaNextAgentsModule : AbpModule |
||||
|
{ |
||||
|
public override void PreConfigureServices(ServiceConfigurationContext context) |
||||
|
{ |
||||
|
PreConfigure<IModule>(elsa => |
||||
|
{ |
||||
|
elsa.UseAgents(); |
||||
|
elsa.UseAgentActivities(); |
||||
|
|
||||
|
elsa.UseAgentsApi(); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,3 @@ |
|||||
|
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
||||
|
<ConfigureAwait ContinueOnCapturedContext="false" /> |
||||
|
</Weavers> |
||||
@ -0,0 +1,30 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
||||
|
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
||||
|
<xs:element name="Weavers"> |
||||
|
<xs:complexType> |
||||
|
<xs:all> |
||||
|
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
||||
|
<xs:complexType> |
||||
|
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
||||
|
</xs:complexType> |
||||
|
</xs:element> |
||||
|
</xs:all> |
||||
|
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
</xs:complexType> |
||||
|
</xs:element> |
||||
|
</xs:schema> |
||||
@ -0,0 +1,25 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
|
||||
|
<Import Project="..\..\..\..\configureawait.props" /> |
||||
|
<Import Project="..\..\..\..\common.props" /> |
||||
|
|
||||
|
<PropertyGroup> |
||||
|
<TargetFramework>net10.0</TargetFramework> |
||||
|
<AssemblyName>LINGYUN.Abp.ElsaNext.EntityFrameworkCore</AssemblyName> |
||||
|
<PackageId>LINGYUN.Abp.ElsaNext.EntityFrameworkCore</PackageId> |
||||
|
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute> |
||||
|
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute> |
||||
|
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute> |
||||
|
<RootNamespace /> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<PackageReference Include="Elsa.Persistence.EFCore" VersionOverride="$(ElsaNextPackageVersion)" /> |
||||
|
<PackageReference Include="Volo.Abp.EntityFrameworkCore" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<ProjectReference Include="..\LINGYUN.Abp.ElsaNext\LINGYUN.Abp.ElsaNext.csproj" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
</Project> |
||||
@ -0,0 +1,57 @@ |
|||||
|
using Elsa.Alterations.Extensions; |
||||
|
using Elsa.Extensions; |
||||
|
using Elsa.Features.Services; |
||||
|
using Elsa.Persistence.EFCore.Modules.Alterations; |
||||
|
using Elsa.Persistence.EFCore.Modules.Identity; |
||||
|
using Elsa.Persistence.EFCore.Modules.Labels; |
||||
|
using Elsa.Persistence.EFCore.Modules.Management; |
||||
|
using Elsa.Persistence.EFCore.Modules.Runtime; |
||||
|
using Elsa.Persistence.EFCore.Modules.Tenants; |
||||
|
using Microsoft.Extensions.DependencyInjection; |
||||
|
using Volo.Abp.Modularity; |
||||
|
|
||||
|
namespace LINGYUN.Abp.ElsaNext.EntityFrameworkCore; |
||||
|
|
||||
|
[DependsOn(typeof(AbpElsaNextModule))] |
||||
|
public class AbpElsaNextEntityFrameworkCoreModule : AbpModule |
||||
|
{ |
||||
|
public override void PreConfigureServices(ServiceConfigurationContext context) |
||||
|
{ |
||||
|
var labelFeature = context.Services.GetPreConfigureActions<EFCoreLabelPersistenceFeature>(); |
||||
|
var alterationsFeature = context.Services.GetPreConfigureActions<EFCoreAlterationsPersistenceFeature>(); |
||||
|
var workflowManagementFeature = context.Services.GetPreConfigureActions<WorkflowManagementPersistenceFeature>(); |
||||
|
var workflowRuntimeFeature = context.Services.GetPreConfigureActions<EFCoreWorkflowRuntimePersistenceFeature>(); |
||||
|
|
||||
|
PreConfigure<IModule>(elsa => |
||||
|
{ |
||||
|
elsa.UseLabels(label => |
||||
|
{ |
||||
|
label.UseEntityFrameworkCore(efCore => |
||||
|
{ |
||||
|
labelFeature.Configure(efCore); |
||||
|
}); |
||||
|
}); |
||||
|
elsa.UseAlterations(alterations => |
||||
|
{ |
||||
|
alterations.UseEntityFrameworkCore(efCore => |
||||
|
{ |
||||
|
alterationsFeature.Configure(efCore); |
||||
|
}); |
||||
|
}); |
||||
|
elsa.UseWorkflowManagement(management => |
||||
|
{ |
||||
|
management.UseEntityFrameworkCore(efCore => |
||||
|
{ |
||||
|
workflowManagementFeature.Configure(efCore); |
||||
|
}); |
||||
|
}); |
||||
|
elsa.UseWorkflowRuntime(runtime => |
||||
|
{ |
||||
|
runtime.UseEntityFrameworkCore(efCore => |
||||
|
{ |
||||
|
workflowRuntimeFeature.Configure(efCore); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,49 @@ |
|||||
|
using Elsa.Studio.Secrets.Extensions; |
||||
|
using LINGYUN.Abp.ElsaNext.Secrets.Blazor.Navigation; |
||||
|
using LINGYUN.Abp.ElsaNext.Studio.Blazor; |
||||
|
using LINGYUN.Abp.ElsaNext.Studio.Translations.Localization; |
||||
|
using Microsoft.Extensions.DependencyInjection; |
||||
|
using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Routing; |
||||
|
using Volo.Abp.Localization; |
||||
|
using Volo.Abp.Modularity; |
||||
|
using Volo.Abp.UI.Navigation; |
||||
|
using Volo.Abp.VirtualFileSystem; |
||||
|
|
||||
|
namespace LINGYUN.Abp.ElsaNext.Secrets.Blazor; |
||||
|
|
||||
|
[DependsOn( |
||||
|
typeof(AbpElsaNextSecretsModule), |
||||
|
typeof(AbpElsaNextStudioBlazorModule))] |
||||
|
public class AbpElsaNextSecretsBlazorModule : AbpModule |
||||
|
{ |
||||
|
public override void ConfigureServices(ServiceConfigurationContext context) |
||||
|
{ |
||||
|
var configuration = context.Configuration.GetSection("Elsa:Studio"); |
||||
|
|
||||
|
Configure<AbpRouterOptions>(options => |
||||
|
{ |
||||
|
options.AdditionalAssemblies.Add(typeof(Elsa.Studio.Secrets.Feature).Assembly); |
||||
|
}); |
||||
|
|
||||
|
Configure<AbpNavigationOptions>(options => |
||||
|
{ |
||||
|
options.MenuContributors.Add(new ElsaStudioSecretsMenuContributor()); |
||||
|
}); |
||||
|
|
||||
|
Configure<AbpVirtualFileSystemOptions>(options => |
||||
|
{ |
||||
|
options.FileSets.AddEmbedded<AbpElsaNextSecretsBlazorModule>("LINGYUN.Abp.ElsaNext.Secrets.Blazor"); |
||||
|
}); |
||||
|
|
||||
|
Configure<AbpLocalizationOptions>(options => |
||||
|
{ |
||||
|
options.Resources |
||||
|
.Get<ElsaStudioResource>() |
||||
|
.AddVirtualJson("/Localization/Resources/SecretsBlazor"); |
||||
|
}); |
||||
|
|
||||
|
var elsaNextStudioBlazoeOptions = context.Services.ExecutePreConfiguredActions<AbpElsaNextStudioBlazorOptions>(); |
||||
|
|
||||
|
context.Services.AddSecretsModule(elsaNextStudioBlazoeOptions.BackendApiConfig); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,3 @@ |
|||||
|
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
||||
|
<ConfigureAwait ContinueOnCapturedContext="false" /> |
||||
|
</Weavers> |
||||
@ -0,0 +1,30 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
||||
|
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
||||
|
<xs:element name="Weavers"> |
||||
|
<xs:complexType> |
||||
|
<xs:all> |
||||
|
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
||||
|
<xs:complexType> |
||||
|
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
||||
|
</xs:complexType> |
||||
|
</xs:element> |
||||
|
</xs:all> |
||||
|
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
</xs:complexType> |
||||
|
</xs:element> |
||||
|
</xs:schema> |
||||
@ -0,0 +1,29 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk.Razor"> |
||||
|
|
||||
|
<Import Project="..\..\..\..\configureawait.props" /> |
||||
|
<Import Project="..\..\..\..\common.props" /> |
||||
|
|
||||
|
<PropertyGroup> |
||||
|
<TargetFramework>net10.0</TargetFramework> |
||||
|
<AssemblyName>LINGYUN.Abp.ElsaNext.Secrets.Blazor</AssemblyName> |
||||
|
<PackageId>LINGYUN.Abp.ElsaNext.Secrets.Blazor</PackageId> |
||||
|
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute> |
||||
|
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute> |
||||
|
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<Content Remove="Localization\Resources\SecretsBlazor\*.json" /> |
||||
|
<EmbeddedResource Include="Localization\Resources\SecretsBlazor\*.json" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<PackageReference Include="Elsa.Studio.Secrets" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<ProjectReference Include="..\LINGYUN.Abp.ElsaNext.Secrets\LINGYUN.Abp.ElsaNext.Secrets.csproj" /> |
||||
|
<ProjectReference Include="..\LINGYUN.Abp.ElsaNext.Studio.Blazor\LINGYUN.Abp.ElsaNext.Studio.Blazor.csproj" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
</Project> |
||||
@ -0,0 +1,7 @@ |
|||||
|
{ |
||||
|
"culture": "en", |
||||
|
"texts": { |
||||
|
"Menu:Secrets": "Secrets", |
||||
|
"Secrets": "Secrets" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,7 @@ |
|||||
|
{ |
||||
|
"culture": "zh-Hans", |
||||
|
"texts": { |
||||
|
"Menu:Secrets": "机密列表", |
||||
|
"Secrets": "机密列表" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,28 @@ |
|||||
|
using LINGYUN.Abp.ElsaNext.Studio.Blazor.Navigation; |
||||
|
using LINGYUN.Abp.ElsaNext.Studio.Translations.Localization; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.UI.Navigation; |
||||
|
|
||||
|
namespace LINGYUN.Abp.ElsaNext.Secrets.Blazor.Navigation; |
||||
|
|
||||
|
public class ElsaStudioSecretsMenuContributor : IMenuContributor |
||||
|
{ |
||||
|
public Task ConfigureMenuAsync(MenuConfigurationContext context) |
||||
|
{ |
||||
|
if (context.Menu.Name == StandardMenus.Main) |
||||
|
{ |
||||
|
var menu = context.Menu; |
||||
|
var l = context.GetLocalizer<ElsaStudioResource>(); |
||||
|
|
||||
|
var group = menu.GetMenuItemOrNull(ElsaStudioMenus.GroupName); |
||||
|
group?.AddItem(new ApplicationMenuItem( |
||||
|
ElsaStudioMenus.GroupName + ".Secrets", |
||||
|
l["Menu:Secrets"], |
||||
|
url: "/security/secrets", |
||||
|
icon: "fa fa-key", |
||||
|
order: 6)); |
||||
|
} |
||||
|
|
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,5 @@ |
|||||
|
@using Microsoft.AspNetCore.Authorization |
||||
|
@using Microsoft.AspNetCore.Components.Web |
||||
|
@using Elsa.Studio.Components |
||||
|
@using Elsa.Studio.Localization |
||||
|
@using MudBlazor |
||||
@ -0,0 +1,3 @@ |
|||||
|
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
||||
|
<ConfigureAwait ContinueOnCapturedContext="false" /> |
||||
|
</Weavers> |
||||
@ -0,0 +1,30 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
||||
|
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
||||
|
<xs:element name="Weavers"> |
||||
|
<xs:complexType> |
||||
|
<xs:all> |
||||
|
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
||||
|
<xs:complexType> |
||||
|
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
||||
|
</xs:complexType> |
||||
|
</xs:element> |
||||
|
</xs:all> |
||||
|
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
</xs:complexType> |
||||
|
</xs:element> |
||||
|
</xs:schema> |
||||
@ -0,0 +1,26 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
|
||||
|
<Import Project="..\..\..\..\configureawait.props" /> |
||||
|
<Import Project="..\..\..\..\common.props" /> |
||||
|
|
||||
|
<PropertyGroup> |
||||
|
<TargetFramework>net10.0</TargetFramework> |
||||
|
<AssemblyName>LINGYUN.Abp.ElsaNext.Secrets</AssemblyName> |
||||
|
<PackageId>LINGYUN.Abp.ElsaNext.Secrets</PackageId> |
||||
|
<ImplicitUsings>enable</ImplicitUsings> |
||||
|
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute> |
||||
|
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute> |
||||
|
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute> |
||||
|
<RootNamespace /> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<PackageReference Include="Elsa.Secrets" /> |
||||
|
<PackageReference Include="Elsa.Secrets.JavaScript" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<ProjectReference Include="..\LINGYUN.Abp.ElsaNext\LINGYUN.Abp.ElsaNext.csproj" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
</Project> |
||||
@ -0,0 +1,24 @@ |
|||||
|
using Elsa.Extensions; |
||||
|
using Elsa.Features.Services; |
||||
|
using Microsoft.Extensions.Configuration; |
||||
|
using Volo.Abp.Modularity; |
||||
|
|
||||
|
namespace LINGYUN.Abp.ElsaNext.Secrets; |
||||
|
|
||||
|
[DependsOn( |
||||
|
typeof(AbpElsaNextModule))] |
||||
|
public class AbpElsaNextSecretsModule : AbpModule |
||||
|
{ |
||||
|
public override void PreConfigureServices(ServiceConfigurationContext context) |
||||
|
{ |
||||
|
var configuration = context.Configuration.GetSection("Elsa"); |
||||
|
PreConfigure<IModule>(elsa => |
||||
|
{ |
||||
|
elsa.UseSecrets(secret => |
||||
|
{ |
||||
|
secret.ConfigureOptions = options => configuration.GetSection("Secrets").Bind(options); |
||||
|
}); |
||||
|
elsa.UseSecretsJavaScript(); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,97 @@ |
|||||
|
using Elsa.Studio.Core.BlazorServer.Extensions; |
||||
|
using Elsa.Studio.Extensions; |
||||
|
using Elsa.Studio.Localization.BlazorServer.Extensions; |
||||
|
using Elsa.Studio.Localization.Models; |
||||
|
using Elsa.Studio.Models; |
||||
|
using Elsa.Studio.Settings.Extensions; |
||||
|
using Elsa.Studio.Shell.Extensions; |
||||
|
using Elsa.Studio.Workflows.Designer.Options; |
||||
|
using Elsa.Studio.Workflows.Extensions; |
||||
|
using LINGYUN.Abp.ElsaNext.Localization; |
||||
|
using LINGYUN.Abp.ElsaNext.Studio.Blazor.Navigation; |
||||
|
using LINGYUN.Abp.ElsaNext.Studio.Translations; |
||||
|
using LINGYUN.Abp.ElsaNext.Studio.Translations.Localization; |
||||
|
using Microsoft.Extensions.Configuration; |
||||
|
using Microsoft.Extensions.DependencyInjection; |
||||
|
using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor; |
||||
|
using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Routing; |
||||
|
using Volo.Abp.Localization; |
||||
|
using Volo.Abp.Modularity; |
||||
|
using Volo.Abp.UI.Navigation; |
||||
|
using Volo.Abp.VirtualFileSystem; |
||||
|
|
||||
|
namespace LINGYUN.Abp.ElsaNext.Studio.Blazor; |
||||
|
|
||||
|
[DependsOn( |
||||
|
typeof(AbpElsaNextModule), |
||||
|
typeof(AbpElsaNextStudioTranslationsModule), |
||||
|
typeof(AbpLocalizationModule), |
||||
|
typeof(AbpUiNavigationModule), |
||||
|
typeof(AbpVirtualFileSystemModule), |
||||
|
typeof(AbpAspNetCoreComponentsWebThemingMudBlazorModule))] |
||||
|
public class AbpElsaNextStudioBlazorModule : AbpModule |
||||
|
{ |
||||
|
public override void PreConfigureServices(ServiceConfigurationContext context) |
||||
|
{ |
||||
|
var configuration = context.Configuration.GetSection("Elsa:Studio"); |
||||
|
|
||||
|
PreConfigure<AbpElsaNextStudioBlazorOptions>(options => |
||||
|
{ |
||||
|
options.BackendApiConfig = new BackendApiConfig |
||||
|
{ |
||||
|
ConfigureBackendOptions = backend => configuration.GetSection("Backend").Bind(backend), |
||||
|
}; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public override void ConfigureServices(ServiceConfigurationContext context) |
||||
|
{ |
||||
|
var configuration = context.Configuration.GetSection("Elsa:Studio"); |
||||
|
|
||||
|
Configure<AbpNavigationOptions>(options => |
||||
|
{ |
||||
|
options.MenuContributors.Add(new ElsaStudioMenuContributor()); |
||||
|
}); |
||||
|
|
||||
|
Configure<AbpRouterOptions>(options => |
||||
|
{ |
||||
|
options.AdditionalAssemblies.Add(typeof(Elsa.Studio.Settings.Feature).Assembly); |
||||
|
options.AdditionalAssemblies.Add(typeof(Elsa.Studio.Workflows.Feature).Assembly); |
||||
|
}); |
||||
|
|
||||
|
Configure<AbpVirtualFileSystemOptions>(options => |
||||
|
{ |
||||
|
options.FileSets.AddEmbedded<AbpElsaNextStudioBlazorModule>("LINGYUN.Abp.ElsaNext.Studio.Blazor"); |
||||
|
}); |
||||
|
|
||||
|
Configure<AbpLocalizationOptions>(options => |
||||
|
{ |
||||
|
options.Resources |
||||
|
.Get<ElsaStudioResource>() |
||||
|
.AddBaseTypes(typeof(ElsaNextResource)) |
||||
|
.AddVirtualJson("/Localization/Resources/ElsaStudioBlazor"); |
||||
|
}); |
||||
|
|
||||
|
context.Services.AddCore(options => configuration.GetSection("Presentation").Bind(options)); |
||||
|
context.Services.AddShell(options => configuration.GetSection("Shell").Bind(options)); |
||||
|
|
||||
|
var elsaNextStudioBlazoeOptions = context.Services.ExecutePreConfiguredActions<AbpElsaNextStudioBlazorOptions>(); |
||||
|
|
||||
|
context.Services.AddRemoteBackend(elsaNextStudioBlazoeOptions.BackendApiConfig); |
||||
|
|
||||
|
context.Services.AddWorkflowsModule(); |
||||
|
context.Services.AddSettingsModule(); |
||||
|
|
||||
|
Configure<DesignerOptions>(configuration.GetSection("DesignerOptions")); |
||||
|
|
||||
|
var abpLocalizationOptions = context.Services.ExecutePreConfiguredActions<AbpLocalizationOptions>(); |
||||
|
context.Services.AddLocalizationModule(new LocalizationConfig |
||||
|
{ |
||||
|
ConfigureLocalizationOptions = options => |
||||
|
{ |
||||
|
options.SupportedCultures = abpLocalizationOptions.Languages.Select(x => x.CultureName).ToArray(); |
||||
|
} |
||||
|
}); |
||||
|
context.Services.AddElsaStudioTranslations(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,12 @@ |
|||||
|
using Elsa.Studio.Models; |
||||
|
|
||||
|
namespace LINGYUN.Abp.ElsaNext.Studio.Blazor; |
||||
|
|
||||
|
public class AbpElsaNextStudioBlazorOptions |
||||
|
{ |
||||
|
public BackendApiConfig BackendApiConfig { get; set; } |
||||
|
public AbpElsaNextStudioBlazorOptions() |
||||
|
{ |
||||
|
BackendApiConfig = new BackendApiConfig(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,3 @@ |
|||||
|
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
||||
|
<ConfigureAwait ContinueOnCapturedContext="false" /> |
||||
|
</Weavers> |
||||
@ -0,0 +1,30 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
||||
|
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
||||
|
<xs:element name="Weavers"> |
||||
|
<xs:complexType> |
||||
|
<xs:all> |
||||
|
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
||||
|
<xs:complexType> |
||||
|
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
||||
|
</xs:complexType> |
||||
|
</xs:element> |
||||
|
</xs:all> |
||||
|
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
</xs:complexType> |
||||
|
</xs:element> |
||||
|
</xs:schema> |
||||
@ -0,0 +1,43 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk.Razor"> |
||||
|
|
||||
|
<Import Project="..\..\..\..\configureawait.props" /> |
||||
|
<Import Project="..\..\..\..\common.props" /> |
||||
|
|
||||
|
<PropertyGroup> |
||||
|
<TargetFramework>net10.0</TargetFramework> |
||||
|
<Nullable>enable</Nullable> |
||||
|
<ImplicitUsings>enable</ImplicitUsings> |
||||
|
<AssemblyName>LINGYUN.Abp.ElsaNext.Studio.Blazor</AssemblyName> |
||||
|
<PackageId>LINGYUN.Abp.ElsaNext.Studio.Blazor</PackageId> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<PackageReference Include="Volo.Abp.Localization" /> |
||||
|
<PackageReference Include="Volo.Abp.UI.Navigation" /> |
||||
|
<PackageReference Include="Volo.Abp.VirtualFileSystem" /> |
||||
|
<PackageReference Include="Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<ProjectReference Include="..\LINGYUN.Abp.ElsaNext\LINGYUN.Abp.ElsaNext.csproj" /> |
||||
|
<!-- ABP-hosted Elsa Studio UI copy (en/zh-Hans) + Elsa ILocalizationProvider bridge. --> |
||||
|
<ProjectReference Include="..\LINGYUN.Abp.ElsaNext.Studio.Translations\LINGYUN.Abp.ElsaNext.Studio.Translations.csproj" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<!-- Elsa Studio, Blazor Server hosting pieces (Elsa.Studio 3.8.0-rc2). --> |
||||
|
<PackageReference Include="Elsa.Studio.Core.BlazorServer" /> |
||||
|
<PackageReference Include="Elsa.Studio.Settings" /> |
||||
|
<PackageReference Include="Elsa.Studio.Shell" /> |
||||
|
<PackageReference Include="Elsa.Studio.Workflows" /> |
||||
|
<PackageReference Include="Elsa.Studio.Workflows.Designer" /> |
||||
|
<PackageReference Include="Elsa.Api.Client" /> |
||||
|
<PackageReference Include="Elsa.Studio.Authentication.Abstractions" /> |
||||
|
<PackageReference Include="Elsa.Studio.Localization.BlazorServer" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<EmbeddedResource Include="Localization\**\*.json" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
</Project> |
||||
@ -0,0 +1,9 @@ |
|||||
|
{ |
||||
|
"culture": "en", |
||||
|
"texts": { |
||||
|
"Menu:ElsaStudio": "Elsa Studio", |
||||
|
"Menu:WorkflowDefinitions": "Workflow Definitions", |
||||
|
"Menu:WorkflowInstances": "Workflow Instances", |
||||
|
"Menu:Settings": "Settings" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,9 @@ |
|||||
|
{ |
||||
|
"culture": "zh-Hans", |
||||
|
"texts": { |
||||
|
"Menu:ElsaStudio": "Elsa 工作室", |
||||
|
"Menu:WorkflowDefinitions": "工作流定义", |
||||
|
"Menu:WorkflowInstances": "工作流实例", |
||||
|
"Menu:Settings": "设置" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,49 @@ |
|||||
|
using LINGYUN.Abp.ElsaNext.Studio.Translations.Localization; |
||||
|
using Volo.Abp.UI.Navigation; |
||||
|
|
||||
|
namespace LINGYUN.Abp.ElsaNext.Studio.Blazor.Navigation; |
||||
|
|
||||
|
public class ElsaStudioMenuContributor : IMenuContributor |
||||
|
{ |
||||
|
public Task ConfigureMenuAsync(MenuConfigurationContext context) |
||||
|
{ |
||||
|
if (context.Menu.Name == StandardMenus.Main) |
||||
|
{ |
||||
|
AddStudioMenu(context); |
||||
|
} |
||||
|
|
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
private static void AddStudioMenu(MenuConfigurationContext context) |
||||
|
{ |
||||
|
var l = context.GetLocalizer<ElsaStudioResource>(); |
||||
|
|
||||
|
var elsaStudioMenu = new ApplicationMenuItem( |
||||
|
ElsaStudioMenus.GroupName, |
||||
|
l["Menu:ElsaStudio"], |
||||
|
icon: "fa fa-diagram-project"); |
||||
|
|
||||
|
elsaStudioMenu.AddItem(new ApplicationMenuItem( |
||||
|
ElsaStudioMenus.GroupName + ".WorkflowDefinitions", |
||||
|
l["Menu:WorkflowDefinitions"], |
||||
|
url: "/workflows/definitions", |
||||
|
icon: "fa fa-list", |
||||
|
order: 1)); |
||||
|
elsaStudioMenu.AddItem(new ApplicationMenuItem( |
||||
|
ElsaStudioMenus.GroupName + ".WorkflowInstances", |
||||
|
l["Menu:WorkflowInstances"], |
||||
|
url: "/workflows/instances", |
||||
|
icon: "fa fa-cogs", |
||||
|
order: 2)); |
||||
|
|
||||
|
elsaStudioMenu.AddItem(new ApplicationMenuItem( |
||||
|
ElsaStudioMenus.GroupName + ".Settings", |
||||
|
l["Menu:Settings"], |
||||
|
url: "/settings", |
||||
|
icon: "fa fa-gear", |
||||
|
order: 99)); |
||||
|
|
||||
|
context.Menu.AddItem(elsaStudioMenu); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,6 @@ |
|||||
|
namespace LINGYUN.Abp.ElsaNext.Studio.Blazor.Navigation; |
||||
|
|
||||
|
public static class ElsaStudioMenus |
||||
|
{ |
||||
|
public const string GroupName = "ElsaStudio"; |
||||
|
} |
||||
@ -0,0 +1,26 @@ |
|||||
|
{ |
||||
|
"iisSettings": { |
||||
|
"iisExpress": { |
||||
|
"applicationUrl": "http://localhost:34746", |
||||
|
"sslPort": 0 |
||||
|
} |
||||
|
}, |
||||
|
"profiles": { |
||||
|
"http": { |
||||
|
"commandName": "Project", |
||||
|
"dotnetRunMessages": true, |
||||
|
"launchBrowser": true, |
||||
|
"applicationUrl": "http://localhost:5130", |
||||
|
"environmentVariables": { |
||||
|
"ASPNETCORE_ENVIRONMENT": "Development" |
||||
|
} |
||||
|
}, |
||||
|
"IIS Express": { |
||||
|
"commandName": "IISExpress", |
||||
|
"launchBrowser": true, |
||||
|
"environmentVariables": { |
||||
|
"ASPNETCORE_ENVIRONMENT": "Development" |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,12 @@ |
|||||
|
@using Microsoft.AspNetCore.Authorization |
||||
|
@using Microsoft.AspNetCore.Components |
||||
|
@using Microsoft.AspNetCore.Components.Rendering |
||||
|
@using Microsoft.AspNetCore.Components.Routing |
||||
|
@using Microsoft.AspNetCore.Components.Web |
||||
|
@using Microsoft.JSInterop |
||||
|
@using MudBlazor |
||||
|
@using Elsa.Studio.Components |
||||
|
@using Elsa.Studio.Workflows.Components.WorkflowDefinitionEditor |
||||
|
@using Elsa.Studio.Workflows.Components.WorkflowInstanceViewer |
||||
|
@using Elsa.Studio.Workflows.Components.WorkflowDefinitionList |
||||
|
@using Elsa.Studio.Workflows.Components.WorkflowInstanceList |
||||
@ -0,0 +1,61 @@ |
|||||
|
using Elsa.Extensions; |
||||
|
using Elsa.Features.Services; |
||||
|
using Elsa.Studio.Dashboard.Extensions; |
||||
|
using LINGYUN.Abp.ElsaNext.Studio.Blazor; |
||||
|
using LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor.Extensions; |
||||
|
using LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor.Navigation; |
||||
|
using LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor.Services; |
||||
|
using LINGYUN.Abp.ElsaNext.Studio.Translations.Localization; |
||||
|
using Microsoft.Extensions.DependencyInjection; |
||||
|
using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Routing; |
||||
|
using Volo.Abp.Localization; |
||||
|
using Volo.Abp.Modularity; |
||||
|
using Volo.Abp.UI.Navigation; |
||||
|
using Volo.Abp.VirtualFileSystem; |
||||
|
|
||||
|
namespace LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor; |
||||
|
|
||||
|
[DependsOn(typeof(AbpElsaNextStudioBlazorModule))] |
||||
|
public class AbpElsaNextStudioDashboardBlazorModule : AbpModule |
||||
|
{ |
||||
|
public override void PreConfigureServices(ServiceConfigurationContext context) |
||||
|
{ |
||||
|
PreConfigure<IModule>(elsa => |
||||
|
{ |
||||
|
elsa.UseWorkflowRuntimeDashboard(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public override void ConfigureServices(ServiceConfigurationContext context) |
||||
|
{ |
||||
|
var configuration = context.Configuration.GetSection("Elsa:Studio"); |
||||
|
|
||||
|
Configure<AbpRouterOptions>(options => |
||||
|
{ |
||||
|
options.AdditionalAssemblies.Add(typeof(AbpElsaNextStudioDashboardBlazorModule).Assembly); |
||||
|
}); |
||||
|
|
||||
|
Configure<AbpNavigationOptions>(options => |
||||
|
{ |
||||
|
options.MenuContributors.Add(new ElsaStudioDashboardMenuContributor()); |
||||
|
}); |
||||
|
|
||||
|
Configure<AbpVirtualFileSystemOptions>(options => |
||||
|
{ |
||||
|
options.FileSets.AddEmbedded<AbpElsaNextStudioDashboardBlazorModule>("LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor"); |
||||
|
}); |
||||
|
Configure<AbpLocalizationOptions>(options => |
||||
|
{ |
||||
|
options.Resources |
||||
|
.Get<ElsaStudioResource>() |
||||
|
.AddVirtualJson("/Localization/Resources/ElsaStudioDashboardBlazor"); |
||||
|
}); |
||||
|
|
||||
|
var elsaNextStudioBlazoeOptions = context.Services.ExecutePreConfiguredActions<AbpElsaNextStudioBlazorOptions>(); |
||||
|
|
||||
|
context.Services.AddDashboardModule(elsaNextStudioBlazoeOptions.BackendApiConfig); |
||||
|
context.Services.AddDashboardWidgets(); |
||||
|
|
||||
|
context.Services.AddScoped<ElsaStudioDashboardText>(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,18 @@ |
|||||
|
using Elsa.Studio.Dashboard.Widgets; |
||||
|
using LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor.Widgets; |
||||
|
using Microsoft.Extensions.DependencyInjection; |
||||
|
|
||||
|
namespace LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor.Extensions; |
||||
|
|
||||
|
public static class DashboardWidgetServiceCollectionExtensions |
||||
|
{ |
||||
|
public static IServiceCollection AddDashboardWidgets(this IServiceCollection services) |
||||
|
{ |
||||
|
return services |
||||
|
.AddDashboardWidget<OperationalHealthWidget>("dashboard.workflow.metrics", DashboardWidgetZones.Metrics, 100, "Workflow metrics", payloadKind: "WorkflowInstances") |
||||
|
.AddDashboardWidget<NeedsAttentionWidget>("dashboard.needs-attention", DashboardWidgetZones.Findings, 100, "Needs attention") |
||||
|
.AddDashboardWidget<ExecutionTrendWidget>("dashboard.workflow.trend", DashboardWidgetZones.Trend, 100, "Workflow trends", payloadKind: "WorkflowTrends") |
||||
|
.AddDashboardWidget<RecentActivityWidget>("dashboard.workflow.recent-activity", DashboardWidgetZones.Activity, 100, "Recent activity", payloadKind: "RecentActivity") |
||||
|
.AddDashboardWidget<HotspotsWidget>("dashboard.workflow.hotspots", DashboardWidgetZones.SecondaryPanels, 100, "Workflow hotspots", payloadKind: "WorkflowHotspots"); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,3 @@ |
|||||
|
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
||||
|
<ConfigureAwait ContinueOnCapturedContext="false" /> |
||||
|
</Weavers> |
||||
@ -0,0 +1,30 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
||||
|
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
||||
|
<xs:element name="Weavers"> |
||||
|
<xs:complexType> |
||||
|
<xs:all> |
||||
|
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
||||
|
<xs:complexType> |
||||
|
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
||||
|
</xs:complexType> |
||||
|
</xs:element> |
||||
|
</xs:all> |
||||
|
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
</xs:complexType> |
||||
|
</xs:element> |
||||
|
</xs:schema> |
||||
@ -0,0 +1,31 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk.Razor"> |
||||
|
|
||||
|
<Import Project="..\..\..\..\configureawait.props" /> |
||||
|
<Import Project="..\..\..\..\common.props" /> |
||||
|
|
||||
|
<PropertyGroup> |
||||
|
<TargetFramework>net10.0</TargetFramework> |
||||
|
<Nullable>enable</Nullable> |
||||
|
<ImplicitUsings>enable</ImplicitUsings> |
||||
|
<AssemblyName>LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor</AssemblyName> |
||||
|
<PackageId>LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor</PackageId> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<!-- Elsa Studio Dashboard (official, rc2): page shell, zones, models and API client. The |
||||
|
workflow widgets are re-implemented in this module (Widgets/) as localized copies. --> |
||||
|
<PackageReference Include="Elsa.Studio.Dashboard" /> |
||||
|
<!-- 使用自定义仪表板组件 --> |
||||
|
<!--<PackageReference Include="Elsa.Studio.Workflows.Dashboard" />--> |
||||
|
<PackageReference Include="Elsa.Workflows.Runtime.Dashboard" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<ProjectReference Include="..\LINGYUN.Abp.ElsaNext.Studio.Blazor\LINGYUN.Abp.ElsaNext.Studio.Blazor.csproj" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<EmbeddedResource Include="Localization\**\*.json" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
</Project> |
||||
@ -0,0 +1,6 @@ |
|||||
|
{ |
||||
|
"culture": "en", |
||||
|
"texts": { |
||||
|
"Menu:Dashboard": "Dashboard" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,6 @@ |
|||||
|
{ |
||||
|
"culture": "zh-Hans", |
||||
|
"texts": { |
||||
|
"Menu:Dashboard": "仪表盘" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,31 @@ |
|||||
|
using LINGYUN.Abp.ElsaNext.Studio.Blazor.Navigation; |
||||
|
using LINGYUN.Abp.ElsaNext.Studio.Translations.Localization; |
||||
|
using Volo.Abp.UI.Navigation; |
||||
|
|
||||
|
namespace LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor.Navigation; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Adds the dashboard page to the "ElsaStudio" menu group (created by the Studio.Blazor module;
|
||||
|
/// created here too when absent so this module stays self-contained).
|
||||
|
/// </summary>
|
||||
|
public class ElsaStudioDashboardMenuContributor : IMenuContributor |
||||
|
{ |
||||
|
public Task ConfigureMenuAsync(MenuConfigurationContext context) |
||||
|
{ |
||||
|
if (context.Menu.Name == StandardMenus.Main) |
||||
|
{ |
||||
|
var menu = context.Menu; |
||||
|
var l = context.GetLocalizer<ElsaStudioResource>(); |
||||
|
|
||||
|
var group = menu.GetMenuItemOrNull(ElsaStudioMenus.GroupName); |
||||
|
group?.AddItem(new ApplicationMenuItem( |
||||
|
ElsaStudioMenus.GroupName + ".Dashboard", |
||||
|
l["Menu:Dashboard"], |
||||
|
url: "/dashboard", |
||||
|
icon: "fa fa-chart-line", |
||||
|
order: 0)); |
||||
|
} |
||||
|
|
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,92 @@ |
|||||
|
@page "/dashboard" |
||||
|
@attribute [Authorize] |
||||
|
@inherits StudioComponentBase |
||||
|
@inject ILocalizer Localizer |
||||
|
|
||||
|
<HeadContent> |
||||
|
<link rel="stylesheet" href="_content/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor/dashboard.css" /> |
||||
|
</HeadContent> |
||||
|
|
||||
|
<PageTitle>@Localizer["Dashboard"]</PageTitle> |
||||
|
|
||||
|
<MudContainer MaxWidth="MaxWidth.False" Class="dashboard-shell" Style="@DashboardStyles.Shell"> |
||||
|
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Class="dashboard-header" Spacing="3" Style="@DashboardStyles.Header"> |
||||
|
<MudStack Spacing="1"> |
||||
|
<MudText Typo="Typo.h4">@Localizer["Dashboard"]</MudText> |
||||
|
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1" Class="flex-wrap"> |
||||
|
<MudText Typo="Typo.body2" Class="dashboard-muted">@BackendLabel</MudText> |
||||
|
@if (_snapshot != null) |
||||
|
{ |
||||
|
var runtime = _snapshot.Overview.Runtime.Status; |
||||
|
<MudChip T="string" Size="Size.Small" Variant="Variant.Filled" Color="@DashboardUiMapper.RuntimeColor(runtime)" Icon="@Icons.Material.Outlined.Circle">@Text.RuntimeStatusLabel(runtime)</MudChip> |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
<MudChip T="string" Size="Size.Small" Variant="Variant.Filled" Color="Color.Error" Icon="@Icons.Material.Outlined.CloudOff">@StatusLabel</MudChip> |
||||
|
} |
||||
|
<MudText Typo="Typo.caption" Class="dashboard-muted">@LastRefreshedLabel</MudText> |
||||
|
</MudStack> |
||||
|
</MudStack> |
||||
|
|
||||
|
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.FlexEnd" Spacing="1" Class="flex-wrap"> |
||||
|
<MudToggleGroup T="string" Value="@_selectedRange" ValueChanged="OnRangeChangedAsync" Color="Color.Primary" SelectionMode="SelectionMode.SingleSelection" Size="Size.Small"> |
||||
|
@foreach (var option in DashboardRangeMapper.Options) |
||||
|
{ |
||||
|
<MudToggleItem T="string" Value="@option.Value">@option.Label</MudToggleItem> |
||||
|
} |
||||
|
</MudToggleGroup> |
||||
|
<MudTooltip Text="@Localizer["Refresh dashboard"]" Delay="400"> |
||||
|
<MudIconButton Icon="@Icons.Material.Outlined.Refresh" Color="Color.Primary" Disabled="@_loading" OnClick="RefreshAsync" aria-label="Refresh dashboard" /> |
||||
|
</MudTooltip> |
||||
|
</MudStack> |
||||
|
</MudStack> |
||||
|
|
||||
|
@if (!string.IsNullOrWhiteSpace(_message)) |
||||
|
{ |
||||
|
<MudAlert Severity="@AlertSeverity" Dense="true" Class="mb-4">@_message</MudAlert> |
||||
|
} |
||||
|
|
||||
|
@if (_loading && _snapshot == null) |
||||
|
{ |
||||
|
<MudGrid Spacing="2" Class="mb-4"> |
||||
|
@for (var i = 0; i < 6; i++) |
||||
|
{ |
||||
|
<MudItem xs="12" sm="6" md="4" xl="2"> |
||||
|
<MudSkeleton Height="112px" Animation="Animation.Wave" /> |
||||
|
</MudItem> |
||||
|
} |
||||
|
</MudGrid> |
||||
|
<MudSkeleton Height="360px" Animation="Animation.Wave" /> |
||||
|
} |
||||
|
else if (_snapshot == null) |
||||
|
{ |
||||
|
<MudPaper Outlined="true" Elevation="0" Class="dashboard-panel dashboard-empty-state" Style="@DashboardStyles.EmptyPanel"> |
||||
|
<MudIcon Icon="@Icons.Material.Outlined.DashboardCustomize" Color="Color.Default" /> |
||||
|
<MudText Typo="Typo.h6">@StatusLabel</MudText> |
||||
|
<MudText Typo="Typo.body2" Class="dashboard-muted">@(_message ?? @Localizer["Dashboard data is unavailable from the selected backend."])</MudText> |
||||
|
</MudPaper> |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
<DashboardWidgetZone Widgets="@GetWidgets(DashboardWidgetZones.Metrics)" Context="@WidgetContext" /> |
||||
|
|
||||
|
<MudStack Spacing="2" Class="dashboard-main-stack"> |
||||
|
<DashboardWidgetZone Widgets="@GetWidgets(DashboardWidgetZones.Trend)" Context="@WidgetContext" /> |
||||
|
<DashboardWidgetZone Widgets="@GetWidgets(DashboardWidgetZones.PrimaryPanels)" Context="@WidgetContext" /> |
||||
|
|
||||
|
<MudGrid Spacing="2" Class="dashboard-activity-grid"> |
||||
|
<MudItem xs="12" lg="8"> |
||||
|
<DashboardWidgetZone Widgets="@GetWidgets(DashboardWidgetZones.Activity)" Context="@WidgetContext" /> |
||||
|
</MudItem> |
||||
|
<MudItem xs="12" lg="4"> |
||||
|
<MudStack Spacing="2"> |
||||
|
<DashboardWidgetZone Widgets="@GetWidgets(DashboardWidgetZones.Findings)" Context="@WidgetContext" /> |
||||
|
<DashboardWidgetZone Widgets="@GetWidgets(DashboardWidgetZones.SecondaryPanels)" Context="@WidgetContext" /> |
||||
|
</MudStack> |
||||
|
</MudItem> |
||||
|
</MudGrid> |
||||
|
|
||||
|
<DashboardWidgetZone Widgets="@GetWidgets(DashboardWidgetZones.DiagnosticsStatus)" Context="@WidgetContext" Class="dashboard-diagnostics-grid" ItemClass="dashboard-diagnostics-grid-item" /> |
||||
|
</MudStack> |
||||
|
} |
||||
|
</MudContainer> |
||||
@ -0,0 +1,211 @@ |
|||||
|
using Elsa.Studio.Contracts; |
||||
|
using Elsa.Studio.Dashboard.Models; |
||||
|
using Elsa.Studio.Dashboard.Services; |
||||
|
using Elsa.Studio.Dashboard.Widgets; |
||||
|
using Elsa.Studio.Localization; |
||||
|
using LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor.Services; |
||||
|
using Microsoft.AspNetCore.Components; |
||||
|
using MudBlazor; |
||||
|
|
||||
|
namespace LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor.Pages; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Elsa Studio dashboard page (official Elsa.Studio.Dashboard content hosted under
|
||||
|
/// <c>/elsa/workflows/dashboard</c>). The dashboard service reads the Elsa dashboard API with the
|
||||
|
/// circuit session token; the widgets (metrics/trend/activity/...) come from the official
|
||||
|
/// dashboard modules.
|
||||
|
/// </summary>
|
||||
|
public partial class Dashboard : IAsyncDisposable |
||||
|
{ |
||||
|
private CancellationTokenSource? _loadCancellationTokenSource; |
||||
|
private DashboardSnapshot? _snapshot; |
||||
|
private DashboardLoadStatus _status = DashboardLoadStatus.Unavailable; |
||||
|
private string _selectedRange = DashboardRangeKeys.TwentyFourHours; |
||||
|
private string? _message; |
||||
|
private bool _loading; |
||||
|
private bool _disposed; |
||||
|
private bool _subscribedToFeatureInitialized; |
||||
|
private DateTimeOffset? _lastRefreshedAt; |
||||
|
|
||||
|
[Inject] private IDashboardService DashboardService { get; set; } = null!; |
||||
|
[Inject] private IDashboardWidgetRegistry WidgetRegistry { get; set; } = null!; |
||||
|
[Inject] private IEnumerable<DashboardWidgetDescriptor> Widgets { get; set; } = []; |
||||
|
[Inject] private NavigationManager NavigationManager { get; set; } = null!; |
||||
|
[Inject] private IFeatureService FeatureService { get; set; } = null!; |
||||
|
[Inject] private ElsaStudioDashboardText Text { get; set; } = null!; |
||||
|
|
||||
|
private DashboardWidgetContext WidgetContext => new( |
||||
|
_selectedRange, |
||||
|
_loading, |
||||
|
_lastRefreshedAt, |
||||
|
_status, |
||||
|
_message, |
||||
|
_snapshot, |
||||
|
RefreshAsync, |
||||
|
NavigationManager); |
||||
|
|
||||
|
private string BackendLabel |
||||
|
{ |
||||
|
get |
||||
|
{ |
||||
|
if (_snapshot == null) |
||||
|
return Localizer["Selected backend"]; |
||||
|
|
||||
|
var overview = _snapshot.Overview; |
||||
|
var backendName = string.IsNullOrWhiteSpace(overview.BackendName) ? Localizer["Backend"] : overview.BackendName; |
||||
|
return string.IsNullOrWhiteSpace(overview.EnvironmentName) ? backendName : $"{backendName} / {overview.EnvironmentName}"; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private string LastRefreshedLabel => _lastRefreshedAt == null ? Localizer["Not refreshed yet"] : $"{Localizer["Refreshed"]} {Text.RelativeTimestamp(_lastRefreshedAt)}"; |
||||
|
|
||||
|
private string StatusLabel => _status switch |
||||
|
{ |
||||
|
DashboardLoadStatus.Unauthorized => Localizer["No access"], |
||||
|
DashboardLoadStatus.BackendDisconnected => Localizer["Backend disconnected"], |
||||
|
DashboardLoadStatus.Failed => Localizer["Refresh failed"], |
||||
|
DashboardLoadStatus.Loaded => Localizer["Loaded"], |
||||
|
_ => Localizer["Dashboard unavailable"] |
||||
|
}; |
||||
|
|
||||
|
private Severity AlertSeverity => _status switch |
||||
|
{ |
||||
|
DashboardLoadStatus.Unauthorized => Severity.Warning, |
||||
|
DashboardLoadStatus.Loaded => Severity.Info, |
||||
|
_ => Severity.Error |
||||
|
}; |
||||
|
|
||||
|
private IReadOnlyCollection<DashboardWidgetDescriptor> GetWidgets(string zone) => |
||||
|
Widgets |
||||
|
.Concat(WidgetRegistry.List()) |
||||
|
.DistinctBy(x => x.Id) |
||||
|
.Where(x => x.Zone == zone && x.IsVisible(WidgetContext)) |
||||
|
.OrderBy(x => x.Order) |
||||
|
.ThenBy(x => x.Id, StringComparer.Ordinal) |
||||
|
.ToList(); |
||||
|
|
||||
|
protected override async Task OnInitializedAsync() |
||||
|
{ |
||||
|
// Features register their widgets into the (singleton) widget registry during
|
||||
|
// InitializeFeaturesAsync; the official host triggers this from its own startup tasks, so
|
||||
|
// do it here to be safe when hosting the page standalone in an ABP layout.
|
||||
|
try |
||||
|
{ |
||||
|
await FeatureService.InitializeFeaturesAsync(); |
||||
|
} |
||||
|
catch (Exception) |
||||
|
{ |
||||
|
// Remote feature discovery is unavailable (e.g. backend feature API down); the page
|
||||
|
// still renders the DI-registered widgets.
|
||||
|
} |
||||
|
await RefreshAsync(); |
||||
|
} |
||||
|
|
||||
|
protected override void OnAfterRender(bool firstRender) |
||||
|
{ |
||||
|
if (!firstRender || _disposed) |
||||
|
return; |
||||
|
|
||||
|
FeatureService.Initialized += OnFeatureServiceInitialized; |
||||
|
_subscribedToFeatureInitialized = true; |
||||
|
} |
||||
|
|
||||
|
private void OnFeatureServiceInitialized() |
||||
|
{ |
||||
|
_ = RefreshWidgetsAfterFeatureInitializationAsync(); |
||||
|
} |
||||
|
|
||||
|
private async Task RefreshWidgetsAfterFeatureInitializationAsync() |
||||
|
{ |
||||
|
if (_disposed) |
||||
|
return; |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
await InvokeAsync(StateHasChanged); |
||||
|
} |
||||
|
catch (InvalidOperationException) when (_disposed) |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private async Task OnRangeChangedAsync(string? range) |
||||
|
{ |
||||
|
var selectedRange = DashboardRangeMapper.Normalize(range); |
||||
|
|
||||
|
if (_selectedRange == selectedRange) |
||||
|
return; |
||||
|
|
||||
|
_selectedRange = selectedRange; |
||||
|
await RefreshAsync(); |
||||
|
} |
||||
|
|
||||
|
private async Task RefreshAsync() |
||||
|
{ |
||||
|
await LoadAsync(_selectedRange); |
||||
|
} |
||||
|
|
||||
|
private async Task LoadAsync(string range) |
||||
|
{ |
||||
|
await CancelCurrentLoadAsync(); |
||||
|
|
||||
|
var cancellationTokenSource = new CancellationTokenSource(); |
||||
|
_loadCancellationTokenSource = cancellationTokenSource; |
||||
|
_loading = true; |
||||
|
_message = null; |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
var result = await DashboardService.LoadAsync(range, cancellationToken: cancellationTokenSource.Token); |
||||
|
_status = result.Status; |
||||
|
|
||||
|
if (result.Snapshot != null) |
||||
|
{ |
||||
|
_snapshot = result.Snapshot; |
||||
|
_lastRefreshedAt = DateTimeOffset.UtcNow; |
||||
|
_message = null; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
_message = result.Message; |
||||
|
} |
||||
|
} |
||||
|
catch (OperationCanceledException) when (cancellationTokenSource.IsCancellationRequested) |
||||
|
{ |
||||
|
} |
||||
|
catch (Exception e) |
||||
|
{ |
||||
|
_status = DashboardLoadStatus.Failed; |
||||
|
_message = e.Message; |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
if (ReferenceEquals(_loadCancellationTokenSource, cancellationTokenSource)) |
||||
|
{ |
||||
|
_loading = false; |
||||
|
_loadCancellationTokenSource = null; |
||||
|
} |
||||
|
|
||||
|
cancellationTokenSource.Dispose(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private async Task CancelCurrentLoadAsync() |
||||
|
{ |
||||
|
if (_loadCancellationTokenSource == null) |
||||
|
return; |
||||
|
|
||||
|
await _loadCancellationTokenSource.CancelAsync(); |
||||
|
_loadCancellationTokenSource = null; |
||||
|
} |
||||
|
|
||||
|
public async ValueTask DisposeAsync() |
||||
|
{ |
||||
|
_disposed = true; |
||||
|
|
||||
|
if (_subscribedToFeatureInitialized) |
||||
|
FeatureService.Initialized -= OnFeatureServiceInitialized; |
||||
|
|
||||
|
await CancelCurrentLoadAsync(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,120 @@ |
|||||
|
using Elsa.Studio.Dashboard.Models; |
||||
|
using Elsa.Studio.Localization; |
||||
|
using LINGYUN.Abp.ElsaNext.Studio.Translations; |
||||
|
using System.Globalization; |
||||
|
|
||||
|
namespace LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor.Services; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Culture-aware text and metric formatting for the dashboard widgets. The official Elsa Studio
|
||||
|
/// dashboard widgets hard-code their English copy and format numbers with the ambient thread
|
||||
|
/// culture, so this module renders localized copies that resolve every label through
|
||||
|
/// <see cref="ILocalizer"/> (which follows the ABP UI culture captured on the circuit) and format
|
||||
|
/// numbers with that same culture.
|
||||
|
/// </summary>
|
||||
|
public class ElsaStudioDashboardText |
||||
|
{ |
||||
|
private readonly ILocalizer _localizer; |
||||
|
|
||||
|
public ElsaStudioDashboardText(ILocalizer localizer) |
||||
|
{ |
||||
|
_localizer = localizer; |
||||
|
} |
||||
|
|
||||
|
/// <summary>The UI culture captured at prerender (fallback: current thread culture).</summary>
|
||||
|
public CultureInfo Culture => CultureInfo.CurrentUICulture; |
||||
|
|
||||
|
/// <summary>Localized text for the given key (falls back to the key itself).</summary>
|
||||
|
public string this[string key] => _localizer[key]; |
||||
|
|
||||
|
/// <summary>Localized capability label of the "needs attention" backend capability.</summary>
|
||||
|
public string CapabilityLabel(DashboardCapabilityStatus? capability) |
||||
|
{ |
||||
|
return capability?.Status switch |
||||
|
{ |
||||
|
"Available" => _localizer["Available"], |
||||
|
"Unauthorized" => _localizer["No access"], |
||||
|
"NotInstalled" => _localizer["Not installed"], |
||||
|
_ => _localizer["Unavailable"] |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
/// <summary>Localized label of the runtime status chip shown in the dashboard header.</summary>
|
||||
|
public string RuntimeStatusLabel(string? status) |
||||
|
{ |
||||
|
return status switch |
||||
|
{ |
||||
|
DashboardRuntimeStatusKeys.AcceptingWork => _localizer["Accepting work"], |
||||
|
DashboardRuntimeStatusKeys.Paused => _localizer["Paused"], |
||||
|
DashboardRuntimeStatusKeys.Draining => _localizer["Draining"], |
||||
|
_ => _localizer["Unavailable"] |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
/// <summary>Humanized caption for the selected dashboard range.</summary>
|
||||
|
public string RangeCaption(string selectedRange) |
||||
|
{ |
||||
|
return selectedRange switch |
||||
|
{ |
||||
|
DashboardRangeKeys.OneHour => _localizer["Last hour"], |
||||
|
DashboardRangeKeys.SevenDays => _localizer["Last 7 days"], |
||||
|
_ => _localizer["Last 24 hours"] |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
public string Count(long value) => value.ToString("N0", Culture); |
||||
|
|
||||
|
public string Count(int value) => Count((long)value); |
||||
|
|
||||
|
public string Duration(TimeSpan? value) |
||||
|
{ |
||||
|
if (value == null) |
||||
|
return "N/A"; |
||||
|
|
||||
|
var duration = value.Value; |
||||
|
|
||||
|
if (duration.TotalMilliseconds < 1000) |
||||
|
return $"{duration.TotalMilliseconds:N0} ms"; |
||||
|
|
||||
|
if (duration.TotalMinutes < 1) |
||||
|
return $"{duration.TotalSeconds:N1} s"; |
||||
|
|
||||
|
if (duration.TotalHours < 1) |
||||
|
return $"{duration.TotalMinutes:N1} min"; |
||||
|
|
||||
|
return $"{duration.TotalHours:N1} h"; |
||||
|
} |
||||
|
|
||||
|
public string DateTime(DateTimeOffset? value) |
||||
|
{ |
||||
|
return value == null ? "N/A" : value.Value.ToLocalTime().ToString("g", Culture); |
||||
|
} |
||||
|
|
||||
|
public string RelativeTimestamp(DateTimeOffset? value) |
||||
|
{ |
||||
|
if (value == null) |
||||
|
return "N/A"; |
||||
|
|
||||
|
var elapsed = DateTimeOffset.UtcNow - value.Value.ToUniversalTime(); |
||||
|
|
||||
|
if (elapsed.TotalSeconds < 60) |
||||
|
return _localizer["Just now"]; |
||||
|
|
||||
|
if (elapsed.TotalMinutes < 60) |
||||
|
return $"{elapsed.TotalMinutes:N0} {_localizer["min ago"]}"; |
||||
|
|
||||
|
if (elapsed.TotalHours < 24) |
||||
|
return $"{elapsed.TotalHours:N0} {_localizer["h ago"]}"; |
||||
|
|
||||
|
return $"{elapsed.TotalDays:N0} {_localizer["d ago"]}"; |
||||
|
} |
||||
|
|
||||
|
/// <summary>Trend chart x-axis label for a bucket timestamp.</summary>
|
||||
|
public string TrendLabel(DateTimeOffset value, string granularity) |
||||
|
{ |
||||
|
var local = value.ToLocalTime(); |
||||
|
return granularity == DashboardTrendGranularity.Day |
||||
|
? local.ToString(Culture.Name.StartsWith("zh", StringComparison.OrdinalIgnoreCase) ? "M月d日" : "MMM d", Culture) |
||||
|
: local.ToString("HH:mm", Culture); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,59 @@ |
|||||
|
@using LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor.Services |
||||
|
|
||||
|
@inject ElsaStudioDashboardText Text |
||||
|
|
||||
|
@if (Context.Snapshot != null) |
||||
|
{ |
||||
|
var response = Context.Snapshot.Trend; |
||||
|
|
||||
|
<MudPaper Outlined="true" Elevation="0" Class="dashboard-panel dashboard-section dashboard-chart-panel" Style="@DashboardStyles.ChartPanel"> |
||||
|
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Class="dashboard-section-header" Style="@DashboardStyles.SectionHeader"> |
||||
|
<MudText Typo="Typo.subtitle1">@Text["Execution trend"]</MudText> |
||||
|
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.FlexEnd" Spacing="1" Class="dashboard-trend-actions" Style="@DashboardStyles.TrendActions"> |
||||
|
<MudText Typo="Typo.caption" Class="dashboard-muted">@RangeCaption(response)</MudText> |
||||
|
</MudStack> |
||||
|
</MudStack> |
||||
|
|
||||
|
@if (Context.Loading) |
||||
|
{ |
||||
|
<MudSkeleton Height="220px" Animation="Animation.Wave" /> |
||||
|
} |
||||
|
else if (response.Buckets.Count == 0) |
||||
|
{ |
||||
|
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text">@Text["No workflow activity in this range."]</MudAlert> |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
<div class="dashboard-trend-chart-frame"> |
||||
|
<MudChart T="double" |
||||
|
Class="dashboard-trend-chart" |
||||
|
ChartType="ChartType.Line" |
||||
|
ChartSeries="@Series(response)" |
||||
|
ChartLabels="@Labels(response)" |
||||
|
Width="100%" |
||||
|
Height="220px" |
||||
|
MatchBoundsToSize="true" /> |
||||
|
</div> |
||||
|
} |
||||
|
</MudPaper> |
||||
|
} |
||||
|
|
||||
|
@code { |
||||
|
[Parameter] public DashboardWidgetContext Context { get; set; } = null!; |
||||
|
[Parameter] public DashboardWidgetDescriptor Descriptor { get; set; } = null!; |
||||
|
|
||||
|
private string RangeCaption(DashboardTrendResponse response) => |
||||
|
$"{Text.DateTime(response.From)} - {Text.DateTime(response.To)}"; |
||||
|
|
||||
|
private string[] Labels(DashboardTrendResponse response) => response.Buckets |
||||
|
.Select(x => Text.TrendLabel(x.From, response.Granularity)) |
||||
|
.ToArray(); |
||||
|
|
||||
|
private List<ChartSeries<double>> Series(DashboardTrendResponse response) => |
||||
|
[ |
||||
|
new() { Name = Text["Started"], Data = response.Buckets.Select(x => (double)x.CreatedOrStarted).ToArray() }, |
||||
|
new() { Name = Text["Finished"], Data = response.Buckets.Select(x => (double)x.Finished).ToArray() }, |
||||
|
new() { Name = Text["Faulted"], Data = response.Buckets.Select(x => (double)x.Faulted).ToArray() }, |
||||
|
new() { Name = Text["Suspended"], Data = response.Buckets.Select(x => (double)x.Suspended).ToArray() } |
||||
|
]; |
||||
|
} |
||||
@ -0,0 +1,49 @@ |
|||||
|
@using LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor.Services |
||||
|
|
||||
|
@inject ElsaStudioDashboardText Text |
||||
|
|
||||
|
@if (Context.Snapshot?.Hotspots != null) |
||||
|
{ |
||||
|
var response = Context.Snapshot.Hotspots; |
||||
|
|
||||
|
<MudPaper Outlined="true" Elevation="0" Class="dashboard-panel dashboard-section" Style="@DashboardStyles.Panel"> |
||||
|
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Class="dashboard-section-header" Style="@DashboardStyles.SectionHeader"> |
||||
|
<MudText Typo="Typo.subtitle1">@Text["Workflow hotspots"]</MudText> |
||||
|
<MudChip T="string" Size="Size.Small" Variant="Variant.Outlined">@MetricLabel(response.Metric)</MudChip> |
||||
|
</MudStack> |
||||
|
|
||||
|
@if (response.Items.Count == 0) |
||||
|
{ |
||||
|
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text">@Text["No hotspots in this range."]</MudAlert> |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
<MudTable T="DashboardHotspot" Items="@response.Items" Dense="true" Hover="true" Elevation="0" Breakpoint="Breakpoint.Sm"> |
||||
|
<HeaderContent> |
||||
|
<MudTh>@Text["Workflow"]</MudTh> |
||||
|
<MudTh>@Text["Value"]</MudTh> |
||||
|
<MudTh>@Text["Avg duration"]</MudTh> |
||||
|
</HeaderContent> |
||||
|
<RowTemplate> |
||||
|
<MudTd DataLabel="@Text["Workflow"]"> |
||||
|
<MudStack Spacing="0"> |
||||
|
<MudText Typo="Typo.body2">@DisplayName(context)</MudText> |
||||
|
<MudText Typo="Typo.caption" Class="dashboard-muted">@context.DefinitionId</MudText> |
||||
|
</MudStack> |
||||
|
</MudTd> |
||||
|
<MudTd DataLabel="@Text["Value"]">@Text.Count(context.Value)</MudTd> |
||||
|
<MudTd DataLabel="@Text["Avg duration"]">@Text.Duration(context.AverageDuration)</MudTd> |
||||
|
</RowTemplate> |
||||
|
</MudTable> |
||||
|
} |
||||
|
</MudPaper> |
||||
|
} |
||||
|
|
||||
|
@code { |
||||
|
[Parameter] public DashboardWidgetContext Context { get; set; } = null!; |
||||
|
[Parameter] public DashboardWidgetDescriptor Descriptor { get; set; } = null!; |
||||
|
|
||||
|
private string MetricLabel(string? metric) => string.IsNullOrWhiteSpace(metric) ? string.Empty : Text[metric]; |
||||
|
|
||||
|
private static string DisplayName(DashboardHotspot item) => string.IsNullOrWhiteSpace(item.WorkflowName) ? item.DefinitionId : item.WorkflowName!; |
||||
|
} |
||||
@ -0,0 +1,42 @@ |
|||||
|
@using LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor.Services |
||||
|
|
||||
|
@inject ElsaStudioDashboardText Text |
||||
|
|
||||
|
@if (Context.Snapshot != null) |
||||
|
{ |
||||
|
var findings = Context.Snapshot.NeedsAttention.Findings; |
||||
|
var capability = Context.Snapshot.NeedsAttention.Capability; |
||||
|
|
||||
|
<MudPaper Outlined="true" Elevation="0" Class="dashboard-panel dashboard-section" Style="@DashboardStyles.Panel"> |
||||
|
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Class="dashboard-section-header" Style="@DashboardStyles.SectionHeader"> |
||||
|
<MudText Typo="Typo.subtitle1">@Text["Needs attention"]</MudText> |
||||
|
<MudChip T="string" Size="Size.Small" Variant="Variant.Outlined" Color="@DashboardUiMapper.CapabilityColor(capability)">@Text.CapabilityLabel(capability)</MudChip> |
||||
|
</MudStack> |
||||
|
|
||||
|
@if (findings.Count == 0) |
||||
|
{ |
||||
|
<MudAlert Severity="Severity.Success" Dense="true" Variant="Variant.Text" Icon="@Icons.Material.Outlined.CheckCircle"> |
||||
|
@Text["No active findings in the selected range."] |
||||
|
</MudAlert> |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
<MudList T="DashboardFinding" Dense="true" Class="dashboard-finding-list" Style="@DashboardStyles.FindingList"> |
||||
|
@foreach (var item in findings.OrderBy(x => x.Priority).Take(8)) |
||||
|
{ |
||||
|
<MudListItem T="DashboardFinding" Value="@item" Icon="@DashboardUiMapper.SeverityIcon(item.Severity)" IconColor="@DashboardUiMapper.SeverityColor(item.Severity)"> |
||||
|
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Spacing="2" Class="dashboard-finding-row" Style="@DashboardStyles.FindingRow"> |
||||
|
<MudText Typo="Typo.body2">@item.Message</MudText> |
||||
|
<MudChip T="string" Size="Size.Small" Variant="Variant.Text" Color="@DashboardUiMapper.SeverityColor(item.Severity)">@item.Severity</MudChip> |
||||
|
</MudStack> |
||||
|
</MudListItem> |
||||
|
} |
||||
|
</MudList> |
||||
|
} |
||||
|
</MudPaper> |
||||
|
} |
||||
|
|
||||
|
@code { |
||||
|
[Parameter] public DashboardWidgetContext Context { get; set; } = null!; |
||||
|
[Parameter] public DashboardWidgetDescriptor Descriptor { get; set; } = null!; |
||||
|
} |
||||
@ -0,0 +1,46 @@ |
|||||
|
@using LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor.Services |
||||
|
|
||||
|
@inject ElsaStudioDashboardText Text |
||||
|
|
||||
|
@if (Context.Snapshot != null) |
||||
|
{ |
||||
|
var metrics = Context.Snapshot.Overview.WorkflowInstances; |
||||
|
var healthMetrics = new[] |
||||
|
{ |
||||
|
new HealthMetric(Text["Running"], Text.Count(metrics.Running), Text["Current active instances"], Icons.Material.Outlined.PlayCircle, Color.Info), |
||||
|
new HealthMetric(Text["Completed"], Text.Count(metrics.Completed), Text.RangeCaption(Context.SelectedRange), Icons.Material.Outlined.CheckCircle, Color.Success), |
||||
|
new HealthMetric(Text["Faulted"], Text.Count(metrics.Faulted), Text.RangeCaption(Context.SelectedRange), Icons.Material.Outlined.ErrorOutline, metrics.Faulted > 0 ? Color.Error : Color.Default), |
||||
|
new HealthMetric(Text["Suspended"], Text.Count(metrics.Suspended), Text["Current suspended instances"], Icons.Material.Outlined.PauseCircle, metrics.Suspended > 0 ? Color.Warning : Color.Default), |
||||
|
new HealthMetric(Text["Avg duration"], Text.Duration(metrics.AverageDuration), Text.RangeCaption(Context.SelectedRange), Icons.Material.Outlined.Timer, Color.Default), |
||||
|
new HealthMetric(Text["Needs attention"], Text.Count(Context.Snapshot.NeedsAttention.Findings.Count), Text["Prioritized findings"], Icons.Material.Outlined.ReportProblem, Context.Snapshot.NeedsAttention.Findings.Count > 0 ? Color.Warning : Color.Success) |
||||
|
}; |
||||
|
|
||||
|
<div class="dashboard-operational-health"> |
||||
|
<MudText Typo="Typo.subtitle1" Class="dashboard-operational-health-heading">@Text["Operational health"]</MudText> |
||||
|
|
||||
|
<div class="dashboard-operational-health-metrics"> |
||||
|
@foreach (var metric in healthMetrics) |
||||
|
{ |
||||
|
<div class="dashboard-operational-health-metric"> |
||||
|
@MetricContent(metric) |
||||
|
</div> |
||||
|
} |
||||
|
</div> |
||||
|
</div> |
||||
|
} |
||||
|
|
||||
|
@code { |
||||
|
[Parameter] public DashboardWidgetContext Context { get; set; } = null!; |
||||
|
[Parameter] public DashboardWidgetDescriptor Descriptor { get; set; } = null!; |
||||
|
|
||||
|
private RenderFragment<HealthMetric> MetricContent => metric => @<text> |
||||
|
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Spacing="2"> |
||||
|
<MudText Typo="Typo.overline">@metric.Label</MudText> |
||||
|
<MudIcon Icon="@metric.Icon" Size="Size.Small" Color="@metric.Color" /> |
||||
|
</MudStack> |
||||
|
<MudText Typo="Typo.h5" Color="@metric.Color" Class="dashboard-operational-health-value">@metric.Value</MudText> |
||||
|
<MudText Typo="Typo.caption" Class="dashboard-operational-health-caption">@metric.Caption</MudText> |
||||
|
</text>; |
||||
|
|
||||
|
private sealed record HealthMetric(string Label, string Value, string Caption, string Icon, Color Color); |
||||
|
} |
||||
@ -0,0 +1,101 @@ |
|||||
|
@using LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor.Services |
||||
|
@inject NavigationManager NavigationManager |
||||
|
@inject ElsaStudioDashboardText Text |
||||
|
|
||||
|
@if (Context.Snapshot != null) |
||||
|
{ |
||||
|
var items = Context.Snapshot.RecentActivity.Items; |
||||
|
|
||||
|
<MudPaper Outlined="true" Elevation="0" Class="dashboard-panel dashboard-section" Style="@DashboardStyles.Panel"> |
||||
|
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Class="dashboard-section-header" Style="@DashboardStyles.SectionHeader"> |
||||
|
<MudText Typo="Typo.subtitle1">@Text["Recent workflow activity"]</MudText> |
||||
|
<MudText Typo="Typo.caption" Class="dashboard-muted">@items.Count @(items.Count == 1 ? Text["item"] : Text["items"])</MudText> |
||||
|
</MudStack> |
||||
|
|
||||
|
<MudTable T="DashboardRecentActivityItem" Items="@items" Dense="true" Hover="true" Elevation="0" Breakpoint="Breakpoint.Sm" RowClassFunc="GetRowClass" OnRowClick="OnRowClick"> |
||||
|
<HeaderContent> |
||||
|
<MudTh>@Text["Workflow / instance"]</MudTh> |
||||
|
<MudTh>@Text["Status"]</MudTh> |
||||
|
<MudTh>@Text["Sub status"]</MudTh> |
||||
|
<MudTh>@Text["Incidents"]</MudTh> |
||||
|
<MudTh>@Text["Duration"]</MudTh> |
||||
|
<MudTh>@Text["Updated"]</MudTh> |
||||
|
<MudTh></MudTh> |
||||
|
</HeaderContent> |
||||
|
<RowTemplate> |
||||
|
<MudTd DataLabel="@Text["Workflow / instance"]"> |
||||
|
<MudStack Spacing="0"> |
||||
|
<MudText Typo="Typo.body2">@DisplayName(context)</MudText> |
||||
|
<MudText Typo="Typo.caption" Class="dashboard-muted">@context.InstanceId</MudText> |
||||
|
</MudStack> |
||||
|
</MudTd> |
||||
|
<MudTd DataLabel="@Text["Status"]"> |
||||
|
<MudChip T="string" Size="Size.Small" Variant="Variant.Outlined" Color="@StatusColor(context.Status)">@LocalizedStatus(context.Status)</MudChip> |
||||
|
</MudTd> |
||||
|
<MudTd DataLabel="@Text["Sub status"]"> |
||||
|
<MudChip T="string" Size="Size.Small" Variant="Variant.Text" Color="@SubStatusColor(context.SubStatus)">@LocalizedStatus(context.SubStatus)</MudChip> |
||||
|
</MudTd> |
||||
|
<MudTd DataLabel="@Text["Incidents"]">@Text.Count(context.IncidentCount)</MudTd> |
||||
|
<MudTd DataLabel="@Text["Duration"]">@Text.Duration(context.Duration)</MudTd> |
||||
|
<MudTd DataLabel="@Text["Updated"]">@Text.RelativeTimestamp(context.UpdatedAt ?? context.FinishedAt ?? context.CreatedAt)</MudTd> |
||||
|
<MudTd> |
||||
|
<MudTooltip Text="@Text["Open workflow instance"]"> |
||||
|
<MudIconButton Icon="@Icons.Material.Outlined.OpenInNew" Size="Size.Small" Href="@InstanceUrl(context)" aria-label="@($"{Text["Open workflow instance"]} {context.InstanceId}")" /> |
||||
|
</MudTooltip> |
||||
|
</MudTd> |
||||
|
</RowTemplate> |
||||
|
<NoRecordsContent> |
||||
|
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text">@Text["No workflow instances changed in this range."]</MudAlert> |
||||
|
</NoRecordsContent> |
||||
|
</MudTable> |
||||
|
</MudPaper> |
||||
|
} |
||||
|
|
||||
|
@code { |
||||
|
[Parameter] public DashboardWidgetContext Context { get; set; } = null!; |
||||
|
[Parameter] public DashboardWidgetDescriptor Descriptor { get; set; } = null!; |
||||
|
|
||||
|
private void OnRowClick(TableRowClickEventArgs<DashboardRecentActivityItem> args) |
||||
|
{ |
||||
|
if (args.Item == null) |
||||
|
return; |
||||
|
|
||||
|
NavigationManager.NavigateTo(InstanceUrl(args.Item)); |
||||
|
} |
||||
|
|
||||
|
// Official deep links target the standalone Studio host routes; this ABP host hosts the |
||||
|
// instance viewer at /elsa/workflows/instances/{id}, so build that link directly. |
||||
|
private static string InstanceUrl(DashboardRecentActivityItem item) => $"workflows/instances/{item.InstanceId}"; |
||||
|
|
||||
|
private string LocalizedStatus(string? status) => string.IsNullOrWhiteSpace(status) ? string.Empty : Text[status]; |
||||
|
|
||||
|
private static string DisplayName(DashboardRecentActivityItem item) => string.IsNullOrWhiteSpace(item.WorkflowName) ? item.DefinitionId : item.WorkflowName!; |
||||
|
|
||||
|
private static string GetRowClass(DashboardRecentActivityItem item, int rowNumber) |
||||
|
{ |
||||
|
return item.SubStatus is "Faulted" or "Interrupted" || item.IncidentCount > 0 ? "dashboard-row-warning" : string.Empty; |
||||
|
} |
||||
|
|
||||
|
private static Color StatusColor(string status) |
||||
|
{ |
||||
|
return status switch |
||||
|
{ |
||||
|
"Running" => Color.Info, |
||||
|
"Finished" or "Completed" => Color.Success, |
||||
|
"Faulted" => Color.Error, |
||||
|
_ => Color.Default |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
private static Color SubStatusColor(string subStatus) |
||||
|
{ |
||||
|
return subStatus switch |
||||
|
{ |
||||
|
"Faulted" => Color.Error, |
||||
|
"Interrupted" or "Suspended" => Color.Warning, |
||||
|
"Finished" or "Completed" => Color.Success, |
||||
|
_ => Color.Default |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,9 @@ |
|||||
|
@using Microsoft.AspNetCore.Authorization |
||||
|
@using Microsoft.AspNetCore.Components.Web |
||||
|
@using Elsa.Studio.Components |
||||
|
@using Elsa.Studio.Dashboard.Components |
||||
|
@using Elsa.Studio.Dashboard.Models |
||||
|
@using Elsa.Studio.Dashboard.Services |
||||
|
@using Elsa.Studio.Dashboard.Widgets |
||||
|
@using Elsa.Studio.Localization |
||||
|
@using MudBlazor |
||||
@ -0,0 +1,208 @@ |
|||||
|
/* Elsa Studio Dashboard visuals for the localized dashboard page/widgets (this module). |
||||
|
The ABP Blazor theme layout does not load Razor Class Library scoped-css bundles, so these |
||||
|
rules are global (class names are dashboard-specific) and injected by the page via <HeadContent>. |
||||
|
Mirrors the official Elsa.Studio.Dashboard / Elsa.Studio.Workflows.Dashboard scoped css. */ |
||||
|
|
||||
|
.dashboard-shell { |
||||
|
width: 100%; |
||||
|
max-width: none; |
||||
|
min-height: 100%; |
||||
|
padding: 24px 32px 32px; |
||||
|
box-sizing: border-box; |
||||
|
background: var(--mud-palette-background); |
||||
|
} |
||||
|
|
||||
|
.dashboard-header { |
||||
|
gap: 12px 16px; |
||||
|
margin-bottom: 14px; |
||||
|
} |
||||
|
|
||||
|
.dashboard-muted { |
||||
|
color: var(--mud-palette-text-secondary); |
||||
|
} |
||||
|
|
||||
|
.dashboard-panel { |
||||
|
border-radius: 8px; |
||||
|
background: var(--mud-palette-surface); |
||||
|
} |
||||
|
|
||||
|
.dashboard-shell .dashboard-panel { |
||||
|
border-radius: 8px; |
||||
|
background: var(--mud-palette-surface); |
||||
|
} |
||||
|
|
||||
|
.dashboard-empty-state { |
||||
|
text-align: center; |
||||
|
} |
||||
|
|
||||
|
.dashboard-metrics { |
||||
|
margin-bottom: 8px; |
||||
|
} |
||||
|
|
||||
|
.dashboard-card-link { |
||||
|
color: inherit; |
||||
|
display: block; |
||||
|
height: 100%; |
||||
|
} |
||||
|
|
||||
|
.dashboard-shell .dashboard-card-link { |
||||
|
color: inherit; |
||||
|
display: block; |
||||
|
height: 100%; |
||||
|
} |
||||
|
|
||||
|
.dashboard-shell .dashboard-metric-card { |
||||
|
min-height: 104px; |
||||
|
height: 100%; |
||||
|
padding: 12px; |
||||
|
transition: border-color 120ms ease, background-color 120ms ease; |
||||
|
} |
||||
|
|
||||
|
.dashboard-shell .dashboard-metric-card-clickable:hover, |
||||
|
.dashboard-shell .dashboard-metric-card-clickable:focus-within { |
||||
|
border-color: var(--mud-palette-primary); |
||||
|
background: rgba(var(--mud-palette-primary-rgb), 0.04); |
||||
|
} |
||||
|
|
||||
|
.dashboard-shell .dashboard-metric-value { |
||||
|
line-height: 1.1; |
||||
|
margin-top: 6px; |
||||
|
} |
||||
|
|
||||
|
.dashboard-main-stack { |
||||
|
margin-top: 8px; |
||||
|
} |
||||
|
|
||||
|
.dashboard-activity-grid { |
||||
|
margin-top: 0; |
||||
|
} |
||||
|
|
||||
|
.dashboard-shell .dashboard-section { |
||||
|
padding: 14px; |
||||
|
} |
||||
|
|
||||
|
.dashboard-shell .dashboard-section-header { |
||||
|
margin-bottom: 10px; |
||||
|
} |
||||
|
|
||||
|
.dashboard-shell .dashboard-chart-panel { |
||||
|
min-height: 320px; |
||||
|
} |
||||
|
|
||||
|
.dashboard-shell .dashboard-finding-list { |
||||
|
margin: -6px; |
||||
|
} |
||||
|
|
||||
|
.dashboard-shell .dashboard-finding-row { |
||||
|
width: 100%; |
||||
|
min-width: 0; |
||||
|
} |
||||
|
|
||||
|
.dashboard-shell .dashboard-finding-row .mud-typography { |
||||
|
min-width: 0; |
||||
|
} |
||||
|
|
||||
|
.dashboard-shell .dashboard-row-warning { |
||||
|
background: rgba(var(--mud-palette-warning-rgb), 0.08); |
||||
|
} |
||||
|
|
||||
|
.dashboard-shell .dashboard-diagnostics-block { |
||||
|
border: 1px solid var(--mud-palette-lines-default); |
||||
|
border-radius: 8px; |
||||
|
padding: 12px; |
||||
|
} |
||||
|
|
||||
|
.dashboard-shell .dashboard-diagnostics-value { |
||||
|
min-height: 28px; |
||||
|
} |
||||
|
|
||||
|
.dashboard-shell .mud-table-cell { |
||||
|
padding: 6px 10px; |
||||
|
} |
||||
|
|
||||
|
/* Operational health metrics (localized copy of the official metrics widget) */ |
||||
|
|
||||
|
.dashboard-operational-health { |
||||
|
margin-bottom: 8px; |
||||
|
} |
||||
|
|
||||
|
.dashboard-operational-health-heading { |
||||
|
margin-bottom: 14px; |
||||
|
} |
||||
|
|
||||
|
.dashboard-operational-health-metrics { |
||||
|
display: grid; |
||||
|
gap: 10px; |
||||
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); |
||||
|
} |
||||
|
|
||||
|
.dashboard-operational-health-metric { |
||||
|
background: var(--mud-palette-surface); |
||||
|
border: 1px solid var(--mud-palette-lines-default); |
||||
|
border-radius: 8px; |
||||
|
min-width: 0; |
||||
|
padding: 12px 14px; |
||||
|
transition: border-color 120ms ease, background-color 120ms ease; |
||||
|
} |
||||
|
|
||||
|
.dashboard-operational-health-metric:hover, |
||||
|
.dashboard-operational-health-metric:focus-within { |
||||
|
border-color: var(--mud-palette-primary); |
||||
|
} |
||||
|
|
||||
|
.dashboard-operational-health-value { |
||||
|
line-height: 1.1; |
||||
|
margin-top: 8px; |
||||
|
} |
||||
|
|
||||
|
.dashboard-operational-health-caption { |
||||
|
color: var(--mud-palette-text-secondary); |
||||
|
} |
||||
|
|
||||
|
/* Execution trend chart (localized copy of the official trend widget) */ |
||||
|
|
||||
|
.dashboard-trend-chart-frame { |
||||
|
width: 100%; |
||||
|
min-width: 0; |
||||
|
height: 260px; |
||||
|
} |
||||
|
|
||||
|
.dashboard-trend-chart-frame .dashboard-trend-chart { |
||||
|
width: 100% !important; |
||||
|
min-width: 0; |
||||
|
} |
||||
|
|
||||
|
.dashboard-trend-chart-frame .dashboard-trend-chart svg { |
||||
|
width: 100% !important; |
||||
|
max-width: 100%; |
||||
|
} |
||||
|
|
||||
|
.dashboard-trend-chart-frame .mud-chart-legend { |
||||
|
width: 100%; |
||||
|
justify-content: center; |
||||
|
margin-top: 6px; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
@media (max-width: 768px) { |
||||
|
.dashboard-shell { |
||||
|
padding: 16px; |
||||
|
} |
||||
|
|
||||
|
.dashboard-header { |
||||
|
align-items: flex-start; |
||||
|
gap: 10px; |
||||
|
} |
||||
|
|
||||
|
.dashboard-shell .dashboard-section { |
||||
|
padding: 12px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
@media (max-width: 720px) { |
||||
|
.dashboard-trend-chart-frame { |
||||
|
height: 260px; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,87 @@ |
|||||
|
using Elsa.Diagnostics.OpenTelemetry.Extensions; |
||||
|
using Elsa.Extensions; |
||||
|
using Elsa.Features.Services; |
||||
|
using Elsa.OpenTelemetry.Middleware; |
||||
|
using Elsa.Studio.Diagnostics.OpenTelemetry.Dashboard.Extensions; |
||||
|
using Elsa.Studio.Diagnostics.OpenTelemetry.Extensions; |
||||
|
using Elsa.Workflows.Telemetry; |
||||
|
using LINGYUN.Abp.ElsaNext.Studio.Blazor; |
||||
|
using LINGYUN.Abp.ElsaNext.Studio.Diagnostics.OpenTelemetry.Blazor.Extensions; |
||||
|
using LINGYUN.Abp.ElsaNext.Studio.Diagnostics.OpenTelemetry.Blazor.Navigation; |
||||
|
using LINGYUN.Abp.ElsaNext.Studio.Translations.Localization; |
||||
|
using Microsoft.AspNetCore.Routing; |
||||
|
using Microsoft.Extensions.DependencyInjection; |
||||
|
using Volo.Abp.AspNetCore; |
||||
|
using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Routing; |
||||
|
using Volo.Abp.Localization; |
||||
|
using Volo.Abp.Modularity; |
||||
|
using Volo.Abp.UI.Navigation; |
||||
|
using Volo.Abp.VirtualFileSystem; |
||||
|
|
||||
|
namespace LINGYUN.Abp.ElsaNext.Studio.Diagnostics.OpenTelemetry.Blazor; |
||||
|
|
||||
|
[DependsOn( |
||||
|
typeof(AbpAspNetCoreModule), |
||||
|
typeof(AbpElsaNextStudioBlazorModule))] |
||||
|
public class AbpElsaNextStudioDiagnosticsOpenTelemetryBlazorModule : AbpModule |
||||
|
{ |
||||
|
public override void PreConfigureServices(ServiceConfigurationContext context) |
||||
|
{ |
||||
|
PreConfigure<IModule>(elsa => |
||||
|
{ |
||||
|
elsa.UseWorkflows(workflows => |
||||
|
{ |
||||
|
workflows.WithDefaultWorkflowExecutionPipeline(pipeline => |
||||
|
pipeline.UseWorkflowExecutionTracing()); |
||||
|
workflows.WithDefaultActivityExecutionPipeline(pipeline => |
||||
|
pipeline.UseActivityExecutionTracing()); |
||||
|
}); |
||||
|
elsa.UseOpenTelemetry(); |
||||
|
elsa.UseOpenTelemetryDiagnostics(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public override void ConfigureServices(ServiceConfigurationContext context) |
||||
|
{ |
||||
|
var configuration = context.Configuration.GetSection("Elsa:Studio"); |
||||
|
|
||||
|
Configure<AbpRouterOptions>(options => |
||||
|
{ |
||||
|
options.AdditionalAssemblies.Add(typeof(Elsa.Studio.Diagnostics.OpenTelemetry.Feature).Assembly); |
||||
|
}); |
||||
|
|
||||
|
Configure<AbpNavigationOptions>(options => |
||||
|
{ |
||||
|
options.MenuContributors.Add(new ElsaStudioOpenTelemetryMenuContributor()); |
||||
|
}); |
||||
|
|
||||
|
Configure<AbpVirtualFileSystemOptions>(options => |
||||
|
{ |
||||
|
options.FileSets.AddEmbedded<AbpElsaNextStudioDiagnosticsOpenTelemetryBlazorModule>("LINGYUN.Abp.ElsaNext.Studio.Diagnostics.OpenTelemetry.Blazor"); |
||||
|
}); |
||||
|
|
||||
|
Configure<AbpLocalizationOptions>(options => |
||||
|
{ |
||||
|
options.Resources |
||||
|
.Get<ElsaStudioResource>() |
||||
|
.AddVirtualJson("/Localization/Resources/ElsaStudioOpenTelemetryBlazor"); |
||||
|
}); |
||||
|
|
||||
|
var elsaNextStudioBlazoeOptions = context.Services.ExecutePreConfiguredActions<AbpElsaNextStudioBlazorOptions>(); |
||||
|
|
||||
|
context.Services.AddOpenTelemetryDiagnosticsModule(elsaNextStudioBlazoeOptions.BackendApiConfig); |
||||
|
context.Services.AddOpenTelemetryDashboardModule(); |
||||
|
context.Services.AddOpenTelemetryDashboardWidget(); |
||||
|
context.Services.AddOpenTelemetry() |
||||
|
.WithTracing(tracing => tracing.AddSource(WorkflowInstrumentation.ActivitySourceName)) |
||||
|
.WithMetrics(metrics => metrics.AddMeter(WorkflowInstrumentation.MeterName)); |
||||
|
|
||||
|
Configure<AbpEndpointRouterOptions>(options => |
||||
|
{ |
||||
|
options.EndpointConfigureActions.Add((context) => |
||||
|
{ |
||||
|
context.Endpoints.MapOpenTelemetryHub(); |
||||
|
}); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,20 @@ |
|||||
|
using Elsa.Studio.Dashboard.Widgets; |
||||
|
using Elsa.Studio.Diagnostics.OpenTelemetry.Dashboard.UI.Dashboard; |
||||
|
using Microsoft.Extensions.DependencyInjection; |
||||
|
|
||||
|
namespace LINGYUN.Abp.ElsaNext.Studio.Diagnostics.OpenTelemetry.Blazor.Extensions; |
||||
|
|
||||
|
public static class OpenTelemetryWidgetServiceCollectionExtensions |
||||
|
{ |
||||
|
public static IServiceCollection AddOpenTelemetryDashboardWidget(this IServiceCollection services) |
||||
|
{ |
||||
|
return services |
||||
|
.AddDashboardWidget<OpenTelemetryDashboardWidget>( |
||||
|
"diagnostics.open-telemetry", |
||||
|
DashboardWidgetZones.DiagnosticsStatus, |
||||
|
300, |
||||
|
"OpenTelemetry", |
||||
|
requiredBackendCapability: "OpenTelemetry", |
||||
|
payloadKind: "OpenTelemetry.StorageDiagnostics"); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,3 @@ |
|||||
|
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
||||
|
<ConfigureAwait /> |
||||
|
</Weavers> |
||||
@ -0,0 +1,30 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
||||
|
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
||||
|
<xs:element name="Weavers"> |
||||
|
<xs:complexType> |
||||
|
<xs:all> |
||||
|
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
||||
|
<xs:complexType> |
||||
|
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
||||
|
</xs:complexType> |
||||
|
</xs:element> |
||||
|
</xs:all> |
||||
|
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
</xs:complexType> |
||||
|
</xs:element> |
||||
|
</xs:schema> |
||||
@ -0,0 +1,34 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk.Razor"> |
||||
|
|
||||
|
<Import Project="..\..\..\..\configureawait.props" /> |
||||
|
<Import Project="..\..\..\..\common.props" /> |
||||
|
|
||||
|
<PropertyGroup> |
||||
|
<TargetFramework>net10.0</TargetFramework> |
||||
|
<Nullable>enable</Nullable> |
||||
|
<ImplicitUsings>enable</ImplicitUsings> |
||||
|
<AssemblyName>LINGYUN.Abp.ElsaNext.Studio.Diagnostics.OpenTelemetry.Blazor</AssemblyName> |
||||
|
<PackageId>LINGYUN.Abp.ElsaNext.Studio.Diagnostics.OpenTelemetry.Blazor</PackageId> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<PackageReference Include="Volo.Abp.AspNetCore" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<PackageReference Include="OpenTelemetry.Extensions.Hosting" /> |
||||
|
<PackageReference Include="Elsa.OpenTelemetry" /> |
||||
|
<PackageReference Include="Elsa.Diagnostics.OpenTelemetry" /> |
||||
|
<PackageReference Include="Elsa.Studio.Diagnostics.OpenTelemetry.Dashboard" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<ProjectReference Include="..\LINGYUN.Abp.ElsaNext.Studio.Blazor\LINGYUN.Abp.ElsaNext.Studio.Blazor.csproj" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<Content Remove="Localization\**\*.json" /> |
||||
|
<EmbeddedResource Include="Localization\**\*.json" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
</Project> |
||||
@ -0,0 +1,7 @@ |
|||||
|
{ |
||||
|
"culture": "en", |
||||
|
"texts": { |
||||
|
"Menu:OpenTelemetry": "Open Telemetry", |
||||
|
"OpenTelemetry": "Open Telemetry" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,7 @@ |
|||||
|
{ |
||||
|
"culture": "zh-Hans", |
||||
|
"texts": { |
||||
|
"Menu:OpenTelemetry": "Open Telemetry", |
||||
|
"OpenTelemetry": "Open Telemetry" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,27 @@ |
|||||
|
using LINGYUN.Abp.ElsaNext.Studio.Blazor.Navigation; |
||||
|
using LINGYUN.Abp.ElsaNext.Studio.Translations.Localization; |
||||
|
using Volo.Abp.UI.Navigation; |
||||
|
|
||||
|
namespace LINGYUN.Abp.ElsaNext.Studio.Diagnostics.OpenTelemetry.Blazor.Navigation; |
||||
|
|
||||
|
public class ElsaStudioOpenTelemetryMenuContributor : IMenuContributor |
||||
|
{ |
||||
|
public Task ConfigureMenuAsync(MenuConfigurationContext context) |
||||
|
{ |
||||
|
if (context.Menu.Name == StandardMenus.Main) |
||||
|
{ |
||||
|
var menu = context.Menu; |
||||
|
var l = context.GetLocalizer<ElsaStudioResource>(); |
||||
|
|
||||
|
var group = menu.GetMenuItemOrNull(ElsaStudioMenus.GroupName); |
||||
|
group?.AddItem(new ApplicationMenuItem( |
||||
|
ElsaStudioMenus.GroupName + ".OpenTelemetry", |
||||
|
l["Menu:OpenTelemetry"], |
||||
|
url: "/diagnostics/opentelemetry", |
||||
|
icon: "fa fa-x-ray", |
||||
|
order: 4)); |
||||
|
} |
||||
|
|
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,5 @@ |
|||||
|
@using Microsoft.AspNetCore.Authorization |
||||
|
@using Microsoft.AspNetCore.Components.Web |
||||
|
@using Elsa.Studio.Components |
||||
|
@using Elsa.Studio.Localization |
||||
|
@using MudBlazor |
||||
@ -0,0 +1,74 @@ |
|||||
|
using Elsa.Diagnostics.StructuredLogs.Extensions; |
||||
|
using Elsa.Extensions; |
||||
|
using Elsa.Features.Services; |
||||
|
using Elsa.Studio.Diagnostics.StructuredLogs.Dashboard.Extensions; |
||||
|
using Elsa.Studio.Diagnostics.StructuredLogs.Extensions; |
||||
|
using LINGYUN.Abp.ElsaNext.Studio.Blazor; |
||||
|
using LINGYUN.Abp.ElsaNext.Studio.Diagnostics.StructuredLogs.Blazor.Extensions; |
||||
|
using LINGYUN.Abp.ElsaNext.Studio.Diagnostics.StructuredLogs.Blazor.Navigation; |
||||
|
using LINGYUN.Abp.ElsaNext.Studio.Translations.Localization; |
||||
|
using Microsoft.AspNetCore.Routing; |
||||
|
using Microsoft.Extensions.DependencyInjection; |
||||
|
using Volo.Abp.AspNetCore; |
||||
|
using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Routing; |
||||
|
using Volo.Abp.Localization; |
||||
|
using Volo.Abp.Modularity; |
||||
|
using Volo.Abp.UI.Navigation; |
||||
|
using Volo.Abp.VirtualFileSystem; |
||||
|
|
||||
|
namespace LINGYUN.Abp.ElsaNext.Studio.Diagnostics.StructuredLogs.Blazor; |
||||
|
|
||||
|
[DependsOn( |
||||
|
typeof(AbpAspNetCoreModule), |
||||
|
typeof(AbpElsaNextStudioBlazorModule))] |
||||
|
public class AbpElsaNextStudioDiagnosticsStructuredLogsBlazorModule : AbpModule |
||||
|
{ |
||||
|
public override void PreConfigureServices(ServiceConfigurationContext context) |
||||
|
{ |
||||
|
PreConfigure<IModule>(elsa => |
||||
|
{ |
||||
|
elsa.UseStructuredLogs(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public override void ConfigureServices(ServiceConfigurationContext context) |
||||
|
{ |
||||
|
var configuration = context.Configuration.GetSection("Elsa:Studio"); |
||||
|
|
||||
|
Configure<AbpRouterOptions>(options => |
||||
|
{ |
||||
|
// options.AdditionalAssemblies.Add(typeof(Elsa.Studio.Diagnostics.StructuredLogs.Feature).Assembly);
|
||||
|
options.AdditionalAssemblies.Add(typeof(AbpElsaNextStudioDiagnosticsStructuredLogsBlazorModule).Assembly); |
||||
|
}); |
||||
|
|
||||
|
Configure<AbpNavigationOptions>(options => |
||||
|
{ |
||||
|
options.MenuContributors.Add(new ElsaStudioStructuredLogsMenuContributor()); |
||||
|
}); |
||||
|
|
||||
|
Configure<AbpVirtualFileSystemOptions>(options => |
||||
|
{ |
||||
|
options.FileSets.AddEmbedded<AbpElsaNextStudioDiagnosticsStructuredLogsBlazorModule>("LINGYUN.Abp.ElsaNext.Studio.Diagnostics.StructuredLogs.Blazor"); |
||||
|
}); |
||||
|
Configure<AbpLocalizationOptions>(options => |
||||
|
{ |
||||
|
options.Resources |
||||
|
.Get<ElsaStudioResource>() |
||||
|
.AddVirtualJson("/Localization/Resources/ElsaStudioStructuredLogsBlazor"); |
||||
|
}); |
||||
|
|
||||
|
var elsaNextStudioBlazoeOptions = context.Services.ExecutePreConfiguredActions<AbpElsaNextStudioBlazorOptions>(); |
||||
|
|
||||
|
context.Services.AddStructuredLogsModule(elsaNextStudioBlazoeOptions.BackendApiConfig); |
||||
|
context.Services.AddStructuredLogsDashboardModule(); |
||||
|
context.Services.AddStructuredLogsDashboardWidget(); |
||||
|
|
||||
|
Configure<AbpEndpointRouterOptions>(options => |
||||
|
{ |
||||
|
options.EndpointConfigureActions.Add((context) => |
||||
|
{ |
||||
|
context.Endpoints.MapStructuredLogsHub(); |
||||
|
}); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,20 @@ |
|||||
|
using Elsa.Studio.Dashboard.Widgets; |
||||
|
using Elsa.Studio.Diagnostics.StructuredLogs.Dashboard.UI.Dashboard; |
||||
|
using Microsoft.Extensions.DependencyInjection; |
||||
|
|
||||
|
namespace LINGYUN.Abp.ElsaNext.Studio.Diagnostics.StructuredLogs.Blazor.Extensions; |
||||
|
|
||||
|
public static class OpenTelemetryWidgetServiceCollectionExtensions |
||||
|
{ |
||||
|
public static IServiceCollection AddStructuredLogsDashboardWidget(this IServiceCollection services) |
||||
|
{ |
||||
|
return services |
||||
|
.AddDashboardWidget<StructuredLogsDashboardWidget>( |
||||
|
"diagnostics.structured-logs", |
||||
|
DashboardWidgetZones.DiagnosticsStatus, |
||||
|
100, |
||||
|
"Structured logs", |
||||
|
requiredBackendCapability: "StructuredLogs", |
||||
|
payloadKind: "Diagnostics.StructuredLogs"); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,3 @@ |
|||||
|
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
||||
|
<ConfigureAwait /> |
||||
|
</Weavers> |
||||
@ -0,0 +1,30 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
||||
|
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
||||
|
<xs:element name="Weavers"> |
||||
|
<xs:complexType> |
||||
|
<xs:all> |
||||
|
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
||||
|
<xs:complexType> |
||||
|
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
||||
|
</xs:complexType> |
||||
|
</xs:element> |
||||
|
</xs:all> |
||||
|
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
</xs:complexType> |
||||
|
</xs:element> |
||||
|
</xs:schema> |
||||
@ -0,0 +1,33 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk.Razor"> |
||||
|
|
||||
|
<Import Project="..\..\..\..\configureawait.props" /> |
||||
|
<Import Project="..\..\..\..\common.props" /> |
||||
|
|
||||
|
<PropertyGroup> |
||||
|
<TargetFramework>net10.0</TargetFramework> |
||||
|
<Nullable>enable</Nullable> |
||||
|
<ImplicitUsings>enable</ImplicitUsings> |
||||
|
<AssemblyName>LINGYUN.Abp.ElsaNext.Studio.Diagnostics.StructuredLogs.Blazor</AssemblyName> |
||||
|
<PackageId>LINGYUN.Abp.ElsaNext.Studio.Diagnostics.StructuredLogs.Blazor</PackageId> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<PackageReference Include="Volo.Abp.AspNetCore" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<!-- Official Elsa Studio structured-logs module (rc2): services, SignalR observer, API client |
||||
|
and the viewer page component reused by this module's ABP page (see Pages/). --> |
||||
|
<PackageReference Include="Elsa.Diagnostics.StructuredLogs" /> |
||||
|
<PackageReference Include="Elsa.Studio.Diagnostics.StructuredLogs.Dashboard" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<ProjectReference Include="..\LINGYUN.Abp.ElsaNext.Studio.Blazor\LINGYUN.Abp.ElsaNext.Studio.Blazor.csproj" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<EmbeddedResource Include="Localization\**\*.json" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
</Project> |
||||
@ -0,0 +1,7 @@ |
|||||
|
{ |
||||
|
"culture": "en", |
||||
|
"texts": { |
||||
|
"Menu:StructuredLogs": "Structured logs", |
||||
|
"Logs": "Structured logs" |
||||
|
} |
||||
|
} |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue