diff --git a/Directory.Packages.props b/Directory.Packages.props index 3892a028e..b738bc230 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -2,7 +2,7 @@ 10.0.0 2.17.0 - 3.6.0 + 3.8.0 10.6.0 10.6.0 10.0.9 @@ -14,6 +14,7 @@ + @@ -28,13 +29,20 @@ + + + + + + + @@ -48,6 +56,7 @@ + @@ -148,6 +157,7 @@ + @@ -162,9 +172,9 @@ + - @@ -227,20 +237,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + - + + + + + + + - - - + + + + + + + @@ -278,6 +352,7 @@ + @@ -320,6 +395,7 @@ + @@ -392,6 +468,7 @@ + diff --git a/aspnet-core/LINGYUN.MicroService.Aspire.slnx b/aspnet-core/LINGYUN.MicroService.Aspire.slnx index b00ca5382..0ee73ccac 100644 --- a/aspnet-core/LINGYUN.MicroService.Aspire.slnx +++ b/aspnet-core/LINGYUN.MicroService.Aspire.slnx @@ -130,8 +130,8 @@ - + @@ -552,6 +552,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer.DbMigrator/appsettings.json b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer.DbMigrator/appsettings.json index 6a90cf896..b1f35fb39 100644 --- a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer.DbMigrator/appsettings.json +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer.DbMigrator/appsettings.json @@ -7,7 +7,10 @@ "VueAdmin": { "ClientId": "vue-admin-client", "ClientSecret": "1q2w3e*", - "RootUrls": [ "http://localhost:5666" ] + "RootUrls": [ + "http://localhost:5666", + "http://127.0.0.1:5666" + ] }, "InternalService": { "ClientId": "InternalServiceClient", @@ -17,16 +20,34 @@ "ClientId": "vue-oauth-client", "RootUrls": [ "http://localhost:5666", + "http://127.0.0.1:5666", "http://localhost:30000", + "http://127.0.0.1:30000", "http://localhost:30010", + "http://127.0.0.1:30010", "http://localhost:30015", + "http://127.0.0.1:30015", "http://localhost:30020", + "http://127.0.0.1:30020", "http://localhost:30025", + "http://127.0.0.1:30025", "http://localhost:30030", + "http://127.0.0.1:30030", "http://localhost:30040", + "http://127.0.0.1:30040", "http://localhost:30045", + "http://127.0.0.1:30045", + "http://localhost:30050", + "http://127.0.0.1:30050", + "http://localhost:30060", + "http://127.0.0.1:30060" + ] + }, + "ElsaStudioClient": { + "ClientId": "elsa-studio-client", + "RootUrls": [ "http://localhost:30050", - "http://localhost:30060" + "http://127.0.0.1:30050" ] } } diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer.EntityFrameworkCore/DataSeeds/OpenIddictDataSeeder.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer.EntityFrameworkCore/DataSeeds/OpenIddictDataSeeder.cs index 6bd44bf85..66d9fc538 100644 --- a/aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer.EntityFrameworkCore/DataSeeds/OpenIddictDataSeeder.cs +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.AuthServer.EntityFrameworkCore/DataSeeds/OpenIddictDataSeeder.cs @@ -576,5 +576,51 @@ public class OpenIddictDataSeeder : OpenIddictDataSeedContributorBase, ITransien }; await PermissionDataSeeder.SeedAsync(ClientPermissionValueProvider.ProviderName, oauthClientId, oauthClientPermissions); } + + var elsaStudioClientId = configurationSection["ElsaStudioClient:ClientId"]; + if (!elsaStudioClientId.IsNullOrWhiteSpace()) + { + Logger.LogInformation("Seeding application {elsaStudioClientId}...", elsaStudioClientId); + + var elsaStudioClientSecret = configurationSection["ElsaStudioClient:ClientSecret"]; + var elsaStudioClientRootUrls = configurationSection.GetSection("ElsaStudioClient:RootUrls").Get>() ?? []; + + var elsaStudioClientRedirectUrls = new List(); + var elsaStudioClientPostLogoutRedirectUrls = new List(); + elsaStudioClientRootUrls.ForEach(url => + { + elsaStudioClientRedirectUrls.Add(url.EnsureEndsWith('/')); + elsaStudioClientRedirectUrls.Add(url.EnsureEndsWith('/') + "signin-oidc"); + elsaStudioClientRedirectUrls.Add(url.EnsureEndsWith('/') + "swagger/oauth2-redirect.html"); + + elsaStudioClientPostLogoutRedirectUrls.Add(url.EnsureEndsWith('/')); + elsaStudioClientPostLogoutRedirectUrls.Add(url.EnsureEndsWith('/') + "signout-oidc"); + elsaStudioClientPostLogoutRedirectUrls.Add(url.EnsureEndsWith('/') + "signout-callback-oidc"); + }); + + await CreateOrUpdateApplicationAsync( + OpenIddictConstants.ApplicationTypes.Web, + elsaStudioClientId, + !elsaStudioClientSecret.IsNullOrWhiteSpace() + ? OpenIddictConstants.ClientTypes.Confidential + : OpenIddictConstants.ClientTypes.Public, + OpenIddictConstants.ConsentTypes.Implicit, + "Abp Elsa Studio Client", + elsaStudioClientSecret, + [OpenIddictConstants.GrantTypes.AuthorizationCode, + OpenIddictConstants.GrantTypes.RefreshToken, + OpenIddictConstants.GrantTypes.Password], + [OpenIddictConstants.Scopes.OpenId, + OpenIddictConstants.Scopes.Email, + OpenIddictConstants.Scopes.Roles, + OpenIddictConstants.Scopes.Address, + OpenIddictConstants.Scopes.Phone, + OpenIddictConstants.Scopes.Profile, + OpenIddictConstants.Scopes.OfflineAccess, + scope, + "workflow-service"], + elsaStudioClientRedirectUrls, + elsaStudioClientPostLogoutRedirectUrls); + } } } diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/Components/App.razor b/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/Components/App.razor new file mode 100644 index 000000000..bb6788876 --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/Components/App.razor @@ -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; +} + + + + + + + + Elsa Studio (Blazor Server) + + + + + + @* Elsa Studio UI styles (RCL static assets) + offline icon-font fallback. *@ + + + + + + + + + + + + + + + + + + @* Monaco + Blazor Server bootstrap (mirrors the official Elsa Studio Server host). *@ + + + + + + + + +
+ @if (Env.IsDevelopment()) + { + An unhandled exception has occurred. See browser dev tools for details. + } + else if (Env.IsStaging() || Env.IsProduction()) + { + An error has occurred. This application may no longer respond until reloaded. + } + Reload + ✖ +
+ + + diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/Components/Index.razor b/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/Components/Index.razor new file mode 100644 index 000000000..c2e608b82 --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/Components/Index.razor @@ -0,0 +1,137 @@ +@page "/" +@using Microsoft.AspNetCore.Components.Authorization +@inject AuthenticationStateProvider AuthenticationStateProvider +@inject NavigationManager Navigation + + + + + + @if (!IsAuthenticated) + { + 欢迎使用 Elsa 工作室 + + 以 Blazor Server 模式集成的 Elsa 工作流工作室:服务端渲染与交互,接口调用在服务端完成,凭据不出浏览器。 + + 登录 + } + else + { + 欢迎回来 + + 通过左侧菜单进入 Elsa 工作室:管理工作流定义与实例,打开设计器进行可视化编排。 + + 工作流定义 + } + + + + + + + + 关于 + + + 服务端 Blazor Server(SignalR)承载 Elsa Studio;认证走外部 OIDC AuthServer 授权码 + PKCE; + Studio 页面以 ABP 子页面呈现(路由在菜单中定义)。 + + + + 集成模块:LINGYUN.Abp.ElsaNext.Studio.Blazor + + + + + + + + + + + More from ABP.IO + + + + + Latest Release Logs + + + Video Courses + + + Samples + + + Books + + + FAQ + + + + + + + + + Follow us on Social Media + + + + + + + X.com (Twitter) + + + Discord + + + Stack Overflow + + + + + + + YouTube + + + Instagram + + + GitHub + + + + + + + + + +@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("/workflows/definitions"); +} diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/Components/LoginRedirect.razor b/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/Components/LoginRedirect.razor new file mode 100644 index 000000000..edd773c0d --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/Components/LoginRedirect.razor @@ -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); + } +} diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/Components/Routes.razor b/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/Components/Routes.razor new file mode 100644 index 000000000..abe6f6e0f --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/Components/Routes.razor @@ -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 RouterOptions + + + + + + + + + diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/Components/_Imports.razor b/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/Components/_Imports.razor new file mode 100644 index 000000000..3c085a7eb --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/Components/_Imports.razor @@ -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 diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/ElsaShellPermissionPolicyProvider.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/ElsaShellPermissionPolicyProvider.cs new file mode 100644 index 000000000..b02b46750 --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/ElsaShellPermissionPolicyProvider.cs @@ -0,0 +1,42 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Options; +using System; +using System.Threading.Tasks; + +namespace LINGYUN.Abp.MicroService.WorkflowService; + +/// +/// Root 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 ), +/// so epPolicy names are bridged to that grant; other policy names fall back to the defaults. +/// +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())); + + /// + public Task 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(policy); + } + + /// + public Task GetDefaultPolicyAsync() => Task.FromResult(AllowAll); + + /// + public Task GetFallbackPolicyAsync() => Task.FromResult(AllowAll); +} diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/Extensions/ElsaStudioServiceCollectionExtensions.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/Extensions/ElsaStudioServiceCollectionExtensions.cs new file mode 100644 index 000000000..fd6855625 --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/Extensions/ElsaStudioServiceCollectionExtensions.cs @@ -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 configure, + Func> configureRetryPolicy = null) + { + var options = new OidcOptions(); + configure(options); + services.AddSingleton(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(); + 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(); + services.AddScoped(); + + // Shared token refresh service used by both session refresh and backend API token acquisition + services.AddScoped(); + + // Cookie authentication events for automatic session token refresh (standard ASP.NET Core pattern) + services.AddScoped(); + + // 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>(); + services.AddScoped(); + services.AddScoped(); + + // 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; + } +} diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/LINGYUN.Abp.MicroService.WorkflowService.csproj b/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/LINGYUN.Abp.MicroService.WorkflowService.csproj index df5920d1a..4a9b0aa05 100644 --- a/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/LINGYUN.Abp.MicroService.WorkflowService.csproj +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/LINGYUN.Abp.MicroService.WorkflowService.csproj @@ -7,20 +7,27 @@ LINGYUN.Abp.MicroService.WorkflowService
+ + + + + + - - - - - + + + + + + + - - + @@ -31,14 +38,17 @@ - + - + + + + - + @@ -62,21 +72,40 @@ - - - - - + - + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/Localization/Resources/en.json b/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/Localization/Resources/en.json new file mode 100644 index 000000000..55cc4d025 --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/Localization/Resources/en.json @@ -0,0 +1,6 @@ +{ + "culture": "en", + "texts": { + "Menu:Logout": "Logout" + } +} diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/Localization/Resources/zh-Hans.json b/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/Localization/Resources/zh-Hans.json new file mode 100644 index 000000000..e0c5360eb --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/Localization/Resources/zh-Hans.json @@ -0,0 +1,6 @@ +{ + "culture": "zh-Hans", + "texts": { + "Menu:Logout": "退出登录" + } +} diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/Navigation/UserMenuContributor.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/Navigation/UserMenuContributor.cs new file mode 100644 index 000000000..7aee5f4ae --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/Navigation/UserMenuContributor.cs @@ -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(); + + 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; + } +} diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/Pages/Account/Login.cshtml b/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/Pages/Account/Login.cshtml new file mode 100644 index 000000000..7e20249e0 --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/Pages/Account/Login.cshtml @@ -0,0 +1,6 @@ +@page "/Account/Login" +@model LINGYUN.Abp.MicroService.WorkflowService.Pages.Auth.LoginModel +@{ + ViewData["Title"] = "Login"; +} +

Redirecting to the identity provider…

diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/Pages/Account/Login.cshtml.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/Pages/Account/Login.cshtml.cs new file mode 100644 index 000000000..971e55c84 --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/Pages/Account/Login.cshtml.cs @@ -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; + +/// +/// 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. +/// +public class LoginModel : PageModel +{ + public IActionResult OnGet(string returnUrl = null) + { + var redirectUri = string.IsNullOrWhiteSpace(returnUrl) ? "/" : returnUrl; + return Challenge( + new AuthenticationProperties { RedirectUri = redirectUri }, + OpenIdConnectDefaults.AuthenticationScheme); + } +} diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/Pages/Account/Logout.cshtml b/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/Pages/Account/Logout.cshtml new file mode 100644 index 000000000..d45471480 --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/Pages/Account/Logout.cshtml @@ -0,0 +1,6 @@ +@page "/Account/Logout" +@model LINGYUN.Abp.MicroService.WorkflowService.Pages.Auth.LogoutModel +@{ + ViewData["Title"] = "Logout"; +} +

Signing out…

diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/Pages/Account/Logout.cshtml.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/Pages/Account/Logout.cshtml.cs new file mode 100644 index 000000000..cdaf35550 --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/Pages/Account/Logout.cshtml.cs @@ -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; + +/// +/// Signs out the local cookie and ends the OIDC session at the external AuthServer. +/// +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); + } +} diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/Pages/_Host.cshtml b/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/Pages/_Host.cshtml deleted file mode 100644 index 4b74acc72..000000000 --- a/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/Pages/_Host.cshtml +++ /dev/null @@ -1,42 +0,0 @@ -@page "/" -@inject IConfiguration Configuration; -@{ - var serverUrl = Configuration["Elsa:Server:BaseUrl"]; -} - - - - - - Elsa Workflows - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/Pages/_ViewImports.cshtml b/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/Pages/_ViewImports.cshtml index ca9c771ea..1ac695742 100644 --- a/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/Pages/_ViewImports.cshtml +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/Pages/_ViewImports.cshtml @@ -1,2 +1,3 @@ -@using Microsoft.Extensions.Configuration -@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers \ No newline at end of file +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Http +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/Program.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/Program.cs index 42b511467..4d1d94064 100644 --- a/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/Program.cs +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/Program.cs @@ -1,20 +1,25 @@ -using LINGYUN.Abp.Identity.Session.AspNetCore; +using Elsa.Extensions; +using LINGYUN.Abp.Identity.Session.AspNetCore; using LINGYUN.Abp.MicroService.WorkflowService; +using LINGYUN.Abp.MicroService.WorkflowService.Components; using LINGYUN.Abp.Serilog.Enrichers.Application; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; using Serilog; using System; using System.IO; +using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Routing; using Volo.Abp.IO; using Volo.Abp.Modularity.PlugIns; Log.Information("Starting WorkflowService Host..."); var builder = WebApplication.CreateBuilder(args); +builder.WebHost.UseStaticWebAssets(); builder.Host.AddAppSettingsSecretsJson() .UseAutofac() .ConfigureAppConfiguration((context, config) => @@ -27,7 +32,7 @@ builder.Host.AddAppSettingsSecretsJson() .UseSerilog((context, provider, config) => { config.ReadFrom.Configuration(context.Configuration); - }); + }, writeToProviders: true); builder.AddServiceDefaults(); @@ -49,18 +54,19 @@ await app.InitializeApplicationAsync(); app.MapDefaultEndpoints(); app.UseForwardedHeaders(); -// 本地化 -app.UseMapRequestLocalization(); +app.UseCors(); app.UseCorrelationId(); -app.MapAbpStaticAssets(); +app.UseMapRequestLocalization(); app.UseRouting(); -app.UseCors(); +app.UseStaticFiles(); +app.MapAbpStaticAssets(); app.UseAuthentication(); app.UseJwtTokenMiddleware(); app.UseMultiTenancy(); app.UseAbpSession(); app.UseDynamicClaims(); app.UseAuthorization(); +app.UseAntiforgery(); app.UseSwagger(); app.UseAbpSwaggerUI(options => { @@ -70,9 +76,15 @@ app.UseAbpSwaggerUI(options => options.OAuthClientId(configuration["AuthServer:SwaggerClientId"]); options.OAuthScopes(configuration["AuthServer:Audience"]); }); +app.UseWorkflowsApi(); // Use Elsa API endpoints. +app.UseWorkflows(); // Use Elsa middleware for HTTP Endpoint activities. app.UseAuditing(); app.UseAbpSerilogEnrichers(); -app.UseConfiguredEndpoints(); -app.UseHttpActivities(); +app.UseConfiguredEndpoints(builder => +{ + builder.MapRazorComponents() + .AddInteractiveServerRenderMode() + .AddAdditionalAssemblies(builder.ServiceProvider.GetRequiredService>().Value.AdditionalAssemblies.ToArray()); +}); await app.RunAsync(); \ No newline at end of file diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/WorkflowServiceModule.Configure.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/WorkflowServiceModule.Configure.cs index 7fe8f244e..149e8fbd7 100644 --- a/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/WorkflowServiceModule.Configure.cs +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/WorkflowServiceModule.Configure.cs @@ -1,31 +1,50 @@ using DotNetCore.CAP; -using Elsa; -using Elsa.Options; -using Elsa.Rebus.RabbitMq; -using LINGYUN.Abp.BackgroundTasks; +using Elsa.Agents; +using Elsa.AI.Persistence.EFCore.Sqlite.Extensions; +using Elsa.Alterations.Extensions; +using Elsa.Extensions; +using Elsa.Features.Services; +using Elsa.Persistence.EFCore.Extensions; +using Elsa.Persistence.EFCore.Modules.Alterations; +using Elsa.Persistence.EFCore.Modules.Labels; +using Elsa.Persistence.EFCore.Modules.Management; +using Elsa.Persistence.EFCore.Modules.Runtime; +using Elsa.Secrets.Persistence.EFCore.Extensions; +using Elsa.Secrets.Persistence.EFCore.Sqlite.Extensions; +using Elsa.Studio.Authentication.OpenIdConnect.HttpMessageHandlers; +using Elsa.Studio.Workflows.Designer.Extensions; +using LINGYUN.Abp.ElsaNext.Studio.Blazor; using LINGYUN.Abp.Localization.CultureMap; using LINGYUN.Abp.LocalizationManagement; +using LINGYUN.Abp.MicroService.WorkflowService.Extensions; +using LINGYUN.Abp.MicroService.WorkflowService.Navigation; +using LINGYUN.Abp.Notifications; using LINGYUN.Abp.Serilog.Enrichers.UniqueId; +using LINGYUN.Abp.WebhooksManagement; +using Localization.Resources.AbpUi; using Medallion.Threading; using Medallion.Threading.Redis; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.DataProtection; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.StackExchangeRedis; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Logging; using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi; -using Quartz; using StackExchange.Redis; using Swashbuckle.AspNetCore.SwaggerGen; using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Security.Claims; using System.Text.Encodings.Web; using System.Text.Unicode; +using Volo.Abp.AspNetCore.Components.Web; +using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Routing; using Volo.Abp.AspNetCore.Mvc; using Volo.Abp.AspNetCore.Mvc.AntiForgery; using Volo.Abp.Auditing; @@ -38,18 +57,12 @@ using Volo.Abp.Json.SystemTextJson; using Volo.Abp.Localization; using Volo.Abp.MultiTenancy; using Volo.Abp.PermissionManagement; -using Volo.Abp.Quartz; +using Volo.Abp.Roles; using Volo.Abp.Security.Claims; using Volo.Abp.Threading; using Volo.Abp.Timing; +using Volo.Abp.UI.Navigation; using Volo.Abp.VirtualFileSystem; -using ConsoleStartup = Elsa.Activities.Console.Startup; -using HttpStartup = Elsa.Activities.Http.Startup; -using JavaScriptStartup = Elsa.Scripting.JavaScript.Startup; -using TemporalQuartzStartup = Elsa.Activities.Temporal.Quartz.Startup; -using UserTaskStartup = Elsa.Activities.UserTask.Startup; -using WebhooksListEndpoint = Elsa.Webhooks.Api.Endpoints.List; -using WebhooksStartup = Elsa.Activities.Webhooks.Startup; namespace LINGYUN.Abp.MicroService.WorkflowService; @@ -105,102 +118,124 @@ public partial class WorkflowServiceModule private void PreConfigureQuartz(IConfiguration configuration) { - PreConfigure(options => - { - // 如果使用持久化存储, 则配置quartz持久层 - if (configuration.GetSection("Quartz:UsePersistentStore").Get()) - { - var settings = configuration.GetSection("Quartz:Properties").Get>(); - if (settings != null) - { - foreach (var setting in settings) - { - options.Properties[setting.Key] = setting.Value; - } - } + //PreConfigure(options => + //{ + // // 如果使用持久化存储, 则配置quartz持久层 + // if (configuration.GetSection("Quartz:UsePersistentStore").Get()) + // { + // var settings = configuration.GetSection("Quartz:Properties").Get>(); + // if (settings != null) + // { + // foreach (var setting in settings) + // { + // options.Properties[setting.Key] = setting.Value; + // } + // } - options.Configurator += (config) => - { - config.UsePersistentStore(store => - { - store.UseProperties = false; - store.UseNewtonsoftJsonSerializer(); - }); - }; - } - }); + // options.Configurator += (config) => + // { + // config.UsePersistentStore(store => + // { + // store.UseProperties = false; + // store.UseNewtonsoftJsonSerializer(); + // }); + // }; + // } + //}); } private void ConfigureBackgroundTasks(IServiceCollection services, IConfiguration configuration) { - Configure(options => - { - options.NodeName = services.GetApplicationName(); - }); + //Configure(options => + //{ + // options.NodeName = services.GetApplicationName(); + //}); } private void PreConfigureElsa(IServiceCollection services, IConfiguration configuration) { - var elsaSection = configuration.GetSection("Elsa"); - var startups = new[] - { - typeof(ConsoleStartup), - typeof(HttpStartup), - typeof(UserTaskStartup), - typeof(TemporalQuartzStartup), - typeof(JavaScriptStartup), - typeof(WebhooksStartup), - }; + PreConfigure(options => + { + options.IsBlazorWebApp = true; + }); + + services.AddAIPersistenceStores(options => options.UseSqlite()); - PreConfigure(elsa => + PreConfigure(elsa => { - elsa - .AddActivitiesFrom() - .AddWorkflowsFrom() - .AddFeatures(startups, configuration) - .ConfigureWorkflowChannels(options => elsaSection.GetSection("WorkflowChannels").Bind(options)) - .UseRabbitMq(elsaSection["Rebus:RabbitMQ:Connection"]); + elsa.UseWorkflowManagement(management => + { + management.UseEntityFrameworkCore(ef => ef.UseSqlite()); + }); - elsa.DistributedLockingOptionsBuilder - .UseProviderFactory(sp => name => - { - var provider = sp.GetRequiredService(); + elsa.UseWorkflowRuntime(runtime => + { + runtime.UseEntityFrameworkCore(ef => ef.UseSqlite()); + }); - return provider.CreateLock(name); - }); - }); + elsa.UseAlterations(alteration => + { + alteration.UseEntityFrameworkCore(ef => ef.UseSqlite()); + }); + + elsa.UseAgentPersistence(agent => + { + agent.UseEntityFrameworkCore(ef => ef.UseSqlite()); + }); + + elsa.UseLabels(label => + { + label.UseEntityFrameworkCore(ef => ef.UseSqlite()); + }); + + elsa.UseSecrets(secret => + { + secret.UseEntityFrameworkCore(ef => ef.UseSqlite()); + }); + + elsa.UseQuartz(quartz => + { + quartz.UseSqlite(); + }); - services.AddNotificationHandlersFrom(); + elsa.UseScheduling(scheduling => + { + scheduling.UseQuartzScheduler(); + }); + }); - PreConfigure(mvcBuilder => + PreConfigure(options => { - mvcBuilder.AddApplicationPartIfNotExists(typeof(WebhooksListEndpoint).Assembly); + options.BackendApiConfig.ConfigureHttpClientBuilder = options => + { + options.AuthenticationHandler = typeof(OidcAuthenticatingApiHttpMessageHandler); + options.ConfigureHttpClient = (_, client) => + { + // Set a long time out to simplify debugging both Elsa Studio and the Elsa Server backend. + client.Timeout = TimeSpan.FromHours(1); + }; + }; }); + + services.AddRazorComponents() + .AddInteractiveServerComponents(options => + { + options.RootComponents.RegisterCustomElsaStudioElements(); + }); } - private void ConfigureEndpoints(IServiceCollection services) + private void ConfigureElsa(IServiceCollection services, IConfiguration configuration) { - // 不需要 - //Configure(options => + //Configure(options => //{ - // options.EndpointConfigureActions.Add( - // (context) => - // { - // context.Endpoints.MapFallbackToPage("/_Host"); - // }); + // options.WithFeature(); //}); - var preActions = services.GetPreConfigureActions(); - services.AddAbpApiVersioning(options => - { - options.ReportApiVersions = true; - options.AssumeDefaultVersionWhenUnspecified = true; + // EndpointSecurityOptions.DisableSecurity(); - //options.ApiVersionReader = new HeaderApiVersionReader("api-version"); //Supports header too - //options.ApiVersionReader = new MediaTypeApiVersionReader(); //Supports accept header too - }, mvcOptions => + services.AddOpenIdConnectAuth(options => { - mvcOptions.ConfigureAbp(preActions.Configure()); + configuration.GetSection("Authentication:OpenIdConnect").Bind(options); }); } @@ -296,6 +331,16 @@ public partial class WorkflowServiceModule { options.ExposeIntegrationServices = true; }); + + Configure(options => + { + options.MenuContributors.Add(new UserMenuContributor()); + }); + + Configure(options => + { + options.AppAssembly = typeof(WorkflowServiceModule).Assembly; + }); } private void ConfigureVirtualFileSystem() @@ -306,12 +351,30 @@ public partial class WorkflowServiceModule }); } + private void ConfigureNotificationsManagement() + { + Configure(options => + { + options.IsDynamicNotificationsStoreEnabled = true; + options.SaveStaticNotificationsToDatabase = false; + }); + } + private void ConfigurePermissionManagement() { Configure(options => { options.IsDynamicPermissionStoreEnabled = true; - options.SaveStaticPermissionsToDatabase = false; + options.SaveStaticPermissionsToDatabase = true; + }); + } + + private void ConfigureWebhooksManagement() + { + Configure(options => + { + options.IsDynamicWebhookStoreEnabled = true; + options.SaveStaticWebhooksToDatabase = false; }); } @@ -343,7 +406,7 @@ public partial class WorkflowServiceModule { options.IsDynamicClaimsEnabled = true; options.RemoteRefreshUrl = configuration["App:RefreshClaimsUrl"] + options.RemoteRefreshUrl; - + options.DynamicClaims.Add(Elsa.PermissionNames.ClaimType); }); } @@ -411,6 +474,9 @@ public partial class WorkflowServiceModule { options.Languages.Add(new LanguageInfo("en", "en", "English")); options.Languages.Add(new LanguageInfo("zh-Hans", "zh-Hans", "简体中文")); + + options.Resources.Get() + .AddVirtualJson("/Localization/Resources"); }); Configure(options => @@ -438,7 +504,7 @@ public partial class WorkflowServiceModule options.AutoValidate = false; }); - services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + services.AddAuthentication() .AddAbpJwtBearer(options => { configuration.GetSection("AuthServer").Bind(options); @@ -454,7 +520,38 @@ public partial class WorkflowServiceModule { options.TokenValidationParameters.ValidAudiences = validAudiences; } - }); + + options.Events ??= new JwtBearerEvents(); + var previousOnTokenValidated = options.Events.OnTokenValidated; + options.Events.OnTokenValidated = async context => + { + await previousOnTokenValidated(context); + + if (context.Principal?.Identity?.IsAuthenticated == true) + { + //var options = context.HttpContext.RequestServices.GetRequiredService>(); + //var permissionChecker = context.HttpContext.RequestServices.GetRequiredService(); + //var permissionNames = options.Value.PermissionMaps.Select(x => x.Source).ToArray(); + //var checkResult = await permissionChecker.IsGrantedAsync(context.Principal, permissionNames); + + //var grantPermissions = checkResult.Result.Where(x => x.Value == PermissionGrantResult.Granted).Select(x => x.Key); + + //var elsaPermissions = options.Value.PermissionMaps.Where(x => grantPermissions.Contains(x.Source)) + // .Select(x => new Claim("permissions", x.Target)) + // .ToList(); + + //context.Principal.AddIdentity(new ClaimsIdentity(elsaPermissions)); + + var roleClaims = context.Principal.FindAll(AbpClaimTypes.Role); + if (roleClaims.Any(x => x.Value.Equals(AbpRoleConsts.AdminRoleName, StringComparison.CurrentCultureIgnoreCase))) + { + context.Principal.AddIdentity(new ClaimsIdentity( + new[] { new Claim(Elsa.PermissionNames.ClaimType, Elsa.PermissionNames.All) })); + } + } + }; + }) + ; services .AddDataProtection() diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/WorkflowServiceModule.cs b/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/WorkflowServiceModule.cs index dc23b98f5..a00464ccb 100644 --- a/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/WorkflowServiceModule.cs +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/WorkflowServiceModule.cs @@ -2,16 +2,29 @@ using LINGYUN.Abp.AspNetCore.Mvc.Wrapper; using LINGYUN.Abp.AuditLogging.Elasticsearch; using LINGYUN.Abp.Authorization.OrganizationUnits; -using LINGYUN.Abp.BackgroundTasks.DistributedLocking; -using LINGYUN.Abp.BackgroundTasks.Quartz; using LINGYUN.Abp.BlobStoring.BlobManagement; using LINGYUN.Abp.Claims.Mapping; using LINGYUN.Abp.Data.DbMigrator; using LINGYUN.Abp.Dynamic.Definitions; -using LINGYUN.Abp.Elsa; -using LINGYUN.Abp.Elsa.Activities; -using LINGYUN.Abp.Elsa.EntityFrameworkCore.PostgreSql; -using LINGYUN.Abp.Elsa.Notifications; +using LINGYUN.Abp.ElsaNext.Agents.Blazor; +using LINGYUN.Abp.ElsaNext.BlobStoring; +using LINGYUN.Abp.ElsaNext.Email; +using LINGYUN.Abp.ElsaNext.Labels; +using LINGYUN.Abp.ElsaNext.Notifications; +using LINGYUN.Abp.ElsaNext.Secrets.Blazor; +using LINGYUN.Abp.ElsaNext.Server; +using LINGYUN.Abp.ElsaNext.Studio.AI.Blazor; +using LINGYUN.Abp.ElsaNext.Studio.Alterations.Blazor; +using LINGYUN.Abp.ElsaNext.Studio.Blazor; +using LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor; +using LINGYUN.Abp.ElsaNext.Studio.Diagnostics.OpenTelemetry.Blazor; +using LINGYUN.Abp.ElsaNext.Studio.Diagnostics.StructuredLogs.Blazor; +using LINGYUN.Abp.ElsaNext.Studio.Identity.Blazor; +using LINGYUN.Abp.ElsaNext.Studio.Labels.Blazor; +using LINGYUN.Abp.ElsaNext.Studio.Notifications.Blazor; +using LINGYUN.Abp.ElsaNext.Studio.Saas.Blazor; +using LINGYUN.Abp.ElsaNext.Studio.Webhooks.Blazor; +using LINGYUN.Abp.ElsaNext.Webhooks; using LINGYUN.Abp.Emailing.Platform; using LINGYUN.Abp.EventBus.CAP; using LINGYUN.Abp.ExceptionHandling.Emailing; @@ -19,25 +32,27 @@ using LINGYUN.Abp.Http.Client.Wrapper; using LINGYUN.Abp.Identity.Session.AspNetCore; using LINGYUN.Abp.Localization.CultureMap; using LINGYUN.Abp.LocalizationManagement.EntityFrameworkCore; -using LINGYUN.Abp.Quartz.PostgresSqlInstaller; +using LINGYUN.Abp.Notifications.EntityFrameworkCore; using LINGYUN.Abp.Saas.EntityFrameworkCore; using LINGYUN.Abp.Serilog.Enrichers.Application; using LINGYUN.Abp.Serilog.Enrichers.UniqueId; using LINGYUN.Abp.Sms.Platform; -using LINGYUN.Abp.TaskManagement.EntityFrameworkCore; +using LINGYUN.Abp.WebhooksManagement.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using System; using Volo.Abp; using Volo.Abp.AspNetCore.Authentication.JwtBearer; +using Volo.Abp.AspNetCore.Authentication.OpenIdConnect; +using Volo.Abp.AspNetCore.Components.Server.MudBlazorBasicTheme; using Volo.Abp.AspNetCore.MultiTenancy; using Volo.Abp.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc.NewtonsoftJson; using Volo.Abp.AspNetCore.Serilog; using Volo.Abp.Autofac; using Volo.Abp.Caching.StackExchangeRedis; using Volo.Abp.FeatureManagement.EntityFrameworkCore; using Volo.Abp.Http.Client.IdentityModel.Web; +using Volo.Abp.Identity.EntityFrameworkCore; using Volo.Abp.Modularity; using Volo.Abp.PermissionManagement.EntityFrameworkCore; using Volo.Abp.SettingManagement.EntityFrameworkCore; @@ -52,29 +67,48 @@ namespace LINGYUN.Abp.MicroService.WorkflowService; typeof(AbpAuditLoggingElasticsearchModule), typeof(AbpAspNetCoreSerilogModule), typeof(AbpBlobStoringBlobManagementModule), - typeof(AbpElsaModule), - typeof(AbpElsaServerModule), - typeof(AbpElsaActivitiesModule), - typeof(AbpElsaNotificationsModule), + typeof(AbpElsaNextBlobStoringModule), + typeof(AbpElsaNextEmailModule), + typeof(AbpElsaNextLabelsModule), + typeof(AbpElsaNextNotificationsModule), + typeof(AbpElsaNextWebhooksModule), + typeof(AbpElsaNextServerModule), + typeof(AbpElsaNextStudioBlazorModule), + typeof(AbpElsaNextAgentsBlazorModule), + typeof(AbpElsaNextSecretsBlazorModule), + typeof(AbpElsaNextStudioAIBlazorModule), + typeof(AbpElsaNextStudioAlterationsBlazorModule), + typeof(AbpElsaNextStudioDashboardBlazorModule), + typeof(AbpElsaNextStudioDiagnosticsOpenTelemetryBlazorModule), + typeof(AbpElsaNextStudioDiagnosticsStructuredLogsBlazorModule), + typeof(AbpElsaNextStudioIdentityBlazorModule), + typeof(AbpElsaNextStudioLabelsBlazorModule), + typeof(AbpElsaNextStudioNotificationsBlazorModule), + typeof(AbpElsaNextStudioSaasBlazorModule), + typeof(AbpElsaNextStudioWebhooksBlazorModule), typeof(AbpEmailingExceptionHandlingModule), typeof(AbpHttpClientIdentityModelWebModule), typeof(AbpAspNetCoreMultiTenancyModule), - typeof(AbpBackgroundTasksQuartzModule), - typeof(AbpBackgroundTasksDistributedLockingModule), - typeof(AbpQuartzPostgresSqlInstallerModule), - typeof(TaskManagementEntityFrameworkCoreModule), + //typeof(AbpBackgroundTasksQuartzModule), + //typeof(AbpBackgroundTasksDistributedLockingModule), + //typeof(AbpQuartzPostgresSqlInstallerModule), + //typeof(TaskManagementEntityFrameworkCoreModule), typeof(AbpFeatureManagementEntityFrameworkCoreModule), typeof(AbpPermissionManagementEntityFrameworkCoreModule), typeof(AbpSettingManagementEntityFrameworkCoreModule), typeof(AbpSaasEntityFrameworkCoreModule), + typeof(AbpIdentityEntityFrameworkCoreModule), typeof(AbpLocalizationManagementEntityFrameworkCoreModule), - typeof(AbpElsaEntityFrameworkCorePostgreSqlModule), + typeof(AbpNotificationsEntityFrameworkCoreModule), + typeof(WebhooksManagementEntityFrameworkCoreModule), typeof(AbpAuthorizationOrganizationUnitsModule), typeof(AbpAspNetCoreAuthenticationJwtBearerModule), + typeof(AbpAspNetCoreAuthenticationOpenIdConnectModule), typeof(AbpTextTemplatingScribanModule), typeof(AbpDataDbMigratorModule), typeof(AbpCachingStackExchangeRedisModule), typeof(AbpAspNetCoreMvcModule), + typeof(AbpAspNetCoreComponentsServerMudBlazorBasicThemeModule), typeof(AbpSwashbuckleModule), typeof(AbpCAPEventBusModule), typeof(AbpLocalizationCultureMapModule), @@ -83,7 +117,6 @@ namespace LINGYUN.Abp.MicroService.WorkflowService; typeof(AbpSmsPlatformModule), typeof(AbpEmailingPlatformModule), typeof(AbpClaimsMappingModule), - typeof(AbpAspNetCoreMvcNewtonsoftModule), typeof(AbpAspNetCoreHttpOverridesModule), typeof(AbpDynamicDefinitionsModule), typeof(AbpIdentitySessionAspNetCoreModule), @@ -114,21 +147,21 @@ public partial class WorkflowServiceModule : AbpModule ConfigureDbContext(); ConfigureLocalization(); ConfigureVirtualFileSystem(); + ConfigureNotificationsManagement(); ConfigurePermissionManagement(); + ConfigureWebhooksManagement(); ConfigureTiming(configuration); ConfigureCaching(configuration); ConfigureAuditing(configuration); ConfigureIdentity(configuration); ConfigureMultiTenancy(configuration); - ConfigureEndpoints(context.Services); ConfigureMvc(context.Services, configuration); ConfigureCors(context.Services, configuration); + ConfigureElsa(context.Services, configuration); ConfigureSwagger(context.Services, configuration); ConfigureBlobStoring(context.Services, configuration); ConfigureDistributedLock(context.Services, configuration); ConfigureBackgroundTasks(context.Services, configuration); ConfigureSecurity(context.Services, configuration, hostingEnvironment.IsDevelopment()); - - context.Services.AddRazorPages(); } } diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/appsettings.Development.json b/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/appsettings.Development.json index 9e4f85d16..4ffceca51 100644 --- a/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/appsettings.Development.json +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/appsettings.Development.json @@ -33,77 +33,56 @@ } }, "Elsa": { - "Features": { - "DefaultPersistence": { - "Enabled": true, - "ConnectionStringIdentifier": "Default", - "YesSql": { - "MySql": { + "AI": { + "Host": { + "DefaultProviderName": "copilot", + "Providers": [ + { + "Name": "copilot", + "Provider": "copilot", + "Model": "GPT-5 mini", "Enabled": true } - }, - "EntityFrameworkCore": { - "MySql": { - "Enabled": true - } - } - }, - "Console": true, - "Http": true, - "Email": true, - "TemporalQuartz": true, - "JavaScriptActivities": true, - "UserTask": true, - "Conductor": true, - "Telnyx": true, - "BlobStoring": true, - "Emailing": true, - "Notification": true, - "Sms": true, - "IM": true, - "PublishWebhook": true, - "Webhooks": { - "Enabled": true, - "ConnectionStringIdentifier": "Default", - "YesSql": { - "MySql": { - "Enabled": true - } - }, - "EntityFrameworkCore": { - "MySql": { - "Enabled": true - } - } + ] }, - "WorkflowSettings": { - "Enabled": true, - "ConnectionStringIdentifier": "Default", - "YesSql": { - "MySql": { - "Enabled": true - } - }, - "EntityFrameworkCore": { - "MySql": { - "Enabled": true - } - } + "Copilot": { + "EnableStreaming": true, + "Model": "GPT-5 mini", + "WorkingDirectory": "copilot", + "BaseDirectory": "./workspace" } }, - "Rebus": { - "RabbitMQ": { - "Connection": "amqp://admin:123456@localhost:5672/" - } + "Http": { + "BaseUrl": "http://localhost:30050" + }, + "Secrets": { + "EncryptionKey": "Q0hBTkdFX01FX1RPX0FfU0VDVVJFX1JBTkRPTV9LRVk=" }, "Server": { - "BaseUrl": "http://localhost:30035" + "EnableRealTimeWorkflows": true + }, + "Studio": { + "Backend": { + "Url": "http://localhost:30050/elsa/api" + }, + "DesignerOptions": { + "UseReactFlow": false + }, + "Shell": { + "DisableAuthorization": true + }, + "Presentation": { + "Theme": "classic" + } } }, "RemoteServices": { "BlobManagement": { "BaseUrl": "http://localhost:30025", - "IdentityClient": "InternalServiceClient", + "UseCurrentAccessToken": false + }, + "Platform": { + "BaseUrl": "http://localhost:30025", "UseCurrentAccessToken": false } }, @@ -159,8 +138,26 @@ "ValidAudiences": [ "lingyun-abp-application" ], "MapInboundClaims": false, "RequireHttpsMetadata": false, - "ElsaClientId": "InternalServiceClient", - "SwaggerClientId": "vue-oauth-client" + "ClientId": "elsa-studio-client", + "SwaggerClientId": "elsa-studio-client" + }, + "Authentication": { + "OpenIdConnect": { + "Authority": "http://localhost:44385", + "ClientId": "elsa-studio-client", + "ClientSecret": null, + "AuthenticationScopes": [ + "openid", + "profile", + "roles", + "email", + "offline_access", + "workflow-service" + ], + "MapInboundClaims": false, + "RequireHttpsMetadata": false, + "SaveTokens": true + } }, "Logging": { "Serilog": { @@ -183,7 +180,8 @@ "Override": { "System": "Warning", "Microsoft": "Warning", - "DotNetCore": "Warning" + "DotNetCore": "Warning", + "LINGYUN.Abp.ElsaNext.Notifications": "Debug" } }, "WriteTo": [ diff --git a/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/package.json b/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/package.json new file mode 100644 index 000000000..32c7e2da9 --- /dev/null +++ b/aspnet-core/aspire/LINGYUN.Abp.MicroService.WorkflowService/package.json @@ -0,0 +1,8 @@ +{ + "version": "10.6.0", + "name": "aspire-workflow-service", + "private": true, + "dependencies": { + "@abp/aspnetcore.components.server.basictheme": "10.6.0" + } +} \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.Elsa.Activities.BlobStoring/LINGYUN/Abp/Elsa/Activities/BlobStoring/ElsaBlobContainer.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.Elsa.Activities.BlobStoring/LINGYUN/Abp/Elsa/Activities/BlobStoring/ElsaBlobContainer.cs index b37a25c74..1a7e8cc87 100644 --- a/aspnet-core/modules/elsa/LINGYUN.Abp.Elsa.Activities.BlobStoring/LINGYUN/Abp/Elsa/Activities/BlobStoring/ElsaBlobContainer.cs +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.Elsa.Activities.BlobStoring/LINGYUN/Abp/Elsa/Activities/BlobStoring/ElsaBlobContainer.cs @@ -2,7 +2,7 @@ namespace LINGYUN.Abp.Elsa.Activities.BlobStoring; -[BlobContainerName("elsa")] +[BlobContainerName("workflow")] public class ElsaBlobContainer { } diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.AI/FodyWeavers.xml b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.AI/FodyWeavers.xml new file mode 100644 index 000000000..5d6962159 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.AI/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.AI/FodyWeavers.xsd b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.AI/FodyWeavers.xsd new file mode 100644 index 000000000..3f3946e28 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.AI/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.AI/LINGYUN.Abp.ElsaNext.AI.csproj b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.AI/LINGYUN.Abp.ElsaNext.AI.csproj new file mode 100644 index 000000000..9d149eb73 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.AI/LINGYUN.Abp.ElsaNext.AI.csproj @@ -0,0 +1,24 @@ + + + + + + + net10.0 + LINGYUN.Abp.ElsaNext.AI + LINGYUN.Abp.ElsaNext.AI + false + false + false + + + + + + + + + + + + diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.AI/LINGYUN/Abp/ElsaNext/AI/AbpElsaNextAIModule.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.AI/LINGYUN/Abp/ElsaNext/AI/AbpElsaNextAIModule.cs new file mode 100644 index 000000000..0570b6f44 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.AI/LINGYUN/Abp/ElsaNext/AI/AbpElsaNextAIModule.cs @@ -0,0 +1,23 @@ +using Elsa.Features.Services; +using LINGYUN.Abp.ElsaNext.AI.Extensions; +using Microsoft.Extensions.Configuration; +using Volo.Abp.Modularity; + +namespace LINGYUN.Abp.ElsaNext.AI; + +[DependsOn(typeof(AbpElsaNextModule))] +public class AbpElsaNextAIModule : AbpModule +{ + public override void PreConfigureServices(ServiceConfigurationContext context) + { + var configuration = context.Configuration.GetSection("Elsa:AI"); + PreConfigure(elsa => + { + elsa.UseAbpAI(ai => + { + ai.ConfigureOptions = options => configuration.GetSection("Host").Bind(options); + ai.ConfigureCopilotOptions = options => configuration.GetSection("Copilot").Bind(options); + }); + }); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.AI/LINGYUN/Abp/ElsaNext/AI/Extensions/ModuleExtensions.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.AI/LINGYUN/Abp/ElsaNext/AI/Extensions/ModuleExtensions.cs new file mode 100644 index 000000000..6f651b2a9 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.AI/LINGYUN/Abp/ElsaNext/AI/Extensions/ModuleExtensions.cs @@ -0,0 +1,14 @@ +using Elsa.Extensions; +using Elsa.Features.Services; +using LINGYUN.Abp.ElsaNext.AI.Features; +using System; + +namespace LINGYUN.Abp.ElsaNext.AI.Extensions; + +public static class ModuleExtensions +{ + public static IModule UseAbpAI(this IModule module, Action? configure = null) + { + return module.Use(configure); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.AI/LINGYUN/Abp/ElsaNext/AI/Features/AbpAIFeature.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.AI/LINGYUN/Abp/ElsaNext/AI/Features/AbpAIFeature.cs new file mode 100644 index 000000000..689dbda46 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.AI/LINGYUN/Abp/ElsaNext/AI/Features/AbpAIFeature.cs @@ -0,0 +1,44 @@ +using Elsa.AI.Copilot.Options; +using Elsa.AI.Host.Features; +using Elsa.AI.Host.Options; +using Elsa.AI.Host.Services; +using Elsa.Extensions; +using Elsa.Features.Abstractions; +using Elsa.Features.Services; +using Microsoft.Extensions.DependencyInjection; +using System; + +namespace LINGYUN.Abp.ElsaNext.AI.Features; + +public class AbpAIFeature(IModule module) : FeatureBase(module) +{ + public Action? ConfigureOptions { get; set; } + public Action? ConfigureCopilotOptions { get; set; } + public Action? ConfigureToolEnablement { get; set; } + + public override void Configure() + { + Module.AddFastEndpointsAssembly(); + } + + public override void Apply() + { + Services.AddAIHostServices(ConfigureOptions); + Services.AddCopilotAIProvider(ConfigureCopilotOptions); + if (ConfigureToolEnablement != null) + { + Services.AddSingleton(ConfigureToolEnablement); + } + Module.AddFastEndpointsFromModule(); + } + + public AbpAIFeature EnableWorkflowProposalTools() + { + ConfigureToolEnablement += enablement => + { + enablement.Enable("workflows.proposeCreate"); + enablement.Enable("workflows.proposeUpdate"); + }; + return this; + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Agents.Blazor/AbpElsaNextAgentsBlazorModule.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Agents.Blazor/AbpElsaNextAgentsBlazorModule.cs new file mode 100644 index 000000000..551a4d516 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Agents.Blazor/AbpElsaNextAgentsBlazorModule.cs @@ -0,0 +1,49 @@ +using Elsa.Studio.Extensions; +using LINGYUN.Abp.ElsaNext.Agents.Blazor.Navigation; +using LINGYUN.Abp.ElsaNext.Localization; +using LINGYUN.Abp.ElsaNext.Studio.Blazor; +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(options => + { + options.AdditionalAssemblies.Add(typeof(Elsa.Studio.Agents.Feature).Assembly); + }); + + Configure(options => + { + options.MenuContributors.Add(new ElsaStudioAgentsMenuContributor()); + }); + + Configure(options => + { + options.FileSets.AddEmbedded("LINGYUN.Abp.ElsaNext.Agents.Blazor"); + }); + + Configure(options => + { + options.Resources + .Get() + .AddVirtualJson("/Localization/Resources/AgentsBlazor"); + }); + + var elsaNextStudioBlazoeOptions = context.Services.ExecutePreConfiguredActions(); + + context.Services.AddAgentsModule(elsaNextStudioBlazoeOptions.BackendApiConfig); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Agents.Blazor/FodyWeavers.xml b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Agents.Blazor/FodyWeavers.xml new file mode 100644 index 000000000..5d6962159 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Agents.Blazor/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Agents.Blazor/FodyWeavers.xsd b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Agents.Blazor/FodyWeavers.xsd new file mode 100644 index 000000000..3f3946e28 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Agents.Blazor/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Agents.Blazor/LINGYUN.Abp.ElsaNext.Agents.Blazor.csproj b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Agents.Blazor/LINGYUN.Abp.ElsaNext.Agents.Blazor.csproj new file mode 100644 index 000000000..86d30d0d3 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Agents.Blazor/LINGYUN.Abp.ElsaNext.Agents.Blazor.csproj @@ -0,0 +1,29 @@ + + + + + + + net10.0 + LINGYUN.Abp.ElsaNext.Agents.Blazor + LINGYUN.Abp.ElsaNext.Agents.Blazor + false + false + false + + + + + + + + + + + + + + + + + diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Agents.Blazor/Localization/Resources/AgentsBlazor/en.json b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Agents.Blazor/Localization/Resources/AgentsBlazor/en.json new file mode 100644 index 000000000..9dc5c9c95 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Agents.Blazor/Localization/Resources/AgentsBlazor/en.json @@ -0,0 +1,7 @@ +{ + "culture": "en", + "texts": { + "Menu:Agents": "Agents", + "Agents": "Agents" + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Agents.Blazor/Localization/Resources/AgentsBlazor/zh-Hans.json b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Agents.Blazor/Localization/Resources/AgentsBlazor/zh-Hans.json new file mode 100644 index 000000000..e2f1a03f7 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Agents.Blazor/Localization/Resources/AgentsBlazor/zh-Hans.json @@ -0,0 +1,7 @@ +{ + "culture": "zh-Hans", + "texts": { + "Menu:Agents": "代理", + "Agents": "代理" + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Agents.Blazor/Navigation/ElsaStudioAgentsMenuContributor.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Agents.Blazor/Navigation/ElsaStudioAgentsMenuContributor.cs new file mode 100644 index 000000000..c3c522a88 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Agents.Blazor/Navigation/ElsaStudioAgentsMenuContributor.cs @@ -0,0 +1,28 @@ +using LINGYUN.Abp.ElsaNext.Localization; +using LINGYUN.Abp.ElsaNext.Studio.Blazor.Navigation; +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(); + + 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; + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Agents.Blazor/_Imports.razor b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Agents.Blazor/_Imports.razor new file mode 100644 index 000000000..6a4064616 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Agents.Blazor/_Imports.razor @@ -0,0 +1,5 @@ +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Web +@using Elsa.Studio.Components +@using Elsa.Studio.Localization +@using MudBlazor diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Agents/FodyWeavers.xml b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Agents/FodyWeavers.xml new file mode 100644 index 000000000..5d6962159 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Agents/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Agents/FodyWeavers.xsd b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Agents/FodyWeavers.xsd new file mode 100644 index 000000000..3f3946e28 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Agents/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Agents/LINGYUN.Abp.ElsaNext.Agents.csproj b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Agents/LINGYUN.Abp.ElsaNext.Agents.csproj new file mode 100644 index 000000000..a29230927 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Agents/LINGYUN.Abp.ElsaNext.Agents.csproj @@ -0,0 +1,27 @@ + + + + + + + net10.0 + LINGYUN.Abp.ElsaNext.Agents + LINGYUN.Abp.ElsaNext.Agents + enable + false + false + false + + + + + + + + + + + + + + diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Agents/LINGYUN/Abp/ElsaNext/Agents/AbpElsaNextAgentsModule.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Agents/LINGYUN/Abp/ElsaNext/Agents/AbpElsaNextAgentsModule.cs new file mode 100644 index 000000000..070175c09 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Agents/LINGYUN/Abp/ElsaNext/Agents/AbpElsaNextAgentsModule.cs @@ -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(elsa => + { + elsa.UseAgents(); + elsa.UseAgentActivities(); + + elsa.UseAgentsApi(); + }); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Alterations/FodyWeavers.xml b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Alterations/FodyWeavers.xml new file mode 100644 index 000000000..5d6962159 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Alterations/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Alterations/FodyWeavers.xsd b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Alterations/FodyWeavers.xsd new file mode 100644 index 000000000..3f3946e28 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Alterations/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Alterations/LINGYUN.Abp.ElsaNext.Alterations.csproj b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Alterations/LINGYUN.Abp.ElsaNext.Alterations.csproj new file mode 100644 index 000000000..2001eff90 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Alterations/LINGYUN.Abp.ElsaNext.Alterations.csproj @@ -0,0 +1,24 @@ + + + + + + + net10.0 + LINGYUN.Abp.ElsaNext.Alterations + LINGYUN.Abp.ElsaNext.Alterations + false + false + false + + + + + + + + + + + + diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Alterations/LINGYUN/Abp/ElsaNext/Alterations/AbpElsaNextAlterationsModule.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Alterations/LINGYUN/Abp/ElsaNext/Alterations/AbpElsaNextAlterationsModule.cs new file mode 100644 index 000000000..e1f4c0470 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Alterations/LINGYUN/Abp/ElsaNext/Alterations/AbpElsaNextAlterationsModule.cs @@ -0,0 +1,28 @@ +using Elsa.Alterations.Extensions; +using Elsa.Features.Contracts; +using Elsa.Features.Services; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp; +using Volo.Abp.Modularity; + +namespace LINGYUN.Abp.ElsaNext.Alterations; + +[DependsOn( + typeof(AbpElsaNextModule))] +public class AbpElsaNextAlterationsModule : AbpModule +{ + public override void PreConfigureServices(ServiceConfigurationContext context) + { + PreConfigure(elsa => + { + elsa.UseAlterations(); + }); + } + + public override void OnApplicationInitialization(ApplicationInitializationContext context) + { + // TODO: 强制启用Studio中的Alterations? + var registry = context.ServiceProvider.GetRequiredService(); + registry.Add(new("Alterations.ShellFeatures.Alterations", "Elsa", "Alterations.ShellFeatures.Alterations")); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/FodyWeavers.xml b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/FodyWeavers.xml new file mode 100644 index 000000000..5d6962159 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/FodyWeavers.xsd b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/FodyWeavers.xsd new file mode 100644 index 000000000..3f3946e28 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN.Abp.ElsaNext.BlobStoring.csproj b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN.Abp.ElsaNext.BlobStoring.csproj new file mode 100644 index 000000000..60f8ffd5b --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN.Abp.ElsaNext.BlobStoring.csproj @@ -0,0 +1,29 @@ + + + + + + + net10.0 + LINGYUN.Abp.ElsaNext.BlobStoring + LINGYUN.Abp.ElsaNext.BlobStoring + false + false + false + + + + + + + + + + + + + + + + + diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/AbpElsaNextBlobStoringModule.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/AbpElsaNextBlobStoringModule.cs new file mode 100644 index 000000000..251a6f7b7 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/AbpElsaNextBlobStoringModule.cs @@ -0,0 +1,38 @@ +using Elsa.Features.Services; +using LINGYUN.Abp.ElsaNext.BlobStoring.Extensions; +using LINGYUN.Abp.ElsaNext.Localization; +using Volo.Abp.BlobStoring; +using Volo.Abp.Localization; +using Volo.Abp.Modularity; +using Volo.Abp.VirtualFileSystem; + +namespace LINGYUN.Abp.ElsaNext.BlobStoring; + +[DependsOn( + typeof(AbpBlobStoringModule), + typeof(AbpElsaNextModule))] +public class AbpElsaNextBlobStoringModule : AbpModule +{ + public override void PreConfigureServices(ServiceConfigurationContext context) + { + PreConfigure(elsa => + { + elsa.UseAbpBlobStoring(); + }); + } + + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.FileSets.AddEmbedded(); + }); + + Configure(options => + { + options.Resources + .Get() + .AddVirtualJson("/LINGYUN/Abp/ElsaNext/BlobStoring/Localization/Resources"); + }); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/Activities/BlobActivity.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/Activities/BlobActivity.cs new file mode 100644 index 000000000..3b52c5650 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/Activities/BlobActivity.cs @@ -0,0 +1,39 @@ +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using Elsa.Workflows.Models; +using System.Threading.Tasks; +using Volo.Abp.BlobStoring; + +namespace LINGYUN.Abp.ElsaNext.BlobStoring.Activities; + +public abstract class BlobActivity : CodeActivity +{ + [Input(Description = "Path of the blob.")] + public Input Path { get; set; } = default!; + + protected virtual IBlobContainer GetBlobContainer(ActivityExecutionContext context) + { + return context.GetRequiredService>(); + } + + protected async virtual ValueTask OnErrorCompletedAsync(ActivityCompletedContext context) + { + await context.TargetContext.CompleteActivityAsync(); + } +} + +public abstract class BlobActivity : CodeActivity +{ + [Input(Description = "Path of the blob.")] + public Input Path { get; set; } = default!; + + protected virtual IBlobContainer GetBlobContainer(ActivityExecutionContext context) + { + return context.GetRequiredService>(); + } + + protected async virtual ValueTask OnErrorCompletedAsync(ActivityCompletedContext context) + { + await context.TargetContext.CompleteActivityAsync(); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/Activities/DeleteBlob.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/Activities/DeleteBlob.cs new file mode 100644 index 000000000..2cf9dfb71 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/Activities/DeleteBlob.cs @@ -0,0 +1,20 @@ +using Elsa.Extensions; +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using System.Threading.Tasks; + +namespace LINGYUN.Abp.ElsaNext.BlobStoring.Activities; + +[Activity("Elsa", "BlobStoring", "Delete a blob.", Kind = ActivityKind.Task)] +public class DeleteBlob : BlobActivity +{ + protected async override ValueTask ExecuteAsync(ActivityExecutionContext context) + { + var cancellationToken = context.CancellationToken; + var path = Path.Get(context); + var blobContainer = GetBlobContainer(context); + var result = await blobContainer.DeleteAsync(path, cancellationToken); + + Result.Set(context, result); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/Activities/ReadBlob.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/Activities/ReadBlob.cs new file mode 100644 index 000000000..f81a22de0 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/Activities/ReadBlob.cs @@ -0,0 +1,40 @@ +using Elsa.Extensions; +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using Microsoft.Extensions.Logging; +using System; +using System.IO; +using System.Threading.Tasks; + +namespace LINGYUN.Abp.ElsaNext.BlobStoring.Activities; + +[Activity("Elsa", "BlobStoring", "Open a blob.", Kind = ActivityKind.Task)] +public class ReadBlob : BlobActivity +{ + [Port] public IActivity? Error { get; set; } + + protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) + { + var cancellationToken = context.CancellationToken; + var path = Path.Get(context); + var blobContainer = GetBlobContainer(context); + + var logger = context.GetRequiredService>(); + try + { + var data = await blobContainer.GetAsync(path, cancellationToken); + + Result.Set(context, data); + await context.CompleteActivityAsync(); + } + catch (Exception e) + { + logger.LogWarning(e, "Error while open a blob."); + context.AddExecutionLogEntry("Error", e.Message, payload: new + { + e.StackTrace + }); + await context.ScheduleActivityAsync(Error, OnErrorCompletedAsync); + } + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/Activities/SaveBlob.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/Activities/SaveBlob.cs new file mode 100644 index 000000000..74f9c8eb7 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/Activities/SaveBlob.cs @@ -0,0 +1,99 @@ +using Elsa.Extensions; +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using Elsa.Workflows.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using System; +using System.Collections; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.BlobStoring; + +namespace LINGYUN.Abp.ElsaNext.BlobStoring.Activities; + +[Activity("Elsa", "BlobStoring", "Save a blob.", Kind = ActivityKind.Task)] +public class SaveBlob : BlobActivity +{ + [Input(Description = "Blob exists whether overwrite.")] + public Input Overwrite { get; set; } = null!; + + [Input(Description = "The file data to save. This can be a stream, binary data, a string, a form file or a collection of files.")] + public Input Data { get; set; } = null!; + + [Port] public IActivity? Error { get; set; } + + protected async override ValueTask ExecuteAsync(ActivityExecutionContext context) + { + var cancellationToken = context.CancellationToken; + var data = await ResolveAsStreamAsync(Data.Get(context), cancellationToken); + var overwrite = Overwrite.GetOrDefault(context); + var path = Path.Get(context); + var blobContainer = GetBlobContainer(context); + + var logger = context.GetRequiredService>(); + try + { + await blobContainer.SaveAsync(path, data, overwrite, cancellationToken); + await context.CompleteActivityAsync(); + } + catch (Exception e) + { + logger.LogWarning(e, "Error while save a blob."); + context.AddExecutionLogEntry("Error", e.Message, payload: new + { + e.StackTrace + }); + await context.ScheduleActivityAsync(Error, OnErrorCompletedAsync); + } + } + + private async Task ResolveAsStreamAsync(object data, CancellationToken cancellationToken) + { + if (data is Stream stream) + return stream; + + if (data is byte[] bytes) + return new MemoryStream(bytes); + + if (data is IFormFile formFile) + return formFile.OpenReadStream(); + + if (data is string stringData) + return new MemoryStream(Encoding.UTF8.GetBytes(stringData)); + + if (data is IEnumerable enumerable) + { + var files = enumerable.Cast().ToList(); + return files.Count == 1 ? await ResolveAsStreamAsync(files[0], cancellationToken) : await CreateZipArchiveAsync(files, cancellationToken); + } + + throw new NotSupportedException($"The provided data type is not supported: {data.GetType().Name}"); + } + + private async Task CreateZipArchiveAsync(IEnumerable files, CancellationToken cancellationToken = default) + { + var currentFileIndex = 0; + var zipStream = new MemoryStream(); + var zipArchive = new ZipArchive(zipStream, ZipArchiveMode.Create, true); + + foreach (var file in files) + { + var entryName = $"file-{currentFileIndex}.bin"; + var entry = zipArchive.CreateEntry(entryName); + var fileStream = await ResolveAsStreamAsync(file, cancellationToken); + await using var entryStream = entry.Open(); + await fileStream.CopyToAsync(entryStream, cancellationToken); + await entryStream.FlushAsync(cancellationToken); + entryStream.Close(); + currentFileIndex++; + } + + zipStream.Seek(0, SeekOrigin.Begin); + return zipStream; + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/ElsaBlobContainer.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/ElsaBlobContainer.cs new file mode 100644 index 000000000..dfa9f6c5c --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/ElsaBlobContainer.cs @@ -0,0 +1,8 @@ +using Volo.Abp.BlobStoring; + +namespace LINGYUN.Abp.ElsaNext.BlobStoring; + +[BlobContainerName("workflow")] +public class ElsaBlobContainer +{ +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/Extensions/ModuleExtensions.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/Extensions/ModuleExtensions.cs new file mode 100644 index 000000000..837c60b0b --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/Extensions/ModuleExtensions.cs @@ -0,0 +1,14 @@ +using Elsa.Features.Services; +using LINGYUN.Abp.ElsaNext.BlobStoring.Features; +using System; + +namespace LINGYUN.Abp.ElsaNext.BlobStoring.Extensions; + +public static class ModuleExtensions +{ + public static IModule UseAbpBlobStoring(this IModule configuration, Action? configure = null) + { + configuration.Configure(configure); + return configuration; + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/Features/BlobStoringFeature.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/Features/BlobStoringFeature.cs new file mode 100644 index 000000000..b14e0f3e4 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/Features/BlobStoringFeature.cs @@ -0,0 +1,17 @@ +using Elsa.Extensions; +using Elsa.Features.Abstractions; +using Elsa.Features.Services; + +namespace LINGYUN.Abp.ElsaNext.BlobStoring.Features; + +public class BlobStoringFeature : FeatureBase +{ + public BlobStoringFeature(IModule module) : base(module) + { + } + + public override void Configure() + { + Module.AddActivitiesFrom(); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/Localization/Resources/en.json b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/Localization/Resources/en.json new file mode 100644 index 000000000..988f1cc78 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/Localization/Resources/en.json @@ -0,0 +1,12 @@ +{ + "culture": "en", + "texts": { + "Path of the blob.": "Path of the blob.", + "Blob exists whether overwrite.": "Blob exists whether overwrite.", + "The file data to save. This can be a stream, binary data, a string, a form file or a collection of files.": "The file data to save. This can be a stream, binary data, a string, a form file or a collection of files.", + "BlobStoring": "Blob Storing", + "Delete Blob": "Delete a blob.", + "Read Blob.": "Read a blob.", + "Save Blob.": "Save a blob." + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/Localization/Resources/zh-Hans.json b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/Localization/Resources/zh-Hans.json new file mode 100644 index 000000000..353220eb9 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.BlobStoring/LINGYUN/Abp/ElsaNext/BlobStoring/Localization/Resources/zh-Hans.json @@ -0,0 +1,12 @@ +{ + "culture": "zh-Hans", + "texts": { + "Path of the blob.": "文件存储路径。", + "Blob exists whether overwrite.": "覆盖已有文件。", + "The file data to save. This can be a stream, binary data, a string, a form file or a collection of files.": "要保存的文件数据。可以是流、二进制数据、字符串、表单文件或一组文件。", + "BlobStoring": "对象存储", + "Delete Blob": "删除文件", + "Read Blob": "打开文件", + "Save Blob": "保存文件" + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Email/FodyWeavers.xml b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Email/FodyWeavers.xml new file mode 100644 index 000000000..5d6962159 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Email/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Email/FodyWeavers.xsd b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Email/FodyWeavers.xsd new file mode 100644 index 000000000..3f3946e28 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Email/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Email/LINGYUN.Abp.ElsaNext.Email.csproj b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Email/LINGYUN.Abp.ElsaNext.Email.csproj new file mode 100644 index 000000000..8277c0b2b --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Email/LINGYUN.Abp.ElsaNext.Email.csproj @@ -0,0 +1,29 @@ + + + + + + + net10.0 + LINGYUN.Abp.ElsaNext.Email + LINGYUN.Abp.ElsaNext.Email + false + false + false + + + + + + + + + + + + + + + + + diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Email/LINGYUN/Abp/ElsaNext/Email/AbpElsaNextEmailModule.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Email/LINGYUN/Abp/ElsaNext/Email/AbpElsaNextEmailModule.cs new file mode 100644 index 000000000..b73c13f66 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Email/LINGYUN/Abp/ElsaNext/Email/AbpElsaNextEmailModule.cs @@ -0,0 +1,38 @@ +using Elsa.Features.Services; +using LINGYUN.Abp.ElsaNext.Email.Extensions; +using LINGYUN.Abp.ElsaNext.Localization; +using Volo.Abp.Emailing; +using Volo.Abp.Localization; +using Volo.Abp.Modularity; +using Volo.Abp.VirtualFileSystem; + +namespace LINGYUN.Abp.ElsaNext.Email; + +[DependsOn( + typeof(AbpEmailingModule), + typeof(AbpElsaNextModule))] +public class AbpElsaNextEmailModule : AbpModule +{ + public override void PreConfigureServices(ServiceConfigurationContext context) + { + PreConfigure(elsa => + { + elsa.UseAbpEmail(); + }); + } + + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.FileSets.AddEmbedded(); + }); + + Configure(options => + { + options.Resources + .Get() + .AddVirtualJson("/LINGYUN/Abp/ElsaNext/Email/Localization/Resources"); + }); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Email/LINGYUN/Abp/ElsaNext/Email/Activities/SendEmail.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Email/LINGYUN/Abp/ElsaNext/Email/Activities/SendEmail.cs new file mode 100644 index 000000000..a9cc3e01e --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Email/LINGYUN/Abp/ElsaNext/Email/Activities/SendEmail.cs @@ -0,0 +1,274 @@ +using Elsa.Extensions; +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using Elsa.Workflows.Models; +using Elsa.Workflows.UIHints; +using LINGYUN.Abp.ElsaNext.Email.Contracts; +using Microsoft.Extensions.Logging; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Dynamic; +using System.IO; +using System.Net.Mail; +using System.Net.Mime; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.Emailing; +using Volo.Abp.Json; + +namespace LINGYUN.Abp.ElsaNext.Email.Activities; + +[Activity("Elsa", "Email", "Send an email message.", Kind = ActivityKind.Task)] +public class SendEmail : Activity +{ + /// + /// The sender's email address. + /// + [Input(Description = "The sender's email address.")] + public Input From { get; set; } = null!; + + /// + /// The recipients email addresses. + /// + [Input(Description = "The recipients email addresses.", UIHint = InputUIHints.MultiText)] + public Input> To { get; set; } = null!; + + /// + /// The CC recipient email addresses. + /// + [Input( + Description = "The CC recipient email addresses.", + UIHint = InputUIHints.MultiText, + Category = "More")] + public Input> Cc { get; set; } = null!; + + /// + /// The BCC recipients email addresses. + /// + [Input( + Description = "The BCC recipients email addresses.", + UIHint = InputUIHints.MultiText, + Category = "More")] + public Input> Bcc { get; set; } = null!; + + /// + /// The subject of the email message. + /// + [Input(Description = "The subject of the email message.")] + public Input Subject { get; set; } = null!; + + /// + /// The attachments to send with the email message. + /// + [Input( + Description = "The attachments to send with the email message. Can be (an array of) a fully-qualified file path, URL, stream, byte array or instances of EmailAttachment.", + UIHint = InputUIHints.MultiLine + )] + public Input Attachments { get; set; } = null!; + + /// + /// The body of the email message. + /// + [Input( + Description = "The body of the email message.", + UIHint = InputUIHints.MultiLine + )] + public Input Body { get; set; } = null!; + + /// + /// The activity to execute when an error occurs while trying to send the email. + /// + [Port] public IActivity? Error { get; set; } + + /// + protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) + { + var cancellationToken = context.CancellationToken; + var message = new MailMessage(); + var from = From.GetOrDefault(context); + + if (MailAddress.TryCreate(from, out var fromAddress)) + { + message.Sender = fromAddress; + message.From = fromAddress; + } + message.Subject = Subject.GetOrDefault(context) ?? ""; + + await AddAttachmentsAsync(context, message, cancellationToken); + + message.Body = Body.GetOrDefault(context); + + SetRecipientsEmailAddresses(message.To, GetAddresses(context, To)); + SetRecipientsEmailAddresses(message.CC, GetAddresses(context, Cc)); + SetRecipientsEmailAddresses(message.Bcc, GetAddresses(context, Bcc)); + + var emailSender = context.GetRequiredService(); + var logger = context.GetRequiredService>(); + + try + { + await emailSender.SendAsync(message); + await context.CompleteActivityAsync(); + } + catch (Exception e) + { + logger.LogWarning(e, "Error while sending email message"); + context.AddExecutionLogEntry("Error", e.Message, payload: new + { + e.StackTrace + }); + await context.ScheduleActivityAsync(Error, OnErrorCompletedAsync); + } + } + + private async ValueTask OnErrorCompletedAsync(ActivityCompletedContext context) => await context.TargetContext.CompleteActivityAsync(); + + private static ICollection GetAddresses(ActivityExecutionContext context, Input> input) + { + return input.GetOrDefault(context) ?? new List(0); + } + + private async Task AddAttachmentsAsync(ActivityExecutionContext context, MailMessage mailMessage, CancellationToken cancellationToken) + { + var attachments = Attachments.GetOrDefault(context); + + if (attachments == null || attachments is string s && string.IsNullOrWhiteSpace(s)) + { + return; + } + + var index = 0; + var attachmentObjects = InterpretAttachmentsModel(attachments); + + foreach (var attachmentObject in attachmentObjects) + { + switch (attachmentObject) + { + case Uri url: + await AttachOnlineFileAsync(context, mailMessage, url, cancellationToken); + break; + case string path when path?.Contains("://") == true: + await AttachOnlineFileAsync(context, mailMessage, new Uri(path), cancellationToken); + break; + case string path when !string.IsNullOrWhiteSpace(path): + AttachLocalFile(mailMessage, path); + break; + case byte[] bytes: + { + var fileName = $"Attachment-{++index}"; + var byteStream = new MemoryStream(bytes.Length); + await byteStream.WriteAsync(bytes, cancellationToken); + mailMessage.Attachments.Add(new Attachment(byteStream, new ContentType { Name = fileName })); + break; + } + case Stream stream: + { + var fileName = $"Attachment-{++index}"; + mailMessage.Attachments.Add(new Attachment(stream, new ContentType { Name = fileName })); + break; + } + case Attachment attachment: + { + attachment.Name ??= $"Attachment-{++index}"; + mailMessage.Attachments.Add(attachment); + break; + } + case EmailAttachment emailAttachment when emailAttachment.File != null: + { + var fileName = emailAttachment.Name ?? $"Attachment-{++index}"; + var emailAttachmentStream = new MemoryStream(emailAttachment.File.Length); + await emailAttachmentStream.WriteAsync(emailAttachment.File, cancellationToken); + mailMessage.Attachments.Add(new Attachment(emailAttachmentStream, new ContentType { Name = fileName })); + break; + } + case ExpandoObject expandoObject: + { + var dictionary = new Dictionary(expandoObject!, StringComparer.OrdinalIgnoreCase); + var fileName = dictionary.GetValue("FileName") ?? $"Attachment-{++index}"; + var contentType = dictionary.GetValue("ContentType") ?? "application/octet-stream"; + var parsedContentType = new ContentType(contentType); + var content = dictionary.GetValue("Content"); + + if (content is byte[] bytes) + { + var byteStream = new MemoryStream(bytes.Length); + await byteStream.WriteAsync(bytes, cancellationToken); + mailMessage.Attachments.Add(new Attachment(byteStream, new ContentType { Name = fileName })); + } + else if (content is Stream stream) + { + mailMessage.Attachments.Add(new Attachment(stream, new ContentType { Name = fileName })); + } + + break; + } + default: + { + var jsonSerializer = context.GetRequiredService(); + var json = jsonSerializer.Serialize(attachmentObject); + var fileName = $"Attachment-{++index}"; + + var jsonBytes = Encoding.UTF8.GetBytes(json); + var byteStream = new MemoryStream(jsonBytes.Length); + await byteStream.WriteAsync(jsonBytes, cancellationToken); + mailMessage.Attachments.Add(new Attachment(byteStream, new ContentType { Name = fileName })); + break; + } + } + } + } + + private static void AttachLocalFile(MailMessage mailMessage, string path) + { + var stream = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read); + mailMessage.Attachments.Add(new Attachment(stream, new ContentType{ Name = Path.GetFileName(path) })); + } + + private async static Task AttachOnlineFileAsync(ActivityExecutionContext context, MailMessage mailMessage, Uri url, CancellationToken cancellationToken) + { + var fileName = Path.GetFileName(url.LocalPath); + var downloader = context.GetRequiredService(); + var response = await downloader.DownloadAsync(url, cancellationToken); + var contentStream = await response.Content.ReadAsStreamAsync(cancellationToken); + var contentType = response.Content.Headers.ContentType?.MediaType ?? "application/binary"; + mailMessage.Attachments.Add(new Attachment(contentStream, new ContentType(contentType) { Name = fileName })); + } + + private static IEnumerable InterpretAttachmentsModel(object attachments) + { + if (attachments is byte[] bytes) + { + return new[] + { + bytes + }; + } + + return attachments is string text + ? new[] + { + text + } + : attachments as IEnumerable ?? new[] + { + attachments + }; + } + + private static void SetRecipientsEmailAddresses(MailAddressCollection list, IEnumerable? addresses) + { + if (addresses == null) + { + return; + } + foreach (var addresse in addresses) + { + if (MailAddress.TryCreate(addresse, out var mailAddress)) + { + list.Add(mailAddress); + } + } + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Email/LINGYUN/Abp/ElsaNext/Email/Contracts/IDownloader.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Email/LINGYUN/Abp/ElsaNext/Email/Contracts/IDownloader.cs new file mode 100644 index 000000000..89fb4fac2 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Email/LINGYUN/Abp/ElsaNext/Email/Contracts/IDownloader.cs @@ -0,0 +1,16 @@ +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace LINGYUN.Abp.ElsaNext.Email.Contracts; +/// +/// Download files from the Internet. +/// +public interface IDownloader +{ + /// + /// Download whatever is returned at the specified URL. + /// + Task DownloadAsync(Uri url, CancellationToken cancellationToken = default); +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Email/LINGYUN/Abp/ElsaNext/Email/Extensions/ModuleExtensions.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Email/LINGYUN/Abp/ElsaNext/Email/Extensions/ModuleExtensions.cs new file mode 100644 index 000000000..95a388ac5 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Email/LINGYUN/Abp/ElsaNext/Email/Extensions/ModuleExtensions.cs @@ -0,0 +1,14 @@ +using Elsa.Features.Services; +using LINGYUN.Abp.ElsaNext.Email.Features; +using System; + +namespace LINGYUN.Abp.ElsaNext.Email.Extensions; + +public static class ModuleExtensions +{ + public static IModule UseAbpEmail(this IModule configuration, Action? configure = null) + { + configuration.Configure(configure); + return configuration; + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Email/LINGYUN/Abp/ElsaNext/Email/Features/EmailFeature.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Email/LINGYUN/Abp/ElsaNext/Email/Features/EmailFeature.cs new file mode 100644 index 000000000..1c7db185c --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Email/LINGYUN/Abp/ElsaNext/Email/Features/EmailFeature.cs @@ -0,0 +1,30 @@ +using Elsa.Extensions; +using Elsa.Features.Abstractions; +using Elsa.Features.Services; +using LINGYUN.Abp.ElsaNext.Email.Contracts; +using LINGYUN.Abp.ElsaNext.Email.Services; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Net.Http; + +namespace LINGYUN.Abp.ElsaNext.Email.Features; + +public class EmailFeature : FeatureBase +{ + public Action ConfigureDownloaderHttpClient { get; set; } = (_, _) => { }; + + public EmailFeature(IModule module) : base(module) + { + } + + public override void Configure() + { + Module.AddActivitiesFrom(); + } + + public override void Apply() + { + Services + .AddHttpClient(ConfigureDownloaderHttpClient); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Email/LINGYUN/Abp/ElsaNext/Email/Localization/Resources/en.json b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Email/LINGYUN/Abp/ElsaNext/Email/Localization/Resources/en.json new file mode 100644 index 000000000..a10e83028 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Email/LINGYUN/Abp/ElsaNext/Email/Localization/Resources/en.json @@ -0,0 +1,12 @@ +{ + "culture": "en", + "texts": { + "The sender's email address.": "The sender's email address.", + "The recipients email addresses.": "The recipients email addresses.", + "The CC recipient email addresses.": "The CC recipient email addresses.", + "The BCC recipients email addresses.": "The BCC recipients email addresses.", + "The subject of the email message.": "The subject of the email message.", + "The attachments to send with the email message. Can be (an array of) a fully-qualified file path, URL, stream, byte array or instances of EmailAttachment.": "The attachments to send with the email message. Can be (an array of) a fully-qualified file path, URL, stream, byte array or instances of EmailAttachment.", + "The body of the email message.": "The body of the email message." + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Email/LINGYUN/Abp/ElsaNext/Email/Localization/Resources/zh-Hans.json b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Email/LINGYUN/Abp/ElsaNext/Email/Localization/Resources/zh-Hans.json new file mode 100644 index 000000000..d64b1471e --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Email/LINGYUN/Abp/ElsaNext/Email/Localization/Resources/zh-Hans.json @@ -0,0 +1,12 @@ +{ + "culture": "zh-Hans", + "texts": { + "The sender's email address.": "发件人的电子邮件地址。", + "The recipients email addresses.": "收件人的电子邮件地址。", + "The CC recipient email addresses.": "CC收件人电子邮件地址。", + "The BCC recipients email addresses.": "BCC收件人邮箱地址。", + "The subject of the email message.": "邮件主题。", + "The attachments to send with the email message. Can be (an array of) a fully-qualified file path, URL, stream, byte array or instances of EmailAttachment.": "随邮件发送的附件。可以是(一个数组)完全限定的文件路径、URL、流、字节数组,或 EmailAttachment 类的实例。", + "The body of the email message.": "邮件正文。" + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Email/LINGYUN/Abp/ElsaNext/Email/Services/DefaultDownloader.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Email/LINGYUN/Abp/ElsaNext/Email/Services/DefaultDownloader.cs new file mode 100644 index 000000000..9e841603c --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Email/LINGYUN/Abp/ElsaNext/Email/Services/DefaultDownloader.cs @@ -0,0 +1,28 @@ +using LINGYUN.Abp.ElsaNext.Email.Contracts; +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace LINGYUN.Abp.ElsaNext.Email.Services; + +/// +/// Download files from the Internet. +/// +public class DefaultDownloader : IDownloader +{ + private readonly HttpClient _httpClient; + + /// + /// Constructor. + /// + public DefaultDownloader(HttpClient httpClient) + { + _httpClient = httpClient; + } + + /// + /// Download whatever is returned at the specified URL. + /// + public async Task DownloadAsync(Uri url, CancellationToken cancellationToken = default) => await _httpClient.GetAsync(url, cancellationToken); +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.EntityFrameworkCore/FodyWeavers.xml b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.EntityFrameworkCore/FodyWeavers.xml new file mode 100644 index 000000000..5d6962159 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.EntityFrameworkCore/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.EntityFrameworkCore/FodyWeavers.xsd b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.EntityFrameworkCore/FodyWeavers.xsd new file mode 100644 index 000000000..3f3946e28 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.EntityFrameworkCore/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.EntityFrameworkCore/LINGYUN.Abp.ElsaNext.EntityFrameworkCore.csproj b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.EntityFrameworkCore/LINGYUN.Abp.ElsaNext.EntityFrameworkCore.csproj new file mode 100644 index 000000000..aec49e3a9 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.EntityFrameworkCore/LINGYUN.Abp.ElsaNext.EntityFrameworkCore.csproj @@ -0,0 +1,25 @@ + + + + + + + net10.0 + LINGYUN.Abp.ElsaNext.EntityFrameworkCore + LINGYUN.Abp.ElsaNext.EntityFrameworkCore + false + false + false + + + + + + + + + + + + + diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.EntityFrameworkCore/LINGYUN/Abp/ElsaNext/EntityFrameworkCore/AbpElsaNextEntityFrameworkCoreModule.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.EntityFrameworkCore/LINGYUN/Abp/ElsaNext/EntityFrameworkCore/AbpElsaNextEntityFrameworkCoreModule.cs new file mode 100644 index 000000000..01db2eb08 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.EntityFrameworkCore/LINGYUN/Abp/ElsaNext/EntityFrameworkCore/AbpElsaNextEntityFrameworkCoreModule.cs @@ -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(); + var alterationsFeature = context.Services.GetPreConfigureActions(); + var workflowManagementFeature = context.Services.GetPreConfigureActions(); + var workflowRuntimeFeature = context.Services.GetPreConfigureActions(); + + PreConfigure(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); + }); + }); + }); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Labels/FodyWeavers.xml b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Labels/FodyWeavers.xml new file mode 100644 index 000000000..5d6962159 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Labels/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Labels/FodyWeavers.xsd b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Labels/FodyWeavers.xsd new file mode 100644 index 000000000..3f3946e28 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Labels/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Labels/LINGYUN.Abp.ElsaNext.Labels.csproj b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Labels/LINGYUN.Abp.ElsaNext.Labels.csproj new file mode 100644 index 000000000..12c98db78 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Labels/LINGYUN.Abp.ElsaNext.Labels.csproj @@ -0,0 +1,24 @@ + + + + + + + net10.0 + LINGYUN.Abp.ElsaNext.Labels + LINGYUN.Abp.ElsaNext.Labels + false + false + false + + + + + + + + + + + + diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Labels/LINGYUN/Abp/ElsaNext/Labels/AbpElsaNextLabelsModule.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Labels/LINGYUN/Abp/ElsaNext/Labels/AbpElsaNextLabelsModule.cs new file mode 100644 index 000000000..1274c678f --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Labels/LINGYUN/Abp/ElsaNext/Labels/AbpElsaNextLabelsModule.cs @@ -0,0 +1,17 @@ +using Elsa.Extensions; +using Elsa.Features.Services; +using Volo.Abp.Modularity; + +namespace LINGYUN.Abp.ElsaNext.Labels; + +[DependsOn(typeof(AbpElsaNextModule))] +public class AbpElsaNextLabelsModule : AbpModule +{ + public override void PreConfigureServices(ServiceConfigurationContext context) + { + PreConfigure(elsa => + { + elsa.UseLabels(); + }); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Notifications/FodyWeavers.xml b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Notifications/FodyWeavers.xml new file mode 100644 index 000000000..5d6962159 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Notifications/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Notifications/FodyWeavers.xsd b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Notifications/FodyWeavers.xsd new file mode 100644 index 000000000..3f3946e28 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Notifications/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Notifications/LINGYUN.Abp.ElsaNext.Notifications.csproj b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Notifications/LINGYUN.Abp.ElsaNext.Notifications.csproj new file mode 100644 index 000000000..0c1e95496 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Notifications/LINGYUN.Abp.ElsaNext.Notifications.csproj @@ -0,0 +1,26 @@ + + + + + + + net10.0 + LINGYUN.Abp.ElsaNext.Notifications + LINGYUN.Abp.ElsaNext.Notifications + false + false + false + + + + + + + + + + + + + + diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Notifications/LINGYUN/Abp/ElsaNext/Notifications/AbpElsaNextNotificationsModule.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Notifications/LINGYUN/Abp/ElsaNext/Notifications/AbpElsaNextNotificationsModule.cs new file mode 100644 index 000000000..1ca4fa170 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Notifications/LINGYUN/Abp/ElsaNext/Notifications/AbpElsaNextNotificationsModule.cs @@ -0,0 +1,41 @@ +using Elsa.Features.Services; +using LINGYUN.Abp.ElsaNext.Localization; +using LINGYUN.Abp.ElsaNext.Notifications.Extensions; +using LINGYUN.Abp.Notifications; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Localization; +using Volo.Abp.Modularity; +using Volo.Abp.VirtualFileSystem; + +namespace LINGYUN.Abp.ElsaNext.Notifications; + +[DependsOn( + typeof(AbpNotificationsModule), + typeof(AbpElsaNextModule))] +public class AbpElsaNextNotificationsModule : AbpModule +{ + public override void PreConfigureServices(ServiceConfigurationContext context) + { + PreConfigure(elsa => + { + elsa.UseAbpNotifications(); + }); + } + + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.FileSets.AddEmbedded(); + }); + + Configure(options => + { + options.Resources + .Get() + .AddVirtualJson("/LINGYUN/Abp/ElsaNext/Notifications/Localization/Resources"); + }); + + context.Services.AddNotificationHandlersFrom(); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Notifications/LINGYUN/Abp/ElsaNext/Notifications/Activities/SendNotification.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Notifications/LINGYUN/Abp/ElsaNext/Notifications/Activities/SendNotification.cs new file mode 100644 index 000000000..49413a231 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Notifications/LINGYUN/Abp/ElsaNext/Notifications/Activities/SendNotification.cs @@ -0,0 +1,136 @@ +using Elsa.Extensions; +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using Elsa.Workflows.Models; +using Elsa.Workflows.UIHints; +using LINGYUN.Abp.ElsaNext.Notifications.UIHints; +using LINGYUN.Abp.ElsaNext.UIHints; +using LINGYUN.Abp.Notifications; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.MultiTenancy; +using Volo.Abp.Timing; +using INotificationSender = LINGYUN.Abp.Notifications.INotificationSender; + +namespace LINGYUN.Abp.ElsaNext.Notifications.Activities; + +[Activity("Elsa", "Notifications", "Send an notification.", Kind = ActivityKind.Task)] +public class SendNotification : CodeActivity +{ + [Input( + Description = "The name of the registered notification.", + UIHint = NotificationsUIHints.NotificationPicker)] + public Input NotificationName { get; set; } = default!; + + [Input( + Description = "The notification title.", + UIHint = InputUIHints.MultiLine + )] + public Input Title { get; set; } = default!; + + [Input( + Description = "The notification content.", + UIHint = InputUIHints.MultiLine + )] + public Input Content { get; set; } = default!; + + [Input( + Description = "The notification description.", + UIHint = InputUIHints.MultiLine + )] + public Input Description { get; set; } = default!; + + [Input( + Description = "The notification template name.", + UIHint = InputUIHints.MultiLine + )] + public Input Template { get; set; } = default!; + + [Input( + Description = "The notification additional attributes.", + UIHint = InputUIHints.Dictionary + )] + public Input> Propertites { get; set; } = default!; + + [Input( + Description = "The tenant id for sending the notification.", + UIHint = AbpElsaUIHints.TenantPicker)] + public Input TenantId { get; set; } = null!; + + [Input( + Description = "The recipients email addresses.", + UIHint = AbpElsaUIHints.UserPickerMultiple)] + public Input> To { get; set; } = null!; + + [Input( + Description = "The notifications severity.", + Options = new[] { "Success", "Info", "Warn", "Error", "Fatal" }, + DefaultValue = "Info", + UIHint = InputUIHints.DropDown)] + public Input Severity { get; set; } = null!; + + [Output(Description = "The notification id.")] + public Output NotificationId { get; set; } = null!; + + protected async override ValueTask ExecuteAsync(ActivityExecutionContext context) + { + var notificationSender = context.GetRequiredService(); + var currentTenant = context.GetRequiredService(); + var clock = context.GetRequiredService(); + + var tenantId = TenantId.GetOrDefault(context, () => currentTenant.Id); + var userIdentifiers = GetUserIdentifiers(context); + var severity = GetNotificationSeverity(context); + var propertites = Propertites.GetOrDefault(context); + var notificationName = NotificationName.Get(context); + var template = Template.GetOrDefault(context); + if (!string.IsNullOrWhiteSpace(template)) + { + var notificationTemplate = new NotificationTemplate(template, data: propertites); + var notificationTemplateId = await notificationSender.SendNofiterAsync( + notificationName, + notificationTemplate, + userIdentifiers, + tenantId, + severity); + NotificationId.Set(context, notificationTemplateId); + } + else + { + var title = Title.Get(context); + var message = Content.Get(context); + var description = Description.GetOrDefault(context) ?? ""; + + var notificationData = new NotificationData(); + notificationData.WriteStandardData(title!, message!, clock.Now, "elsa-workflow", description); + var notificationId = await notificationSender.SendNofiterAsync( + notificationName, + notificationData, + userIdentifiers, + tenantId, + severity); + NotificationId.Set(context, notificationId); + } + } + + private NotificationSeverity GetNotificationSeverity(ActivityExecutionContext context) + { + var severitySet = Severity.GetOrDefault(context); + if (!string.IsNullOrWhiteSpace(severitySet)) + { + if (Enum.TryParse(severitySet, true, out var severity)) + { + return severity; + } + } + return NotificationSeverity.Info; + } + + private IEnumerable? GetUserIdentifiers(ActivityExecutionContext context) + { + var userIdentifiers = To.GetOrDefault(context); + return userIdentifiers?.Select(to => new UserIdentifier(to, to.ToString())); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Notifications/LINGYUN/Abp/ElsaNext/Notifications/Extensions/ModuleExtensions.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Notifications/LINGYUN/Abp/ElsaNext/Notifications/Extensions/ModuleExtensions.cs new file mode 100644 index 000000000..2573ab38f --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Notifications/LINGYUN/Abp/ElsaNext/Notifications/Extensions/ModuleExtensions.cs @@ -0,0 +1,14 @@ +using Elsa.Features.Services; +using LINGYUN.Abp.ElsaNext.Notifications.Features; +using System; + +namespace LINGYUN.Abp.ElsaNext.Notifications.Extensions; + +public static class ModuleExtensions +{ + public static IModule UseAbpNotifications(this IModule configuration, Action? configure = null) + { + configuration.Configure(configure); + return configuration; + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Notifications/LINGYUN/Abp/ElsaNext/Notifications/Features/NotificationsFeature.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Notifications/LINGYUN/Abp/ElsaNext/Notifications/Features/NotificationsFeature.cs new file mode 100644 index 000000000..8d6ef3b04 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Notifications/LINGYUN/Abp/ElsaNext/Notifications/Features/NotificationsFeature.cs @@ -0,0 +1,17 @@ +using Elsa.Extensions; +using Elsa.Features.Abstractions; +using Elsa.Features.Services; + +namespace LINGYUN.Abp.ElsaNext.Notifications.Features; + +public class NotificationsFeature : FeatureBase +{ + public NotificationsFeature(IModule module) : base(module) + { + } + + public override void Configure() + { + Module.AddActivitiesFrom(); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Notifications/LINGYUN/Abp/ElsaNext/Notifications/Handlers/ActivityNotificationHandler.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Notifications/LINGYUN/Abp/ElsaNext/Notifications/Handlers/ActivityNotificationHandler.cs new file mode 100644 index 000000000..e39215967 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Notifications/LINGYUN/Abp/ElsaNext/Notifications/Handlers/ActivityNotificationHandler.cs @@ -0,0 +1,42 @@ +using Elsa.Mediator.Contracts; +using Elsa.Workflows.Notifications; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace LINGYUN.Abp.ElsaNext.Notifications.Handlers; + +public class ActivityNotificationHandler : + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler +{ + public ILogger Logger { protected get; set; } + public ActivityNotificationHandler() + { + Logger = NullLogger.Instance; + } + + public async virtual Task HandleAsync(ActivityExecuting notification, CancellationToken cancellationToken) + { + Logger.LogDebug("The activity notification: {notification} has been triggered.", notification.ToString()); + } + + public async virtual Task HandleAsync(ActivityExecuted notification, CancellationToken cancellationToken) + { + Logger.LogDebug("The activity notification: {notification} has been triggered.", notification.ToString()); + } + + public async virtual Task HandleAsync(ActivityCancelled notification, CancellationToken cancellationToken) + { + Logger.LogDebug("The activity notification: {notification} has been triggered.", notification.ToString()); + } + + public async virtual Task HandleAsync(ActivityCompleted notification, CancellationToken cancellationToken) + { + Logger.LogDebug("The activity notification: {notification} has been triggered.", notification.ToString()); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Notifications/LINGYUN/Abp/ElsaNext/Notifications/Handlers/WorkflowNotificationHandler.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Notifications/LINGYUN/Abp/ElsaNext/Notifications/Handlers/WorkflowNotificationHandler.cs new file mode 100644 index 000000000..3feaa03f5 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Notifications/LINGYUN/Abp/ElsaNext/Notifications/Handlers/WorkflowNotificationHandler.cs @@ -0,0 +1,53 @@ +using Elsa.Mediator.Contracts; +using Elsa.Workflows.Notifications; +using Elsa.Workflows.Runtime.Notifications; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using System.Threading; +using System.Threading.Tasks; + +namespace LINGYUN.Abp.ElsaNext.Notifications.Handlers; + +public class WorkflowNotificationHandler : + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler +{ + public ILogger Logger { protected get; set; } + public WorkflowNotificationHandler() + { + Logger = NullLogger.Instance; + } + public async virtual Task HandleAsync(WorkflowExecuting notification, CancellationToken cancellationToken) + { + Logger.LogDebug("The workflow notification: {notification} has been triggered.", notification.ToString()); + } + + public async virtual Task HandleAsync(WorkflowStarted notification, CancellationToken cancellationToken) + { + Logger.LogDebug("The workflow notification: {notification} has been triggered.", notification.ToString()); + } + + public async virtual Task HandleAsync(WorkflowFinished notification, CancellationToken cancellationToken) + { + Logger.LogDebug("The workflow notification: {notification} has been triggered.", notification.ToString()); + } + + public async virtual Task HandleAsync(WorkflowExecuted notification, CancellationToken cancellationToken) + { + Logger.LogDebug("The workflow notification: {notification} has been triggered.", notification.ToString()); + } + + public async virtual Task HandleAsync(WorkflowCancelling notification, CancellationToken cancellationToken) + { + Logger.LogDebug("The workflow notification: {notification} has been triggered.", notification.ToString()); + } + + public async virtual Task HandleAsync(WorkflowCancelled notification, CancellationToken cancellationToken) + { + Logger.LogDebug("The workflow notification: {notification} has been triggered.", notification.ToString()); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Notifications/LINGYUN/Abp/ElsaNext/Notifications/Localization/Resources/en.json b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Notifications/LINGYUN/Abp/ElsaNext/Notifications/Localization/Resources/en.json new file mode 100644 index 000000000..477772584 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Notifications/LINGYUN/Abp/ElsaNext/Notifications/Localization/Resources/en.json @@ -0,0 +1,18 @@ +{ + "culture": "en", + "texts": { + "Notifications": "Notifications", + "Send Notification": "Send Notification", + "Send an notification.": "Send an notification.", + "The name of the registered notification.": "The name of the registered notification.", + "The notification title.": "The notification title.", + "The notification content.": "The notification content.", + "The notification description.": "The notification description.", + "The notification template name.": "The notification template name.", + "The notification additional attributes.": "The notification additional attributes.", + "The tenant id for sending the notification.": "The tenant id for sending the notification.", + "The recipients email addresses.": "The recipients email addresses.", + "The notifications severity.": "The notifications severity.", + "The notification id.": "The notification id." + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Notifications/LINGYUN/Abp/ElsaNext/Notifications/Localization/Resources/zh-Hans.json b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Notifications/LINGYUN/Abp/ElsaNext/Notifications/Localization/Resources/zh-Hans.json new file mode 100644 index 000000000..c6717a413 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Notifications/LINGYUN/Abp/ElsaNext/Notifications/Localization/Resources/zh-Hans.json @@ -0,0 +1,18 @@ +{ + "culture": "zh-Hans", + "texts": { + "Notifications": "消息通知", + "Send Notification": "发送通知", + "Send an notification.": "发送通知。", + "The name of the registered notification.": "已注册的通知名称。", + "The notification title.": "通知标题。", + "The notification content.": "通知内容。", + "The notification description.": "通知描述。", + "The notification template name.": "通知模板,使用模板消息时标题、内容、描述内容无效。", + "The notification additional attributes.": "通知属性。", + "The tenant id for sending the notification.": "将要发送通知的租户Id。", + "The recipients email addresses.": "接收通知的用户Id集合。", + "The notifications severity.": "通知严重程度。", + "The notification id.": "通知Id。" + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Notifications/LINGYUN/Abp/ElsaNext/Notifications/UIHints/NotificationsUIHints.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Notifications/LINGYUN/Abp/ElsaNext/Notifications/UIHints/NotificationsUIHints.cs new file mode 100644 index 000000000..2d5dfa3e4 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Notifications/LINGYUN/Abp/ElsaNext/Notifications/UIHints/NotificationsUIHints.cs @@ -0,0 +1,6 @@ +namespace LINGYUN.Abp.ElsaNext.Notifications.UIHints; + +public static class NotificationsUIHints +{ + public const string NotificationPicker = "notification-picker"; +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Secrets.Blazor/AbpElsaNextSecretsBlazorModule.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Secrets.Blazor/AbpElsaNextSecretsBlazorModule.cs new file mode 100644 index 000000000..96909512a --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Secrets.Blazor/AbpElsaNextSecretsBlazorModule.cs @@ -0,0 +1,49 @@ +using Elsa.Studio.Secrets.Extensions; +using LINGYUN.Abp.ElsaNext.Localization; +using LINGYUN.Abp.ElsaNext.Secrets.Blazor.Navigation; +using LINGYUN.Abp.ElsaNext.Studio.Blazor; +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(options => + { + options.AdditionalAssemblies.Add(typeof(Elsa.Studio.Secrets.Feature).Assembly); + }); + + Configure(options => + { + options.MenuContributors.Add(new ElsaStudioSecretsMenuContributor()); + }); + + Configure(options => + { + options.FileSets.AddEmbedded("LINGYUN.Abp.ElsaNext.Secrets.Blazor"); + }); + + Configure(options => + { + options.Resources + .Get() + .AddVirtualJson("/Localization/Resources/SecretsBlazor"); + }); + + var elsaNextStudioBlazoeOptions = context.Services.ExecutePreConfiguredActions(); + + context.Services.AddSecretsModule(elsaNextStudioBlazoeOptions.BackendApiConfig); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Secrets.Blazor/FodyWeavers.xml b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Secrets.Blazor/FodyWeavers.xml new file mode 100644 index 000000000..5d6962159 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Secrets.Blazor/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Secrets.Blazor/FodyWeavers.xsd b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Secrets.Blazor/FodyWeavers.xsd new file mode 100644 index 000000000..3f3946e28 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Secrets.Blazor/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Secrets.Blazor/LINGYUN.Abp.ElsaNext.Secrets.Blazor.csproj b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Secrets.Blazor/LINGYUN.Abp.ElsaNext.Secrets.Blazor.csproj new file mode 100644 index 000000000..19bbec564 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Secrets.Blazor/LINGYUN.Abp.ElsaNext.Secrets.Blazor.csproj @@ -0,0 +1,29 @@ + + + + + + + net10.0 + LINGYUN.Abp.ElsaNext.Secrets.Blazor + LINGYUN.Abp.ElsaNext.Secrets.Blazor + false + false + false + + + + + + + + + + + + + + + + + diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Secrets.Blazor/Localization/Resources/SecretsBlazor/en.json b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Secrets.Blazor/Localization/Resources/SecretsBlazor/en.json new file mode 100644 index 000000000..6a3ff85f2 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Secrets.Blazor/Localization/Resources/SecretsBlazor/en.json @@ -0,0 +1,7 @@ +{ + "culture": "en", + "texts": { + "Menu:Secrets": "Secrets", + "Secrets": "Secrets" + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Secrets.Blazor/Localization/Resources/SecretsBlazor/zh-Hans.json b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Secrets.Blazor/Localization/Resources/SecretsBlazor/zh-Hans.json new file mode 100644 index 000000000..f2525cef3 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Secrets.Blazor/Localization/Resources/SecretsBlazor/zh-Hans.json @@ -0,0 +1,7 @@ +{ + "culture": "zh-Hans", + "texts": { + "Menu:Secrets": "机密", + "Secrets": "机密" + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Secrets.Blazor/Navigation/ElsaStudioSecretsMenuContributor.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Secrets.Blazor/Navigation/ElsaStudioSecretsMenuContributor.cs new file mode 100644 index 000000000..21b25300a --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Secrets.Blazor/Navigation/ElsaStudioSecretsMenuContributor.cs @@ -0,0 +1,28 @@ +using LINGYUN.Abp.ElsaNext.Localization; +using LINGYUN.Abp.ElsaNext.Studio.Blazor.Navigation; +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(); + + 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; + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Secrets.Blazor/_Imports.razor b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Secrets.Blazor/_Imports.razor new file mode 100644 index 000000000..6a4064616 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Secrets.Blazor/_Imports.razor @@ -0,0 +1,5 @@ +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Web +@using Elsa.Studio.Components +@using Elsa.Studio.Localization +@using MudBlazor diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Secrets/FodyWeavers.xml b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Secrets/FodyWeavers.xml new file mode 100644 index 000000000..5d6962159 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Secrets/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Secrets/FodyWeavers.xsd b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Secrets/FodyWeavers.xsd new file mode 100644 index 000000000..3f3946e28 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Secrets/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Secrets/LINGYUN.Abp.ElsaNext.Secrets.csproj b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Secrets/LINGYUN.Abp.ElsaNext.Secrets.csproj new file mode 100644 index 000000000..f04d857e5 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Secrets/LINGYUN.Abp.ElsaNext.Secrets.csproj @@ -0,0 +1,26 @@ + + + + + + + net10.0 + LINGYUN.Abp.ElsaNext.Secrets + LINGYUN.Abp.ElsaNext.Secrets + enable + false + false + false + + + + + + + + + + + + + diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Secrets/LINGYUN/Abp/ElsaNext/Secrets/AbpElsaNextSecretsModule.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Secrets/LINGYUN/Abp/ElsaNext/Secrets/AbpElsaNextSecretsModule.cs new file mode 100644 index 000000000..ee6dfebf7 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Secrets/LINGYUN/Abp/ElsaNext/Secrets/AbpElsaNextSecretsModule.cs @@ -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(elsa => + { + elsa.UseSecrets(secret => + { + secret.ConfigureOptions = options => configuration.GetSection("Secrets").Bind(options); + }); + elsa.UseSecretsJavaScript(); + }); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Server/LINGYUN.Abp.ElsaNext.Server.csproj b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Server/LINGYUN.Abp.ElsaNext.Server.csproj index 29df45ebb..4eeede693 100644 --- a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Server/LINGYUN.Abp.ElsaNext.Server.csproj +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Server/LINGYUN.Abp.ElsaNext.Server.csproj @@ -7,22 +7,35 @@ net10.0 LINGYUN.Abp.ElsaNext.Server LINGYUN.Abp.ElsaNext.Server + enable false false false - Enable - - + + + + + + + + + + + + + + + - + diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Server/LINGYUN/Abp/ElsaNext/Server/AbpElsaNextServerModule.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Server/LINGYUN/Abp/ElsaNext/Server/AbpElsaNextServerModule.cs index 409295960..b5f2f8487 100644 --- a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Server/LINGYUN/Abp/ElsaNext/Server/AbpElsaNextServerModule.cs +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Server/LINGYUN/Abp/ElsaNext/Server/AbpElsaNextServerModule.cs @@ -1,6 +1,15 @@ -using Elsa.Extensions; +using Elsa.Extensions; using Elsa.Features.Services; +using Elsa.Workflows.Api.RealTime.Hubs; +using LINGYUN.Abp.ElsaNext.Localization; +using LINGYUN.Abp.ElsaNext.Permissions; +using LINGYUN.Abp.ElsaNext.Server.Permissions; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Configuration; +using Volo.Abp.Localization; using Volo.Abp.Modularity; +using Volo.Abp.VirtualFileSystem; namespace LINGYUN.Abp.ElsaNext.Server; @@ -9,27 +18,40 @@ public class AbpElsaNextServerModule : AbpModule { public override void PreConfigureServices(ServiceConfigurationContext context) { + var configuration = context.Configuration.GetSection("Elsa"); + PreConfigure(elsa => { // see: https://v3.elsaworkflows.io/docs/installation/elsa-server - // Expose Elsa API endpoints. - elsa.UseWorkflowsApi(); - - // Setup a SignalR hub for real-time updates from the server. - elsa.UseRealTimeWorkflows(); + // Use timer activities. + elsa.UseScheduling(); - // Enable C# workflow expressions - elsa.UseCSharp(); + // Enable JavaScript workflow expressions. + elsa.UseJavaScript(); // Enable Liquid workflow expressions. elsa.UseLiquid(); + // Enable C# workflow expressions + elsa.UseCSharp(); + // Enable HTTP activities. - elsa.UseHttp(); + elsa.UseHttp(http => + { + http.ConfigureHttpOptions = options => configuration.GetSection("Http").Bind(options); + }); - // Use timer activities. - elsa.UseScheduling(); + // Expose Elsa API endpoints. + elsa.UseWorkflowsApi(); + + // Operational dashboard API + elsa.UseDashboardApi(); + + if (configuration["Server:EnableRealTimeWorkflows"] != "false") + { + elsa.UseRealTimeWorkflows(); + } // Register custom activities from the application, if any. elsa.AddActivitiesFrom(); @@ -38,4 +60,92 @@ public class AbpElsaNextServerModule : AbpModule elsa.AddWorkflowsFrom(); }); } + + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.EndpointConfigureActions.Add((context) => + { + context.Endpoints.MapHub("/elsa/hubs/workflow-instance"); + }); + }); + + Configure(options => + { + options.FileSets.AddEmbedded(); + }); + + Configure(options => + { + options.Resources + .Get() + .AddVirtualJson("/LINGYUN/Abp/ElsaNext/Server/Localization/Resources"); + }); + + Configure(options => + { + options.MapPermission( + ElsaWorkflowPermissionNames.Definitions.Default, + ElsaWorkflowPermissions.Definitions.Read); + options.MapPermission( + ElsaWorkflowPermissionNames.Definitions.Delete, + ElsaWorkflowPermissions.Definitions.Delete); + options.MapPermission( + ElsaWorkflowPermissionNames.Definitions.Execute, + ElsaWorkflowPermissions.Definitions.Execute); + options.MapPermission( + ElsaWorkflowPermissionNames.Definitions.Publish, + ElsaWorkflowPermissions.Definitions.Publish); + options.MapPermission( + ElsaWorkflowPermissionNames.Definitions.Retract, + ElsaWorkflowPermissions.Definitions.Retract); + options.MapPermission( + ElsaWorkflowPermissionNames.Definitions.Retract, + ElsaWorkflowPermissions.Definitions.Retract); + options.MapPermission( + ElsaWorkflowPermissionNames.Definitions.Update, + ElsaWorkflowPermissions.Definitions.Write); + options.MapPermission( + ElsaWorkflowPermissionNames.Definitions.Create, + ElsaWorkflowPermissions.Definitions.Write); + options.MapPermission( + ElsaWorkflowPermissionNames.Definitions.Refresh, + ElsaWorkflowPermissions.Definitions.Refresh); + options.MapPermission( + ElsaWorkflowPermissionNames.Definitions.Reload, + ElsaWorkflowPermissions.Definitions.Reload); + options.MapPermission( + ElsaWorkflowPermissionNames.Definitions.Versions.Default, + ElsaWorkflowPermissions.Definitions.Read); + options.MapPermission( + ElsaWorkflowPermissionNames.Definitions.Versions.Delete, + ElsaWorkflowPermissions.Definitions.Delete); + options.MapPermission( + ElsaWorkflowPermissionNames.Definitions.Versions.Revert, + ElsaWorkflowPermissions.Definitions.Publish); + + options.MapPermission( + ElsaWorkflowPermissionNames.Instances.Default, + ElsaWorkflowPermissions.Instances.Read); + options.MapPermission( + ElsaWorkflowPermissionNames.Instances.Delete, + ElsaWorkflowPermissions.Instances.Delete); + options.MapPermission( + ElsaWorkflowPermissionNames.Instances.Create, + ElsaWorkflowPermissions.Instances.Write); + options.MapPermission( + ElsaWorkflowPermissionNames.Instances.Update, + ElsaWorkflowPermissions.Instances.Write); + options.MapPermission( + ElsaWorkflowPermissionNames.Instances.Cancel, + ElsaWorkflowPermissions.Instances.Cancel); + options.MapPermission( + ElsaWorkflowPermissionNames.Instances.Variables.Default, + ElsaWorkflowPermissions.Instances.Read); + options.MapPermission( + ElsaWorkflowPermissionNames.Instances.Variables.Update, + ElsaWorkflowPermissions.Instances.Write); + }); + } } diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Server/LINGYUN/Abp/ElsaNext/Server/AbpElsaPermissionEvaluator.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Server/LINGYUN/Abp/ElsaNext/Server/AbpElsaPermissionEvaluator.cs new file mode 100644 index 000000000..04a8442eb --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Server/LINGYUN/Abp/ElsaNext/Server/AbpElsaPermissionEvaluator.cs @@ -0,0 +1,10 @@ +//using System; +//using System.Collections.Generic; +//using System.Text; + +//namespace LINGYUN.Abp.ElsaNext.Server; + +// TODO: 在IPermissionEvaluator接口可用时启用 +//public class AbpElsaPermissionEvaluator : IPermissionEvaluator +//{ +//} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Server/LINGYUN/Abp/ElsaNext/Server/Localization/Resources/en.json b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Server/LINGYUN/Abp/ElsaNext/Server/Localization/Resources/en.json new file mode 100644 index 000000000..14d6ec5b7 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Server/LINGYUN/Abp/ElsaNext/Server/Localization/Resources/en.json @@ -0,0 +1,9 @@ +{ + "culture": "en", + "texts": { + "Permission:Definitions": "工作流定义", + "Permission:Instances": "工作流实例", + "Permission:Variables": "变量", + "Permission:Versions": "版本" + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Server/LINGYUN/Abp/ElsaNext/Server/Localization/Resources/zh-Hans.json b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Server/LINGYUN/Abp/ElsaNext/Server/Localization/Resources/zh-Hans.json new file mode 100644 index 000000000..d02a5fd89 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Server/LINGYUN/Abp/ElsaNext/Server/Localization/Resources/zh-Hans.json @@ -0,0 +1,9 @@ +{ + "culture": "zh-Hans", + "texts": { + "Permission:Definitions": "工作流定义", + "Permission:Instances": "工作流实例", + "Permission:Variables": "变量", + "Permission:Versions": "版本" + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Server/LINGYUN/Abp/ElsaNext/Server/Permissions/ElsaWorkflowPermissionDefinitionProvider.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Server/LINGYUN/Abp/ElsaNext/Server/Permissions/ElsaWorkflowPermissionDefinitionProvider.cs new file mode 100644 index 000000000..cbfd6b60b --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Server/LINGYUN/Abp/ElsaNext/Server/Permissions/ElsaWorkflowPermissionDefinitionProvider.cs @@ -0,0 +1,102 @@ +using LINGYUN.Abp.ElsaNext.Localization; +using LINGYUN.Abp.ElsaNext.Permissions; +using Volo.Abp.Authorization.Permissions; +using Volo.Abp.Localization; +using Volo.Abp.MultiTenancy; + +namespace LINGYUN.Abp.ElsaNext.Server.Permissions; + +public class ElsaWorkflowPermissionDefinitionProvider : PermissionDefinitionProvider +{ + public override void Define(IPermissionDefinitionContext context) + { + var elsaPermission = context.GetGroupOrNull(ElsaPermissionNames.GroupName); + if (elsaPermission == null) + { + return; + } + + var definitionsPermission = elsaPermission.AddPermission( + ElsaWorkflowPermissionNames.Definitions.Default, + L("Permission:Definitions"), + MultiTenancySides.Host); + definitionsPermission.AddChild( + ElsaWorkflowPermissionNames.Definitions.Create, + L("Permission:Create"), + MultiTenancySides.Host); + definitionsPermission.AddChild( + ElsaWorkflowPermissionNames.Definitions.Update, + L("Permission:Update"), + MultiTenancySides.Host); + definitionsPermission.AddChild( + ElsaWorkflowPermissionNames.Definitions.Delete, + L("Permission:Delete"), + MultiTenancySides.Host); + definitionsPermission.AddChild( + ElsaWorkflowPermissionNames.Definitions.Execute, + L("Permission:Execute"), + MultiTenancySides.Host); + definitionsPermission.AddChild( + ElsaWorkflowPermissionNames.Definitions.Publish, + L("Permission:Publish"), + MultiTenancySides.Host); + definitionsPermission.AddChild( + ElsaWorkflowPermissionNames.Definitions.Retract, + L("Permission:Retract"), + MultiTenancySides.Host); + definitionsPermission.AddChild( + ElsaWorkflowPermissionNames.Definitions.Refresh, + L("Permission:Refresh"), + MultiTenancySides.Host); + definitionsPermission.AddChild( + ElsaWorkflowPermissionNames.Definitions.Reload, + L("Permission:Reload"), + MultiTenancySides.Host); + var definitionsVesionPermission = definitionsPermission.AddChild( + ElsaWorkflowPermissionNames.Definitions.Versions.Default, + L("Permission:Versions"), + MultiTenancySides.Host); + definitionsVesionPermission.AddChild( + ElsaWorkflowPermissionNames.Definitions.Versions.Revert, + L("Permission:Revert"), + MultiTenancySides.Host); + definitionsVesionPermission.AddChild( + ElsaWorkflowPermissionNames.Definitions.Versions.Delete, + L("Permission:Delete"), + MultiTenancySides.Host); + + var instancesPermission = elsaPermission.AddPermission( + ElsaWorkflowPermissionNames.Instances.Default, + L("Permission:Instances"), + MultiTenancySides.Host); + instancesPermission.AddChild( + ElsaWorkflowPermissionNames.Instances.Create, + L("Permission:Create"), + MultiTenancySides.Host); + instancesPermission.AddChild( + ElsaWorkflowPermissionNames.Instances.Update, + L("Permission:Update"), + MultiTenancySides.Host); + instancesPermission.AddChild( + ElsaWorkflowPermissionNames.Instances.Delete, + L("Permission:Delete"), + MultiTenancySides.Host); + instancesPermission.AddChild( + ElsaWorkflowPermissionNames.Instances.Cancel, + L("Permission:Cancel"), + MultiTenancySides.Host); + var instancesVariablesPermission = instancesPermission.AddChild( + ElsaWorkflowPermissionNames.Instances.Variables.Default, + L("Permission:Variables"), + MultiTenancySides.Host); + instancesVariablesPermission.AddChild( + ElsaWorkflowPermissionNames.Instances.Variables.Update, + L("Permission:Update"), + MultiTenancySides.Host); + } + + private static LocalizableString L(string name) + { + return LocalizableString.Create(name); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Server/LINGYUN/Abp/ElsaNext/Server/Permissions/ElsaWorkflowPermissionNames.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Server/LINGYUN/Abp/ElsaNext/Server/Permissions/ElsaWorkflowPermissionNames.cs new file mode 100644 index 000000000..cd38165c1 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Server/LINGYUN/Abp/ElsaNext/Server/Permissions/ElsaWorkflowPermissionNames.cs @@ -0,0 +1,40 @@ +using LINGYUN.Abp.ElsaNext.Permissions; + +namespace LINGYUN.Abp.ElsaNext.Server.Permissions; + +public static class ElsaWorkflowPermissionNames +{ + public const string Prefix = ElsaPermissionNames.GroupName + ".Workflows"; + + public static class Definitions + { + public const string Default = Prefix + ".Definitions"; + public const string Create = Default + ".Create"; + public const string Update = Default + ".Update"; + public const string Delete = Default + ".Delete"; + public const string Execute = Default + ".Execute"; + public const string Publish = Default + ".Publish"; + public const string Retract = Default + ".Retract"; + public const string Refresh = Default + ".Refresh"; + public const string Reload = Default + ".Reload"; + public static class Versions + { + public const string Default = Definitions.Default + ".Versions"; + public const string Delete = Default + ".Delete"; + public const string Revert = Default + ".Revert"; + } + } + public static class Instances + { + public const string Default = Prefix + ".Instances"; + public const string Create = Default + ".Create"; + public const string Update = Default + ".Update"; + public const string Delete = Default + ".Delete"; + public const string Cancel = Default + ".Cancel"; + public static class Variables + { + public const string Default = Instances.Default + ".Variables"; + public const string Update = Default + ".Update"; + } + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Server/LINGYUN/Abp/ElsaNext/Server/Permissions/ElsaWorkflowPermissions.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Server/LINGYUN/Abp/ElsaNext/Server/Permissions/ElsaWorkflowPermissions.cs new file mode 100644 index 000000000..bc86d23f5 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Server/LINGYUN/Abp/ElsaNext/Server/Permissions/ElsaWorkflowPermissions.cs @@ -0,0 +1,27 @@ +using LINGYUN.Abp.ElsaNext.Permissions; + +namespace LINGYUN.Abp.ElsaNext.Server.Permissions; + +public static class ElsaWorkflowPermissions +{ + public static class Definitions + { + public const string Default = "workflow-definitions"; + public const string Read = ElsaPermissionVerbs.Read + ":" + Default; + public const string Write = ElsaPermissionVerbs.Write + ":" + Default; + public const string Delete = ElsaPermissionVerbs.Delete + ":" + Default; + public const string Execute = ElsaPermissionVerbs.Execute + ":" + Default; + public const string Publish = ElsaPermissionVerbs.Publish + ":" + Default; + public const string Retract = ElsaPermissionVerbs.Retract + ":" + Default; + public const string Refresh = ElsaPermissionVerbs.Actions + ":" + Default + ":" + ElsaPermissionVerbs.Refresh; + public const string Reload = ElsaPermissionVerbs.Reload + ":" + Default + ":" + ElsaPermissionVerbs.Reload; + } + public static class Instances + { + public const string Default = "workflow-instances"; + public const string Read = ElsaPermissionVerbs.Read + ":" + Default; + public const string Write = ElsaPermissionVerbs.Write + ":" + Default; + public const string Delete = ElsaPermissionVerbs.Delete + ":" + Default; + public const string Cancel = ElsaPermissionVerbs.Cancel + ":" + Default; + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.AI.Blazor/AbpElsaNextStudioAIBlazorModule.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.AI.Blazor/AbpElsaNextStudioAIBlazorModule.cs new file mode 100644 index 000000000..0d14d6fde --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.AI.Blazor/AbpElsaNextStudioAIBlazorModule.cs @@ -0,0 +1,49 @@ +using Elsa.Studio.AI.Extensions; +using LINGYUN.Abp.ElsaNext.AI; +using LINGYUN.Abp.ElsaNext.Localization; +using LINGYUN.Abp.ElsaNext.Studio.AI.Blazor.Navigation; +using LINGYUN.Abp.ElsaNext.Studio.Blazor; +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.AI.Blazor; + +[DependsOn( + typeof(AbpElsaNextAIModule), + typeof(AbpElsaNextStudioBlazorModule))] +public class AbpElsaNextStudioAIBlazorModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + var configuration = context.Configuration.GetSection("Elsa:Studio"); + + Configure(options => + { + options.AdditionalAssemblies.Add(typeof(AbpElsaNextStudioAIBlazorModule).Assembly); + }); + + Configure(options => + { + options.MenuContributors.Add(new ElsaStudioAIMenuContributor()); + }); + + Configure(options => + { + options.FileSets.AddEmbedded("LINGYUN.Abp.ElsaNext.Studio.AI.Blazor"); + }); + + Configure(options => + { + options.Resources + .Get() + .AddVirtualJson("/Localization/Resources/AIBlazor"); + }); + var elsaNextStudioBlazoeOptions = context.Services.ExecutePreConfiguredActions(); + + context.Services.AddWeaverModule(elsaNextStudioBlazoeOptions.BackendApiConfig); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.AI.Blazor/FodyWeavers.xml b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.AI.Blazor/FodyWeavers.xml new file mode 100644 index 000000000..5d6962159 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.AI.Blazor/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.AI.Blazor/FodyWeavers.xsd b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.AI.Blazor/FodyWeavers.xsd new file mode 100644 index 000000000..3f3946e28 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.AI.Blazor/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.AI.Blazor/LINGYUN.Abp.ElsaNext.Studio.AI.Blazor.csproj b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.AI.Blazor/LINGYUN.Abp.ElsaNext.Studio.AI.Blazor.csproj new file mode 100644 index 000000000..a9b00d7d4 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.AI.Blazor/LINGYUN.Abp.ElsaNext.Studio.AI.Blazor.csproj @@ -0,0 +1,31 @@ + + + + + + + net10.0 + enable + enable + LINGYUN.Abp.ElsaNext.Studio.AI.Blazor + LINGYUN.Abp.ElsaNext.Studio.AI.Blazor + false + false + false + + + + + + + + + + + + + + + + + diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.AI.Blazor/Localization/Resources/AIBlazor/en.json b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.AI.Blazor/Localization/Resources/AIBlazor/en.json new file mode 100644 index 000000000..a6c727672 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.AI.Blazor/Localization/Resources/AIBlazor/en.json @@ -0,0 +1,7 @@ +{ + "culture": "en", + "texts": { + "Menu:AI": "AI", + "AI": "AI" + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.AI.Blazor/Localization/Resources/AIBlazor/zh-Hans.json b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.AI.Blazor/Localization/Resources/AIBlazor/zh-Hans.json new file mode 100644 index 000000000..5b82fa625 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.AI.Blazor/Localization/Resources/AIBlazor/zh-Hans.json @@ -0,0 +1,7 @@ +{ + "culture": "zh-Hans", + "texts": { + "Menu:AI": "AI", + "AI": "AI" + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.AI.Blazor/Navigation/ElsaStudioAIMenuContributor.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.AI.Blazor/Navigation/ElsaStudioAIMenuContributor.cs new file mode 100644 index 000000000..31f6010ac --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.AI.Blazor/Navigation/ElsaStudioAIMenuContributor.cs @@ -0,0 +1,27 @@ +using LINGYUN.Abp.ElsaNext.Localization; +using LINGYUN.Abp.ElsaNext.Studio.Blazor.Navigation; +using Volo.Abp.UI.Navigation; + +namespace LINGYUN.Abp.ElsaNext.Studio.AI.Blazor.Navigation; + +public class ElsaStudioAIMenuContributor : IMenuContributor +{ + public Task ConfigureMenuAsync(MenuConfigurationContext context) + { + if (context.Menu.Name == StandardMenus.Main) + { + var menu = context.Menu; + var l = context.GetLocalizer(); + + var group = menu.GetMenuItemOrNull(ElsaStudioMenus.GroupName); + group?.AddItem(new ApplicationMenuItem( + ElsaStudioMenus.GroupName + ".AI", + l["Menu:AI"], + url: "/ai/weaver", + icon: "fa fa-openai", + order: 6)); + } + + return Task.CompletedTask; + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.AI.Blazor/Pages/Weaver.razor b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.AI.Blazor/Pages/Weaver.razor new file mode 100644 index 000000000..62ef6a33e --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.AI.Blazor/Pages/Weaver.razor @@ -0,0 +1,11 @@ +@page "/ai/weaver" +@attribute [Authorize] +@inherits StudioComponentBase +@using Elsa.Studio.AI.UI.Pages + + + @* *@ + + + + diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.AI.Blazor/_Imports.razor b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.AI.Blazor/_Imports.razor new file mode 100644 index 000000000..6a4064616 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.AI.Blazor/_Imports.razor @@ -0,0 +1,5 @@ +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Web +@using Elsa.Studio.Components +@using Elsa.Studio.Localization +@using MudBlazor diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.AI.Blazor/wwwroot/weaver.css b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.AI.Blazor/wwwroot/weaver.css new file mode 100644 index 000000000..61ffa893e --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.AI.Blazor/wwwroot/weaver.css @@ -0,0 +1,196 @@ +.weaver-page { + height: calc(100vh - 72px); + display: flex; + flex-direction: column; + gap: 12px; +} + +.weaver-heading, +.weaver-thread-header, +.weaver-panel-title, +.weaver-composer-actions, +.weaver-tool, +.weaver-activity { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.weaver-workspace { + min-height: 0; + display: grid; + grid-template-columns: minmax(0, 1fr) 360px; + gap: 12px; + flex: 1; +} + +.weaver-main, +.weaver-side, +.weaver-thread { + min-height: 0; +} + +.weaver-thread, +.weaver-panel { + border: 1px solid var(--mud-palette-lines-default); + border-radius: 8px; + background: var(--mud-palette-surface); +} + +.weaver-thread { + height: 100%; + display: grid; + grid-template-rows: auto minmax(0, 1fr) auto auto; + gap: 12px; + padding: 12px; +} + +.weaver-message-list, +.weaver-activity-list, +.weaver-tool-list { + min-height: 0; + overflow: auto; +} + +.weaver-message-list { + display: flex; + flex-direction: column; + gap: 10px; +} + +.weaver-message { + max-width: 78%; + border: 1px solid var(--mud-palette-lines-default); + border-radius: 8px; + padding: 10px 12px; + background: var(--mud-palette-background); +} + + .weaver-message.is-user { + align-self: flex-end; + background: var(--mud-palette-primary-hover); + } + + .weaver-message.is-assistant { + align-self: flex-start; + } + +.weaver-message-meta { + font-size: 0.72rem; + color: var(--mud-palette-text-secondary); + margin-bottom: 4px; +} + +.weaver-message-text { + white-space: pre-wrap; +} + +.weaver-empty { + margin: auto; + text-align: center; +} + +.weaver-composer { + border-top: 1px solid var(--mud-palette-lines-default); + padding-top: 10px; + display: grid; + gap: 8px; +} + +.weaver-attachments { + min-height: 28px; + flex-wrap: wrap; +} + +.weaver-agent-select { + width: 160px; +} + +.weaver-context-kind { + width: 190px; +} + +.weaver-reference-input { + width: 220px; +} + +.weaver-side { + display: grid; + grid-template-rows: auto minmax(160px, 1fr) minmax(120px, 1fr) auto; + gap: 12px; +} + +.weaver-panel { + padding: 12px; + overflow: auto; +} + +.weaver-capability-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; + margin-top: 10px; +} + +.weaver-capability-pill { + display: flex; + align-items: center; + gap: 6px; + border: 1px solid var(--mud-palette-lines-default); + border-radius: 6px; + padding: 6px 8px; + font-size: 0.82rem; +} + + .weaver-capability-pill.is-enabled { + color: var(--mud-palette-success); + } + + .weaver-capability-pill.is-disabled { + color: var(--mud-palette-text-secondary); + } + +.weaver-activity-list, +.weaver-tool-list { + display: grid; + gap: 8px; + margin-top: 10px; +} + +.weaver-activity, +.weaver-tool, +.weaver-proposal { + border: 1px solid var(--mud-palette-lines-default); + border-radius: 8px; + padding: 8px; +} + +@media (max-width: 960px) { + .weaver-page { + height: auto; + } + + .weaver-workspace { + grid-template-columns: 1fr; + } + + .weaver-side { + grid-template-rows: none; + } + + .weaver-message { + max-width: 100%; + } + + .weaver-composer-actions { + align-items: stretch; + flex-direction: column; + } + + .weaver-agent-select, + .weaver-context-kind, + .weaver-reference-input { + width: 100%; + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Alterations.Blazor/AbpElsaNextStudioAlterationsBlazorModule.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Alterations.Blazor/AbpElsaNextStudioAlterationsBlazorModule.cs new file mode 100644 index 000000000..f60916195 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Alterations.Blazor/AbpElsaNextStudioAlterationsBlazorModule.cs @@ -0,0 +1,47 @@ +using Elsa.Studio.Alterations.Extensions; +using LINGYUN.Abp.ElsaNext.Alterations; +using LINGYUN.Abp.ElsaNext.Localization; +using LINGYUN.Abp.ElsaNext.Studio.Alterations.Blazor.Navigation; +using LINGYUN.Abp.ElsaNext.Studio.Blazor; +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.Alterations.Blazor; + +[DependsOn( + typeof(AbpElsaNextAlterationsModule), + typeof(AbpElsaNextStudioBlazorModule))] +public class AbpElsaNextStudioAlterationsBlazorModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + var configuration = context.Configuration.GetSection("Elsa:Studio"); + + Configure(options => + { + options.AdditionalAssemblies.Add(typeof(Elsa.Studio.Alterations.Feature).Assembly); + }); + + Configure(options => + { + options.MenuContributors.Add(new ElsaStudioAlterationsMenuContributor()); + }); + + Configure(options => + { + options.FileSets.AddEmbedded("LINGYUN.Abp.ElsaNext.Studio.Alterations.Blazor"); + }); + + Configure(options => + { + options.Resources + .Get() + .AddVirtualJson("/Localization/Resources/AlterationsBlazor"); + }); + + context.Services.AddAlterationsModule(); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Alterations.Blazor/FodyWeavers.xml b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Alterations.Blazor/FodyWeavers.xml new file mode 100644 index 000000000..5d6962159 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Alterations.Blazor/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Alterations.Blazor/FodyWeavers.xsd b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Alterations.Blazor/FodyWeavers.xsd new file mode 100644 index 000000000..3f3946e28 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Alterations.Blazor/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Alterations.Blazor/LINGYUN.Abp.ElsaNext.Studio.Alterations.Blazor.csproj b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Alterations.Blazor/LINGYUN.Abp.ElsaNext.Studio.Alterations.Blazor.csproj new file mode 100644 index 000000000..2c9202ba5 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Alterations.Blazor/LINGYUN.Abp.ElsaNext.Studio.Alterations.Blazor.csproj @@ -0,0 +1,29 @@ + + + + + + + net10.0 + LINGYUN.Abp.ElsaNext.Studio.Alterations.Blazor + LINGYUN.Abp.ElsaNext.Studio.Alterations.Blazor + false + false + false + + + + + + + + + + + + + + + + + diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Alterations.Blazor/Localization/Resources/AlterationsBlazor/en.json b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Alterations.Blazor/Localization/Resources/AlterationsBlazor/en.json new file mode 100644 index 000000000..18e81f1fd --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Alterations.Blazor/Localization/Resources/AlterationsBlazor/en.json @@ -0,0 +1,7 @@ +{ + "culture": "en", + "texts": { + "Menu:Alterations": "Alterations", + "Alterations": "Alterations" + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Alterations.Blazor/Localization/Resources/AlterationsBlazor/zh-Hans.json b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Alterations.Blazor/Localization/Resources/AlterationsBlazor/zh-Hans.json new file mode 100644 index 000000000..dd101c953 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Alterations.Blazor/Localization/Resources/AlterationsBlazor/zh-Hans.json @@ -0,0 +1,7 @@ +{ + "culture": "zh-Hans", + "texts": { + "Menu:Alterations": "变更", + "Alterations": "变更" + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Alterations.Blazor/Navigation/ElsaStudioAlterationsMenuContributor.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Alterations.Blazor/Navigation/ElsaStudioAlterationsMenuContributor.cs new file mode 100644 index 000000000..7c1a77171 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Alterations.Blazor/Navigation/ElsaStudioAlterationsMenuContributor.cs @@ -0,0 +1,28 @@ +using LINGYUN.Abp.ElsaNext.Localization; +using LINGYUN.Abp.ElsaNext.Studio.Blazor.Navigation; +using System.Threading.Tasks; +using Volo.Abp.UI.Navigation; + +namespace LINGYUN.Abp.ElsaNext.Studio.Alterations.Blazor.Navigation; + +public class ElsaStudioAlterationsMenuContributor : IMenuContributor +{ + public Task ConfigureMenuAsync(MenuConfigurationContext context) + { + if (context.Menu.Name == StandardMenus.Main) + { + var menu = context.Menu; + var l = context.GetLocalizer(); + + var group = menu.GetMenuItemOrNull(ElsaStudioMenus.GroupName); + group?.AddItem(new ApplicationMenuItem( + ElsaStudioMenus.GroupName + ".Alterations", + l["Menu:Alterations"], + url: "/alterations", + icon: "fa fa-clock-rotate-left", + order: 5)); + } + + return Task.CompletedTask; + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Alterations.Blazor/_Imports.razor b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Alterations.Blazor/_Imports.razor new file mode 100644 index 000000000..6a4064616 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Alterations.Blazor/_Imports.razor @@ -0,0 +1,5 @@ +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Web +@using Elsa.Studio.Components +@using Elsa.Studio.Localization +@using MudBlazor diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/AbpElsaNextStudioBlazorModule.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/AbpElsaNextStudioBlazorModule.cs new file mode 100644 index 000000000..abc027ec9 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/AbpElsaNextStudioBlazorModule.cs @@ -0,0 +1,99 @@ +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.WorkflowContexts.Extensions; +using Elsa.Studio.Workflows.Designer.Options; +using Elsa.Studio.Workflows.Extensions; +using LINGYUN.Abp.ElsaNext.Localization; +using LINGYUN.Abp.ElsaNext.Studio.Blazor.Extensions; +using LINGYUN.Abp.ElsaNext.Studio.Blazor.Navigation; +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.AspNetCore.Mvc.UI.Bundling; +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(AbpLocalizationModule), + typeof(AbpUiNavigationModule), + typeof(AbpVirtualFileSystemModule), + typeof(AbpAspNetCoreComponentsWebThemingMudBlazorModule), + typeof(AbpAspNetCoreMvcUiBundlingAbstractionsModule))] +public class AbpElsaNextStudioBlazorModule : AbpModule +{ + public override void PreConfigureServices(ServiceConfigurationContext context) + { + var configuration = context.Configuration.GetSection("Elsa:Studio"); + + PreConfigure(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(options => + { + options.MenuContributors.Add(new ElsaStudioMenuContributor()); + }); + + Configure(options => + { + options.AdditionalAssemblies.Add(typeof(Elsa.Studio.Settings.Feature).Assembly); + options.AdditionalAssemblies.Add(typeof(Elsa.Studio.Workflows.Feature).Assembly); + }); + + Configure(options => + { + options.FileSets.AddEmbedded("LINGYUN.Abp.ElsaNext.Studio.Blazor"); + }); + + Configure(options => + { + options.Resources + .Get() + .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(); + + context.Services.AddRemoteBackend(elsaNextStudioBlazoeOptions.BackendApiConfig); + + context.Services.AddWorkflowsModule(); + context.Services.AddWorkflowContextsModule(); + context.Services.AddSettingsModule(); + context.Services.AddAbpElsaStudioModule(); + + Configure(configuration.GetSection("DesignerOptions")); + + var abpLocalizationOptions = context.Services.ExecutePreConfiguredActions(); + context.Services.AddLocalizationModule(new LocalizationConfig + { + ConfigureLocalizationOptions = options => + { + options.SupportedCultures = abpLocalizationOptions.Languages.Select(x => x.CultureName).ToArray(); + } + }); + context.Services.AddAbpElsaStudioLocalizer(); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/AbpElsaNextStudioBlazorOptions.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/AbpElsaNextStudioBlazorOptions.cs new file mode 100644 index 000000000..866ba636f --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/AbpElsaNextStudioBlazorOptions.cs @@ -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(); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/AbpElsaStudioLocalizer.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/AbpElsaStudioLocalizer.cs new file mode 100644 index 000000000..6ff0828f9 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/AbpElsaStudioLocalizer.cs @@ -0,0 +1,69 @@ +using Elsa.Studio.Localization; +using LINGYUN.Abp.ElsaNext.Localization; +using Microsoft.Extensions.Localization; + +namespace LINGYUN.Abp.ElsaNext.Studio.Blazor; + +public class AbpElsaStudioLocalizer( + IStringLocalizer _stringLocalizer, + ILocalizationProvider _provider) : ILocalizer +{ + public LocalizedString this[string? key] { + get { + if (string.IsNullOrWhiteSpace(key)) + { + return new LocalizedString(string.Empty, string.Empty, true); + } + + string? translation; + var notFound = false; + var localized = _stringLocalizer[key]; + if (localized.ResourceNotFound) + { + translation = _provider.GetTranslation(key); + if (string.IsNullOrEmpty(translation)) + { + translation = key; + notFound = true; + } + } + else + { + translation = localized.Value; + } + return new LocalizedString(key, translation, notFound); + } + } + + public LocalizedString this[string? key, params object[] arguments] { + get { + if (string.IsNullOrWhiteSpace(key)) + { + return new LocalizedString(string.Empty, string.Empty, true); + } + + string? translation; + var notFound = false; + var localized = _stringLocalizer[key, arguments]; + if (localized.ResourceNotFound) + { + translation = _provider.GetTranslation(key); + if (string.IsNullOrEmpty(translation)) + { + translation = string.Format(key, arguments); + notFound = true; + } + else + { + translation = string.Format(translation, arguments); + } + } + else + { + translation = localized.Value; + } + + return new LocalizedString(key, translation, notFound); + } + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/Components/MudTextFieldEx.razor b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/Components/MudTextFieldEx.razor new file mode 100644 index 000000000..3eccd3812 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/Components/MudTextFieldEx.razor @@ -0,0 +1,111 @@ +@namespace LINGYUN.Abp.ElsaNext.Studio.Blazor.Components +@using System.Globalization + +
+ + + @if (_visibleIcons.Length > 0) + { +
+ @foreach (var icon in _visibleIcons) + { + var definition = icon; + + + + + } +
+ } +
+ + + +@code { + private const string WrapperClassname = "abp-elsa-adornment-text-field"; + + private MudTextFieldExIcon[] _visibleIcons = []; + + [Parameter] public string? Value { get; set; } + + [Parameter] public EventCallback ValueChanged { get; set; } + + [Parameter] public string? Label { get; set; } + + [Parameter] public string? HelperText { get; set; } + + [Parameter] public string? Placeholder { get; set; } + + [Parameter] public Variant Variant { get; set; } = Variant.Outlined; + + [Parameter] public Margin Margin { get; set; } = Margin.Dense; + + [Parameter] public bool ReadOnly { get; set; } + + [Parameter] public bool Disabled { get; set; } + + [Parameter] public bool Immediate { get; set; } = true; + + [Parameter] public InputType InputType { get; set; } = InputType.Text; + + [Parameter] public IReadOnlyList? AdornmentIcons { get; set; } + + [Parameter] public EventCallback OnFieldClick { get; set; } + + [Parameter] public double AdornmentHeight { get; set; } = 40; + + [Parameter] public string? Class { get; set; } + + [Parameter] public string? Style { get; set; } + + private string Classname => string.IsNullOrWhiteSpace(Class) ? WrapperClassname : $"{WrapperClassname} {Class}"; + + private string WrapperStyle => + $"position: relative; --abp-elsa-adornment-space: {((_visibleIcons.Length * 32) + 8).ToString(CultureInfo.InvariantCulture)}px; " + + $"cursor: {(OnFieldClick.HasDelegate && !Disabled ? "pointer" : "default")}; {Style}"; + + private string IconContainerStyle => + $"position: absolute; top: 0; right: 8px; height: {AdornmentHeight.ToString(CultureInfo.InvariantCulture)}px; " + + "display: flex; align-items: center; gap: 2px; z-index: 1;"; + + protected override void OnParametersSet() + { + _visibleIcons = AdornmentIcons?.Where(x => x.Visible).ToArray() ?? []; + } + + private async Task InvokeIconAsync(MudTextFieldExIcon icon) + { + if (icon.OnClick != null) + { + await icon.OnClick(); + } + } + + private async Task HandleFieldClickAsync() + { + if (!Disabled && OnFieldClick.HasDelegate) + { + await OnFieldClick.InvokeAsync(); + } + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/Components/MudTextFieldExIcon.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/Components/MudTextFieldExIcon.cs new file mode 100644 index 000000000..44ad425c4 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/Components/MudTextFieldExIcon.cs @@ -0,0 +1,20 @@ +using MudBlazor; + +namespace LINGYUN.Abp.ElsaNext.Studio.Blazor.Components; + +public sealed class MudTextFieldExIcon +{ + public string Icon { get; init; } = string.Empty; + + public Func? OnClick { get; init; } + + public string? Tooltip { get; init; } + + public string? AriaLabel { get; init; } + + public bool Visible { get; init; } = true; + + public bool Disabled { get; init; } + + public Color Color { get; init; } = Color.Default; +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/Components/WorkflowProperties/Tabs/Properties/Sections/Properties/EditPropertyDialog.razor b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/Components/WorkflowProperties/Tabs/Properties/Sections/Properties/EditPropertyDialog.razor new file mode 100644 index 000000000..236963fb1 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/Components/WorkflowProperties/Tabs/Properties/Sections/Properties/EditPropertyDialog.razor @@ -0,0 +1,18 @@ +@inject ILocalizer Localizer +@inherits StudioComponentBase + + + + + + + + + + + + + @Localizer["Cancel"] + @Localizer["Ok"] + + \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/Components/WorkflowProperties/Tabs/Properties/Sections/Properties/EditPropertyDialog.razor.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/Components/WorkflowProperties/Tabs/Properties/Sections/Properties/EditPropertyDialog.razor.cs new file mode 100644 index 000000000..47585d0c1 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/Components/WorkflowProperties/Tabs/Properties/Sections/Properties/EditPropertyDialog.razor.cs @@ -0,0 +1,80 @@ +using Blazored.FluentValidation; +using Elsa.Api.Client.Resources.WorkflowDefinitions.Models; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Forms; +using MudBlazor; + +namespace LINGYUN.Abp.ElsaNext.Studio.Blazor.Components.WorkflowProperties.Tabs.Properties.Sections.Properties; + +public partial class EditPropertyDialog +{ + private bool _isEdit = false; + private readonly PropertyModel _model = new(); + private EditContext _editContext = null!; + private PropertyModelValidator _validator = null!; + private FluentValidationValidator _fluentValidationValidator = null!; + + [Parameter] public WorkflowDefinition WorkflowDefinition { get; set; } = null!; + [Parameter] public PropertyModel? Property { get; set; } + [CascadingParameter] private IMudDialogInstance MudDialog { get; set; } = null!; + + protected override async Task OnParametersSetAsync() + { + _editContext = new(_model); + _validator = new(WorkflowDefinition, Localizer); + + if (Property == null) + { + _model.Key = GetNewPropertyKey(WorkflowDefinition.CustomProperties); + _isEdit = false; + } + else + { + _model.Key = Property.Key; + _model.Value = Property.Value; + _isEdit = true; + } + } + + private string GetNewPropertyKey(IDictionary existingPropertites) + { + var count = 0; + + while (true) + { + var key = $"Property{++count}"; + + if (existingPropertites.All(x => x.Key != key)) + { + return key; + } + } + } + + private Task OnCancelClicked() + { + MudDialog.Cancel(); + return Task.CompletedTask; + } + + private async Task OnSubmitClicked() + { + if (!await _fluentValidationValidator.ValidateAsync()) + { + return; + } + + await OnValidSubmit(); + } + + private Task OnValidSubmit() + { + var property = Property ?? new PropertyModel(); + + property.Key = _model.Key; + property.Value = _model.Value; + + MudDialog.Close(property); + return Task.CompletedTask; + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/Components/WorkflowProperties/Tabs/Properties/Sections/Properties/PropertyModel.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/Components/WorkflowProperties/Tabs/Properties/Sections/Properties/PropertyModel.cs new file mode 100644 index 000000000..5f69cccb3 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/Components/WorkflowProperties/Tabs/Properties/Sections/Properties/PropertyModel.cs @@ -0,0 +1,17 @@ +namespace LINGYUN.Abp.ElsaNext.Studio.Blazor.Components.WorkflowProperties.Tabs.Properties.Sections.Properties; + +public class PropertyModel +{ + public string Key { get; set; } = null!; + public string Value { get; set; } = null!; + public PropertyModel() + { + + } + + public PropertyModel(string key, string value) + { + Key = key; + Value = value; + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/Components/WorkflowProperties/Tabs/Properties/Sections/Properties/PropertyModelValidator.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/Components/WorkflowProperties/Tabs/Properties/Sections/Properties/PropertyModelValidator.cs new file mode 100644 index 000000000..5303e6b5c --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/Components/WorkflowProperties/Tabs/Properties/Sections/Properties/PropertyModelValidator.cs @@ -0,0 +1,21 @@ +using Elsa.Api.Client.Resources.WorkflowDefinitions.Models; +using Elsa.Studio.Localization; +using FluentValidation; + +namespace LINGYUN.Abp.ElsaNext.Studio.Blazor.Components.WorkflowProperties.Tabs.Properties.Sections.Properties; + +public class PropertyModelValidator : AbstractValidator +{ + public PropertyModelValidator(WorkflowDefinition workflowDefinition, ILocalizer localizer) + { + RuleFor(x => x.Key).NotEmpty().WithMessage(localizer["Please enter a key for the property."]); + + RuleFor(x => x.Value) + .Must((context, name, cancellationToken) => + { + var existingKey = workflowDefinition.CustomProperties.Where(x => x.Key == name).Select(x => x.Key).FirstOrDefault(); + return existingKey == null || existingKey == context.Key; + }) + .WithMessage(localizer["A property with this key already exists in the current scope."]); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/Components/WorkflowProperties/Tabs/Properties/Sections/Properties/WorkflowDefinitionPropertitesEditor.razor b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/Components/WorkflowProperties/Tabs/Properties/Sections/Properties/WorkflowDefinitionPropertitesEditor.razor new file mode 100644 index 000000000..2052f80e1 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/Components/WorkflowProperties/Tabs/Properties/Sections/Properties/WorkflowDefinitionPropertitesEditor.razor @@ -0,0 +1,52 @@ +@inject ILocalizer Localizer +@inherits StudioComponentBase + + + @Localizer["Propertites"] + + + @Localizer["Add property"] + + + @if (!Properties.Any()) + { + @Localizer["There are currently no propertites defined."] + } + else + { + + + + + @Localizer["Key"] + @Localizer["Value"] + + + + + + @foreach (var property in Properties) + { + + @property.Key + @property.Value + + + @Localizer["Edit"] + @Localizer["Delete"] + + + + } + + + + } + + + diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/Components/WorkflowProperties/Tabs/Properties/Sections/Properties/WorkflowDefinitionPropertitesEditor.razor.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/Components/WorkflowProperties/Tabs/Properties/Sections/Properties/WorkflowDefinitionPropertitesEditor.razor.cs new file mode 100644 index 000000000..17d177cc0 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/Components/WorkflowProperties/Tabs/Properties/Sections/Properties/WorkflowDefinitionPropertitesEditor.razor.cs @@ -0,0 +1,95 @@ +using Elsa.Api.Client.Resources.WorkflowDefinitions.Models; +using Elsa.Studio.Workflows.UI.Contracts; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace LINGYUN.Abp.ElsaNext.Studio.Blazor.Components.WorkflowProperties.Tabs.Properties.Sections.Properties; + +public partial class WorkflowDefinitionPropertitesEditor +{ + [Parameter] + public WorkflowDefinition WorkflowDefinition { get; set; } = default!; + + [Parameter] + public EventCallback WorkflowDefinitionUpdated { get; set; } + + [CascadingParameter] public IWorkspace? Workspace { get; set; } + + [Inject] IDialogService DialogService { get; set; } = default!; + + private bool IsReadOnly => Workspace?.IsReadOnly ?? true; + private ICollection Properties => WorkflowDefinition.CustomProperties + .Select(x => new PropertyModel(x.Key, x.Value.ToString()!)) + .ToList(); + + private async Task RaiseWorkflowDefinitionUpdatedAsync() + { + if (WorkflowDefinitionUpdated.HasDelegate) + { + await InvokeAsync(WorkflowDefinitionUpdated.InvokeAsync); + } + } + + private async Task OnEditClicked(PropertyModel property) + { + await OpenPropertyEditorDialog(property); + } + + private async Task OnDeleteClicked(PropertyModel property) + { + var result = await DialogService.ShowMessageBoxAsync(Localizer["Delete selected property?"], Localizer["Are you sure you want to delete the selected property?"], yesText: Localizer["Delete"], cancelText: Localizer["Cancel"]); + + if (result != true) + { + return; + } + + WorkflowDefinition.CustomProperties.RemoveAll(x => x.Key == property.Key); + + await RaiseWorkflowDefinitionUpdatedAsync(); + } + + private async Task OnAddPropertyClicked() + { + await OpenPropertyEditorDialog(null); + } + + private async Task OpenPropertyEditorDialog(PropertyModel? property) + { + var isNew = property == null; + + var parameters = new DialogParameters + { + [nameof(EditPropertyDialog.WorkflowDefinition)] = WorkflowDefinition + }; + + if (!isNew) + { + parameters[nameof(EditPropertyDialog.Property)] = property; + } + + var options = new DialogOptions + { + MaxWidth = MaxWidth.Medium, + FullWidth = true, + CloseButton = true, + CloseOnEscapeKey = true + }; + + var title = property == null ? Localizer["Create property"] : Localizer["Edit property"]; + var dialog = await DialogService.ShowAsync(title, parameters, options); + var result = await dialog.Result; + + if (result?.Canceled != false) + { + return; + } + + if (result.Data is PropertyModel newProperty) + { + WorkflowDefinition.CustomProperties[newProperty.Key] = newProperty.Value; + } + + await RaiseWorkflowDefinitionUpdatedAsync(); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/ElsaStudioLocalizerServiceCollectionExtensions.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/ElsaStudioLocalizerServiceCollectionExtensions.cs new file mode 100644 index 000000000..7d0b0d3be --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/ElsaStudioLocalizerServiceCollectionExtensions.cs @@ -0,0 +1,16 @@ +using Elsa.Studio.Localization; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace LINGYUN.Abp.ElsaNext.Studio.Blazor; + +public static class ElsaStudioLocalizerServiceCollectionExtensions +{ + public static IServiceCollection AddAbpElsaStudioLocalizer(this IServiceCollection services) + { + services.RemoveAll(); + services.AddScoped(); + + return services; + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/Extensions/ServiceCollectionExtensions.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 000000000..77ea56e7e --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,15 @@ +using Elsa.Studio.Contracts; +using LINGYUN.Abp.ElsaNext.Studio.Blazor.Widgets; +using Microsoft.Extensions.DependencyInjection; + +namespace LINGYUN.Abp.ElsaNext.Studio.Blazor.Extensions; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddAbpElsaStudioModule(this IServiceCollection services) + { + services.AddScoped(); + + return services; + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/FodyWeavers.xml b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/FodyWeavers.xml new file mode 100644 index 000000000..5d6962159 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/FodyWeavers.xsd b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/FodyWeavers.xsd new file mode 100644 index 000000000..3f3946e28 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/LINGYUN.Abp.ElsaNext.Studio.Blazor.csproj b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/LINGYUN.Abp.ElsaNext.Studio.Blazor.csproj new file mode 100644 index 000000000..6c0bf5a62 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/LINGYUN.Abp.ElsaNext.Studio.Blazor.csproj @@ -0,0 +1,42 @@ + + + + + + + net10.0 + enable + enable + LINGYUN.Abp.ElsaNext.Studio.Blazor + LINGYUN.Abp.ElsaNext.Studio.Blazor + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/Localization/Resources/ElsaStudioBlazor/en.json b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/Localization/Resources/ElsaStudioBlazor/en.json new file mode 100644 index 000000000..f0470dbbd --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/Localization/Resources/ElsaStudioBlazor/en.json @@ -0,0 +1,619 @@ +{ + "culture": "en", + "texts": { + "Menu:ElsaStudio": "Elsa Studio", + "Menu:WorkflowDefinitions": "Workflow Definitions", + "Menu:WorkflowInstances": "Workflow Instances", + "Menu:Settings": "Settings", + "Propertites": "Propertites", + "Key": "Key", + "Create property": "Create property", + "Edit property": "Edit property", + "Delete selected property?": "Delete selected property?", + "Are you sure you want to delete the selected property?": "Are you sure you want to delete the selected property?", + "Add property": "Add property", + "The key of the property.": "The key of the property.", + "The value of the property.": "The value of the property.", + "There are currently no propertites defined.": "There are currently no propertites defined.", + "Please enter a key for the property.": "Please enter a key for the property.", + "A property with this key already exists in the current scope.": "A property with this key already exists in the current scope.", + "Select": "Select", + "14 days": "14 days", + "180 days": "180 days", + "30 days": "30 days", + "7 days": "7 days", + "90 days": "90 days", + "A brief description of the agent.": "A brief description of the agent.", + "A brief description of the secret.": "A brief description of the secret.", + "A brief description of the workflow.": "A brief description of the workflow.", + "A brief description of this input.": "A brief description of this input.", + "A brief description of this output.": "A brief description of this output.", + "A custom activity category to assign this workflow-as-an-activity to.": "A custom activity category to assign this workflow-as-an-activity to.", + "A custom category by which to group this input.": "A custom category by which to group this input.", + "A custom category by which to group this output.": "A custom category by which to group this output.", + "A default value.": "A default value.", + "A description about the role and purpose of this agent.": "A description about the role and purpose of this agent.", + "A description about the role and purpose of this input variable.": "A description about the role and purpose of this input variable.", + "A description about the secret.": "A description about the secret.", + "A description of this activity to be displayed in the designer.": "A description of this activity to be displayed in the designer.", + "A technical name for the function": "A technical name for the function that this agent perform. For example: WriteCode", + "Acknowledge Datasource Event": "Acknowledge Datasource Event", + "Activation strategy": "Activation strategy", + "Activities": "Activities", + "Activity": "Activity", + "Activity Instance ID": "Activity Instance ID", + "Add": "Add", + "Add input": "Add input", + "Add output": "Add output", + "Add variable": "Add variable", + "Agent": "Agent", + "Agents": "Agents", + "Aggregation": "Aggregation", + "Allow this workflow to be used as an activity.": "Allow this workflow to be used as an activity.", + "API Key": "API Key", + "API Keys": "API Keys", + "Apply": "Apply", + "Auto-update consuming workflows": "Auto-update consuming workflows", + "Auto-save": "Auto-save", + "Branching": "Branching", + "Break": "Break", + "Bulk actions": "Bulk actions", + "Bulk Dispatch Workflows": "Bulk Dispatch Workflows", + "Cancel": "Cancel", + "Category": "Category", + "Check this option if the variable holds an array of the selected type.": "Check this option if the variable holds an array of the selected type.", + "Clear": "Clear", + "Click to open a new workflow tab": "Click to open a new workflow tab", + "Close": "Close", + "Close workflow": "Close workflow", + "Common": "Common", + "Complete": "Complete", + "Completed": "Completed", + "Composition": "Composition", + "Connection Strings": "Connection Strings", + "Console": "Console", + "Container": "Container", + "Correlate": "Correlate", + "Correlation ID": "Correlation ID", + "Create Agent": "Create Agent", + "Create API Key": "Create API Key", + "Create Secret": "Create Secret", + "Create Service": "Create Service", + "Created": "Created", + "CreatedAt": "CreatedAt", + "Create workflow": "Create workflow", + "Cron": "Cron", + "Dashboard": "Dashboard", + "Date": "Date", + "Date Filters": "Date Filters", + "Decision": "Decision", + "Default configuration": "Default configuration", + "Default value": "Default value", + "Definition ID": "Definition ID", + "Definition version": "Definition version", + "Definition version ID": "Definition version ID", + "Definitions": "Definitions", + "Delay": "Delay", + "Delete": "Delete", + "Description": "Description", + "Details": "Details", + "Dispatch Workflow": "Dispatch Workflow", + "Display name": "Display name", + "Display text": "Display text", + "Download File": "Download File", + "Duration": "Duration", + "Edit": "Edit", + "Email": "Email", + "End": "End", + "Enter a list of possible outcomes for this workflow.": "Enter a list of possible outcomes for this workflow.", + "Event": "Event", + "Events": "Events", + "Excel Parser": "Excel Parser", + "Execution Settings": "Execution Settings", + "Executions": "Executions", + "Expires": "Expires", + "Expires In": "Expires In", + "Export": "Export", + "Export the current workflow as a JSON file": "Export the current workflow as a JSON file.", + "Export this version as a JSON file.": "Export this version as a JSON file.", + "Fault": "Fault", + "Finish": "Finish", + "Finished": "Finished", + "FinishedAt": "FinishedAt", + "Flow": "Flow", + "For": "For", + "Fork (flow)": "Fork (flow)", + "Frequency Penalty": "Frequency Penalty", + "Function": "Function", + "Function Name": "Function Name", + "General": "General", + "Has Incidents": "Has Incidents", + "HTTP": "HTTP", + "HTTP Endpoint": "HTTP Endpoint", + "HTTP File Response": "HTTP File Response", + "HTTP Request": "HTTP Request", + "HTTP Request (flow)": "HTTP Request (flow)", + "HTTP Response": "HTTP Response", + "ID": "ID", + "The ID of the activity.": "The ID of the activity.", + "If": "If", + "Import": "Import", + "Import a JSON file containing workflow data to override the current workflow\u0027s contents.": "Import a JSON file containing workflow data to override the current workflow\u0027s contents.", + "Incident Strategy": "Incident Strategy", + "IncidentCount": "IncidentCount", + "Incidents": "Incidents", + "Info": "Info", + "Information": "Information", + "Input": "Input", + "Input Properties": "Input Properties", + "Input/output": "Input/output", + "Inputs": "Inputs", + "Instances": "Instances", + "int": "int", + "Is": "Is", + "Is After": "Is After", + "Is array": "Is array", + "Is Before": "Is Before", + "Is Not": "Is Not", + "Is on or After": "Is on or After", + "Is on or Before": "Is on or Before", + "Join": "Join", + "Journal": "Journal", + "Latest version": "Latest version", + "Loading": "Loading", + "Log Persistence Strategy": "Log Persistence Strategy", + "Looping": "Looping", + "Manage all the things": "Manage all the things", + "Max Tokens": "Max Tokens", + "Metadata": "Metadata", + "The name of the activity. Named activities can be referenced by other activities.": "The name of the activity. Named activities can be referenced by other activities.", + "Never": "Never", + "No": "No", + "No agents found": "No agents found", + "No API keys found": "No API keys found", + "No inputs": "No inputs", + "No outcomes associated with this execution.": "No outcomes associated with this execution.", + "No output associated with this execution.": "No output associated with this execution.", + "No outputs": "No outputs", + "No secrets found": "No secrets found", + "No services found": "No services found", + "No state associated with this execution.": "No state associated with this execution.", + "No variables": "No variables", + "No workflow definitions found": "No workflow definitions found", + "No workflow instances found": "No workflow instances found", + "object": "object", + "Ok": "Ok", + "Open File": "Open File", + "OpenAIChatCompletion": "OpenAIChatCompletion", + "OpenAITextToImage": "OpenAITextToImage", + "Outcomes": "Outcomes", + "Output": "Output", + "Output Properties": "Output Properties", + "Outputs": "Outputs", + "Parser": "Parser", + "Log Persistence": "Log Persistence", + "Plugins": "Plugins", + "Presence Penalty": "Presence Penalty", + "Primitives": "Primitives", + "Prompt Template": "Prompt Template", + "Properties": "Properties", + "Publish": "Publish", + "Publish Event": "Publish Event", + "Published": "Published", + "Published version": "Published version", + "Publish the current workflow": "Publish the current workflow", + "Read Data from Configuration / Runtime": "Read Data from Configuration / Runtime", + "Read Data from Database": "Read Data from Database", + "Rollback to this version": "Rollback to this version", + "Rows Per Page": "Rows Per Page", + "Run": "Run", + "Run Asynchronously": "Run Asynchronously", + "Run JavaScript": "Run JavaScript", + "Run Python": "Run Python", + "Run Task": "Run Task", + "Run workflow": "Run workflow", + "Save": "Save", + "Save File": "Save File", + "Scheduling": "Scheduling", + "Scope": "Scope", + "Scripting": "Scripting", + "Search": "Search...", + "Search on name, id, definition id, definition version id, correlation id": "Search on name, id, definition id, definition version id, correlation id", + "Search on name, description, id, definition id": "Search on name, description, id, definition id", + "Secret": "Secret", + "Secrets": "Secrets", + "Send Email": "Send Email", + "Services": "Services", + "Set Name": "Set Name", + "Set Output": "Set Output", + "Set Variable": "Set Variable", + "Set Workflow Context Parameter": "Set Workflow Context Parameter", + "Settings": "Settings", + "Show accumulated time": "Show accumulated time", + "Show events scoped to current container": "Show events scoped to current container", + "Show incidents": "Show incidents", + "Show relative time": "Show relative time", + "Showing all events": "Showing all events", + "Showing incidents": "Showing incidents", + "Start": "Start", + "Start at": "Start at", + "Start of workflow": "Start of workflow", + "Started": "Started", + "State": "State", + "Status": "Status", + "Storage": "Storage", + "string": "string", + "Sub status": "Sub status", + "Sub-Workflow": "Sub-Workflow", + "Sub-Workflow Inputs": "Sub-Workflow Inputs", + "Sub-Workflow Outputs": "Sub-Workflow Outputs", + "SubStatus": "SubStatus", + "Switch": "Switch", + "Switch (flow)": "Switch (flow)", + "Task": "Task", + "Temperature": "Temperature", + "The .NET type of the input variable.": "The .NET type of the input variable.", + "The .NET type of the output variable.": "The .NET type of the output variable.", + "The description of the output variable.": "The description of the output variable.", + "The lifetime of this secret": "The lifetime of this secret", + "The maximum number": "The maximum number of tokens to generate. Leave empty for maximum.", + "The name of the agent.": "The name of the agent.", + "The name of the function that the agent will execute.": "The name of the function that the agent will execute.", + "The name of the new agent.": "The name of the new agent.", + "The name of the new secret.": "The name of the new secret.", + "The name of the new workflow.": "The name of the new workflow.", + "The name of the variable.": "The name of the variable.", + "The penalty for repeating": "The penalty for repeating any one token. A higher presence penalty will reduce the likelihood of token repetition, fostering more varied text generation.", + "The penalty for repeating any one token": "The penalty for repeating any one token. A higher frequency penalty will reduce the likelihood of token repetition, fostering more varied text generation.", + "The plugins that the agent can use.": "The plugins that the agent can use.", + "The scope of the secret helps the UI to filter available options from dropdown lists.": "The scope of the secret helps the UI to filter available options from dropdown lists.", + "The secret value.": "The secret value.", + "The services that the agent requires.": "The services that the agent requires.", + "The settings of the service in JSON format.": "The settings of the service in JSON format.", + "The system prompt template that the agent will use.": "The system prompt template that the agent will use.", + "The technical name of the API key.": "The technical name of the API key.", + "The technical name of the input variable": "The technical name of the input variable. When referencing this variable in the prompt template, use this name.", + "The technical name of the input.": "The technical name of the input.", + "The technical name of the service.": "The technical name of the service.", + "The temperature of the model": "The temperature of the model. Higher values mean the model will take more risks. Try 0.1 for results that are very close to what the model was trained on. Try 0.9 for very creative results.", + "The type of the service.": "The type of the service.", + "The user-friendly name of this input.": "The user-friendly name of this input.", + "The user-friendly name of this output.": "The user-friendly name of this output.", + "The value of the API key.": "The value of the API key.", + "There are currently no inputs defined.": "There are currently no inputs defined.", + "There are currently no outputs defined.": "There are currently no outputs defined.", + "There are currently no variables defined.": "There are currently no variables defined.", + "This activity does not have any common properties.": "This activity does not have any common properties.", + "This activity does not have any input properties.": "This activity does not have any input properties.", + "This activity does not have any output properties.": "This activity does not have any output properties.", + "This input is an array": "This input is an array", + "This output is an array": "This output is an array", + "This variable is an array": "This variable is an array", + "Time": "Time", + "Timer": "Timer", + "Top Probability determines": "Top Probability determines the cumulative probability threshold for token selection during text generation. Lower values will result in less diversity, making the model pick from the most probable tokens only. Higher values will allow for more diverse outputs, as the model will consider a wider range of tokens.", + "TopP": "TopP", + "Trigger workflow": "Trigger workflow", + "Type": "Type", + "UI Hint": "UI Hint", + "Unable to make changes as the workflow or application is currently in read-only mode": "Unable to make changes as the workflow or application is currently in read-only mode", + "Unpublish": "Unpublish", + "Unpublish the current workflow": "Unpublish the current workflow.", + "Updated": "Updated", + "UpdatedAt": "UpdatedAt", + "Usable as activity": "Usable as activity", + "Use JSON Response": "Use JSON Response", + "Use this version": "Use this version", + "Used": "Used", + "Used to define the initial instructions": "Used to define the initial instructions or context that the Semantic Kernel agent will use to generate responses. This template guides the agent\u0027s behavior and helps it understand the role it needs to play or the tasks it needs to perform.", + "Value": "Value", + "Variables": "Variables", + "Version": "Version", + "Version history": "Version history", + "View": "View", + "Webhooks": "Webhooks", + "When checked, the workflow will be executed asynchronously.": "When checked, the workflow will be executed asynchronously.", + "When you publish": "When you publish a new version, all of the consuming workflows will be updated to point to the new version of this workflow.", + "Whether to show the description in the designer.": "Whether to show the description in the designer.", + "Workflow Context": "Workflow Context", + "Workflow Context Settings": "Workflow Context Settings", + "Workflow Contexts": "Workflow Contexts", + "Workflow definitions Editor": "Workflow definitions Editor", + "Workflow instances": "Workflow instances", + "The name of the workflow.": "The name of the workflow.", + "Workflows": "Workflows", + "Write Line": "Write Line", + "Yes": "Yes", + "You are running in read-only mode. Workflow operations are disabled": "You are running in read-only mode. Workflow operations are disabled", + "You can find documentation and examples here": "You can find documentation and examples here", + "Incident handling strategy": "Incident handling strategy", + "A checkbox": "A checkbox", + "A code editor": "A code editor", + "A dropdown list": "A dropdown list", + "A JSON editor": "A JSON editor", + "A list of checkboxes": "A list of checkboxes", + "A list of radio buttons": "A list of radio buttons", + "A single line of text input": "A single line of text input", + "A variable picker": "A variable picker", + "A variable with this name already exists in the current scope.": "A variable with this name already exists in the current scope.", + "A workflow definition picker": "A workflow definition picker", + "A workflow output definition picker": "A workflow output definition picker", + "A workflow with this name already exists.": "A workflow with this name already exists.", + "An input for multiple words, like a tagging input": "An input for multiple words, like a tagging input", + "An input with this name already exists.": "An input with this name already exists.", + "An outcome picker": "An outcome picker", + "Are you sure you want to cancel all running workflow instances of this workflow definition?": "Are you sure you want to cancel all running workflow instances of this workflow definition?", + "Are you sure you want to cancel the selected workflow instances?": "Are you sure you want to cancel the selected workflow instances?", + "Are you sure you want to cancel this workflow instance?": "Are you sure you want to cancel this workflow instance?", + "Are you sure you want to delete the selected input?": "Are you sure you want to delete the selected input?", + "Are you sure you want to delete the selected output?": "Are you sure you want to delete the selected output?", + "Are you sure you want to delete the selected variable?": "Are you sure you want to delete the selected variable?", + "Are you sure you want to delete the selected versions?": "Are you sure you want to delete the selected versions?", + "Are you sure you want to delete the selected workflow instances?": "Are you sure you want to delete the selected workflow instances?", + "Are you sure you want to delete the selected workflows?": "Are you sure you want to delete the selected workflows?", + "Are you sure you want to delete this version?": "Are you sure you want to delete this version?", + "Are you sure you want to delete this workflow instance?": "Are you sure you want to delete this workflow instance?", + "Are you sure you want to delete this workflow?": "Are you sure you want to delete this workflow?", + "Are you sure you want to publish the selected workflows?": "Are you sure you want to publish the selected workflows?", + "Are you sure you want to unpublish the selected workflows?": "Are you sure you want to unpublish the selected workflows?", + "Cancel running workflow instances?": "Cancel running workflow instances?", + "Cancel selected workflow instances?": "Cancel selected workflow instances?", + "Cancel workflow instance?": "Cancel workflow instance?", + "Checkbox": "Checkbox", + "Checklist": "Checklist", + "Code editor": "Code editor", + "{0} consuming workflow(s) updated": "{0} consuming workflow(s) updated", + "Create input": "Create input", + "Create output": "Create output", + "Create variable": "Create variable", + "Default": "Default", + "Default Strategy": "Default Strategy", + "Delete selected input?": "Delete selected input?", + "Delete selected output?": "Delete selected output?", + "Delete selected variable?": "Delete selected variable?", + "Delete selected versions": "Delete selected versions", + "Delete selected workflow instances?": "Delete selected workflow instances?", + "Delete selected workflows?": "Delete selected workflows?", + "Delete version": "Delete version", + "Delete workflow instance?": "Delete workflow instance?", + "Delete workflow?": "Delete workflow?", + "Dropdown": "Dropdown", + "Edit output": "Edit output", + "Edit variable": "Edit variable", + "Edit Workflow Definition": "Edit Workflow Definition", + "Enter the expression that will be used to determine the log persistence strategy for this property.": "Enter the expression that will be used to determine the log persistence strategy for this property.", + "Failed to import {0} workflow definitions. Errors: {1}": "Failed to import {0} workflow definitions. Errors: {1}", + "imported successfully.": "imported successfully.", + "Inherit": "Inherit", + "JSON editor": "JSON editor", + "Message": "Message", + "Multi line": "Multi line", + "Multi text": "Multi text", + "Multiple lines of text input": "Multiple lines of text input", + "New workflow": "New workflow", + "No workflows found to import.": "No workflows found to import.", + "No workflows were imported.": "No workflows were imported.", + "None": "None", + "Not executed": "Not executed", + "Not usable as activity.": "Not usable as activity.", + "One workflow consuming a published workflow has been updated": "One workflow consuming a published workflow has been updated", + "One workflow is already published": "One workflow is already published", + "One workflow is already unpublished": "One workflow is already unpublished", + "One workflow is not found": "One workflow is not found", + "One workflow is published": "One workflow is published", + "One workflow is unpublished": "One workflow is unpublished", + "Outcome picker": "Outcome picker", + "Output picker": "Output picker", + "Please enter a name for the input.": "Please enter a name for the input.", + "Please enter a name for the output.": "Please enter a name for the output.", + "Please enter a name for the variable.": "Please enter a name for the variable.", + "Please enter a name for the workflow.": "Please enter a name for the workflow.", + "Publish selected workflows?": "Publish selected workflows?", + "Radio list": "Radio list", + "Readonly": "Readonly", + "Show description": "Show description", + "Single line": "Single line", + "Successfully imported": "Successfully imported", + "Successfully imported 1 workflow definition.": "Successfully imported 1 workflow definition.", + "Successfully imported one instance": "Successfully imported one instance", + "Successfully started workflow": "Successfully started workflow", + "The commit strategy to use that controls when the workflow state should be committed.": "The commit strategy to use that controls when the workflow state should be committed.", + "The ID of the variable.": "The ID of the variable.", + "The read-only workflows will not be affected.": "The read-only workflows will not be affected.", + "The text to display in the designer.": "The text to display in the designer.", + "The workflow cannot be started": "The workflow cannot be started", + "Timestamp": "Timestamp", + "Unpublish selected workflows?": "Unpublish selected workflows?", + "Upload JSON and/or ZIP files containing workflow data to import.": "Upload JSON and/or ZIP files containing workflow data to import.", + "Variable picker": "Variable picker", + "When checked, this activity can trigger this workflow.": "When checked, this activity can trigger this workflow.", + "When checked, this activity is the start of this workflow.": "When checked, this activity is the start of this workflow.", + "Workflow definition picker": "Workflow definition picker", + "workflow definitions.": "workflow definitions.", + "Workflow published": "Workflow published", + "Workflow published with Not Found activities": "Workflow published with Not Found activities", + "Workflow retracted": "Workflow retracted", + "Workflow saved": "Workflow saved", + "Workflow unpublished": "Workflow unpublished", + "Workflow was already published": "Workflow was already published", + "workflows are already published": "workflows are already published", + "workflows are already unpublished": "workflows are already unpublished", + "workflows are not found": "workflows are not found", + "workflows are published": "workflows are published", + "workflows are unpublished": "workflows are unpublished", + "workflows consuming published workflows have been updated": "workflows consuming published workflows have been updated", + "Delete version {0}": "Delete version {0}", + "Successfully imported {0} instances": "Successfully imported {0} instances", + "Successfully imported {0} workflow definitions.": "Successfully imported {0} workflow definitions.", + "{0} workflows are already published": "{0} workflows are already published", + "{0} workflows are already unpublished": "{0} workflows are already unpublished", + "{0} workflows are not found": "{0} workflows are not found", + "{0} workflows are published": "{0} workflows are published", + "{0} workflows are unpublished": "{0} workflows are unpublished", + "{0} workflows consuming published workflows have been updated": "{0} workflows consuming published workflows have been updated", + "{0} {1} imported successfully.": "{0} {1} imported successfully.", + "Failed to import {0} {1}.": "Failed to import {0} {1}.", + "Auto layout": "Auto layout", + "Center": "Center", + "Zoom to fit the screen": "Zoom to fit the screen", + "A function to compute the correlation ID to associate a dispatched workflow with.": "A function to compute the correlation ID to associate a dispatched workflow with.", + "A list of expected outcomes to handle.": "A list of expected outcomes to handle.", + "A list of expected status codes to handle.": "A list of expected status codes to handle.", + "A value that allows to add the Authorization header without validation.": "A value that allows to add the Authorization header without validation.", + "Always allow the creation": "Always allow the creation", + "Branches": "Branches", + "Break out of a loop.": "Break out of a loop.", + "Cancelled": "Cancelled", + "Condition": "Condition", + "Content Type": "Content Type", + "Correlation ID Function": "Correlation ID Function", + "Default Item Input Key": "Default Item Input Key", + "Disable Authorization Header Validation": "Disable Authorization Header Validation", + "Executes javaScript code": "Executes javaScript code", + "Executing": "Executing", + "Expected Status Codes": "Expected Status Codes", + "Method": "Method", + "Mode": "Mode", + "Request Content.": "Request Content.", + "Result": "Result", + "Running": "Running", + "Supported Methods": "Supported Methods", + "The Authorization header value to send with the request. For example: Bearer {some-access-token}": "The Authorization header value to send with the request. For example: Bearer {some-access-token}", + "The content to send with the request. Can be a string, an object, a byte array or a stream.": "The content to send with the request. Can be a string, an object, a byte array or a stream.", + "The content type to use when sending the request.": "The content type to use when sending the request.", + "The data source to use for dispatching the workflows.": "The data source to use for dispatching the workflows.", + "The default key to use for the input name when sending the current item to the dispatched workflow. Will not be used if the Items field contain a list of dictionaries": "The default key to use for the input name when sending the current item to the dispatched workflow. Will not be used if the Items field contain a list of dictionaries", + "The definition ID of the workflows to dispatch.": "The definition ID of the workflows to dispatch.", + "The HTTP method to use when sending the request.": "The HTTP method to use when sending the request.", + "The HTTP methods to accept.": "The HTTP methods to accept.", + "Allow authenticated requests only.": "Allow authenticated requests only.", + "Provide a policy to evaluate. If the policy fails, the request is forbidden.": "Provide a policy to evaluate. If the policy fails, the request is forbidden.", + "The maximum time allowed to process the request.": "The maximum time allowed to process the request.", + "The maximum request size allowed in bytes.": "The maximum request size allowed in bytes.", + "The maximum file size allowed in bytes for an individual file.": "The maximum file size allowed in bytes for an individual file.", + "Only file extensions in this list are allowed. Leave empty to allow all extensions": "Only file extensions in this list are allowed. Leave empty to allow all extensions.", + "File extensions in this list are forbidden. Leave empty to not block any extension.": "File extensions in this list are forbidden. Leave empty to not block any extension.", + "Only MIME types in this list are allowed. Leave empty to allow all types": "Only MIME types in this list are allowed. Leave empty to allow all types.", + "A value indicating whether to expose the \"Request too large\" outcome.": "A value indicating whether to expose the \"Request too large\" outcome.", + "A value indicating whether to expose the \"File too large\" outcome.": "A value indicating whether to expose the \"File too large\" outcome.", + "A value indicating whether to expose the \"Invalid file extension\" outcome.": "A value indicating whether to expose the \"Invalid file extension\" outcome.", + "A value indicating whether to expose the \"Invalid file MIME type\" outcome.": "A value indicating whether to expose the \"Invalid file MIME type\" outcome.", + "The parsed request content, if any.": "The parsed request content, if any.", + "The uploaded files, if any.": "The uploaded files, if any.", + "The first uploaded file, if any.": "The first uploaded file, if any.", + "The parsed route data, if any.": "The parsed route data, if any.", + "The querystring data, if any.": "The querystring data, if any.", + "The headers, if any.": "The headers, if any.", + "The join mode determines whether this activity should continue as soon as one inbound path comes in (Wait Any), or once all inbound paths have executed (Wait All).": "The join mode determines whether this activity should continue as soon as one inbound path comes in (Wait Any), or once all inbound paths have executed (Wait All).", + "The outcome or set of outcomes to complete this activity with.": "The outcome or set of outcomes to complete this activity with.", + "The path to associate with the workflow.": "The path to associate with the workflow.", + "The switch mode determines whether the first match should be scheduled, or all matches.": "The switch mode determines whether the first match should be scheduled, or all matches.", + "The URL to download the file from.": "The URL to download the file from.", + "Time Duration": "Time Duration", + "Trigger execution at a specific time in the future.": "Trigger execution at a specific time in the future.", + "URL": "URL", + "Validation Result": "Validation Result", + "Workflow Definition": "Workflow Definition", + "Add case": "Add case", + "Data": "Data", + "Label": "Label", + "String7": "Result", + "Zoom to fit": "Zoom to fit", + "Always allow the creation of a new workflow instance.": "Always allow the creation of a new workflow instance.", + "Assign the current value to the specified variable.": "Assign the current value to the specified variable.", + "Code": "Code", + "Controls whether the end step is upper/lowerbound inclusive or exclusive. True (inclusive) by default.": "Controls whether the end step is upper/lowerbound inclusive or exclusive. True (inclusive) by default.", + "Current Value": "Current Value", + "Interval": "Interval", + "Outer Bound Inclusive": "Outer Bound Inclusive", + "Payload": "Payload", + "Step": "Step", + "The end step.": "The end step.", + "The items to iterate through.": "The items to iterate through.", + "The set of values to iterate.": "The set of values to iterate.", + "The start step.": "The start step.", + "The step size. To count down, enter a negative number.": "The step size. To count down, enter a negative number.", + "Variable": "Variable", + "Provider Type": "Provider Type", + "Configuration": "Configuration", + "Exclude": "Exclude", + "Include": "Include", + "Load": "Load", + "Pending": "Pending", + "min ago": "min ago", + "Loaded": "Loaded", + "Operational health": "Operational health", + "Available": "Available", + "Prioritized findings": "Prioritized findings", + "Unavailable": "Unavailable", + "h ago": "h ago", + "No hotspots in this range.": "No hotspots in this range.", + "Faults": "Faults", + "Dashboard unavailable": "Dashboard unavailable", + "Last hour": "Last hour", + "Last 24 hours": "Last 24 hours", + "Current suspended instances": "Current suspended instances", + "Dashboard data is unavailable from the selected backend.": "Dashboard data is unavailable from the selected backend.", + "Execution trend": "Execution trend", + "items": "items", + "Accepting work": "Accepting work", + "Avg duration": "Avg duration", + "Refreshed": "Refreshed", + "Suspended": "Suspended", + "Backend": "Backend", + "Draining": "Draining", + "Last 7 days": "Last 7 days", + "Selected backend": "Selected backend", + "Not installed": "Not installed", + "Paused": "Paused", + "Needs attention": "Needs attention", + "Refresh failed": "Refresh failed", + "Not refreshed yet": "Not refreshed yet", + "No workflow activity in this range.": "No workflow activity in this range.", + "No access": "No access", + "Current active instances": "Current active instances", + "Workflow hotspots": "Workflow hotspots", + "Interrupted": "Interrupted", + "Actions": "Actions", + "Search on Id, Definition Id, Definition Version Id, Correlation Id": "Search on Id, Definition Id, Definition Version Id, Correlation Id", + "Refresh dashboard": "Refresh dashboard", + "Search on Id, Name, Description, Definition Id": "Search on Id, Name, Description, Definition Id", + "Backend disconnected": "Backend disconnected", + "No workflow instances changed in this range.": "No workflow instances changed in this range.", + "Workflow": "Workflow", + "d ago": "d ago", + "item": "item", + "Open workflow instance": "Open workflow instance", + "Just now": "Just now", + "Workflow / instance": "Workflow / instance", + "Name": "Name", + "No active findings in the selected range.": "No active findings in the selected range.", + "Faulted": "Faulted", + "Recent workflow activity": "Recent workflow activity", + "Logs": "Logs", + "To": "To", + "discarded locally": "discarded locally", + "Auto scroll": "Auto scroll", + "Source": "Source", + "Reconnect": "Reconnect", + "Auto": "Auto", + "Copy selected": "Copy selected", + "Structured Logs": "Structured Logs", + "Wrap messages": "Wrap messages", + "The live structured log subscription is disconnected.": "The live structured log subscription is disconnected.", + "Workflow instance": "Workflow instance", + "Open details": "Open details", + "Minimum level": "Minimum level", + "All sources": "All sources", + "Compact rows": "Compact rows", + "Tenant": "Tenant", + "Compact": "Compact", + "Span ID": "Span ID", + "From": "From", + "rows": "rows", + "Text": "Text", + "Trace ID": "Trace ID", + "Copy visible": "Copy visible", + "Wrap": "Wrap" + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/Localization/Resources/ElsaStudioBlazor/zh-Hans.json b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/Localization/Resources/ElsaStudioBlazor/zh-Hans.json new file mode 100644 index 000000000..668522aea --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/Localization/Resources/ElsaStudioBlazor/zh-Hans.json @@ -0,0 +1,619 @@ +{ + "culture": "zh-Hans", + "texts": { + "Menu:ElsaStudio": "Elsa 工作室", + "Menu:WorkflowDefinitions": "工作流定义", + "Menu:WorkflowInstances": "工作流实例", + "Menu:Settings": "设置", + "Propertites": "属性", + "Key": "键", + "Create property": "创建属性", + "Edit property": "编辑属性", + "Delete selected property?": "删除所选属性?", + "Are you sure you want to delete the selected property?": "您确定要删除所选属性吗?", + "Add property": "添加属性", + "The key of the property.": "属性的键名。", + "The value of the property.": "属性的值。", + "There are currently no propertites defined.": "目前没有定义任何属性。", + "Please enter a key for the property.": "请输入属性的键名。", + "A property with this key already exists in the current scope.": "当前作用域中已存在具有此键的属性。", + "Select": "选择", + "180 days": "180 天", + "30 days": "30 天", + "7 days": "7 天", + "90 days": "90 天", + "A brief description of the workflow.": "工作流的简要描述。", + "A brief description of the agent.": "代理的简要描述。", + "A default value.": "默认值。", + "Activity": "活动", + "Activity Instance ID": "活动实例ID", + "Add input": "添加输入", + "Add variable": "添加变量", + "Aggregation": "聚合", + "Allow this workflow to be used as an activity.": "允许该工作流作为一个活动。", + "Bulk actions": "批量操作", + "Bulk Dispatch Workflows": "批量调度工作流", + "Cancel": "取消", + "Clear": "清空", + "Click to open a new workflow tab": "点击打开一个新的工作流标签页", + "Close": "关闭", + "Close workflow": "关闭工作流", + "Common": "公共", + "Break": "跳出", + "Composition": "混合", + "Console": "控制台", + "Container": "容器", + "Correlate": "关联", + "Correlation ID": "关联ID", + "Create API Key": "创建 API Key", + "Create Service": "创建 Service", + "CreatedAt": "创建于", + "Cron": "Cron表达式", + "Dashboard": "仪表盘", + "Date": "日期", + "Date Filters": "日期筛选器", + "Decision": "决策", + "Default configuration": "默认配置", + "Default value": "默认值", + "Definition version ID": "工作流定义版本ID", + "Definitions": "工作流定义列表", + "Delete": "删除", + "Description": "描述", + "Dispatch Workflow": "调度工作流", + "Display name": "显示名称", + "Display text": "显示文本", + "Edit": "编辑", + "End": "结束", + "Event": "事件", + "Excel Parser": "Excel 解析器", + "Executions": "Execution 列表", + "Export": "导出", + "Export this version as a JSON file.": "导出该版本到JSON文件", + "Fault": "错误", + "Finish": "完成", + "Finished": "已完成", + "Flow": "Flow", + "14 days": "14 天", + "A brief description of this output.": "输出的简要描述。", + "Add": "添加", + "A brief description of this input.": "输入的简要描述。", + "A brief description of the secret.": "secret的简要描述。", + "Activities": "活动列表", + "Add output": "添加输出", + "Apply": "应用", + "Auto-save": "自动保存", + "Category": "分类", + "Complete": "完成", + "Completed": "已完成", + "Definition ID": "工作流定义ID", + "For": "For", + "Definition version": "工作流定义版本", + "Delay": "延迟", + "Create Secret": "创建 Secret", + "Created": "已创建", + "Create workflow": "创建工作流", + "Details": "详情", + "Download File": "下载文件", + "Branching": "分支", + "Execution Settings": "Execution 设置", + "Duration": "持续时间", + "Email": "邮件", + "Events": "事件列表", + "Export the current workflow as a JSON file": "导出当前工作流的JSON文件", + "ID": "ID", + "Import": "导入", + "If": "If", + "FinishedAt": "完成于", + "Activation strategy": "激活策略", + "Agent": "代理", + "Agents": "代理列表", + "API Key": "API Key", + "Auto-update consuming workflows": "自动更新使用中的工作流", + "API Keys": "接口密钥", + "Connection Strings": "连接字符串", + "Create Agent": "创建代理", + "Enter a list of possible outcomes for this workflow.": "键入此工作流可能的结果列表。", + "HTTP File Response": "HTTP文件响应", + "The ID of the activity.": "此活动的ID。", + "Info": "信息", + "Information": "信息", + "Input Properties": "输入属性列表", + "Input/output": "输入/输出", + "Is After": "在此之后", + "Is Not": "不包含", + "Join": "连接", + "Latest version": "最新版本", + "Loading": "加载中", + "Log Persistence Strategy": "日志持久化策略", + "No": "否", + "No inputs": "输入列表为空", + "No outcomes associated with this execution.": "此执行过程没有任何结果。", + "No variables": "没有变量列表", + "object": "对象(Object)", + "Ok": "确认", + "Open File": "打开文件", + "Outcomes": "结果列表", + "Output": "输出", + "Output Properties": "输出属性", + "Outputs": "输出列表", + "Log Persistence": "日志持久化", + "Metadata": "元数据", + "Properties": "属性列表", + "Publish Event": "发布事件", + "Published": "已发布", + "Publish the current workflow": "发布当前工作流", + "Read Data from Configuration / Runtime": "从配置/运行时读取数据", + "Read Data from Database": "从数据库读取数据", + "Rollback to this version": "回退到此版本", + "Secret": "Secret", + "Send Email": "发送电子邮件", + "Services": "服务", + "Rows Per Page": "每页行数", + "Run": "运行", + "Run Asynchronously": "运行(异步)", + "Run JavaScript": "运行JavaScript", + "Run Python": "运行Python", + "Run workflow": "运行工作流", + "Save File": "保存文件", + "Scope": "范围", + "Settings": "设置列表", + "Start": "开始", + "Started": "已开始", + "State": "状态", + "Storage": "存储库", + "string": "字符串", + "Sub-Workflow Inputs": "子工作流输入列表", + "The name of the new agent.": "新代理的名称。", + "Sub-Workflow Outputs": "子工作流输出列表", + "SubStatus": "子状态", + "Switch": "选择器", + "Switch (flow)": "选择器(流模式)", + "Task": "任务", + "The description of the output variable.": "输出变量的描述。", + "This output is an array": "这个输出是一个数组", + "This input is an array": "这个输入是一个数组", + "This activity does not have any output properties.": "这个活动没有任何输出属性。", + "There are currently no variables defined.": "目前没有定义任何变量。", + "There are currently no inputs defined.": "目前没有定义任何输入。", + "There are currently no outputs defined.": "目前没有定义任何输出。", + "The type of the service.": "服务的类型。", + "Variables": "变量列表", + "Version history": "历史版本", + "View": "视图", + "When you publish": "当您发布一个新版本时,所有的被使用工作流将被更新,以指向该工作流的新版本。", + "Workflow Context": "工作流上下文", + "Workflow Contexts": "工作流上下文列表", + "Workflow definitions Editor": "工作流定义编辑器", + "Workflow instances": "工作流实例", + "The name of the workflow.": "工作流的名称。", + "You are running in read-only mode. Workflow operations are disabled": "您正在以只读模式运行。工作流操作被禁止", + "You can find documentation and examples here": "你可以在这里找到文档和示例", + "Version": "版本", + "Webhooks": "Webhooks", + "When checked, the workflow will be executed asynchronously.": "选中时,工作流将异步执行。", + "Whether to show the description in the designer.": "是否在设计器中显示描述。", + "Workflow Context Settings": "工作流上下文设置", + "Timer": "计时器", + "Time": "时间", + "This variable is an array": "这个变量是一个数组", + "This activity does not have any input properties.": "这个活动没有任何输入属性。", + "The value of the API key.": "API密钥的值。", + "The name of the agent.": "代理的名称。", + "The name of the function that the agent will execute.": "代理将执行的函数的名称。", + "Search": "搜索…", + "Value": "值", + "Write Line": "写行", + "Yes": "是", + "Search on name, id, definition id, definition version id, correlation id": "搜索 名称,id,定义id,定义版本id,相关id", + "Search on name, description, id, definition id": "搜索 名称,描述,id,定义id", + "Set Workflow Context Parameter": "设置工作流上下文参数", + "Set Output": "设置输出", + "Set Variable": "设置变量", + "Showing all events": "显示所有事件", + "Start at": "开始于", + "Status": "状态", + "Sub status": "子状态", + "Sub-Workflow": "子工作流", + "Temperature": "温度", + "The name of the activity. Named activities can be referenced by other activities.": "活动的名称。命名活动后可以被其他活动引用。", + "No output associated with this execution.": "此执行过程没有任何输出。", + "No outputs": "输出列表为空", + "Plugins": "插件列表", + "Publish": "发布", + "Published version": "已发布版本", + "Save": "保存", + "HTTP Endpoint": "HTTP 终节点", + "HTTP Request": "HTTP请求", + "HTTP Request (flow)": "HTTP请求(流模式)", + "HTTP Response": "HTTP响应", + "Input": "输入", + "Import a JSON file containing workflow data to override the current workflow\u0027s contents.": "导入一个包含工作流数据的JSON文件,以覆盖当前工作流的内容。", + "Inputs": "输入列表", + "Instances": "实例列表", + "int": "整型(Int)", + "Is array": "是数组", + "Is Before": "在此之前", + "A description about the role and purpose of this input variable.": "关于此输入变量的角色和目的的描述。", + "A custom activity category to assign this workflow-as-an-activity to.": "分配给此工作流作为活动的自定义活动类别。", + "A custom category by which to group this input.": "用于对此输入进行分组的自定义类别。", + "A custom category by which to group this output.": "用于对此输出进行分组的自定义类别。", + "A description about the secret.": "关于此密钥的描述。", + "A technical name for the function": "此代理执行的功能名称。例如:编写代码(WriteCode)", + "Acknowledge Datasource Event": "确认数据源事件", + "Check this option if the variable holds an array of the selected type.": "如果变量持有选定类型的数组,请勾选此选项。", + "Expires": "过期", + "Expires In": "过期于", + "Fork (flow)": "分支(流)", + "Frequency Penalty": "频率惩罚", + "Function": "函数", + "Function Name": "函数名称", + "General": "常规", + "HTTP": "HTTP", + "Has Incidents": "存在异常事件", + "IncidentCount": "异常事件数量", + "Incident Strategy": "异常事件策略", + "Incidents": "异常事件", + "Is": "是", + "Is on or After": "等于或在此之后", + "Is on or Before": "等于或在此之前", + "No agents found": "未找到任何代理", + "No API keys found": "未找到任何 API keys", + "No secrets found": "未找到任何 secrets", + "No services found": "未找到任何服务", + "No state associated with this execution.": "此执行过程没有任何状态。", + "No workflow instances found": "未找到任何工作流实例", + "No workflow definitions found": "未找到任何工作流定义", + "OpenAIChatCompletion": "OpenAI对话模型(OpenAIChatCompletion)", + "OpenAITextToImage": "OpenAI文本转图像模型(OpenAITextToImage)", + "Parser": "解析器", + "Looping": "循环控制", + "Manage all the things": "管理所有事项", + "Max Tokens": "最大 Tokens 数量", + "Never": "从不", + "Prompt Template": "提示词模版", + "Presence Penalty": "重复惩罚(Presence Penalty)", + "Run Task": "运行Task", + "Set Name": "设置名称", + "Scheduling": "计划控制", + "Secrets": "Secrets列表", + "Show accumulated time": "显示累计时长", + "Show incidents": "显示异常事件列表", + "Showing incidents": "正显示异常事件列表中", + "Start of workflow": "启动工作流", + "The lifetime of this secret": "此 secret 的生命周期", + "The .NET type of the input variable.": "此输入变量的.NET类型", + "The .NET type of the output variable.": "此输出变量的.NET类型", + "The maximum number": "可生成的最大tokens数量。留空时为可用的最大值。", + "The name of the new secret.": "新 secret 的名称。", + "The name of the new workflow.": "新工作流的名称。", + "The name of the variable.": "此变量的名称。", + "The penalty for repeating": "当Token重复时的惩罚。重复惩罚值越高,生成重复Token的可能性就越低,从而促进生成更多样化的文本。", + "The plugins that the agent can use.": "此代理可用的插件列表", + "The technical name of the service.": "此服务的专业名称。", + "The technical name of the input.": "此输入的专业名称。", + "The technical name of the API key.": "此 API key 的专业名称。", + "The technical name of the input variable": "此输入变量的专业名称。在提示模板中引用此变量时,请使用此名称。", + "Unable to make changes as the workflow or application is currently in read-only mode": "无法更改,因为当前工作流或应用处于只读模式。", + "UI Hint": "用户界面提示", + "Type": "类型", + "Trigger workflow": "触发工作流", + "TopP": "TopP", + "Top Probability determines": "TopP 决定了文本生成过程中 Token 选择的累积概率阈值。较低的值会导致多样性减少,使模型仅从最可能的 Token 中选择。较高的值将允许输出更加多样化,因为模型会考虑更广泛的 Token 范围。", + "This activity does not have any common properties.": "此活动没有任何公共属性。", + "The user-friendly name of this output.": "此输出的显示文本。", + "The user-friendly name of this input.": "此输入出的显示文本。", + "Used": "已使用", + "Use this version": "使用此版本", + "Use JSON Response": "启用 JSON Response", + "Usable as activity": "可用作活动", + "UpdatedAt": "更新于", + "Updated": "已更新", + "Unpublish the current workflow": "取消发布当前工作流。", + "Unpublish": "取消发布", + "Workflows": "工作流列表", + "Incident handling strategy": "异常事件处理策略", + "A description of this activity to be displayed in the designer.": "在设计器中显示的此活动的描述。", + "A description about the role and purpose of this agent.": "关于此代理的角色和目的的描述。", + "The services that the agent requires.": "此代理所需的服务列表", + "The secret value.": "此 secret 的值", + "Used to define the initial instructions": "初始指令或上下文,用于定义Semantic Kernel代理如何生成内容。此模板指导代理的行为,并帮助它理解需要扮演的角色或执行的任务。", + "The temperature of the model": "模型的温度。值越高,意味着模型将承担更大的风险。尝试0.1以获得与模型训练内容非常接近的结果。尝试0.9以获得极具创意的结果。", + "Journal": "日志", + "Primitives": "基本元素", + "Scripting": "脚本语言", + "Show events scoped to current container": "显示当前容器范围内的事件", + "Show relative time": "显示相对时间", + "The settings of the service in JSON format.": "服务的设置以JSON格式表示。", + "The system prompt template that the agent will use.": "系统提示模板,代理将使用该模板。", + "The penalty for repeating any one token": "重复任何一个token的惩罚。更高的频率惩罚将减少token重复的可能性,促进更多样化的文本生成。", + "The scope of the secret helps the UI to filter available options from dropdown lists.": "secret的范围可以从下拉列表中筛选可用选项。", + "A code editor": "代码编辑器", + "A dropdown list": "下拉列表", + "A JSON editor": "JSON编辑器", + "A list of checkboxes": "复选框列表", + "A list of radio buttons": "单选按钮列表", + "A single line of text input": "单行文本输入", + "A checkbox": "复选框", + "A variable with this name already exists in the current scope.": "当前作用域中已经存在这个名称的变量。", + "A workflow definition picker": "工作流定义选择器", + "A workflow output definition picker": "工作流输出定义选择器", + "An input with this name already exists.": "这个名称的输入已经存在。", + "Are you sure you want to delete the selected input?": "您确定要删除选定的输入吗?", + "Are you sure you want to delete the selected workflows?": "您确定要删除选定的工作流吗?", + "Are you sure you want to delete this version?": "您确定要删除此版本吗?", + "Are you sure you want to delete this workflow instance?": "您确定要删除此工作流实例吗?", + "Cancel running workflow instances?": "确定取消正在运行的工作流实例?", + "Cancel selected workflow instances?": "确定取消选定的工作流实例?", + "Checkbox": "复选框", + "Create variable": "创建变量", + "Default": "默认", + "Delete selected input?": "确定删除选定的输入?", + "Delete selected workflow instances?": "确认删除选定的工作流实例?", + "Delete selected workflows?": "确定删除选定的工作流?", + "Edit variable": "编辑变量", + "Enter the expression that will be used to determine the log persistence strategy for this property.": "输入将用于确定此属性的日志持久性策略的表达式。", + "Failed to import {0} workflow definitions. Errors: {1}": "导入 {0} 个工作流定义失败。错误:{1}", + "imported successfully.": "导入成功。", + "Inherit": "继承", + "Multi line": "多行", + "Multi text": "多文本", + "Not executed": "未执行", + "None": "无", + "Not usable as activity.": "不能作为活动使用。", + "One workflow consuming a published workflow has been updated": "一个使用已发布工作流的工作流已被更新", + "One workflow is already published": "已经发布了一个工作流", + "Output picker": "输出选择器", + "Please enter a name for the variable.": "请输入这个变量的名称。", + "Please enter a name for the workflow.": "请输入这个工作流的名称。", + "Single line": "单行", + "Successfully imported": "导入成功", + "The text to display in the designer.": "要在设计器中显示的文本。", + "Timestamp": "时间戳", + "When checked, this activity is the start of this workflow.": "当选中时,此活动是此工作流的开始。", + "Workflow definition picker": "工作流定义选择器", + "Workflow published": "工作流已发布", + "Workflow published with Not Found activities": "发布的工作流没有找到活动", + "workflows are unpublished": "工作流已撤销发布", + "workflows consuming published workflows have been updated": "已更新使用当前已发布工作流的其他工作流", + "Delete version {0}": "删除版本 {0}", + "{0} workflows are already published": "{0} 个工作流已发布", + "{0} workflows consuming published workflows have been updated": "已更新 {0} 个使用已发布工作流的工作流", + "{0} {1} imported successfully.": "{0} {1} 导入成功。", + "Are you sure you want to cancel this workflow instance?": "您确定要取消此工作流实例吗?", + "No workflows found to import.": "没有找到要导入的工作流。", + "Delete workflow?": "确定删除工作流?", + "No workflows were imported.": "没有导入任何工作流。", + "Multiple lines of text input": "多行文本输入", + "Successfully imported 1 workflow definition.": "成功导入 1 个工作流定义。", + "Delete version": "删除版本", + "Checklist": "复选框列表", + "A workflow with this name already exists.": "具有此名称的工作流已经存在。", + "Are you sure you want to delete the selected output?": "您确定要删除选定的输出吗?", + "The workflow cannot be started": "无法启动工作流", + "Create input": "创建输入", + "One workflow is published": "发布一个工作流", + "Delete selected versions": "删除选定版本", + "Publish selected workflows?": "发布选定的工作流?", + "Successfully started workflow": "启动工作流成功", + "Edit output": "编辑输出", + "Are you sure you want to cancel the selected workflow instances?": "您确定要取消选定的工作流实例吗?", + "Are you sure you want to delete this workflow?": "您确定要删除此工作流吗?", + "A variable picker": "变量选择器", + "Default Strategy": "默认策略", + "Code editor": "代码编辑器", + "Are you sure you want to cancel all running workflow instances of this workflow definition?": "确定要取消此工作流定义的所有正在运行的工作流实例吗?", + "Are you sure you want to publish the selected workflows?": "您确定要发布选定的工作流吗?", + "Are you sure you want to delete the selected variable?": "您确定要删除选定的变量吗?", + "Create output": "创建输出", + "Delete workflow instance?": "确定删除工作流实例?", + "An outcome picker": "结果选择器", + "Are you sure you want to delete the selected versions?": "您确定要删除选定的版本吗?", + "An input for multiple words, like a tagging input": "多个单词的输入,如标记输入", + "Are you sure you want to delete the selected workflow instances?": "您确定要删除选定的工作流实例吗?", + "Are you sure you want to unpublish the selected workflows?": "您确定要取消选定的工作流吗?", + "Delete selected variable?": "确认删除选定的变量?", + "Please enter a name for the output.": "请输入这个输出的名称。", + "Cancel workflow instance?": "取消工作流实例?", + "Edit Workflow Definition": "编辑工作流定义", + "New workflow": "新工作流", + "One workflow is not found": "没有找到一个工作流", + "Delete selected output?": "确定删除选定的输出?", + "{0} consuming workflow(s) updated": "{0} 个使用中的工作流已更新", + "Dropdown": "下拉", + "Radio list": "单选列表", + "Message": "消息", + "One workflow is unpublished": "撤销发布一个工作流", + "JSON editor": "JSON 编辑器", + "Please enter a name for the input.": "请输入这个输入的名称。", + "Workflow was already published": "工作流已经发布", + "Outcome picker": "结果选择器", + "One workflow is already unpublished": "一个工作流已经撤销发布", + "Readonly": "只读", + "Successfully imported one instance": "成功导入一个实例", + "workflow definitions.": "工作流定义。", + "Show description": "显示描述", + "Workflow retracted": "已撤销工作流", + "The commit strategy to use that controls when the workflow state should be committed.": "要使用的提交策略控制应何时该提交工作流状态。", + "The ID of the variable.": "变量的ID。", + "The read-only workflows will not be affected.": "只读工作流不会受到影响。", + "Upload JSON and/or ZIP files containing workflow data to import.": "上传包含工作流数据的JSON文件或包含工作流数据的JSON文件的压缩包(ZIP)导入。", + "Variable picker": "变量选择器", + "Unpublish selected workflows?": "确定取消发布选定的工作流?", + "When checked, this activity can trigger this workflow.": "当被选中时,此活动可以触发此工作流。", + "Workflow saved": "已保存工作流", + "workflows are not found": "未找到工作流", + "workflows are already published": "工作流已经发布", + "Successfully imported {0} instances": "成功导入 {0} 个实例", + "Workflow unpublished": "已取消发布工作流", + "workflows are already unpublished": "工作流已撤销发布", + "workflows are published": "工作流已发布", + "{0} workflows are published": "{0} 个工作流已发布", + "Failed to import {0} {1}.": "{0} {1} 导入失败。", + "Successfully imported {0} workflow definitions.": "成功导入 {0} 个工作流定义。", + "{0} workflows are unpublished": "{0} 个工作流已取消发布", + "{0} workflows are not found": "{0} 个工作流未找到", + "{0} workflows are already unpublished": "{0} 个工作流已取消发布", + "The join mode determines whether this activity should continue as soon as one inbound path comes in (Wait Any), or once all inbound paths have executed (Wait All).": "连接模式决定这个活动是否应该在一个入站路径进入时就继续(等待任意一个),还是在所有入站路径都执行完毕后才继续(等待全部)。", + "The data source to use for dispatching the workflows.": "用于分派工作流的数据源。", + "The default key to use for the input name when sending the current item to the dispatched workflow. Will not be used if the Items field contain a list of dictionaries": "在将当前项发送到被分派的工作流时用于输入名称的默认键。如果Items字段包含字典列表,则不会使用该键", + "Auto layout": "自动布局", + "Center": "中心", + "Zoom to fit the screen": "缩放适应屏幕", + "A function to compute the correlation ID to associate a dispatched workflow with.": "一个用来计算关联分发的工作流的相关性ID的函数。", + "A list of expected outcomes to handle.": "一个预期结果列表进行处理。", + "A list of expected status codes to handle.": "一个要处理的预期状态码列表。", + "A value that allows to add the Authorization header without validation.": "一个允许在不进行验证的情况下添加授权头的值。", + "Always allow the creation": "始终允许创建", + "Branches": "分支", + "Break out of a loop.": "跳出循环。", + "Cancelled": "已取消", + "Condition": "状态", + "Content Type": "内容形式", + "Correlation ID Function": "相关 ID 函数", + "Default Item Input Key": "默认项目输入键", + "Disable Authorization Header Validation": "禁用授权头验证", + "Executes javaScript code": "执行JavaScript代码", + "Executing": "执行中", + "Expected Status Codes": "预期的状态码", + "Method": "方法", + "Mode": "模式", + "Request Content.": "请求内容。", + "Result": "结果", + "Running": "运行中", + "Supported Methods": "建议方法", + "The Authorization header value to send with the request. For example: Bearer {some-access-token}": "和请求一起发送的授权头值。例如:Bearer {some - access - token}", + "The content to send with the request. Can be a string, an object, a byte array or a stream.": "随请求发送的内容。可以是字符串、对象、字节数组或者流。", + "The content type to use when sending the request.": "发送请求时使用的内容类型。", + "The definition ID of the workflows to dispatch.": "要分派的工作流的定义ID。", + "The HTTP method to use when sending the request.": "发送请求时使用的HTTP方法。", + "The HTTP methods to accept.": "要接受的HTTP方法。", + "Allow authenticated requests only.": "仅允许经过身份验证的请求。", + "Provide a policy to evaluate. If the policy fails, the request is forbidden.": "提供一个用于评估的策略。如果策略失败,则请求被禁止。", + "The maximum time allowed to process the request.": "处理请求的最长允许时间。", + "The maximum request size allowed in bytes.": "允许的最大请求大小(以字节为单位)。", + "The maximum file size allowed in bytes for an individual file.": "单个文件允许的最大字节数。", + "Only file extensions in this list are allowed. Leave empty to allow all extensions": "仅允许此列表中的文件扩展名。留空表示允许所有扩展名。", + "File extensions in this list are forbidden. Leave empty to not block any extension.": "此列表中的文件扩展名被禁止。留空则不会阻止任何扩展名。", + "Only MIME types in this list are allowed. Leave empty to allow all types": "仅允许此列表中的 MIME 类型。留空表示允许所有类型。", + "A value indicating whether to expose the \"Request too large\" outcome.": "表示是否暴露“请求过大”结果的值。", + "A value indicating whether to expose the \"File too large\" outcome.": "指示是否显示“文件过大”结果的值。", + "A value indicating whether to expose the \"Invalid file extension\" outcome.": "指示是否显示“无效文件扩展名”结果的值。", + "A value indicating whether to expose the \"Invalid file MIME type\" outcome.": "指示是否暴露“无效文件MIME类型”结果的值。", + "The parsed request content, if any.": "解析后的请求内容,如有。", + "The uploaded files, if any.": "已上传的文件,如有。", + "The first uploaded file, if any.": "第一个上传的文件,如有。", + "The parsed route data, if any.": "解析后的路由数据,如有。", + "The querystring data, if any.": "查询字符串数据,如有。", + "The headers, if any.": "请求头,如有。", + "The outcome or set of outcomes to complete this activity with.": "完成此活动的结果或结果集。", + "The path to associate with the workflow.": "与工作流关联的路径。", + "The switch mode determines whether the first match should be scheduled, or all matches.": "切换模式决定是应该安排第一个匹配项,还是安排所有匹配项。", + "The URL to download the file from.": "用于下载文件的URL。", + "Time Duration": "持续时间", + "Trigger execution at a specific time in the future.": "在未来特定的时间触发执行。", + "URL": "URL", + "Validation Result": "验证结果", + "Workflow Definition": "工作流定义", + "Add case": "添加示例", + "Data": "数据", + "Label": "标签", + "String7": "结果", + "Zoom to fit": "缩放匹配", + "Always allow the creation of a new workflow instance.": "总是允许创建新的工作流实例。", + "Assign the current value to the specified variable.": "将当前值赋给指定的变量。", + "Code": "代码", + "Controls whether the end step is upper/lowerbound inclusive or exclusive. True (inclusive) by default.": "控制结束步骤是包含还是排除上界/下界。默认为True(包含)。", + "Current Value": "现有赋值", + "Interval": "间隔", + "Outer Bound Inclusive": "包含外部边界", + "Payload": "负载", + "Step": "步", + "The end step.": "终止步骤。", + "The items to iterate through.": "要迭代的项目。", + "The set of values to iterate.": "要迭代的值集合。", + "The start step.": "初始化。", + "The step size. To count down, enter a negative number.": "步长。要倒计时,请输入一个负数。", + "Variable": "变量", + "Exclude": "排除", + "Include": "包括", + "Load": "加载", + "Configuration": "配置", + "Provider Type": "提供者类型", + "Pending": "待处理", + "min ago": "分钟前", + "Loaded": "已加载", + "Operational health": "运行健康", + "Available": "可用", + "Prioritized findings": "优先关注项", + "Unavailable": "不可用", + "h ago": "小时前", + "No hotspots in this range.": "此范围内没有热点数据。", + "Faults": "故障", + "Dashboard unavailable": "仪表盘不可用", + "Last hour": "最近 1 小时", + "Last 24 hours": "最近 24 小时", + "Current suspended instances": "当前挂起实例", + "Dashboard data is unavailable from the selected backend.": "无法从所选后端获取仪表盘数据。", + "Execution trend": "执行趋势", + "items": "项", + "Accepting work": "接收中", + "Avg duration": "平均耗时", + "Refreshed": "刷新于", + "Suspended": "已挂起", + "Backend": "后端", + "Draining": "排空中", + "Last 7 days": "最近 7 天", + "Selected backend": "所选后端", + "Not installed": "未安装", + "Paused": "已暂停", + "Needs attention": "需要注意", + "Refresh failed": "刷新失败", + "Not refreshed yet": "尚未刷新", + "No workflow activity in this range.": "此范围内没有工作流活动。", + "No access": "无权限", + "Current active instances": "当前活动实例", + "Workflow hotspots": "工作流热点", + "Interrupted": "已中断", + "Actions": "操作", + "Search on Id, Definition Id, Definition Version Id, Correlation Id": "按 ID、定义 ID、定义版本 ID、相关性 ID 搜索", + "Refresh dashboard": "刷新仪表盘", + "Search on Id, Name, Description, Definition Id": "按 ID、名称、描述、定义 ID 搜索", + "Backend disconnected": "后端连接断开", + "No workflow instances changed in this range.": "此范围内没有工作流实例发生变化。", + "Workflow": "工作流", + "d ago": "天前", + "item": "项", + "Open workflow instance": "打开工作流实例", + "Just now": "刚刚", + "Workflow / instance": "工作流 / 实例", + "Name": "名称", + "No active findings in the selected range.": "所选范围内没有活动发现项。", + "Faulted": "失败", + "Recent workflow activity": "最近的工作流活动", + "Logs": "日志", + "To": "至", + "discarded locally": "已在本地丢弃", + "Auto scroll": "自动滚动", + "Source": "来源", + "Reconnect": "重新连接", + "Auto": "自动", + "Copy selected": "复制所选", + "Structured Logs": "结构化日志", + "Wrap messages": "自动换行", + "The live structured log subscription is disconnected.": "实时结构化日志订阅已断开。", + "Workflow instance": "工作流实例", + "Open details": "打开详情", + "Minimum level": "最低级别", + "All sources": "所有来源", + "Compact rows": "紧凑行", + "Tenant": "租户", + "Compact": "紧凑", + "Span ID": "跨度 ID", + "From": "从", + "rows": "行", + "Text": "文本", + "Trace ID": "追踪 ID", + "Copy visible": "复制可见", + "Wrap": "换行" + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/Navigation/ElsaStudioMenuContributor.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/Navigation/ElsaStudioMenuContributor.cs new file mode 100644 index 000000000..f5f9e3908 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/Navigation/ElsaStudioMenuContributor.cs @@ -0,0 +1,49 @@ +using LINGYUN.Abp.ElsaNext.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(); + + 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); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/Navigation/ElsaStudioMenus.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/Navigation/ElsaStudioMenus.cs new file mode 100644 index 000000000..4116114d7 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/Navigation/ElsaStudioMenus.cs @@ -0,0 +1,6 @@ +namespace LINGYUN.Abp.ElsaNext.Studio.Blazor.Navigation; + +public static class ElsaStudioMenus +{ + public const string GroupName = "ElsaStudio"; +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/Properties/launchSettings.json b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/Properties/launchSettings.json new file mode 100644 index 000000000..c882c2461 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/Properties/launchSettings.json @@ -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" + } + } + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/Widgets/WorkflowDefinitionPropertitesEditorWidget.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/Widgets/WorkflowDefinitionPropertitesEditorWidget.cs new file mode 100644 index 000000000..f6fe129fb --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/Widgets/WorkflowDefinitionPropertitesEditorWidget.cs @@ -0,0 +1,19 @@ +using Elsa.Studio.Workflows.Widgets; +using LINGYUN.Abp.ElsaNext.Studio.Blazor.Components.WorkflowProperties.Tabs.Properties.Sections.Properties; +using Microsoft.AspNetCore.Components; + +namespace LINGYUN.Abp.ElsaNext.Studio.Blazor.Widgets; + +public class WorkflowDefinitionPropertitesEditorWidget : WorkflowDefinitionPropertiesWidgetBase +{ + public override double Order => 15; + + public override Func, RenderFragment> Render => attributes => builder => + { + builder.OpenComponent(0); + builder.AddAttribute(1, nameof(WorkflowDefinitionPropertitesEditor.WorkflowDefinition), attributes["WorkflowDefinition"]); + builder.AddAttribute(2, nameof(WorkflowDefinitionPropertitesEditor.WorkflowDefinitionUpdated), attributes["WorkflowDefinitionUpdated"]); + builder.CloseComponent(); + + }; +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/_Imports.razor b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/_Imports.razor new file mode 100644 index 000000000..148ae623d --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Blazor/_Imports.razor @@ -0,0 +1,16 @@ +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Rendering +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.JSInterop +@using MudBlazor +@using MudExtensions +@using Blazored.FluentValidation +@using Elsa.Studio.Components +@using Elsa.Studio.Localization +@using Elsa.Studio.Workflows.Components.WorkflowDefinitionEditor +@using Elsa.Studio.Workflows.Components.WorkflowInstanceViewer +@using Elsa.Studio.Workflows.Components.WorkflowDefinitionList +@using Elsa.Studio.Workflows.Components.WorkflowInstanceList diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor/AbpElsaNextStudioDashboardBlazorModule.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor/AbpElsaNextStudioDashboardBlazorModule.cs new file mode 100644 index 000000000..6d65013b0 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor/AbpElsaNextStudioDashboardBlazorModule.cs @@ -0,0 +1,61 @@ +using Elsa.Extensions; +using Elsa.Features.Services; +using Elsa.Studio.Dashboard.Extensions; +using LINGYUN.Abp.ElsaNext.Localization; +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 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(elsa => + { + elsa.UseWorkflowRuntimeDashboard(); + }); + } + + public override void ConfigureServices(ServiceConfigurationContext context) + { + var configuration = context.Configuration.GetSection("Elsa:Studio"); + + Configure(options => + { + options.AdditionalAssemblies.Add(typeof(AbpElsaNextStudioDashboardBlazorModule).Assembly); + }); + + Configure(options => + { + options.MenuContributors.Add(new ElsaStudioDashboardMenuContributor()); + }); + + Configure(options => + { + options.FileSets.AddEmbedded("LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor"); + }); + Configure(options => + { + options.Resources + .Get() + .AddVirtualJson("/Localization/Resources/ElsaStudioDashboardBlazor"); + }); + + var elsaNextStudioBlazoeOptions = context.Services.ExecutePreConfiguredActions(); + + context.Services.AddDashboardModule(elsaNextStudioBlazoeOptions.BackendApiConfig); + context.Services.AddDashboardWidgets(); + + context.Services.AddScoped(); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor/Extensions/DashboardWidgetServiceCollectionExtensions.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor/Extensions/DashboardWidgetServiceCollectionExtensions.cs new file mode 100644 index 000000000..3e577d00e --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor/Extensions/DashboardWidgetServiceCollectionExtensions.cs @@ -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("dashboard.workflow.metrics", DashboardWidgetZones.Metrics, 100, "Workflow metrics", payloadKind: "WorkflowInstances") + .AddDashboardWidget("dashboard.needs-attention", DashboardWidgetZones.Findings, 100, "Needs attention") + .AddDashboardWidget("dashboard.workflow.trend", DashboardWidgetZones.Trend, 100, "Workflow trends", payloadKind: "WorkflowTrends") + .AddDashboardWidget("dashboard.workflow.recent-activity", DashboardWidgetZones.Activity, 100, "Recent activity", payloadKind: "RecentActivity") + .AddDashboardWidget("dashboard.workflow.hotspots", DashboardWidgetZones.SecondaryPanels, 100, "Workflow hotspots", payloadKind: "WorkflowHotspots"); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor/FodyWeavers.xml b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor/FodyWeavers.xml new file mode 100644 index 000000000..5d6962159 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor/FodyWeavers.xsd b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor/FodyWeavers.xsd new file mode 100644 index 000000000..3f3946e28 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor.csproj b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor.csproj new file mode 100644 index 000000000..2eb8232f1 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor.csproj @@ -0,0 +1,31 @@ + + + + + + + net10.0 + enable + enable + LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor + LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor + + + + + + + + + + + + + + + + + + + diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor/Localization/Resources/ElsaStudioDashboardBlazor/en.json b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor/Localization/Resources/ElsaStudioDashboardBlazor/en.json new file mode 100644 index 000000000..754e8edd1 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor/Localization/Resources/ElsaStudioDashboardBlazor/en.json @@ -0,0 +1,6 @@ +{ + "culture": "en", + "texts": { + "Menu:Dashboard": "Dashboard" + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor/Localization/Resources/ElsaStudioDashboardBlazor/zh-Hans.json b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor/Localization/Resources/ElsaStudioDashboardBlazor/zh-Hans.json new file mode 100644 index 000000000..5a64b49ad --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor/Localization/Resources/ElsaStudioDashboardBlazor/zh-Hans.json @@ -0,0 +1,6 @@ +{ + "culture": "zh-Hans", + "texts": { + "Menu:Dashboard": "仪表盘" + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor/Navigation/ElsaStudioDashboardMenuContributor.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor/Navigation/ElsaStudioDashboardMenuContributor.cs new file mode 100644 index 000000000..ae8fe29bd --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor/Navigation/ElsaStudioDashboardMenuContributor.cs @@ -0,0 +1,31 @@ +using LINGYUN.Abp.ElsaNext.Localization; +using LINGYUN.Abp.ElsaNext.Studio.Blazor.Navigation; +using Volo.Abp.UI.Navigation; + +namespace LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor.Navigation; + +/// +/// 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). +/// +public class ElsaStudioDashboardMenuContributor : IMenuContributor +{ + public Task ConfigureMenuAsync(MenuConfigurationContext context) + { + if (context.Menu.Name == StandardMenus.Main) + { + var menu = context.Menu; + var l = context.GetLocalizer(); + + 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; + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor/Pages/Dashboard.razor b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor/Pages/Dashboard.razor new file mode 100644 index 000000000..c57663582 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor/Pages/Dashboard.razor @@ -0,0 +1,92 @@ +@page "/dashboard" +@attribute [Authorize] +@inherits StudioComponentBase +@inject ILocalizer Localizer + + + + + +@Localizer["Dashboard"] + + + + + @Localizer["Dashboard"] + + @BackendLabel + @if (_snapshot != null) + { + var runtime = _snapshot.Overview.Runtime.Status; + @Text.RuntimeStatusLabel(runtime) + } + else + { + @StatusLabel + } + @LastRefreshedLabel + + + + + + @foreach (var option in DashboardRangeMapper.Options) + { + @option.Label + } + + + + + + + + @if (!string.IsNullOrWhiteSpace(_message)) + { + @_message + } + + @if (_loading && _snapshot == null) + { + + @for (var i = 0; i < 6; i++) + { + + + + } + + + } + else if (_snapshot == null) + { + + + @StatusLabel + @(_message ?? @Localizer["Dashboard data is unavailable from the selected backend."]) + + } + else + { + + + + + + + + + + + + + + + + + + + + + } + diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor/Pages/Dashboard.razor.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor/Pages/Dashboard.razor.cs new file mode 100644 index 000000000..e9625f978 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor/Pages/Dashboard.razor.cs @@ -0,0 +1,192 @@ +using Elsa.Studio.Contracts; +using Elsa.Studio.Dashboard.Models; +using Elsa.Studio.Dashboard.Services; +using Elsa.Studio.Dashboard.Widgets; +using LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor.Services; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor.Pages; + +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 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 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() + { + 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(); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor/Services/ElsaStudioDashboardText.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor/Services/ElsaStudioDashboardText.cs new file mode 100644 index 000000000..c6ad4c2e0 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor/Services/ElsaStudioDashboardText.cs @@ -0,0 +1,119 @@ +using Elsa.Studio.Dashboard.Models; +using Elsa.Studio.Localization; +using System.Globalization; + +namespace LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor.Services; + +/// +/// 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 +/// (which follows the ABP UI culture captured on the circuit) and format +/// numbers with that same culture. +/// +public class ElsaStudioDashboardText +{ + private readonly ILocalizer _localizer; + + public ElsaStudioDashboardText(ILocalizer localizer) + { + _localizer = localizer; + } + + /// The UI culture captured at prerender (fallback: current thread culture). + public CultureInfo Culture => CultureInfo.CurrentUICulture; + + /// Localized text for the given key (falls back to the key itself). + public string this[string key] => _localizer[key]; + + /// Localized capability label of the "needs attention" backend capability. + public string CapabilityLabel(DashboardCapabilityStatus? capability) + { + return capability?.Status switch + { + "Available" => _localizer["Available"], + "Unauthorized" => _localizer["No access"], + "NotInstalled" => _localizer["Not installed"], + _ => _localizer["Unavailable"] + }; + } + + /// Localized label of the runtime status chip shown in the dashboard header. + public string RuntimeStatusLabel(string? status) + { + return status switch + { + DashboardRuntimeStatusKeys.AcceptingWork => _localizer["Accepting work"], + DashboardRuntimeStatusKeys.Paused => _localizer["Paused"], + DashboardRuntimeStatusKeys.Draining => _localizer["Draining"], + _ => _localizer["Unavailable"] + }; + } + + /// Humanized caption for the selected dashboard range. + 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"]}"; + } + + /// Trend chart x-axis label for a bucket timestamp. + 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); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor/Widgets/ExecutionTrendWidget.razor b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor/Widgets/ExecutionTrendWidget.razor new file mode 100644 index 000000000..42e19cb6f --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor/Widgets/ExecutionTrendWidget.razor @@ -0,0 +1,59 @@ +@using LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor.Services + +@inject ElsaStudioDashboardText Text + +@if (Context.Snapshot != null) +{ + var response = Context.Snapshot.Trend; + + + + @Text["Execution trend"] + + @RangeCaption(response) + + + + @if (Context.Loading) + { + + } + else if (response.Buckets.Count == 0) + { + @Text["No workflow activity in this range."] + } + else + { +
+ +
+ } +
+} + +@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> 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() } + ]; +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor/Widgets/HotspotsWidget.razor b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor/Widgets/HotspotsWidget.razor new file mode 100644 index 000000000..07e8b72d0 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor/Widgets/HotspotsWidget.razor @@ -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; + + + + @Text["Workflow hotspots"] + @MetricLabel(response.Metric) + + + @if (response.Items.Count == 0) + { + @Text["No hotspots in this range."] + } + else + { + + + @Text["Workflow"] + @Text["Value"] + @Text["Avg duration"] + + + + + @DisplayName(context) + @context.DefinitionId + + + @Text.Count(context.Value) + @Text.Duration(context.AverageDuration) + + + } + +} + +@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!; +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor/Widgets/NeedsAttentionWidget.razor b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor/Widgets/NeedsAttentionWidget.razor new file mode 100644 index 000000000..b26457288 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor/Widgets/NeedsAttentionWidget.razor @@ -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; + + + + @Text["Needs attention"] + @Text.CapabilityLabel(capability) + + + @if (findings.Count == 0) + { + + @Text["No active findings in the selected range."] + + } + else + { + + @foreach (var item in findings.OrderBy(x => x.Priority).Take(8)) + { + + + @item.Message + @item.Severity + + + } + + } + +} + +@code { + [Parameter] public DashboardWidgetContext Context { get; set; } = null!; + [Parameter] public DashboardWidgetDescriptor Descriptor { get; set; } = null!; +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor/Widgets/OperationalHealthWidget.razor b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor/Widgets/OperationalHealthWidget.razor new file mode 100644 index 000000000..0d6146d80 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor/Widgets/OperationalHealthWidget.razor @@ -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) + }; + +
+ @Text["Operational health"] + +
+ @foreach (var metric in healthMetrics) + { +
+ @MetricContent(metric) +
+ } +
+
+} + +@code { + [Parameter] public DashboardWidgetContext Context { get; set; } = null!; + [Parameter] public DashboardWidgetDescriptor Descriptor { get; set; } = null!; + + private RenderFragment MetricContent => metric => @ + + @metric.Label + + + @metric.Value + @metric.Caption + ; + + private sealed record HealthMetric(string Label, string Value, string Caption, string Icon, Color Color); +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor/Widgets/RecentActivityWidget.razor b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor/Widgets/RecentActivityWidget.razor new file mode 100644 index 000000000..28e90ad2d --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor/Widgets/RecentActivityWidget.razor @@ -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; + + + + @Text["Recent workflow activity"] + @items.Count @(items.Count == 1 ? Text["item"] : Text["items"]) + + + + + @Text["Workflow / instance"] + @Text["Status"] + @Text["Sub status"] + @Text["Incidents"] + @Text["Duration"] + @Text["Updated"] + + + + + + @DisplayName(context) + @context.InstanceId + + + + @LocalizedStatus(context.Status) + + + @LocalizedStatus(context.SubStatus) + + @Text.Count(context.IncidentCount) + @Text.Duration(context.Duration) + @Text.RelativeTimestamp(context.UpdatedAt ?? context.FinishedAt ?? context.CreatedAt) + + + + + + + + @Text["No workflow instances changed in this range."] + + + +} + +@code { + [Parameter] public DashboardWidgetContext Context { get; set; } = null!; + [Parameter] public DashboardWidgetDescriptor Descriptor { get; set; } = null!; + + private void OnRowClick(TableRowClickEventArgs 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}/view"; + + 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 + }; + } +} + diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor/_Imports.razor b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor/_Imports.razor new file mode 100644 index 000000000..ce03c82ed --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor/_Imports.razor @@ -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 diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor/wwwroot/dashboard.css b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor/wwwroot/dashboard.css new file mode 100644 index 000000000..d561acea3 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Dashboard.Blazor/wwwroot/dashboard.css @@ -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 . + 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; + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.OpenTelemetry.Blazor/AbpElsaNextStudioDiagnosticsOpenTelemetryBlazorModule.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.OpenTelemetry.Blazor/AbpElsaNextStudioDiagnosticsOpenTelemetryBlazorModule.cs new file mode 100644 index 000000000..79abc3311 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.OpenTelemetry.Blazor/AbpElsaNextStudioDiagnosticsOpenTelemetryBlazorModule.cs @@ -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.Localization; +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 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(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(options => + { + options.AdditionalAssemblies.Add(typeof(Elsa.Studio.Diagnostics.OpenTelemetry.Feature).Assembly); + }); + + Configure(options => + { + options.MenuContributors.Add(new ElsaStudioOpenTelemetryMenuContributor()); + }); + + Configure(options => + { + options.FileSets.AddEmbedded("LINGYUN.Abp.ElsaNext.Studio.Diagnostics.OpenTelemetry.Blazor"); + }); + + Configure(options => + { + options.Resources + .Get() + .AddVirtualJson("/Localization/Resources/ElsaStudioOpenTelemetryBlazor"); + }); + + var elsaNextStudioBlazoeOptions = context.Services.ExecutePreConfiguredActions(); + + 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(options => + { + options.EndpointConfigureActions.Add((context) => + { + context.Endpoints.MapOpenTelemetryHub(); + }); + }); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.OpenTelemetry.Blazor/Extensions/OpenTelemetryWidgetServiceCollectionExtensions.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.OpenTelemetry.Blazor/Extensions/OpenTelemetryWidgetServiceCollectionExtensions.cs new file mode 100644 index 000000000..82919c87c --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.OpenTelemetry.Blazor/Extensions/OpenTelemetryWidgetServiceCollectionExtensions.cs @@ -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( + "diagnostics.open-telemetry", + DashboardWidgetZones.DiagnosticsStatus, + 300, + "OpenTelemetry", + requiredBackendCapability: "OpenTelemetry", + payloadKind: "OpenTelemetry.StorageDiagnostics"); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.OpenTelemetry.Blazor/FodyWeavers.xml b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.OpenTelemetry.Blazor/FodyWeavers.xml new file mode 100644 index 000000000..00e1d9a1c --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.OpenTelemetry.Blazor/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.OpenTelemetry.Blazor/FodyWeavers.xsd b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.OpenTelemetry.Blazor/FodyWeavers.xsd new file mode 100644 index 000000000..3f3946e28 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.OpenTelemetry.Blazor/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.OpenTelemetry.Blazor/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.OpenTelemetry.Blazor.csproj b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.OpenTelemetry.Blazor/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.OpenTelemetry.Blazor.csproj new file mode 100644 index 000000000..159b69241 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.OpenTelemetry.Blazor/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.OpenTelemetry.Blazor.csproj @@ -0,0 +1,34 @@ + + + + + + + net10.0 + enable + enable + LINGYUN.Abp.ElsaNext.Studio.Diagnostics.OpenTelemetry.Blazor + LINGYUN.Abp.ElsaNext.Studio.Diagnostics.OpenTelemetry.Blazor + + + + + + + + + + + + + + + + + + + + + + + diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.OpenTelemetry.Blazor/Localization/Resources/ElsaStudioOpenTelemetryBlazor/en.json b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.OpenTelemetry.Blazor/Localization/Resources/ElsaStudioOpenTelemetryBlazor/en.json new file mode 100644 index 000000000..53b64a571 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.OpenTelemetry.Blazor/Localization/Resources/ElsaStudioOpenTelemetryBlazor/en.json @@ -0,0 +1,7 @@ +{ + "culture": "en", + "texts": { + "Menu:OpenTelemetry": "Open Telemetry", + "OpenTelemetry": "Open Telemetry" + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.OpenTelemetry.Blazor/Localization/Resources/ElsaStudioOpenTelemetryBlazor/zh-Hans.json b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.OpenTelemetry.Blazor/Localization/Resources/ElsaStudioOpenTelemetryBlazor/zh-Hans.json new file mode 100644 index 000000000..37c1c6529 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.OpenTelemetry.Blazor/Localization/Resources/ElsaStudioOpenTelemetryBlazor/zh-Hans.json @@ -0,0 +1,7 @@ +{ + "culture": "zh-Hans", + "texts": { + "Menu:OpenTelemetry": "Open Telemetry", + "OpenTelemetry": "Open Telemetry" + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.OpenTelemetry.Blazor/Navigation/ElsaStudioOpenTelemetryMenuContributor.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.OpenTelemetry.Blazor/Navigation/ElsaStudioOpenTelemetryMenuContributor.cs new file mode 100644 index 000000000..2ccf4c1e5 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.OpenTelemetry.Blazor/Navigation/ElsaStudioOpenTelemetryMenuContributor.cs @@ -0,0 +1,27 @@ +using LINGYUN.Abp.ElsaNext.Localization; +using LINGYUN.Abp.ElsaNext.Studio.Blazor.Navigation; +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(); + + 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; + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.OpenTelemetry.Blazor/_Imports.razor b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.OpenTelemetry.Blazor/_Imports.razor new file mode 100644 index 000000000..6a4064616 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.OpenTelemetry.Blazor/_Imports.razor @@ -0,0 +1,5 @@ +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Web +@using Elsa.Studio.Components +@using Elsa.Studio.Localization +@using MudBlazor diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.StructuredLogs.Blazor/AbpElsaNextStudioDiagnosticsStructuredLogsBlazorModule.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.StructuredLogs.Blazor/AbpElsaNextStudioDiagnosticsStructuredLogsBlazorModule.cs new file mode 100644 index 000000000..035717ffc --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.StructuredLogs.Blazor/AbpElsaNextStudioDiagnosticsStructuredLogsBlazorModule.cs @@ -0,0 +1,76 @@ +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.Localization; +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 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(elsa => + { + elsa.UseStructuredLogs(); + elsa.UseStructuredLogsDashboard(); + }); + } + + public override void ConfigureServices(ServiceConfigurationContext context) + { + var configuration = context.Configuration.GetSection("Elsa:Studio"); + + Configure(options => + { + // options.AdditionalAssemblies.Add(typeof(Elsa.Studio.Diagnostics.StructuredLogs.Feature).Assembly); + options.AdditionalAssemblies.Add(typeof(AbpElsaNextStudioDiagnosticsStructuredLogsBlazorModule).Assembly); + }); + + Configure(options => + { + options.MenuContributors.Add(new ElsaStudioStructuredLogsMenuContributor()); + }); + + Configure(options => + { + options.FileSets.AddEmbedded("LINGYUN.Abp.ElsaNext.Studio.Diagnostics.StructuredLogs.Blazor"); + }); + + Configure(options => + { + options.Resources + .Get() + .AddVirtualJson("/Localization/Resources/ElsaStudioStructuredLogsBlazor"); + }); + + var elsaNextStudioBlazoeOptions = context.Services.ExecutePreConfiguredActions(); + + context.Services.AddStructuredLogsModule(elsaNextStudioBlazoeOptions.BackendApiConfig); + context.Services.AddStructuredLogsDashboardModule(); + context.Services.AddStructuredLogsDashboardWidget(); + + Configure(options => + { + options.EndpointConfigureActions.Add((context) => + { + context.Endpoints.MapStructuredLogsHub(); + }); + }); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.StructuredLogs.Blazor/Extensions/OpenTelemetryWidgetServiceCollectionExtensions.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.StructuredLogs.Blazor/Extensions/OpenTelemetryWidgetServiceCollectionExtensions.cs new file mode 100644 index 000000000..9836b3a9e --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.StructuredLogs.Blazor/Extensions/OpenTelemetryWidgetServiceCollectionExtensions.cs @@ -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( + "diagnostics.structured-logs", + DashboardWidgetZones.DiagnosticsStatus, + 100, + "Structured logs", + requiredBackendCapability: "StructuredLogs", + payloadKind: "Diagnostics.StructuredLogs"); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.StructuredLogs.Blazor/FodyWeavers.xml b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.StructuredLogs.Blazor/FodyWeavers.xml new file mode 100644 index 000000000..00e1d9a1c --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.StructuredLogs.Blazor/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.StructuredLogs.Blazor/FodyWeavers.xsd b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.StructuredLogs.Blazor/FodyWeavers.xsd new file mode 100644 index 000000000..3f3946e28 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.StructuredLogs.Blazor/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.StructuredLogs.Blazor/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.StructuredLogs.Blazor.csproj b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.StructuredLogs.Blazor/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.StructuredLogs.Blazor.csproj new file mode 100644 index 000000000..29689cbd9 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.StructuredLogs.Blazor/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.StructuredLogs.Blazor.csproj @@ -0,0 +1,34 @@ + + + + + + + net10.0 + enable + enable + LINGYUN.Abp.ElsaNext.Studio.Diagnostics.StructuredLogs.Blazor + LINGYUN.Abp.ElsaNext.Studio.Diagnostics.StructuredLogs.Blazor + + + + + + + + + + + + + + + + + + + + + + diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.StructuredLogs.Blazor/Localization/Resources/ElsaStudioStructuredLogsBlazor/en.json b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.StructuredLogs.Blazor/Localization/Resources/ElsaStudioStructuredLogsBlazor/en.json new file mode 100644 index 000000000..f83ce9422 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.StructuredLogs.Blazor/Localization/Resources/ElsaStudioStructuredLogsBlazor/en.json @@ -0,0 +1,7 @@ +{ + "culture": "en", + "texts": { + "Menu:StructuredLogs": "Structured logs", + "Logs": "Structured logs" + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.StructuredLogs.Blazor/Localization/Resources/ElsaStudioStructuredLogsBlazor/zh-Hans.json b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.StructuredLogs.Blazor/Localization/Resources/ElsaStudioStructuredLogsBlazor/zh-Hans.json new file mode 100644 index 000000000..f5026c284 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.StructuredLogs.Blazor/Localization/Resources/ElsaStudioStructuredLogsBlazor/zh-Hans.json @@ -0,0 +1,7 @@ +{ + "culture": "zh-Hans", + "texts": { + "Menu:StructuredLogs": "结构化日志", + "Logs": "结构化日志" + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.StructuredLogs.Blazor/Navigation/ElsaStudioStructuredLogsMenuContributor.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.StructuredLogs.Blazor/Navigation/ElsaStudioStructuredLogsMenuContributor.cs new file mode 100644 index 000000000..794773584 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.StructuredLogs.Blazor/Navigation/ElsaStudioStructuredLogsMenuContributor.cs @@ -0,0 +1,31 @@ +using LINGYUN.Abp.ElsaNext.Localization; +using LINGYUN.Abp.ElsaNext.Studio.Blazor.Navigation; +using Volo.Abp.UI.Navigation; + +namespace LINGYUN.Abp.ElsaNext.Studio.Diagnostics.StructuredLogs.Blazor.Navigation; + +/// +/// Adds the structured-logs page to the "ElsaStudio" menu group (created by the Studio.Blazor +/// module; created here too when absent so this module stays self-contained). +/// +public class ElsaStudioStructuredLogsMenuContributor : IMenuContributor +{ + public Task ConfigureMenuAsync(MenuConfigurationContext context) + { + if (context.Menu.Name == StandardMenus.Main) + { + var menu = context.Menu; + var l = context.GetLocalizer(); + + var group = menu.GetMenuItemOrNull(ElsaStudioMenus.GroupName); + group?.AddItem(new ApplicationMenuItem( + ElsaStudioMenus.GroupName + ".StructuredLogs", + l["Menu:StructuredLogs"], + url: "/diagnostics/structured-logs", + icon: "fa fa-receipt", + order: 4)); + } + + return Task.CompletedTask; + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.StructuredLogs.Blazor/Pages/Logs.razor b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.StructuredLogs.Blazor/Pages/Logs.razor new file mode 100644 index 000000000..f3c1dcedd --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.StructuredLogs.Blazor/Pages/Logs.razor @@ -0,0 +1,14 @@ +@page "/diagnostics/structured-logs" +@attribute [Authorize] +@inherits StudioComponentBase +@inject ILocalizer Localizer + +@using Elsa.Studio.Diagnostics.StructuredLogs.UI.Pages + + + + + + + + diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.StructuredLogs.Blazor/_Imports.razor b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.StructuredLogs.Blazor/_Imports.razor new file mode 100644 index 000000000..35625bdfb --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.StructuredLogs.Blazor/_Imports.razor @@ -0,0 +1,6 @@ +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Web +@using Elsa.Studio.Components +@using Elsa.Studio.Localization +@using MudBlazor +@using Volo.Abp.AspNetCore.Components.Web.Theming \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.StructuredLogs.Blazor/wwwroot/structuredLogs-overrides.css b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.StructuredLogs.Blazor/wwwroot/structuredLogs-overrides.css new file mode 100644 index 000000000..1c75fa7c3 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Diagnostics.StructuredLogs.Blazor/wwwroot/structuredLogs-overrides.css @@ -0,0 +1,51 @@ +/* Overrides for the official structured-logs viewer: rows grow with the message up to a few + clamped lines instead of inflating the whole list (or forcing horizontal scrolling in auto + mode). Loaded after the official structuredLogs.css by the module page. */ + +.structured-logs-surface .structured-log-row { + width: auto; + grid-template-columns: 24px 18px 96px 42px minmax(120px, 190px) minmax(190px, 330px) minmax(120px, 220px) minmax(0, 1fr); +} + +.structured-logs-surface .structured-log-message { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 4; + overflow: hidden; + overflow-wrap: anywhere; + white-space: pre-wrap; +} + +.structured-log-exception { + max-height: 96px; + overflow: auto; +} + +@media (max-width: 1280px) { + .structured-logs-surface .structured-log-row { + grid-template-columns: 24px 18px 96px 42px minmax(120px, 190px) minmax(160px, 1fr); + } + + .structured-logs-surface .structured-log-source, + .structured-logs-surface .structured-log-category, + .structured-logs-surface .structured-log-correlation { + display: none; + } +} + +@media (max-width: 960px) { + .structured-logs-surface .structured-log-row { + grid-template-columns: 24px 18px 92px 40px minmax(160px, 1fr); + width: auto; + } + + .structured-logs-surface .structured-log-source, + .structured-logs-surface .structured-log-category, + .structured-logs-surface .structured-log-correlation { + display: none; + } + + .structured-log-exception { + grid-column: 5; + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Identity.Blazor/AbpElsaNextStudioIdentityBlazorModule.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Identity.Blazor/AbpElsaNextStudioIdentityBlazorModule.cs new file mode 100644 index 000000000..238bdd4dc --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Identity.Blazor/AbpElsaNextStudioIdentityBlazorModule.cs @@ -0,0 +1,17 @@ +using LINGYUN.Abp.ElsaNext.Studio.Blazor; +using LINGYUN.Abp.ElsaNext.Studio.Identity.Blazor.Extensions; +using Volo.Abp.Identity; +using Volo.Abp.Modularity; + +namespace LINGYUN.Abp.ElsaNext.Studio.Identity.Blazor; + +[DependsOn( + typeof(AbpIdentityDomainModule), + typeof(AbpElsaNextStudioBlazorModule))] +public class AbpElsaNextStudioIdentityBlazorModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + context.Services.AddIdentityUIHintHandlers(); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Identity.Blazor/Components/IdentityUserPicker.razor b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Identity.Blazor/Components/IdentityUserPicker.razor new file mode 100644 index 000000000..d1b2854cf --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Identity.Blazor/Components/IdentityUserPicker.razor @@ -0,0 +1,226 @@ +@using Elsa.Api.Client.Shared.UIHints.DropDown +@using Elsa.Studio.Models +@using LINGYUN.Abp.ElsaNext.Studio.Blazor.Components +@using System.Text.Json +@using Volo.Abp.Identity +@inject ILocalizer Localizer + +@inherits StudioComponentBase +@{ + var inputDescriptor = EditorContext.InputDescriptor; + var displayName = inputDescriptor.DisplayName; + var description = inputDescriptor.Description; +} + + + + + + + +@code { + private readonly List _selectedItems = []; + + [Parameter] public bool AllowMultiple { get; set; } + + [Parameter] public DisplayInputEditorContext EditorContext { get; set; } = null!; + + [Inject] protected IIdentityUserRepository IdentityUserRepository { get; set; } = null!; + [Inject] protected IDialogService DialogService { get; set; } = null!; + + private string SelectedText => string.Join(", ", _selectedItems.Select(x => x.Text)); + + protected override async Task OnInitializedAsync() + { + var selectedItems = await ResolveSelectedItemsAsync(); + + if (selectedItems.Count > 0) + { + await InvokeAsync(() => + { + _selectedItems.Clear(); + _selectedItems.AddRange(selectedItems); + }); + } + } + + private async Task OpenSelectDialogAsync() + { + if (EditorContext.IsReadOnly) + { + return; + } + + var inputDescriptor = EditorContext.InputDescriptor; + var parameters = new DialogParameters + { + { x => x.Title, Localizer[inputDescriptor.DisplayName ?? inputDescriptor.Name] }, + { x => x.AllowMultiple, AllowMultiple }, + { x => x.SelectedValue, _selectedItems.FirstOrDefault()?.Value }, + { x => x.SelectedValues, _selectedItems.Select(x => x.Value).ToArray() } + }; + var options = new DialogOptions + { + CloseOnEscapeKey = true, + FullWidth = true, + MaxWidth = MaxWidth.Medium + }; + + var dialog = await DialogService.ShowAsync(string.Empty, parameters, options); + var result = await dialog.Result; + + if (result is not { Canceled: false }) + { + return; + } + + if (result.Data is SelectListItem selectedItem) + { + await InvokeAsync(async () => + { + _selectedItems.Clear(); + _selectedItems.Add(selectedItem); + await EditorContext.UpdateValueOrLiteralExpressionAsync(selectedItem.Value); + }); + + return; + } + + if (result.Data is IReadOnlyList selectedItems) + { + await InvokeAsync(async () => + { + _selectedItems.Clear(); + _selectedItems.AddRange(selectedItems); + + await EditorContext.UpdateValueOrObjectExpressionAsync(_selectedItems.Select(x => x.Value).ToList()); + }); + } + } + + private MudTextFieldExIcon[] GetAdornmentIcons() => + [ + new() + { + Icon = Icons.Material.Filled.Search, + Tooltip = Localizer["Search"], + Disabled = EditorContext.IsReadOnly, + OnClick = OpenSelectDialogAsync + }, + new() + { + Icon = Icons.Material.Filled.Clear, + Tooltip = Localizer["Clear"], + Visible = _selectedItems.Count > 0, + Disabled = EditorContext.IsReadOnly, + OnClick = ClearAsync + } + ]; + + private async Task ClearAsync() + { + await InvokeAsync(async () => + { + _selectedItems.Clear(); + + if (AllowMultiple) + { + await EditorContext.UpdateValueOrObjectExpressionAsync(new List()); + } + else + { + await EditorContext.UpdateValueOrLiteralExpressionAsync(string.Empty); + } + }); + } + + private async Task> ResolveSelectedItemsAsync() + { + return AllowMultiple + ? await ResolveSelectedUsersAsync() + : await ResolveSelectedUserAsync(); + } + + private async Task> ResolveSelectedUserAsync() + { + var value = EditorContext.GetLiteralValueOrDefault(); + + if (string.IsNullOrWhiteSpace(value)) + { + return []; + } + + if (Guid.TryParse(value, out var userId)) + { + var user = await IdentityUserRepository.FindAsync(userId); + + if (user != null) + { + return [new SelectListItem(user.Name ?? user.UserName, user.Id.ToString())]; + } + } + + return [new SelectListItem(value, value)]; + } + + private async Task> ResolveSelectedUsersAsync() + { + var values = ParseSelectedValues(EditorContext.GetObjectValueOrDefault()); + + if (values.Count == 0) + { + return []; + } + + var userIds = values.Where(x => Guid.TryParse(x, out _)).Select(Guid.Parse).ToList(); + List users = []; + + if (userIds.Count > 0) + { + users = await IdentityUserRepository.GetListByIdsAsync(userIds); + } + + var names = users.ToDictionary(x => x.Id.ToString(), x => x.Name ?? x.UserName, StringComparer.OrdinalIgnoreCase); + + return values + .Select(value => new SelectListItem(names.TryGetValue(value, out var name) ? name : value, value)) + .ToList(); + } + + private static IReadOnlyList ParseSelectedValues(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return []; + } + + var trimmed = value.Trim(); + + try + { + if (trimmed.StartsWith('[')) + { + return JsonSerializer.Deserialize>(trimmed) ?? []; + } + + if (trimmed.StartsWith('"')) + { + var single = JsonSerializer.Deserialize(trimmed); + return string.IsNullOrWhiteSpace(single) ? [] : [single]; + } + } + catch (JsonException) + { + } + + return trimmed.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Identity.Blazor/Components/IdentityUserSelectDialog.razor b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Identity.Blazor/Components/IdentityUserSelectDialog.razor new file mode 100644 index 000000000..7af350a8a --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Identity.Blazor/Components/IdentityUserSelectDialog.razor @@ -0,0 +1,219 @@ +@using Elsa.Api.Client.Shared.UIHints.DropDown +@using System.Threading +@using Volo.Abp.Identity +@inject ILocalizer Localizer + +@implements IDisposable + + + + @Title + + + + + + + + @Localizer["Name"] + + + + @context.Text + + @if (AllowMultiple) + { + @if (_selectedItems.Contains(context)) + { + + } + } + else if (context.Value == SelectedValue) + { + + } + else + { + + @Localizer["Select"] + + } + + + + @Localizer["No results found"] + + + + + + @if (AllowMultiple) + { + @Localizer["Select"] + } + @Localizer["Cancel"] + + + +@code { + private const int DebounceIntervalMilliseconds = 350; + + private MudTable? _table; + private string? _searchText; + private HashSet _selectedItems = []; + private CancellationTokenSource? _debounceCancellationTokenSource; + + [CascadingParameter] private IMudDialogInstance MudDialog { get; set; } = null!; + + [Parameter] public string Title { get; set; } = string.Empty; + + [Parameter] public bool AllowMultiple { get; set; } + + [Parameter] public string? SelectedValue { get; set; } + + [Parameter] public IReadOnlyCollection SelectedValues { get; set; } = []; + + [Inject] protected IIdentityUserRepository IdentityUserRepository { get; set; } = null!; + + public void Dispose() + { + _debounceCancellationTokenSource?.Cancel(); + _debounceCancellationTokenSource?.Dispose(); + } + + private async Task OnSearchTextChangedAsync(string? searchValue) + { + _searchText = searchValue; + + _debounceCancellationTokenSource?.Cancel(); + _debounceCancellationTokenSource?.Dispose(); + _debounceCancellationTokenSource = new CancellationTokenSource(); + + try + { + await Task.Delay(DebounceIntervalMilliseconds, _debounceCancellationTokenSource.Token); + + var table = _table; + + if (table != null) + { + await InvokeAsync(() => table.ReloadServerData()); + } + } + catch (OperationCanceledException) + { + } + } + + private async Task> LoadUsersAsync(TableState state, CancellationToken cancellationToken) + { + var filter = string.IsNullOrWhiteSpace(_searchText) ? null : _searchText.Trim(); + + var users = await IdentityUserRepository.GetListAsync( + sorting: null, + maxResultCount: state.PageSize, + skipCount: state.Page * state.PageSize, + filter: filter, + cancellationToken: cancellationToken); + + var totalCount = await IdentityUserRepository.GetCountAsync( + filter: filter, + cancellationToken: cancellationToken); + + var items = users + .Select(user => new SelectListItem( + user.Name.IsNullOrWhiteSpace() ? user.UserName : user.Name, + user.Id.ToString())) + .ToList(); + + if (AllowMultiple && SelectedValues.Count > 0) + { + var selectedItems = new HashSet(_selectedItems); + + foreach (var item in items.Where(x => SelectedValues.Contains(x.Value))) + { + selectedItems.Add(item); + } + + _selectedItems = selectedItems; + } + + return new TableData + { + Items = items, + TotalItems = (int)totalCount + }; + } + + private void OnSelectedItemsChanged(HashSet selectedItems) + { + _selectedItems = selectedItems; + } + + private async Task OnRowClickAsync(TableRowClickEventArgs args) + { + if (!AllowMultiple) + { + await SelectAsync(args.Item); + return; + } + + ToggleSelection(args.Item); + } + + private void ToggleSelection(SelectListItem? item) + { + if (item == null) + { + return; + } + + var selectedItems = new HashSet(_selectedItems); + + if (!selectedItems.Remove(item)) + { + selectedItems.Add(item); + } + + _selectedItems = selectedItems; + } + + private Task ConfirmAsync() + { + MudDialog.Close(DialogResult.Ok>(_selectedItems.ToList())); + return Task.CompletedTask; + } + + private Task SelectAsync(SelectListItem? item) + { + if (item == null) + { + return Task.CompletedTask; + } + + MudDialog.Close(DialogResult.Ok(item)); + return Task.CompletedTask; + } + + private void CancelAsync() + { + MudDialog.Cancel(); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Identity.Blazor/Extensions/ServiceCollectionExtensions.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Identity.Blazor/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 000000000..ec079e1b0 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Identity.Blazor/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,13 @@ +using Elsa.Studio.Extensions; +using LINGYUN.Abp.ElsaNext.Studio.Identity.Blazor.Handlers; +using Microsoft.Extensions.DependencyInjection; + +namespace LINGYUN.Abp.ElsaNext.Studio.Identity.Blazor.Extensions; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddIdentityUIHintHandlers(this IServiceCollection services) + { + return services.AddUIHintHandler(); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Identity.Blazor/FodyWeavers.xml b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Identity.Blazor/FodyWeavers.xml new file mode 100644 index 000000000..5d6962159 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Identity.Blazor/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Identity.Blazor/FodyWeavers.xsd b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Identity.Blazor/FodyWeavers.xsd new file mode 100644 index 000000000..3f3946e28 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Identity.Blazor/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Identity.Blazor/Handlers/IdentityPickerHandler.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Identity.Blazor/Handlers/IdentityPickerHandler.cs new file mode 100644 index 000000000..5b2455122 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Identity.Blazor/Handlers/IdentityPickerHandler.cs @@ -0,0 +1,28 @@ +using Elsa.Studio; +using Elsa.Studio.Contracts; +using Elsa.Studio.Models; +using LINGYUN.Abp.ElsaNext.Studio.Identity.Blazor.Components; +using LINGYUN.Abp.ElsaNext.UIHints; +using Microsoft.AspNetCore.Components; + +namespace LINGYUN.Abp.ElsaNext.Studio.Identity.Blazor.Handlers; + +public class IdentityPickerHandler : IUIHintHandler +{ + public bool GetSupportsUIHint(string uiHint) => uiHint is AbpElsaUIHints.UserPicker or AbpElsaUIHints.UserPickerMultiple; + + public string UISyntax => WellKnownSyntaxNames.Object; + + public RenderFragment DisplayInputEditor(DisplayInputEditorContext context) + { + var allowMultiple = context.InputDescriptor.UIHint == AbpElsaUIHints.UserPickerMultiple; + + return builder => + { + builder.OpenComponent(0, typeof(IdentityUserPicker)); + builder.AddAttribute(1, nameof(IdentityUserPicker.EditorContext), context); + builder.AddAttribute(2, nameof(IdentityUserPicker.AllowMultiple), true); + builder.CloseComponent(); + }; + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Identity.Blazor/LINGYUN.Abp.ElsaNext.Studio.Identity.Blazor.csproj b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Identity.Blazor/LINGYUN.Abp.ElsaNext.Studio.Identity.Blazor.csproj new file mode 100644 index 000000000..5457ddac3 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Identity.Blazor/LINGYUN.Abp.ElsaNext.Studio.Identity.Blazor.csproj @@ -0,0 +1,23 @@ + + + + + + + net10.0 + LINGYUN.Abp.ElsaNext.Studio.Identity.Blazor + LINGYUN.Abp.ElsaNext.Studio.Identity.Blazor + false + false + false + + + + + + + + + + + diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Identity.Blazor/_Imports.razor b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Identity.Blazor/_Imports.razor new file mode 100644 index 000000000..1f6d130e9 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Identity.Blazor/_Imports.razor @@ -0,0 +1,5 @@ +@using Microsoft.AspNetCore.Components.Web +@using Elsa.Studio.Components +@using Elsa.Studio.Localization +@using MudBlazor +@using MudExtensions \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Labels.Blazor/AbpElsaNextStudioLabelsBlazorModule.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Labels.Blazor/AbpElsaNextStudioLabelsBlazorModule.cs new file mode 100644 index 000000000..c7412f3ef --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Labels.Blazor/AbpElsaNextStudioLabelsBlazorModule.cs @@ -0,0 +1,49 @@ +using Elsa.Studio.Labels; +using LINGYUN.Abp.ElsaNext.Labels; +using LINGYUN.Abp.ElsaNext.Localization; +using LINGYUN.Abp.ElsaNext.Studio.Blazor; +using LINGYUN.Abp.ElsaNext.Studio.Labels.Blazor.Navigation; +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.Labels.Blazor; + +[DependsOn( + typeof(AbpElsaNextLabelsModule), + typeof(AbpElsaNextStudioBlazorModule))] +public class AbpElsaNextStudioLabelsBlazorModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + var configuration = context.Configuration.GetSection("Elsa:Studio"); + + Configure(options => + { + options.AdditionalAssemblies.Add(typeof(Elsa.Studio.Labels.Feature).Assembly); + }); + + Configure(options => + { + options.MenuContributors.Add(new ElsaStudioLabelsMenuContributor()); + }); + + Configure(options => + { + options.FileSets.AddEmbedded("LINGYUN.Abp.ElsaNext.Studio.Labels.Blazor"); + }); + + Configure(options => + { + options.Resources + .Get() + .AddVirtualJson("/Localization/Resources/LabelsBlazor"); + }); + var elsaNextStudioBlazoeOptions = context.Services.ExecutePreConfiguredActions(); + + context.Services.AddLabelsModule(elsaNextStudioBlazoeOptions.BackendApiConfig); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Labels.Blazor/FodyWeavers.xml b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Labels.Blazor/FodyWeavers.xml new file mode 100644 index 000000000..5d6962159 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Labels.Blazor/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Labels.Blazor/FodyWeavers.xsd b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Labels.Blazor/FodyWeavers.xsd new file mode 100644 index 000000000..3f3946e28 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Labels.Blazor/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Labels.Blazor/LINGYUN.Abp.ElsaNext.Studio.Labels.Blazor.csproj b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Labels.Blazor/LINGYUN.Abp.ElsaNext.Studio.Labels.Blazor.csproj new file mode 100644 index 000000000..d330e39ce --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Labels.Blazor/LINGYUN.Abp.ElsaNext.Studio.Labels.Blazor.csproj @@ -0,0 +1,31 @@ + + + + + + + net10.0 + enable + enable + LINGYUN.Abp.ElsaNext.Studio.Labels.Blazor + LINGYUN.Abp.ElsaNext.Studio.Labels.Blazor + false + false + false + + + + + + + + + + + + + + + + + diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Labels.Blazor/Localization/Resources/LabelsBlazor/en.json b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Labels.Blazor/Localization/Resources/LabelsBlazor/en.json new file mode 100644 index 000000000..5df48ba44 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Labels.Blazor/Localization/Resources/LabelsBlazor/en.json @@ -0,0 +1,25 @@ +{ + "culture": "en", + "texts": { + "Menu:Labels": "Labels", + "Labels": "Labels", + "The name of the label.": "The name of the label.", + "The name of the new label.": "The name of the new label.", + "A description about the label.": "A description about the label.", + "A brief description of the label.": "A brief description of the label.", + "The color of the label.": "The color of the label.", + "No Labels found": "No Labels found", + "Label": "Label", + "Bulk actions": "Bulk actions", + "Delete": "Delete", + "Create Label": "Create Label", + "General": "General", + "Name": "Name", + "Description": "Description", + "Color": "Color", + "Loading": "Loading", + "Save": "Save", + "Cancel": "Cancel", + "Ok": "Ok" + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Labels.Blazor/Localization/Resources/LabelsBlazor/zh-Hans.json b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Labels.Blazor/Localization/Resources/LabelsBlazor/zh-Hans.json new file mode 100644 index 000000000..4682c89f1 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Labels.Blazor/Localization/Resources/LabelsBlazor/zh-Hans.json @@ -0,0 +1,25 @@ +{ + "culture": "zh-Hans", + "texts": { + "Menu:Labels": "标签", + "Labels": "标签", + "The name of the label.": "标签名称。", + "The name of the new label.": "新标签的名称。", + "A description about the label.": "标签说明。", + "A brief description of the label.": "标签的简要说明。", + "The color of the label.": "标签的颜色。", + "No Labels found": "未找到标签", + "Label": "标签", + "Bulk actions": "批量操作", + "Delete": "删除", + "Create Label": "创建标签", + "General": "通用", + "Name": "名称", + "Description": "说明", + "Color": "颜色", + "Loading": "加载中", + "Save": "保存", + "Cancel": "取消", + "Ok": "确定" + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Labels.Blazor/Navigation/ElsaStudioLabelsMenuContributor.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Labels.Blazor/Navigation/ElsaStudioLabelsMenuContributor.cs new file mode 100644 index 000000000..7edb090d1 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Labels.Blazor/Navigation/ElsaStudioLabelsMenuContributor.cs @@ -0,0 +1,27 @@ +using LINGYUN.Abp.ElsaNext.Localization; +using LINGYUN.Abp.ElsaNext.Studio.Blazor.Navigation; +using Volo.Abp.UI.Navigation; + +namespace LINGYUN.Abp.ElsaNext.Studio.Labels.Blazor.Navigation; + +public class ElsaStudioLabelsMenuContributor : IMenuContributor +{ + public Task ConfigureMenuAsync(MenuConfigurationContext context) + { + if (context.Menu.Name == StandardMenus.Main) + { + var menu = context.Menu; + var l = context.GetLocalizer(); + + var group = menu.GetMenuItemOrNull(ElsaStudioMenus.GroupName); + group?.AddItem(new ApplicationMenuItem( + ElsaStudioMenus.GroupName + ".Labels", + l["Menu:Labels"], + url: "/labels", + icon: "fa fa-tags", + order: 200)); + } + + return Task.CompletedTask; + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Labels.Blazor/_Imports.razor b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Labels.Blazor/_Imports.razor new file mode 100644 index 000000000..6a4064616 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Labels.Blazor/_Imports.razor @@ -0,0 +1,5 @@ +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Web +@using Elsa.Studio.Components +@using Elsa.Studio.Localization +@using MudBlazor diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Notifications.Blazor/AbpElsaNextStudioNotificationsBlazorModule.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Notifications.Blazor/AbpElsaNextStudioNotificationsBlazorModule.cs new file mode 100644 index 000000000..1949e64e2 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Notifications.Blazor/AbpElsaNextStudioNotificationsBlazorModule.cs @@ -0,0 +1,17 @@ +using LINGYUN.Abp.ElsaNext.Notifications; +using LINGYUN.Abp.ElsaNext.Studio.Blazor; +using LINGYUN.Abp.ElsaNext.Studio.Notifications.Blazor.Extensions; +using Volo.Abp.Modularity; + +namespace LINGYUN.Abp.ElsaNext.Studio.Notifications.Blazor; + +[DependsOn( + typeof(AbpElsaNextNotificationsModule), + typeof(AbpElsaNextStudioBlazorModule))] +public class AbpElsaNextStudioNotificationsBlazorModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + context.Services.AddNotificationsUIHintHandlers(); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Notifications.Blazor/Components/NotificationPicker.razor b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Notifications.Blazor/Components/NotificationPicker.razor new file mode 100644 index 000000000..c6dda794c --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Notifications.Blazor/Components/NotificationPicker.razor @@ -0,0 +1,23 @@ +@using LINGYUN.Abp.ElsaNext.Studio.Blazor.Components +@inject ILocalizer Localizer + +@inherits StudioComponentBase +@{ + var inputDescriptor = EditorContext.InputDescriptor; + var displayName = inputDescriptor.DisplayName; + var description = inputDescriptor.Description; +} + + + + + + \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Notifications.Blazor/Components/NotificationPicker.razor.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Notifications.Blazor/Components/NotificationPicker.razor.cs new file mode 100644 index 000000000..db12314b4 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Notifications.Blazor/Components/NotificationPicker.razor.cs @@ -0,0 +1,110 @@ +using Elsa.Api.Client.Shared.UIHints.DropDown; +using Elsa.Studio.Models; +using LINGYUN.Abp.ElsaNext.Studio.Blazor.Components; +using LINGYUN.Abp.Notifications; +using Microsoft.AspNetCore.Components; +using Microsoft.Extensions.Localization; +using MudBlazor; +using System.Threading.Tasks; + +namespace LINGYUN.Abp.ElsaNext.Studio.Notifications.Blazor.Components; + +public partial class NotificationPicker +{ + private SelectListItem? _selectedItem; + + [Parameter] public DisplayInputEditorContext EditorContext { get; set; } = null!; + + [Inject] protected IDialogService DialogService { get; set; } = null!; + [Inject] private IStringLocalizerFactory StringLocalizerFactory { get; set; } = null!; + [Inject] private INotificationDefinitionManager NotificationDefinitionManager { get; set; } = null!; + + protected async override Task OnInitializedAsync() + { + var selectedItem = await ResolveSelectedItemAsync(); + + if (selectedItem != null) + { + await InvokeAsync(() => _selectedItem = selectedItem); + } + } + + private async Task ResolveSelectedItemAsync() + { + var value = EditorContext.GetLiteralValueOrDefault(); + + if (string.IsNullOrWhiteSpace(value)) + { + return null; + } + + var notificationDefine = await NotificationDefinitionManager.GetOrNullAsync(value); + if (notificationDefine != null) + { + return new SelectListItem(notificationDefine.DisplayName.Localize(StringLocalizerFactory), notificationDefine.Name); + } + + return new SelectListItem(value, value); + } + + private async Task OpenSelectDialogAsync() + { + if (EditorContext.IsReadOnly) + { + return; + } + + var inputDescriptor = EditorContext.InputDescriptor; + var parameters = new DialogParameters + { + { x => x.Title, Localizer[inputDescriptor.DisplayName ?? inputDescriptor.Name] }, + { x => x.SelectedValue, _selectedItem?.Value } + }; + var options = new DialogOptions + { + CloseOnEscapeKey = true, + FullWidth = true, + MaxWidth = MaxWidth.Medium + }; + + var dialog = await DialogService.ShowAsync(string.Empty, parameters, options); + var result = await dialog.Result; + + if (result is { Canceled: false, Data: SelectListItem item }) + { + await InvokeAsync(async () => + { + _selectedItem = item; + await EditorContext.UpdateValueOrLiteralExpressionAsync(item.Value); + }); + } + } + + private MudTextFieldExIcon[] GetAdornmentIcons() => + [ + new() + { + Icon = Icons.Material.Filled.Search, + Tooltip = Localizer["Search"], + Disabled = EditorContext.IsReadOnly, + OnClick = OpenSelectDialogAsync + }, + new() + { + Icon = Icons.Material.Filled.Clear, + Tooltip = Localizer["Clear"], + Visible = _selectedItem != null, + Disabled = EditorContext.IsReadOnly, + OnClick = ClearAsync + } + ]; + + private async Task ClearAsync() + { + await InvokeAsync(async () => + { + _selectedItem = null; + await EditorContext.UpdateValueOrLiteralExpressionAsync(string.Empty); + }); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Notifications.Blazor/Components/NotificationSelectDialog.razor b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Notifications.Blazor/Components/NotificationSelectDialog.razor new file mode 100644 index 000000000..bd87ddcbb --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Notifications.Blazor/Components/NotificationSelectDialog.razor @@ -0,0 +1,155 @@ +@using System.Threading +@using Elsa.Api.Client.Shared.UIHints.DropDown +@using LINGYUN.Abp.Notifications +@using Microsoft.Extensions.Localization +@inject ILocalizer Localizer + +@implements IDisposable + + + + @Title + + + + + + + + @Localizer["Name"] + + + + @context.Text + + @if (context.Value == SelectedValue) + { + + } + else + { + + @Localizer["Select"] + + } + + + + + + + @Localizer["No results found"] + + + + + + @Localizer["Cancel"] + + + +@code { + private const int DebounceIntervalMilliseconds = 350; + + private MudTable? _table; + private string? _searchText; + private CancellationTokenSource? _debounceCancellationTokenSource; + + [CascadingParameter] private IMudDialogInstance MudDialog { get; set; } = null!; + + [Parameter] public string Title { get; set; } = string.Empty; + + [Parameter] public string? SelectedValue { get; set; } + + [Inject] private IStringLocalizerFactory StringLocalizerFactory { get; set; } = null!; + [Inject] private INotificationDefinitionManager NotificationDefinitionManager { get; set; } = null!; + + public void Dispose() + { + _debounceCancellationTokenSource?.Cancel(); + _debounceCancellationTokenSource?.Dispose(); + } + + private async Task OnSearchTextChangedAsync(string? searchValue) + { + _searchText = searchValue; + + _debounceCancellationTokenSource?.Cancel(); + _debounceCancellationTokenSource?.Dispose(); + _debounceCancellationTokenSource = new CancellationTokenSource(); + + try + { + await Task.Delay(DebounceIntervalMilliseconds, _debounceCancellationTokenSource.Token); + + var table = _table; + + if (table != null) + { + await InvokeAsync(() => table.ReloadServerData()); + } + } + catch (OperationCanceledException) + { + } + } + + private async Task> LoadTenantsAsync(TableState state, CancellationToken cancellationToken) + { + var filter = string.IsNullOrWhiteSpace(_searchText) ? null : _searchText.Trim(); + + var notifications = await NotificationDefinitionManager.GetNotificationsAsync(); + + var items = notifications + .Select(notification => new SelectListItem( + notification.DisplayName.Localize(StringLocalizerFactory), + notification.Name)) + .WhereIf(!string.IsNullOrWhiteSpace(filter), x => x.Text.Contains(filter!) || x.Value.Contains(filter!)) + .OrderBy(x => x.Text) + .Skip(state.Page * state.PageSize) + .Take(state.PageSize) + .ToList(); + + return new TableData + { + Items = items, + TotalItems = notifications.Count, + }; + } + + private async Task OnRowClickAsync(TableRowClickEventArgs args) + { + await SelectAsync(args.Item); + } + + private Task SelectAsync(SelectListItem? item) + { + if (item == null) + { + return Task.CompletedTask; + } + + MudDialog.Close(DialogResult.Ok(item)); + return Task.CompletedTask; + } + + private void CancelAsync() + { + MudDialog.Cancel(); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Notifications.Blazor/Extensions/ServiceCollectionExtensions.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Notifications.Blazor/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 000000000..dc948297f --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Notifications.Blazor/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,13 @@ +using Elsa.Studio.Extensions; +using LINGYUN.Abp.ElsaNext.Studio.Notifications.Blazor.Handlers; +using Microsoft.Extensions.DependencyInjection; + +namespace LINGYUN.Abp.ElsaNext.Studio.Notifications.Blazor.Extensions; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddNotificationsUIHintHandlers(this IServiceCollection services) + { + return services.AddUIHintHandler(); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Notifications.Blazor/FodyWeavers.xml b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Notifications.Blazor/FodyWeavers.xml new file mode 100644 index 000000000..5d6962159 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Notifications.Blazor/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Notifications.Blazor/FodyWeavers.xsd b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Notifications.Blazor/FodyWeavers.xsd new file mode 100644 index 000000000..3f3946e28 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Notifications.Blazor/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Notifications.Blazor/Handlers/NotificationPickerHandler.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Notifications.Blazor/Handlers/NotificationPickerHandler.cs new file mode 100644 index 000000000..a9f560008 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Notifications.Blazor/Handlers/NotificationPickerHandler.cs @@ -0,0 +1,25 @@ +using Elsa.Studio; +using Elsa.Studio.Contracts; +using Elsa.Studio.Models; +using LINGYUN.Abp.ElsaNext.Notifications.UIHints; +using LINGYUN.Abp.ElsaNext.Studio.Notifications.Blazor.Components; +using Microsoft.AspNetCore.Components; + +namespace LINGYUN.Abp.ElsaNext.Studio.Notifications.Blazor.Handlers; + +public class NotificationPickerHandler : IUIHintHandler +{ + public bool GetSupportsUIHint(string uiHint) => uiHint is NotificationsUIHints.NotificationPicker; + + public string UISyntax => WellKnownSyntaxNames.Object; + + public RenderFragment DisplayInputEditor(DisplayInputEditorContext context) + { + return builder => + { + builder.OpenComponent(0, typeof(NotificationPicker)); + builder.AddAttribute(1, nameof(NotificationPicker.EditorContext), context); + builder.CloseComponent(); + }; + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Notifications.Blazor/LINGYUN.Abp.ElsaNext.Studio.Notifications.Blazor.csproj b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Notifications.Blazor/LINGYUN.Abp.ElsaNext.Studio.Notifications.Blazor.csproj new file mode 100644 index 000000000..d19187dd3 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Notifications.Blazor/LINGYUN.Abp.ElsaNext.Studio.Notifications.Blazor.csproj @@ -0,0 +1,20 @@ + + + + + + + net10.0 + LINGYUN.Abp.ElsaNext.Studio.Notifications.Blazor + LINGYUN.Abp.ElsaNext.Studio.Notifications.Blazor + false + false + false + + + + + + + + diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Notifications.Blazor/_Imports.razor b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Notifications.Blazor/_Imports.razor new file mode 100644 index 000000000..1f6d130e9 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Notifications.Blazor/_Imports.razor @@ -0,0 +1,5 @@ +@using Microsoft.AspNetCore.Components.Web +@using Elsa.Studio.Components +@using Elsa.Studio.Localization +@using MudBlazor +@using MudExtensions \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Saas.Blazor/AbpElsaNextStudioSaasBlazorModule.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Saas.Blazor/AbpElsaNextStudioSaasBlazorModule.cs new file mode 100644 index 000000000..b0a02a7ae --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Saas.Blazor/AbpElsaNextStudioSaasBlazorModule.cs @@ -0,0 +1,17 @@ +using LINGYUN.Abp.ElsaNext.Studio.Blazor; +using LINGYUN.Abp.ElsaNext.Studio.Saas.Blazor.Extensions; +using LINGYUN.Abp.Saas; +using Volo.Abp.Modularity; + +namespace LINGYUN.Abp.ElsaNext.Studio.Saas.Blazor; + +[DependsOn( + typeof(AbpSaasDomainModule), + typeof(AbpElsaNextStudioBlazorModule))] +public class AbpElsaNextStudioSaasBlazorModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + context.Services.AddSaasUIHintHandlers(); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Saas.Blazor/Components/TenantPicker.razor b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Saas.Blazor/Components/TenantPicker.razor new file mode 100644 index 000000000..7fe21fe08 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Saas.Blazor/Components/TenantPicker.razor @@ -0,0 +1,128 @@ +@using Elsa.Api.Client.Shared.UIHints.DropDown +@using Elsa.Studio.Models +@using LINGYUN.Abp.ElsaNext.Studio.Blazor.Components +@using LINGYUN.Abp.Saas.Tenants +@inject ILocalizer Localizer + +@inherits StudioComponentBase +@{ + var inputDescriptor = EditorContext.InputDescriptor; + var displayName = inputDescriptor.DisplayName; + var description = inputDescriptor.Description; +} + + + + + + + +@code { + private SelectListItem? _selectedItem; + + [Parameter] public DisplayInputEditorContext EditorContext { get; set; } = null!; + + [Inject] protected ITenantRepository TenantRepository { get; set; } = null!; + [Inject] protected IDialogService DialogService { get; set; } = null!; + + protected override async Task OnInitializedAsync() + { + var selectedItem = await ResolveSelectedItemAsync(); + + if (selectedItem != null) + { + await InvokeAsync(() => _selectedItem = selectedItem); + } + } + + private async Task OpenSelectDialogAsync() + { + if (EditorContext.IsReadOnly) + { + return; + } + + var inputDescriptor = EditorContext.InputDescriptor; + var parameters = new DialogParameters + { + { x => x.Title, Localizer[inputDescriptor.DisplayName ?? inputDescriptor.Name] }, + { x => x.SelectedValue, _selectedItem?.Value } + }; + var options = new DialogOptions + { + CloseOnEscapeKey = true, + FullWidth = true, + MaxWidth = MaxWidth.Medium + }; + + var dialog = await DialogService.ShowAsync(string.Empty, parameters, options); + var result = await dialog.Result; + + if (result is { Canceled: false, Data: SelectListItem item }) + { + await InvokeAsync(async () => + { + _selectedItem = item; + await EditorContext.UpdateValueOrLiteralExpressionAsync(item.Value); + }); + } + } + + private MudTextFieldExIcon[] GetAdornmentIcons() => + [ + new() + { + Icon = Icons.Material.Filled.Search, + Tooltip = Localizer["Search"], + Disabled = EditorContext.IsReadOnly, + OnClick = OpenSelectDialogAsync + }, + new() + { + Icon = Icons.Material.Filled.Clear, + Tooltip = Localizer["Clear"], + Visible = _selectedItem != null, + Disabled = EditorContext.IsReadOnly, + OnClick = ClearAsync + } + ]; + + private async Task ClearAsync() + { + await InvokeAsync(async () => + { + _selectedItem = null; + await EditorContext.UpdateValueOrLiteralExpressionAsync(string.Empty); + }); + } + + private async Task ResolveSelectedItemAsync() + { + var value = EditorContext.GetLiteralValueOrDefault(); + + if (string.IsNullOrWhiteSpace(value)) + { + return null; + } + + if (Guid.TryParse(value, out var tenantId)) + { + var tenant = await TenantRepository.FindAsync(tenantId); + + if (tenant != null) + { + return new SelectListItem(tenant.Name, tenant.Id.ToString()); + } + } + + return new SelectListItem(value, value); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Saas.Blazor/Components/TenantSelectDialog.razor b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Saas.Blazor/Components/TenantSelectDialog.razor new file mode 100644 index 000000000..253f7040f --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Saas.Blazor/Components/TenantSelectDialog.razor @@ -0,0 +1,151 @@ +@using Elsa.Api.Client.Shared.UIHints.DropDown +@using System.Threading +@using LINGYUN.Abp.Saas.Tenants +@inject ILocalizer Localizer + +@implements IDisposable + + + + @Title + + + + + + + + @Localizer["Name"] + + + + @context.Text + + @if (context.Value == SelectedValue) + { + + } + else + { + + @Localizer["Select"] + + } + + + + @Localizer["No results found"] + + + + + + @Localizer["Cancel"] + + + +@code { + private const int DebounceIntervalMilliseconds = 350; + + private MudTable? _table; + private string? _searchText; + private CancellationTokenSource? _debounceCancellationTokenSource; + + [CascadingParameter] private IMudDialogInstance MudDialog { get; set; } = null!; + + [Parameter] public string Title { get; set; } = string.Empty; + + [Parameter] public string? SelectedValue { get; set; } + + [Inject] protected ITenantRepository TenantRepository { get; set; } = null!; + + public void Dispose() + { + _debounceCancellationTokenSource?.Cancel(); + _debounceCancellationTokenSource?.Dispose(); + } + + private async Task OnSearchTextChangedAsync(string? searchValue) + { + _searchText = searchValue; + + _debounceCancellationTokenSource?.Cancel(); + _debounceCancellationTokenSource?.Dispose(); + _debounceCancellationTokenSource = new CancellationTokenSource(); + + try + { + await Task.Delay(DebounceIntervalMilliseconds, _debounceCancellationTokenSource.Token); + + var table = _table; + + if (table != null) + { + await InvokeAsync(() => table.ReloadServerData()); + } + } + catch (OperationCanceledException) + { + } + } + + private async Task> LoadTenantsAsync(TableState state, CancellationToken cancellationToken) + { + var filter = string.IsNullOrWhiteSpace(_searchText) ? null : _searchText.Trim(); + + var tenants = await TenantRepository.GetListAsync( + filter: filter, + maxResultCount: state.PageSize, + skipCount: state.Page * state.PageSize, + cancellationToken: cancellationToken); + + var totalCount = await TenantRepository.GetCountAsync( + filter: filter, + cancellationToken: cancellationToken); + + var items = tenants + .Select(tenant => new SelectListItem(tenant.Name, tenant.Id.ToString())) + .ToList(); + + return new TableData + { + Items = items, + TotalItems = (int)totalCount + }; + } + + private async Task OnRowClickAsync(TableRowClickEventArgs args) + { + await SelectAsync(args.Item); + } + + private Task SelectAsync(SelectListItem? item) + { + if (item == null) + { + return Task.CompletedTask; + } + + MudDialog.Close(DialogResult.Ok(item)); + return Task.CompletedTask; + } + + private void CancelAsync() + { + MudDialog.Cancel(); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Saas.Blazor/Extensions/ServiceCollectionExtensions.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Saas.Blazor/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 000000000..18a820d87 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Saas.Blazor/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,13 @@ +using Elsa.Studio.Extensions; +using LINGYUN.Abp.ElsaNext.Studio.Saas.Blazor.Handlers; +using Microsoft.Extensions.DependencyInjection; + +namespace LINGYUN.Abp.ElsaNext.Studio.Saas.Blazor.Extensions; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddSaasUIHintHandlers(this IServiceCollection services) + { + return services.AddUIHintHandler(); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Saas.Blazor/FodyWeavers.xml b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Saas.Blazor/FodyWeavers.xml new file mode 100644 index 000000000..5d6962159 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Saas.Blazor/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Saas.Blazor/FodyWeavers.xsd b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Saas.Blazor/FodyWeavers.xsd new file mode 100644 index 000000000..3f3946e28 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Saas.Blazor/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Saas.Blazor/Handlers/TenantPickerHandler.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Saas.Blazor/Handlers/TenantPickerHandler.cs new file mode 100644 index 000000000..e3859ec2d --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Saas.Blazor/Handlers/TenantPickerHandler.cs @@ -0,0 +1,25 @@ +using Elsa.Studio; +using Elsa.Studio.Contracts; +using Elsa.Studio.Models; +using LINGYUN.Abp.ElsaNext.Studio.Saas.Blazor.Components; +using LINGYUN.Abp.ElsaNext.UIHints; +using Microsoft.AspNetCore.Components; + +namespace LINGYUN.Abp.ElsaNext.Studio.Saas.Blazor.Handlers; + +public class TenantPickerHandler : IUIHintHandler +{ + public bool GetSupportsUIHint(string uiHint) => uiHint is AbpElsaUIHints.TenantPicker; + + public string UISyntax => WellKnownSyntaxNames.Object; + + public RenderFragment DisplayInputEditor(DisplayInputEditorContext context) + { + return builder => + { + builder.OpenComponent(0, typeof(TenantPicker)); + builder.AddAttribute(1, nameof(TenantPicker.EditorContext), context); + builder.CloseComponent(); + }; + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Saas.Blazor/LINGYUN.Abp.ElsaNext.Studio.Saas.Blazor.csproj b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Saas.Blazor/LINGYUN.Abp.ElsaNext.Studio.Saas.Blazor.csproj new file mode 100644 index 000000000..e973fa786 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Saas.Blazor/LINGYUN.Abp.ElsaNext.Studio.Saas.Blazor.csproj @@ -0,0 +1,20 @@ + + + + + + + net10.0 + LINGYUN.Abp.ElsaNext.Studio.Saas.Blazor + LINGYUN.Abp.ElsaNext.Studio.Saas.Blazor + false + false + false + + + + + + + + diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Saas.Blazor/_Imports.razor b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Saas.Blazor/_Imports.razor new file mode 100644 index 000000000..1f6d130e9 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Saas.Blazor/_Imports.razor @@ -0,0 +1,5 @@ +@using Microsoft.AspNetCore.Components.Web +@using Elsa.Studio.Components +@using Elsa.Studio.Localization +@using MudBlazor +@using MudExtensions \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Webhooks.Blazor/AbpElsaNextStudioWebhooksBlazorModule.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Webhooks.Blazor/AbpElsaNextStudioWebhooksBlazorModule.cs new file mode 100644 index 000000000..afff5a6ef --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Webhooks.Blazor/AbpElsaNextStudioWebhooksBlazorModule.cs @@ -0,0 +1,17 @@ +using LINGYUN.Abp.ElsaNext.Studio.Blazor; +using LINGYUN.Abp.ElsaNext.Studio.Webhooks.Blazor.Extensions; +using LINGYUN.Abp.ElsaNext.Webhooks; +using Volo.Abp.Modularity; + +namespace LINGYUN.Abp.ElsaNext.Studio.Webhooks.Blazor; + +[DependsOn( + typeof(AbpElsaNextWebhooksModule), + typeof(AbpElsaNextStudioBlazorModule))] +public class AbpElsaNextStudioWebhooksBlazorModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + context.Services.AddWebhooksUIHintHandlers(); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Webhooks.Blazor/Components/WebhookPicker.razor b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Webhooks.Blazor/Components/WebhookPicker.razor new file mode 100644 index 000000000..66f5f0b5b --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Webhooks.Blazor/Components/WebhookPicker.razor @@ -0,0 +1,130 @@ +@using Elsa.Api.Client.Shared.UIHints.DropDown +@using Elsa.Studio.Models +@using LINGYUN.Abp.ElsaNext.Studio.Blazor.Components +@using LINGYUN.Abp.Webhooks +@using Microsoft.Extensions.Localization +@inject ILocalizer Localizer + +@inherits StudioComponentBase +@{ + var inputDescriptor = EditorContext.InputDescriptor; + var displayName = inputDescriptor.DisplayName; + var description = inputDescriptor.Description; +} + + + + + + + +@code { + private SelectListItem? _selectedItem; + + [Parameter] public DisplayInputEditorContext EditorContext { get; set; } = null!; + + [Inject] protected IDialogService DialogService { get; set; } = null!; + [Inject] private IStringLocalizerFactory StringLocalizerFactory { get; set; } = null!; + [Inject] private IWebhookDefinitionManager WebhookDefinitionManager { get; set; } = null!; + + protected async override Task OnInitializedAsync() + { + var selectedItem = await ResolveSelectedItemAsync(); + + if (selectedItem != null) + { + await InvokeAsync(() => _selectedItem = selectedItem); + } + } + + private async Task ResolveSelectedItemAsync() + { + var value = EditorContext.GetLiteralValueOrDefault(); + + if (string.IsNullOrWhiteSpace(value)) + { + return null; + } + + var webhookDefine = await WebhookDefinitionManager.GetOrNullAsync(value); + if (webhookDefine != null) + { + return new SelectListItem( + webhookDefine.DisplayName != null + ? webhookDefine.DisplayName.Localize(StringLocalizerFactory) + : webhookDefine.Name, + webhookDefine.Name); + } + + return new SelectListItem(value, value); + } + + private async Task OpenSelectDialogAsync() + { + if (EditorContext.IsReadOnly) + { + return; + } + + var inputDescriptor = EditorContext.InputDescriptor; + var parameters = new DialogParameters + { + { x => x.Title, Localizer[inputDescriptor.DisplayName ?? inputDescriptor.Name] }, + { x => x.SelectedValue, _selectedItem?.Value } + }; + var options = new DialogOptions + { + CloseOnEscapeKey = true, + FullWidth = true, + MaxWidth = MaxWidth.Medium + }; + + var dialog = await DialogService.ShowAsync(string.Empty, parameters, options); + var result = await dialog.Result; + + if (result is { Canceled: false, Data: SelectListItem item }) + { + await InvokeAsync(async () => + { + _selectedItem = item; + await EditorContext.UpdateValueOrLiteralExpressionAsync(item.Value); + }); + } + } + + private MudTextFieldExIcon[] GetAdornmentIcons() => + [ + new() + { + Icon = Icons.Material.Filled.Search, + Tooltip = Localizer["Search"], + Disabled = EditorContext.IsReadOnly, + OnClick = OpenSelectDialogAsync + }, + new() + { + Icon = Icons.Material.Filled.Clear, + Tooltip = Localizer["Clear"], + Visible = _selectedItem != null, + Disabled = EditorContext.IsReadOnly, + OnClick = ClearAsync + } + ]; + + private async Task ClearAsync() + { + await InvokeAsync(async () => + { + _selectedItem = null; + await EditorContext.UpdateValueOrLiteralExpressionAsync(string.Empty); + }); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Webhooks.Blazor/Components/WebhookSelectDialog.razor b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Webhooks.Blazor/Components/WebhookSelectDialog.razor new file mode 100644 index 000000000..7d3770a97 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Webhooks.Blazor/Components/WebhookSelectDialog.razor @@ -0,0 +1,156 @@ +@using System.Threading +@using Elsa.Api.Client.Shared.UIHints.DropDown +@using LINGYUN.Abp.Webhooks +@using Microsoft.Extensions.Localization +@inject ILocalizer Localizer + +@implements IDisposable + + + + @Title + + + + + + + + @Localizer["Name"] + + + + @context.Text + + @if (context.Value == SelectedValue) + { + + } + else + { + + @Localizer["Select"] + + } + + + + + + + @Localizer["No results found"] + + + + + + @Localizer["Cancel"] + + + +@code { + private const int DebounceIntervalMilliseconds = 350; + + private MudTable? _table; + private string? _searchText; + private CancellationTokenSource? _debounceCancellationTokenSource; + + [CascadingParameter] private IMudDialogInstance MudDialog { get; set; } = null!; + + [Parameter] public string Title { get; set; } = string.Empty; + + [Parameter] public string? SelectedValue { get; set; } + + [Inject] private IStringLocalizerFactory StringLocalizerFactory { get; set; } = null!; + [Inject] private IWebhookDefinitionManager WebhookDefinitionManager { get; set; } = null!; + + public void Dispose() + { + _debounceCancellationTokenSource?.Cancel(); + _debounceCancellationTokenSource?.Dispose(); + } + + private async Task OnSearchTextChangedAsync(string? searchValue) + { + _searchText = searchValue; + + _debounceCancellationTokenSource?.Cancel(); + _debounceCancellationTokenSource?.Dispose(); + _debounceCancellationTokenSource = new CancellationTokenSource(); + + try + { + await Task.Delay(DebounceIntervalMilliseconds, _debounceCancellationTokenSource.Token); + + var table = _table; + + if (table != null) + { + await InvokeAsync(() => table.ReloadServerData()); + } + } + catch (OperationCanceledException) + { + } + } + + private async Task> LoadTenantsAsync(TableState state, CancellationToken cancellationToken) + { + var filter = string.IsNullOrWhiteSpace(_searchText) ? null : _searchText.Trim(); + + var webhooks = await WebhookDefinitionManager.GetWebhooksAsync(); + + var items = webhooks + .Select(webhook => new SelectListItem( + webhook.DisplayName != null + ? webhook.DisplayName.Localize(StringLocalizerFactory) + : webhook.Name, + webhook.Name)) + .WhereIf(!string.IsNullOrWhiteSpace(filter), x => x.Text.Contains(filter!) || x.Value.Contains(filter!)) + .OrderBy(x => x.Text) + .Skip(state.Page * state.PageSize) + .Take(state.PageSize) + .ToList(); + + return new TableData + { + Items = items, + TotalItems = webhooks.Count, + }; + } + + private async Task OnRowClickAsync(TableRowClickEventArgs args) + { + await SelectAsync(args.Item); + } + + private Task SelectAsync(SelectListItem? item) + { + if (item == null) + { + return Task.CompletedTask; + } + + MudDialog.Close(DialogResult.Ok(item)); + return Task.CompletedTask; + } + + private void CancelAsync() + { + MudDialog.Cancel(); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Webhooks.Blazor/Extensions/ServiceCollectionExtensions.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Webhooks.Blazor/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 000000000..69b9a5395 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Webhooks.Blazor/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,13 @@ +using Elsa.Studio.Extensions; +using LINGYUN.Abp.ElsaNext.Studio.Webhooks.Blazor.Handlers; +using Microsoft.Extensions.DependencyInjection; + +namespace LINGYUN.Abp.ElsaNext.Studio.Webhooks.Blazor.Extensions; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddWebhooksUIHintHandlers(this IServiceCollection services) + { + return services.AddUIHintHandler(); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Webhooks.Blazor/FodyWeavers.xml b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Webhooks.Blazor/FodyWeavers.xml new file mode 100644 index 000000000..5d6962159 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Webhooks.Blazor/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Webhooks.Blazor/FodyWeavers.xsd b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Webhooks.Blazor/FodyWeavers.xsd new file mode 100644 index 000000000..3f3946e28 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Webhooks.Blazor/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Webhooks.Blazor/Handlers/WebhookPickerHandler.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Webhooks.Blazor/Handlers/WebhookPickerHandler.cs new file mode 100644 index 000000000..b48cb92ef --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Webhooks.Blazor/Handlers/WebhookPickerHandler.cs @@ -0,0 +1,25 @@ +using Elsa.Studio; +using Elsa.Studio.Contracts; +using Elsa.Studio.Models; +using LINGYUN.Abp.ElsaNext.Studio.Webhooks.Blazor.Components; +using LINGYUN.Abp.ElsaNext.Webhooks.UIHints; +using Microsoft.AspNetCore.Components; + +namespace LINGYUN.Abp.ElsaNext.Studio.Webhooks.Blazor.Handlers; + +public class WebhookPickerHandler : IUIHintHandler +{ + public bool GetSupportsUIHint(string uiHint) => uiHint is WebhooksUIHints.WebhookPicker; + + public string UISyntax => WellKnownSyntaxNames.Object; + + public RenderFragment DisplayInputEditor(DisplayInputEditorContext context) + { + return builder => + { + builder.OpenComponent(0, typeof(WebhookPicker)); + builder.AddAttribute(1, nameof(WebhookPicker.EditorContext), context); + builder.CloseComponent(); + }; + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Webhooks.Blazor/LINGYUN.Abp.ElsaNext.Studio.Webhooks.Blazor.csproj b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Webhooks.Blazor/LINGYUN.Abp.ElsaNext.Studio.Webhooks.Blazor.csproj new file mode 100644 index 000000000..8ae04f812 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Webhooks.Blazor/LINGYUN.Abp.ElsaNext.Studio.Webhooks.Blazor.csproj @@ -0,0 +1,20 @@ + + + + + + + net10.0 + LINGYUN.Abp.ElsaNext.Studio.Webhooks.Blazor + LINGYUN.Abp.ElsaNext.Studio.Webhooks.Blazor + false + false + false + + + + + + + + diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Webhooks.Blazor/_Imports.razor b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Webhooks.Blazor/_Imports.razor new file mode 100644 index 000000000..1f6d130e9 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Studio.Webhooks.Blazor/_Imports.razor @@ -0,0 +1,5 @@ +@using Microsoft.AspNetCore.Components.Web +@using Elsa.Studio.Components +@using Elsa.Studio.Localization +@using MudBlazor +@using MudExtensions \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Webhooks/FodyWeavers.xml b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Webhooks/FodyWeavers.xml new file mode 100644 index 000000000..5d6962159 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Webhooks/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Webhooks/FodyWeavers.xsd b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Webhooks/FodyWeavers.xsd new file mode 100644 index 000000000..3f3946e28 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Webhooks/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Webhooks/LINGYUN.Abp.ElsaNext.Webhooks.csproj b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Webhooks/LINGYUN.Abp.ElsaNext.Webhooks.csproj new file mode 100644 index 000000000..5b370bf52 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Webhooks/LINGYUN.Abp.ElsaNext.Webhooks.csproj @@ -0,0 +1,26 @@ + + + + + + + net10.0 + LINGYUN.Abp.ElsaNext.Webhooks + LINGYUN.Abp.ElsaNext.Webhooks + false + false + false + + + + + + + + + + + + + + diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Webhooks/LINGYUN/Abp/ElsaNext/Webhooks/AbpElsaNextWebhooksModule.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Webhooks/LINGYUN/Abp/ElsaNext/Webhooks/AbpElsaNextWebhooksModule.cs new file mode 100644 index 000000000..2ee5a3bf0 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Webhooks/LINGYUN/Abp/ElsaNext/Webhooks/AbpElsaNextWebhooksModule.cs @@ -0,0 +1,38 @@ +using Elsa.Features.Services; +using LINGYUN.Abp.ElsaNext.Localization; +using LINGYUN.Abp.ElsaNext.Webhooks.Extensions; +using LINGYUN.Abp.Webhooks; +using Volo.Abp.Localization; +using Volo.Abp.Modularity; +using Volo.Abp.VirtualFileSystem; + +namespace LINGYUN.Abp.ElsaNext.Webhooks; + +[DependsOn( + typeof(AbpWebhooksModule), + typeof(AbpElsaNextModule))] +public class AbpElsaNextWebhooksModule : AbpModule +{ + public override void PreConfigureServices(ServiceConfigurationContext context) + { + PreConfigure(elsa => + { + elsa.UseAbpWebhooks(); + }); + } + + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.FileSets.AddEmbedded(); + }); + + Configure(options => + { + options.Resources + .Get() + .AddVirtualJson("/LINGYUN/Abp/ElsaNext/Webhooks/Localization/Resources"); + }); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Webhooks/LINGYUN/Abp/ElsaNext/Webhooks/Activities/PublishWebhook.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Webhooks/LINGYUN/Abp/ElsaNext/Webhooks/Activities/PublishWebhook.cs new file mode 100644 index 000000000..42671e57f --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Webhooks/LINGYUN/Abp/ElsaNext/Webhooks/Activities/PublishWebhook.cs @@ -0,0 +1,60 @@ +using Elsa.Extensions; +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using Elsa.Workflows.Models; +using Elsa.Workflows.UIHints; +using LINGYUN.Abp.ElsaNext.Webhooks.UIHints; +using LINGYUN.Abp.Webhooks; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace LINGYUN.Abp.ElsaNext.Webhooks.Activities; + +[Activity("Elsa", "Webhooks", "Send webhooks to subscriptions.", Kind = ActivityKind.Task)] +public class PublishWebhook : CodeActivity +{ + [Input( + Description = "Unique name of the webhook.", + UIHint = WebhooksUIHints.WebhookPicker)] + public Input WebhookName { get; set; } = default!; + + [Input(Description = "Data to send.")] + public Input WebhookData { get; set; } = default!; + + [Input(Description = "If true, It sends the exact same data as the parameter to clients.")] + public Input SendExactSameData { get; set; } = default!; + + [Input(Description = "If true, webhook will only contain given headers. If false given headers will be added to predefined headers in subscription.")] + public Input UseOnlyGivenHeaders { get; set; } = default!; + + [Input( + Description = "That headers will be sent with the webhook.", + UIHint = InputUIHints.Dictionary)] + public Input> Headers { get; set; } = default!; + + protected async override ValueTask ExecuteAsync(ActivityExecutionContext context) + { + var webhookPublisher = context.GetRequiredService(); + + var webhookName = WebhookName.Get(context); + var webhookData = WebhookData.Get(context); + var sendExactSameData = SendExactSameData.GetOrDefault(context) ?? true; + WebhookHeader? webhookHeader = null; + var headers = Headers.GetOrDefault(context); + if (headers != null) + { + var useOnlyGivenHeaders = UseOnlyGivenHeaders.GetOrDefault(context) ?? false; + webhookHeader = new WebhookHeader + { + UseOnlyGivenHeaders = useOnlyGivenHeaders, + Headers = headers, + }; + } + + await webhookPublisher.PublishAsync( + webhookName, + webhookData, + sendExactSameData, + webhookHeader); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Webhooks/LINGYUN/Abp/ElsaNext/Webhooks/Extensions/ModuleExtensions.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Webhooks/LINGYUN/Abp/ElsaNext/Webhooks/Extensions/ModuleExtensions.cs new file mode 100644 index 000000000..c8b0eb992 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Webhooks/LINGYUN/Abp/ElsaNext/Webhooks/Extensions/ModuleExtensions.cs @@ -0,0 +1,14 @@ +using Elsa.Features.Services; +using LINGYUN.Abp.ElsaNext.Webhooks.Features; +using System; + +namespace LINGYUN.Abp.ElsaNext.Webhooks.Extensions; + +public static class ModuleExtensions +{ + public static IModule UseAbpWebhooks(this IModule configuration, Action? configure = null) + { + configuration.Configure(configure); + return configuration; + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Webhooks/LINGYUN/Abp/ElsaNext/Webhooks/Features/WebhooksFeature.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Webhooks/LINGYUN/Abp/ElsaNext/Webhooks/Features/WebhooksFeature.cs new file mode 100644 index 000000000..b5fc3a94e --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Webhooks/LINGYUN/Abp/ElsaNext/Webhooks/Features/WebhooksFeature.cs @@ -0,0 +1,17 @@ +using Elsa.Extensions; +using Elsa.Features.Abstractions; +using Elsa.Features.Services; + +namespace LINGYUN.Abp.ElsaNext.Webhooks.Features; + +public class WebhooksFeature : FeatureBase +{ + public WebhooksFeature(IModule module) : base(module) + { + } + + public override void Configure() + { + Module.AddActivitiesFrom(); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Webhooks/LINGYUN/Abp/ElsaNext/Webhooks/Localization/Resources/en.json b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Webhooks/LINGYUN/Abp/ElsaNext/Webhooks/Localization/Resources/en.json new file mode 100644 index 000000000..cdcb12d4e --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Webhooks/LINGYUN/Abp/ElsaNext/Webhooks/Localization/Resources/en.json @@ -0,0 +1,12 @@ +{ + "culture": "en", + "texts": { + "Webhooks": "Webhooks", + "Publish Webhook": "Publish Webhook", + "Send webhooks to subscriptions.": "Send webhooks to subscriptions.", + "Data to send.": "Data to send.", + "If true, It sends the exact same data as the parameter to clients.": "If true, It sends the exact same data as the parameter to clients.", + "If true, webhook will only contain given headers. If false given headers will be added to predefined headers in subscription.": "If true, webhook will only contain given headers. If false given headers will be added to predefined headers in subscription.", + "That headers will be sent with the webhook.": "That headers will be sent with the webhook." + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Webhooks/LINGYUN/Abp/ElsaNext/Webhooks/Localization/Resources/zh-Hans.json b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Webhooks/LINGYUN/Abp/ElsaNext/Webhooks/Localization/Resources/zh-Hans.json new file mode 100644 index 000000000..f22eb4e92 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Webhooks/LINGYUN/Abp/ElsaNext/Webhooks/Localization/Resources/zh-Hans.json @@ -0,0 +1,12 @@ +{ + "culture": "zh-Hans", + "texts": { + "Webhooks": "Webhooks", + "Publish Webhook": "发布Webhook", + "Send webhooks to subscriptions.": "向订阅方发布Webhook。", + "Data to send.": "要发送的数据。", + "If true, It sends the exact same data as the parameter to clients.": "如果为True,它会向客户端发送与参数完全相同的数据。", + "If true, webhook will only contain given headers. If false given headers will be added to predefined headers in subscription.": "如果为True,webhook 将仅包含指定的标头。如果为False,则在订阅中预定义的标头将添加给定的标头。", + "That headers will be sent with the webhook.": "将随 webhook 一起发送的请求头。" + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Webhooks/LINGYUN/Abp/ElsaNext/Webhooks/UIHints/WebhooksUIHints.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Webhooks/LINGYUN/Abp/ElsaNext/Webhooks/UIHints/WebhooksUIHints.cs new file mode 100644 index 000000000..ad30cb94e --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext.Webhooks/LINGYUN/Abp/ElsaNext/Webhooks/UIHints/WebhooksUIHints.cs @@ -0,0 +1,6 @@ +namespace LINGYUN.Abp.ElsaNext.Webhooks.UIHints; + +public static class WebhooksUIHints +{ + public const string WebhookPicker = "webhook-picker"; +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext/LINGYUN.Abp.ElsaNext.csproj b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext/LINGYUN.Abp.ElsaNext.csproj index 523c4ec8e..628740c84 100644 --- a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext/LINGYUN.Abp.ElsaNext.csproj +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext/LINGYUN.Abp.ElsaNext.csproj @@ -11,17 +11,19 @@ false false - Enable - + + + + + - diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext/LINGYUN/Abp/ElsaNext/AbpElsaNextAutoMapperProfile.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext/LINGYUN/Abp/ElsaNext/AbpElsaNextAutoMapperProfile.cs deleted file mode 100644 index f6cad9125..000000000 --- a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext/LINGYUN/Abp/ElsaNext/AbpElsaNextAutoMapperProfile.cs +++ /dev/null @@ -1,10 +0,0 @@ -using AutoMapper; - -namespace LINGYUN.Abp.ElsaNext; -public class AbpElsaNextAutoMapperProfile : Profile -{ - public AbpElsaNextAutoMapperProfile() - { - - } -} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext/LINGYUN/Abp/ElsaNext/AbpElsaNextModule.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext/LINGYUN/Abp/ElsaNext/AbpElsaNextModule.cs index 193c7b789..070ac3d87 100644 --- a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext/LINGYUN/Abp/ElsaNext/AbpElsaNextModule.cs +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext/LINGYUN/Abp/ElsaNext/AbpElsaNextModule.cs @@ -1,24 +1,21 @@ -using Elsa.Common.Features; -using Elsa.Common.Multitenancy; +using Elsa.Common.Multitenancy; using Elsa.Extensions; using Elsa.Features.Services; -using Elsa.Tenants.Extensions; using Elsa.Workflows; using LINGYUN.Abp.ElsaNext.Localization; using LINGYUN.Abp.ElsaNext.Multitenancy; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -using Volo.Abp.AutoMapper; using Volo.Abp.Features; using Volo.Abp.Json; using Volo.Abp.Localization; using Volo.Abp.Modularity; using Volo.Abp.Threading; +using Volo.Abp.VirtualFileSystem; namespace LINGYUN.Abp.ElsaNext; [DependsOn( - typeof(AbpAutoMapperModule), typeof(AbpFeaturesModule), typeof(AbpThreadingModule), typeof(AbpJsonModule))] @@ -34,19 +31,7 @@ public class AbpElsaNextModule : AbpModule { elsa .AddActivitiesFrom() - .AddWorkflowsFrom() - .UseTenants(tenants => - { - tenants.ConfigureMultitenancy(options => - { - options.TenantResolverPipelineBuilder.Append(); - }); - }); - - elsa.Configure(feature => - { - feature.UseTenantsProvider(); - }); + .AddWorkflowsFrom(); elsaModule.Configure(elsa); }); @@ -54,9 +39,16 @@ public class AbpElsaNextModule : AbpModule context.Services.Replace( ServiceDescriptor.Singleton()); + Configure(options => + { + options.FileSets.AddEmbedded(); + }); + Configure(options => { - options.Resources.Add("en"); + options.Resources + .Add("en") + .AddVirtualJson("/LINGYUN/Abp/ElsaNext/Localization/Resources"); }); } } diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext/LINGYUN/Abp/ElsaNext/Localization/Resources/en.json b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext/LINGYUN/Abp/ElsaNext/Localization/Resources/en.json new file mode 100644 index 000000000..ef9a34049 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext/LINGYUN/Abp/ElsaNext/Localization/Resources/en.json @@ -0,0 +1,14 @@ +{ + "culture": "en", + "texts": { + "Permission:Elsa": "Elsa Workflows", + "Permission:Create": "Create", + "Permission:Update": "Update", + "Permission:Delete": "Delete", + "Permission:Execute": "Execute", + "Permission:Publish": "Publish", + "Permission:Retract": "Retract", + "Permission:Refresh": "Refresh", + "Permission:Reload": "Reload" + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext/LINGYUN/Abp/ElsaNext/Localization/Resources/zh-Hans.json b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext/LINGYUN/Abp/ElsaNext/Localization/Resources/zh-Hans.json new file mode 100644 index 000000000..6b6ab1662 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext/LINGYUN/Abp/ElsaNext/Localization/Resources/zh-Hans.json @@ -0,0 +1,14 @@ +{ + "culture": "zh-Hans", + "texts": { + "Permission:Elsa": "Elsa工作流", + "Permission:Create": "新增", + "Permission:Update": "编辑", + "Permission:Delete": "删除", + "Permission:Execute": "触发", + "Permission:Publish": "发布", + "Permission:Retract": "撤销", + "Permission:Refresh": "刷新", + "Permission:Reload": "重载" + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext/LINGYUN/Abp/ElsaNext/Permissions/AbpElsaPermissionMapClaimsPrincipalContributor.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext/LINGYUN/Abp/ElsaNext/Permissions/AbpElsaPermissionMapClaimsPrincipalContributor.cs new file mode 100644 index 000000000..133848cdc --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext/LINGYUN/Abp/ElsaNext/Permissions/AbpElsaPermissionMapClaimsPrincipalContributor.cs @@ -0,0 +1,37 @@ +//using Microsoft.Extensions.DependencyInjection; +//using Microsoft.Extensions.Options; +//using System; +//using System.Linq; +//using System.Threading.Tasks; +//using Volo.Abp.Authorization.Permissions; +//using Volo.Abp.Security.Claims; + +//namespace LINGYUN.Abp.ElsaNext.Permissions; + +//public class AbpElsaPermissionMapClaimsPrincipalContributor : AbpDynamicClaimsPrincipalContributorBase +//{ +// private const string PermissionClaimType = "permissions"; +// public async override Task ContributeAsync(AbpClaimsPrincipalContributorContext context) +// { +// var claimsIdentity = context.ClaimsPrincipal.Identities.First(); +// if (!claimsIdentity.IsAuthenticated) +// { +// return; +// } + +// var options = context.ServiceProvider.GetRequiredService>(); +// var permissionChecker = context.ServiceProvider.GetRequiredService(); +// var permissionNames = options.Value.PermissionMaps.Select(x => x.Source).ToArray(); +// var checkResult = await permissionChecker.IsGrantedAsync(context.ClaimsPrincipal, permissionNames); + +// var grantPermissions = checkResult.Result.Where(x => x.Value == PermissionGrantResult.Granted).Select(x => x.Key); + +// // PermissionNames.ClaimType => permissions +// // Permission.ToString() => $"{Verb}{Separator}{Resource}"; +// var elsaPermissions = options.Value.PermissionMaps.Where(x => grantPermissions.Contains(x.Source)) +// .Select(x => new AbpDynamicClaim(PermissionClaimType, x.Target)) +// .ToList(); + +// await AddDynamicClaimsAsync(context, claimsIdentity, elsaPermissions); +// } +//} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext/LINGYUN/Abp/ElsaNext/Permissions/AbpElsaPermissionMapOptions.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext/LINGYUN/Abp/ElsaNext/Permissions/AbpElsaPermissionMapOptions.cs new file mode 100644 index 000000000..ae3cfb131 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext/LINGYUN/Abp/ElsaNext/Permissions/AbpElsaPermissionMapOptions.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using System.Linq; + +namespace LINGYUN.Abp.ElsaNext.Permissions; + +public class AbpElsaPermissionMapOptions +{ + public IList PermissionMaps { get; } + public AbpElsaPermissionMapOptions() + { + PermissionMaps = new List(); + } + + public AbpElsaPermissionMapOptions MapPermission(string source, string target) + { + if (!PermissionMaps.Any(x => x.Target == target)) + { + PermissionMaps.Add(new ElsaPermissionMapRecord(source, target)); + } + + return this; + } +} + +public class ElsaPermissionMapRecord +{ + /// + /// Abp Permission + /// + public string Source { get; } + /// + /// Elsa Permission + /// + public string Target { get; } + public ElsaPermissionMapRecord(string source, string target) + { + Source = source; + Target = target; + } +} \ No newline at end of file diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext/LINGYUN/Abp/ElsaNext/Permissions/ElsaPermissionDefinitionProvider.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext/LINGYUN/Abp/ElsaNext/Permissions/ElsaPermissionDefinitionProvider.cs new file mode 100644 index 000000000..a27d6efb8 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext/LINGYUN/Abp/ElsaNext/Permissions/ElsaPermissionDefinitionProvider.cs @@ -0,0 +1,20 @@ +using LINGYUN.Abp.ElsaNext.Localization; +using Volo.Abp.Authorization.Permissions; +using Volo.Abp.Localization; + +namespace LINGYUN.Abp.ElsaNext.Permissions; + +public class ElsaPermissionDefinitionProvider : PermissionDefinitionProvider +{ + public override void Define(IPermissionDefinitionContext context) + { + context.AddGroup( + ElsaPermissionNames.GroupName, + L("Permission:Elsa")); + } + + private static LocalizableString L(string name) + { + return LocalizableString.Create(name); + } +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext/LINGYUN/Abp/ElsaNext/Permissions/ElsaPermissionNames.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext/LINGYUN/Abp/ElsaNext/Permissions/ElsaPermissionNames.cs new file mode 100644 index 000000000..3d057e017 --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext/LINGYUN/Abp/ElsaNext/Permissions/ElsaPermissionNames.cs @@ -0,0 +1,6 @@ +namespace LINGYUN.Abp.ElsaNext.Permissions; + +public static class ElsaPermissionNames +{ + public const string GroupName = "AbpElsaNext"; +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext/LINGYUN/Abp/ElsaNext/Permissions/ElsaPermissionVerbs.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext/LINGYUN/Abp/ElsaNext/Permissions/ElsaPermissionVerbs.cs new file mode 100644 index 000000000..870771e6c --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext/LINGYUN/Abp/ElsaNext/Permissions/ElsaPermissionVerbs.cs @@ -0,0 +1,15 @@ +namespace LINGYUN.Abp.ElsaNext.Permissions; + +public static class ElsaPermissionVerbs +{ + public const string Write = "write"; + public const string Delete = "delete"; + public const string Execute = "exec"; + public const string Read = "read"; + public const string Publish = "publish"; + public const string Retract = "retract"; + public const string Actions = "actions"; + public const string Refresh = "refresh"; + public const string Reload = "reload"; + public const string Cancel = "cancel"; +} diff --git a/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext/LINGYUN/Abp/ElsaNext/UIHints/AbpElsaUIHints.cs b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext/LINGYUN/Abp/ElsaNext/UIHints/AbpElsaUIHints.cs new file mode 100644 index 000000000..bb7999e3f --- /dev/null +++ b/aspnet-core/modules/elsa/LINGYUN.Abp.ElsaNext/LINGYUN/Abp/ElsaNext/UIHints/AbpElsaUIHints.cs @@ -0,0 +1,8 @@ +namespace LINGYUN.Abp.ElsaNext.UIHints; + +public static class AbpElsaUIHints +{ + public const string TenantPicker = "tenant-picker"; + public const string UserPicker = "user-picker"; + public const string UserPickerMultiple = "user-picker-multiple"; +}