Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WEB-522] chore: enabled estimate point analytics for module and cycle #4763

Merged
merged 31 commits into from
Jun 12, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
b302add
chore: updated modal and form validations
gurusainath Jun 10, 2024
3996e58
Merge branch 'develop' of gurusainath:makeplane/plane into estimate-v…
gurusainath Jun 10, 2024
92c1cff
chore: module estimate analytics
NarayanBavisetti Jun 11, 2024
62b8638
Merge branch 'estimate-module-cycle-analytics' of gurusainath:makepla…
gurusainath Jun 11, 2024
866b0df
Merge branch 'develop' of github.com:makeplane/plane into estimate-mo…
NarayanBavisetti Jun 11, 2024
5e7cd78
Merge branch 'estimate-module-cycle-analytics' of github.com:makeplan…
NarayanBavisetti Jun 11, 2024
6ccdba2
chore: state analytics
NarayanBavisetti Jun 11, 2024
4c28f0f
chore: cycle estimate analytics
NarayanBavisetti Jun 11, 2024
7c2f4c2
Merge branch 'develop' of gurusainath:makeplane/plane into estimate-m…
gurusainath Jun 11, 2024
32d5ab8
Merge branch 'estimate-module-cycle-analytics' of gurusainath:makepla…
gurusainath Jun 11, 2024
183a184
chore: module points serializer
NarayanBavisetti Jun 11, 2024
b327873
Merge branch 'estimate-module-cycle-analytics' of gurusainath:makepla…
gurusainath Jun 11, 2024
9a556ad
chore: added fields in serializer
NarayanBavisetti Jun 11, 2024
d620e72
chore: module state estimate points
gurusainath Jun 11, 2024
498246c
dev: resolved merge conflicts
gurusainath Jun 11, 2024
e0f0f07
dev: updated module analytics
gurusainath Jun 11, 2024
08f086f
dev: updated hover description on the burndown
gurusainath Jun 11, 2024
9b90038
Merge branch 'develop' of gurusainath:makeplane/plane into estimate-m…
gurusainath Jun 11, 2024
257054d
dev: UI and module total percentage validation
gurusainath Jun 11, 2024
bfb1b09
chore: estimate points structure change
NarayanBavisetti Jun 11, 2024
551569a
Merge branch 'estimate-module-cycle-analytics' of github.com:makeplan…
NarayanBavisetti Jun 11, 2024
1c376f1
chore: module burndown
NarayanBavisetti Jun 11, 2024
e768041
chore: key values changed
NarayanBavisetti Jun 11, 2024
4df4f25
chore: cycle progress snapshot
NarayanBavisetti Jun 12, 2024
b774cb5
chore: updated module estimate points
gurusainath Jun 12, 2024
1065b38
chore: cycle detail endpoint
NarayanBavisetti Jun 12, 2024
b61efbd
chore: progress snapshot payload change
NarayanBavisetti Jun 12, 2024
f5f7fae
chore: resolved merge conflicts
gurusainath Jun 12, 2024
350ac3f
Merge branch 'estimate-module-cycle-analytics' of gurusainath:makepla…
gurusainath Jun 12, 2024
aa6ec67
chore: updated issue and point dropdown in active cycle
gurusainath Jun 12, 2024
2db70b3
chore: optimized grouped issues count in cycle and module
gurusainath Jun 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
chore: module state estimate points
  • Loading branch information
