[WEB-522] chore: estimate point active cycles pending count and fixed burndown graph total issues (#407)

* chore: workspace active cycle estimate points

* chore: added serializer fields

* chore: updated active cycles issues count and graph payload

* chore: resolved modal imports build error

* chore: updated yarn lock

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
This commit is contained in:
guru_sainath
2024-06-14 20:25:42 +05:30
committed by GitHub
parent 1db0f93d60
commit ce6e82d2bf
4 changed files with 292 additions and 21 deletions

View File

@@ -19,6 +19,12 @@ class ActiveCycleSerializer(BaseSerializer):
started_issues = serializers.IntegerField(read_only=True)
unstarted_issues = serializers.IntegerField(read_only=True)
backlog_issues = serializers.IntegerField(read_only=True)
backlog_estimate_points = serializers.IntegerField(read_only=True)
unstarted_estimate_points = serializers.IntegerField(read_only=True)
started_estimate_points = serializers.IntegerField(read_only=True)
cancelled_estimate_points = serializers.IntegerField(read_only=True)
total_estimate_points = serializers.IntegerField(read_only=True)
# active | draft | upcoming | completed
status = serializers.CharField(read_only=True)
@@ -54,5 +60,11 @@ class ActiveCycleSerializer(BaseSerializer):
"backlog_issues",
"status",
"project_detail",
"backlog_estimate_points",
"unstarted_estimate_points",
"started_estimate_points",
"cancelled_estimate_points",
"total_estimate_points",
]
read_only_fields = fields

View File

@@ -9,8 +9,12 @@ from django.db.models import (
Prefetch,
Q,
Value,
Sum,
When,
Subquery,
IntegerField,
)
from django.db.models.functions import Cast, Coalesce
from django.utils import timezone
@@ -21,13 +25,7 @@ from plane.app.permissions import (
from plane.app.serializers import (
ActiveCycleSerializer,
)
from plane.db.models import (
Cycle,
CycleFavorite,
Issue,
Label,
User,
)
from plane.db.models import Cycle, CycleFavorite, Issue, Label, User, Project
from plane.utils.analytics_plot import burndown_plot
from plane.app.views.base import BaseAPIView
@@ -39,6 +37,106 @@ class ActiveCycleEndpoint(BaseAPIView):
def get_results_controller(self, results, plot_type, active_cycles=None):
for cycle in results:
estimate_type = Project.objects.filter(
pk=cycle["project_id"],
workspace__slug=self.kwargs.get("slug"),
estimate__isnull=False,
estimate__type="points",
).exists()
cycle["estimate_distribution"] = {}
if estimate_type:
assignee_distribution = (
Issue.objects.filter(
issue_cycle__cycle_id=cycle["id"],
project_id=cycle["project_id"],
workspace__slug=self.kwargs.get("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_estimates=Sum(
Cast("estimate_point__value", IntegerField())
)
)
.annotate(
completed_estimates=Sum(
Cast("estimate_point__value", IntegerField()),
filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
is_draft=False,
),
)
)
.annotate(
pending_estimates=Sum(
Cast("estimate_point__value", IntegerField()),
filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
is_draft=False,
),
)
)
.order_by("display_name")
)
label_distribution = (
Issue.objects.filter(
issue_cycle__cycle_id=cycle["id"],
project_id=cycle["project_id"],
workspace__slug=self.kwargs.get("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_estimates=Sum(
Cast("estimate_point__value", IntegerField())
)
)
.annotate(
completed_estimates=Sum(
Cast("estimate_point__value", IntegerField()),
filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
is_draft=False,
),
)
)
.annotate(
pending_estimates=Sum(
Cast("estimate_point__value", IntegerField()),
filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
is_draft=False,
),
)
)
.order_by("label_name")
)
cycle["estimate_distribution"] = {
"assignees": assignee_distribution,
"labels": label_distribution,
"completion_chart": {},
}
if cycle["start_date"] and cycle["end_date"]:
cycle["estimate_distribution"]["completion_chart"] = (
burndown_plot(
queryset=active_cycles.get(pk=cycle["id"]),
slug=self.kwargs.get("slug"),
project_id=cycle["project_id"],
cycle_id=cycle["id"],
plot_type="points",
)
)
assignee_distribution = (
Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle["id"],
@@ -127,7 +225,7 @@ class ActiveCycleEndpoint(BaseAPIView):
slug=self.kwargs.get("slug"),
project_id=cycle["project_id"],
cycle_id=cycle["id"],
plot_type=plot_type
plot_type="issues",
)
return results
@@ -139,6 +237,89 @@ class ActiveCycleEndpoint(BaseAPIView):
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", IntegerField())
)
)
.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", IntegerField())
)
)
.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", IntegerField())
)
)
.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", IntegerField())
)
)
.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", IntegerField())
)
)
.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", IntegerField())
)
)
.values("total_estimate_points")[:1]
)
active_cycles = (
Cycle.objects.filter(
workspace__slug=slug,
@@ -229,6 +410,42 @@ class ActiveCycleEndpoint(BaseAPIView):
output_field=CharField(),
)
)
.annotate(
backlog_estimate_points=Coalesce(
Subquery(backlog_estimate_point),
Value(0, output_field=IntegerField()),
),
)
.annotate(
unstarted_estimate_points=Coalesce(
Subquery(unstarted_estimate_point),
Value(0, output_field=IntegerField()),
),
)
.annotate(
started_estimate_points=Coalesce(
Subquery(started_estimate_point),
Value(0, output_field=IntegerField()),
),
)
.annotate(
cancelled_estimate_points=Coalesce(
Subquery(cancelled_estimate_point),
Value(0, output_field=IntegerField()),
),
)
.annotate(
completed_estimate_points=Coalesce(
Subquery(completed_estimate_point),
Value(0, output_field=IntegerField()),
),
)
.annotate(
total_estimate_points=Coalesce(
Subquery(total_estimate_point),
Value(0, output_field=IntegerField()),
),
)
.prefetch_related(
Prefetch(
"issue_cycle__issue__assignees",
@@ -255,7 +472,7 @@ class ActiveCycleEndpoint(BaseAPIView):
active_cycles, many=True
).data,
controller=lambda results: self.get_results_controller(
results,plot_type, active_cycles
results, plot_type, active_cycles
),
default_per_page=int(request.GET.get("per_page", 3)),
)

