Skip to content

Commit

Permalink
[release/8.0-preview5] Support custom styles for resource style (#3246)
Browse files Browse the repository at this point in the history
* Support custom styles for resource style - Added support for sending custom style to the dashboard via the resource server protocol. - Use this for azure resource to show more descriptive state updates.

* Make style updates a single field in the app model - This avoid reusing old state styles by mistake when changing text - Added KnownResourceStyles

* Fixed proto comments

---------

Co-authored-by: David Fowler <[email protected]>
  • Loading branch information
github-actions[bot] and davidfowl authored Mar 28, 2024
1 parent 5b40170 commit 590f3f7
Show file tree
Hide file tree
Showing 16 changed files with 137 additions and 54 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// The .NET Foundation licenses this file to you under the MIT license.
var builder = DistributedApplication.CreateBuilder(args);

builder.AddAzureProvisioning();

var storage = builder.AddAzureStorage("storage").RunAsEmulator(container =>
{
container.WithDataBindMount();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ public Task BeforeStartAsync(DistributedApplicationModel appModel, CancellationT
{
foreach (var resource in appModel.Resources.OfType<TestResource>())
{
var states = new[] { "Starting", "Running", "Finished" };
var states = new[] { "Starting", "Running", "Finished", "Uploading", "Downloading", "Processing", "Provisioning" };
var stateStyles = new[] { "info", "success", "warning", "error" };

var logger = loggerService.GetLogger(resource);

Expand All @@ -55,10 +56,10 @@ await notificationService.PublishUpdateAsync(resource, state => state with
while (await timer.WaitForNextTickAsync(_tokenSource.Token))
{
var randomState = states[Random.Shared.Next(0, states.Length)];
var randomStyle = stateStyles[Random.Shared.Next(0, stateStyles.Length)];
await notificationService.PublishUpdateAsync(resource, state => state with
{
State = randomState
State = new(randomState, randomStyle)
});
logger.LogInformation("Test resource {ResourceName} is now in state {State}", resource.Name, randomState);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,37 @@ else if (Resource is { State: /* unknown */ null or { Length: 0 } })
Color="Color.Neutral"
Class="severity-icon" />
}
else if (!string.IsNullOrEmpty(Resource.StateStyle))
{
switch (Resource.StateStyle)
{
case "warning":
<FluentIcon Icon="Icons.Filled.Size16.Warning"
Color="Color.Warning"
Class="severity-icon" />
break;
case "error":
<FluentIcon Icon="Icons.Filled.Size16.ErrorCircle"
Color="Color.Error"
Class="severity-icon" />
break;
case "success":
<FluentIcon Icon="Icons.Filled.Size16.Circle"
Color="Color.Success"
Class="severity-icon" />
break;
case "info":
<FluentIcon Icon="Icons.Filled.Size16.Circle"
Color="Color.Info"
Class="severity-icon" />
break;
default:
<FluentIcon Icon="Icons.Filled.Size16.Circle"
Color="Color.Neutral"
Class="severity-icon" />
break;
}
}
else
{
<FluentIcon Icon="Icons.Filled.Size16.CheckmarkCircle"
Expand Down

This file was deleted.

1 change: 1 addition & 0 deletions src/Aspire.Dashboard/Model/ResourceViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public sealed class ResourceViewModel
public required string DisplayName { get; init; }
public required string Uid { get; init; }
public required string? State { get; init; }
public required string? StateStyle { get; init; }
public required DateTime? CreationTimeStamp { get; init; }
public required ImmutableArray<EnvironmentVariableViewModel> Environment { get; init; }
public required ImmutableArray<UrlViewModel> Urls { get; init; }
Expand Down
1 change: 1 addition & 0 deletions src/Aspire.Dashboard/Protos/Partials.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public ResourceViewModel ToViewModel()
Environment = GetEnvironment(),
Urls = GetUrls(),
State = HasState ? State : null,
StateStyle = HasStateStyle ? StateStyle : null,
Commands = GetCommands()
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,6 @@ public async Task BeforeStartAsync(DistributedApplicationModel appModel, Cancell
.ToLookup(x => x.Root, x => x.Child);

// Sets the state of the resource and all of its children
Task SetStateAsync(IAzureResource resource, string state) =>
UpdateStateAsync(resource, s => s with
{
State = state
});

async Task UpdateStateAsync(IAzureResource resource, Func<CustomResourceSnapshot, CustomResourceSnapshot> stateFactory)
{
await notificationService.PublishUpdateAsync(resource, stateFactory).ConfigureAwait(false);
Expand All @@ -104,11 +98,19 @@ async Task AfterProvisionAsync(IAzureResource resource)
{
await resource.ProvisioningTaskCompletionSource!.Task.ConfigureAwait(false);

await SetStateAsync(resource, "Running").ConfigureAwait(false);
await UpdateStateAsync(resource, s => s with
{
State = new("Running", KnownResourceStateStyles.Success)
})
.ConfigureAwait(false);
}
catch (Exception)
{
await SetStateAsync(resource, "FailedToStart").ConfigureAwait(false);
await UpdateStateAsync(resource, s => s with
{
State = new("Failed to Provision", KnownResourceStateStyles.Error)
})
.ConfigureAwait(false);
}
}

Expand All @@ -117,7 +119,11 @@ async Task AfterProvisionAsync(IAzureResource resource)
{
r.ProvisioningTaskCompletionSource = new(TaskCreationOptions.RunContinuationsAsynchronously);

await SetStateAsync(r, "Starting").ConfigureAwait(false);
await UpdateStateAsync(r, s => s with
{
State = new("Starting", KnownResourceStateStyles.Info)
})
.ConfigureAwait(false);

// After the resource is provisioned, set its state
_ = AfterProvisionAsync(r);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ await notificationService.PublishUpdateAsync(resource, state =>
return state with
{
State = "Running",
State = new("Running", KnownResourceStateStyles.Success),
Urls = [.. portalUrls],
Properties = props
};
Expand All @@ -110,7 +110,7 @@ public override async Task GetOrCreateResourceAsync(AzureBicepResource resource,
await notificationService.PublishUpdateAsync(resource, state => state with
{
ResourceType = resource.GetType().Name,
State = "Starting",
State = new("Starting", KnownResourceStateStyles.Info),
Properties = [
new("azure.subscription.id", context.Subscription.Id.Name),
new("azure.resource.group", context.ResourceGroup.Id.Name),
Expand Down Expand Up @@ -152,6 +152,12 @@ await notificationService.PublishUpdateAsync(resource, state => state with

if (keyVault is null)
{
await notificationService.PublishUpdateAsync(resource, state => state with
{
State = new("Provisioning Keyvault", KnownResourceStateStyles.Info)
}).ConfigureAwait(false);

// A vault's name must be between 3-24 alphanumeric characters. The name must begin with a letter, end with a letter or digit, and not contain consecutive hyphens.
// Follow this link for more information: https://go.microsoft.com/fwlink/?linkid=2147742
var vaultName = $"v{Guid.NewGuid().ToString("N")[0..20]}";
Expand Down Expand Up @@ -190,6 +196,15 @@ await notificationService.PublishUpdateAsync(resource, state => state with
OnErrorData = data => resourceLogger.Log(LogLevel.Error, 0, data, null, (s, e) => s),
};

await notificationService.PublishUpdateAsync(resource, state =>
{
return state with
{
State = new("Compiling ARM template", KnownResourceStateStyles.Info)
};
})
.ConfigureAwait(false);

if (!await ExecuteCommand(templateSpec).ConfigureAwait(false))
{
throw new InvalidOperationException();
Expand All @@ -205,6 +220,15 @@ await notificationService.PublishUpdateAsync(resource, state => state with

var sw = Stopwatch.StartNew();

await notificationService.PublishUpdateAsync(resource, state =>
{
return state with
{
State = new("Creating ARM Deployment", KnownResourceStateStyles.Info)
};
})
.ConfigureAwait(false);

var operation = await deployments.CreateOrUpdateAsync(WaitUntil.Started, resource.Name, new ArmDeploymentContent(new(ArmDeploymentMode.Incremental)
{
Template = BinaryData.FromString(armTemplateContents.ToString()),
Expand All @@ -222,6 +246,7 @@ await notificationService.PublishUpdateAsync(resource, state =>
{
return state with
{
State = new("Waiting for Deployment", KnownResourceStateStyles.Info),
Urls = [.. state.Urls, new(Name: "deployment", Url: url, IsInternal: false)],
};
})
Expand Down Expand Up @@ -315,7 +340,7 @@ await notificationService.PublishUpdateAsync(resource, state =>
return state with
{
State = "Running",
State = new("Running", KnownResourceStateStyles.Success),
CreationTimeStamp = DateTime.UtcNow,
Properties = properties
};
Expand Down
43 changes: 42 additions & 1 deletion src/Aspire.Hosting/ApplicationModel/CustomResourceSnapshot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public sealed record CustomResourceSnapshot
/// <summary>
/// Represents the state of the resource.
/// </summary>
public string? State { get; init; }
public ResourceStateSnapshot? State { get; init; }

/// <summary>
/// The exit code of the resource.
Expand All @@ -44,7 +44,21 @@ public sealed record CustomResourceSnapshot
/// The URLs that should show up in the dashboard for this resource.
/// </summary>
public ImmutableArray<UrlSnapshot> Urls { get; init; } = [];
}

/// <summary>
/// A snapshot of the resource state
/// </summary>
/// <param name="Text">The text for the state update.</param>
/// <param name="Style">The style for the state update. Use <seealso cref="KnownResourceStateStyles"/> for the supported styles.</param>
public sealed record ResourceStateSnapshot(string Text, string? Style)
{
/// <summary>
/// Convert text to state snapshot. The style will be null by default
/// </summary>
/// <param name="s"></param>
public static implicit operator ResourceStateSnapshot?(string? s) =>
s is null ? null : new(Text: s, Style: null);
}

/// <summary>
Expand All @@ -69,3 +83,30 @@ public sealed record UrlSnapshot(string Name, string Url, bool IsInternal);
/// <param name="Name">The name of the property.</param>
/// <param name="Value">The value of the property.</param>
public sealed record ResourcePropertySnapshot(string Name, object? Value);

/// <summary>
/// The set of well known resource states
/// </summary>
public static class KnownResourceStateStyles
{
/// <summary>
/// The success state
/// </summary>
public static readonly string Success = "success";

/// <summary>
/// The error state. Useful for error messages.
/// </summary>
public static readonly string Error = "error";

/// <summary>
/// The info state. Useful for infomational messages.
/// </summary>
public static readonly string Info = "info";

/// <summary>
/// The warn state. Useful for showing warnings.
/// </summary>
public static readonly string Warn = "warn";

}
3 changes: 2 additions & 1 deletion src/Aspire.Hosting/Dashboard/DashboardServiceData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ static GenericResourceSnapshot CreateResourceSnapshot(IResource resource, string
Urls = snapshot.Urls,
Environment = snapshot.EnvironmentVariables,
ExitCode = snapshot.ExitCode,
State = snapshot.State
State = snapshot.State?.Text,
StateStyle = snapshot.State?.Style,
};
}
Expand Down
1 change: 1 addition & 0 deletions src/Aspire.Hosting/Dashboard/ResourceSnapshot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ internal abstract class ResourceSnapshot
public required string DisplayName { get; init; }
public required string Uid { get; init; }
public required string? State { get; init; }
public required string? StateStyle { get; init; }
public required int? ExitCode { get; init; }
public required DateTime? CreationTimeStamp { get; init; }
public required ImmutableArray<EnvironmentVariableSnapshot> Environment { get; init; }
Expand Down
1 change: 1 addition & 0 deletions src/Aspire.Hosting/Dashboard/proto/Partials.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public static Resource FromSnapshot(ResourceSnapshot snapshot)
DisplayName = snapshot.DisplayName,
Uid = snapshot.Uid,
State = snapshot.State ?? "",
StateStyle = snapshot.StateStyle ?? "",
};

if (snapshot.CreationTimeStamp.HasValue)
Expand Down
5 changes: 5 additions & 0 deletions src/Aspire.Hosting/Dashboard/proto/resource_service.proto
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,11 @@ message Resource {

// The list of urls that this resource exposes
repeated Url urls = 13;

// The style of the state. This is used to determine the state icon.
// Supported styles are "success", "info", "warning" and "error". Any other style
// will be treated as "unknown".
optional string state_style = 14;
}

////////////////////////////////////////////
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ private static ResourceViewModel CreateResource(ImmutableArray<UrlViewModel> url
Urls = urls,
Properties = FrozenDictionary<string, Value>.Empty,
State = null,
StateStyle = null,
Commands = []
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ private static ResourceViewModel CreateResource(string name, string? serviceAddr
Properties = FrozenDictionary<string, Value>.Empty,
Urls = servicePort is null || servicePort is null ? [] : [new UrlViewModel(name, new($"http://{serviceAddress}:{servicePort}"), isInternal: false)],
State = null,
StateStyle = null,
Commands = []
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,8 @@ private static GenericResourceSnapshot CreateResourceSnapshot(string name)
{
Name = name,
Uid = "",
State = "",
State = null,
StateStyle = null,
ExitCode = null,
CreationTimeStamp = null,
DisplayName = "",
Expand Down

0 comments on commit 590f3f7

Please sign in to comment.