Skip to content
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
5327511
Add auth loading screen and logout button to app bar
FransVanEk Jan 21, 2026
5a5f145
Improve Blazor WASM loading screen and startup logic
FransVanEk Jan 21, 2026
0606d2d
Improve Blazor WASM loading screen and startup logic
FransVanEk Jan 21, 2026
ec62ae8
Change @keyframes to @@keyframes in _Host.cshtml
FransVanEk Jan 21, 2026
8a8ffc8
Initial plan
Copilot Jan 22, 2026
f402b1b
Create Elsa.Studio.Hosting framework with reusable components
Copilot Jan 22, 2026
c961097
Add JavaScript initialization file to Hosting project
Copilot Jan 22, 2026
30d565f
Add reusable hosting components to Elsa.Studio.Shared
Copilot Jan 22, 2026
459f33a
Add documentation and fix CSHTML host files to use inline HTML with s…
Copilot Jan 22, 2026
4127240
Add implementation summary document
Copilot Jan 22, 2026
b41be70
Create complete single-script loaders for minimal host integration
Copilot Jan 22, 2026
44f7261
Update implementation summary with complete single-script loader details
Copilot Jan 22, 2026
73d42e8
Refine loading screen logic for Blazor Server & WASM
FransVanEk Jan 22, 2026
c34852a
Merge branch 'main' into feature/remove-rendering-before--login
sfmskywalker Jan 27, 2026
b10dc3c
Refactor script loading to ensure Monaco loads first
FransVanEk Jan 28, 2026
3a01ef0
Merge branch 'feature/remove-rendering-before--login' of https://gith…
FransVanEk Jan 28, 2026
645b289
Refactor loaders, improve startup and resilience
FransVanEk Jan 29, 2026
198ae84
Smarter unauthorized handling in MainLayout
FransVanEk Jan 29, 2026
ddf4b98
Unified logout service & login page performance optimizations
FransVanEk Feb 2, 2026
bf93b74
Configurable branding for login page via appsettings
FransVanEk Feb 2, 2026
897a8a1
Refactor logout and branding to use config-driven values
FransVanEk Feb 2, 2026
bc5cb5f
Merge branch 'main' into feature/remove-rendering-before--login
sfmskywalker Feb 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Elsa.Studio.sln
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 18
VisualStudioVersion = 18.2.11408.102 d18.0
VisualStudioVersion = 18.2.11408.102
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{875A7E2E-4B7C-4AF0-A71E-3980B73AF363}"
ProjectSection(SolutionItems) = preProject
Expand Down
212 changes: 212 additions & 0 deletions IMPLEMENTATION_SUMMARY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
# Elsa Studio Hosting Components - Implementation Summary

## Problem Statement
There was a lot of repetitive code in host application HTML, CSHTML, and JavaScript files that wasn't transferrable to other Elsa Studio host applications. Integrators had to copy and paste all the boilerplate code to set up a new host.

## Solution
Created complete, reusable single-script loaders in the `Elsa.Studio.Shared` project that handle EVERYTHING - CSS, JavaScript, loading screen, and initialization.

## What Was Created

### 1. Complete Single-Script Loaders
**Location:** `src/framework/Elsa.Studio.Shared/wwwroot/js/`

Three comprehensive loaders that dynamically inject all dependencies:

**elsa-studio-loader-server.js** - For Blazor Server
- Dynamically injects all required CSS links (MudBlazor, Radzen, Elsa Studio Shell, Workflows Designer)
- Dynamically loads all JavaScript libraries (BlazorMonaco, MudBlazor, Radzen, etc.)
- Creates and injects loading screen HTML and CSS
- Initializes Blazor Server with 10-second timeout fallback
- Exposes `ElsaStudio.hideLoading()` API

**elsa-studio-loader-wasm.js** - For Blazor WebAssembly
- Dynamically injects all required CSS links
- Dynamically loads all JavaScript libraries including WebAssembly authentication
- Creates and injects loading screen HTML and CSS
- Initializes Blazor WASM with safety timeout
- Handles script loading order and dependencies

**elsa-studio-loader-hosted-wasm.js** - For Hosted WebAssembly
- All features of WASM loader
- Custom `loadBootResource` configuration for multi-tenant scenarios
- Dynamic base path resolution support

### 2. Optional Standalone CSS
**Location:** `src/framework/Elsa.Studio.Shared/wwwroot/css/elsa-loading.css`

Standalone CSS file for the loading screen (optional, as loaders inject inline styles).

### 3. Razor Components (For Pure Blazor)
**Location:** `src/framework/Elsa.Studio.Shared/Components/Hosting/`

These remain available for use in pure Blazor components:
- `ElsaStudioHead.razor`
- `ElsaStudioScripts.razor`
- `ElsaStudioLoadingScreen.razor`
- `ElsaStudioInitScript.razor`
- `BlazorHostingMode.cs`

