Skip to content

Commit f86de0f

Browse files
thaafeiCopilot
andcommitted
add backend for metric order
Co-authored-by: Copilot <copilot@github.com>
1 parent 63ab03f commit f86de0f

10 files changed

Lines changed: 277 additions & 28 deletions

File tree

src/backend/api/database/metrics/management/__init__.py

Whitespace-only changes.

src/backend/api/database/metrics/management/commands/__init__.py

Whitespace-only changes.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from django.core.management.base import BaseCommand
2+
from django.db.models import F
3+
4+
from api.database.metrics.models import Metric, MetricOrder
5+
6+
7+
class Command(BaseCommand):
8+
help = "Populate MetricOrder with current metrics organized by category"
9+
10+
def handle(self, *args, **options):
11+
try:
12+
# Get all metrics grouped by category
13+
metrics = Metric.objects.all().order_by("category", "metric_name")
14+
15+
category_order = {}
16+
for metric in metrics:
17+
category = metric.category or "Uncategorized"
18+
if category not in category_order:
19+
category_order[category] = []
20+
category_order[category].append(str(metric.metric_ID))
21+
22+
# Get or create the MetricOrder instance
23+
metric_order, created = MetricOrder.objects.get_or_create(pk=1)
24+
metric_order.category_order = category_order
25+
metric_order.save()
26+
27+
if created:
28+
self.stdout.write(
29+
self.style.SUCCESS(
30+
f"✓ Created MetricOrder with {len(metrics)} metrics"
31+
)
32+
)
33+
else:
34+
self.stdout.write(
35+
self.style.SUCCESS(
36+
f"✓ Updated MetricOrder with {len(metrics)} metrics"
37+
)
38+
)
39+
40+
# Print summary
41+
self.stdout.write("\nMetrics by category:")
42+
for category, metric_ids in sorted(category_order.items()):
43+
self.stdout.write(f" {category}: {len(metric_ids)} metrics")
44+
45+
except Exception as e:
46+
self.stdout.write(
47+
self.style.ERROR(f"✗ Error populating MetricOrder: {str(e)}")
48+
)
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Generated by Django 5.2.7 on 2026-05-04 02:06
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("metrics", "0008_alter_metric_value_type"),
10+
]
11+
12+
operations = [
13+
migrations.CreateModel(
14+
name="MetricOrder",
15+
fields=[
16+
(
17+
"id",
18+
models.BigAutoField(
19+
auto_created=True,
20+
primary_key=True,
21+
serialize=False,
22+
verbose_name="ID",
23+
),
24+
),
25+
(
26+
"category_order",
27+
models.JSONField(
28+
default=dict,
29+
help_text="JSON object mapping category names to ordered lists of metric IDs",
30+
),
31+
),
32+
("updated_at", models.DateTimeField(auto_now=True)),
33+
],
34+
options={
35+
"verbose_name_plural": "Metric Orders",
36+
},
37+
),
38+
]

src/backend/api/database/metrics/models.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,18 @@ class Metric(models.Model):
3939

4040
def __str__(self):
4141
return self.metric_name
42+
43+
44+
class MetricOrder(models.Model):
45+
"""Stores the display order of metrics by category"""
46+
category_order = models.JSONField(
47+
default=dict,
48+
help_text="JSON object mapping category names to ordered lists of metric IDs"
49+
)
50+
updated_at = models.DateTimeField(auto_now=True)
51+
52+
class Meta:
53+
verbose_name_plural = "Metric Orders"
54+
55+
def __str__(self):
56+
return f"Metric Order (updated {self.updated_at})"

