[POC] Provide a metric dashboard - #30
Conversation
📝 WalkthroughWalkthroughAdds an operator-scoped metrics feature: backend serializers and a DRF viewset supporting filtering, aggregation, and grouping; URL routing; a demo-data management command; frontend types, client, hook, page, styles, translations; and Recharts for visualization. Changes
Sequence DiagramsequenceDiagram
actor User
participant Frontend as MetricsPage
participant API as OperatorMetricsViewSet
participant Service as ServiceModel
participant Perms as OperatorAccessPermission
participant DB as Database
participant Serializer as MetricSerializer/AggregatedMetricSerializer
User->>Frontend: select filters (key, service, ...)
Frontend->>API: GET /operators/{id}/metrics?key=...&service=...
API->>Service: validate service exists
Service-->>API: service record
API->>Perms: compute allowed organization ids
Perms-->>API: allowed org ids
API->>DB: query metrics with filters/agg/grouping
DB-->>API: metric rows or aggregates
API->>Serializer: serialize results
Serializer-->>API: JSON payload
API-->>Frontend: MetricsResponse | GroupedMetricsResponse | AggregatedMetric
Frontend->>User: render chart/UI
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@src/backend/core/management/commands/create_demo_operator.py`:
- Line 94: The log statement at logger.info("Created user operator role: %s ->
%s", user.email, operator.name) exposes PII (user.email); update it to avoid
logging the email by replacing user.email with a non-PII identifier such as
user.id or user.pk (or an obfuscated/masked version) in the
create_demo_operator.py code path where the logger.info call lives so the
message still records the created mapping without exposing the user's email.
- Around line 72-74: The current logging statements in create_demo_operator.py
call logger.info with the raw email variable, which exposes PII; update the
logger calls in the block that uses logger.info("Created user: %s", email) and
logger.info("Found existing user: %s", email) to avoid logging the full email
(variable email). Replace them with a non-PII alternative such as logging the
created user's internal id (user.id) or a masked/anonymized identifier (e.g.,
mask_email(email) or email[:1] + "***" + domain) or simply logging a user count
message ("Created demo user" / "Found existing demo user") so no raw email is
emitted.
In `@src/frontend/src/pages/operators/`[operator_id]/metrics/index.tsx:
- Around line 267-270: The "Loading..." literal should be replaced with the i18n
translation function: import and call useTranslation in the component (const { t
} = useTranslation()) if not already present, then change the placeholder
content from "Loading..." to {t('common.loading')} (or your project's preferred
key like 'loading'), keeping the surrounding JSX and the isMetricsLoading
conditional intact; ensure the useTranslation import comes from your i18n
library (e.g., react-i18next) and that the translation key exists in your locale
files.
🧹 Nitpick comments (6)
src/frontend/src/features/i18n/translations.json (1)
214-217: Consider reusing existing account type translations.The
account_typestranslations duplicate values already defined inaccounts.filter.types(lines 132-135). If the UI allows, consider referencing a shared key to avoid maintaining duplicate strings.src/frontend/src/pages/operators/[operator_id]/metrics/index.tsx (2)
32-42: Consider fetching metric keys and account types from the backend.The comment acknowledges this limitation. Hardcoding these values creates a maintenance burden and potential frontend/backend drift.
For now this is acceptable for WIP, but consider adding a
/api/.../operators/{operator_id}/metrics/schema/endpoint or similar in the future to provide available keys and account types dynamically.
161-176: Breadcrumb button navigates to the current page.The breadcrumb button navigates to
/operators/${operatorId}/metrics, which is the current page. This creates a redundant navigation action.🔧 Consider removing the click handler or updating the path
<Breadcrumbs items={[ { - content: <button className="c__breadcrumbs__button" onClick={() => router.push(`/operators/${operatorId}/metrics`)}>{t("metrics.title")}</button>, + content: <span className="c__breadcrumbs__button">{t("metrics.title")}</span>, }, ]} />src/backend/core/api/viewsets/metrics.py (3)
85-87: Use a set comprehension instead of a generator.Per static analysis, this can be simplified.
♻️ Suggested fix
# Get allowed organizations for this operator (convert UUIDs to strings for comparison) - allowed_org_ids = set( - str(org_id) for org_id in self._get_operator_organizations(operator_id) - ) + allowed_org_ids = { + str(org_id) for org_id in self._get_operator_organizations(operator_id) + }
57-82: Consider adding logging for observability.As per coding guidelines, the application should log expected and unexpected actions. Consider adding logging for:
- When a service is not found (INFO level)
- When invalid/unauthorized organization IDs are requested (DEBUG level)
- Optionally, successful metric queries at DEBUG level for troubleshooting
📝 Example
import logging logger = logging.getLogger(__name__) # In list method, after service not found: logger.info("Service %s not found for metrics request", service_id) # After filtering unauthorized orgs: if requested_org_ids - allowed_org_ids: logger.debug( "Filtered out unauthorized org IDs: %s", requested_org_ids - allowed_org_ids )
131-144: Optimize to avoid duplicate database queries.The current implementation issues two separate queries: one for the aggregation and one for the count. These can be combined into a single query.
♻️ Suggested fix
+from django.db.models import Avg, Count, Sum # ... in list method: if agg == "sum": - result = queryset.aggregate(value=Sum("value")) + result = queryset.aggregate(value=Sum("value"), count=Count("id")) else: # avg - result = queryset.aggregate(value=Avg("value")) + result = queryset.aggregate(value=Avg("value"), count=Count("id")) serializer = serializers.AggregatedMetricSerializer( { "key": key, "service_id": service_id, "aggregation": agg, "value": result["value"] or 0, - "count": queryset.count(), + "count": result["count"], } )
| logger.info("Created user: %s", email) | ||
| else: | ||
| logger.info("Found existing user: %s", email) |
There was a problem hiding this comment.
Avoid logging email addresses (PII).
Per coding guidelines, logging should not include PII (Personal Identifiable Information). Consider logging a user count or anonymized identifier instead.
🛡️ Proposed fix
if created:
- logger.info("Created user: %s", email)
+ logger.info("Created new user for demo operator")
else:
- logger.info("Found existing user: %s", email)
+ logger.info("Found existing user for demo operator")📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| logger.info("Created user: %s", email) | |
| else: | |
| logger.info("Found existing user: %s", email) | |
| if created: | |
| logger.info("Created new user for demo operator") | |
| else: | |
| logger.info("Found existing user for demo operator") |
🤖 Prompt for AI Agents
In `@src/backend/core/management/commands/create_demo_operator.py` around lines 72
- 74, The current logging statements in create_demo_operator.py call logger.info
with the raw email variable, which exposes PII; update the logger calls in the
block that uses logger.info("Created user: %s", email) and logger.info("Found
existing user: %s", email) to avoid logging the full email (variable email).
Replace them with a non-PII alternative such as logging the created user's
internal id (user.id) or a masked/anonymized identifier (e.g., mask_email(email)
or email[:1] + "***" + domain) or simply logging a user count message ("Created
demo user" / "Found existing demo user") so no raw email is emitted.
| operator=operator, | ||
| role="admin", | ||
| ) | ||
| logger.info("Created user operator role: %s -> %s", user.email, operator.name) |
There was a problem hiding this comment.
Avoid logging email addresses (PII).
Same PII concern as noted above.
🛡️ Proposed fix
- logger.info("Created user operator role: %s -> %s", user.email, operator.name)
+ logger.info("Created user operator role for operator: %s", operator.name)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| logger.info("Created user operator role: %s -> %s", user.email, operator.name) | |
| logger.info("Created user operator role for operator: %s", operator.name) |
🤖 Prompt for AI Agents
In `@src/backend/core/management/commands/create_demo_operator.py` at line 94, The
log statement at logger.info("Created user operator role: %s -> %s", user.email,
operator.name) exposes PII (user.email); update it to avoid logging the email by
replacing user.email with a non-PII identifier such as user.id or user.pk (or an
obfuscated/masked version) in the create_demo_operator.py code path where the
logger.info call lives so the message still records the created mapping without
exposing the user's email.
| ) : isMetricsLoading ? ( | ||
| <div className="dc__metrics__chart-container__placeholder"> | ||
| Loading... | ||
| </div> |
There was a problem hiding this comment.
Use i18n for loading text.
The "Loading..." text should use the translation function for consistency with the rest of the page.
♻️ Proposed fix
) : isMetricsLoading ? (
<div className="dc__metrics__chart-container__placeholder">
- Loading...
+ {t("common.loading")}
</div>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ) : isMetricsLoading ? ( | |
| <div className="dc__metrics__chart-container__placeholder"> | |
| Loading... | |
| </div> | |
| ) : isMetricsLoading ? ( | |
| <div className="dc__metrics__chart-container__placeholder"> | |
| {t("common.loading")} | |
| </div> |
🤖 Prompt for AI Agents
In `@src/frontend/src/pages/operators/`[operator_id]/metrics/index.tsx around
lines 267 - 270, The "Loading..." literal should be replaced with the i18n
translation function: import and call useTranslation in the component (const { t
} = useTranslation()) if not already present, then change the placeholder
content from "Loading..." to {t('common.loading')} (or your project's preferred
key like 'loading'), keeping the surrounding JSX and the isMetricsLoading
conditional intact; ensure the useTranslation import comes from your i18n
library (e.g., react-i18next) and that the translation key exists in your locale
files.
ca00df0 to
1d7c7a9
Compare
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/backend/core/api/viewsets/metrics.py`:
- Around line 137-145: After fetching the Service with
models.Service.objects.get(id=service_id) you must verify it belongs to the
current operator scope (e.g., compare service.operator or service.operator_id
against the request's operator/context operator) and return a 404/403 if it does
not; update the validation in the same block that defines service so that after
retrieving the object you check membership (e.g., service.operator !=
request.operator or service.operator_id not in operator scope) and respond with
an appropriate error response to prevent accepting services outside the operator
perimeter.
- Around line 179-183: The code currently passes potentially malformed IDs from
accounts_param into queryset.filter(account_id__in=account_ids) which can raise
DB errors; update the handling around
self._parse_comma_separated_ids(accounts_param) to validate each ID as a UUID
and return a controlled 400 on invalid input: call _parse_comma_separated_ids
inside a try/except that catches invalid UUID/ValueError (or change
_parse_comma_separated_ids to raise a ValueError on invalid tokens), and on
exception raise rest_framework.exceptions.ParseError (or
rest_framework.exceptions.ValidationError) with a clear message like "Invalid
account id(s)" so malformed IDs never reach queryset.filter; ensure you import
the chosen exception and keep the variables accounts_param, account_ids, and the
queryset.filter(account_id__in=account_ids) usage intact.
In `@src/backend/core/management/commands/create_demo_operator.py`:
- Around line 294-368: The handle method performs multiple DB writes and must be
wrapped in a single atomic transaction to avoid partial demo data on failure;
import transaction from django.db and wrap the main seeding flow (from creating
the operator in create_operator(...) through creating accounts/metrics and
linking organizations via link_organizations(...) and
create_accounts_and_metrics(...)) in a with transaction.atomic(): block so any
exception rolls back all inserts, re-raise exceptions as currently done to
preserve error handling and logging.
In `@src/frontend/package.json`:
- Line 29: The package.json currently lists "recharts": "2.15.3" but is missing
the peer dependency "react-is" required for React 19 compatibility; update
src/frontend/package.json to add "react-is" with a version matching your React
19 line (e.g., "react-is": "^19.0.0") alongside the existing "recharts":
"2.15.3" entry so recharts resolves correctly.
In `@src/frontend/src/hooks/useQueries.tsx`:
- Around line 366-367: The query key currently uses JSON.stringify(params) which
can fragment cache entries; update the queryKey in the useQueries hook to use a
structured key like ["operators", operatorId, "metrics", params] (passing the
params object directly) and keep queryFn as () => getOperatorMetrics(operatorId,
params!) — ensure params is stable/serializable (or normalized) before adding it
to the key so equivalent param objects produce the same cache key.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 46440241-ab33-42a0-81e7-087cf686d0cd
⛔ Files ignored due to path filters (1)
src/frontend/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (11)
src/backend/core/api/serializers.pysrc/backend/core/api/viewsets/metrics.pysrc/backend/core/management/commands/create_demo_operator.pysrc/backend/core/urls.pysrc/frontend/package.jsonsrc/frontend/src/features/api/Repository.tssrc/frontend/src/features/i18n/translations.jsonsrc/frontend/src/hooks/useQueries.tsxsrc/frontend/src/pages/operators/[operator_id]/metrics/index.scsssrc/frontend/src/pages/operators/[operator_id]/metrics/index.tsxsrc/frontend/src/styles/globals.scss
| # Validate service exists | ||
| try: | ||
| service = models.Service.objects.get(id=service_id) | ||
| except models.Service.DoesNotExist: | ||
| return Response( | ||
| {"error": f"Service with id '{service_id}' not found."}, | ||
| status=status.HTTP_404_NOT_FOUND, | ||
| ) | ||
|
|
There was a problem hiding this comment.
Enforce that service belongs to the operator scope.
Current validation only checks existence. At Line 139, a service outside the operator perimeter is accepted, which weakens endpoint scoping.
Proposed fix
- try:
- service = models.Service.objects.get(id=service_id)
- except models.Service.DoesNotExist:
+ try:
+ service = models.Service.objects.get(id=service_id)
+ except models.Service.DoesNotExist:
return Response(
{"error": f"Service with id '{service_id}' not found."},
status=status.HTTP_404_NOT_FOUND,
)
+ if not models.OperatorServiceConfig.objects.filter(
+ operator_id=operator_id,
+ service_id=service_id,
+ ).exists():
+ return Response(
+ {"error": "Service is not accessible for this operator."},
+ status=status.HTTP_403_FORBIDDEN,
+ )🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/backend/core/api/viewsets/metrics.py` around lines 137 - 145, After
fetching the Service with models.Service.objects.get(id=service_id) you must
verify it belongs to the current operator scope (e.g., compare service.operator
or service.operator_id against the request's operator/context operator) and
return a 404/403 if it does not; update the validation in the same block that
defines service so that after retrieving the object you check membership (e.g.,
service.operator != request.operator or service.operator_id not in operator
scope) and respond with an appropriate error response to prevent accepting
services outside the operator perimeter.
| accounts_param = request.query_params.get("accounts") | ||
| if accounts_param: | ||
| account_ids = self._parse_comma_separated_ids(accounts_param) | ||
| queryset = queryset.filter(account_id__in=account_ids) | ||
|
|
There was a problem hiding this comment.
Validate accounts IDs before applying UUID filter.
At Line 182, malformed IDs can bubble into DB errors instead of returning a controlled 400 response.
Proposed fix
+import uuid
...
accounts_param = request.query_params.get("accounts")
if accounts_param:
- account_ids = self._parse_comma_separated_ids(accounts_param)
+ account_ids = self._parse_comma_separated_ids(accounts_param)
+ try:
+ account_ids = [str(uuid.UUID(v)) for v in account_ids]
+ except (TypeError, ValueError):
+ return Response(
+ {"error": "Query parameter 'accounts' must contain valid UUIDs."},
+ status=status.HTTP_400_BAD_REQUEST,
+ )
queryset = queryset.filter(account_id__in=account_ids)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/backend/core/api/viewsets/metrics.py` around lines 179 - 183, The code
currently passes potentially malformed IDs from accounts_param into
queryset.filter(account_id__in=account_ids) which can raise DB errors; update
the handling around self._parse_comma_separated_ids(accounts_param) to validate
each ID as a UUID and return a controlled 400 on invalid input: call
_parse_comma_separated_ids inside a try/except that catches invalid
UUID/ValueError (or change _parse_comma_separated_ids to raise a ValueError on
invalid tokens), and on exception raise rest_framework.exceptions.ParseError (or
rest_framework.exceptions.ValidationError) with a clear message like "Invalid
account id(s)" so malformed IDs never reach queryset.filter; ensure you import
the chosen exception and keep the variables accounts_param, account_ids, and the
queryset.filter(account_id__in=account_ids) usage intact.
| def handle(self, *args, **options): | ||
| """Execute the command.""" | ||
| email = options["email"] | ||
| operator_name = options["operator_name"] or f"Demo Operator {uuid.uuid4().hex[:6]}" | ||
|
|
||
| self.stdout.write(f"Creating demo operator: {operator_name}") | ||
| self.stdout.write(f"User email: {email}") | ||
| self.stdout.write("") | ||
|
|
||
| # Step 1: Create operator | ||
| self.stdout.write("Step 1: Creating operator...") | ||
| operator = create_operator(operator_name) | ||
| self.stdout.write(self.style.SUCCESS(f" Created operator: {operator.name}")) | ||
|
|
||
| # Step 2: Get or create user and link to operator | ||
| self.stdout.write("Step 2: Setting up user and role...") | ||
| user = get_or_create_user(email) | ||
| create_user_operator_role(user, operator) | ||
| self.stdout.write(self.style.SUCCESS(f" Linked user {email} to operator")) | ||
|
|
||
| # Step 3: Create services | ||
| self.stdout.write("Step 3: Creating services...") | ||
| messages_service, drive_service = create_services(operator) | ||
| self.stdout.write(self.style.SUCCESS(f" Created messages service: {messages_service.name}")) | ||
| self.stdout.write(self.style.SUCCESS(f" Created drive service: {drive_service.name}")) | ||
|
|
||
| # Step 4: Link organizations | ||
| self.stdout.write(f"Step 4: Linking {ORGANIZATIONS_TO_LINK} organizations...") | ||
| try: | ||
| organizations = link_organizations(operator, ORGANIZATIONS_TO_LINK) | ||
| self.stdout.write( | ||
| self.style.SUCCESS(f" Linked {len(organizations)} organizations") | ||
| ) | ||
| except CommandError as e: | ||
| self.stdout.write(self.style.ERROR(str(e))) | ||
| raise | ||
|
|
||
| # Step 5: Create accounts and metrics for each organization | ||
| self.stdout.write("Step 5: Creating accounts and metrics...") | ||
| total_accounts = 0 | ||
| total_metrics = 0 | ||
|
|
||
| for organization in organizations: | ||
| self.stdout.write(f" Processing organization: {organization.name}") | ||
|
|
||
| # Drive service: accounts with type "user" (with email and external_id) | ||
| drive_accounts, drive_metrics = create_accounts_and_metrics( | ||
| organization=organization, | ||
| service=drive_service, | ||
| account_type="user", | ||
| count=METRICS_PER_SERVICE, | ||
| include_email=True, | ||
| ) | ||
| total_accounts += len(drive_accounts) | ||
| total_metrics += len(drive_metrics) | ||
|
|
||
| # Messages service: accounts with type "mailbox" (no email, only external_id) | ||
| messages_accounts, messages_metrics = create_accounts_and_metrics( | ||
| organization=organization, | ||
| service=messages_service, | ||
| account_type="mailbox", | ||
| count=METRICS_PER_SERVICE, | ||
| include_email=False, | ||
| ) | ||
| total_accounts += len(messages_accounts) | ||
| total_metrics += len(messages_metrics) | ||
|
|
||
| self.stdout.write("") | ||
| self.stdout.write(self.style.SUCCESS("Demo operator created successfully!")) | ||
| self.stdout.write(self.style.SUCCESS(f" Operator: {operator.name} (ID: {operator.id})")) | ||
| self.stdout.write(self.style.SUCCESS(f" User: {email}")) | ||
| self.stdout.write(self.style.SUCCESS(f" Services: {messages_service.name}, {drive_service.name}")) | ||
| self.stdout.write(self.style.SUCCESS(f" Organizations linked: {len(organizations)}")) | ||
| self.stdout.write(self.style.SUCCESS(f" Total accounts created: {total_accounts}")) | ||
| self.stdout.write(self.style.SUCCESS(f" Total metrics created: {total_metrics}")) |
There was a problem hiding this comment.
Wrap the end-to-end seeding flow in a DB transaction.
A failure after partial inserts leaves inconsistent demo data (operator/services/roles/metrics partially created). Add an atomic boundary in handle (Line 294 onward).
Proposed fix
+from django.db import transaction
...
def handle(self, *args, **options):
"""Execute the command."""
- email = options["email"]
- operator_name = options["operator_name"] or f"Demo Operator {uuid.uuid4().hex[:6]}"
+ email = options["email"]
+ operator_name = options["operator_name"] or f"Demo Operator {uuid.uuid4().hex[:6]}"
- self.stdout.write(f"Creating demo operator: {operator_name}")
- self.stdout.write(f"User email: {email}")
- self.stdout.write("")
+ with transaction.atomic():
+ self.stdout.write(f"Creating demo operator: {operator_name}")
+ self.stdout.write(f"User email: {email}")
+ self.stdout.write("")
- # Step 1: Create operator
- self.stdout.write("Step 1: Creating operator...")
- operator = create_operator(operator_name)
- self.stdout.write(self.style.SUCCESS(f" Created operator: {operator.name}"))
+ # Step 1: Create operator
+ self.stdout.write("Step 1: Creating operator...")
+ operator = create_operator(operator_name)
+ self.stdout.write(self.style.SUCCESS(f" Created operator: {operator.name}"))
...📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| def handle(self, *args, **options): | |
| """Execute the command.""" | |
| email = options["email"] | |
| operator_name = options["operator_name"] or f"Demo Operator {uuid.uuid4().hex[:6]}" | |
| self.stdout.write(f"Creating demo operator: {operator_name}") | |
| self.stdout.write(f"User email: {email}") | |
| self.stdout.write("") | |
| # Step 1: Create operator | |
| self.stdout.write("Step 1: Creating operator...") | |
| operator = create_operator(operator_name) | |
| self.stdout.write(self.style.SUCCESS(f" Created operator: {operator.name}")) | |
| # Step 2: Get or create user and link to operator | |
| self.stdout.write("Step 2: Setting up user and role...") | |
| user = get_or_create_user(email) | |
| create_user_operator_role(user, operator) | |
| self.stdout.write(self.style.SUCCESS(f" Linked user {email} to operator")) | |
| # Step 3: Create services | |
| self.stdout.write("Step 3: Creating services...") | |
| messages_service, drive_service = create_services(operator) | |
| self.stdout.write(self.style.SUCCESS(f" Created messages service: {messages_service.name}")) | |
| self.stdout.write(self.style.SUCCESS(f" Created drive service: {drive_service.name}")) | |
| # Step 4: Link organizations | |
| self.stdout.write(f"Step 4: Linking {ORGANIZATIONS_TO_LINK} organizations...") | |
| try: | |
| organizations = link_organizations(operator, ORGANIZATIONS_TO_LINK) | |
| self.stdout.write( | |
| self.style.SUCCESS(f" Linked {len(organizations)} organizations") | |
| ) | |
| except CommandError as e: | |
| self.stdout.write(self.style.ERROR(str(e))) | |
| raise | |
| # Step 5: Create accounts and metrics for each organization | |
| self.stdout.write("Step 5: Creating accounts and metrics...") | |
| total_accounts = 0 | |
| total_metrics = 0 | |
| for organization in organizations: | |
| self.stdout.write(f" Processing organization: {organization.name}") | |
| # Drive service: accounts with type "user" (with email and external_id) | |
| drive_accounts, drive_metrics = create_accounts_and_metrics( | |
| organization=organization, | |
| service=drive_service, | |
| account_type="user", | |
| count=METRICS_PER_SERVICE, | |
| include_email=True, | |
| ) | |
| total_accounts += len(drive_accounts) | |
| total_metrics += len(drive_metrics) | |
| # Messages service: accounts with type "mailbox" (no email, only external_id) | |
| messages_accounts, messages_metrics = create_accounts_and_metrics( | |
| organization=organization, | |
| service=messages_service, | |
| account_type="mailbox", | |
| count=METRICS_PER_SERVICE, | |
| include_email=False, | |
| ) | |
| total_accounts += len(messages_accounts) | |
| total_metrics += len(messages_metrics) | |
| self.stdout.write("") | |
| self.stdout.write(self.style.SUCCESS("Demo operator created successfully!")) | |
| self.stdout.write(self.style.SUCCESS(f" Operator: {operator.name} (ID: {operator.id})")) | |
| self.stdout.write(self.style.SUCCESS(f" User: {email}")) | |
| self.stdout.write(self.style.SUCCESS(f" Services: {messages_service.name}, {drive_service.name}")) | |
| self.stdout.write(self.style.SUCCESS(f" Organizations linked: {len(organizations)}")) | |
| self.stdout.write(self.style.SUCCESS(f" Total accounts created: {total_accounts}")) | |
| self.stdout.write(self.style.SUCCESS(f" Total metrics created: {total_metrics}")) | |
| def handle(self, *args, **options): | |
| """Execute the command.""" | |
| from django.db import transaction | |
| email = options["email"] | |
| operator_name = options["operator_name"] or f"Demo Operator {uuid.uuid4().hex[:6]}" | |
| with transaction.atomic(): | |
| self.stdout.write(f"Creating demo operator: {operator_name}") | |
| self.stdout.write(f"User email: {email}") | |
| self.stdout.write("") | |
| # Step 1: Create operator | |
| self.stdout.write("Step 1: Creating operator...") | |
| operator = create_operator(operator_name) | |
| self.stdout.write(self.style.SUCCESS(f" Created operator: {operator.name}")) | |
| # Step 2: Get or create user and link to operator | |
| self.stdout.write("Step 2: Setting up user and role...") | |
| user = get_or_create_user(email) | |
| create_user_operator_role(user, operator) | |
| self.stdout.write(self.style.SUCCESS(f" Linked user {email} to operator")) | |
| # Step 3: Create services | |
| self.stdout.write("Step 3: Creating services...") | |
| messages_service, drive_service = create_services(operator) | |
| self.stdout.write(self.style.SUCCESS(f" Created messages service: {messages_service.name}")) | |
| self.stdout.write(self.style.SUCCESS(f" Created drive service: {drive_service.name}")) | |
| # Step 4: Link organizations | |
| self.stdout.write(f"Step 4: Linking {ORGANIZATIONS_TO_LINK} organizations...") | |
| try: | |
| organizations = link_organizations(operator, ORGANIZATIONS_TO_LINK) | |
| self.stdout.write( | |
| self.style.SUCCESS(f" Linked {len(organizations)} organizations") | |
| ) | |
| except CommandError as e: | |
| self.stdout.write(self.style.ERROR(str(e))) | |
| raise | |
| # Step 5: Create accounts and metrics for each organization | |
| self.stdout.write("Step 5: Creating accounts and metrics...") | |
| total_accounts = 0 | |
| total_metrics = 0 | |
| for organization in organizations: | |
| self.stdout.write(f" Processing organization: {organization.name}") | |
| # Drive service: accounts with type "user" (with email and external_id) | |
| drive_accounts, drive_metrics = create_accounts_and_metrics( | |
| organization=organization, | |
| service=drive_service, | |
| account_type="user", | |
| count=METRICS_PER_SERVICE, | |
| include_email=True, | |
| ) | |
| total_accounts += len(drive_accounts) | |
| total_metrics += len(drive_metrics) | |
| # Messages service: accounts with type "mailbox" (no email, only external_id) | |
| messages_accounts, messages_metrics = create_accounts_and_metrics( | |
| organization=organization, | |
| service=messages_service, | |
| account_type="mailbox", | |
| count=METRICS_PER_SERVICE, | |
| include_email=False, | |
| ) | |
| total_accounts += len(messages_accounts) | |
| total_metrics += len(messages_metrics) | |
| self.stdout.write("") | |
| self.stdout.write(self.style.SUCCESS("Demo operator created successfully!")) | |
| self.stdout.write(self.style.SUCCESS(f" Operator: {operator.name} (ID: {operator.id})")) | |
| self.stdout.write(self.style.SUCCESS(f" User: {email}")) | |
| self.stdout.write(self.style.SUCCESS(f" Services: {messages_service.name}, {drive_service.name}")) | |
| self.stdout.write(self.style.SUCCESS(f" Organizations linked: {len(organizations)}")) | |
| self.stdout.write(self.style.SUCCESS(f" Total accounts created: {total_accounts}")) | |
| self.stdout.write(self.style.SUCCESS(f" Total metrics created: {total_metrics}")) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/backend/core/management/commands/create_demo_operator.py` around lines
294 - 368, The handle method performs multiple DB writes and must be wrapped in
a single atomic transaction to avoid partial demo data on failure; import
transaction from django.db and wrap the main seeding flow (from creating the
operator in create_operator(...) through creating accounts/metrics and linking
organizations via link_organizations(...) and create_accounts_and_metrics(...))
in a with transaction.atomic(): block so any exception rolls back all inserts,
re-raise exceptions as currently done to preserve error handling and logging.
| "react-hook-form": "7.67.0", | ||
| "react-i18next": "15.4.1", | ||
| "react-toastify": "11.0.2", | ||
| "recharts": "2.15.3", |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
What are the official peerDependencies and React version compatibility for recharts 2.15.3, and does it explicitly support React 19.x?
💡 Result:
recharts 2.15.3 official peerDependencies (as published around the 2.15.x “Final 2.x React 19 support” timeframe): - react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 [1] - react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 [1] - (Additionally required in this period) react-is must be version-matched/overridden for React 19 to work with recharts (this is explicitly called out by the project’s React 19 support notes for the 2.x line). [2][3] Does it explicitly support React 19.x? - Yes. React 19 support is explicitly added to the peerDependencies (React and ReactDOM include “|| ^19.0.0”). [1] - However, the project also explicitly warns that for React 19 to work correctly in the 2.x line, you must override/ensure a matching react-is version (a limitation noted in the 2.x React 19 support release notes and the related support issue). [2][3] Note: The peerDependency range above is evidenced in the recharts v2.15.0 package.json diff that introduced React 19 into peerDeps, and the 2.15.x patch releases (including 2.15.3) are part of that same React-19-supported 2.x line. [1][4]
Citations:
- 1: recharts/recharts@v2.14.1...v2.15.0
- 2: https://github.com/recharts/recharts/releases/tag/v2.15.0
- 3: Support React 19 recharts/recharts#4558
- 4: recharts/recharts@v2.15.3...v2.15.4
🏁 Script executed:
cat -n src/frontend/package.json | grep -E "(react-is|recharts|react[^a-z-]|react-dom)"Repository: suitenumerique/st-deploycenter
Length of output: 292
Add react-is dependency to package.json—required by recharts for React 19 compatibility.
While recharts@2.15.3 officially supports React 19, the recharts project explicitly requires a matching react-is version for React 19 to work correctly with the 2.x line (per v2.15.0 release notes and issue #4558). This dependency is currently missing. Add react-is with a version that matches your React version (typically ^19.0.0 for React 19.1.0).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/frontend/package.json` at line 29, The package.json currently lists
"recharts": "2.15.3" but is missing the peer dependency "react-is" required for
React 19 compatibility; update src/frontend/package.json to add "react-is" with
a version matching your React 19 line (e.g., "react-is": "^19.0.0") alongside
the existing "recharts": "2.15.3" entry so recharts resolves correctly.
| queryKey: ["operators", operatorId, "metrics", JSON.stringify(params)], | ||
| queryFn: () => getOperatorMetrics(operatorId, params!), |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | ⚡ Quick win
Use structured query keys instead of JSON.stringify(params).
At Line 366, serializing params can fragment cache entries for equivalent objects with different key order. Prefer passing the params object directly in queryKey.
Proposed fix
export const useOperatorMetrics = (
operatorId: string,
params: MetricsParams | null,
) => {
return useQuery({
- queryKey: ["operators", operatorId, "metrics", JSON.stringify(params)],
+ queryKey: ["operators", operatorId, "metrics", params],
queryFn: () => getOperatorMetrics(operatorId, params!),
enabled: !!operatorId && !!params?.key && !!params?.service,
});
};📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| queryKey: ["operators", operatorId, "metrics", JSON.stringify(params)], | |
| queryFn: () => getOperatorMetrics(operatorId, params!), | |
| export const useOperatorMetrics = ( | |
| operatorId: string, | |
| params: MetricsParams | null, | |
| ) => { | |
| return useQuery({ | |
| queryKey: ["operators", operatorId, "metrics", params], | |
| queryFn: () => getOperatorMetrics(operatorId, params!), | |
| enabled: !!operatorId && !!params?.key && !!params?.service, | |
| }); | |
| }; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/frontend/src/hooks/useQueries.tsx` around lines 366 - 367, The query key
currently uses JSON.stringify(params) which can fragment cache entries; update
the queryKey in the useQueries hook to use a structured key like ["operators",
operatorId, "metrics", params] (passing the params object directly) and keep
queryFn as () => getOperatorMetrics(operatorId, params!) — ensure params is
stable/serializable (or normalized) before adding it to the key so equivalent
param objects produce the same cache key.
Add metrics endpoints scoped to an operator with filtering by service/key/organization/account_type and optional aggregation or grouping. Powers the frontend metrics dashboard.
Render filtered/aggregated metrics with recharts, including a unit selector (raw, MB, GB, TB) so storage values can be read in a relevant scale without flooding the chart with bytes.
Seed a complete operator with services, organizations and metrics so the dashboard can be exercised end-to-end in local and demo environments without hand-crafting fixtures.
Surface "Adhérents" and "Métriques" entries on every operator-scoped page so users can move between sections without going through the breadcrumb.
74ddcaa to
a87de80
Compare
Purpose
Provide a metric dashboard. It is a first shot waiting for feedbacks.
To get a sample or data in order to test this dashboard run
python manage.py create_demo_operatorTodo
Proposal
Summary by CodeRabbit
New Features
Chores