-
Notifications
You must be signed in to change notification settings - Fork 680
Point at Gaffer and Navigator from the embedded UI #5695
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
George-Payne
wants to merge
4
commits into
master
Choose a base branch
from
embedded-ui-tool-links
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
c00bde8
Point at Gaffer from the embedded UI
George-Payne 5d14540
Open Navigator on this node from the sidebar
George-Payne c2cbbeb
Document the Tools sidebar links
George-Payne ae60440
Escape UTM values and stop guarding a required parameter
George-Payne File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| // Copyright (c) Kurrent, Inc and/or licensed to Kurrent, Inc under one or more agreements. | ||
| // Kurrent, Inc licenses this file to you under the Kurrent License v1 (see LICENSE.md). | ||
|
|
||
| using KurrentDB.Components.Shared; | ||
| using Xunit; | ||
|
|
||
| namespace KurrentDB.Components.Tests; | ||
|
|
||
| public class GafferLinkTests { | ||
| [Fact] | ||
| public void Carries_the_shared_attribution_and_the_placement() { | ||
| var url = GafferLink.For("projections_list"); | ||
|
|
||
| Assert.StartsWith("https://gaffer.kurrent.io/?", url); | ||
| Assert.Contains("utm_source=embedded-ui", url); | ||
| Assert.Contains("utm_medium=referral", url); | ||
| Assert.Contains("utm_campaign=projections", url); // the default | ||
| Assert.Contains("utm_content=projections_list", url); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void Campaign_can_be_overridden_for_a_surface_off_the_projections_pages() => | ||
| Assert.Contains("utm_campaign=tools&utm_content=sidebar", GafferLink.For("sidebar", campaign: "tools")); | ||
|
|
||
| // Reserved characters would otherwise split the query string and truncate the attribution silently. | ||
| [Fact] | ||
| public void Reserved_characters_are_escaped_rather_than_ending_the_parameter() { | ||
| var url = GafferLink.For("a&b=c d", campaign: "x&y"); | ||
|
|
||
| Assert.Contains("utm_campaign=x%26y", url); | ||
| Assert.Contains("utm_content=a%26b%3Dc%20d", url); | ||
| Assert.EndsWith("utm_content=a%26b%3Dc%20d", url); // nothing leaked into a new parameter | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| // Copyright (c) Kurrent, Inc and/or licensed to Kurrent, Inc under one or more agreements. | ||
| // Kurrent, Inc licenses this file to you under the Kurrent License v1 (see LICENSE.md). | ||
|
|
||
| using KurrentDB.Components.Shared; | ||
| using Xunit; | ||
|
|
||
| namespace KurrentDB.Components.Tests; | ||
|
|
||
| // The sidebar hands this string to the OS to launch Navigator, so a wrong scheme or a missing tls flag | ||
| // surfaces as "the app opened and then failed to connect", with nothing in this UI to explain why. | ||
| public class NavigatorLinkTests { | ||
| [Fact] | ||
| public void Single_node_uses_the_plain_scheme() => | ||
| Assert.Equal("kurrentdb://db.example.com:2113", | ||
| NavigatorLink.DeepLink("https://db.example.com:2113/", memberCount: 1)); | ||
|
|
||
| // Gossip hasn't reported yet: treat it as a single node rather than sending Navigator through discovery. | ||
| [Fact] | ||
| public void No_known_members_uses_the_plain_scheme() => | ||
| Assert.Equal("kurrentdb://db.example.com:2113", | ||
| NavigatorLink.DeepLink("https://db.example.com:2113/", memberCount: 0)); | ||
|
|
||
| [Fact] | ||
| public void Cluster_uses_the_discover_scheme() => | ||
| Assert.Equal("kurrentdb+discover://db.example.com:2113", | ||
| NavigatorLink.DeepLink("https://db.example.com:2113/", memberCount: 3)); | ||
|
|
||
| // An http node is running insecure; without this Navigator would attempt TLS and fail to connect. | ||
| [Fact] | ||
| public void Insecure_node_carries_tls_false() => | ||
| Assert.Equal("kurrentdb://localhost:2113?tls=false", | ||
| NavigatorLink.DeepLink("http://localhost:2113/", memberCount: 1)); | ||
|
|
||
| [Fact] | ||
| public void Insecure_cluster_carries_tls_false() => | ||
| Assert.Equal("kurrentdb+discover://localhost:2113?tls=false", | ||
| NavigatorLink.DeepLink("http://localhost:2113/", memberCount: 2)); | ||
|
|
||
| // The port is always explicit, including when it is the scheme default, so Navigator never has to guess. | ||
| [Fact] | ||
| public void Default_port_is_still_explicit() => | ||
| Assert.Equal("kurrentdb://db.example.com:443", | ||
| NavigatorLink.DeepLink("https://db.example.com/", memberCount: 1)); | ||
|
|
||
| [Fact] | ||
| public void Fallback_is_the_download_page_with_attribution() { | ||
| Assert.StartsWith("https://navigator.kurrent.io/", NavigatorLink.Fallback); | ||
| Assert.Contains("utm_source=embedded-ui", NavigatorLink.Fallback); | ||
| } | ||
| } |
138 changes: 138 additions & 0 deletions
138
src/KurrentDB.Components.Tests/ProjectionsGafferRibbonTests.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,138 @@ | ||
| // Copyright (c) Kurrent, Inc and/or licensed to Kurrent, Inc under one or more agreements. | ||
| // Kurrent, Inc licenses this file to you under the Kurrent License v1 (see LICENSE.md). | ||
|
|
||
| using System.Security.Claims; | ||
| using System.Threading.Tasks; | ||
| using Bunit; | ||
| using EventStore.Plugins.Authorization; | ||
| using KurrentDB.Components.Cluster; | ||
| using KurrentDB.Components.Projections; | ||
| using KurrentDB.Components.Tests.TestUtilities; | ||
| using KurrentDB.Core.Authorization; | ||
| using KurrentDB.Core.Bus; | ||
| using KurrentDB.Projections.Core.Messages; | ||
| using KurrentDB.Projections.Core.Services; | ||
| using Microsoft.AspNetCore.Components.Authorization; | ||
| using Microsoft.Extensions.DependencyInjection; | ||
| using Xunit; | ||
| // The component class name collides with its namespace; alias the type. | ||
| using ProjectionsPage = KurrentDB.Components.Projections.Projections; | ||
|
|
||
| namespace KurrentDB.Components.Tests; | ||
|
|
||
| // The list-page ribbon is unconditional on the working page: it shows regardless of what the grid holds, and | ||
| // stays out of the "not enabled" / "go to the leader" states. The detail-page link is user projections only. | ||
| public class ProjectionsGafferRibbonTests { | ||
| const string RibbonText = "author, debug, test and deploy projections"; | ||
| const string DetailLinkText = "Debug and deploy projections"; | ||
|
|
||
| static Task<AuthenticationState> AuthState(string name) => | ||
| Task.FromResult(new AuthenticationState( | ||
| new ClaimsPrincipal(new ClaimsIdentity([new Claim(ClaimTypes.Name, name)], authenticationType: "Test")))); | ||
|
|
||
| static ProjectionStatistics Projection(string name) => | ||
| new() { Name = name, Status = "Running", Mode = ProjectionMode.Continuous, Progress = 100 }; | ||
|
|
||
| // ProjectionsService is hand-built because "projections disabled" is a null publisher (see | ||
| // ProjectionsService.Available) and the container won't supply null. GossipMonitor needs an IPublisher of | ||
| // its own, hence the stand-in; unstarted, its CurrentState stays null, which is the state that renders | ||
| // the grid rather than the leader notice. | ||
| static BunitContext PageContext(IPublisher projections) => | ||
| MudBunit.NewContext(services => { | ||
| services.AddSingleton<IPublisher>(projections ?? new ReplyPublisher(_ => { })); | ||
| services.AddSingleton<IAuthorizationProvider>(new PassthroughAuthorizationProvider()); | ||
| services.AddScoped(sp => new ProjectionsService(projections, sp.GetRequiredService<IAuthorizationProvider>())); | ||
| services.AddSingleton<GossipMonitor>(); | ||
| }); | ||
|
|
||
| static IPublisher StatsPublisher(params ProjectionStatistics[] projections) => | ||
| new ReplyPublisher(msg => { | ||
| if (msg is ProjectionManagementMessage.Command.GetStatistics q) | ||
| q.Envelope.ReplyWith(new ProjectionManagementMessage.Statistics(projections)); | ||
| }); | ||
|
|
||
| // The detail page reads stats, then the source query, then the state. Every reply is supplied so no read | ||
| // falls through to its 5s timeout. | ||
| static IPublisher DetailPublisher(string name, string query) => | ||
| new ReplyPublisher(msg => { | ||
| switch (msg) { | ||
| case ProjectionManagementMessage.Command.GetStatistics s: | ||
| s.Envelope.ReplyWith(new ProjectionManagementMessage.Statistics([Projection(name)])); | ||
| break; | ||
| case ProjectionManagementMessage.Command.GetQuery q: | ||
| q.Envelope.ReplyWith(new ProjectionManagementMessage.ProjectionQuery( | ||
| name, query, emitEnabled: false, projectionType: "JS", trackEmittedStreams: false, | ||
| checkpointsEnabled: true, definition: null, outputConfig: null)); | ||
| break; | ||
| case ProjectionManagementMessage.Command.GetState st: | ||
| st.Envelope.ReplyWith(new ProjectionManagementMessage.ProjectionState( | ||
| name, partition: "", state: "{}", position: null)); | ||
| break; | ||
| } | ||
| }); | ||
|
|
||
| [Fact] | ||
| public async Task Ribbon_shows_when_the_grid_has_only_system_projections() { | ||
| await using var ctx = PageContext(StatsPublisher(Projection("$by_category"), Projection("$streams"))); | ||
|
|
||
| var cut = ctx.Render<ProjectionsPage>(p => p.AddCascadingValue(AuthState("admin"))); | ||
|
|
||
| cut.WaitForAssertion(() => { | ||
| Assert.Contains("$by_category", cut.Markup); | ||
| Assert.Contains(RibbonText, cut.Markup); | ||
| }); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task Ribbon_still_shows_once_user_projections_exist() { | ||
| await using var ctx = PageContext(StatsPublisher(Projection("order-totals"))); | ||
|
|
||
| var cut = ctx.Render<ProjectionsPage>(p => p.AddCascadingValue(AuthState("admin"))); | ||
|
|
||
| cut.WaitForAssertion(() => { | ||
| Assert.Contains("order-totals", cut.Markup); | ||
| Assert.Contains(RibbonText, cut.Markup); | ||
| }); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task Ribbon_is_absent_when_projections_are_disabled() { | ||
| await using var ctx = PageContext(projections: null!); | ||
|
|
||
| var cut = ctx.Render<ProjectionsPage>(p => p.AddCascadingValue(AuthState("admin"))); | ||
|
|
||
| cut.WaitForAssertion(() => { | ||
| Assert.Contains("Projections are not enabled on this server", cut.Markup); | ||
| Assert.DoesNotContain(RibbonText, cut.Markup); | ||
| }); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task Detail_offers_the_link_for_a_user_projection() { | ||
| await using var ctx = PageContext(DetailPublisher("order-totals", "fromStream('orders')")); | ||
|
|
||
| var cut = ctx.Render<ProjectionDetail>(p => p | ||
| .Add(d => d.Name, "order-totals") | ||
| .AddCascadingValue(AuthState("admin"))); | ||
|
|
||
| cut.WaitForAssertion(() => { | ||
| Assert.Contains("fromStream", cut.Markup); | ||
| Assert.Contains(DetailLinkText, cut.Markup); | ||
| }); | ||
| } | ||
|
|
||
| // System projections ship with the server, so there is nothing to author locally and no link to offer. | ||
| [Fact] | ||
| public async Task Detail_withholds_the_link_for_a_system_projection() { | ||
| await using var ctx = PageContext(DetailPublisher("$by_category", "fromAll()")); | ||
|
|
||
| var cut = ctx.Render<ProjectionDetail>(p => p | ||
| .Add(d => d.Name, "$by_category") | ||
| .AddCascadingValue(AuthState("admin"))); | ||
|
|
||
| cut.WaitForAssertion(() => { | ||
| Assert.Contains("fromAll", cut.Markup); // the Source section did render | ||
| Assert.DoesNotContain(DetailLinkText, cut.Markup); | ||
| }); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.