### 4. Comprehensive Documentation
**Location:** `src/framework/Elsa.Studio.Shared/Components/Hosting/README.md`

Complete integration guide with minimal examples for all scenarios.

## Changes to Host Projects

### Before (Repetitive Pattern)
Each host had ~50-60 lines of boilerplate:
- 5-7 CSS `<link>` tags
- 7-8 JavaScript `<script>` tags
- 12 lines of loading screen HTML
- 8 lines of CSS animation
- 10-20 lines of initialization JavaScript

### After (Minimal Pattern)
Each host now has just **1 line**:
```html
<script src="_content/Elsa.Studio.Shared/js/elsa-studio-loader-[mode].js"></script>
```

### Updated Files
1. **Elsa.Studio.Host.Server** - `Pages/_Host.cshtml`
- Removed all CSS links
- Removed all script tags
- Removed loading screen HTML and CSS
- Removed initialization JavaScript
- Added single loader script: `elsa-studio-loader-server.js`

2. **Elsa.Studio.Host.Wasm** - `wwwroot/index.html`
- Removed all CSS links
- Removed all script tags
- Removed loading screen HTML and CSS
- Removed initialization JavaScript
- Added single loader script: `elsa-studio-loader-wasm.js`

3. **Elsa.Studio.Host.HostedWasm** - `Pages/_Host.cshtml`
- Removed all CSS links
- Removed all script tags
- Removed loading screen HTML and CSS
- Removed initialization JavaScript
- Added single loader script: `elsa-studio-loader-hosted-wasm.js`
- Kept `window.getClientConfig` for API URL injection

## Benefits

### For Integrators
✅ **Truly Minimal** - Just 1 script tag, that's it!
✅ **Zero Boilerplate** - No CSS, HTML, or JavaScript to maintain
✅ **Copy-Paste Ready** - Documentation provides complete working examples
✅ **No Duplication** - All initialization logic centralized
✅ **Consistent** - Same UI and behavior across all integration scenarios

### For Maintenance
✅ **Single Source of Truth** - Update once in loader, applies everywhere
✅ **Packaged** - Delivered via Elsa.Studio.Shared NuGet package
✅ **Testable** - Centralized code is easier to test
✅ **Documented** - Clear examples for all scenarios
✅ **Future-Proof** - Add new dependencies in loader, all hosts get them automatically

### Code Reduction
- **~97% reduction** per host (from ~50 lines to 1 line)
- **0 lines** of boilerplate to maintain in each host
- **3 reusable** loader scripts covering all scenarios

## Integration Examples

### Minimal Blazor Server Host

```cshtml
@page "/"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<base href="~/" />
<title>Elsa Studio</title>
<component type="typeof(HeadOutlet)" render-mode="Server" />
</head>
<body>
<component type="typeof(App)" render-mode="Server" />

<div id="blazor-error-ui">
An error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>

<!-- That's it! Everything else is handled by the loader -->
<script src="_content/Elsa.Studio.Shared/js/elsa-studio-loader-server.js"></script>
</body>
</html>
```

### Minimal Blazor WASM Host

```html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<base href="/" />
<title>Elsa Studio</title>
</head>
<body>
<div id="app"></div>

<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>

<!-- That's it! Everything else is handled by the loader -->
<script src="_content/Elsa.Studio.Shared/js/elsa-studio-loader-wasm.js"></script>
</body>
</html>
```

## How It Works

The loaders use JavaScript to:
1. **Dynamically create and inject CSS `<link>` elements** - No need to manually list CSS files
2. **Dynamically create and inject script `<script>` elements** - No need to manually list JavaScript files
3. **Generate loading screen HTML** - Injected at runtime into the body
4. **Add loading screen CSS** - Injected at runtime into the head
5. **Initialize Blazor** - Automatic startup with appropriate configuration
6. **Manage loading state** - Hide screen when Blazor is ready

All of this happens automatically when the single loader script runs.

## Testing

- ✅ All host projects build successfully
- ✅ Server host compiles without errors
- ✅ WASM host compiles without errors
- ✅ HostedWasm host compiles without errors
- ✅ Loaders inject CSS dynamically
- ✅ Loaders inject scripts dynamically
- ✅ Loading screens appear and disappear correctly
- ✅ Blazor initializes properly

## Backward Compatibility

The solution maintains backward compatibility:
- Original `elsa-studio-init.js` still available
- Razor components still available for pure Blazor scenarios
- Legacy function names still work
- No breaking changes to APIs or contracts

## Next Steps for Integrators

1. Add reference to `Elsa.Studio.Shared` NuGet package
2. Use one of the minimal integration examples from README
3. Deploy - everything is handled automatically!

See `src/framework/Elsa.Studio.Shared/Components/Hosting/README.md` for complete documentation.

## Summary

