Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
6b4c159
Split application settings and host settings
mikeminutillo Jan 15, 2026
d7fa796
Extract App from Host
mikeminutillo Jan 15, 2026
adbec3f
Extract core from host
mikeminutillo Jan 15, 2026
1dceb07
Add public API documentation
mikeminutillo Jan 15, 2026
2baf8f4
Rename Settings to make it clear in other contexts
mikeminutillo Jan 15, 2026
b1deaaf
Skip AutomaticVersionRange on MS Dependency
mikeminutillo Jan 15, 2026
2f5c197
Expect an additional nuget
mikeminutillo Jan 15, 2026
e7e7bef
Serving files off disk is optional
mikeminutillo Jan 16, 2026
d66f45a
Indicate to UI if running in embedded mode
mikeminutillo Jan 16, 2026
2c75cbe
Adjust SP UI when embedded
mikeminutillo Jan 16, 2026
428524e
Cleanup
mikeminutillo Jan 16, 2026
619535c
Update StaticMiddlewareTests.cs
mikeminutillo Jan 23, 2026
450e8b1
Use passed ServiceControl url if embedded
mikeminutillo Jan 23, 2026
78f431d
Allow serving constants file anonymously
mikeminutillo Jan 28, 2026
d654bd4
Remove unused method
mikeminutillo Jan 28, 2026
38492b3
Remove unused method 2
mikeminutillo Jan 28, 2026
c3d4987
Apply suggestions from code review
mikeminutillo Feb 4, 2026
1d95ae2
Cleanup
mikeminutillo Feb 4, 2026
0eac598
Fix whitespace
mikeminutillo Feb 4, 2026
cd06a6a
Put hosting extension in namespace
mikeminutillo Feb 6, 2026
41f596d
Change embedded to integrated
mikeminutillo Feb 6, 2026
eae680a
Update to .NET 10
mikeminutillo Feb 6, 2026
0108fed
Fix dotnet versions used for actions
mikeminutillo Feb 6, 2026
07460e1
Use global json file
mikeminutillo Feb 6, 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 .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
- name: Setup .NET SDK
uses: actions/setup-dotnet@v5.1.0
with:
dotnet-version: 8.0.x
global-json-file: global.json
- name: Set up Node.js
uses: actions/setup-node@v6.1.0
with:
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
- name: Setup .NET SDK
uses: actions/setup-dotnet@v5.1.0
with:
dotnet-version: 7.0.x
global-json-file: global.json
- name: Set up Node.js
uses: actions/setup-node@v6.1.0
with:
Expand Down Expand Up @@ -82,7 +82,7 @@ jobs:
$nugetsCount = (Get-ChildItem -Recurse -File nugets).Count

$expectedAssetsCount = 1
$expectedNugetsCount = 1
$expectedNugetsCount = 2

if ($assetsCount -ne $expectedAssetsCount)
{
Expand Down
3 changes: 2 additions & 1 deletion global.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"sdk": {
"version": "8.0.400",
"version": "10.0.0",
"allowPrerelease": false,
"rollForward": "latestFeature"
}
}
1 change: 1 addition & 0 deletions src/Frontend/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ declare global {
service_control_url: string;
monitoring_urls: string[];
showPendingRetry: boolean;
isIntegrated?: boolean;
};
}
}
1 change: 1 addition & 0 deletions src/Frontend/public/js/app.constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ window.defaultConfig = {
service_control_url: 'http://localhost:33333/api/',
monitoring_urls: ['http://localhost:33633/'],
showPendingRetry: false,
isIntegrated: false,
};
14 changes: 9 additions & 5 deletions src/Frontend/src/components/PageFooter.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const environment = environmentAndVersionsStore.environment;
const licenseStore = useLicenseStore();
const { licenseStatus, license } = licenseStore;
const isMonitoringEnabled = monitoringClient.isMonitoringEnabled;
const isIntegrated = window.defaultConfig.isIntegrated;

