Add app source and tests
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
193
lib/Peach/designs/MetricsPanel.dart
Normal file
193
lib/Peach/designs/MetricsPanel.dart
Normal file
@@ -0,0 +1,193 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../about/internal/application/TextType.dart';
|
||||
|
||||
import '../about/internal/application/DeploymentRecord.dart';
|
||||
import '../utils/Colors.dart';
|
||||
import 'text/Text.dart';
|
||||
import '../utils/CommonUtils.dart';
|
||||
|
||||
/// Deployment metrics: success-rate ring plus stat tiles.
|
||||
class MetricsPanel extends StatelessWidget {
|
||||
final List<DeploymentRecord> history;
|
||||
const MetricsPanel({super.key, required this.history});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final jp = context.jp;
|
||||
final total = history.length;
|
||||
final success = history
|
||||
.where((h) => h.status == DeployStatus.success)
|
||||
.length;
|
||||
final failed = history
|
||||
.where((h) => h.status == DeployStatus.failure)
|
||||
.length;
|
||||
final reverted = history
|
||||
.where((h) => h.status == DeployStatus.reverted)
|
||||
.length;
|
||||
final attempts = success + failed;
|
||||
final rate = attempts == 0 ? 0.0 : success / attempts;
|
||||
final last = history.isNotEmpty ? history.first : null;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: PeachTheme.panel(context, glowColor: PeachTheme.seed),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
text(
|
||||
'METRICS',
|
||||
12,
|
||||
TextType.Regular,
|
||||
color: jp.textDim,
|
||||
letterSpacing: 1.2,
|
||||
weight: FontWeight.w700,
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Row(
|
||||
children: [
|
||||
_rateRing(jp, rate, attempts),
|
||||
const SizedBox(width: 18),
|
||||
Expanded(
|
||||
child: Wrap(
|
||||
spacing: 10,
|
||||
runSpacing: 10,
|
||||
children: [
|
||||
_tile(jp, 'Total', '$total', jp.text),
|
||||
_tile(jp, 'Success', '$success', PeachTheme.seed),
|
||||
_tile(jp, 'Failed', '$failed', PeachTheme.danger),
|
||||
_tile(jp, 'Reverted', '$reverted', PeachTheme.warn),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (last != null) ...[
|
||||
const SizedBox(height: 14),
|
||||
Divider(height: 1, color: jp.border),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.schedule, size: 14, color: jp.textFaint),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: text(
|
||||
'Last: ${last.serverName} · ${last.serviceLabel} · ${timeAgo(last.time)}',
|
||||
12.5,
|
||||
TextType.Regular,
|
||||
color: jp.textDim,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
weight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
_statusDot(last.status),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _rateRing(JpColors jp, double rate, int attempts) {
|
||||
final pct = (rate * 100).round();
|
||||
final color = rate >= 0.8
|
||||
? PeachTheme.seed
|
||||
: rate >= 0.5
|
||||
? PeachTheme.warn
|
||||
: PeachTheme.danger;
|
||||
return SizedBox(
|
||||
width: 84,
|
||||
height: 84,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 84,
|
||||
height: 84,
|
||||
child: CircularProgressIndicator(
|
||||
value: attempts == 0 ? 0 : rate,
|
||||
strokeWidth: 8,
|
||||
backgroundColor: jp.surface2,
|
||||
valueColor: AlwaysStoppedAnimation(color),
|
||||
strokeCap: StrokeCap.round,
|
||||
),
|
||||
),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
text(
|
||||
attempts == 0 ? '—' : '$pct%',
|
||||
20,
|
||||
TextType.Regular,
|
||||
color: color,
|
||||
weight: FontWeight.w800,
|
||||
),
|
||||
text(
|
||||
'SUCCESS',
|
||||
8,
|
||||
TextType.Regular,
|
||||
color: jp.textFaint,
|
||||
letterSpacing: 1,
|
||||
weight: FontWeight.w700,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _tile(JpColors jp, String label, String value, Color color) {
|
||||
return Container(
|
||||
width: 92,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: jp.bg.withValues(alpha: 0.5),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: jp.border),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
text(
|
||||
value,
|
||||
20,
|
||||
TextType.Regular,
|
||||
color: color,
|
||||
weight: FontWeight.w800,
|
||||
),
|
||||
text(
|
||||
label.toUpperCase(),
|
||||
9,
|
||||
TextType.Regular,
|
||||
color: jp.textFaint,
|
||||
letterSpacing: 0.8,
|
||||
weight: FontWeight.w700,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _statusDot(DeployStatus s) {
|
||||
final color = switch (s) {
|
||||
DeployStatus.success => PeachTheme.seed,
|
||||
DeployStatus.failure => PeachTheme.danger,
|
||||
DeployStatus.reverted => PeachTheme.warn,
|
||||
};
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.16),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: text(
|
||||
s.label,
|
||||
10,
|
||||
TextType.Regular,
|
||||
color: color,
|
||||
weight: FontWeight.w700,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user