**Problem:** 50+ lines of repetitive boilerplate per host
**Solution:** Single-script loaders that handle everything
**Result:** 1 line per host, ~97% code reduction, full reuse achieved
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<MudIconButton Icon="@Icons.Material.Outlined.Logout" Color="Color.Inherit" Href="/login" Target="_self" Title="Logout"/>
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace Elsa.Studio.Shared.Components.Hosting;

/// <summary>
/// Represents the Blazor hosting mode.
/// </summary>
public enum BlazorHostingMode
{
/// <summary>
/// Blazor Server hosting mode.
/// </summary>
Server,

/// <summary>
/// Blazor WebAssembly hosting mode.
/// </summary>
WebAssembly
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
@namespace Elsa.Studio.Shared.Components.Hosting

@*
Elsa Studio Head Component - Includes all required CSS links.
Usage: <ElsaStudioHead />
*@

<link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
<link href="_content/CodeBeam.MudBlazor.Extensions/MudExtensions.min.css" rel="stylesheet" />
<link href="_content/Radzen.Blazor/css/material-base.css" rel="stylesheet">
<link href="_content/Elsa.Studio.Shell/css/shell.css" rel="stylesheet">
@if (IncludeDesignerCss)
{
<link href="_content/Elsa.Studio.Workflows.Designer/designer.css" rel="stylesheet">
}

@code {
/// <summary>
/// Whether to include the Workflow Designer CSS. Default is true.
/// </summary>
[Parameter]
public bool IncludeDesignerCss { get; set; } = true;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
@namespace Elsa.Studio.Shared.Components.Hosting

@*
Elsa Studio Initialization Script Component - Includes JavaScript for loading screen and Blazor initialization.
Usage: <ElsaStudioInitScript Mode="BlazorHostingMode.Server" />
*@

<script src="_content/Elsa.Studio.Shared/js/elsa-studio-init.js"></script>

@if (Mode == BlazorHostingMode.Server)
{
<script>
ElsaStudio.initializeBlazorServer(@MaxWaitTimeMs);
</script>
}
else if (Mode == BlazorHostingMode.WebAssembly)
{
<script>
// Initialize Blazor WASM when DOM is ready
function initializeElsaBlazorWasm() {
@if (CustomConfig != null)
{
@:ElsaStudio.initializeBlazorWasm(@CustomConfig);
}
else
{
@:ElsaStudio.initializeBlazorWasm();
}
}

if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeElsaBlazorWasm);
} else {
initializeElsaBlazorWasm();
}

// Safety timeout
setTimeout(() => {
ElsaStudio.hideLoading();
}, @SafetyTimeoutMs);
</script>
}

@code {
/// <summary>
/// The Blazor hosting mode (Server or WebAssembly).
/// </summary>
[Parameter]
public BlazorHostingMode Mode { get; set; } = BlazorHostingMode.Server;

/// <summary>
/// For Server mode: Maximum time (in milliseconds) to wait before hiding loading screen. Default is 10000 (10 seconds).
/// </summary>
[Parameter]
public int MaxWaitTimeMs { get; set; } = 10000;

/// <summary>
/// For WebAssembly mode: Safety timeout (in milliseconds) to ensure loading screen is hidden. Default is 5000 (5 seconds).
/// </summary>
[Parameter]
public int SafetyTimeoutMs { get; set; } = 5000;

/// <summary>
/// For WebAssembly mode: Custom Blazor configuration as a JavaScript object literal.
/// Example: "{ loadBootResource: function(type, name, defaultUri, integrity) { return defaultUri; } }"
/// </summary>
[Parameter]
public string? CustomConfig { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
@namespace Elsa.Studio.Shared.Components.Hosting

@*
Elsa Studio Loading Screen Component - Shows a loading spinner during initialization.
Usage: <ElsaStudioLoadingScreen Id="app-loading" />
*@

<div id="@Id" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: #f5f5f5; display: flex; justify-content: center; align-items: center; z-index: 9999;">
<div style="text-align: center;">
<div style="width: 40px; height: 40px; border: 4px solid #e0e0e0; border-top: 4px solid #1976d2; border-radius: 50%; animation: elsa-loading-spin 1s linear infinite; margin: 0 auto 20px;"></div>
<div id="@TextId" style="color: #666; font-family: 'Roboto', sans-serif;">@Text</div>
</div>
</div>

<style>
@@keyframes elsa-loading-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

.blazor-ready #@Id {
display: none !important;
}
</style>

@code {
/// <summary>
/// The ID for the loading screen element. Default is "elsa-loading".
/// </summary>
[Parameter]
public string Id { get; set; } = "elsa-loading";

/// <summary>
/// The ID for the loading text element. Default is "elsa-loading-text".
/// </summary>
[Parameter]
public string TextId { get; set; } = "elsa-loading-text";

/// <summary>
/// The initial loading text. Default is "Initializing...".
/// </summary>
[Parameter]
public string Text { get; set; } = "Initializing...";
}
Loading