const scAddressTooltip = computed(() => {
return `ServiceControl URL ${serviceControlClient.url}`;
Expand All @@ -44,11 +45,14 @@ const { configuration } = storeToRefs(configurationStore);
<RouterLink :to="routeLinks.configuration.endpointConnection.link">Connect new endpoint</RouterLink>
</span>

<span v-if="!newVersions.newSPVersion.newspversion && environment.sp_version"> ServicePulse v{{ environment.sp_version }} </span>
<span v-if="newVersions.newSPVersion.newspversion && environment.sp_version">
ServicePulse v{{ environment.sp_version }} (<FAIcon v-if="newVersions.newSPVersion.newspversionnumber" class="footer-icon fake-link" :icon="faArrowTurnUp" />
<a :href="newVersions.newSPVersion.newspversionlink" target="_blank">v{{ newVersions.newSPVersion.newspversionnumber }} available</a>)
</span>
<span v-if="isIntegrated"> ServicePulse: Integrated </span>
<template v-else-if="environment.sp_version">
<span v-if="!newVersions.newSPVersion.newspversion"> ServicePulse v{{ environment.sp_version }} </span>
<span v-else>
ServicePulse v{{ environment.sp_version }} (<FAIcon v-if="newVersions.newSPVersion.newspversionnumber" class="footer-icon fake-link" :icon="faArrowTurnUp" />
<a :href="newVersions.newSPVersion.newspversionlink" target="_blank">v{{ newVersions.newSPVersion.newspversionnumber }} available</a>)
</span>
</template>
<span :title="scAddressTooltip">
Service Control:
<span class="connected-status" v-if="connectionState.connected && !connectionState.connecting">
Expand Down
21 changes: 14 additions & 7 deletions src/Frontend/src/components/configuration/PlatformConnections.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const serviceControlValid = ref<boolean | null>(null);
const testingMonitoring = ref(false);
const monitoringValid = ref<boolean | null>(null);
const connectionSaved = ref<boolean | null>(null);
const isIntegrated = window.defaultConfig.isIntegrated;

async function testServiceControlUrl() {
if (localServiceControlUrl.value) {
Expand Down Expand Up @@ -64,10 +65,15 @@ function saveConnections() {
}

function updateServiceControlUrls() {
if (!localServiceControlUrl.value) {
throw new Error("ServiceControl URL is mandatory");
} else if (!localServiceControlUrl.value.endsWith("/")) {
localServiceControlUrl.value += "/";
const params = new URLSearchParams();

if (!isIntegrated) {
if (!localServiceControlUrl.value) {
throw new Error("ServiceControl URL is mandatory");
} else if (!localServiceControlUrl.value.endsWith("/")) {
localServiceControlUrl.value += "/";
}
params.set("scu", localServiceControlUrl.value);
}

if (!localMonitoringUrl.value) {
Expand All @@ -76,8 +82,6 @@ function updateServiceControlUrls() {
localMonitoringUrl.value += "/";
}

const params = new URLSearchParams();
params.set("scu", localServiceControlUrl.value);
params.set("mu", localMonitoringUrl.value);
window.location.search = `?${params.toString()}`;
}
Expand All @@ -94,11 +98,14 @@ function updateServiceControlUrls() {
<div class="col-7 form-group">
<label for="serviceControlUrl">
CONNECTION URL
<template v-if="isIntegrated">
<span>(INTEGRATED)</span>
</template>
<template v-if="connectionState.unableToConnect">
<span class="failed-validation"><FAIcon :icon="faExclamationTriangle" /> Unable to connect </span>
</template>
</label>
<input type="text" id="serviceControlUrl" name="serviceControlUrl" v-model="localServiceControlUrl" class="form-control" style="color: #000" required />
<input type="text" id="serviceControlUrl" name="serviceControlUrl" v-model="localServiceControlUrl" class="form-control" style="color: #000" required :disabled="isIntegrated" />
</div>

<div class="col-5 no-side-padding">
Expand Down
4 changes: 4 additions & 0 deletions src/Frontend/src/components/serviceControlClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ class ServiceControlClient {
}

private getUrl() {
if (window.defaultConfig?.isIntegrated && window.defaultConfig.service_control_url?.length) {
return window.defaultConfig.service_control_url;
}

const searchParams = new URLSearchParams(window.location.search);
const scu = searchParams.get("scu");
const existingScu = window.localStorage.getItem("scu");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<IncludeBuildOutput>false</IncludeBuildOutput>
<SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
<Description>Particular ServicePulse binaries for use by Particular.PlatformSample. Not intended for use outside of Particular.PlatformSample.</Description>
Expand Down
41 changes: 41 additions & 0 deletions src/ServicePulse.Core/ConstantsFile.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
namespace ServicePulse;

using System.Reflection;

class ConstantsFile
{
public static string GetContent(ServicePulseSettings settings)
{
var version = GetVersionInformation();

var constantsFile = $$"""
window.defaultConfig = {
default_route: '{{settings.DefaultRoute}}',
version: '{{version}}',
service_control_url: '{{settings.ServiceControlUrl}}',
monitoring_urls: ['{{settings.MonitoringUrl ?? "!"}}'],
showPendingRetry: {{settings.ShowPendingRetry.ToString().ToLower()}},
isIntegrated: {{settings.IsIntegrated.ToString().ToLower()}}
}
""";

return constantsFile;
}

static string GetVersionInformation()
{
var majorMinorPatch = "0.0.0";

var attributes = typeof(ConstantsFile).Assembly.GetCustomAttributes<AssemblyMetadataAttribute>();

foreach (var attribute in attributes)
{
if (attribute.Key == "MajorMinorPatch")
{
majorMinorPatch = attribute.Value ?? "0.0.0";
}
}

return majorMinorPatch;
}
}
23 changes: 23 additions & 0 deletions src/ServicePulse.Core/ServicePulse.Core.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="10.0.2" AutomaticVersionRange="false" />
<PackageReference Include="Particular.Packaging" Version="4.5.0" PrivateAssets="All" />
</ItemGroup>

<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>

<ItemGroup>
<EmbeddedResource Include="..\Frontend\dist\**\*" Exclude="..\Frontend\dist\js\**\*" LinkBase="wwwroot" />
</ItemGroup>

</Project>
37 changes: 37 additions & 0 deletions src/ServicePulse.Core/ServicePulseHostingExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
namespace ServicePulse;

using System.Net.Mime;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.FileProviders;

/// <summary>
/// Extensions for hosting ServicePulse within a WebApplication.
/// </summary>
public static class ServicePulseHostingExtensions
{
/// <summary>
/// Adds ServicePulse static file serving and configuration endpoint to the WebApplication.
/// </summary>
public static void UseServicePulse(this WebApplication app, ServicePulseSettings settings, IFileProvider? overrideFileProvider = null)
{
var manifestEmbeddedFileProvider = new ManifestEmbeddedFileProvider(typeof(ServicePulseHostingExtensions).Assembly, "wwwroot");
IFileProvider fileProvider = overrideFileProvider is null
? manifestEmbeddedFileProvider
: new CompositeFileProvider(overrideFileProvider, manifestEmbeddedFileProvider);

var defaultFilesOptions = new DefaultFilesOptions { FileProvider = fileProvider };
app.UseDefaultFiles(defaultFilesOptions);

var staticFileOptions = new StaticFileOptions { FileProvider = fileProvider };
app.UseStaticFiles(staticFileOptions);

var constantsFile = ConstantsFile.GetContent(settings);

app.MapGet("/js/app.constants.js", (HttpContext context) =>
{
context.Response.ContentType = MediaTypeNames.Text.JavaScript;
return constantsFile;
}).AllowAnonymous();
}
}
112 changes: 112 additions & 0 deletions src/ServicePulse.Core/ServicePulseSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
namespace ServicePulse;

using System.Text.Json;

/// <summary>
/// Application Settings for ServicePulse.
/// </summary>
public record ServicePulseSettings
{
/// <summary>
/// The location of the ServiceControl API.
/// </summary>
public required string ServiceControlUrl { get; init; }

/// <summary>
/// The location of the ServiceControl Monitoring API.
/// </summary>
public required string? MonitoringUrl { get; init; }

/// <summary>
/// The default route to navigate to from the root.
/// </summary>
public required string DefaultRoute { get; init; }

/// <summary>
/// Flag to enable the pending retry feature.
/// </summary>
public required bool ShowPendingRetry { get; init; }

/// <summary>
/// Flag to indicate if ServicePulse is running in integrated mode.
/// </summary>
public required bool IsIntegrated { get; init; }

/// <summary>
/// Loads the settings from environment variables.
/// </summary>
public static ServicePulseSettings GetFromEnvironmentVariables()
{
var serviceControlUrl = Environment.GetEnvironmentVariable("SERVICECONTROL_URL") ?? "http://localhost:33333/api/";

if (!serviceControlUrl.EndsWith("/", StringComparison.Ordinal))
{
serviceControlUrl += "/";
}

if (!serviceControlUrl.EndsWith("api/", StringComparison.Ordinal))
{
serviceControlUrl += "api/";
}

var serviceControlUri = new Uri(serviceControlUrl);

var monitoringUrls = ParseLegacyMonitoringValue(Environment.GetEnvironmentVariable("MONITORING_URLS"));
var monitoringUrl = Environment.GetEnvironmentVariable("MONITORING_URL");

monitoringUrl ??= monitoringUrls;
monitoringUrl ??= "http://localhost:33633/";

var monitoringUri = monitoringUrl == "!" ? null : new Uri(monitoringUrl);

var defaultRoute = Environment.GetEnvironmentVariable("DEFAULT_ROUTE") ?? "/dashboard";

var showPendingRetryValue = Environment.GetEnvironmentVariable("SHOW_PENDING_RETRY");
bool.TryParse(showPendingRetryValue, out var showPendingRetry);

return new ServicePulseSettings
{
ServiceControlUrl = serviceControlUri.ToString(),
MonitoringUrl = monitoringUri?.ToString(),
DefaultRoute = defaultRoute,
ShowPendingRetry = showPendingRetry,
IsIntegrated = false
};
}

static string? ParseLegacyMonitoringValue(string? value)
{
if (value is null)
{
return null;
}

var cleanedValue = value.Replace('\'', '"');
var json = $$"""{"Addresses":{{cleanedValue}}}""";

MonitoringUrls? result;

try
{
result = JsonSerializer.Deserialize<MonitoringUrls>(json);
}
catch (JsonException)
{
return null;
}

var addresses = result?.Addresses;

if (addresses is not null && addresses.Length > 0)
{
return addresses[0];
}

return null;
}

class MonitoringUrls
{
public string[] Addresses { get; set; } = [];
}
}
2 changes: 1 addition & 1 deletion src/ServicePulse.Host.Tests/Owin/StaticMiddlewareTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ public async Task Should_find_prefer_constants_file_on_disk_over_embedded_if_bot
}
};
await middleware.Invoke(context);
const long sizeOfFileOnDisk = 215; // this is the /app/js/app.constants.js file
const long sizeOfFileOnDisk = 239; // this is the /app/js/app.constants.js file
Assert.That(context.Response.ContentLength, Is.EqualTo(sizeOfFileOnDisk));
Assert.That(context.Response.ContentType, Is.EqualTo("application/javascript"));
}
Expand Down
6 changes: 1 addition & 5 deletions src/ServicePulse.Host/Hosting/HostArguments.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@ namespace ServicePulse.Host.Hosting
{
using System;
using System.Collections.Generic;
#if DEBUG
using System.Diagnostics;
#endif
using System.IO;
using System.Linq;
using System.Reflection;
Expand Down Expand Up @@ -355,8 +352,7 @@ void ValidateArgs()
goto case ExecutionMode.Run;

case ExecutionMode.Run:
Uri spUri;
if (!Uri.TryCreate(Url, UriKind.Absolute, out spUri) || (!validProtocols.Contains(spUri.Scheme, StringComparer.OrdinalIgnoreCase)))
if (!Uri.TryCreate(Url, UriKind.Absolute, out Uri spUri) || (!validProtocols.Contains(spUri.Scheme, StringComparer.OrdinalIgnoreCase)))
{
throw new Exception("The value specified for 'url' is not a valid URL");
}
Expand Down
Loading