feat: Enhance frontend loading experience and service status handling
Some checks failed
Integration Tests / integration-tests (push) Failing after 20s
Integration Tests / performance-tests (push) Has been skipped
Service Adapters (Python FastAPI) / test (3.11) (push) Failing after 23s
Frontend (React) / test (20) (push) Failing after 1m3s
Frontend (React) / build (push) Has been skipped
Frontend (React) / lighthouse (push) Has been skipped
Service Adapters (Python FastAPI) / test (3.12) (push) Failing after 23s
Service Adapters (Python FastAPI) / test (3.13) (push) Failing after 20s
Service Adapters (Python FastAPI) / build (push) Has been skipped
Some checks failed
Integration Tests / integration-tests (push) Failing after 20s
Integration Tests / performance-tests (push) Has been skipped
Service Adapters (Python FastAPI) / test (3.11) (push) Failing after 23s
Frontend (React) / test (20) (push) Failing after 1m3s
Frontend (React) / build (push) Has been skipped
Frontend (React) / lighthouse (push) Has been skipped
Service Adapters (Python FastAPI) / test (3.12) (push) Failing after 23s
Service Adapters (Python FastAPI) / test (3.13) (push) Failing after 20s
Service Adapters (Python FastAPI) / build (push) Has been skipped
### Summary of Changes - Removed proxy configuration in `rsbuild.config.js` as the API Gateway is not running. - Added smooth transitions and gentle loading overlays in CSS for improved user experience during data loading. - Updated `Dashboard` component to conditionally display loading spinner and gentle loading overlay based on data fetching state. - Enhanced `useOfflineAwareServiceStatus` and `useOfflineAwareSystemData` hooks to manage loading states and service status more effectively. - Increased refresh intervals for service status and system data to reduce API call frequency. ### Expected Results - Improved user experience with smoother loading transitions and better feedback during data refreshes. - Enhanced handling of service status checks, providing clearer information when services are unavailable. - Streamlined code for managing loading states, making it easier to maintain and extend in the future.
This commit is contained in:
12
services/service-adapters/utils/__init__.py
Normal file
12
services/service-adapters/utils/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
||||
"""
|
||||
Utilities Package
|
||||
|
||||
This package contains utility functions for the service adapters.
|
||||
"""
|
||||
|
||||
from .time_formatter import format_uptime_for_frontend, format_response_time
|
||||
|
||||
__all__ = [
|
||||
"format_uptime_for_frontend",
|
||||
"format_response_time",
|
||||
]
|
||||
199
services/service-adapters/utils/time_formatter.py
Normal file
199
services/service-adapters/utils/time_formatter.py
Normal file
@@ -0,0 +1,199 @@
|
||||
"""
|
||||
Time Formatting Utilities
|
||||
|
||||
This module provides utilities for formatting time durations and timestamps
|
||||
into human-readable formats for the frontend.
|
||||
"""
|
||||
|
||||
import re
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional, Union
|
||||
|
||||
|
||||
def format_uptime_for_frontend(uptime_value: Optional[str]) -> str:
|
||||
"""
|
||||
Format uptime value for frontend display in "Xd Xh Xm" format.
|
||||
|
||||
Args:
|
||||
uptime_value: Raw uptime value (timestamp, epoch, duration string, etc.)
|
||||
|
||||
Returns:
|
||||
Formatted uptime string like "2d 5h 30m" or "0d 0h" if invalid
|
||||
"""
|
||||
if not uptime_value:
|
||||
return "0d 0h"
|
||||
|
||||
try:
|
||||
# Try to parse as timestamp (ISO format)
|
||||
if _is_timestamp(uptime_value):
|
||||
return _format_timestamp_uptime(uptime_value)
|
||||
|
||||
# Try to parse as epoch timestamp
|
||||
if _is_epoch(uptime_value):
|
||||
return _format_epoch_uptime(uptime_value)
|
||||
|
||||
# Try to parse as duration string (e.g., "2h 30m", "5d 2h 15m")
|
||||
if _is_duration_string(uptime_value):
|
||||
return _format_duration_string(uptime_value)
|
||||
|
||||
# Try to parse as numeric seconds
|
||||
if _is_numeric_seconds(uptime_value):
|
||||
return _format_seconds_uptime(float(uptime_value))
|
||||
|
||||
# If none of the above, return as-is or default
|
||||
return uptime_value if len(uptime_value) < 50 else "0d 0h"
|
||||
|
||||
except Exception:
|
||||
return "0d 0h"
|
||||
|
||||
|
||||
def _is_timestamp(value: str) -> bool:
|
||||
"""Check if value is an ISO timestamp."""
|
||||
try:
|
||||
datetime.fromisoformat(value.replace('Z', '+00:00'))
|
||||
return True
|
||||
except (ValueError, AttributeError):
|
||||
return False
|
||||
|
||||
|
||||
def _is_epoch(value: str) -> bool:
|
||||
"""Check if value is an epoch timestamp."""
|
||||
try:
|
||||
float(value)
|
||||
return len(value) >= 10 and float(value) > 1000000000 # Reasonable epoch range
|
||||
except (ValueError, TypeError):
|
||||
return False
|
||||
|
||||
|
||||
def _is_duration_string(value: str) -> bool:
|
||||
"""Check if value is a duration string like '2h 30m' or '5d 2h 15m'."""
|
||||
# Look for patterns like "2h 30m", "5d 2h 15m", "1d 2h 3m 4s"
|
||||
pattern = r'^\d+[dhms]\s*(\d+[dhms]\s*)*$'
|
||||
return bool(re.match(pattern, value.strip()))
|
||||
|
||||
|
||||
def _is_numeric_seconds(value: str) -> bool:
|
||||
"""Check if value is numeric seconds."""
|
||||
try:
|
||||
float(value)
|
||||
return True
|
||||
except (ValueError, TypeError):
|
||||
return False
|
||||
|
||||
|
||||
def _format_timestamp_uptime(timestamp: str) -> str:
|
||||
"""Format timestamp uptime (time since timestamp)."""
|
||||
try:
|
||||
# Parse timestamp
|
||||
dt = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
|
||||
if dt.tzinfo is None:
|
||||
dt = dt.replace(tzinfo=timezone.utc)
|
||||
|
||||
# Calculate time difference
|
||||
now = datetime.now(timezone.utc)
|
||||
diff = now - dt
|
||||
|
||||
return _format_timedelta(diff)
|
||||
except Exception:
|
||||
return "0d 0h"
|
||||
|
||||
|
||||
def _format_epoch_uptime(epoch_str: str) -> str:
|
||||
"""Format epoch timestamp uptime."""
|
||||
try:
|
||||
epoch = float(epoch_str)
|
||||
dt = datetime.fromtimestamp(epoch, tz=timezone.utc)
|
||||
now = datetime.now(timezone.utc)
|
||||
diff = now - dt
|
||||
|
||||
return _format_timedelta(diff)
|
||||
except Exception:
|
||||
return "0d 0h"
|
||||
|
||||
|
||||
def _format_duration_string(duration: str) -> str:
|
||||
"""Format duration string to standardized format."""
|
||||
try:
|
||||
# Parse duration string like "2h 30m" or "5d 2h 15m"
|
||||
total_seconds = _parse_duration_string(duration)
|
||||
return _format_seconds_uptime(total_seconds)
|
||||
except Exception:
|
||||
return "0d 0h"
|
||||
|
||||
|
||||
def _format_seconds_uptime(seconds: float) -> str:
|
||||
"""Format seconds to "Xd Xh Xm" format."""
|
||||
return _format_timedelta_from_seconds(seconds)
|
||||
|
||||
|
||||
def _parse_duration_string(duration: str) -> float:
|
||||
"""Parse duration string to total seconds."""
|
||||
total_seconds = 0
|
||||
|
||||
# Extract days
|
||||
days_match = re.search(r'(\d+)d', duration)
|
||||
if days_match:
|
||||
total_seconds += int(days_match.group(1)) * 86400
|
||||
|
||||
# Extract hours
|
||||
hours_match = re.search(r'(\d+)h', duration)
|
||||
if hours_match:
|
||||
total_seconds += int(hours_match.group(1)) * 3600
|
||||
|
||||
# Extract minutes
|
||||
minutes_match = re.search(r'(\d+)m', duration)
|
||||
if minutes_match:
|
||||
total_seconds += int(minutes_match.group(1)) * 60
|
||||
|
||||
# Extract seconds
|
||||
seconds_match = re.search(r'(\d+)s', duration)
|
||||
if seconds_match:
|
||||
total_seconds += int(seconds_match.group(1))
|
||||
|
||||
return total_seconds
|
||||
|
||||
|
||||
def _format_timedelta(td) -> str:
|
||||
"""Format timedelta to "Xd Xh Xm" format."""
|
||||
total_seconds = int(td.total_seconds())
|
||||
return _format_timedelta_from_seconds(total_seconds)
|
||||
|
||||
|
||||
def _format_timedelta_from_seconds(total_seconds: Union[int, float]) -> str:
|
||||
"""Format total seconds to "Xd Xh Xm" format."""
|
||||
if total_seconds < 0:
|
||||
return "0d 0h"
|
||||
|
||||
# Convert to int to avoid decimal places
|
||||
total_seconds = int(total_seconds)
|
||||
|
||||
days = total_seconds // 86400
|
||||
hours = (total_seconds % 86400) // 3600
|
||||
minutes = (total_seconds % 3600) // 60
|
||||
|
||||
# Only show days if > 0
|
||||
if days > 0:
|
||||
return f"{days}d {hours}h {minutes}m"
|
||||
elif hours > 0:
|
||||
return f"{hours}h {minutes}m"
|
||||
else:
|
||||
return f"{minutes}m"
|
||||
|
||||
|
||||
def format_response_time(seconds: Optional[float]) -> str:
|
||||
"""
|
||||
Format response time for display.
|
||||
|
||||
Args:
|
||||
seconds: Response time in seconds
|
||||
|
||||
Returns:
|
||||
Formatted response time string
|
||||
"""
|
||||
if seconds is None:
|
||||
return "N/A"
|
||||
|
||||
if seconds < 1:
|
||||
return f"{seconds * 1000:.0f}ms"
|
||||
else:
|
||||
return f"{seconds:.2f}s"
|
||||
Reference in New Issue
Block a user