View File

@@ -1,6 +1,6 @@
import { FC } from "react";
// types
import { ICycle } from "@plane/types";
import { FC, Fragment, useState } from "react";
import { ICycle, TCyclePlotType } from "@plane/types";
import { CustomSelect } from "@plane/ui";
// components
import ProgressChart from "@/components/core/sidebar/progress-chart";
@@ -8,13 +8,37 @@ export type ActiveCycleProductivityProps = {
cycle: ICycle;
};
const cycleBurnDownChartOptions = [
{ value: "burndown", label: "Issues" },
{ value: "points", label: "Points" },
];
export const ActiveCycleProductivity: FC<ActiveCycleProductivityProps> = (props) => {
const { cycle } = props;
// state
const [plotType, setPlotType] = useState<TCyclePlotType>("burndown");
// derived values
const chartDistributionData = plotType === "points" ? cycle?.estimate_distribution : cycle?.distribution || undefined;
const completionChartDistributionData = chartDistributionData?.completion_chart || undefined;
return (
<div className="flex flex-col gap-4 p-4 min-h-52 border border-custom-border-200 rounded-lg">
<div className="flex items-center justify-between gap-4">
<h3 className="text-lg text-custom-text-300 font-medium">Issue burndown</h3>
<div className="flex items-center gap-2">
<CustomSelect
value={plotType}
label={<span>{cycleBurnDownChartOptions.find((v) => v.value === plotType)?.label ?? "None"}</span>}
onChange={(value: TCyclePlotType) => setPlotType(value)}
maxHeight="lg"
>
{cycleBurnDownChartOptions.map((item) => (
<CustomSelect.Option key={item.value} value={item.value}>
{item.label}
</CustomSelect.Option>
))}
</CustomSelect>
</div>
</div>
<div className="h-full w-full">
@@ -29,15 +53,34 @@ export const ActiveCycleProductivity: FC<ActiveCycleProductivityProps> = (props)
<span>Current</span>
</div>
</div>
<span>{`Pending issues - ${cycle.backlog_issues + cycle.unstarted_issues + cycle.started_issues}`}</span>
{plotType === "points" ? (
<span>{`Pending points - ${cycle.backlog_estimate_points + cycle.unstarted_estimate_points + cycle.started_estimate_points}`}</span>
) : (
<span>{`Pending issues - ${cycle.backlog_issues + cycle.unstarted_issues + cycle.started_issues}`}</span>
)}
</div>
<div className="relative -mt-4">
<ProgressChart
distribution={cycle.distribution?.completion_chart ?? {}}
startDate={cycle.start_date ?? ""}
endDate={cycle.end_date ?? ""}
totalIssues={cycle.total_issues}
/>
{completionChartDistributionData && (
<Fragment>
{plotType === "points" ? (
<ProgressChart
distribution={completionChartDistributionData}
startDate={cycle.start_date ?? ""}
endDate={cycle.end_date ?? ""}
totalIssues={cycle.total_estimate_points || 0}
plotTitle={"points"}
/>
) : (
<ProgressChart
distribution={completionChartDistributionData}
startDate={cycle.start_date ?? ""}
endDate={cycle.end_date ?? ""}
totalIssues={cycle.total_issues || 0}
plotTitle={"issues"}
/>
)}
</Fragment>
)}
</div>
</div>
</div>

View File

@@ -1,8 +1,7 @@
import { FC, useEffect, useState } from "react";
import { observer } from "mobx-react";
import { TEstimateSystemKeys, TEstimateUpdateStageKeys } from "@plane/types";
// components
import { EModalPosition, EModalWidth, ModalCore } from "@/components/core";
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
// hooks
import { useEstimate } from "@/hooks/store";
// plane web components