mirror of
https://github.com/radzenhq/radzen-blazor.git
synced 2026-02-04 05:35:44 +00:00
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:
55
RadzenBlazorDemos.Host/Controllers/PlaygroundController.cs
Normal file
55
RadzenBlazorDemos.Host/Controllers/PlaygroundController.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
220
RadzenBlazorDemos.Host/Services/PlaygroundService.cs
Normal file
220
RadzenBlazorDemos.Host/Services/PlaygroundService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "",
|
||||
|
||||
@@ -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": "",
|
||||
|
||||
308
RadzenBlazorDemos/Pages/Playground.razor
Normal file
308
RadzenBlazorDemos/Pages/Playground.razor
Normal 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; }
|
||||
}
|
||||
}
|
||||
56
RadzenBlazorDemos/Services/CompilerServiceFactory.cs
Normal file
56
RadzenBlazorDemos/Services/CompilerServiceFactory.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
61
RadzenBlazorDemos/Shared/PlaygroundLayout.razor
Normal file
61
RadzenBlazorDemos/Shared/PlaygroundLayout.razor
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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%;
|
||||
}
|
||||
Reference in New Issue
Block a user