Sort metrics numerically on the CCP Metrics page
Add a natural-sort comparator for metric IDs (e.g. 2.3.6i, 5.2.6, 10.1.1) and apply it to the metric breakdown cards, the vertical detail table, and the forecast burndown metric dropdown. Metrics now appear in ascending numerical order instead of arbitrary API response order. Closes #24
This commit is contained in:
@@ -8,6 +8,26 @@ const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
|
|||||||
const TEAL = '#14B8A6';
|
const TEAL = '#14B8A6';
|
||||||
const PURPLE = '#A78BFA';
|
const PURPLE = '#A78BFA';
|
||||||
|
|
||||||
|
// Natural sort comparator for metric IDs like "2.3.6i", "5.2.6", "10.1.1".
|
||||||
|
// Splits on "." and compares each segment numerically (trailing letters after digits sort after pure numbers).
|
||||||
|
function compareMetricIds(a, b) {
|
||||||
|
const partsA = a.split('.');
|
||||||
|
const partsB = b.split('.');
|
||||||
|
const len = Math.max(partsA.length, partsB.length);
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
const segA = partsA[i] || '';
|
||||||
|
const segB = partsB[i] || '';
|
||||||
|
const numA = parseFloat(segA) || 0;
|
||||||
|
const numB = parseFloat(segB) || 0;
|
||||||
|
if (numA !== numB) return numA - numB;
|
||||||
|
// Same numeric prefix — compare the suffix (e.g. "6i" vs "6")
|
||||||
|
const suffA = segA.replace(/^[\d.]+/, '');
|
||||||
|
const suffB = segB.replace(/^[\d.]+/, '');
|
||||||
|
if (suffA !== suffB) return suffA.localeCompare(suffB);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Styles
|
// Styles
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -106,7 +126,8 @@ function MetricBreakdownPanel({ metrics }) {
|
|||||||
if (!metrics || metrics.length === 0) return null;
|
if (!metrics || metrics.length === 0) return null;
|
||||||
|
|
||||||
// Only show metrics with non_compliant > 0
|
// Only show metrics with non_compliant > 0
|
||||||
const ncMetrics = metrics.filter(m => m.non_compliant > 0);
|
const ncMetrics = metrics.filter(m => m.non_compliant > 0)
|
||||||
|
.sort((a, b) => compareMetricIds(a.metric_id, b.metric_id));
|
||||||
if (ncMetrics.length === 0) return null;
|
if (ncMetrics.length === 0) return null;
|
||||||
|
|
||||||
const TOP_COUNT = 8;
|
const TOP_COUNT = 8;
|
||||||
@@ -628,9 +649,10 @@ function VerticalDetailView({ vertical, onBack, onSelectMetric }) {
|
|||||||
if (loading) return <div style={{ padding: '2rem', textAlign: 'center', color: '#64748B' }}><Loader style={{ animation: 'spin 1s linear infinite' }} /> Loading...</div>;
|
if (loading) return <div style={{ padding: '2rem', textAlign: 'center', color: '#64748B' }}><Loader style={{ animation: 'spin 1s linear infinite' }} /> Loading...</div>;
|
||||||
|
|
||||||
// Filter metrics by team if a team filter is active
|
// Filter metrics by team if a team filter is active
|
||||||
const displayMetrics = teamFilter
|
const displayMetrics = (teamFilter
|
||||||
? metrics.filter(m => m.sub_teams && m.sub_teams.some(st => st.team === teamFilter))
|
? metrics.filter(m => m.sub_teams && m.sub_teams.some(st => st.team === teamFilter))
|
||||||
: metrics;
|
: metrics
|
||||||
|
).slice().sort((a, b) => compareMetricIds(a.metric_id, b.metric_id));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -1282,7 +1304,7 @@ function MetricSelector({ onMetricSelect, selectedMetric }) {
|
|||||||
minWidth: '200px',
|
minWidth: '200px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{metrics.map(m => (
|
{metrics.slice().sort((a, b) => compareMetricIds(a.metric_id, b.metric_id)).map(m => (
|
||||||
<option key={m.metric_id} value={m.metric_id}>
|
<option key={m.metric_id} value={m.metric_id}>
|
||||||
{m.metric_id} — {m.device_count} device{m.device_count !== 1 ? 's' : ''}
|
{m.metric_id} — {m.device_count} device{m.device_count !== 1 ? 's' : ''}
|
||||||
</option>
|
</option>
|
||||||
|
|||||||
Reference in New Issue
Block a user