src/backend/api/database/metrics/urls.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
MetricCategoryView,
66
MetricListCreateView,
77
MetricListFlatView,
8+
MetricReorderView,
89
MetricRetrieveUpdateDestroyView,
910
MetricRulesView,
1011
MetricUpdateWeightView,
@@ -14,6 +15,7 @@
1415
path("", MetricListCreateView.as_view(), name="metric-list-create"),
1516
path("auto-options/", AutoMetricOptionsView.as_view(), name="metric-auto-options"),
1617
path("all/", MetricListFlatView.as_view(), name="metric-all-flat"),
18+
path("reorder/", MetricReorderView.as_view(), name="metric-reorder"),
1719
path("rules/", MetricRulesView.as_view(), name="metric-rules"),
1820
path("categories/", MetricCategoryView.as_view(), name="metric-categories"),
1921
path(

src/backend/api/database/metrics/views.py

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from rest_framework.response import Response
99
from rest_framework.views import APIView
1010

11-
from .models import Metric
11+
from .models import Metric, MetricOrder
1212
from .serializers import FlatMetricSerializer, MetricSerializer
1313

1414

@@ -134,3 +134,75 @@ def patch(self, request, metric_id):
134134
class MetricListFlatView(generics.ListAPIView):
135135
serializer_class = FlatMetricSerializer
136136
queryset = Metric.objects.all().order_by("metric_name")
137+
138+
139+
class MetricReorderView(APIView):
140+
permission_classes = [IsAuthenticatedOrReadOnly] # noqa: F811
141+
142+
def get(self, request):
143+
"""Retrieve the current metric display order"""
144+
try:
145+
metric_order = MetricOrder.objects.first()
146+
if metric_order:
147+
return Response(
148+
{"category_order": metric_order.category_order},
149+
status=status.HTTP_200_OK,
150+
)
151+
else:
152+
return Response(
153+
{"category_order": {}},
154+
status=status.HTTP_200_OK,
155+
)
156+
except Exception as e:
157+
return Response(
158+
{"error": str(e)},
159+
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
160+
)
161+
162+
def post(self, request):
163+
"""Save the metric display order"""
164+
try:
165+
category_order = request.data.get("category_order")
166+
if category_order is None:
167+
return Response(
168+
{"error": "category_order field is required"},
169+
status=status.HTTP_400_BAD_REQUEST,
170+
)
171+
172+
# Validate that all metric IDs exist
173+
all_metric_ids = set()
174+
for category_metrics in category_order.values():
175+
if isinstance(category_metrics, list):
176+
all_metric_ids.update(category_metrics)
177+
178+
# Check if all metric IDs exist
179+
existing_metrics = set(
180+
str(m_id) for m_id in Metric.objects.filter(
181+
metric_ID__in=all_metric_ids
182+
).values_list("metric_ID", flat=True)
183+
)
184+
185+
invalid_ids = all_metric_ids - existing_metrics
186+
if invalid_ids:
187+
return Response(
188+
{"error": f"Invalid metric IDs: {list(invalid_ids)}"},
189+
status=status.HTTP_400_BAD_REQUEST,
190+
)
191+
192+
# Get or create the single MetricOrder instance
193+
metric_order, _ = MetricOrder.objects.get_or_create(pk=1)
194+
metric_order.category_order = category_order
195+
metric_order.save()
196+
197+
return Response(
198+
{
199+
"message": "Metric display order updated successfully",
200+
"category_order": metric_order.category_order,
201+
},
202+
status=status.HTTP_200_OK,
203+
)
204+
except Exception as e:
205+
return Response(
206+
{"error": str(e)},
207+
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
208+
)

src/frontend/src/components/ReorderMetricsModal.tsx

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,9 @@
11
import React from "react";
2-
import { Metric } from "../pages/MetricPageTypes";
3-
4-
interface ReorderMetricsModalProps {
5-
isOpen: boolean;
6-
categoryOrder: string[];
7-
reorderCategory: string;
8-
categoryMetricOrder: Record<string, string[]>;
9-
metricsById: Map<string, Metric>;
10-
onCategoryChange: (category: string) => void;
11-
onMoveMetric: (metricId: string, direction: "up" | "down") => void;
12-
onClose: () => void;
13-
onSave: () => void;
14-
}
2+
import { ReorderMetricsModalProps } from "../pages/MetricPageTypes";
153

164
export const ReorderMetricsModal: React.FC<ReorderMetricsModalProps> = ({
175
isOpen,
6+
formError,
187
categoryOrder,
198
reorderCategory,
209
categoryMetricOrder,
@@ -25,6 +14,10 @@ export const ReorderMetricsModal: React.FC<ReorderMetricsModalProps> = ({
2514
onSave,
2615
}) => {
2716
if (!isOpen) return null;
17+
const handleReorder = async () => {
18+
const ok = await onSave();
19+
if (ok) onClose();
20+
};
2821

2922
return (
3023
<div
@@ -80,7 +73,21 @@ export const ReorderMetricsModal: React.FC<ReorderMetricsModalProps> = ({
8073
8174
</button>
8275
</div>
83-
76+
{formError && (
77+
<div
78+
style={{
79+
marginBottom: 12,
80+
padding: "10px 12px",
81+
borderRadius: 10,
82+
border: "1px solid rgba(255, 99, 99, 0.45)",
83+
background: "rgba(255, 99, 99, 0.10)",
84+
color: "#ffb3b3",
85+
fontSize: "0.95rem",
86+
}}
87+
>
88+
{formError}
89+
</div>
90+
)}
8491
<div style={{ display: "grid", gap: 12 }}>
8592
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
8693
<label style={{ opacity: 0.85 }}>Category</label>
@@ -171,7 +178,7 @@ export const ReorderMetricsModal: React.FC<ReorderMetricsModalProps> = ({
171178
<button className="dx-btn dx-btn-outline" onClick={onClose}>
172179
Cancel
173180
</button>
174-
<button className="dx-btn dx-btn-primary" onClick={onSave}>
181+
<button className="dx-btn dx-btn-primary" onClick={handleReorder}>
175182
Save Order
176183
</button>
177184
</div>

src/frontend/src/pages/MetricPageTypes.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,17 @@ export interface AddMetricModalProps {
7575
setEditTemplate: (value: string) => void;
7676
onEditTypeChange: (value: string) => void;
7777
isRuleType: (type: string) => boolean;
78-
}
78+
}
79+
80+
export interface ReorderMetricsModalProps {
81+
isOpen: boolean;
82+
formError: string;
83+
categoryOrder: string[];
84+
reorderCategory: string;
85+
categoryMetricOrder: Record<string, string[]>;
86+
metricsById: Map<string, Metric>;
87+
onCategoryChange: (category: string) => void;
88+
onMoveMetric: (metricId: string, direction: "up" | "down") => void;
89+
onClose: () => void;
90+
onSave: () => Promise<boolean>;
91+
}

0 commit comments

Comments
 (0)