using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using IronTelemetry.Client;
namespace IronServices.Maui.Controls;
#region Enums
///
/// Display status for a user journey.
///
public enum JourneyDisplayStatus
{
InProgress,
Completed,
Failed,
Abandoned
}
///
/// Display status for a step within a journey.
///
public enum StepDisplayStatus
{
InProgress,
Completed,
Failed,
Skipped
}
///
/// View mode for the UserJourneyView control.
///
public enum JourneyViewMode
{
///
/// Vertical timeline with nested steps shown with indentation.
///
Timeline,
///
/// Expandable tree hierarchy with collapsible nodes.
///
Tree,
///
/// Horizontal flowchart-style diagram showing step progression.
///
Flow
}
#endregion
#region JourneyItem
///
/// Represents a user journey for display in UserJourneyView.
///
public class JourneyItem : INotifyPropertyChanged
{
private bool _isExpanded;
private bool _isSelected;
///
/// Unique journey identifier.
///
public string JourneyId { get; set; } = "";
///
/// Journey name (e.g., "Checkout Flow", "User Onboarding").
///
public string Name { get; set; } = "";
///
/// Associated user ID.
///
public string? UserId { get; set; }
///
/// Associated user email.
///
public string? UserEmail { get; set; }
///
/// Current journey status.
///
public JourneyDisplayStatus Status { get; set; } = JourneyDisplayStatus.InProgress;
///
/// When the journey started.
///
public DateTime StartTime { get; set; }
///
/// When the journey ended (null if still in progress).
///
public DateTime? EndTime { get; set; }
///
/// Duration in milliseconds.
///
public double? DurationMs { get; set; }
///
/// Custom metadata attached to the journey.
///
public Dictionary Metadata { get; set; } = new();
///
/// Top-level steps in this journey.
///
public ObservableCollection Steps { get; } = new();
///
/// Breadcrumbs captured during this journey.
///
public List Breadcrumbs { get; set; } = new();
///
/// Exceptions captured during this journey.
///
public List Exceptions { get; set; } = new();
///
/// UI state: whether this journey is expanded in tree view.
///
public bool IsExpanded
{
get => _isExpanded;
set { _isExpanded = value; OnPropertyChanged(); }
}
///
/// UI state: whether this journey is currently selected.
///
public bool IsSelected
{
get => _isSelected;
set { _isSelected = value; OnPropertyChanged(); }
}
#region Computed Properties
///
/// Formatted duration for display.
///
public string DurationDisplay
{
get
{
if (!DurationMs.HasValue) return "In Progress";
if (DurationMs.Value < 1000) return $"{DurationMs:F0}ms";
if (DurationMs.Value < 60000) return $"{DurationMs.Value / 1000:F1}s";
return $"{DurationMs.Value / 60000:F1}m";
}
}
///
/// Relative time since journey started.
///
public string TimeAgo
{
get
{
var span = DateTime.UtcNow - StartTime;
if (span.TotalMinutes < 1) return "just now";
if (span.TotalMinutes < 60) return $"{(int)span.TotalMinutes}m ago";
if (span.TotalHours < 24) return $"{(int)span.TotalHours}h ago";
if (span.TotalDays < 7) return $"{(int)span.TotalDays}d ago";
return StartTime.ToLocalTime().ToString("MMM d");
}
}
///
/// Status text for display.
///
public string StatusDisplay => Status switch
{
JourneyDisplayStatus.InProgress => "In Progress",
JourneyDisplayStatus.Completed => "Completed",
JourneyDisplayStatus.Failed => "Failed",
JourneyDisplayStatus.Abandoned => "Abandoned",
_ => "Unknown"
};
///
/// Color for status badge.
///
public Color StatusColor => Status switch
{
JourneyDisplayStatus.InProgress => Colors.Blue,
JourneyDisplayStatus.Completed => Colors.Green,
JourneyDisplayStatus.Failed => Colors.Red,
JourneyDisplayStatus.Abandoned => Colors.Gray,
_ => Colors.Gray
};
///
/// Icon/text for status badge.
///
public string StatusIcon => Status switch
{
JourneyDisplayStatus.InProgress => "...",
JourneyDisplayStatus.Completed => "OK",
JourneyDisplayStatus.Failed => "X",
JourneyDisplayStatus.Abandoned => "-",
_ => "?"
};
///
/// Total number of steps (including nested).
///
public int StepCount => CountAllSteps(Steps);
///
/// Number of failed steps.
///
public int FailedStepCount => CountFailedSteps(Steps);
///
/// Whether there are any failed steps.
///
public bool HasFailedSteps => FailedStepCount > 0;
///
/// Whether there are any exceptions.
///
public bool HasExceptions => Exceptions.Count > 0;
///
/// Whether a user is associated with this journey.
///
public bool HasUser => !string.IsNullOrEmpty(UserId);
///
/// Start time formatted for display.
///
public string StartTimeDisplay => StartTime.ToLocalTime().ToString("HH:mm:ss");
#endregion
#region Helper Methods
private static int CountAllSteps(IEnumerable steps)
{
int count = 0;
foreach (var step in steps)
{
count++;
count += CountAllSteps(step.ChildSteps);
}
return count;
}
private static int CountFailedSteps(IEnumerable steps)
{
int count = 0;
foreach (var step in steps)
{
if (step.Status == StepDisplayStatus.Failed) count++;
count += CountFailedSteps(step.ChildSteps);
}
return count;
}
///
/// Get a flattened list of all steps for timeline display.
///
public List GetFlattenedSteps()
{
var result = new List();
FlattenSteps(Steps, result, 0);
return result;
}
private static void FlattenSteps(IEnumerable steps, List result, int level)
{
foreach (var step in steps)
{
step.NestingLevel = level;
result.Add(step);
FlattenSteps(step.ChildSteps, result, level + 1);
}
}
#endregion
#region INotifyPropertyChanged
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string? name = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
#endregion
}
#endregion
#region StepItem
///
/// Represents a step within a user journey.
///
public class StepItem : INotifyPropertyChanged
{
private bool _isExpanded;
private bool _isSelected;
private int _nestingLevel;
///
/// Unique step identifier.
///
public string StepId { get; set; } = "";
///
/// Parent step ID for nested steps.
///
public string? ParentStepId { get; set; }
///
/// Step name (e.g., "Validate Cart", "Process Payment").
///
public string Name { get; set; } = "";
///
/// Step category (e.g., "business", "technical", "navigation").
///
public string? Category { get; set; }
///
/// Current step status.
///
public StepDisplayStatus Status { get; set; } = StepDisplayStatus.InProgress;
///
/// Reason for failure if status is Failed.
///
public string? FailureReason { get; set; }
///
/// When the step started.
///
public DateTime StartTime { get; set; }
///
/// When the step ended.
///
public DateTime? EndTime { get; set; }
///
/// Duration in milliseconds.
///
public double? DurationMs { get; set; }
///
/// Custom data attached to the step.
///
public Dictionary Data { get; set; } = new();
///
/// Child steps nested under this step.
///
public ObservableCollection ChildSteps { get; } = new();
///
/// Breadcrumbs captured during this step.
///
public List Breadcrumbs { get; set; } = new();
///
/// UI state: nesting level for indentation.
///
public int NestingLevel
{
get => _nestingLevel;
set { _nestingLevel = value; OnPropertyChanged(); OnPropertyChanged(nameof(IndentMargin)); }
}
///
/// UI state: whether this step is expanded in tree view.
///
public bool IsExpanded
{
get => _isExpanded;
set { _isExpanded = value; OnPropertyChanged(); }
}
///
/// UI state: whether this step is currently selected.
///
public bool IsSelected
{
get => _isSelected;
set { _isSelected = value; OnPropertyChanged(); }
}
#region Computed Properties
///
/// Formatted duration for display.
///
public string DurationDisplay
{
get
{
if (!DurationMs.HasValue) return "...";
if (DurationMs.Value < 1000) return $"{DurationMs:F0}ms";
return $"{DurationMs.Value / 1000:F1}s";
}
}
///
/// Category for display (defaults to "general").
///
public string CategoryDisplay => Category ?? "general";
///
/// Color for status indicator.
///
public Color StatusColor => Status switch
{
StepDisplayStatus.InProgress => Colors.Blue,
StepDisplayStatus.Completed => Colors.Green,
StepDisplayStatus.Failed => Colors.Red,
StepDisplayStatus.Skipped => Colors.Gray,
_ => Colors.Gray
};
///
/// Icon/text for status indicator.
///
public string StatusIcon => Status switch
{
StepDisplayStatus.InProgress => "...",
StepDisplayStatus.Completed => "OK",
StepDisplayStatus.Failed => "X",
StepDisplayStatus.Skipped => "-",
_ => "?"
};
///
/// Margin for indentation based on nesting level.
///
public Thickness IndentMargin => new(NestingLevel * 20, 0, 0, 0);
///
/// Whether this step has child steps.
///
public bool HasChildren => ChildSteps.Count > 0;
///
/// Whether this step has a failure reason.
///
public bool HasFailureReason => !string.IsNullOrEmpty(FailureReason);
///
/// Start time formatted for display.
///
public string StartTimeDisplay => StartTime.ToLocalTime().ToString("HH:mm:ss.fff");
#endregion
#region INotifyPropertyChanged
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string? name = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
#endregion
}
#endregion
#region BreadcrumbItem
///
/// Represents a breadcrumb captured during journey/step execution.
///
public class BreadcrumbItem
{
///
/// When the breadcrumb was captured.
///
public DateTime Timestamp { get; set; }
///
/// Breadcrumb category.
///
public string Category { get; set; } = "";
///
/// Breadcrumb message.
///
public string Message { get; set; } = "";
///
/// Log level: "info", "warning", "error", "debug".
///
public string Level { get; set; } = "Info";
///
/// Optional additional data.
///
public Dictionary? Data { get; set; }
#region Computed Properties
///
/// Timestamp formatted for display.
///
public string TimestampDisplay => Timestamp.ToLocalTime().ToString("HH:mm:ss.fff");
///
/// Color based on log level.
///
public Color LevelColor => Level.ToLower() switch
{
"error" => Colors.Red,
"warning" => Colors.Orange,
"info" => Colors.Blue,
"debug" => Colors.Gray,
_ => Colors.Gray
};
///
/// Level display text.
///
public string LevelDisplay => Level.ToUpper();
#endregion
}
#endregion
#region ExceptionItem
///
/// Represents an exception captured during journey execution.
///
public class ExceptionItem
{
///
/// When the exception was captured.
///
public DateTime Timestamp { get; set; }
///
/// Exception type name.
///
public string ExceptionType { get; set; } = "";
///
/// Exception message.
///
public string Message { get; set; } = "";
///
/// Stack trace.
///
public string? StackTrace { get; set; }
///
/// Step ID where the exception was captured.
///
public string? StepId { get; set; }
#region Computed Properties
///
/// Timestamp formatted for display.
///
public string TimestampDisplay => Timestamp.ToLocalTime().ToString("HH:mm:ss");
///
/// Whether there is a stack trace.
///
public bool HasStackTrace => !string.IsNullOrEmpty(StackTrace);
///
/// Short display combining type and message.
///
public string ShortDisplay => $"{ExceptionType}: {Message}";
#endregion
}
#endregion
#region JourneyReconstructor
///
/// Utility class to reconstruct journey hierarchy from flat EnvelopeItem list.
///
public static class JourneyReconstructor
{
///
/// Reconstruct journey hierarchy from flat EnvelopeItem list.
///
public static List ReconstructJourneys(IEnumerable items)
{
var journeyMap = new Dictionary();
var stepMap = new Dictionary();
var journeyBreadcrumbs = new Dictionary>();
var journeyExceptions = new Dictionary>();
var stepToJourney = new Dictionary(); // stepId -> journeyId
// Process items in timestamp order
foreach (var item in items.OrderBy(i => i.Timestamp))
{
switch (item.Type)
{
case "journey_start":
if (!string.IsNullOrEmpty(item.JourneyId))
{
journeyMap[item.JourneyId] = new JourneyItem
{
JourneyId = item.JourneyId,
Name = item.Name ?? "Unknown Journey",
UserId = item.UserId,
UserEmail = item.UserEmail,
StartTime = item.Timestamp,
Status = JourneyDisplayStatus.InProgress
};
journeyBreadcrumbs[item.JourneyId] = new List();
journeyExceptions[item.JourneyId] = new List();
}
break;
case "journey_end":
if (!string.IsNullOrEmpty(item.JourneyId) &&
journeyMap.TryGetValue(item.JourneyId, out var journey))
{
journey.EndTime = item.Timestamp;
journey.Status = ParseJourneyStatus(item.Status);
if (item.Metadata.TryGetValue("durationMs", out var durationObj))
{
journey.DurationMs = Convert.ToDouble(durationObj);
}
else
{
journey.DurationMs = (item.Timestamp - journey.StartTime).TotalMilliseconds;
}
foreach (var kvp in item.Metadata)
{
journey.Metadata[kvp.Key] = kvp.Value;
}
}
break;
case "step_start":
if (!string.IsNullOrEmpty(item.StepId))
{
var step = new StepItem
{
StepId = item.StepId,
ParentStepId = item.ParentStepId,
Name = item.Name ?? "Unknown Step",
Category = item.Category,
StartTime = item.Timestamp,
Status = StepDisplayStatus.InProgress
};
stepMap[item.StepId] = step;
if (!string.IsNullOrEmpty(item.JourneyId))
{
stepToJourney[item.StepId] = item.JourneyId;
}
}
break;
case "step_end":
if (!string.IsNullOrEmpty(item.StepId) &&
stepMap.TryGetValue(item.StepId, out var endStep))
{
endStep.EndTime = item.Timestamp;
endStep.Status = ParseStepStatus(item.Status);
if (item.Data.TryGetValue("durationMs", out var durObj))
{
endStep.DurationMs = Convert.ToDouble(durObj);
}
else
{
endStep.DurationMs = (item.Timestamp - endStep.StartTime).TotalMilliseconds;
}
if (item.Data.TryGetValue("failureReason", out var reasonObj))
{
endStep.FailureReason = reasonObj?.ToString();
}
foreach (var kvp in item.Data)
{
endStep.Data[kvp.Key] = kvp.Value;
}
}
break;
case "exception":
if (!string.IsNullOrEmpty(item.JourneyId) &&
journeyExceptions.TryGetValue(item.JourneyId, out var excList))
{
excList.Add(new ExceptionItem
{
Timestamp = item.Timestamp,
ExceptionType = item.ExceptionType ?? "Exception",
Message = item.Message ?? "",
StackTrace = item.StackTrace,
StepId = item.StepId
});
}
break;
}
// Collect breadcrumbs
if (!string.IsNullOrEmpty(item.JourneyId) &&
item.Breadcrumbs?.Count > 0 &&
journeyBreadcrumbs.TryGetValue(item.JourneyId, out var bcList))
{
foreach (var bc in item.Breadcrumbs)
{
bcList.Add(new BreadcrumbItem
{
Timestamp = bc.Timestamp ?? item.Timestamp,
Category = bc.Category,
Message = bc.Message,
Level = bc.Level,
Data = bc.Data
});
}
}
}
// Build step hierarchy
foreach (var step in stepMap.Values)
{
if (!string.IsNullOrEmpty(step.ParentStepId) &&
stepMap.TryGetValue(step.ParentStepId, out var parentStep))
{
parentStep.ChildSteps.Add(step);
step.NestingLevel = parentStep.NestingLevel + 1;
}
}
// Associate root-level steps with journeys
foreach (var step in stepMap.Values)
{
if (string.IsNullOrEmpty(step.ParentStepId) &&
stepToJourney.TryGetValue(step.StepId, out var journeyId) &&
journeyMap.TryGetValue(journeyId, out var ownerJourney))
{
ownerJourney.Steps.Add(step);
}
}
// Associate breadcrumbs and exceptions
foreach (var journey in journeyMap.Values)
{
if (journeyBreadcrumbs.TryGetValue(journey.JourneyId, out var bcs))
{
journey.Breadcrumbs = bcs.OrderBy(b => b.Timestamp).ToList();
}
if (journeyExceptions.TryGetValue(journey.JourneyId, out var excs))
{
journey.Exceptions = excs.OrderBy(e => e.Timestamp).ToList();
}
}
return journeyMap.Values.ToList();
}
private static JourneyDisplayStatus ParseJourneyStatus(string? status) => status?.ToLower() switch
{
"completed" => JourneyDisplayStatus.Completed,
"failed" => JourneyDisplayStatus.Failed,
"abandoned" => JourneyDisplayStatus.Abandoned,
_ => JourneyDisplayStatus.InProgress
};
private static StepDisplayStatus ParseStepStatus(string? status) => status?.ToLower() switch
{
"completed" => StepDisplayStatus.Completed,
"failed" => StepDisplayStatus.Failed,
"skipped" => StepDisplayStatus.Skipped,
_ => StepDisplayStatus.InProgress
};
}
#endregion