gurusainath committed Jun 11, 2024
commit d620e7284348577f8ed997ffbac7336ce65c109d
12 changes: 6 additions & 6 deletions packages/types/src/cycle/cycle.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,12 @@ export type TProgressSnapshot = {
export type TAssigneesDistribution = {
assignee_id: string | null;
avatar: string | null;
completed_issues: number;
first_name: string | null;
last_name: string | null;
display_name: string | null;
pending_issues: number;
total_issues: number;
total: number;
pending: number;
completed: number;
};

export type TCompletionChartDistribution = {
Expand All @@ -72,11 +72,11 @@ export type TCompletionChartDistribution = {

export type TLabelsDistribution = {
color: string | null;
completed_issues: number;
label_id: string | null;
label_name: string | null;
pending_issues: number;
total_issues: number;
total: number;
pending: number;
completed: number;
};

export interface CycleIssueResponse {
Expand Down
17 changes: 12 additions & 5 deletions packages/types/src/module/modules.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,18 @@ export type TModuleStatus =

export interface IModule {
backlog_issues: number;
started_issues: number;
unstarted_issues: number;
cancelled_issues: number;
backlog_estimate_issues: number;
started_estimate_issues: number;
unstarted_estimate_issues: number;
cancelled_estimate_issues: number;
total_issues: number;
completed_issues: number;
total_estimate_points?: number;
completed_estimate_points?: number;

created_at: string;
created_by?: string;
description: string;
Expand All @@ -39,13 +49,8 @@ export interface IModule {
sort_order: number;
sub_issues?: number;
start_date: string | null;
started_issues: number;
status?: TModuleStatus;
target_date: string | null;
total_issues: number;
unstarted_issues: number;
total_estimate_points?: number;
completed_estimate_points?: number;
updated_at: string;
updated_by?: string;
archived_at: string | null;
Expand Down Expand Up @@ -78,3 +83,5 @@ export type ModuleLink = {
export type SelectModuleType =
| (IModule & { actionType: "edit" | "delete" | "create-issue" })
| undefined;

export type TModulePlotType = "burndown" | "points";
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import useSWR from "swr";
import { EmptyState } from "@/components/common";
import { PageHead } from "@/components/core";
import { ModuleLayoutRoot } from "@/components/issues";
import { ModuleDetailsSidebar } from "@/components/modules";
import { ModuleAnalyticsSidebar } from "@/components/modules";
// hooks
import { useModule, useProject } from "@/hooks/store";
import useLocalStorage from "@/hooks/use-local-storage";
Expand Down Expand Up @@ -68,7 +68,7 @@ const ModuleIssuesPage = observer(() => {
"0px 1px 4px 0px rgba(0, 0, 0, 0.06), 0px 2px 4px 0px rgba(16, 24, 40, 0.06), 0px 1px 8px -1px rgba(16, 24, 40, 0.06)",
}}
>
<ModuleDetailsSidebar moduleId={moduleId.toString()} handleClose={toggleSidebar} />
<ModuleAnalyticsSidebar moduleId={moduleId.toString()} handleClose={toggleSidebar} />
</div>
)}
</div>
Expand Down
16 changes: 8 additions & 8 deletions web/components/core/sidebar/sidebar-progress-stats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,8 @@ export const SidebarProgressStats: React.FC<Props> = observer((props) => {
<span>{assignee?.display_name ?? ""}</span>
</div>
}
completed={assignee.completed_issues}
total={assignee.total_issues}
completed={assignee.completed}
total={assignee.total}
{...(!isPeekView &&
!isCompleted && {
onClick: () => handleFiltersUpdate("assignees", assignee.assignee_id ?? ""),
Expand All @@ -183,8 +183,8 @@ export const SidebarProgressStats: React.FC<Props> = observer((props) => {
<span>No assignee</span>
</div>
}
completed={assignee.completed_issues}
total={assignee.total_issues}
completed={assignee.completed}
total={assignee.total}
/>
);
})
Expand Down Expand Up @@ -218,8 +218,8 @@ export const SidebarProgressStats: React.FC<Props> = observer((props) => {
<span className="text-xs">{label.label_name ?? "No labels"}</span>
</div>
}
completed={label.completed_issues}
total={label.total_issues}
completed={label.completed}
total={label.total}
{...(!isPeekView &&
!isCompleted && {
onClick: () => handleFiltersUpdate("labels", label.label_id ?? ""),
Expand All @@ -242,8 +242,8 @@ export const SidebarProgressStats: React.FC<Props> = observer((props) => {
<span className="text-xs">{label.label_name ?? "No labels"}</span>
</div>
}
completed={label.completed_issues}
total={label.total_issues}
completed={label.completed}
total={label.total}
/>
);
}
Expand Down
2 changes: 2 additions & 0 deletions web/components/modules/analytics-sidebar/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./root";
export * from "./issue-progress";
243 changes: 243 additions & 0 deletions web/components/modules/analytics-sidebar/issue-progress.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
"use client";

import { FC, Fragment, useCallback, useState } from "react";
import isEqual from "lodash/isEqual";
import { useSearchParams } from "next/navigation";
import { AlertCircle, ChevronUp, ChevronDown } from "lucide-react";
import { Disclosure, Transition } from "@headlessui/react";
import { IIssueFilterOptions, TModulePlotType } from "@plane/types";
import { CustomSelect } from "@plane/ui";
// components
import { SidebarProgressStats } from "@/components/core";
import ProgressChart from "@/components/core/sidebar/progress-chart";
// constants
import { EEstimateSystem } from "@/constants/estimates";
import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue";
// helpers
import { getDate } from "@/helpers/date-time.helper";
// hooks
import { useIssues, useModule, useProjectEstimates } from "@/hooks/store";

type TModuleAnalyticsProgress = {
workspaceSlug: string;
projectId: string;
moduleId: string;
};

const moduleBurnDownChartOptions = [
{ value: "burndown", label: "Issues" },
{ value: "points", label: "Points" },
];

export const ModuleAnalyticsProgress: FC<TModuleAnalyticsProgress> = (props) => {
// props
const { workspaceSlug, projectId, moduleId } = props;
// router
const searchParams = useSearchParams();
const peekModule = searchParams.get("peekModule") || undefined;
// hooks
const { areEstimateEnabledByProjectId, currentActiveEstimateId, estimateById } = useProjectEstimates();
const { getModuleById, fetchAnalyticsModuleDetails } = useModule();
const {
issuesFilter: { issueFilters, updateFilters },
} = useIssues(EIssuesStoreType.MODULE);
// state
const [progressPlotType, setProgressPlotType] = useState<TModulePlotType>("burndown");
// derived values
const moduleDetails = getModuleById(moduleId);
const isCurrentProjectEstimateEnabled = projectId && areEstimateEnabledByProjectId(projectId) ? true : false;
const estimateDetails =
isCurrentProjectEstimateEnabled && currentActiveEstimateId && estimateById(currentActiveEstimateId);
const isCurrentEstimateTypeIsPoints = estimateDetails && estimateDetails?.type === EEstimateSystem.POINTS;

const progressHeaderPercentage = moduleDetails
? isCurrentEstimateTypeIsPoints
? Math.round(((moduleDetails.completed_estimate_points || 0) / (moduleDetails.total_estimate_points || 0)) * 100)
: Math.round((moduleDetails.completed_issues / moduleDetails.total_issues) * 100)
: undefined;

const moduleStartDate = getDate(moduleDetails?.start_date);
const moduleEndDate = getDate(moduleDetails?.target_date);
const isModuleStartDateValid = moduleStartDate && moduleStartDate <= new Date();
const isModuleEndDateValid = moduleStartDate && moduleEndDate && moduleEndDate >= moduleStartDate;
const isModuleDateValid = isModuleStartDateValid && isModuleEndDateValid;

// handlers
const onChange = async (value: TModulePlotType) => {
setProgressPlotType(value);

if (!workspaceSlug || !projectId || !moduleId) return;
try {
await fetchAnalyticsModuleDetails(workspaceSlug, projectId, moduleId, value);
} catch (error) {
console.log("error", error);
}
};

const handleFiltersUpdate = useCallback(
(key: keyof IIssueFilterOptions, value: string | string[]) => {
if (!workspaceSlug || !projectId) return;

let newValues = issueFilters?.filters?.[key] ?? [];

if (Array.isArray(value)) {
if (key === "state") {
if (isEqual(newValues, value)) newValues = [];
else newValues = value;
} else {
value.forEach((val) => {
if (!newValues.includes(val)) newValues.push(val);
else newValues.splice(newValues.indexOf(val), 1);
});
}
} else {
if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else newValues.push(value);
}
updateFilters(
workspaceSlug.toString(),
projectId.toString(),
EIssueFilterType.FILTERS,
{ [key]: newValues },
moduleId
);
},
[workspaceSlug, projectId, moduleId, issueFilters, updateFilters]
);

if (!moduleDetails) return <></>;
return (
<div className="border-t border-custom-border-200 space-y-4 py-4">
<Disclosure defaultOpen={isModuleDateValid ? true : false}>
{({ open }) => (
<div className="space-y-6">
{/* progress bar header */}
{isModuleDateValid ? (
<div className="relative w-full flex justify-between items-center gap-2">
<Disclosure.Button className="relative flex items-center gap-2 w-full">
<div className="font-medium text-custom-text-200 text-sm">Progress</div>
{progressHeaderPercentage && (
<div className="flex h-5 w-9 items-center justify-center rounded bg-amber-500/20 text-xs font-medium text-amber-500">{`${progressHeaderPercentage}%`}</div>
)}
<div className="ml-auto">
{open ? (
<ChevronUp className="h-3 w-3" aria-hidden="true" />
) : (
<ChevronDown className="h-3 w-3" aria-hidden="true" />
)}
</div>
</Disclosure.Button>
<div>
<CustomSelect
value={progressPlotType}
label={
<span>
{moduleBurnDownChartOptions.find((v) => v.value === progressPlotType)?.label ?? "None"}
</span>
}
onChange={onChange}
maxHeight="lg"
>
{moduleBurnDownChartOptions.map((item) => (
<CustomSelect.Option key={item.value} value={item.value}>
{item.label}
</CustomSelect.Option>
))}
</CustomSelect>
</div>
</div>
) : (
<div className="relative w-full flex justify-between items-center gap-2">
<div className="font-medium text-custom-text-200 text-sm">Progress</div>
<div className="flex items-center gap-1">
<AlertCircle height={14} width={14} className="text-custom-text-200" />
<span className="text-xs italic text-custom-text-200">
{moduleDetails?.start_date && moduleDetails?.target_date
? "This module isn't active yet."
: "Invalid date. Please enter valid date."}
</span>
</div>
</div>
)}

<Transition show={open}>
<Disclosure.Panel className="space-y-4">
{/* progress burndown chart */}
<div>
<div className="relative flex items-center gap-2">
<div className="flex items-center justify-center gap-1 text-xs">
<span className="h-2.5 w-2.5 rounded-full bg-[#A9BBD0]" />
<span>Ideal</span>
</div>
<div className="flex items-center justify-center gap-1 text-xs">
<span className="h-2.5 w-2.5 rounded-full bg-[#4C8FFF]" />
<span>Current</span>
</div>
</div>
{moduleDetails.start_date &&
moduleDetails.target_date &&
moduleDetails.distribution?.completion_chart && (
<Fragment>
{isCurrentEstimateTypeIsPoints && moduleDetails.total_estimate_points ? (
<ProgressChart
distribution={moduleDetails.distribution?.completion_chart ?? {}}
startDate={moduleDetails.start_date}
endDate={moduleDetails.target_date}
totalIssues={moduleDetails.total_estimate_points}
/>
) : (
<ProgressChart
distribution={moduleDetails.distribution?.completion_chart ?? {}}
startDate={moduleDetails.start_date}
endDate={moduleDetails.target_date}
totalIssues={moduleDetails.total_issues}
/>
)}
</Fragment>
)}
</div>

{/* progress detailed view */}
<div>
{moduleDetails.total_issues > 0 && (
<div className="w-full border-t border-custom-border-200 pt-5">
<SidebarProgressStats
distribution={moduleDetails.distribution}
groupedIssues={{
backlog: isCurrentEstimateTypeIsPoints
? moduleDetails.backlog_estimate_issues
: moduleDetails.backlog_issues,
unstarted: isCurrentEstimateTypeIsPoints
? moduleDetails.unstarted_estimate_issues
: moduleDetails.unstarted_issues,
started: isCurrentEstimateTypeIsPoints
? moduleDetails.started_estimate_issues
: moduleDetails.started_issues,
completed: isCurrentEstimateTypeIsPoints
? moduleDetails.completed_estimate_points || 0
: moduleDetails.completed_issues,
cancelled: isCurrentEstimateTypeIsPoints
? moduleDetails.cancelled_estimate_issues
: moduleDetails.cancelled_issues,
}}
totalIssues={
isCurrentEstimateTypeIsPoints
? moduleDetails.total_estimate_points || 0
: moduleDetails.total_issues
}
module={moduleDetails}
isPeekView={Boolean(peekModule)}
filters={issueFilters}
handleFiltersUpdate={handleFiltersUpdate}
/>
</div>
)}
</div>
</Disclosure.Panel>
</Transition>
</div>
)}
</Disclosure>
</div>
);
};
Loading