From 0302ae80ce678429110798b8fc3edf25989a171a Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Tue, 7 Jul 2026 02:02:48 -0400 Subject: [PATCH] fix(ui): remove transient query surface Signed-off-by: Yordis Prieto --- docs/admin-ui.md | 10 +- docs/projections.md | 3 +- .../Components/Layout/NavMenu.razor | 1 - .../Components/Pages/Navigator.razor | 1 - .../Components/Pages/ProjectionDebug.razor | 2 - .../Components/Pages/Projections.razor | 22 +- .../Components/Pages/Query.razor | 211 ------------------ .../Components/Pages/StreamDetail.razor | 1 - .../Services/ProjectionBrowserService.cs | 30 ++- .../Services/QueryBrowserService.cs | 94 -------- src/EventStore.ClusterNode/Program.cs | 1 - 11 files changed, 26 insertions(+), 350 deletions(-) delete mode 100644 src/EventStore.ClusterNode/Components/Pages/Query.razor delete mode 100644 src/EventStore.ClusterNode/Components/Services/QueryBrowserService.cs diff --git a/docs/admin-ui.md b/docs/admin-ui.md index 2c9b8a9d04..c414b79bfe 100644 --- a/docs/admin-ui.md +++ b/docs/admin-ui.md @@ -16,7 +16,7 @@ The dashboard opens at `/ui` and combines the daily operational view in one plac ## Navigator -The _Navigator_ page links to the main workspace areas: streams, queries, projections, subscriptions, users, operations, observability, and configuration. +The _Navigator_ page links to the main workspace areas: streams, projections, subscriptions, users, operations, observability, and configuration. ## Observability @@ -37,15 +37,11 @@ The _Streams_ page lets you find recently created or changed streams and open a - paged event browsing - event detail views with data and metadata -- append, metadata, ACL, delete, and query actions - -## Query - -The _Query_ page runs transient JavaScript projections for short-lived analysis and can stop the generated transient projection from the same page. +- append, metadata, ACL, and delete actions ## Projections -The _Projections_ page shows user, system, and optionally transient projections. It supports creating projections, creating standard projections, bulk enable or disable, and opening a projection for inspection. +The _Projections_ page shows user and system projections. It supports creating projections, creating standard projections, bulk enable or disable, and opening a projection for inspection. Projection detail pages provide source, status, config, state, result, debug, edit, reset, and delete workflows. Projection features are available when projections are enabled on the node. diff --git a/docs/projections.md b/docs/projections.md index e75f6504e6..03bea3fb1d 100644 --- a/docs/projections.md +++ b/docs/projections.md @@ -514,8 +514,7 @@ Once the projection is running, open the Admin UI, go to _Projections_, select t _Debug_. The debug page shows the current projection source, state payload, and status, with actions to stop or reset the projection when the runtime state needs to be cleared. -Use _Open in query_ to run the projection source as a transient query, or _Edit source_ to change the persistent -projection definition. +Use _Edit source_ to change the persistent projection definition. ## Projections settings diff --git a/src/EventStore.ClusterNode/Components/Layout/NavMenu.razor b/src/EventStore.ClusterNode/Components/Layout/NavMenu.razor index 7e9d87c67d..17fc82de6d 100644 --- a/src/EventStore.ClusterNode/Components/Layout/NavMenu.razor +++ b/src/EventStore.ClusterNode/Components/Layout/NavMenu.razor @@ -24,7 +24,6 @@

Data plane

