From 3222c4a6dd39aa1d3139e5b43f249c20177ae107 Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Mon, 26 Aug 2024 14:31:29 +0530 Subject: [PATCH 1/9] chore: project cycle optimization --- apiserver/plane/app/urls/cycle.py | 12 + apiserver/plane/app/views/__init__.py | 2 + apiserver/plane/app/views/cycle/base.py | 990 +++++++++--------------- 3 files changed, 382 insertions(+), 622 deletions(-) diff --git a/apiserver/plane/app/urls/cycle.py b/apiserver/plane/app/urls/cycle.py index ce2e0f6dcce..0d62d0271da 100644 --- a/apiserver/plane/app/urls/cycle.py +++ b/apiserver/plane/app/urls/cycle.py @@ -6,6 +6,8 @@ CycleIssueViewSet, CycleDateCheckEndpoint, CycleFavoriteViewSet, + CycleProgressEndpoint, + CycleAnalyticsEndpoint, TransferCycleIssueEndpoint, CycleUserPropertiesEndpoint, CycleArchiveUnarchiveEndpoint, @@ -106,4 +108,14 @@ CycleArchiveUnarchiveEndpoint.as_view(), name="cycle-archive-unarchive", ), + path( + "workspaces//projects//cycles//progress/", + CycleProgressEndpoint.as_view(), + name="project-cycle", + ), + path( + "workspaces//projects//cycles//analytics/", + CycleAnalyticsEndpoint.as_view(), + name="project-cycle", + ), ] diff --git a/apiserver/plane/app/views/__init__.py b/apiserver/plane/app/views/__init__.py index 5568542f70a..f3dd1a21e39 100644 --- a/apiserver/plane/app/views/__init__.py +++ b/apiserver/plane/app/views/__init__.py @@ -98,6 +98,8 @@ CycleUserPropertiesEndpoint, CycleViewSet, TransferCycleIssueEndpoint, + CycleAnalyticsEndpoint, + CycleProgressEndpoint, ) from .cycle.issue import ( CycleIssueViewSet, diff --git a/apiserver/plane/app/views/cycle/base.py b/apiserver/plane/app/views/cycle/base.py index 86f9de0ef62..d8dde0d4ef8 100644 --- a/apiserver/plane/app/views/cycle/base.py +++ b/apiserver/plane/app/views/cycle/base.py @@ -17,7 +17,6 @@ UUIDField, Value, When, - Subquery, Sum, FloatField, ) @@ -28,9 +27,7 @@ # Third party imports from rest_framework import status from rest_framework.response import Response -from plane.app.permissions import ( - allow_permission, ROLE -) +from plane.app.permissions import allow_permission, ROLE from plane.app.serializers import ( CycleSerializer, CycleUserPropertiesSerializer, @@ -69,89 +66,6 @@ def get_queryset(self): project_id=self.kwargs.get("project_id"), workspace__slug=self.kwargs.get("slug"), ) - backlog_estimate_point = ( - Issue.issue_objects.filter( - estimate_point__estimate__type="points", - state__group="backlog", - issue_cycle__cycle_id=OuterRef("pk"), - ) - .values("issue_cycle__cycle_id") - .annotate( - backlog_estimate_point=Sum( - Cast("estimate_point__value", FloatField()) - ) - ) - .values("backlog_estimate_point")[:1] - ) - unstarted_estimate_point = ( - Issue.issue_objects.filter( - estimate_point__estimate__type="points", - state__group="unstarted", - issue_cycle__cycle_id=OuterRef("pk"), - ) - .values("issue_cycle__cycle_id") - .annotate( - unstarted_estimate_point=Sum( - Cast("estimate_point__value", FloatField()) - ) - ) - .values("unstarted_estimate_point")[:1] - ) - started_estimate_point = ( - Issue.issue_objects.filter( - estimate_point__estimate__type="points", - state__group="started", - issue_cycle__cycle_id=OuterRef("pk"), - ) - .values("issue_cycle__cycle_id") - .annotate( - started_estimate_point=Sum( - Cast("estimate_point__value", FloatField()) - ) - ) - .values("started_estimate_point")[:1] - ) - cancelled_estimate_point = ( - Issue.issue_objects.filter( - estimate_point__estimate__type="points", - state__group="cancelled", - issue_cycle__cycle_id=OuterRef("pk"), - ) - .values("issue_cycle__cycle_id") - .annotate( - cancelled_estimate_point=Sum( - Cast("estimate_point__value", FloatField()) - ) - ) - .values("cancelled_estimate_point")[:1] - ) - completed_estimate_point = ( - Issue.issue_objects.filter( - estimate_point__estimate__type="points", - state__group="completed", - issue_cycle__cycle_id=OuterRef("pk"), - ) - .values("issue_cycle__cycle_id") - .annotate( - completed_estimate_points=Sum( - Cast("estimate_point__value", FloatField()) - ) - ) - .values("completed_estimate_points")[:1] - ) - total_estimate_point = ( - Issue.issue_objects.filter( - estimate_point__estimate__type="points", - issue_cycle__cycle_id=OuterRef("pk"), - ) - .values("issue_cycle__cycle_id") - .annotate( - total_estimate_points=Sum( - Cast("estimate_point__value", FloatField()) - ) - ) - .values("total_estimate_points")[:1] - ) return self.filter_queryset( super() .get_queryset() @@ -201,50 +115,6 @@ def get_queryset(self): ), ) ) - .annotate( - cancelled_issues=Count( - "issue_cycle__issue__id", - distinct=True, - filter=Q( - issue_cycle__issue__state__group="cancelled", - issue_cycle__issue__archived_at__isnull=True, - issue_cycle__issue__is_draft=False, - ), - ) - ) - .annotate( - started_issues=Count( - "issue_cycle__issue__id", - distinct=True, - filter=Q( - issue_cycle__issue__state__group="started", - issue_cycle__issue__archived_at__isnull=True, - issue_cycle__issue__is_draft=False, - ), - ) - ) - .annotate( - unstarted_issues=Count( - "issue_cycle__issue__id", - distinct=True, - filter=Q( - issue_cycle__issue__state__group="unstarted", - issue_cycle__issue__archived_at__isnull=True, - issue_cycle__issue__is_draft=False, - ), - ) - ) - .annotate( - backlog_issues=Count( - "issue_cycle__issue__id", - distinct=True, - filter=Q( - issue_cycle__issue__state__group="backlog", - issue_cycle__issue__archived_at__isnull=True, - issue_cycle__issue__is_draft=False, - ), - ) - ) .annotate( status=Case( When( @@ -276,42 +146,6 @@ def get_queryset(self): Value([], output_field=ArrayField(UUIDField())), ) ) - .annotate( - backlog_estimate_points=Coalesce( - Subquery(backlog_estimate_point), - Value(0, output_field=FloatField()), - ), - ) - .annotate( - unstarted_estimate_points=Coalesce( - Subquery(unstarted_estimate_point), - Value(0, output_field=FloatField()), - ), - ) - .annotate( - started_estimate_points=Coalesce( - Subquery(started_estimate_point), - Value(0, output_field=FloatField()), - ), - ) - .annotate( - cancelled_estimate_points=Coalesce( - Subquery(cancelled_estimate_point), - Value(0, output_field=FloatField()), - ), - ) - .annotate( - completed_estimate_points=Coalesce( - Subquery(completed_estimate_point), - Value(0, output_field=FloatField()), - ), - ) - .annotate( - total_estimate_points=Coalesce( - Subquery(total_estimate_point), - Value(0, output_field=FloatField()), - ), - ) .order_by("-is_favorite", "name") .distinct() ) @@ -348,227 +182,16 @@ def list(self, request, slug, project_id): "external_id", "progress_snapshot", "logo_props", - # meta fields - "backlog_estimate_points", - "unstarted_estimate_points", - "started_estimate_points", - "cancelled_estimate_points", - "completed_estimate_points", - "total_estimate_points", "is_favorite", "total_issues", - "cancelled_issues", "completed_issues", - "started_issues", - "unstarted_issues", - "backlog_issues", "assignee_ids", "status", "created_by", ) - estimate_type = Project.objects.filter( - workspace__slug=slug, - pk=project_id, - estimate__isnull=False, - estimate__type="points", - ).exists() if data: - data[0]["estimate_distribution"] = {} - if estimate_type: - assignee_distribution = ( - Issue.issue_objects.filter( - issue_cycle__cycle_id=data[0]["id"], - workspace__slug=slug, - project_id=project_id, - ) - .annotate(display_name=F("assignees__display_name")) - .annotate(assignee_id=F("assignees__id")) - .annotate(avatar=F("assignees__avatar")) - .values("display_name", "assignee_id", "avatar") - .annotate( - total_estimates=Sum( - Cast("estimate_point__value", FloatField()) - ) - ) - .annotate( - completed_estimates=Sum( - Cast("estimate_point__value", FloatField()), - filter=Q( - completed_at__isnull=False, - archived_at__isnull=True, - is_draft=False, - ), - ) - ) - .annotate( - pending_estimates=Sum( - Cast("estimate_point__value", FloatField()), - filter=Q( - completed_at__isnull=True, - archived_at__isnull=True, - is_draft=False, - ), - ) - ) - .order_by("display_name") - ) - - label_distribution = ( - Issue.issue_objects.filter( - issue_cycle__cycle_id=data[0]["id"], - workspace__slug=slug, - project_id=project_id, - ) - .annotate(label_name=F("labels__name")) - .annotate(color=F("labels__color")) - .annotate(label_id=F("labels__id")) - .values("label_name", "color", "label_id") - .annotate( - total_estimates=Sum( - Cast("estimate_point__value", FloatField()) - ) - ) - .annotate( - completed_estimates=Sum( - Cast("estimate_point__value", FloatField()), - filter=Q( - completed_at__isnull=False, - archived_at__isnull=True, - is_draft=False, - ), - ) - ) - .annotate( - pending_estimates=Sum( - Cast("estimate_point__value", FloatField()), - filter=Q( - completed_at__isnull=True, - archived_at__isnull=True, - is_draft=False, - ), - ) - ) - .order_by("label_name") - ) - data[0]["estimate_distribution"] = { - "assignees": assignee_distribution, - "labels": label_distribution, - "completion_chart": {}, - } - - if data[0]["start_date"] and data[0]["end_date"]: - data[0]["estimate_distribution"][ - "completion_chart" - ] = burndown_plot( - queryset=queryset.first(), - slug=slug, - project_id=project_id, - plot_type="points", - cycle_id=data[0]["id"], - ) - - assignee_distribution = ( - Issue.issue_objects.filter( - issue_cycle__cycle_id=data[0]["id"], - workspace__slug=slug, - project_id=project_id, - ) - .annotate(display_name=F("assignees__display_name")) - .annotate(assignee_id=F("assignees__id")) - .annotate(avatar=F("assignees__avatar")) - .values("display_name", "assignee_id", "avatar") - .annotate( - total_issues=Count( - "id", - filter=Q( - archived_at__isnull=True, - is_draft=False, - ), - ), - ) - .annotate( - completed_issues=Count( - "id", - filter=Q( - completed_at__isnull=False, - archived_at__isnull=True, - is_draft=False, - ), - ) - ) - .annotate( - pending_issues=Count( - "id", - filter=Q( - completed_at__isnull=True, - archived_at__isnull=True, - is_draft=False, - ), - ) - ) - .order_by("display_name") - ) - - label_distribution = ( - Issue.issue_objects.filter( - issue_cycle__cycle_id=data[0]["id"], - workspace__slug=slug, - project_id=project_id, - ) - .annotate(label_name=F("labels__name")) - .annotate(color=F("labels__color")) - .annotate(label_id=F("labels__id")) - .values("label_name", "color", "label_id") - .annotate( - total_issues=Count( - "id", - filter=Q( - archived_at__isnull=True, - is_draft=False, - ), - ), - ) - .annotate( - completed_issues=Count( - "id", - filter=Q( - completed_at__isnull=False, - archived_at__isnull=True, - is_draft=False, - ), - ) - ) - .annotate( - pending_issues=Count( - "id", - filter=Q( - completed_at__isnull=True, - archived_at__isnull=True, - is_draft=False, - ), - ) - ) - .order_by("label_name") - ) - data[0]["distribution"] = { - "assignees": assignee_distribution, - "labels": label_distribution, - "completion_chart": {}, - } - - if data[0]["start_date"] and data[0]["end_date"]: - data[0]["distribution"]["completion_chart"] = ( - burndown_plot( - queryset=queryset.first(), - slug=slug, - project_id=project_id, - plot_type="issues", - cycle_id=data[0]["id"], - ) - ) - - return Response(data, status=status.HTTP_200_OK) + return Response(data, status=status.HTTP_200_OK) data = queryset.values( # necessary fields @@ -588,15 +211,9 @@ def list(self, request, slug, project_id): "progress_snapshot", "logo_props", # meta fields - "completed_estimate_points", - "total_estimate_points", "is_favorite", "total_issues", - "cancelled_issues", "completed_issues", - "started_issues", - "unstarted_issues", - "backlog_issues", "assignee_ids", "status", "created_by", @@ -639,15 +256,9 @@ def create(self, request, slug, project_id): "progress_snapshot", "logo_props", # meta fields - "completed_estimate_points", - "total_estimate_points", "is_favorite", - "cancelled_issues", "total_issues", "completed_issues", - "started_issues", - "unstarted_issues", - "backlog_issues", "assignee_ids", "status", "created_by", @@ -737,15 +348,9 @@ def partial_update(self, request, slug, project_id, pk): "progress_snapshot", "logo_props", # meta fields - "completed_estimate_points", - "total_estimate_points", "is_favorite", "total_issues", - "cancelled_issues", "completed_issues", - "started_issues", - "unstarted_issues", - "backlog_issues", "assignee_ids", "status", "created_by", @@ -802,232 +407,16 @@ def retrieve(self, request, slug, project_id, pk): "sub_issues", "logo_props", # meta fields - "completed_estimate_points", - "total_estimate_points", - "is_favorite", - "total_issues", - "cancelled_issues", - "completed_issues", - "started_issues", - "unstarted_issues", - "backlog_issues", - "assignee_ids", - "status", - "created_by", - ) - .first() - ) - queryset = queryset.first() - - estimate_type = Project.objects.filter( - workspace__slug=slug, - pk=project_id, - estimate__isnull=False, - estimate__type="points", - ).exists() - - data["estimate_distribution"] = {} - if estimate_type: - assignee_distribution = ( - Issue.issue_objects.filter( - issue_cycle__cycle_id=pk, - workspace__slug=slug, - project_id=project_id, - ) - .annotate(display_name=F("assignees__display_name")) - .annotate(assignee_id=F("assignees__id")) - .annotate(avatar=F("assignees__avatar")) - .values("display_name", "assignee_id", "avatar") - .annotate( - total_estimates=Sum( - Cast("estimate_point__value", FloatField()) - ) - ) - .annotate( - completed_estimates=Sum( - Cast("estimate_point__value", FloatField()), - filter=Q( - completed_at__isnull=False, - archived_at__isnull=True, - is_draft=False, - ), - ) - ) - .annotate( - pending_estimates=Sum( - Cast("estimate_point__value", FloatField()), - filter=Q( - completed_at__isnull=True, - archived_at__isnull=True, - is_draft=False, - ), - ) - ) - .order_by("display_name") - ) - - label_distribution = ( - Issue.issue_objects.filter( - issue_cycle__cycle_id=pk, - workspace__slug=slug, - project_id=project_id, - ) - .annotate(label_name=F("labels__name")) - .annotate(color=F("labels__color")) - .annotate(label_id=F("labels__id")) - .values("label_name", "color", "label_id") - .annotate( - total_estimates=Sum( - Cast("estimate_point__value", FloatField()) - ) - ) - .annotate( - completed_estimates=Sum( - Cast("estimate_point__value", FloatField()), - filter=Q( - completed_at__isnull=False, - archived_at__isnull=True, - is_draft=False, - ), - ) - ) - .annotate( - pending_estimates=Sum( - Cast("estimate_point__value", FloatField()), - filter=Q( - completed_at__isnull=True, - archived_at__isnull=True, - is_draft=False, - ), - ) - ) - .order_by("label_name") - ) - data["estimate_distribution"] = { - "assignees": assignee_distribution, - "labels": label_distribution, - "completion_chart": {}, - } - - if data["start_date"] and data["end_date"]: - data["estimate_distribution"]["completion_chart"] = ( - burndown_plot( - queryset=queryset, - slug=slug, - project_id=project_id, - plot_type="points", - cycle_id=pk, - ) - ) - - # Assignee Distribution - assignee_distribution = ( - Issue.issue_objects.filter( - issue_cycle__cycle_id=pk, - workspace__slug=slug, - project_id=project_id, - ) - .annotate(first_name=F("assignees__first_name")) - .annotate(last_name=F("assignees__last_name")) - .annotate(assignee_id=F("assignees__id")) - .annotate(avatar=F("assignees__avatar")) - .annotate(display_name=F("assignees__display_name")) - .values( - "first_name", - "last_name", - "assignee_id", - "avatar", - "display_name", - ) - .annotate( - total_issues=Count( - "id", - filter=Q( - archived_at__isnull=True, - is_draft=False, - ), - ), - ) - .annotate( - completed_issues=Count( - "id", - filter=Q( - completed_at__isnull=False, - archived_at__isnull=True, - is_draft=False, - ), - ) - ) - .annotate( - pending_issues=Count( - "id", - filter=Q( - completed_at__isnull=True, - archived_at__isnull=True, - is_draft=False, - ), - ) - ) - .order_by("first_name", "last_name") - ) - - # Label Distribution - label_distribution = ( - Issue.issue_objects.filter( - issue_cycle__cycle_id=pk, - workspace__slug=slug, - project_id=project_id, - ) - .annotate(label_name=F("labels__name")) - .annotate(color=F("labels__color")) - .annotate(label_id=F("labels__id")) - .values("label_name", "color", "label_id") - .annotate( - total_issues=Count( - "id", - filter=Q( - archived_at__isnull=True, - is_draft=False, - ), - ), - ) - .annotate( - completed_issues=Count( - "id", - filter=Q( - completed_at__isnull=False, - archived_at__isnull=True, - is_draft=False, - ), - ) - ) - .annotate( - pending_issues=Count( - "id", - filter=Q( - completed_at__isnull=True, - archived_at__isnull=True, - is_draft=False, - ), - ) - ) - .order_by("label_name") - ) - - data["distribution"] = { - "assignees": assignee_distribution, - "labels": label_distribution, - "completion_chart": {}, - } - - if queryset.start_date and queryset.end_date: - data["distribution"]["completion_chart"] = burndown_plot( - queryset=queryset, - slug=slug, - project_id=project_id, - plot_type="issues", - cycle_id=pk, + "is_favorite", + "total_issues", + "completed_issues", + "assignee_ids", + "status", + "created_by", ) + .first() + ) + queryset = queryset.first() recent_visited_task.delay( slug=slug, @@ -1614,3 +1003,360 @@ def get(self, request, slug, project_id, cycle_id): ) serializer = CycleUserPropertiesSerializer(cycle_properties) return Response(serializer.data, status=status.HTTP_200_OK) + + +class CycleProgressEndpoint(BaseAPIView): + + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST]) + def get(self, request, slug, project_id, cycle_id): + + aggregate_estimates = ( + Issue.issue_objects.filter( + estimate_point__estimate__type="points", + issue_cycle__cycle_id=cycle_id, + workspace__slug=slug, + project_id=project_id, + ) + .annotate( + value_as_float=Cast("estimate_point__value", FloatField()) + ) + .aggregate( + backlog_estimate_point=Sum( + Case( + When(state__group="backlog", then="value_as_float"), + default=Value(0), + output_field=FloatField(), + ) + ), + unstarted_estimate_point=Sum( + Case( + When(state__group="unstarted", then="value_as_float"), + default=Value(0), + output_field=FloatField(), + ) + ), + started_estimate_point=Sum( + Case( + When(state__group="started", then="value_as_float"), + default=Value(0), + output_field=FloatField(), + ) + ), + cancelled_estimate_point=Sum( + Case( + When(state__group="cancelled", then="value_as_float"), + default=Value(0), + output_field=FloatField(), + ) + ), + completed_estimate_points=Sum( + Case( + When(state__group="completed", then="value_as_float"), + default=Value(0), + output_field=FloatField(), + ) + ), + total_estimate_points=Sum( + "value_as_float", + default=Value(0), + output_field=FloatField(), + ), + ) + ) + + backlog_issues = Issue.issue_objects.filter( + issue_cycle__cycle_id=cycle_id, + workspace__slug=slug, + project_id=project_id, + state__group="backlog", + ).count() + + unstarted_issues = Issue.issue_objects.filter( + issue_cycle__cycle_id=cycle_id, + workspace__slug=slug, + project_id=project_id, + state__group="unstarted", + ).count() + + started_issues = Issue.issue_objects.filter( + issue_cycle__cycle_id=cycle_id, + workspace__slug=slug, + project_id=project_id, + state__group="started", + ).count() + + cancelled_issues = Issue.issue_objects.filter( + issue_cycle__cycle_id=cycle_id, + workspace__slug=slug, + project_id=project_id, + state__group="cancelled", + ).count() + + completed_issues = Issue.issue_objects.filter( + issue_cycle__cycle_id=cycle_id, + workspace__slug=slug, + project_id=project_id, + state__group="completed", + ).count() + + total_issues = Issue.issue_objects.filter( + issue_cycle__cycle_id=cycle_id, + workspace__slug=slug, + project_id=project_id, + ).count() + + return Response( + { + "backlog_estimate_point": aggregate_estimates[ + "backlog_estimate_point" + ] + or 0, + "unstarted_estimate_point": aggregate_estimates[ + "unstarted_estimate_point" + ] + or 0, + "started_estimate_point": aggregate_estimates[ + "started_estimate_point" + ] + or 0, + "cancelled_estimate_point": aggregate_estimates[ + "cancelled_estimate_point" + ] + or 0, + "completed_estimate_point": aggregate_estimates[ + "completed_estimate_points" + ] + or 0, + "total_estimate_point": aggregate_estimates[ + "total_estimate_points" + ], + "backlog_issues": backlog_issues, + "total_issues": total_issues, + "completed_issues": completed_issues, + "cancelled_issues": cancelled_issues, + "started_issues": started_issues, + "unstarted_issues": unstarted_issues, + }, + status=status.HTTP_200_OK, + ) + + +class CycleAnalyticsEndpoint(BaseAPIView): + + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST]) + def get(self, request, slug, project_id, cycle_id): + analytic_type = request.GET.get("type", "issues") + cycle = ( + Cycle.objects.filter( + workspace__slug=slug, + project_id=project_id, + id=cycle_id, + ) + .annotate( + total_issues=Count( + "issue_cycle__issue__id", + distinct=True, + filter=Q( + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), + ) + ) + .first() + ) + + if not cycle.start_date or not cycle.end_date: + return Response( + {"error": "Cycle has no start or end date"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + estimate_type = Project.objects.filter( + workspace__slug=slug, + pk=project_id, + estimate__isnull=False, + estimate__type="points", + ).exists() + + assignee_distribution = {} + label_distribution = {} + completion_chart = {} + + if analytic_type == "points" and estimate_type: + assignee_distribution = ( + Issue.issue_objects.filter( + issue_cycle__cycle_id=cycle_id, + workspace__slug=slug, + project_id=project_id, + ) + .annotate(display_name=F("assignees__display_name")) + .annotate(assignee_id=F("assignees__id")) + .annotate(avatar=F("assignees__avatar")) + .values("display_name", "assignee_id", "avatar") + .annotate( + total_estimates=Sum( + Cast("estimate_point__value", FloatField()) + ) + ) + .annotate( + completed_estimates=Sum( + Cast("estimate_point__value", FloatField()), + filter=Q( + completed_at__isnull=False, + archived_at__isnull=True, + is_draft=False, + ), + ) + ) + .annotate( + pending_estimates=Sum( + Cast("estimate_point__value", FloatField()), + filter=Q( + completed_at__isnull=True, + archived_at__isnull=True, + is_draft=False, + ), + ) + ) + .order_by("display_name") + ) + + label_distribution = ( + Issue.issue_objects.filter( + issue_cycle__cycle_id=cycle_id, + workspace__slug=slug, + project_id=project_id, + ) + .annotate(label_name=F("labels__name")) + .annotate(color=F("labels__color")) + .annotate(label_id=F("labels__id")) + .values("label_name", "color", "label_id") + .annotate( + total_estimates=Sum( + Cast("estimate_point__value", FloatField()) + ) + ) + .annotate( + completed_estimates=Sum( + Cast("estimate_point__value", FloatField()), + filter=Q( + completed_at__isnull=False, + archived_at__isnull=True, + is_draft=False, + ), + ) + ) + .annotate( + pending_estimates=Sum( + Cast("estimate_point__value", FloatField()), + filter=Q( + completed_at__isnull=True, + archived_at__isnull=True, + is_draft=False, + ), + ) + ) + .order_by("label_name") + ) + completion_chart = burndown_plot( + queryset=cycle, + slug=slug, + project_id=project_id, + plot_type="points", + cycle_id=cycle_id, + ) + + if analytic_type == "issues": + assignee_distribution = ( + Issue.issue_objects.filter( + issue_cycle__cycle_id=cycle_id, + project_id=project_id, + workspace__slug=slug, + ) + .annotate(display_name=F("assignees__display_name")) + .annotate(assignee_id=F("assignees__id")) + .annotate(avatar=F("assignees__avatar")) + .values("display_name", "assignee_id", "avatar") + .annotate( + total_issues=Count( + "assignee_id", + filter=Q(archived_at__isnull=True, is_draft=False), + ), + ) + .annotate( + completed_issues=Count( + "assignee_id", + filter=Q( + completed_at__isnull=False, + archived_at__isnull=True, + is_draft=False, + ), + ) + ) + .annotate( + pending_issues=Count( + "assignee_id", + filter=Q( + completed_at__isnull=True, + archived_at__isnull=True, + is_draft=False, + ), + ) + ) + .order_by("display_name") + ) + + label_distribution = ( + Issue.issue_objects.filter( + issue_cycle__cycle_id=cycle_id, + project_id=project_id, + workspace__slug=slug, + ) + .annotate(label_name=F("labels__name")) + .annotate(color=F("labels__color")) + .annotate(label_id=F("labels__id")) + .values("label_name", "color", "label_id") + .annotate( + total_issues=Count( + "label_id", + filter=Q(archived_at__isnull=True, is_draft=False), + ) + ) + .annotate( + completed_issues=Count( + "label_id", + filter=Q( + completed_at__isnull=False, + archived_at__isnull=True, + is_draft=False, + ), + ) + ) + .annotate( + pending_issues=Count( + "label_id", + filter=Q( + completed_at__isnull=True, + archived_at__isnull=True, + is_draft=False, + ), + ) + ) + .order_by("label_name") + ) + completion_chart = burndown_plot( + queryset=cycle, + slug=slug, + project_id=project_id, + cycle_id=cycle_id, + plot_type="issues", + ) + + return Response( + { + "assignee": assignee_distribution, + "label": label_distribution, + "completion_chart": completion_chart, + }, + status=status.HTTP_200_OK, + ) From cec4a41bf993d96904f29d0eb1ce5d4817951e57 Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Mon, 26 Aug 2024 16:55:35 +0530 Subject: [PATCH 2/9] fix: typo --- apiserver/plane/app/views/cycle/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/plane/app/views/cycle/base.py b/apiserver/plane/app/views/cycle/base.py index d8dde0d4ef8..5211fb402a2 100644 --- a/apiserver/plane/app/views/cycle/base.py +++ b/apiserver/plane/app/views/cycle/base.py @@ -1354,7 +1354,7 @@ def get(self, request, slug, project_id, cycle_id): return Response( { - "assignee": assignee_distribution, + "assignees": assignee_distribution, "label": label_distribution, "completion_chart": completion_chart, }, From 08ec56d473106af834ad76ae7df9198d9cd8215c Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Mon, 26 Aug 2024 18:59:49 +0530 Subject: [PATCH 3/9] chore: changed the label typo --- apiserver/plane/app/views/cycle/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/plane/app/views/cycle/base.py b/apiserver/plane/app/views/cycle/base.py index 5211fb402a2..ea71d608742 100644 --- a/apiserver/plane/app/views/cycle/base.py +++ b/apiserver/plane/app/views/cycle/base.py @@ -1355,7 +1355,7 @@ def get(self, request, slug, project_id, cycle_id): return Response( { "assignees": assignee_distribution, - "label": label_distribution, + "labels": label_distribution, "completion_chart": completion_chart, }, status=status.HTTP_200_OK, From 712eaf0db60e4ffe43a133fbf6d28cd4791e1fec Mon Sep 17 00:00:00 2001 From: gakshita Date: Mon, 26 Aug 2024 19:00:48 +0530 Subject: [PATCH 4/9] feat: intergrated optimized api --- apiserver/plane/app/views/cycle/base.py | 2 +- .../cycles/(detail)/[cycleId]/page.tsx | 19 ++-- .../cycles/active-cycle/cycle-stats.tsx | 23 ++--- .../cycles/active-cycle/productivity.tsx | 40 ++++---- .../cycles/active-cycle/progress.tsx | 34 ++++--- .../components/cycles/active-cycle/root.tsx | 67 ++++--------- .../cycles/active-cycle/use-cycles-details.ts | 93 +++++++++++++++++++ web/core/services/cycle.service.ts | 9 +- web/core/store/cycle.store.ts | 58 +++++++++++- 9 files changed, 230 insertions(+), 115 deletions(-) create mode 100644 web/core/components/cycles/active-cycle/use-cycles-details.ts diff --git a/apiserver/plane/app/views/cycle/base.py b/apiserver/plane/app/views/cycle/base.py index 5211fb402a2..ea71d608742 100644 --- a/apiserver/plane/app/views/cycle/base.py +++ b/apiserver/plane/app/views/cycle/base.py @@ -1355,7 +1355,7 @@ def get(self, request, slug, project_id, cycle_id): return Response( { "assignees": assignee_distribution, - "label": label_distribution, + "labels": label_distribution, "completion_chart": completion_chart, }, status=status.HTTP_200_OK, diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/[cycleId]/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/[cycleId]/page.tsx index e63cefa098d..6f31416c8cb 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/[cycleId]/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/[cycleId]/page.tsx @@ -2,11 +2,11 @@ import { observer } from "mobx-react"; import { useParams } from "next/navigation"; -import useSWR from "swr"; // components import { EmptyState } from "@/components/common"; import { PageHead } from "@/components/core"; import { CycleDetailsSidebar } from "@/components/cycles"; +import useCyclesDetails from "@/components/cycles/active-cycle/use-cycles-details"; import { CycleLayoutRoot } from "@/components/issues/issue-layouts"; // constants // import { EIssuesStoreType } from "@/constants/issue"; @@ -24,18 +24,17 @@ const CycleDetailPage = observer(() => { const router = useAppRouter(); const { workspaceSlug, projectId, cycleId } = useParams(); // store hooks - const { fetchCycleDetails, getCycleById } = useCycle(); + const { getCycleById, loader } = useCycle(); const { getProjectById } = useProject(); // const { issuesFilter } = useIssues(EIssuesStoreType.CYCLE); // hooks const { setValue, storedValue } = useLocalStorage("cycle_sidebar_collapsed", "false"); - // fetching cycle details - const { error } = useSWR( - workspaceSlug && projectId && cycleId ? `CYCLE_DETAILS_${cycleId.toString()}` : null, - workspaceSlug && projectId && cycleId - ? () => fetchCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleId.toString()) - : null - ); + + useCyclesDetails({ + workspaceSlug: workspaceSlug.toString(), + projectId: projectId.toString(), + cycleId: cycleId.toString(), + }); // derived values const isSidebarCollapsed = storedValue ? (storedValue === "true" ? true : false) : false; const cycle = cycleId ? getCycleById(cycleId.toString()) : undefined; @@ -52,7 +51,7 @@ const CycleDetailPage = observer(() => { return ( <> - {error ? ( + {!cycle && !loader ? ( void; + cycleIssueDetails: ActiveCycleIssueDetails; }; export const ActiveCycleStats: FC = observer((props) => { - const { workspaceSlug, projectId, cycle, cycleId, handleFiltersUpdate } = props; + const { workspaceSlug, projectId, cycle, cycleId, handleFiltersUpdate, cycleIssueDetails } = props; const { storedValue: tab, setValue: setTab } = useLocalStorage("activeCycleTab", "Assignees"); @@ -57,21 +58,12 @@ export const ActiveCycleStats: FC = observer((props) => { } }; const { - issues: { getActiveCycleById, fetchActiveCycleIssues, fetchNextActiveCycleIssues }, + issues: { fetchNextActiveCycleIssues }, } = useIssues(EIssuesStoreType.CYCLE); const { issue: { getIssueById }, setPeekIssue, } = useIssueDetail(); - - useSWR( - workspaceSlug && projectId && cycleId ? CYCLE_ISSUES_WITH_PARAMS(cycleId, { priority: "urgent,high" }) : null, - workspaceSlug && projectId && cycleId ? () => fetchActiveCycleIssues(workspaceSlug, projectId, 30, cycleId) : null, - { revalidateIfStale: false, revalidateOnFocus: false } - ); - - const cycleIssueDetails = cycleId ? getActiveCycleById(cycleId) : { nextPageResults: false }; - const loadMoreIssues = useCallback(() => { if (!cycleId) return; fetchNextActiveCycleIssues(workspaceSlug, projectId, cycleId); @@ -87,6 +79,7 @@ export const ActiveCycleStats: FC = observer((props) => { ); + return cycleId ? (
= observer((props) => { as="div" className="flex h-52 w-full flex-col gap-1 overflow-y-auto text-custom-text-200 vertical-scrollbar scrollbar-sm" > - {cycle ? ( + {cycle && !isEmpty(cycle.distribution) ? ( cycle?.distribution?.assignees && cycle.distribution.assignees.length > 0 ? ( cycle.distribution?.assignees?.map((assignee, index) => { if (assignee.assignee_id) @@ -306,7 +299,7 @@ export const ActiveCycleStats: FC = observer((props) => { as="div" className="flex h-52 w-full flex-col gap-1 overflow-y-auto text-custom-text-200 vertical-scrollbar scrollbar-sm" > - {cycle ? ( + {cycle && !isEmpty(cycle.distribution) ? ( cycle?.distribution?.labels && cycle.distribution.labels.length > 0 ? ( cycle.distribution.labels?.map((label, index) => ( = observer((props) => { const { workspaceSlug, projectId, cycle } = props; // hooks - const { getPlotTypeByCycleId, setPlotType, fetchCycleDetails } = useCycle(); + const { getPlotTypeByCycleId, setPlotType } = useCycle(); const { currentActiveEstimateId, areEstimateEnabledByProjectId, estimateById } = useProjectEstimates(); - // state - const [loader, setLoader] = useState(false); + // derived values const plotType: TCyclePlotType = (cycle && getPlotTypeByCycleId(cycle.id)) || "burndown"; const onChange = async (value: TCyclePlotType) => { + console.log(value, "value"); if (!workspaceSlug || !projectId || !cycle || !cycle.id) return; setPlotType(cycle.id, value); - try { - setLoader(true); - await fetchCycleDetails(workspaceSlug, projectId, cycle.id); - setLoader(false); - } catch (error) { - setLoader(false); - setPlotType(cycle.id, plotType); - } + // try { + // setLoader(true); + // await fetchCycleDetails(workspaceSlug, projectId, cycle.id); + // setLoader(false); + // } catch (error) { + // setLoader(false); + // setPlotType(cycle.id, plotType); + // } }; const isCurrentProjectEstimateEnabled = projectId && areEstimateEnabledByProjectId(projectId) ? true : false; @@ -55,7 +56,7 @@ export const ActiveCycleProductivity: FC = observe cycle && plotType === "points" ? cycle?.estimate_distribution : cycle?.distribution || undefined; const completionChartDistributionData = chartDistributionData?.completion_chart || undefined; - return cycle ? ( + return cycle && completionChartDistributionData && cycle.progress_snapshot ? (
@@ -75,7 +76,6 @@ export const ActiveCycleProductivity: FC = observe ))} - {loader && }
)}
@@ -95,10 +95,14 @@ export const ActiveCycleProductivity: FC = observe Current
- {plotType === "points" ? ( - {`Pending points - ${cycle.backlog_estimate_points + cycle.unstarted_estimate_points + cycle.started_estimate_points}`} + {isEmpty(cycle.progress_snapshot) ? ( + + + + ) : plotType === "points" ? ( + {`Pending points - ${cycle.progress_snapshot.backlog_estimate_points + cycle.progress_snapshot.unstarted_estimate_points + cycle.progress_snapshot.started_estimate_points}`} ) : ( - {`Pending issues - ${cycle.backlog_issues + cycle.unstarted_issues + cycle.started_issues}`} + {`Pending issues - ${cycle.progress_snapshot.backlog_issues + cycle.progress_snapshot.unstarted_issues + cycle.progress_snapshot.started_issues}`} )} diff --git a/web/core/components/cycles/active-cycle/progress.tsx b/web/core/components/cycles/active-cycle/progress.tsx index fc6e86561a0..58eec20c8fb 100644 --- a/web/core/components/cycles/active-cycle/progress.tsx +++ b/web/core/components/cycles/active-cycle/progress.tsx @@ -1,6 +1,7 @@ "use client"; import { FC } from "react"; +import isEmpty from "lodash/isEmpty"; import { observer } from "mobx-react"; // types import { ICycle, IIssueFilterOptions } from "@plane/types"; @@ -16,47 +17,50 @@ import { useProjectState } from "@/hooks/store"; export type ActiveCycleProgressProps = { cycle: ICycle | null; + workspaceSlug: string; + projectId: string; handleFiltersUpdate: (key: keyof IIssueFilterOptions, value: string[], redirect?: boolean) => void; }; export const ActiveCycleProgress: FC = observer((props) => { - const { cycle, handleFiltersUpdate } = props; + const { handleFiltersUpdate, cycle } = props; // store hooks const { groupedProjectStates } = useProjectState(); + // derived values const progressIndicatorData = PROGRESS_STATE_GROUPS_DETAILS.map((group, index) => ({ id: index, name: group.title, value: cycle && cycle.total_issues > 0 ? (cycle[group.key as keyof ICycle] as number) : 0, color: group.color, })); - + const progressData = cycle?.progress_snapshot; const groupedIssues: any = cycle ? { - completed: cycle.completed_issues, - started: cycle.started_issues, - unstarted: cycle.unstarted_issues, - backlog: cycle.backlog_issues, + completed: progressData?.completed_issues, + started: progressData?.started_issues, + unstarted: progressData?.unstarted_issues, + backlog: progressData?.backlog_issues, } : {}; - return cycle ? ( + return !isEmpty(progressData) ? (

Progress

- {cycle.total_issues > 0 && ( + {progressData.total_issues > 0 && ( - {`${cycle.completed_issues + cycle.cancelled_issues}/${cycle.total_issues - cycle.cancelled_issues} ${ - cycle.completed_issues + cycle.cancelled_issues > 1 ? "Issues" : "Issue" + {`${progressData.completed_issues + progressData.cancelled_issues}/${progressData.total_issues - progressData.cancelled_issues} ${ + progressData.completed_issues + progressData.cancelled_issues > 1 ? "Issues" : "Issue" } closed`} )}
- {cycle.total_issues > 0 && } + {progressData.total_issues > 0 && }
- {cycle.total_issues > 0 ? ( + {progressData.total_issues > 0 ? (
{Object.keys(groupedIssues).map((group, index) => ( <> @@ -88,11 +92,11 @@ export const ActiveCycleProgress: FC = observer((props )} ))} - {cycle.cancelled_issues > 0 && ( + {progressData.cancelled_issues > 0 && ( - {`${cycle.cancelled_issues} cancelled ${ - cycle.cancelled_issues > 1 ? "issues are" : "issue is" + {`${progressData.cancelled_issues} cancelled ${ + progressData.cancelled_issues > 1 ? "issues are" : "issue is" } excluded from this report.`}{" "} diff --git a/web/core/components/cycles/active-cycle/root.tsx b/web/core/components/cycles/active-cycle/root.tsx index 610de473c0f..b56a756aaa6 100644 --- a/web/core/components/cycles/active-cycle/root.tsx +++ b/web/core/components/cycles/active-cycle/root.tsx @@ -1,13 +1,7 @@ "use client"; -import { useCallback } from "react"; -import isEqual from "lodash/isEqual"; import { observer } from "mobx-react"; -import { useRouter } from "next/navigation"; -import useSWR from "swr"; import { Disclosure } from "@headlessui/react"; -// types -import { IIssueFilterOptions } from "@plane/types"; // ui import { Loader } from "@plane/ui"; // components @@ -21,9 +15,9 @@ import { import { EmptyState } from "@/components/empty-state"; // constants import { EmptyStateType } from "@/constants/empty-state"; -import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue"; -// hooks -import { useCycle, useIssues } from "@/hooks/store"; +import { ActiveCycleIssueDetails } from "@/store/issue/cycle"; +import useCyclesDetails from "./use-cycles-details"; +import { useCycle } from "@/hooks/store"; interface IActiveCycleDetails { workspaceSlug: string; @@ -31,51 +25,16 @@ interface IActiveCycleDetails { } export const ActiveCycleRoot: React.FC = observer((props) => { - // props const { workspaceSlug, projectId } = props; - // router - const router = useRouter(); - // store hooks + const { currentProjectActiveCycle, currentProjectActiveCycleId } = useCycle(); const { - issuesFilter: { issueFilters, updateFilters }, - } = useIssues(EIssuesStoreType.CYCLE); - const { currentProjectActiveCycle, fetchActiveCycle, currentProjectActiveCycleId, getActiveCycleById } = useCycle(); - // derived values - const activeCycle = currentProjectActiveCycleId ? getActiveCycleById(currentProjectActiveCycleId) : null; - // fetch active cycle details - const { isLoading } = useSWR( - workspaceSlug && projectId ? `PROJECT_ACTIVE_CYCLE_${projectId}` : null, - workspaceSlug && projectId ? () => fetchActiveCycle(workspaceSlug, projectId) : null - ); - - const handleFiltersUpdate = useCallback( - (key: keyof IIssueFilterOptions, value: string[], redirect?: boolean) => { - if (!workspaceSlug || !projectId || !currentProjectActiveCycleId) return; - - const newFilters: IIssueFilterOptions = {}; - Object.keys(issueFilters?.filters ?? {}).forEach((key) => { - newFilters[key as keyof IIssueFilterOptions] = []; - }); - - let newValues: string[] = []; - - if (isEqual(newValues, value)) newValues = []; - else newValues = value; - - updateFilters( - workspaceSlug.toString(), - projectId.toString(), - EIssueFilterType.FILTERS, - { ...newFilters, [key]: newValues }, - currentProjectActiveCycleId.toString() - ); - if (redirect) router.push(`/${workspaceSlug}/projects/${projectId}/cycles/${currentProjectActiveCycleId}`); - }, - [workspaceSlug, projectId, currentProjectActiveCycleId, issueFilters, updateFilters, router] - ); + handleFiltersUpdate, + cycle: activeCycle, + cycleIssueDetails, + } = useCyclesDetails({ workspaceSlug, projectId, cycleId: currentProjectActiveCycleId }); // show loader if active cycle is loading - if (!currentProjectActiveCycle && isLoading) + if (!currentProjectActiveCycle) return ( @@ -106,7 +65,12 @@ export const ActiveCycleRoot: React.FC = observer((props) = )}
- + = observer((props) = cycle={activeCycle} cycleId={currentProjectActiveCycleId} handleFiltersUpdate={handleFiltersUpdate} + cycleIssueDetails={cycleIssueDetails as ActiveCycleIssueDetails} />
diff --git a/web/core/components/cycles/active-cycle/use-cycles-details.ts b/web/core/components/cycles/active-cycle/use-cycles-details.ts new file mode 100644 index 00000000000..9e450fa0088 --- /dev/null +++ b/web/core/components/cycles/active-cycle/use-cycles-details.ts @@ -0,0 +1,93 @@ +import { useCallback } from "react"; +import isEqual from "lodash/isEqual"; +import { useRouter } from "next/navigation"; +import useSWR from "swr"; +import { IIssueFilterOptions } from "@plane/types"; +import { CYCLE_ISSUES_WITH_PARAMS } from "@/constants/fetch-keys"; +import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue"; +import { useCycle, useIssues } from "@/hooks/store"; + +interface IActiveCycleDetails { + workspaceSlug: string; + projectId: string; + cycleId: string | null; +} + +const useCyclesDetails = (props: IActiveCycleDetails) => { + // props + const { workspaceSlug, projectId, cycleId } = props; + // router + const router = useRouter(); + // store hooks + const { + issuesFilter: { issueFilters, updateFilters }, + issues: { getActiveCycleById: getActiveCycleByIdFromIssue, fetchActiveCycleIssues }, + } = useIssues(EIssuesStoreType.CYCLE); + + const { fetchActiveCycleProgress, getCycleById, fetchActiveCycleAnalytics } = useCycle(); + // derived values + const cycle = cycleId ? getCycleById(cycleId) : null; + + // fetch cycle details + useSWR( + workspaceSlug && projectId && cycle ? `PROJECT_ACTIVE_CYCLE_${projectId}_PROGRESS` : null, + workspaceSlug && projectId && cycle ? () => fetchActiveCycleProgress(workspaceSlug, projectId, cycle.id) : null + ); + useSWR( + workspaceSlug && projectId && cycle && !cycle?.distribution ? `PROJECT_ACTIVE_CYCLE_${projectId}_DURATION` : null, + workspaceSlug && projectId && cycle && !cycle?.distribution + ? () => fetchActiveCycleAnalytics(workspaceSlug, projectId, cycle.id, "issues") + : null + ); + useSWR( + workspaceSlug && projectId && cycle && !cycle?.estimate_distribution + ? `PROJECT_ACTIVE_CYCLE_${projectId}_ESTIMATE_DURATION` + : null, + workspaceSlug && projectId && cycle && !cycle?.estimate_distribution + ? () => fetchActiveCycleAnalytics(workspaceSlug, projectId, cycle.id, "points") + : null + ); + useSWR( + workspaceSlug && projectId && cycle?.id ? CYCLE_ISSUES_WITH_PARAMS(cycle?.id, { priority: "urgent,high" }) : null, + workspaceSlug && projectId && cycle?.id + ? () => fetchActiveCycleIssues(workspaceSlug, projectId, 30, cycle?.id) + : null, + { revalidateIfStale: false, revalidateOnFocus: false } + ); + + const cycleIssueDetails = cycle?.id ? getActiveCycleByIdFromIssue(cycle?.id) : { nextPageResults: false }; + + const handleFiltersUpdate = useCallback( + (key: keyof IIssueFilterOptions, value: string[], redirect?: boolean) => { + if (!workspaceSlug || !projectId || !cycleId) return; + + const newFilters: IIssueFilterOptions = {}; + Object.keys(issueFilters?.filters ?? {}).forEach((key) => { + newFilters[key as keyof IIssueFilterOptions] = []; + }); + + let newValues: string[] = []; + + if (isEqual(newValues, value)) newValues = []; + else newValues = value; + + updateFilters( + workspaceSlug.toString(), + projectId.toString(), + EIssueFilterType.FILTERS, + { ...newFilters, [key]: newValues }, + cycleId.toString() + ); + if (redirect) router.push(`/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}`); + }, + [workspaceSlug, projectId, cycleId, issueFilters, updateFilters, router] + ); + return { + cycle, + cycleId, + router, + handleFiltersUpdate, + cycleIssueDetails, + }; +}; +export default useCyclesDetails; diff --git a/web/core/services/cycle.service.ts b/web/core/services/cycle.service.ts index 5d4b9d7b8b3..be8a25281b2 100644 --- a/web/core/services/cycle.service.ts +++ b/web/core/services/cycle.service.ts @@ -4,8 +4,9 @@ import type { ICycle, TIssuesResponse, IWorkspaceActiveCyclesResponse, - IWorkspaceProgressResponse, - IWorkspaceAnalyticsResponse, + TCycleDistribution, + TProgressSnapshot, + TCycleEstimateDistribution, } from "@plane/types"; import { API_BASE_URL } from "@/helpers/common.helper"; import { APIService } from "@/services/api.service"; @@ -20,7 +21,7 @@ export class CycleService extends APIService { projectId: string, cycleId: string, analytic_type: string = "points" - ): Promise { + ): Promise { return this.get( `/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/analytics?type=${analytic_type}` ) @@ -34,7 +35,7 @@ export class CycleService extends APIService { workspaceSlug: string, projectId: string, cycleId: string - ): Promise { + ): Promise { return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/progress/`) .then((res) => res?.data) .catch((err) => { diff --git a/web/core/store/cycle.store.ts b/web/core/store/cycle.store.ts index b9a1f147648..a8172354f6b 100644 --- a/web/core/store/cycle.store.ts +++ b/web/core/store/cycle.store.ts @@ -4,7 +4,14 @@ import sortBy from "lodash/sortBy"; import { action, computed, observable, makeObservable, runInAction } from "mobx"; import { computedFn } from "mobx-utils"; // types -import { ICycle, CycleDateCheckData, TCyclePlotType } from "@plane/types"; +import { + ICycle, + CycleDateCheckData, + TCyclePlotType, + TProgressSnapshot, + TCycleEstimateDistribution, + TCycleDistribution, +} from "@plane/types"; // helpers import { orderCycles, shouldFilterCycle } from "@/helpers/cycle.helper"; import { getDate } from "@/helpers/date-time.helper"; @@ -55,6 +62,13 @@ export interface ICycleStore { fetchArchivedCycles: (workspaceSlug: string, projectId: string) => Promise; fetchArchivedCycleDetails: (workspaceSlug: string, projectId: string, cycleId: string) => Promise; fetchCycleDetails: (workspaceSlug: string, projectId: string, cycleId: string) => Promise; + fetchActiveCycleProgress: (workspaceSlug: string, projectId: string, cycleId: string) => Promise; + fetchActiveCycleAnalytics: ( + workspaceSlug: string, + projectId: string, + cycleId: string, + analytic_type: string + ) => Promise; // crud createCycle: (workspaceSlug: string, projectId: string, data: Partial) => Promise; updateCycleDetails: ( @@ -113,6 +127,8 @@ export class CycleStore implements ICycleStore { fetchActiveCycle: action, fetchArchivedCycles: action, fetchArchivedCycleDetails: action, + fetchActiveCycleProgress: action, + fetchActiveCycleAnalytics: action, fetchCycleDetails: action, createCycle: action, updateCycleDetails: action, @@ -214,11 +230,13 @@ export class CycleStore implements ICycleStore { get currentProjectActiveCycleId() { const projectId = this.rootStore.router.projectId; if (!projectId) return null; + console.log("this.cycleMap", { ...this.cycleMap["eca9a3b1-650a-433f-9edf-712b01af49d5"] }); const activeCycle = Object.keys(this.cycleMap ?? {}).find( (cycleId) => this.cycleMap?.[cycleId]?.project_id === projectId && this.cycleMap?.[cycleId]?.status?.toLowerCase() === "current" ); + console.log("activeCycle", activeCycle); return activeCycle || null; } @@ -403,6 +421,7 @@ export class CycleStore implements ICycleStore { runInAction(() => { response.forEach((cycle) => { set(this.cycleMap, [cycle.id], cycle); + cycle.status?.toLowerCase() === "current" && set(this.activeCycleIdMap, [cycle.id], true); }); set(this.fetchedMap, projectId, true); this.loader = false; @@ -457,6 +476,43 @@ export class CycleStore implements ICycleStore { return response; }); + /** + * @description fetches active cycle progress + * @param workspaceSlug + * @param projectId + * @param cycleId + * @returns + */ + fetchActiveCycleProgress = async (workspaceSlug: string, projectId: string, cycleId: string) => + await this.cycleService.workspaceActiveCyclesProgress(workspaceSlug, projectId, cycleId).then((cycle) => { + runInAction(() => { + set(this.cycleMap, [cycleId, "progress_snapshot"], cycle); + }); + return cycle; + }); + + /** + * @description fetches active cycle analytics + * @param workspaceSlug + * @param projectId + * @param cycleId + * @returns + */ + fetchActiveCycleAnalytics = async ( + workspaceSlug: string, + projectId: string, + cycleId: string, + analytic_type: string + ) => + await this.cycleService + .workspaceActiveCyclesAnalytics(workspaceSlug, projectId, cycleId, analytic_type) + .then((cycle) => { + runInAction(() => { + set(this.cycleMap, [cycleId, analytic_type === "points" ? "estimate_distribution" : "distribution"], cycle); + }); + return cycle; + }); + /** * @description fetches cycle details * @param workspaceSlug From 251b21e32861dbaf58e4c56126ba5a43405f66f1 Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Mon, 26 Aug 2024 19:33:56 +0530 Subject: [PATCH 5/9] chore: added every key as plural --- apiserver/plane/app/views/cycle/base.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apiserver/plane/app/views/cycle/base.py b/apiserver/plane/app/views/cycle/base.py index ea71d608742..ecc83630f21 100644 --- a/apiserver/plane/app/views/cycle/base.py +++ b/apiserver/plane/app/views/cycle/base.py @@ -1107,27 +1107,27 @@ def get(self, request, slug, project_id, cycle_id): return Response( { - "backlog_estimate_point": aggregate_estimates[ + "backlog_estimate_points": aggregate_estimates[ "backlog_estimate_point" ] or 0, - "unstarted_estimate_point": aggregate_estimates[ + "unstarted_estimate_points": aggregate_estimates[ "unstarted_estimate_point" ] or 0, - "started_estimate_point": aggregate_estimates[ + "started_estimate_points": aggregate_estimates[ "started_estimate_point" ] or 0, - "cancelled_estimate_point": aggregate_estimates[ + "cancelled_estimate_points": aggregate_estimates[ "cancelled_estimate_point" ] or 0, - "completed_estimate_point": aggregate_estimates[ + "completed_estimate_points": aggregate_estimates[ "completed_estimate_points" ] or 0, - "total_estimate_point": aggregate_estimates[ + "total_estimate_points": aggregate_estimates[ "total_estimate_points" ], "backlog_issues": backlog_issues, From 8c7c4ca6bcb1d61bd5c35edd1530ec94cab2488e Mon Sep 17 00:00:00 2001 From: gakshita Date: Mon, 26 Aug 2024 19:37:10 +0530 Subject: [PATCH 6/9] fix: productivity dropdown --- .../cycles/active-cycle/productivity.tsx | 20 +++----------- .../cycles/active-cycle/progress.tsx | 27 +++++++++---------- web/core/store/cycle.store.ts | 8 +++--- 3 files changed, 20 insertions(+), 35 deletions(-) diff --git a/web/core/components/cycles/active-cycle/productivity.tsx b/web/core/components/cycles/active-cycle/productivity.tsx index 1098de96a7c..0bc7045ca9d 100644 --- a/web/core/components/cycles/active-cycle/productivity.tsx +++ b/web/core/components/cycles/active-cycle/productivity.tsx @@ -1,5 +1,4 @@ import { FC, Fragment } from "react"; -import isEmpty from "lodash/isEmpty"; import { observer } from "mobx-react"; import Link from "next/link"; import { ICycle, TCyclePlotType } from "@plane/types"; @@ -34,17 +33,8 @@ export const ActiveCycleProductivity: FC = observe const plotType: TCyclePlotType = (cycle && getPlotTypeByCycleId(cycle.id)) || "burndown"; const onChange = async (value: TCyclePlotType) => { - console.log(value, "value"); if (!workspaceSlug || !projectId || !cycle || !cycle.id) return; setPlotType(cycle.id, value); - // try { - // setLoader(true); - // await fetchCycleDetails(workspaceSlug, projectId, cycle.id); - // setLoader(false); - // } catch (error) { - // setLoader(false); - // setPlotType(cycle.id, plotType); - // } }; const isCurrentProjectEstimateEnabled = projectId && areEstimateEnabledByProjectId(projectId) ? true : false; @@ -95,14 +85,10 @@ export const ActiveCycleProductivity: FC = observe Current
- {isEmpty(cycle.progress_snapshot) ? ( - - - - ) : plotType === "points" ? ( - {`Pending points - ${cycle.progress_snapshot.backlog_estimate_points + cycle.progress_snapshot.unstarted_estimate_points + cycle.progress_snapshot.started_estimate_points}`} + {plotType === "points" ? ( + {`Pending points - ${cycle.backlog_estimate_points + cycle.unstarted_estimate_points + cycle.started_estimate_points}`} ) : ( - {`Pending issues - ${cycle.progress_snapshot.backlog_issues + cycle.progress_snapshot.unstarted_issues + cycle.progress_snapshot.started_issues}`} + {`Pending issues - ${cycle.backlog_issues + cycle.unstarted_issues + cycle.started_issues}`} )} diff --git a/web/core/components/cycles/active-cycle/progress.tsx b/web/core/components/cycles/active-cycle/progress.tsx index 58eec20c8fb..3951e1bcaf4 100644 --- a/web/core/components/cycles/active-cycle/progress.tsx +++ b/web/core/components/cycles/active-cycle/progress.tsx @@ -34,33 +34,32 @@ export const ActiveCycleProgress: FC = observer((props value: cycle && cycle.total_issues > 0 ? (cycle[group.key as keyof ICycle] as number) : 0, color: group.color, })); - const progressData = cycle?.progress_snapshot; const groupedIssues: any = cycle ? { - completed: progressData?.completed_issues, - started: progressData?.started_issues, - unstarted: progressData?.unstarted_issues, - backlog: progressData?.backlog_issues, + completed: cycle?.completed_issues, + started: cycle?.started_issues, + unstarted: cycle?.unstarted_issues, + backlog: cycle?.backlog_issues, } : {}; - return !isEmpty(progressData) ? ( + return !isEmpty(cycle) ? (

Progress

- {progressData.total_issues > 0 && ( + {cycle.total_issues > 0 && ( - {`${progressData.completed_issues + progressData.cancelled_issues}/${progressData.total_issues - progressData.cancelled_issues} ${ - progressData.completed_issues + progressData.cancelled_issues > 1 ? "Issues" : "Issue" + {`${cycle.completed_issues + cycle.cancelled_issues}/${cycle.total_issues - cycle.cancelled_issues} ${ + cycle.completed_issues + cycle.cancelled_issues > 1 ? "Issues" : "Issue" } closed`} )}
- {progressData.total_issues > 0 && } + {cycle.total_issues > 0 && }
- {progressData.total_issues > 0 ? ( + {cycle.total_issues > 0 ? (
{Object.keys(groupedIssues).map((group, index) => ( <> @@ -92,11 +91,11 @@ export const ActiveCycleProgress: FC = observer((props )} ))} - {progressData.cancelled_issues > 0 && ( + {cycle.cancelled_issues > 0 && ( - {`${progressData.cancelled_issues} cancelled ${ - progressData.cancelled_issues > 1 ? "issues are" : "issue is" + {`${cycle.cancelled_issues} cancelled ${ + cycle.cancelled_issues > 1 ? "issues are" : "issue is" } excluded from this report.`}{" "} diff --git a/web/core/store/cycle.store.ts b/web/core/store/cycle.store.ts index a8172354f6b..564434e6ec0 100644 --- a/web/core/store/cycle.store.ts +++ b/web/core/store/cycle.store.ts @@ -107,7 +107,7 @@ export class CycleStore implements ICycleStore { // observables loader: observable.ref, cycleMap: observable, - plotType: observable.ref, + plotType: observable, activeCycleIdMap: observable, fetchedMap: observable, // computed @@ -484,11 +484,11 @@ export class CycleStore implements ICycleStore { * @returns */ fetchActiveCycleProgress = async (workspaceSlug: string, projectId: string, cycleId: string) => - await this.cycleService.workspaceActiveCyclesProgress(workspaceSlug, projectId, cycleId).then((cycle) => { + await this.cycleService.workspaceActiveCyclesProgress(workspaceSlug, projectId, cycleId).then((progress) => { runInAction(() => { - set(this.cycleMap, [cycleId, "progress_snapshot"], cycle); + set(this.cycleMap, [cycleId], { ...this.cycleMap[cycleId], ...progress }); }); - return cycle; + return progress; }); /** From 5777ee883048a99ec8c04fe44c07c60d4403bdd6 Mon Sep 17 00:00:00 2001 From: gakshita Date: Mon, 26 Aug 2024 19:49:23 +0530 Subject: [PATCH 7/9] fix: removed logging --- web/core/store/cycle.store.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/web/core/store/cycle.store.ts b/web/core/store/cycle.store.ts index 564434e6ec0..f1b88885377 100644 --- a/web/core/store/cycle.store.ts +++ b/web/core/store/cycle.store.ts @@ -230,13 +230,11 @@ export class CycleStore implements ICycleStore { get currentProjectActiveCycleId() { const projectId = this.rootStore.router.projectId; if (!projectId) return null; - console.log("this.cycleMap", { ...this.cycleMap["eca9a3b1-650a-433f-9edf-712b01af49d5"] }); const activeCycle = Object.keys(this.cycleMap ?? {}).find( (cycleId) => this.cycleMap?.[cycleId]?.project_id === projectId && this.cycleMap?.[cycleId]?.status?.toLowerCase() === "current" ); - console.log("activeCycle", activeCycle); return activeCycle || null; } From 94c1262f0504661658af9df2d6c57684b3dec223 Mon Sep 17 00:00:00 2001 From: gakshita Date: Tue, 27 Aug 2024 12:44:48 +0530 Subject: [PATCH 8/9] fix: handled loading --- web/core/components/cycles/active-cycle/root.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/core/components/cycles/active-cycle/root.tsx b/web/core/components/cycles/active-cycle/root.tsx index b56a756aaa6..e9ee656832d 100644 --- a/web/core/components/cycles/active-cycle/root.tsx +++ b/web/core/components/cycles/active-cycle/root.tsx @@ -26,7 +26,7 @@ interface IActiveCycleDetails { export const ActiveCycleRoot: React.FC = observer((props) => { const { workspaceSlug, projectId } = props; - const { currentProjectActiveCycle, currentProjectActiveCycleId } = useCycle(); + const { currentProjectActiveCycle, currentProjectActiveCycleId, loader } = useCycle(); const { handleFiltersUpdate, cycle: activeCycle, @@ -34,7 +34,7 @@ export const ActiveCycleRoot: React.FC = observer((props) = } = useCyclesDetails({ workspaceSlug, projectId, cycleId: currentProjectActiveCycleId }); // show loader if active cycle is loading - if (!currentProjectActiveCycle) + if (!currentProjectActiveCycle && loader) return ( From 20a0e3f0a4db5306ef003ddd57712448dfaf0fd4 Mon Sep 17 00:00:00 2001 From: gakshita Date: Tue, 27 Aug 2024 14:35:27 +0530 Subject: [PATCH 9/9] fix: loaders --- .../cycles/active-cycle/productivity.tsx | 2 +- .../components/cycles/active-cycle/progress.tsx | 3 +-- web/core/components/cycles/active-cycle/root.tsx | 14 ++------------ .../cycles/active-cycle/use-cycles-details.ts | 3 ++- .../cycles/analytics-sidebar/issue-progress.tsx | 8 ++++++-- 5 files changed, 12 insertions(+), 18 deletions(-) diff --git a/web/core/components/cycles/active-cycle/productivity.tsx b/web/core/components/cycles/active-cycle/productivity.tsx index 0bc7045ca9d..31a865eae38 100644 --- a/web/core/components/cycles/active-cycle/productivity.tsx +++ b/web/core/components/cycles/active-cycle/productivity.tsx @@ -46,7 +46,7 @@ export const ActiveCycleProductivity: FC = observe cycle && plotType === "points" ? cycle?.estimate_distribution : cycle?.distribution || undefined; const completionChartDistributionData = chartDistributionData?.completion_chart || undefined; - return cycle && completionChartDistributionData && cycle.progress_snapshot ? ( + return cycle && completionChartDistributionData ? (
diff --git a/web/core/components/cycles/active-cycle/progress.tsx b/web/core/components/cycles/active-cycle/progress.tsx index 3951e1bcaf4..f75c51526c4 100644 --- a/web/core/components/cycles/active-cycle/progress.tsx +++ b/web/core/components/cycles/active-cycle/progress.tsx @@ -1,7 +1,6 @@ "use client"; import { FC } from "react"; -import isEmpty from "lodash/isEmpty"; import { observer } from "mobx-react"; // types import { ICycle, IIssueFilterOptions } from "@plane/types"; @@ -43,7 +42,7 @@ export const ActiveCycleProgress: FC = observer((props } : {}; - return !isEmpty(cycle) ? ( + return cycle && cycle.hasOwnProperty("started_issues") ? (
diff --git a/web/core/components/cycles/active-cycle/root.tsx b/web/core/components/cycles/active-cycle/root.tsx index e9ee656832d..00be0e34fa2 100644 --- a/web/core/components/cycles/active-cycle/root.tsx +++ b/web/core/components/cycles/active-cycle/root.tsx @@ -2,8 +2,6 @@ import { observer } from "mobx-react"; import { Disclosure } from "@headlessui/react"; -// ui -import { Loader } from "@plane/ui"; // components import { ActiveCycleProductivity, @@ -15,9 +13,9 @@ import { import { EmptyState } from "@/components/empty-state"; // constants import { EmptyStateType } from "@/constants/empty-state"; +import { useCycle } from "@/hooks/store"; import { ActiveCycleIssueDetails } from "@/store/issue/cycle"; import useCyclesDetails from "./use-cycles-details"; -import { useCycle } from "@/hooks/store"; interface IActiveCycleDetails { workspaceSlug: string; @@ -26,21 +24,13 @@ interface IActiveCycleDetails { export const ActiveCycleRoot: React.FC = observer((props) => { const { workspaceSlug, projectId } = props; - const { currentProjectActiveCycle, currentProjectActiveCycleId, loader } = useCycle(); + const { currentProjectActiveCycle, currentProjectActiveCycleId } = useCycle(); const { handleFiltersUpdate, cycle: activeCycle, cycleIssueDetails, } = useCyclesDetails({ workspaceSlug, projectId, cycleId: currentProjectActiveCycleId }); - // show loader if active cycle is loading - if (!currentProjectActiveCycle && loader) - return ( - - - - ); - return ( <> diff --git a/web/core/components/cycles/active-cycle/use-cycles-details.ts b/web/core/components/cycles/active-cycle/use-cycles-details.ts index 9e450fa0088..fa0ed01eac2 100644 --- a/web/core/components/cycles/active-cycle/use-cycles-details.ts +++ b/web/core/components/cycles/active-cycle/use-cycles-details.ts @@ -31,7 +31,8 @@ const useCyclesDetails = (props: IActiveCycleDetails) => { // fetch cycle details useSWR( workspaceSlug && projectId && cycle ? `PROJECT_ACTIVE_CYCLE_${projectId}_PROGRESS` : null, - workspaceSlug && projectId && cycle ? () => fetchActiveCycleProgress(workspaceSlug, projectId, cycle.id) : null + workspaceSlug && projectId && cycle ? () => fetchActiveCycleProgress(workspaceSlug, projectId, cycle.id) : null, + { revalidateIfStale: false, revalidateOnFocus: false } ); useSWR( workspaceSlug && projectId && cycle && !cycle?.distribution ? `PROJECT_ACTIVE_CYCLE_${projectId}_DURATION` : null, diff --git a/web/core/components/cycles/analytics-sidebar/issue-progress.tsx b/web/core/components/cycles/analytics-sidebar/issue-progress.tsx index a78974c6195..ea44cce8156 100644 --- a/web/core/components/cycles/analytics-sidebar/issue-progress.tsx +++ b/web/core/components/cycles/analytics-sidebar/issue-progress.tsx @@ -8,7 +8,7 @@ import { useSearchParams } from "next/navigation"; import { AlertCircle, ChevronUp, ChevronDown } from "lucide-react"; import { Disclosure, Transition } from "@headlessui/react"; import { ICycle, IIssueFilterOptions, TCyclePlotType, TProgressSnapshot } from "@plane/types"; -import { CustomSelect, Spinner } from "@plane/ui"; +import { CustomSelect, Loader, Spinner } from "@plane/ui"; // components import ProgressChart from "@/components/core/sidebar/progress-chart"; import { CycleProgressStats } from "@/components/cycles"; @@ -231,7 +231,7 @@ export const CycleAnalyticsProgress: FC = observer((pro Current
- {cycleStartDate && cycleEndDate && completionChartDistributionData && ( + {cycleStartDate && cycleEndDate && completionChartDistributionData ? ( {plotType === "points" ? ( = observer((pro /> )} + ) : ( + + + )}