Skip to content

Commit 2aa6edc

Browse files
Feart: add Spectre Console for enhanced output (#17)
* Add Spectre.Console for enhanced output Co-authored-by: Copilot <[email protected]> * Update src/Console/SpectreConsoleReporter.cs Co-authored-by: Copilot <[email protected]> * Update src/Console/SpectreConsoleReporter.cs Co-authored-by: Copilot <[email protected]> * Update src/Console/SpectreConsoleReporter.cs Co-authored-by: Copilot <[email protected]> * Update src/Console/SpectreConsoleReporter.cs Co-authored-by: Copilot <[email protected]> --------- Co-authored-by: Copilot <[email protected]>
1 parent 211a124 commit 2aa6edc

File tree

4 files changed

+183
-3
lines changed

4 files changed

+183
-3
lines changed

src/CloudFlareExtension.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,6 @@
1717
<PackageReference Include="System.Net.Http.Json" Version="9.0.10" />
1818
<PackageReference Include="Nerdbank.GitVersioning" Version="3.8.118" PrivateAssets="All" />
1919
<PackageReference Include="Bicep.LocalDeploy" Version="1.0.1" />
20+
<PackageReference Include="Spectre.Console" Version="0.50.0" />
2021
</ItemGroup>
2122
</Project>
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
using System;
2+
using System.Threading;
3+
using System.Threading.Tasks;
4+
using CloudFlareExtension.Models;
5+
using Spectre.Console;
6+
7+
namespace CloudFlareExtension.ConsoleOutput;
8+
9+
internal static class SpectreConsoleReporter
10+
{
11+
private const string DisableEnvironmentVariable = "CLOUDFLARE_EXTENSION_NO_CONSOLE";
12+
13+
private static readonly Lazy<bool> Interactive = new(() =>
14+
!System.Console.IsOutputRedirected &&
15+
string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(DisableEnvironmentVariable)));
16+
17+
private static readonly Lazy<IAnsiConsole> LazyConsole = new(() =>
18+
{
19+
var settings = new AnsiConsoleSettings
20+
{
21+
Out = new AnsiConsoleOutput(System.Console.Error)
22+
};
23+
24+
return AnsiConsole.Create(settings);
25+
});
26+
27+
internal static bool IsEnabled => Interactive.Value;
28+
29+
private static IAnsiConsole Ansi => LazyConsole.Value;
30+
31+
internal static async Task<T> RunOperationAsync<T>(string description, Func<CancellationToken, Task<T>> operation, CancellationToken cancellationToken)
32+
{
33+
if (!IsEnabled)
34+
{
35+
return await operation(cancellationToken);
36+
}
37+
38+
var escapedDescription = Markup.Escape(description);
39+
T? result = default;
40+
Exception? failure = null;
41+
42+
await Ansi.Progress()
43+
.Columns(new TaskDescriptionColumn(), new SpinnerColumn(), new ProgressBarColumn(), new PercentageColumn())
44+
.StartAsync(async ctx =>
45+
{
46+
var task = ctx.AddTask(escapedDescription, autoStart: true);
47+
try
48+
{
49+
result = await operation(cancellationToken);
50+
task.Value = task.MaxValue;
51+
}
52+
catch (Exception ex)
53+
{
54+
failure = ex;
55+
task.StopTask();
56+
}
57+
});
58+
59+
if (failure is not null)
60+
{
61+
throw failure;
62+
}
63+
64+
return result!;
65+
}
66+
67+
internal static void WriteInfo(string message)
68+
{
69+
if (!IsEnabled)
70+
{
71+
return;
72+
}
73+
74+
Ansi.MarkupLine($"[cyan]{Markup.Escape(message)}[/]");
75+
}
76+
77+
internal static void WriteSuccess(string message)
78+
{
79+
if (!IsEnabled)
80+
{
81+
return;
82+
}
83+
84+
Ansi.MarkupLine($"[green]✔ {Markup.Escape(message)}[/]");
85+
}
86+
87+
internal static void WriteWarning(string message)
88+
{
89+
if (!IsEnabled)
90+
{
91+
return;
92+
}
93+
94+
Ansi.MarkupLine($"[yellow]⚠ {Markup.Escape(message)}[/]");
95+
}
96+
97+
internal static void RenderDnsRecordSummary(CloudFlareDnsRecord record)
98+
{
99+
if (!IsEnabled)
100+
{
101+
return;
102+
}
103+
104+
var table = CreateSummaryTable("DNS Record");
105+
table.AddRow("Name", record.Name);
106+
table.AddRow("Type", record.Type);
107+
table.AddRow("Content", record.Content);
108+
table.AddRow("TTL", record.Ttl.ToString());
109+
table.AddRow("Proxied", record.Proxied.ToString());
110+
table.AddRow("Proxiable", record.Proxiable.ToString());
111+
table.AddRow("Zone Id", record.ZoneId);
112+
table.AddRow("Record Id", record.RecordId ?? "(pending)");
113+
if (!string.IsNullOrWhiteSpace(record.Comment))
114+
{
115+
table.AddRow("Comment", record.Comment!);
116+
}
117+
118+
Ansi.Write(table);
119+
}
120+
121+
internal static void RenderZoneSummary(CloudFlareZone zone, bool existedPrior)
122+
{
123+
if (!IsEnabled)
124+
{
125+
return;
126+
}
127+
128+
var table = CreateSummaryTable(existedPrior ? "Existing Zone" : "Created Zone");
129+
table.AddRow("Name", zone.Name);
130+
table.AddRow("Zone Id", zone.ZoneId ?? "(pending)");
131+
table.AddRow("Status", zone.Status);
132+
table.AddRow("Plan", zone.Plan);
133+
table.AddRow("Paused", zone.Paused.ToString());
134+
if (zone.NameServers is { Length: > 0 })
135+
{
136+
table.AddRow("Name Servers", string.Join(", ", zone.NameServers));
137+
}
138+
139+
Ansi.Write(table);
140+
}
141+
142+
private static Table CreateSummaryTable(string title)
143+
{
144+
var table = new Table
145+
{
146+
Title = new TableTitle($"[bold]{Markup.Escape(title)}[/]")
147+
};
148+
149+
table.Border(TableBorder.Rounded);
150+
table.Expand();
151+
table.AddColumn(new TableColumn("[grey]Property[/]"));
152+
table.AddColumn(new TableColumn("[grey]Value[/]"));
153+
154+
return table;
155+
}
156+
}

src/Handlers/CloudFlareDnsRecordHandler.cs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Bicep.Local.Extension.Host.Handlers;
2+
using CloudFlareExtension.ConsoleOutput;
23
using CloudFlareExtension.Models;
34
using CloudFlareExtension.Services;
45

@@ -25,14 +26,24 @@ protected override async Task<ResourceResponse> CreateOrUpdate(ResourceRequest r
2526
throw new InvalidOperationException($"ZoneId is required for DNS record '{request.Properties.Name}'. Please provide the CloudFlare Zone ID in your Bicep template.");
2627
}
2728

28-
// Create the DNS record
29-
var createdRecord = await apiService.CreateDnsRecordAsync(request.Properties, request.Properties.ZoneId, cancellationToken);
29+
SpectreConsoleReporter.WriteInfo(
30+
$"Applying DNS record '{request.Properties.Name}' ({request.Properties.Type}) in zone '{request.Properties.ZoneName}'.");
31+
32+
// Create the DNS record (with interactive progress when available)
33+
var createdRecord = await SpectreConsoleReporter.RunOperationAsync(
34+
$"Cloudflare DNS API -> {request.Properties.Type} {request.Properties.Name}",
35+
ct => apiService.CreateDnsRecordAsync(request.Properties, request.Properties.ZoneId, ct),
36+
cancellationToken);
3037

3138
// Update properties with the created record data
3239
request.Properties.RecordId = createdRecord.RecordId;
3340
request.Properties.ZoneId = createdRecord.ZoneId;
3441
request.Properties.Proxiable = createdRecord.Proxiable;
3542

43+
SpectreConsoleReporter.WriteSuccess(
44+
$"DNS record '{request.Properties.Name}' ({request.Properties.Type}) synced successfully.");
45+
SpectreConsoleReporter.RenderDnsRecordSummary(request.Properties);
46+
3647
return GetResponse(request);
3748
}
3849
catch (Exception ex)

src/Handlers/CloudFlareZoneHandler.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Bicep.Local.Extension.Host.Handlers;
2+
using CloudFlareExtension.ConsoleOutput;
23
using CloudFlareExtension.Models;
34
using CloudFlareExtension.Services;
45

@@ -19,6 +20,8 @@ protected override async Task<ResourceResponse> CreateOrUpdate(ResourceRequest r
1920
var config = Configuration.GetConfiguration();
2021
using var apiService = new CloudFlareApiService(config);
2122

23+
SpectreConsoleReporter.WriteInfo($"Ensuring CloudFlare zone '{request.Properties.Name}' exists.");
24+
2225
// Check if zone already exists
2326
var existingZone = await apiService.GetZoneAsync(request.Properties.Name, cancellationToken);
2427

@@ -29,16 +32,25 @@ protected override async Task<ResourceResponse> CreateOrUpdate(ResourceRequest r
2932
request.Properties.Status = existingZone.Status;
3033
request.Properties.NameServers = existingZone.NameServers;
3134
request.Properties.Paused = existingZone.Paused;
35+
36+
SpectreConsoleReporter.WriteWarning($"Zone '{request.Properties.Name}' already exists. Skipping creation.");
37+
SpectreConsoleReporter.RenderZoneSummary(request.Properties, existedPrior: true);
3238
}
3339
else
3440
{
3541
// Create new zone
36-
var createdZone = await apiService.CreateZoneAsync(request.Properties, cancellationToken);
42+
var createdZone = await SpectreConsoleReporter.RunOperationAsync(
43+
$"Create CloudFlare zone '{request.Properties.Name}'",
44+
ct => apiService.CreateZoneAsync(request.Properties, ct),
45+
cancellationToken);
3746

3847
// Update properties with the created zone data
3948
request.Properties.ZoneId = createdZone.ZoneId;
4049
request.Properties.Status = createdZone.Status;
4150
request.Properties.NameServers = createdZone.NameServers;
51+
52+
SpectreConsoleReporter.WriteSuccess($"Created CloudFlare zone '{request.Properties.Name}'.");
53+
SpectreConsoleReporter.RenderZoneSummary(request.Properties, existedPrior: false);
4254
}
4355

4456
return GetResponse(request);

0 commit comments

Comments
 (0)