Playground (#2400)

* Playground page.

* Persist the changes.

* Add copy button.

* Style Playground

* Update Playground button in CodeViewer

* Add AntiForgery support.

* Update Save alert

* Extract common code.

* Loading tweaks.

---------

Co-authored-by: yordanov <vasil@yordanov.info>
This commit is contained in:
Atanas Korchev
2025-12-19 17:36:32 +02:00
committed by GitHub
parent c4913a94c4
commit e2a46157b9
12 changed files with 744 additions and 31 deletions

View File

@@ -0,0 +1,55 @@
using Microsoft.AspNetCore.Mvc;
using RadzenBlazorDemos.Host.Services;
using System.Threading.Tasks;
namespace RadzenBlazorDemos.Host.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class PlaygroundController(PlaygroundService playgroundService) : ControllerBase
{
[HttpGet("{id}")]
public async Task<IActionResult> GetSnippet(string id)
{
if (!playgroundService.IsConfigured)
{
return StatusCode(503, new { error = "Playground storage is not configured." });
}
var snippet = await playgroundService.GetSnippetAsync(id);
if (snippet == null)
{
return NotFound(new { error = "Snippet not found." });
}
return Ok(new
{
id = snippet.Id,
source = snippet.Source,
parentId = snippet.ParentId,
createdAt = snippet.CreatedAt,
updatedAt = snippet.UpdatedAt
});
}
[HttpPost("save")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> SaveSnippet([FromBody] SaveSnippetRequest request)
{
if (!playgroundService.IsConfigured)
{
return StatusCode(503, new { error = "Playground storage is not configured." });
}
if (string.IsNullOrWhiteSpace(request.Source))
{
return BadRequest(new { error = "Source code is required." });
}
var result = await playgroundService.SaveSnippetAsync(request);
return Ok(result);
}
}
}

View File

@@ -15,6 +15,7 @@ using Radzen;
using RadzenBlazorDemos;
using RadzenBlazorDemos.Data;
using RadzenBlazorDemos.Services;
using RadzenBlazorDemos.Host.Services;
using Microsoft.AspNetCore.Http;
var builder = WebApplication.CreateBuilder(args);
@@ -54,6 +55,14 @@ builder.Services.AddSingleton<GitHubService>();
builder.Services.AddAIChatService(options =>
builder.Configuration.GetSection("AIChatService").Bind(options));
builder.Services.Configure<PlaygroundOptions>(builder.Configuration.GetSection("Playground"));
builder.Services.AddSingleton<PlaygroundService>();
builder.Services.AddAntiforgery(options =>
{
options.HeaderName = "X-CSRF-TOKEN";
});
builder.Services.AddLocalization();
/* --> Uncomment to enable localization

View File

@@ -11,6 +11,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.*-*" />
<PackageReference Include="Microsoft.AspNetCore.OData" Version="9.*-*" />
<PackageReference Include="DocumentFormat.OpenXml" Version="2.20.0" />
<PackageReference Include="AWSSDK.S3" Version="3.*" />
</ItemGroup>
<ItemGroup>
<Content Include="*.db">

View File

@@ -0,0 +1,220 @@
#nullable enable
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Amazon.S3;
using Amazon.S3.Model;
using Microsoft.Extensions.Options;
namespace RadzenBlazorDemos.Host.Services
{
public class PlaygroundOptions
{
public S3Options S3 { get; set; } = new();
}
public class S3Options
{
public string ServiceUrl { get; set; } = "";
public string BucketName { get; set; } = "playground-snippets";
public string AccessKey { get; set; } = "";
public string SecretKey { get; set; } = "";
}
public class Snippet
{
public string Id { get; set; } = "";
public string Source { get; set; } = "";
public string EditTokenHash { get; set; } = "";
public string? ParentId { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
}
public class SaveSnippetRequest
{
public string? Id { get; set; }
public string Source { get; set; } = "";
public string? EditToken { get; set; }
}
public class SaveSnippetResponse
{
public string Id { get; set; } = "";
public string EditToken { get; set; } = "";
public bool IsNew { get; set; }
}
public class PlaygroundService
{
private readonly IAmazonS3 _s3Client;
private readonly string _bucketName;
private readonly bool _isConfigured;
public PlaygroundService(IOptions<PlaygroundOptions> options)
{
var s3Options = options.Value.S3;
_bucketName = s3Options.BucketName;
_isConfigured = !string.IsNullOrEmpty(s3Options.ServiceUrl)
&& !string.IsNullOrEmpty(s3Options.AccessKey)
&& !string.IsNullOrEmpty(s3Options.SecretKey);
if (_isConfigured)
{
var config = new AmazonS3Config
{
ServiceURL = s3Options.ServiceUrl,
ForcePathStyle = true
};
_s3Client = new AmazonS3Client(s3Options.AccessKey, s3Options.SecretKey, config);
}
else
{
// Create a dummy client that won't be used
_s3Client = null!;
}
}
public bool IsConfigured => _isConfigured;
public async Task<Snippet?> GetSnippetAsync(string id)
{
if (!_isConfigured) return null;
try
{
var response = await _s3Client.GetObjectAsync(_bucketName, $"snippets/{id}.json");
using var reader = new StreamReader(response.ResponseStream);
var json = await reader.ReadToEndAsync();
return JsonSerializer.Deserialize<Snippet>(json, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
}
catch (AmazonS3Exception ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
{
return null;
}
}
public async Task<SaveSnippetResponse> SaveSnippetAsync(SaveSnippetRequest request)
{
if (!_isConfigured)
{
throw new InvalidOperationException("Playground S3 storage is not configured.");
}
var now = DateTime.UtcNow;
// Case 1: New snippet (no id provided)
if (string.IsNullOrEmpty(request.Id))
{
return await CreateNewSnippetAsync(request.Source, null, now);
}
// Case 2: Existing snippet - check edit token
var existingSnippet = await GetSnippetAsync(request.Id);
if (existingSnippet == null)
{
// Snippet doesn't exist, create new one
return await CreateNewSnippetAsync(request.Source, null, now);
}
// Verify edit token
if (!string.IsNullOrEmpty(request.EditToken) &&
VerifyEditToken(request.EditToken, existingSnippet.EditTokenHash))
{
// Valid token - update in place
return await UpdateSnippetAsync(existingSnippet, request.Source, request.EditToken, now);
}
// Invalid or missing token - clone the snippet
return await CreateNewSnippetAsync(request.Source, request.Id, now);
}
private async Task<SaveSnippetResponse> CreateNewSnippetAsync(string source, string? parentId, DateTime now)
{
var id = Guid.NewGuid().ToString();
var editToken = GenerateEditToken();
var editTokenHash = HashEditToken(editToken);
var snippet = new Snippet
{
Id = id,
Source = source,
EditTokenHash = editTokenHash,
ParentId = parentId,
CreatedAt = now,
UpdatedAt = now
};
await SaveToS3Async(snippet);
return new SaveSnippetResponse
{
Id = id,
EditToken = editToken,
IsNew = true
};
}
private async Task<SaveSnippetResponse> UpdateSnippetAsync(Snippet snippet, string source, string editToken, DateTime now)
{
snippet.Source = source;
snippet.UpdatedAt = now;
await SaveToS3Async(snippet);
return new SaveSnippetResponse
{
Id = snippet.Id,
EditToken = editToken,
IsNew = false
};
}
private async Task SaveToS3Async(Snippet snippet)
{
var json = JsonSerializer.Serialize(snippet, new JsonSerializerOptions
{
WriteIndented = true
});
var putRequest = new PutObjectRequest
{
BucketName = _bucketName,
Key = $"snippets/{snippet.Id}.json",
ContentBody = json,
ContentType = "application/json"
};
await _s3Client.PutObjectAsync(putRequest);
}
private static string GenerateEditToken()
{
var bytes = new byte[32];
using var rng = RandomNumberGenerator.Create();
rng.GetBytes(bytes);
return Convert.ToBase64String(bytes);
}
private static string HashEditToken(string token)
{
var bytes = Encoding.UTF8.GetBytes(token);
var hash = SHA256.HashData(bytes);
return Convert.ToBase64String(hash);
}
private static bool VerifyEditToken(string token, string hash)
{
var computedHash = HashEditToken(token);
return computedHash == hash;
}
}
}

View File

@@ -6,6 +6,14 @@
"Microsoft": "Information"
}
},
"Playground": {
"S3": {
"ServiceUrl": "",
"BucketName": "radzen-playground",
"AccessKey": "",
"SecretKey": ""
}
},
"AIChatService": {
"Endpoint": "https://api.cloudflare.com/client/v4/accounts/dac31e6601b57aa9edbead03210a6fd6/ai/v1/chat/completions",
"ApiKey": "",

View File

@@ -7,6 +7,14 @@
}
},
"AllowedHosts": "*",
"Playground": {
"S3": {
"ServiceUrl": "",
"BucketName": "radzen-playground",
"AccessKey": "",
"SecretKey": ""
}
},
"AIChatService": {
"Endpoint": "https://api.cloudflare.com/client/v4/accounts/dac31e6601b57aa9edbead03210a6fd6/ai/v1/chat/completions",
"ApiKey": "",

View File

@@ -0,0 +1,308 @@
@page "/playground"
@page "/playground/{Id}"
@layout PlaygroundLayout
@using System.Text.RegularExpressions
@using System.Text.Json
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@inject IJSRuntime JSRuntime
@inject HttpClient Http
@inject NavigationManager NavigationManager
@inject IServiceProvider ServiceProvider
@inject AntiforgeryStateProvider AntiforgeryStateProvider
@implements IDisposable
<PageTitle>Playground | Radzen Blazor Components</PageTitle>
<RadzenSplitter Orientation="Orientation.Horizontal" Style="flex: 1;">
<RadzenSplitterPane Size="50%" Min="300px">
<RadzenStack class="rz-p-0 playground" Orientation="Orientation.Vertical" Gap="0" Style="height: 100%;">
<RadzenRow Gap="0.5rem" RowGap="0" class="rz-p-2 rz-ps-lg-4 rz-border-bottom" AlignItems="AlignItems.Center" Style="min-height: 48px;">
<RadzenColumn>
<RadzenText TextStyle="TextStyle.Subtitle2" class="rz-m-0" >
Source Code
</RadzenText>
</RadzenColumn>
<RadzenColumn>
<RadzenStack Orientation="Orientation.Horizontal" AlignItems="AlignItems.Center" JustifyContent="JustifyContent.End" Gap="0.5rem">
<RadzenButton IsBusy=@isSaving Disabled=@(isBusy || isSaving) Click="Save" Text="Save" Icon="save" ButtonStyle="ButtonStyle.Base" Variant="Variant.Flat" Size="ButtonSize.Small" />
<RadzenButton IsBusy=@isBusy Disabled=@isBusy Click="Run" Text="Run" Icon="play_circle" ButtonStyle="ButtonStyle.Primary" Variant="Variant.Flat" Size="ButtonSize.Small" />
</RadzenStack>
</RadzenColumn>
<RadzenColumn Size="12">
<RadzenAlert Visible=@(error != null) AlertStyle="AlertStyle.Danger" Variant="Variant.Flat" Shade="Shade.Lighter" Size="AlertSize.Small" class="rz-mb-2">@error</RadzenAlert>
<RadzenAlert Visible=@(saveSuccess) AlertStyle="AlertStyle.Success" Variant="Variant.Flat" Shade="Shade.Lighter" Size="AlertSize.Small" class="rz-mb-2">
<RadzenText TextStyle="TextStyle.Subtitle1">Snippet saved! Share this URL:</RadzenText>
<RadzenText TextStyle="TextStyle.Subtitle2">@currentUrl</RadzenText>
<RadzenButton Click="CopyUrl" Text="Copy" Icon="content_copy" ButtonStyle="ButtonStyle.Success" Size="ButtonSize.ExtraSmall" Variant="Variant.Flat" />
</RadzenAlert>
</RadzenColumn>
</RadzenRow>
<div style="flex: 1; min-height: 0;">
<Monaco @ref="monaco" Value="@source" ValueChanged="OnValueChanged" Language="razor" Style="height: 100%; width: 100%;" />
</div>
</RadzenStack>
</RadzenSplitterPane>
<RadzenSplitterPane Min="300px">
<RadzenStack class="rz-p-0 playground" Orientation="Orientation.Vertical" Gap="0" Style="height: 100%;">
<RadzenRow Gap="0.5rem" RowGap="0" class="rz-p-2 rz-ps-lg-4 rz-border-bottom" AlignItems="AlignItems.Center" Style="min-height: 48px;">
<RadzenColumn>
<RadzenText TextStyle="TextStyle.Subtitle2" class="rz-m-0" >
Preview
</RadzenText>
</RadzenColumn>
</RadzenRow>
<div class="rz-p-4" style="flex: 1; overflow: auto;">
@if (dynamicContent != null)
{
@dynamicContent
}
else
{
<RadzenText TextStyle="TextStyle.Body2" class="rz-text-disabled-color">
Click "Run" to preview your code.
</RadzenText>
}
</div>
</RadzenStack>
</RadzenSplitterPane>
</RadzenSplitter>
@code {
private Monaco monaco;
private string source = DefaultSource;
private string error;
private bool isBusy;
private bool isSaving;
private bool saveSuccess;
private string currentUrl;
private RenderFragment dynamicContent;
private CompilerServiceFactory compilerFactory;
private string editToken;
private const string DefaultSource = @"<RadzenButton Text=""Hello World!"" Click=""@OnClick"" />
<RadzenText TextStyle=""TextStyle.Body1"" class=""rz-mt-4"">@message</RadzenText>
@code {
string message = ""Click the button above!"";
void OnClick()
{
message = $""Button clicked at {DateTime.Now:HH:mm:ss}"";
}
}";
[Parameter]
public string Id { get; set; }
[SupplyParameterFromQuery]
[Parameter]
public string Example { get; set; }
protected override async Task OnInitializedAsync()
{
// Load from saved snippet if Id is provided
if (!string.IsNullOrEmpty(Id))
{
await LoadSnippetAsync(Id);
}
// Load from example if Example query param is provided
else if (!string.IsNullOrEmpty(Example))
{
await LoadExampleAsync(Example);
}
}
async Task LoadSnippetAsync(string id)
{
try
{
var response = await Http.GetAsync($"/api/playground/{id}");
if (response.IsSuccessStatusCode)
{
var snippet = await response.Content.ReadFromJsonAsync<SnippetResponse>();
if (snippet != null)
{
source = snippet.Source;
// Try to get the edit token from localStorage
editToken = await GetEditTokenAsync(id);
}
}
}
catch
{
// Use default source if snippet cannot be loaded
}
}
async Task LoadExampleAsync(string example)
{
try
{
var exampleSource = await Http.GetStringAsync($"/demos/Pages/{example}.txt");
source = Clean(exampleSource);
}
catch
{
// Use default source if example cannot be loaded
}
}
string Clean(string source)
{
return Regex.Replace(source, "</?RadzenExample[^>]*>\n?", "");
}
void OnValueChanged(string value)
{
source = value;
saveSuccess = false;
}
async Task Run()
{
try
{
error = null;
isBusy = true;
StateHasChanged();
await Task.Yield();
compilerFactory ??= new CompilerServiceFactory(ServiceProvider, NavigationManager);
var compiler = await compilerFactory.GetCompilerAsync();
var type = await compiler.CompileAsync(source);
dynamicContent = builder =>
{
builder.OpenComponent(0, type);
builder.CloseComponent();
};
}
catch (Exception ex)
{
error = ex.Message;
dynamicContent = null;
}
finally
{
isBusy = false;
}
}
async Task Save()
{
try
{
error = null;
saveSuccess = false;
isSaving = true;
StateHasChanged();
var antiforgeryToken = AntiforgeryStateProvider.GetAntiforgeryToken();
var request = new
{
id = Id,
source = source,
editToken = editToken
};
var httpRequest = new HttpRequestMessage(HttpMethod.Post, "/api/playground/save");
httpRequest.Content = JsonContent.Create(request);
if (antiforgeryToken?.Value != null)
{
httpRequest.Headers.Add("X-CSRF-TOKEN", antiforgeryToken.Value);
}
var response = await Http.SendAsync(httpRequest);
if (!response.IsSuccessStatusCode)
{
var errorResponse = await response.Content.ReadAsStringAsync();
throw new Exception($"Failed to save: {errorResponse}");
}
var result = await response.Content.ReadFromJsonAsync<SaveResponse>();
if (result != null)
{
// Store the edit token in localStorage
await SetEditTokenAsync(result.Id, result.EditToken);
editToken = result.EditToken;
// Navigate to the new URL if it's a new snippet or clone
if (result.IsNew || Id != result.Id)
{
Id = result.Id;
NavigationManager.NavigateTo($"/playground/{result.Id}", replace: false);
}
currentUrl = NavigationManager.ToAbsoluteUri($"/playground/{result.Id}").ToString();
saveSuccess = true;
}
}
catch (Exception ex)
{
error = ex.Message;
}
finally
{
isSaving = false;
}
}
async Task CopyUrl()
{
if (!string.IsNullOrEmpty(currentUrl))
{
await JSRuntime.InvokeVoidAsync("navigator.clipboard.writeText", currentUrl);
}
}
async Task<string> GetEditTokenAsync(string id)
{
try
{
return await JSRuntime.InvokeAsync<string>("localStorage.getItem", $"playground-token-{id}");
}
catch
{
return null;
}
}
async Task SetEditTokenAsync(string id, string token)
{
try
{
await JSRuntime.InvokeVoidAsync("localStorage.setItem", $"playground-token-{id}", token);
}
catch
{
// Ignore localStorage errors
}
}
public void Dispose()
{
GC.Collect();
}
class SnippetResponse
{
public string Id { get; set; }
public string Source { get; set; }
public string ParentId { get; set; }
}
class SaveResponse
{
public string Id { get; set; }
public string EditToken { get; set; }
public bool IsNew { get; set; }
}
}

View File

@@ -0,0 +1,56 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.WebAssembly.Services;
namespace RadzenBlazorDemos
{
public class CompilerServiceFactory
{
private static readonly string[] LazyAssemblies = new[]
{
"Microsoft.CodeAnalysis.dll",
"Microsoft.CodeAnalysis.CSharp.dll",
"Microsoft.CodeAnalysis.Razor.dll",
"Microsoft.AspNetCore.Razor.Language.dll",
"Microsoft.AspNetCore.Mvc.Razor.Extensions.dll",
"MetadataReferenceService.BlazorWasm.dll",
"MetadataReferenceService.Abstractions.dll"
};
private readonly IServiceProvider serviceProvider;
private readonly NavigationManager navigationManager;
private ICompilerService compiler;
public CompilerServiceFactory(IServiceProvider serviceProvider, NavigationManager navigationManager)
{
this.serviceProvider = serviceProvider;
this.navigationManager = navigationManager;
}
public async Task<ICompilerService> GetCompilerAsync()
{
if (compiler != null)
{
return compiler;
}
var assemblyLoader = serviceProvider.GetService(typeof(LazyAssemblyLoader)) as LazyAssemblyLoader;
if (assemblyLoader != null)
{
await assemblyLoader.LoadAssembliesAsync(LazyAssemblies);
}
var compilerType = AppDomain.CurrentDomain.GetAssemblies()
.Select(a => a.GetType("RadzenBlazorDemos.CompilerService", false))
.FirstOrDefault(t => t != null);
compiler = (ICompilerService)Activator.CreateInstance(compilerType, navigationManager);
return compiler;
}
}
}

View File

@@ -4,7 +4,6 @@
@using System.Reflection;
@using Microsoft.Extensions.DependencyInjection;
@using System.Linq;
@using Microsoft.AspNetCore.Components.WebAssembly.Services
@inject IJSRuntime JSRuntime
@inject HttpClient Http
@inject NavigationManager NavigationManager
@@ -19,6 +18,7 @@
Copy
</button>
<RadzenButton Visible=@(!ReadOnly) Disabled=@isBusy Click="@Reset" Text="Reset" Icon="refresh" ButtonStyle="ButtonStyle.Base" Size="ButtonSize.ExtraSmall" Variant="Variant.Text" class="rz-text-truncate" />
<RadzenLink Visible=@(!ReadOnly) Path="@($"/playground?example={Uri.EscapeDataString(Path.GetFileNameWithoutExtension(PageName))}")" Text="Open in Playground" target="_blank" class="rz-button rz-button-xs rz-variant-outlined rz-base rz-shade-default rz-text-truncate" Title="Open in Playground" />
</RadzenStack>
</RadzenColumn>
<RadzenColumn>
@@ -56,7 +56,7 @@
private string source;
private string error;
private string language = "razor";
private ICompilerService compiler;
private CompilerServiceFactory compilerFactory;
[Parameter]
public EventCallback<Type> Compiled { get; set; }
@@ -125,6 +125,8 @@
await OnValueChanged(source);
}
bool isBusy;
async Task Run()
@@ -137,7 +139,8 @@
await Task.Yield();
await EnsureCompilerAsync();
compilerFactory ??= new CompilerServiceFactory(ServiceProvider, NavigationManager);
var compiler = await compilerFactory.GetCompilerAsync();
var type = await compiler.CompileAsync(source);
@@ -152,31 +155,4 @@
isBusy = false;
await JSRuntime.InvokeVoidAsync("eval", $"instances['{monaco.Id}'].updateOptions({{domReadOnly: false, readOnly: false }})");
}
async Task EnsureCompilerAsync()
{
if (compiler != null)
{
return;
}
var assemblyLoader = ServiceProvider.GetService<LazyAssemblyLoader>();
if (assemblyLoader != null)
{
await assemblyLoader.LoadAssembliesAsync(new[]
{
"Microsoft.CodeAnalysis.dll",
"Microsoft.CodeAnalysis.CSharp.dll",
"Microsoft.CodeAnalysis.Razor.dll",
"Microsoft.AspNetCore.Razor.Language.dll",
"Microsoft.AspNetCore.Mvc.Razor.Extensions.dll",
"MetadataReferenceService.BlazorWasm.dll",
"MetadataReferenceService.Abstractions.dll"
});
}
var compilerType = AppDomain.CurrentDomain.GetAssemblies().Select(a => a.GetType("RadzenBlazorDemos.CompilerService", false)).FirstOrDefault(t => t != null);
compiler = (ICompilerService)Activator.CreateInstance(compilerType, NavigationManager);
}
}

View File

@@ -1,6 +1,6 @@
@inherits Radzen.RadzenComponent
<div @ref="editor" style="height: 500px; width: 100%;" dir="ltr"></div>
<div @ref="editor" class="code-editor" dir="ltr"></div>
@code {
IJSObjectReference monaco;
ElementReference editor;

View File

@@ -0,0 +1,61 @@
@using Microsoft.AspNetCore.Components
@using Radzen.Blazor
@inherits LayoutComponentBase
@inject ThemeService ThemeService
@inject QueryStringThemeService QueryStringThemeService
@inject ExampleService ExampleService
@inject NavigationManager UriHelper
@inject IJSRuntime JSRuntime
@inject TooltipService TooltipService
@inject DialogService DialogService
<Canonical />
<RadzenComponents />
<RadzenLayout>
<RadzenHeader class="rz-p-0">
<ChildContent>
<MainTopNav />
<RadzenRow AlignItems="AlignItems.Center" JustifyContent="JustifyContent.Start" Gap="0px" class="demos-nav" Style="height: 42px;">
<RadzenColumn Size="12" SizeSM="5">
<RadzenStack Orientation="Orientation.Horizontal" AlignItems="AlignItems.Center">
<RadzenText Text="Radzen Blazor Components" title="Radzen Blazor Components" class="rz-mx-2 rz-mx-lg-4" />
</RadzenStack>
</RadzenColumn>
<RadzenColumn Size="7" class="rz-display-none rz-display-sm-block">
<RadzenStack Orientation="Orientation.Horizontal" AlignItems="AlignItems.Center" JustifyContent="JustifyContent.End">
<div class="rz-display-none rz-display-sm-inline-flex align-items-center">
<RadzenLink Path="/docs/api" Text="API Reference" title="Radzen Blazor Components API Reference" class="rz-mx-2 rz-mx-lg-4" target="_blank" />
<RadzenLink Path="https://github.com/radzenhq/radzen-blazor" Text="GitHub" title="Star Radzen Blazor Components on GitHub" class="rz-text-nowrap rz-mx-2 rz-mx-lg-4" target="_blank" />
</div>
</RadzenStack>
</RadzenColumn>
</RadzenRow>
</ChildContent>
</RadzenHeader>
<RadzenBody class="rz-p-0 rz-display-flex">
@Body
</RadzenBody>
</RadzenLayout>
@if (!rendered)
{
<div class="rz-app-loading">
<div class="logo-loading"></div>
</div>
}
@code {
bool rendered;
protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
{
rendered = true;
StateHasChanged();
}
}
}

View File

@@ -628,4 +628,15 @@ svg.illustration {
inset-inline-start: 0;
width: 100%;
height: 100%;
}
.code-editor {
height: 500px;
width: 100%;
}
.playground .code-editor {
height: 100%;
width: 100%;
}