Streams - Query Projections Subscriptions
diff --git a/src/EventStore.ClusterNode/Components/Pages/Navigator.razor b/src/EventStore.ClusterNode/Components/Pages/Navigator.razor index c67c344b13..d1f52e8c74 100644 --- a/src/EventStore.ClusterNode/Components/Pages/Navigator.razor +++ b/src/EventStore.ClusterNode/Components/Pages/Navigator.razor @@ -11,7 +11,6 @@
- diff --git a/src/EventStore.ClusterNode/Components/Pages/ProjectionDebug.razor b/src/EventStore.ClusterNode/Components/Pages/ProjectionDebug.razor index e2ca58bc1b..197646ac53 100644 --- a/src/EventStore.ClusterNode/Components/Pages/ProjectionDebug.razor +++ b/src/EventStore.ClusterNode/Components/Pages/ProjectionDebug.razor @@ -65,7 +65,6 @@ - Open in query Edit source @@ -105,7 +104,6 @@ private string LinkName => Page?.Name ?? Name; private string DetailHref => $"/ui/projections/{Uri.EscapeDataString(LinkName)}"; private string EditHref => $"/ui/projections/edit/{Uri.EscapeDataString(LinkName)}"; - private string QueryHref => $"/ui/query?location={Uri.EscapeDataString(LinkName)}"; protected override async Task OnParametersSetAsync() { DisableInput ??= new(); diff --git a/src/EventStore.ClusterNode/Components/Pages/Projections.razor b/src/EventStore.ClusterNode/Components/Pages/Projections.razor index 1d07de8943..47a56dafdc 100644 --- a/src/EventStore.ClusterNode/Components/Pages/Projections.razor +++ b/src/EventStore.ClusterNode/Components/Pages/Projections.razor @@ -12,7 +12,6 @@
@@ -55,7 +54,7 @@ {
-

Showing @(Page.IncludeQueries ? "all projections including transient queries" : "non-transient projections").

+

Showing non-transient projections.

@@ -128,8 +127,6 @@
@code { - [SupplyParameterFromQuery] public bool IncludeQueries { get; set; } - [SupplyParameterFromForm(FormName = "projection-enable-all")] private ProjectionBulkInput EnableAllInput { get; set; } @@ -138,8 +135,7 @@ private ProjectionListPage Page { get; set; } private ProjectionBulkCommandResult CommandResult { get; set; } - private string EmptyProjectionsMessage => - Page?.IncludeQueries == true ? "No projections are available." : "No non-transient projections are available."; + private const string EmptyProjectionsMessage = "No non-transient projections are available."; protected override async Task OnParametersSetAsync() { EnableAllInput ??= new(); @@ -149,7 +145,7 @@ try { - Page = await ProjectionBrowser.ReadAll(IncludeQueries); + Page = await ProjectionBrowser.ReadAll(); } catch (OperationCanceledException) { @@ -157,13 +153,13 @@ } catch (Exception ex) { - Page = ProjectionListPage.Unavailable($"Failed to load projections: {UiMessages.Friendly(ex)}", IncludeQueries); + Page = ProjectionListPage.Unavailable($"Failed to load projections: {UiMessages.Friendly(ex)}"); } } private async Task EnableAll() { try { - CommandResult = await ProjectionBrowser.EnableAll(IncludeQueries); + CommandResult = await ProjectionBrowser.EnableAll(); } catch (OperationCanceledException) { CommandResult = ProjectionBulkCommandResult.Failure("Enable projections was canceled."); return; @@ -177,7 +173,7 @@ private async Task DisableAll() { try { - CommandResult = await ProjectionBrowser.DisableAll(IncludeQueries); + CommandResult = await ProjectionBrowser.DisableAll(); } catch (OperationCanceledException) { CommandResult = ProjectionBulkCommandResult.Failure("Disable projections was canceled."); return; @@ -191,11 +187,11 @@ private async Task RefreshAfterBulkAction() { try { - Page = await ProjectionBrowser.ReadAll(IncludeQueries); + Page = await ProjectionBrowser.ReadAll(); } catch (OperationCanceledException) { - Page = ProjectionListPage.Unavailable("Projection command completed, but refresh was canceled.", IncludeQueries); + Page = ProjectionListPage.Unavailable("Projection command completed, but refresh was canceled."); } catch (Exception ex) { - Page = ProjectionListPage.Unavailable($"Projection command completed, but refresh failed: {UiMessages.Friendly(ex)}", IncludeQueries); + Page = ProjectionListPage.Unavailable($"Projection command completed, but refresh failed: {UiMessages.Friendly(ex)}"); } } diff --git a/src/EventStore.ClusterNode/Components/Pages/Query.razor b/src/EventStore.ClusterNode/Components/Pages/Query.razor deleted file mode 100644 index fe50255490..0000000000 --- a/src/EventStore.ClusterNode/Components/Pages/Query.razor +++ /dev/null @@ -1,211 +0,0 @@ -@page "/ui/query" -@inject QueryBrowserService QueryBrowser -@inject ProjectionBrowserService Projections -@using System.ComponentModel.DataAnnotations -@using System.Security.Cryptography -@using Microsoft.AspNetCore.DataProtection -@using Microsoft.AspNetCore.Components.Forms -@inject IDataProtectionProvider DataProtectionProvider - -Query - EventStore UI - -
-
-

Query

-

Run transient projection queries with the guardrails visible.

-

Submit JavaScript projection queries and inspect the created transient projection from the same page.

-
-
-

Execution model

-

Queries run as transient projections under the current authenticated user. They are suitable for inspection workflows, not as a substitute for permanent projection management.

-
- - - -
-
-
- -
- - -
- @if (CommandResult is not null) - { -

@CommandResult.Message

- } - @if (Result is null) - { -
- Run a query to create a transient projection and reveal result links. -
- } - else if (!Result.IsAvailable) - { -
- @Result.Message -
- } - else - { -
-
- - -
-

@Result.ProjectionName

-

The projection may still be processing. Open its detail or debug page to inspect the latest server-side view.

-
- Projection detail - - - - - - Debug -
-
- } -
-
- -@code { - private const string DefaultQuery = "fromAll().when({\n $init: function () { return { count: 0 }; },\n $any: function (state, event) {\n state.count += 1;\n }\n});"; - - [SupplyParameterFromQuery] public string InitStreamId { get; set; } = ""; - [SupplyParameterFromQuery] public string Location { get; set; } = ""; - - [SupplyParameterFromForm(FormName = "transient-query")] - private QueryInput Input { get; set; } - - [SupplyParameterFromForm(FormName = "query-stop")] - private QueryStopInput StopInput { get; set; } - - private QueryRunPage Result { get; set; } - private ProjectionCommandResult CommandResult { get; set; } - private string _lastSeededQuery = ""; - private string _lastLoadedLocation = ""; - private IDataProtector StopTargetProtector => - DataProtectionProvider.CreateProtector("EventStore.ClusterNode.Components.Pages.Query.StopProjection"); - - protected override async Task OnParametersSetAsync() { - Input ??= new() { Query = DefaultQuery }; - StopInput ??= new(); - - if (!string.IsNullOrWhiteSpace(Location)) { - if (string.Equals(_lastLoadedLocation, Location, StringComparison.OrdinalIgnoreCase) || - (string.IsNullOrWhiteSpace(_lastLoadedLocation) && !CanSeedQuery())) - return; - - try { - var page = await Projections.ReadProjectionQuery(Location); - if (page.Query is not null) { - Input.Query = page.Query.Query; - _lastSeededQuery = page.Query.Query; - _lastLoadedLocation = Location; - Result = QueryRunPage.Success(Input.Query, page.Query.Name); - StopInput.StopToken = ProtectStopTarget(page.Query.Name); - } else { - Result = QueryRunPage.Unavailable(Input.Query, page.Message); - } - } catch (OperationCanceledException) { - throw; - } catch (Exception ex) { - Result = QueryRunPage.Unavailable(Input.Query, $"Failed to load query projection: {UiMessages.Friendly(ex)}"); - } - - return; - } - - if (string.IsNullOrWhiteSpace(InitStreamId)) { - if (!string.IsNullOrWhiteSpace(_lastSeededQuery) && - string.Equals(Input.Query, _lastSeededQuery, StringComparison.Ordinal)) - Input.Query = DefaultQuery; - - _lastSeededQuery = ""; - _lastLoadedLocation = ""; - return; - } - - var seed = StreamQuery(InitStreamId); - if (!string.Equals(Input.Query, DefaultQuery, StringComparison.Ordinal) && - !string.Equals(Input.Query, _lastSeededQuery, StringComparison.Ordinal)) - return; - - Input.Query = seed; - _lastSeededQuery = seed; - } - - private async Task RunQuery() { - try { - Result = await QueryBrowser.RunTransient(Input.Query); - StopInput.StopToken = ProtectStopTarget(Result.ProjectionName); - CommandResult = null; - } catch (OperationCanceledException) { - Result = QueryRunPage.Unavailable(Input.Query, "Query execution was canceled."); - } catch (Exception ex) { - Result = QueryRunPage.Unavailable(Input.Query, $"Failed to run query: {UiMessages.Friendly(ex)}"); - } - } - - private async Task StopProjection() { - if (!TryReadStopTarget(out var projectionName)) { - CommandResult = ProjectionCommandResult.Failure("", "Run or load a query projection before stopping it."); - return; - } - - Result = QueryRunPage.Success(Input.Query, projectionName); - StopInput.StopToken = ProtectStopTarget(projectionName); - try { - CommandResult = await Projections.Disable(projectionName); - } catch (OperationCanceledException) { - CommandResult = ProjectionCommandResult.Failure(projectionName, "Query break was canceled."); - } catch (Exception ex) { - CommandResult = ProjectionCommandResult.Failure(projectionName, $"Failed to stop query projection: {UiMessages.Friendly(ex)}"); - } - } - - private static string StreamQuery(string streamId) => - $"fromStream({System.Text.Json.JsonSerializer.Serialize(streamId)}).when({{\n $init: function () {{ return {{ count: 0 }}; }},\n $any: function (state, event) {{\n state.count += 1;\n }}\n}});"; - - private bool CanSeedQuery() => - string.Equals(Input.Query, DefaultQuery, StringComparison.Ordinal) || - string.Equals(Input.Query, _lastSeededQuery, StringComparison.Ordinal); - - private string ProtectStopTarget(string projectionName) => - string.IsNullOrWhiteSpace(projectionName) ? "" : StopTargetProtector.Protect(projectionName); - - private bool TryReadStopTarget(out string projectionName) { - projectionName = ""; - if (string.IsNullOrWhiteSpace(StopInput.StopToken)) - return false; - - try { - projectionName = StopTargetProtector.Unprotect(StopInput.StopToken); - return !string.IsNullOrWhiteSpace(projectionName); - } catch (CryptographicException) { - return false; - } - } - - private sealed class QueryInput { - [Required(ErrorMessage = "Enter a query before running it.")] - public string Query { get; set; } = ""; - } - - private sealed class QueryStopInput { - public string StopToken { get; set; } = ""; - } -} diff --git a/src/EventStore.ClusterNode/Components/Pages/StreamDetail.razor b/src/EventStore.ClusterNode/Components/Pages/StreamDetail.razor index 8dba8608c4..5215c260fe 100644 --- a/src/EventStore.ClusterNode/Components/Pages/StreamDetail.razor +++ b/src/EventStore.ClusterNode/Components/Pages/StreamDetail.razor @@ -23,7 +23,6 @@ { Delete } - Query diff --git a/src/EventStore.ClusterNode/Components/Services/ProjectionBrowserService.cs b/src/EventStore.ClusterNode/Components/Services/ProjectionBrowserService.cs index 10704dd56a..8fa8c29cd6 100644 --- a/src/EventStore.ClusterNode/Components/Services/ProjectionBrowserService.cs +++ b/src/EventStore.ClusterNode/Components/Services/ProjectionBrowserService.cs @@ -58,19 +58,18 @@ public sealed class ProjectionBrowserService( "Reading Speed Test Handler") }; - public async Task ReadAll(bool includeQueries, CancellationToken cancellationToken = default) + public async Task ReadAll(CancellationToken cancellationToken = default) { if (!await HasAccess(ListOperation, cancellationToken)) { - return ProjectionListPage.Unavailable("Projection list access was denied.", includeQueries); + return ProjectionListPage.Unavailable("Projection list access was denied."); } - var read = await ReadStatistics(includeQueries ? null : ProjectionMode.AllNonTransient, name: null, cancellationToken); + var read = await ReadStatistics(ProjectionMode.AllNonTransient, name: null, cancellationToken); return read.IsAvailable ? ProjectionListPage.Success( - read.Projections.OrderBy(x => x.Name, StringComparer.OrdinalIgnoreCase).ToArray(), - includeQueries) - : ProjectionListPage.Unavailable(read.Message, includeQueries); + read.Projections.OrderBy(x => x.Name, StringComparer.OrdinalIgnoreCase).ToArray()) + : ProjectionListPage.Unavailable(read.Message); } public async Task ReadProjection( @@ -294,9 +293,9 @@ public Task Reset(string name, CancellationToken cancel new ProjectionManagementMessage.RunAs(CurrentUser)), cancellationToken); - public async Task EnableAll(bool includeQueries, CancellationToken cancellationToken = default) + public async Task EnableAll(CancellationToken cancellationToken = default) { - var page = await ReadAll(includeQueries, cancellationToken); + var page = await ReadAll(cancellationToken); if (!page.IsAvailable) { return ProjectionBulkCommandResult.Failure(page.Message); @@ -306,9 +305,9 @@ public async Task EnableAll(bool includeQueries, Ca return await RunBulk(candidates, projection => Enable(projection.Name, cancellationToken), "enable"); } - public async Task DisableAll(bool includeQueries, CancellationToken cancellationToken = default) + public async Task DisableAll(CancellationToken cancellationToken = default) { - var page = await ReadAll(includeQueries, cancellationToken); + var page = await ReadAll(cancellationToken); if (!page.IsAvailable) { return ProjectionBulkCommandResult.Failure(page.Message); @@ -799,7 +798,6 @@ public static ProjectionDataRead Unavailable(string message) => public sealed record ProjectionListPage( IReadOnlyList Projections, - bool IncludeQueries, string Message) { public bool IsAvailable => string.IsNullOrWhiteSpace(Message); @@ -810,14 +808,12 @@ public sealed record ProjectionListPage( public string RunningCountLabel => IsAvailable ? RunningCount.ToString() : "-"; public string FaultedCountLabel => IsAvailable ? FaultedCount.ToString() : "-"; public string DisabledCountLabel => IsAvailable ? DisabledCount.ToString() : "-"; - public string ToggleQueriesHref => IncludeQueries ? "/ui/projections" : "/ui/projections?includeQueries=true"; - public string ToggleQueriesLabel => IncludeQueries ? "Hide transient queries" : "Include transient queries"; - public static ProjectionListPage Success(IReadOnlyList projections, bool includeQueries) => - new(projections, includeQueries, ""); + public static ProjectionListPage Success(IReadOnlyList projections) => + new(projections, ""); - public static ProjectionListPage Unavailable(string message, bool includeQueries = false) => - new(Array.Empty(), includeQueries, message); + public static ProjectionListPage Unavailable(string message) => + new(Array.Empty(), message); } public sealed record ProjectionDetailPage( diff --git a/src/EventStore.ClusterNode/Components/Services/QueryBrowserService.cs b/src/EventStore.ClusterNode/Components/Services/QueryBrowserService.cs deleted file mode 100644 index 5dd5f807c3..0000000000 --- a/src/EventStore.ClusterNode/Components/Services/QueryBrowserService.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System; -using System.Security.Claims; -using System.Threading; -using System.Threading.Tasks; -using EventStore.Core.Bus; -using EventStore.Core.Messaging; -using EventStore.Plugins.Authorization; -using EventStore.Projections.Core.Messages; -using EventStore.Projections.Core.Services; -using Microsoft.AspNetCore.Http; - -namespace EventStore.ClusterNode.Components.Services; - -public sealed class QueryBrowserService( - IPublisher publisher, - IAuthorizationProvider authorizationProvider, - IHttpContextAccessor httpContextAccessor) -{ - private static readonly TimeSpan ReadTimeout = TimeSpan.FromSeconds(10); - private static readonly Operation CreateQueryOperation = new Operation(Operations.Projections.Create) - .WithParameter(Operations.Projections.Parameters.Query); - - public async Task RunTransient(string query, CancellationToken cancellationToken = default) - { - if (string.IsNullOrWhiteSpace(query)) - { - return QueryRunPage.Unavailable(query ?? "", "Enter a query before running it."); - } - - if (!await HasAccess(CreateQueryOperation, cancellationToken)) - { - return QueryRunPage.Unavailable(query, "Transient query access was denied."); - } - - var projectionName = Guid.NewGuid().ToString("D"); - var envelope = new TaskCompletionEnvelope( - mapFailure: message => message is ProjectionManagementMessage.OperationFailed failed ? failed.Reason ?? "" : null); - - ProjectionManagementMessage.Updated completed; - try - { - publisher.Publish(new ProjectionManagementMessage.Command.Post( - envelope, - ProjectionMode.Transient, - projectionName, - new ProjectionManagementMessage.RunAs(CurrentUser), - "JS", - query, - enabled: true, - checkpointsEnabled: false, - emitEnabled: false, - trackEmittedStreams: false, - enableRunAs: true)); - - completed = await envelope.Task.WaitAsync(ReadTimeout, cancellationToken); - } - catch (TimeoutException) - { - return QueryRunPage.Unavailable(query, - $"Timed out creating transient query '{projectionName}'. It may still be visible temporarily before automatic expiry."); - } - catch (OperationCanceledException) - { - throw; - } - catch (Exception ex) - { - return QueryRunPage.Unavailable(query, $"Unable to create transient query: {UiMessages.Friendly(ex)}"); - } - - return QueryRunPage.Success(query, completed.Name); - } - - private ClaimsPrincipal CurrentUser => - httpContextAccessor.HttpContext?.User ?? new ClaimsPrincipal(new ClaimsIdentity()); - - private Task HasAccess(Operation operation, CancellationToken cancellationToken) => - authorizationProvider.CheckAccessAsync(CurrentUser, operation, cancellationToken).AsTask(); - -} - -public sealed record QueryRunPage( - string Query, - string ProjectionName, - string Message) -{ - public bool IsAvailable => string.IsNullOrWhiteSpace(Message); - public bool HasProjection => !string.IsNullOrWhiteSpace(ProjectionName); - public string ProjectionHref => HasProjection ? $"/ui/projections/{Uri.EscapeDataString(ProjectionName)}" : ""; - public string DebugHref => HasProjection ? $"/ui/projections/debug/{Uri.EscapeDataString(ProjectionName)}" : ""; - - public static QueryRunPage Success(string query, string projectionName) => new(query, projectionName, ""); - public static QueryRunPage Unavailable(string query, string message) => new(query, "", message); -} diff --git a/src/EventStore.ClusterNode/Program.cs b/src/EventStore.ClusterNode/Program.cs index e8f181a341..4265968fc7 100644 --- a/src/EventStore.ClusterNode/Program.cs +++ b/src/EventStore.ClusterNode/Program.cs @@ -276,7 +276,6 @@ async Task Run(ClusterVNodeHostedService hostedService, ManualResetEventSlim sig builder.Services.AddHttpContextAccessor(); builder.Services.AddRazorComponents(); builder.Services.AddScoped(); - builder.Services.AddScoped(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton();