diff --git a/Blocktrust.CredentialWorkflow.Web/Components/Pages/Log.razor b/Blocktrust.CredentialWorkflow.Web/Components/Pages/Log.razor index 180421e..79fa257 100644 --- a/Blocktrust.CredentialWorkflow.Web/Components/Pages/Log.razor +++ b/Blocktrust.CredentialWorkflow.Web/Components/Pages/Log.razor @@ -1,10 +1,10 @@ @page "/logs" +@using System.Text.Json @using Blocktrust.CredentialWorkflow.Core.Commands.WorkflowOutcome.GetWorkflowOutcomes @using Blocktrust.CredentialWorkflow.Web.Components.Layout @using Blocktrust.CredentialWorkflow.Web.Services @using MediatR @using Microsoft.AspNetCore.Components.Authorization -@using Blocktrust.CredentialWorkflow.Core.Domain.Common @using Blocktrust.CredentialWorkflow.Core.Domain.Enums @using Blocktrust.CredentialWorkflow.Core.Domain.ProcessFlow.Actions @using Blocktrust.CredentialWorkflow.Core.Domain.Workflow @@ -14,21 +14,22 @@ @inject IMediator Mediator @inject AppStateService AppStateService @inject ILogger Logger +@inject IJSRuntime JSRuntime @rendermode @(new InteractiveServerRenderMode(prerender: false)) @attribute [Authorize()] @layout AppLayout - Blocktrust Credential Workflow Platform - Logs @if (AppStateService.IsInitialized) { -
+
-
- @foreach (var workflow in AppStateService.WorkflowSummaries.OrderByDescending(w => w.UpdatedUtc)) { @@ -37,76 +38,189 @@
-
+
@if (workflowOutcomes.Any()) { - - - - - - - - - - @foreach (var workflowOutcome in workflowOutcomes) - { - - - - - - @if (expandedWorkflowOutcomeId == workflowOutcome.WorkflowOutcomeId) - { - - - - } - } - -
StateStartedEnded
- - @workflowOutcome.WorkflowOutcomeState - - @workflowOutcome.StartedUtc?.ToString("g")@workflowOutcome.EndedUtc?.ToString("g")
-
- @if (workflowOutcome.WorkflowOutcomeState == EWorkflowOutcomeState.Success) - { -

- ActionWorkflowOutcome - JSON: @(string.IsNullOrEmpty(workflowOutcome.ActionOutcomesJson) ? "N/A" : workflowOutcome.ActionOutcomesJson) -

-

- Execution Context - @(string.IsNullOrEmpty(workflowOutcome.ExecutionContext) ? "N/A" : workflowOutcome.ExecutionContext) -

- } - else if (workflowOutcome.WorkflowOutcomeState == EWorkflowOutcomeState.NotStarted) - { -

This operation did not start yet.

- } - else - { -

- ActionWorkflowOutcome - JSON: @(string.IsNullOrEmpty(workflowOutcome.ActionOutcomesJson) ? "N/A" : workflowOutcome.ActionOutcomesJson) -

-

- Error - JSON: @(string.IsNullOrEmpty(workflowOutcome.ErrorJson) ? "N/A" : workflowOutcome.ErrorJson) -

-

- Execution Context - @(string.IsNullOrEmpty(workflowOutcome.ExecutionContext) ? "N/A" : workflowOutcome.ExecutionContext) -

- } -
-
+
+
+ + + + + + + + + + @foreach (var workflowOutcome in workflowOutcomes) + { + + + + + + @if (expandedWorkflowOutcomeId == workflowOutcome.WorkflowOutcomeId) + { + + + + } + } + +
StateStartedEnded
+ + @workflowOutcome.WorkflowOutcomeState + + @workflowOutcome.StartedUtc?.ToString("g")@workflowOutcome.EndedUtc?.ToString("g")
+
+ @if (!string.IsNullOrEmpty(workflowOutcome.ActionOutcomesJson)) + { +
+
+

Action Outcomes

+ +
+
+ @foreach (var outcome in ParseAndFormatJson(workflowOutcome.ActionOutcomesJson)) + { +
+
+
+
Outcome ID
+
@outcome.OutcomeId
+
+
+
Action ID
+
@outcome.ActionId
+
+
+ + @if (!string.IsNullOrEmpty(outcome.OutcomeJson)) + { +
+
+
Outcome JSON
+ +
+
+
@TruncateText(FormatJson(outcome.OutcomeJson), 200)
+
+
+ } +
+ } +
+
+ } + + @if (!string.IsNullOrEmpty(workflowOutcome.ExecutionContext)) + { +
+
+

Execution Context

+ +
+ @{ + var context = ParseExecutionContext(workflowOutcome.ExecutionContext); + } +
+
+
Method
+
@context.Method
+
+ + @if (context.QueryParameters.Any()) + { +
+
Query Parameters
+
+ + + + + + + + + + @foreach (var param in context.QueryParameters) + { + + + + + + } + +
KeyValue
@param.Key +
@TruncateText(param.Value, 50)
+
+ @if (param.Value.Length > 50) + { + + } +
+
+
+ } + + @if (!string.IsNullOrEmpty(context.Body)) + { +
+
+
Body
+ +
+
+
@TruncateText(FormatJson(context.Body), 200)
+
+
+ } +
+
+ } + + @if (!string.IsNullOrEmpty(workflowOutcome.ErrorJson)) + { +
+
+

Error

+ +
+
+
@TruncateText(FormatJson(workflowOutcome.ErrorJson), 200)
+
+
+ } +
+
+
+
} else { -

No outcomes found for the selected workflow.

+
+

No outcomes found for the selected workflow.

+
}
@@ -116,6 +230,7 @@ else

Loading...

} + @code { private readonly CancellationTokenSource cts = new CancellationTokenSource(); private Guid? selectedWorkflowId; @@ -158,11 +273,11 @@ else private string GetOutcomeStateColor(EWorkflowOutcomeState state) => state switch { - EWorkflowOutcomeState.Success => "text-green-600", - EWorkflowOutcomeState.FailedWithErrors => "text-red-600", - EWorkflowOutcomeState.NotStarted => "text-gray-600", - EWorkflowOutcomeState.Running => "text-blue-600", - _ => "text-gray-600" + EWorkflowOutcomeState.Success => "bg-green-100 text-green-800", + EWorkflowOutcomeState.FailedWithErrors => "bg-red-100 text-red-800", + EWorkflowOutcomeState.NotStarted => "bg-slate-100 text-slate-800", + EWorkflowOutcomeState.Running => "bg-blue-100 text-blue-800", + _ => "bg-slate-100 text-slate-800" }; private void ToggleOutcomeDetails(Guid workflowOutcomeId) @@ -177,4 +292,82 @@ else } } + private class ActionOutcomeDto + { + public string OutcomeId { get; set; } = ""; + public string ActionId { get; set; } = ""; + public int EActionOutcome { get; set; } + public string StartedUtc { get; set; } = ""; + public string EndedUtc { get; set; } = ""; + public string? ErrorJson { get; set; } + public string? OutcomeJson { get; set; } + } + + private class ExecutionContext + { + public string Method { get; set; } = ""; + public Dictionary QueryParameters { get; set; } = new(); + public string Body { get; set; } = ""; + } + + private List ParseAndFormatJson(string json) + { + try + { + return JsonSerializer.Deserialize>(json) ?? new List(); + } + catch (Exception ex) + { + Logger.LogError(ex, "Failed to parse JSON"); + return new List(); + } + } + + private ExecutionContext ParseExecutionContext(string json) + { + try + { + return JsonSerializer.Deserialize(json) ?? new ExecutionContext(); + } + catch (Exception ex) + { + Logger.LogError(ex, "Failed to parse execution context"); + return new ExecutionContext(); + } + } + + private string FormatJson(string json) + { + try + { + using var document = JsonDocument.Parse(json); + var options = new JsonSerializerOptions { WriteIndented = true }; + return JsonSerializer.Serialize(document, options); + } + catch (Exception ex) + { + Logger.LogError(ex, "Failed to format JSON"); + return json; + } + } + + private string TruncateText(string text, int maxLength) + { + if (string.IsNullOrEmpty(text) || text.Length <= maxLength) + return text; + + return $"{text[..maxLength]}..."; + } + + private async Task CopyToClipboard(string text) + { + try + { + await JSRuntime.InvokeVoidAsync("navigator.clipboard.writeText", text); + } + catch (Exception ex) + { + Logger.LogError(ex, "Failed to copy text to clipboard"); + } + } } \ No newline at end of file diff --git a/Blocktrust.CredentialWorkflow.Web/wwwroot/app.css b/Blocktrust.CredentialWorkflow.Web/wwwroot/app.css index 4036f41..0df5b33 100644 --- a/Blocktrust.CredentialWorkflow.Web/wwwroot/app.css +++ b/Blocktrust.CredentialWorkflow.Web/wwwroot/app.css @@ -897,6 +897,15 @@ video { min-width: 100%; } +.min-w-0 { + min-width: 0px; +} + +.min-w-fit { + min-width: -moz-fit-content; + min-width: fit-content; +} + .max-w-md { max-width: 28rem; } @@ -905,10 +914,18 @@ video { max-width: 56rem; } +.max-w-full { + max-width: 100%; +} + .flex-1 { flex: 1 1 0%; } +.shrink-0 { + flex-shrink: 0; +} + .flex-grow { flex-grow: 1; } @@ -956,6 +973,14 @@ video { grid-template-columns: repeat(6, minmax(0, 1fr)); } +.grid-cols-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.grid-cols-1 { + grid-template-columns: repeat(1, minmax(0, 1fr)); +} + .flex-row { flex-direction: row; } @@ -1008,6 +1033,10 @@ video { gap: 2rem; } +.gap-2 { + gap: 0.5rem; +} + .space-x-2 > :not([hidden]) ~ :not([hidden]) { --tw-space-x-reverse: 0; margin-right: calc(0.5rem * var(--tw-space-x-reverse)); @@ -1050,6 +1079,12 @@ video { margin-bottom: calc(1.25rem * var(--tw-space-y-reverse)); } +.space-y-6 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(1.5rem * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(1.5rem * var(--tw-space-y-reverse)); +} + .divide-y > :not([hidden]) ~ :not([hidden]) { --tw-divide-y-reverse: 0; border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse))); @@ -1061,6 +1096,11 @@ video { border-color: rgb(229 231 235 / var(--tw-divide-opacity)); } +.divide-slate-200 > :not([hidden]) ~ :not([hidden]) { + --tw-divide-opacity: 1; + border-color: rgb(226 232 240 / var(--tw-divide-opacity)); +} + .self-end { align-self: flex-end; } @@ -1073,14 +1113,36 @@ video { overflow: hidden; } +.overflow-x-auto { + overflow-x: auto; +} + .overflow-y-auto { overflow-y: auto; } +.truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + .whitespace-nowrap { white-space: nowrap; } +.whitespace-pre { + white-space: pre; +} + +.whitespace-pre-wrap { + white-space: pre-wrap; +} + +.break-all { + word-break: break-all; +} + .rounded { border-radius: 0.25rem; } @@ -1135,6 +1197,10 @@ video { border-top-width: 2px; } +.border-b { + border-bottom-width: 1px; +} + .border-blue-500 { --tw-border-opacity: 1; border-color: rgb(59 130 246 / var(--tw-border-opacity)); @@ -1305,6 +1371,16 @@ video { background-color: rgb(248 250 252 / var(--tw-bg-opacity)); } +.bg-red-50 { + --tw-bg-opacity: 1; + background-color: rgb(254 242 242 / var(--tw-bg-opacity)); +} + +.bg-slate-100 { + --tw-bg-opacity: 1; + background-color: rgb(241 245 249 / var(--tw-bg-opacity)); +} + .bg-opacity-50 { --tw-bg-opacity: 0.5; } @@ -1398,6 +1474,11 @@ video { padding-bottom: 2rem; } +.px-2\.5 { + padding-left: 0.625rem; + padding-right: 0.625rem; +} + .pl-4 { padding-left: 1rem; } @@ -1808,6 +1889,16 @@ a { background-color: rgb(71 85 105 / var(--tw-bg-opacity)); } +.hover\:bg-slate-100:hover { + --tw-bg-opacity: 1; + background-color: rgb(241 245 249 / var(--tw-bg-opacity)); +} + +.hover\:bg-slate-50:hover { + --tw-bg-opacity: 1; + background-color: rgb(248 250 252 / var(--tw-bg-opacity)); +} + .hover\:text-blue-700:hover { --tw-text-opacity: 1; color: rgb(29 78 216 / var(--tw-text-opacity)); @@ -1853,6 +1944,11 @@ a { color: rgb(239 68 68 / var(--tw-text-opacity)); } +.hover\:text-slate-700:hover { + --tw-text-opacity: 1; + color: rgb(51 65 85 / var(--tw-text-opacity)); +} + .focus\:border-slate-400:focus { --tw-border-opacity: 1; border-color: rgb(148 163 184 / var(--tw-border-opacity)); @@ -1901,6 +1997,12 @@ a { opacity: 1; } +@media (min-width: 640px) { + .sm\:grid-cols-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + @media (min-width: 768px) { .md\:col-span-2 { grid-column: span 2 / span 2;