|
|
@@ -70,6 +70,22 @@ function fmtTimestamp(unixSec: number): string {
|
|
|
return `${d.toLocaleDateString()} ${hh}:${mm}:${ss}`;
|
|
|
}
|
|
|
|
|
|
+function formatFullTimestamp(unixSec: number): string {
|
|
|
+ const d = new Date(unixSec * 1000);
|
|
|
+ const today = new Date();
|
|
|
+ const sameDay = d.getFullYear() === today.getFullYear()
|
|
|
+ && d.getMonth() === today.getMonth()
|
|
|
+ && d.getDate() === today.getDate();
|
|
|
+ const hh = String(d.getHours()).padStart(2, '0');
|
|
|
+ const mm = String(d.getMinutes()).padStart(2, '0');
|
|
|
+ const ss = String(d.getSeconds()).padStart(2, '0');
|
|
|
+ const time = `${hh}:${mm}:${ss}`;
|
|
|
+ if (sameDay) return time;
|
|
|
+ const MM = String(d.getMonth() + 1).padStart(2, '0');
|
|
|
+ const DD = String(d.getDate()).padStart(2, '0');
|
|
|
+ return `${MM}-${DD} ${time}`;
|
|
|
+}
|
|
|
+
|
|
|
export default function XrayMetricsModal({ open, onClose }: XrayMetricsModalProps) {
|
|
|
const { t } = useTranslation();
|
|
|
const { isMobile } = useMediaQuery();
|
|
|
@@ -77,6 +93,7 @@ export default function XrayMetricsModal({ open, onClose }: XrayMetricsModalProp
|
|
|
const [bucket, setBucket] = useState(2);
|
|
|
const [points, setPoints] = useState<number[]>([]);
|
|
|
const [labels, setLabels] = useState<string[]>([]);
|
|
|
+ const [timestamps, setTimestamps] = useState<number[]>([]);
|
|
|
const [state, setState] = useState<XrayState>({ enabled: false, listen: '', reason: '' });
|
|
|
const [obsTags, setObsTags] = useState<ObservatoryTag[]>([]);
|
|
|
const [obsActiveTag, setObsActiveTag] = useState('');
|
|
|
@@ -90,10 +107,27 @@ export default function XrayMetricsModal({ open, onClose }: XrayMetricsModalProp
|
|
|
|
|
|
const activeObsTag = obsTags.find((tg) => tg.tag === obsActiveTag) || null;
|
|
|
|
|
|
+ const tsLookup = useMemo(() => {
|
|
|
+ const m = new Map<string, number>();
|
|
|
+ for (let i = 0; i < labels.length; i++) {
|
|
|
+ m.set(labels[i], timestamps[i]);
|
|
|
+ }
|
|
|
+ return m;
|
|
|
+ }, [labels, timestamps]);
|
|
|
+
|
|
|
+ const tooltipLabelFormatter = useCallback(
|
|
|
+ (label: string) => {
|
|
|
+ const ts = tsLookup.get(label);
|
|
|
+ return ts ? formatFullTimestamp(ts) : label;
|
|
|
+ },
|
|
|
+ [tsLookup],
|
|
|
+ );
|
|
|
+
|
|
|
const applyHistory = useCallback((msg: Msg<{ t: number; v: number }[]> | null | undefined, currentBucket: number) => {
|
|
|
if (msg?.success && Array.isArray(msg.obj)) {
|
|
|
const vals: number[] = [];
|
|
|
const labs: string[] = [];
|
|
|
+ const tss: number[] = [];
|
|
|
for (const p of msg.obj) {
|
|
|
const d = new Date(p.t * 1000);
|
|
|
const hh = String(d.getHours()).padStart(2, '0');
|
|
|
@@ -101,12 +135,15 @@ export default function XrayMetricsModal({ open, onClose }: XrayMetricsModalProp
|
|
|
const ss = String(d.getSeconds()).padStart(2, '0');
|
|
|
labs.push(currentBucket >= 60 ? `${hh}:${mm}` : `${hh}:${mm}:${ss}`);
|
|
|
vals.push(Number(p.v) || 0);
|
|
|
+ tss.push(Number(p.t) || 0);
|
|
|
}
|
|
|
setLabels(labs);
|
|
|
setPoints(vals);
|
|
|
+ setTimestamps(tss);
|
|
|
} else {
|
|
|
setLabels([]);
|
|
|
setPoints([]);
|
|
|
+ setTimestamps([]);
|
|
|
}
|
|
|
}, []);
|
|
|
|
|
|
@@ -148,6 +185,7 @@ export default function XrayMetricsModal({ open, onClose }: XrayMetricsModalProp
|
|
|
console.error('Failed to fetch xray metrics bucket', e);
|
|
|
setLabels([]);
|
|
|
setPoints([]);
|
|
|
+ setTimestamps([]);
|
|
|
}
|
|
|
}, [activeMetric, bucket, applyHistory]);
|
|
|
|
|
|
@@ -155,6 +193,7 @@ export default function XrayMetricsModal({ open, onClose }: XrayMetricsModalProp
|
|
|
if (!obsActiveTag) {
|
|
|
setLabels([]);
|
|
|
setPoints([]);
|
|
|
+ setTimestamps([]);
|
|
|
return;
|
|
|
}
|
|
|
try {
|
|
|
@@ -165,6 +204,7 @@ export default function XrayMetricsModal({ open, onClose }: XrayMetricsModalProp
|
|
|
console.error('Failed to fetch observatory bucket', e);
|
|
|
setLabels([]);
|
|
|
setPoints([]);
|
|
|
+ setTimestamps([]);
|
|
|
}
|
|
|
}, [obsActiveTag, bucket, applyHistory]);
|
|
|
|
|
|
@@ -225,8 +265,8 @@ export default function XrayMetricsModal({ open, onClose }: XrayMetricsModalProp
|
|
|
width={isMobile ? '95vw' : 900}
|
|
|
onCancel={onClose}
|
|
|
title={
|
|
|
- <>
|
|
|
- {t('pages.index.xrayMetricsTitle')}
|
|
|
+ <div className="metric-modal-title">
|
|
|
+ <span>{t('pages.index.xrayMetricsTitle')}</span>
|
|
|
<Select
|
|
|
value={bucket}
|
|
|
size="small"
|
|
|
@@ -241,7 +281,7 @@ export default function XrayMetricsModal({ open, onClose }: XrayMetricsModalProp
|
|
|
{ value: 300, label: '5h' },
|
|
|
]}
|
|
|
/>
|
|
|
- </>
|
|
|
+ </div>
|
|
|
}
|
|
|
>
|
|
|
{!state.enabled && (
|
|
|
@@ -313,16 +353,10 @@ export default function XrayMetricsModal({ open, onClose }: XrayMetricsModalProp
|
|
|
)}
|
|
|
|
|
|
<div className="cpu-chart-wrap">
|
|
|
- <div className="cpu-chart-meta">
|
|
|
- Timeframe: {bucket} sec per point (total {points.length} points)
|
|
|
- {state.enabled && state.listen && (
|
|
|
- <span className="listen-tag"> · {state.listen}</span>
|
|
|
- )}
|
|
|
- </div>
|
|
|
<Sparkline
|
|
|
data={points}
|
|
|
labels={labels}
|
|
|
- height={220}
|
|
|
+ height={260}
|
|
|
stroke={strokeColor}
|
|
|
strokeWidth={2.2}
|
|
|
showGrid
|
|
|
@@ -335,6 +369,8 @@ export default function XrayMetricsModal({ open, onClose }: XrayMetricsModalProp
|
|
|
valueMin={0}
|
|
|
valueMax={null}
|
|
|
yFormatter={yFormatter}
|
|
|
+ tooltipLabelFormatter={tooltipLabelFormatter}
|
|
|
+ extrema={{ show: true, formatter: yFormatter }}
|
|
|
/>
|
|
|
</div>
|
|
|
</Modal>
|