mirror of
https://github.com/makeplane/plane.git
synced 2025-12-29 00:24:56 +01:00
[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:
@@ -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
|
||||
|
||||
@@ -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)),
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user