From 25475219649b0085941679bd319cc40cc8b18524 Mon Sep 17 00:00:00 2001 From: harshit078 Date: Tue, 6 May 2025 17:50:15 +0530 Subject: [PATCH 1/9] Initialsed Deployment duration and trends with Tabs support on UI --- .../DoraMetrics/DoraMetricsDuration.tsx | 180 +++++++++ .../DeploymentInsightsOverlay.tsx | 360 ++++++++++-------- 2 files changed, 381 insertions(+), 159 deletions(-) create mode 100644 web-server/src/content/DoraMetrics/DoraMetricsDuration.tsx diff --git a/web-server/src/content/DoraMetrics/DoraMetricsDuration.tsx b/web-server/src/content/DoraMetrics/DoraMetricsDuration.tsx new file mode 100644 index 000000000..ecfbe37e0 --- /dev/null +++ b/web-server/src/content/DoraMetrics/DoraMetricsDuration.tsx @@ -0,0 +1,180 @@ +import { Card, Chip, Tooltip, useTheme } from '@mui/material'; +import { FC, useMemo } from 'react'; +import { CheckCircleRounded, AccessTimeRounded, OpenInNew, Code, ArrowForwardIosRounded } from '@mui/icons-material'; + +import { FlexBox } from '@/components/FlexBox'; +import { Line } from '@/components/Text'; +import { Deployment } from '@/types/resources'; +import { getDurationString, isoDateString } from '@/utils/date'; + +type DeploymentCardType = 'Longest' | 'Shortest'; + +interface DeploymentWithValidDuration extends Deployment { + duration: number; +} + +interface DeploymentCardProps { + deployment: DeploymentWithValidDuration; + type: DeploymentCardType; +} + +interface DoraMetricsDurationProps { + deployments: Deployment[]; +} + +const calculateDurationFromConductedAt = (conductedAt: string): number | undefined => { + try { + const deploymentDate = new Date(conductedAt); + if (isNaN(deploymentDate.getTime())) { + return undefined; + } + const now = new Date(); + return Math.floor((now.getTime() - deploymentDate.getTime()) / 1000); + } catch (error) { + console.error('Error calculating duration:', error); + return undefined; + } +}; + +const formatDeploymentDate = (dateString: string): string => { + if (!dateString) return 'Unknown Date'; + + try { + const date = new Date(dateString); + const isoDate = isoDateString(date); + const formattedDate = new Date(isoDate); + + const options: Intl.DateTimeFormatOptions = { + month: 'short', + day: 'numeric', + year: 'numeric', + hour: 'numeric', + minute: 'numeric', + hour12: true + }; + + return formattedDate.toLocaleDateString('en-US', options); + } catch (error) { + console.error('Error formatting date:', error); + return 'Invalid Date'; + } +}; + +const DeploymentCard: FC = ({ deployment }) => { + const theme = useTheme(); + + const handleDeploymentClick = () => { + if (deployment.html_url) { + window.open(deployment.html_url, '_blank', 'noopener,noreferrer'); + } + }; + + return ( + + + + + + Run On {formatDeploymentDate(deployment.conducted_at)} + + + + + + + + + + + + + {deployment.pr_count || 0} new PR's + + + {deployment.head_branch || 'Unknown Branch'} + + + {getDurationString(deployment.duration)} + + + + + + ); +}; + +export const DoraMetricsDuration: FC = ({ deployments }) => { + const { longestDeployment, shortestDeployment } = useMemo(() => { + if (!Array.isArray(deployments) || !deployments.length) { + return { longestDeployment: null, shortestDeployment: null }; + } + + const validDeployments = deployments + .filter((d): d is Deployment => Boolean(d.conducted_at)) + .map(deployment => ({ + ...deployment, + duration: calculateDurationFromConductedAt(deployment.conducted_at) + })) + .filter((d): d is DeploymentWithValidDuration => + typeof d.duration === 'number' && + d.duration >= 0 + ); + + if (!validDeployments.length) { + return { longestDeployment: null, shortestDeployment: null }; + } + + // Function to calculate Longest and shortest deployments + const { longest, shortest } = validDeployments.reduce((acc, current) => ({ + longest: !acc.longest || current.duration > acc.longest.duration ? current : acc.longest, + shortest: !acc.shortest || current.duration < acc.shortest.duration ? current : acc.shortest + }), { + longest: null as DeploymentWithValidDuration | null, + shortest: null as DeploymentWithValidDuration | null + }); + + return { longestDeployment: longest, shortestDeployment: shortest }; + }, [deployments]); + + return ( + + + + Longest Deployment + + + + + Shortest Deployment + + + + + ); +}; diff --git a/web-server/src/content/PullRequests/DeploymentInsightsOverlay.tsx b/web-server/src/content/PullRequests/DeploymentInsightsOverlay.tsx index 339700fa4..dbff882dd 100644 --- a/web-server/src/content/PullRequests/DeploymentInsightsOverlay.tsx +++ b/web-server/src/content/PullRequests/DeploymentInsightsOverlay.tsx @@ -1,17 +1,20 @@ -import { ArrowDownwardRounded } from '@mui/icons-material'; +import { ArrowDownwardRounded, CodeRounded, AccessTimeRounded } from '@mui/icons-material'; import { Card, Divider, useTheme, Collapse, Box } from '@mui/material'; import pluralize from 'pluralize'; import { ascend, descend, groupBy, path, prop, sort } from 'ramda'; -import { FC, useCallback, useEffect, useMemo } from 'react'; - +import { FC, useCallback, useEffect, useMemo, useState } from 'react'; +import { DoraMetricsTrend } from '../DoraMetrics/DoraMetricsTrend'; +import { DoraMetricsDuration } from '../DoraMetrics/DoraMetricsDuration'; import { FlexBox } from '@/components/FlexBox'; import { MiniButton } from '@/components/MiniButton'; import { MiniCircularLoader } from '@/components/MiniLoader'; import { ProgressBar } from '@/components/ProgressBar'; import { PullRequestsTableMini } from '@/components/PRTableMini/PullRequestsTableMini'; import Scrollbar from '@/components/Scrollbar'; +import { Tabs, TabItem } from '@/components/Tabs'; import { Line } from '@/components/Text'; import { FetchState } from '@/constants/ui-states'; +import { DoraMetricsComparisonPill } from '@/content/DoraMetrics/DoraMetricsComparisonPill'; import { useAuth } from '@/hooks/useAuth'; import { useBoolState, useEasyState } from '@/hooks/useEasyState'; import { @@ -29,6 +32,8 @@ import { useDispatch, useSelector } from '@/store'; import { Deployment, PR, RepoWorkflowExtended } from '@/types/resources'; import { percent } from '@/utils/datatype'; import { depFn } from '@/utils/fn'; +import { format } from 'date-fns'; +import { getDurationString } from '@/utils/date'; import { DeploymentItem } from './DeploymentItem'; @@ -40,12 +45,24 @@ enum DepStatusFilter { const hideTableColumns = new Set(['reviewers', 'rework_cycles']); +enum TabKeys { + ANALYTICS = 'analytics', + EVENTS = 'events' +} + export const DeploymentInsightsOverlay = () => { const { orgId } = useAuth(); const { singleTeamId, team, dates } = useSingleTeamConfig(); const dispatch = useDispatch(); const depFilter = useEasyState(DepStatusFilter.All); const branchesForPrFilters = useBranchesForPrFilters(); + const [activeTab, setActiveTab] = useState(TabKeys.EVENTS); + const tabItems: TabItem[] = [ + { key: TabKeys.ANALYTICS, label: 'Deployment Analytics' }, + { key: TabKeys.EVENTS, label: 'Deployment Events' } + ]; + const handleTabSelect = (item: TabItem) => setActiveTab(item.key as string); + useEffect(() => { if (!singleTeamId) return; @@ -205,11 +222,10 @@ export const DeploymentInsightsOverlay = () => { px={1} py={1 / 2} corner={theme.spacing(1)} - border={`1px solid ${ - repo.id === selectedRepo.value + border={`1px solid ${repo.id === selectedRepo.value ? theme.colors.info.main : theme.colors.secondary.light - }`} + }`} pointer bgcolor={ repo.id === selectedRepo.value @@ -221,7 +237,9 @@ export const DeploymentInsightsOverlay = () => { ? theme.colors.info.main : undefined } - fontWeight={repo.id === selectedRepo.value ? 'bold' : undefined} + fontWeight={ + repo.id === selectedRepo.value ? 'bold' : undefined + } onClick={() => { selectedRepo.set(repo.id as ID); }} @@ -267,170 +285,194 @@ export const DeploymentInsightsOverlay = () => { }} minHeight={'calc(100vh - 275px)'} > - - - - - - No. of deployments {'->'} - {' '} - { - depFilter.set(DepStatusFilter.All); - }} - > - {deployments.length} - {' '} - {Boolean(failedDeps.length) ? ( - { - depFilter.set(DepStatusFilter.Fail); - }} - pointer - > - ({failedDeps.length} failed) - - ) : ( + item.key === activeTab} + /> + {activeTab === TabKeys.ANALYTICS ? ( + + + + + + No. of deployments {'->'} + {' '} { - depFilter.set(DepStatusFilter.Pass); + depFilter.set(DepStatusFilter.All); }} - pointer > - (All passed) - - )} - - { - depFilter.set(DepStatusFilter.Pass); - }} - remainingOnClick={() => { - depFilter.set(DepStatusFilter.Fail); - }} - /> - - - - - - - depFilter.set(DepStatusFilter.All)} - variant={ - depFilter.value === DepStatusFilter.All - ? 'contained' - : 'outlined' - } - > - All - - depFilter.set(DepStatusFilter.Pass)} - variant={ - depFilter.value === DepStatusFilter.Pass - ? 'contained' - : 'outlined' - } - > - Successful - - depFilter.set(DepStatusFilter.Fail)} - variant={ - depFilter.value === DepStatusFilter.Fail - ? 'contained' - : 'outlined' - } - > - Failed - + {deployments.length} + {' '} + {Boolean(failedDeps.length) ? ( + { + depFilter.set(DepStatusFilter.Fail); + }} + pointer + > + ({failedDeps.length} failed) + + ) : ( + { + depFilter.set(DepStatusFilter.Pass); + }} + pointer + > + (All passed) + + )} + + { + depFilter.set(DepStatusFilter.Pass); + }} + remainingOnClick={() => { + depFilter.set(DepStatusFilter.Fail); + }} + /> - - - - - {filteredDeployments.length ? ( - groupedDeployments.map(([workflow, deployments]) => ( - + + + + + + + ) : ( + <> + + + + + + depFilter.set(DepStatusFilter.All)} + variant={ + depFilter.value === DepStatusFilter.All + ? 'contained' + : 'outlined' + } + > + All + + depFilter.set(DepStatusFilter.Pass)} + variant={ + depFilter.value === DepStatusFilter.Pass + ? 'contained' + : 'outlined' + } + > + Successful + + depFilter.set(DepStatusFilter.Fail)} + variant={ + depFilter.value === DepStatusFilter.Fail + ? 'contained' + : 'outlined' + } + > + Failed + + + + + + + {filteredDeployments.length ? ( + groupedDeployments.map( + ([workflow, deployments]) => ( + + ) + ) + ) : ( + + + No deployments matching the current filter. + + + depFilter.set(DepStatusFilter.All) + } + pointer + > + See all deployments? + + + )} + + + + + + {selectedDep ? ( + + + Selected Deployment + + + + + {loadingPrs ? ( + + ) : prs.length ? ( + - )) - ) : ( - + ) : ( - No deployments matching the current filter. + No new PRs linked to this deployment. - depFilter.set(DepStatusFilter.All)} - pointer - > - See all deployments? - - - )} + )} + - - - - - {selectedDep ? ( - - - Selected Deployment - - - - - {loadingPrs ? ( - - ) : prs.length ? ( - - ) : ( - - No new PRs linked to this deployment. + ) : ( + + + Select a deployment on the left - )} - - - ) : ( - - - Select a deployment on the left - - - to view PRs included in that deployment - + + to view PRs included in that deployment + + + )} - )} - - + + + )} ) ) : ( From d7168a130137ec9371054988898025b709ae5ecd Mon Sep 17 00:00:00 2001 From: harshit078 Date: Tue, 6 May 2025 17:55:42 +0530 Subject: [PATCH 2/9] Added duration type for deployment duration for logic calculation --- web-server/src/types/resources.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/web-server/src/types/resources.ts b/web-server/src/types/resources.ts index 6bdcd5748..6b84ba6b2 100644 --- a/web-server/src/types/resources.ts +++ b/web-server/src/types/resources.ts @@ -223,6 +223,7 @@ export type Deployment = { conducted_at: DateString; pr_count: number; run_duration: number; + duration: number; html_url: string; repo_workflow_id: ID; }; From 9c9b07496ecda268700fad119d833e1e88d3e834 Mon Sep 17 00:00:00 2001 From: harshit078 Date: Tue, 13 May 2025 18:57:58 +0530 Subject: [PATCH 3/9] Updated logic to use run_duration and added Trends and Graphs metric --- .../DoraMetrics/DoraMetricsDuration.tsx | 51 +-- .../content/DoraMetrics/DoraMetricsTrend.tsx | 370 ++++++++++++++++++ .../DeploymentInsightsOverlay.tsx | 8 +- web-server/src/types/resources.ts | 1 - 4 files changed, 391 insertions(+), 39 deletions(-) create mode 100644 web-server/src/content/DoraMetrics/DoraMetricsTrend.tsx diff --git a/web-server/src/content/DoraMetrics/DoraMetricsDuration.tsx b/web-server/src/content/DoraMetrics/DoraMetricsDuration.tsx index ecfbe37e0..05b95f439 100644 --- a/web-server/src/content/DoraMetrics/DoraMetricsDuration.tsx +++ b/web-server/src/content/DoraMetrics/DoraMetricsDuration.tsx @@ -9,12 +9,9 @@ import { getDurationString, isoDateString } from '@/utils/date'; type DeploymentCardType = 'Longest' | 'Shortest'; -interface DeploymentWithValidDuration extends Deployment { - duration: number; -} interface DeploymentCardProps { - deployment: DeploymentWithValidDuration; + deployment: Deployment; type: DeploymentCardType; } @@ -22,19 +19,6 @@ interface DoraMetricsDurationProps { deployments: Deployment[]; } -const calculateDurationFromConductedAt = (conductedAt: string): number | undefined => { - try { - const deploymentDate = new Date(conductedAt); - if (isNaN(deploymentDate.getTime())) { - return undefined; - } - const now = new Date(); - return Math.floor((now.getTime() - deploymentDate.getTime()) / 1000); - } catch (error) { - console.error('Error calculating duration:', error); - return undefined; - } -}; const formatDeploymentDate = (dateString: string): string => { if (!dateString) return 'Unknown Date'; @@ -112,9 +96,17 @@ const DeploymentCard: FC = ({ deployment }) => { {deployment.head_branch || 'Unknown Branch'} - {getDurationString(deployment.duration)} + + {getDurationString(deployment.run_duration)} + @@ -130,15 +122,8 @@ export const DoraMetricsDuration: FC = ({ deployments } const validDeployments = deployments - .filter((d): d is Deployment => Boolean(d.conducted_at)) - .map(deployment => ({ - ...deployment, - duration: calculateDurationFromConductedAt(deployment.conducted_at) - })) - .filter((d): d is DeploymentWithValidDuration => - typeof d.duration === 'number' && - d.duration >= 0 - ); + .filter((d): d is Deployment => Boolean(d.conducted_at && typeof d.run_duration === 'number')) + .filter((d) => d.run_duration >= 0); if (!validDeployments.length) { return { longestDeployment: null, shortestDeployment: null }; @@ -146,11 +131,11 @@ export const DoraMetricsDuration: FC = ({ deployments // Function to calculate Longest and shortest deployments const { longest, shortest } = validDeployments.reduce((acc, current) => ({ - longest: !acc.longest || current.duration > acc.longest.duration ? current : acc.longest, - shortest: !acc.shortest || current.duration < acc.shortest.duration ? current : acc.shortest + longest: !acc.longest || current.run_duration > acc.longest.run_duration ? current : acc.longest, + shortest: !acc.shortest || current.run_duration < acc.shortest.run_duration ? current : acc.shortest }), { - longest: null as DeploymentWithValidDuration | null, - shortest: null as DeploymentWithValidDuration | null + longest: null as Deployment | null, + shortest: null as Deployment | null }); return { longestDeployment: longest, shortestDeployment: shortest }; @@ -160,7 +145,7 @@ export const DoraMetricsDuration: FC = ({ deployments - Longest Deployment + Longest Deployment = ({ deployments - Shortest Deployment + Shortest Deployment { + + if (typeof deployment.run_duration === 'number' && deployment.run_duration > 0) { + return deployment.run_duration; + } + + + try { + const conductedAt = new Date(deployment.conducted_at); + const createdAt = new Date(deployment.created_at); + if (isNaN(conductedAt.getTime()) || isNaN(createdAt.getTime())) { + return 0; + } + const durationMs = conductedAt.getTime() - createdAt.getTime(); + return Math.max(0, Math.floor(durationMs / 1000)); + } catch (e) { + console.error("Error calculating deployment duration", e); + return 0; + } +}; + +const getDeploymentDurationInHours = (deployment: Deployment): number => { + const seconds = getDeploymentDurationInSeconds(deployment); + return +(seconds / 3600).toFixed(2); // Convert to hours and round to 2 decimal places +}; + +export const calculateDeploymentTrends = ( + deployments: Deployment[] +): { + durationTrend: TrendData; + prCountTrend: TrendData; +} => { + if (!deployments || deployments.length < 2) { + return { + durationTrend: { value: 0, change: 0, state: 'neutral' }, + prCountTrend: { value: 0, change: 0, state: 'neutral' } + }; + } + + const sortedDeployments = [...deployments].sort( + (a, b) => + new Date(a.conducted_at).getTime() - new Date(b.conducted_at).getTime() + ); + + const midpoint = Math.floor(sortedDeployments.length / 2); + const firstHalf = sortedDeployments.slice(0, midpoint); + const secondHalf = sortedDeployments.slice(midpoint); + + console.log(deployments) + + // Calculate average duration for each half + const getAvgDuration = (deps: Deployment[]) => { + const totalDuration = deps.reduce((sum, dep) => sum + getDeploymentDurationInSeconds(dep), 0); + return deps.length > 0 ? totalDuration / deps.length : 0; + }; + + const firstHalfAvgDuration = getAvgDuration(firstHalf); + const secondHalfAvgDuration = getAvgDuration(secondHalf); + + const durationChange = firstHalfAvgDuration + ? ((secondHalfAvgDuration - firstHalfAvgDuration) / firstHalfAvgDuration) * 100 + : 0; + + const avgDuration = getAvgDuration(sortedDeployments); + + const getAvgPrCount = (deps: Deployment[]): number => { + if (!deps || deps.length === 0) return 0; + + // Group deployments by date first + const deploymentsByDate = deps.reduce((acc, dep) => { + const date = new Date(dep.conducted_at).toLocaleDateString('en-US'); + if (!acc[date]) { + acc[date] = { totalPRs: 0, count: 0 }; + } + acc[date].totalPRs += dep.pr_count || 0; + acc[date].count++; + return acc; + }, {} as Record); + + const dailyTotals = Object.values(deploymentsByDate); + const totalPRs = dailyTotals.reduce((sum, day) => sum + day.totalPRs, 0); + const numberOfDays = dailyTotals.length; + + return numberOfDays > 0 ? totalPRs / numberOfDays : 0; + }; + + const firstHalfAvgPrCount = getAvgPrCount(firstHalf); + const secondHalfAvgPrCount = getAvgPrCount(secondHalf); + + const prCountChange = firstHalfAvgPrCount + ? ((secondHalfAvgPrCount - firstHalfAvgPrCount) / firstHalfAvgPrCount) * 100 + : 0; + + const avgPrCount = getAvgPrCount(sortedDeployments); + + return { + durationTrend: { + value: avgDuration, + change: durationChange, + state: determineTrendState(durationChange, false) + }, + prCountTrend: { + value: avgPrCount, + change: prCountChange, + state: determineTrendState(prCountChange, true) + } + }; +}; + +const determineTrendState = ( + change: number, + isPositiveWhenIncreasing: boolean +): 'positive' | 'negative' | 'neutral' => { + if (Math.abs(change) <= MEANINGFUL_CHANGE_THRESHOLD) { + return 'neutral'; + } + + const isIncreasing = change > 0; + if (isIncreasing) { + return isPositiveWhenIncreasing ? 'positive' : 'negative'; + } else { + return isPositiveWhenIncreasing ? 'negative' : 'positive'; + } +}; + +export const DeploymentTrendPill: FC<{ + label: string; + change: number; + state: 'positive' | 'negative' | 'neutral'; + value: number; + valueFormat?: (val: number) => string; +}> = ({ label, change, state }) => { + const theme = useTheme(); + + const text = ( + state === 'positive' ? 'Increasing ' + label : state === 'negative' ? 'Decreasing ' + label : 'Stable ' + label + ) + + const useMultiplierFormat = Math.abs(change) > 100; + const formattedChange = useMultiplierFormat + ? `${percentageToMultiplier(change)}` + : `${Math.round(change)}%`; + + const color = darken ( + state === 'positive' + ? theme.colors.success.main + : theme.colors.warning.main, + state === 'neutral' ? 0.5 : 0, + + ) + + const icon = + state === 'positive' ? ( + + ) : state === 'negative' ? ( + + ) : ( + + ); + + return ( + + {text} + + + + {formattedChange} + + {icon} + + + + ); +}; + +export const DoraMetricsTrend: FC = () => { + const theme = useTheme(); + const { deployments_map = {} } = useSelector( + (state) => state.doraMetrics.team_deployments + ); + + const allDeployments = useMemo(() => { + return Object.values(deployments_map).flat(); + }, [deployments_map]); + + const { durationTrend, prCountTrend } = useMemo(() => { + return calculateDeploymentTrends(allDeployments); + }, [allDeployments]); + + const chartData = useMemo(() => { + const sortedDeployments = [...allDeployments].sort( + (a, b) => new Date(a.conducted_at).getTime() - new Date(b.conducted_at).getTime() + ); + + const deploymentsByDate = sortedDeployments.reduce((acc, deployment) => { + const date = new Date(deployment.conducted_at).toLocaleDateString('en-US', { + day: 'numeric', + month: 'short' + }); + + if (!acc[date]) { + acc[date] = { + deployments: [], + totalDuration: 0, + totalPRs: 0 + }; + } + + const durationInHours = getDeploymentDurationInHours(deployment); + acc[date].deployments.push(deployment); + acc[date].totalDuration += durationInHours; + acc[date].totalPRs += deployment.pr_count || 0; + + return acc; + }, {} as Record); + + const dates = Object.keys(deploymentsByDate); + const durations = dates.map(date => deploymentsByDate[date].totalDuration); + const prCounts = dates.map(date => deploymentsByDate[date].totalPRs); + + const maxDuration = Math.max(...durations); + const yAxisMax = Math.ceil(maxDuration); + + const series: ChartSeries = [ + { + type: 'bar', + label: 'Deployment Duration (hours)', + data: durations, + yAxisID: 'y', + borderColor: theme.colors.success.main, + order: 0 + }, + { + type: 'bar', + label: 'PR Count', + data: prCounts, + yAxisID: 'y1', + backgroundColor: theme.colors.info.main, + borderWidth: 2, + tension: 0.4, + order: 1 + } + ]; + + return { labels: dates, series, yAxisMax }; + }, [allDeployments, theme.colors]); + + return ( + + + Deployment Trend + + + + + +
+ value + 'h' + }, + max: chartData.yAxisMax, + grid: { + color: darken(theme.colors.success.lighter, 0.2) + } + }, + y1: { + type: 'linear', + display: true, + position: 'right', + title: { + display: true, + text: 'PR Count', + color: theme.colors.info.main + }, + ticks: { + color: theme.colors.info.main, + stepSize: 1 + }, + grid: { + drawOnChartArea: false + } + } + }, + plugins: { + legend: { + display: true, + position: 'top' + }, + tooltip: { + callbacks: { + label: (context) => { + const label = context.dataset.label || ''; + const value = context.parsed.y; + if (label.includes('Duration')) { + return `${label}: ${value.toFixed(2)}h`; + } + return `${label}: ${value.toFixed(0)}`; + } + } + } + } + } + }} + /> +
+
+ ); +}; + +export default DoraMetricsTrend; diff --git a/web-server/src/content/PullRequests/DeploymentInsightsOverlay.tsx b/web-server/src/content/PullRequests/DeploymentInsightsOverlay.tsx index dbff882dd..dc9536add 100644 --- a/web-server/src/content/PullRequests/DeploymentInsightsOverlay.tsx +++ b/web-server/src/content/PullRequests/DeploymentInsightsOverlay.tsx @@ -1,4 +1,4 @@ -import { ArrowDownwardRounded, CodeRounded, AccessTimeRounded } from '@mui/icons-material'; +import { ArrowDownwardRounded } from '@mui/icons-material'; import { Card, Divider, useTheme, Collapse, Box } from '@mui/material'; import pluralize from 'pluralize'; import { ascend, descend, groupBy, path, prop, sort } from 'ramda'; @@ -14,7 +14,6 @@ import Scrollbar from '@/components/Scrollbar'; import { Tabs, TabItem } from '@/components/Tabs'; import { Line } from '@/components/Text'; import { FetchState } from '@/constants/ui-states'; -import { DoraMetricsComparisonPill } from '@/content/DoraMetrics/DoraMetricsComparisonPill'; import { useAuth } from '@/hooks/useAuth'; import { useBoolState, useEasyState } from '@/hooks/useEasyState'; import { @@ -32,8 +31,6 @@ import { useDispatch, useSelector } from '@/store'; import { Deployment, PR, RepoWorkflowExtended } from '@/types/resources'; import { percent } from '@/utils/datatype'; import { depFn } from '@/utils/fn'; -import { format } from 'date-fns'; -import { getDurationString } from '@/utils/date'; import { DeploymentItem } from './DeploymentItem'; @@ -292,7 +289,8 @@ export const DeploymentInsightsOverlay = () => { /> {activeTab === TabKeys.ANALYTICS ? ( - + + diff --git a/web-server/src/types/resources.ts b/web-server/src/types/resources.ts index 6b84ba6b2..6bdcd5748 100644 --- a/web-server/src/types/resources.ts +++ b/web-server/src/types/resources.ts @@ -223,7 +223,6 @@ export type Deployment = { conducted_at: DateString; pr_count: number; run_duration: number; - duration: number; html_url: string; repo_workflow_id: ID; }; From 9cdb98f7af87fc7238b392943d20f671036cdde3 Mon Sep 17 00:00:00 2001 From: harshit078 Date: Fri, 16 May 2025 15:57:33 +0530 Subject: [PATCH 4/9] Updated logic to handle DeploymentSources and added a check for both the case --- .../content/DoraMetrics/DoraMetricsTrend.tsx | 193 +++++++++++------- .../DeploymentInsightsOverlay.tsx | 2 +- 2 files changed, 123 insertions(+), 72 deletions(-) diff --git a/web-server/src/content/DoraMetrics/DoraMetricsTrend.tsx b/web-server/src/content/DoraMetrics/DoraMetricsTrend.tsx index 0a9eaf8a6..1d4b15bb7 100644 --- a/web-server/src/content/DoraMetrics/DoraMetricsTrend.tsx +++ b/web-server/src/content/DoraMetrics/DoraMetricsTrend.tsx @@ -22,8 +22,7 @@ interface TrendData { } const getDeploymentDurationInSeconds = (deployment: Deployment): number => { - - if (typeof deployment.run_duration === 'number' && deployment.run_duration > 0) { + if (deployment.id?.startsWith('WORKFLOW') && typeof deployment.run_duration === 'number' && deployment.run_duration > 0) { return deployment.run_duration; } @@ -44,7 +43,7 @@ const getDeploymentDurationInSeconds = (deployment: Deployment): number => { const getDeploymentDurationInHours = (deployment: Deployment): number => { const seconds = getDeploymentDurationInSeconds(deployment); - return +(seconds / 3600).toFixed(2); // Convert to hours and round to 2 decimal places + return +(seconds / 3600).toFixed(2); }; export const calculateDeploymentTrends = ( @@ -60,7 +59,25 @@ export const calculateDeploymentTrends = ( }; } - const sortedDeployments = [...deployments].sort( + // Filter valid deployments early + const validDeployments = deployments.filter(dep => { + const hasValidDates = dep.conducted_at && new Date(dep.conducted_at).toString() !== 'Invalid Date'; + + if (dep.id.startsWith('WORKFLOW')) { + return hasValidDates && typeof dep.run_duration === 'number'; + } + + return hasValidDates; + }); + + if (validDeployments.length < 2) { + return { + durationTrend: { value: 0, change: 0, state: 'neutral' }, + prCountTrend: { value: 0, change: 0, state: 'neutral' } + }; + } + + const sortedDeployments = [...validDeployments].sort( (a, b) => new Date(a.conducted_at).getTime() - new Date(b.conducted_at).getTime() ); @@ -69,7 +86,6 @@ export const calculateDeploymentTrends = ( const firstHalf = sortedDeployments.slice(0, midpoint); const secondHalf = sortedDeployments.slice(midpoint); - console.log(deployments) // Calculate average duration for each half const getAvgDuration = (deps: Deployment[]) => { @@ -89,8 +105,12 @@ export const calculateDeploymentTrends = ( const getAvgPrCount = (deps: Deployment[]): number => { if (!deps || deps.length === 0) return 0; - // Group deployments by date first - const deploymentsByDate = deps.reduce((acc, dep) => { + // Filter deployments that have valid PR count data + const depsWithPrCount = deps.filter(dep => dep.pr_count >= 0); + + if (depsWithPrCount.length === 0) return 0; + + const deploymentsByDate = depsWithPrCount.reduce((acc, dep) => { const date = new Date(dep.conducted_at).toLocaleDateString('en-US'); if (!acc[date]) { acc[date] = { totalPRs: 0, count: 0 }; @@ -101,10 +121,11 @@ export const calculateDeploymentTrends = ( }, {} as Record); const dailyTotals = Object.values(deploymentsByDate); - const totalPRs = dailyTotals.reduce((sum, day) => sum + day.totalPRs, 0); - const numberOfDays = dailyTotals.length; - - return numberOfDays > 0 ? totalPRs / numberOfDays : 0; + + const avgPrPerDay = dailyTotals.map(day => day.totalPRs / day.count); + const totalAvgPr = avgPrPerDay.reduce((sum, avg) => sum + avg, 0); + + return avgPrPerDay.length > 0 ? totalAvgPr / avgPrPerDay.length : 0; }; const firstHalfAvgPrCount = getAvgPrCount(firstHalf); @@ -183,7 +204,7 @@ export const DeploymentTrendPill: FC<{ return ( { }, [allDeployments]); const chartData = useMemo(() => { - const sortedDeployments = [...allDeployments].sort( + const validDeployments = allDeployments.filter(dep => { + const hasValidDates = dep.conducted_at && new Date(dep.conducted_at).toString() !== 'Invalid Date'; + + if (dep.id?.startsWith('WORKFLOW')) { + return hasValidDates && typeof dep.run_duration === 'number' && dep.run_duration >= 0; + } + + return hasValidDates && dep.created_at && new Date(dep.created_at).toString() !== 'Invalid Date'; + }); + + if (!validDeployments.length) { + return { labels: [], series: [], yAxisMax: 0 }; + } + + const sortedDeployments = [...validDeployments].sort( (a, b) => new Date(a.conducted_at).getTime() - new Date(b.conducted_at).getTime() ); @@ -234,21 +269,35 @@ export const DoraMetricsTrend: FC = () => { acc[date] = { deployments: [], totalDuration: 0, - totalPRs: 0 + totalPRs: 0, + prDeploymentCount: 0 }; } const durationInHours = getDeploymentDurationInHours(deployment); acc[date].deployments.push(deployment); acc[date].totalDuration += durationInHours; - acc[date].totalPRs += deployment.pr_count || 0; + + if (deployment.pr_count >= 0) { + acc[date].totalPRs += deployment.pr_count || 0; + acc[date].prDeploymentCount++; + } return acc; - }, {} as Record); + }, {} as Record); const dates = Object.keys(deploymentsByDate); const durations = dates.map(date => deploymentsByDate[date].totalDuration); - const prCounts = dates.map(date => deploymentsByDate[date].totalPRs); + + const prCounts = dates.map(date => { + const { totalPRs, prDeploymentCount } = deploymentsByDate[date]; + return prDeploymentCount > 0 ? totalPRs / prDeploymentCount : 0; + }); const maxDuration = Math.max(...durations); const yAxisMax = Math.ceil(maxDuration); @@ -297,71 +346,73 @@ export const DoraMetricsTrend: FC = () => { />
- 0 && ( + value + 'h' + position: 'left', + title: { + display: true, + text: 'Duration (hours)', + color: theme.colors.success.main + }, + ticks: { + color: theme.colors.success.main, + callback: (value) => value + 'h' + }, + max: chartData.yAxisMax, + grid: { + color: darken(theme.colors.success.lighter, 0.2) + } }, - max: chartData.yAxisMax, - grid: { - color: darken(theme.colors.success.lighter, 0.2) + y1: { + type: 'linear', + display: true, + position: 'right', + title: { + display: true, + text: 'PR Count', + color: theme.colors.info.main + }, + ticks: { + color: theme.colors.info.main, + stepSize: 1 + }, + grid: { + drawOnChartArea: false + } } }, - y1: { - type: 'linear', - display: true, - position: 'right', - title: { + plugins: { + legend: { display: true, - text: 'PR Count', - color: theme.colors.info.main - }, - ticks: { - color: theme.colors.info.main, - stepSize: 1 + position: 'top' }, - grid: { - drawOnChartArea: false - } - } - }, - plugins: { - legend: { - display: true, - position: 'top' - }, - tooltip: { - callbacks: { - label: (context) => { - const label = context.dataset.label || ''; - const value = context.parsed.y; - if (label.includes('Duration')) { - return `${label}: ${value.toFixed(2)}h`; + tooltip: { + callbacks: { + label: (context) => { + const label = context.dataset.label || ''; + const value = context.parsed.y; + if (label.includes('Duration')) { + return `${label}: ${value.toFixed(2)}h`; + } + return `${label}: ${value.toFixed(0)}`; } - return `${label}: ${value.toFixed(0)}`; } } } } - } - }} - /> + }} + /> + )}
); diff --git a/web-server/src/content/PullRequests/DeploymentInsightsOverlay.tsx b/web-server/src/content/PullRequests/DeploymentInsightsOverlay.tsx index dc9536add..88bb28084 100644 --- a/web-server/src/content/PullRequests/DeploymentInsightsOverlay.tsx +++ b/web-server/src/content/PullRequests/DeploymentInsightsOverlay.tsx @@ -357,7 +357,7 @@ export const DeploymentInsightsOverlay = () => { ) : ( <> - + From f9e85d239fc175afb7fda9f37e72d3eda108851a Mon Sep 17 00:00:00 2001 From: harshit078 Date: Fri, 23 May 2025 14:45:38 +0530 Subject: [PATCH 5/9] Addressed comments and updated chart label and border color --- .../content/DoraMetrics/DoraMetricsTrend.tsx | 229 ++++++++++-------- 1 file changed, 133 insertions(+), 96 deletions(-) diff --git a/web-server/src/content/DoraMetrics/DoraMetricsTrend.tsx b/web-server/src/content/DoraMetrics/DoraMetricsTrend.tsx index 1d4b15bb7..8fecf1a90 100644 --- a/web-server/src/content/DoraMetrics/DoraMetricsTrend.tsx +++ b/web-server/src/content/DoraMetrics/DoraMetricsTrend.tsx @@ -5,12 +5,13 @@ import { } from '@mui/icons-material'; import { darken, useTheme } from '@mui/material'; import { FC, useMemo } from 'react'; + +import { Chart2, ChartSeries } from '@/components/Chart2'; import { FlexBox } from '@/components/FlexBox'; import { Line } from '@/components/Text'; import { useSelector } from '@/store'; import { Deployment } from '@/types/resources'; import { percentageToMultiplier } from '@/utils/datatype'; -import { Chart2, ChartSeries } from '@/components/Chart2'; const MEANINGFUL_CHANGE_THRESHOLD = 0.5; @@ -22,11 +23,14 @@ interface TrendData { } const getDeploymentDurationInSeconds = (deployment: Deployment): number => { - if (deployment.id?.startsWith('WORKFLOW') && typeof deployment.run_duration === 'number' && deployment.run_duration > 0) { + if ( + deployment.id?.startsWith('WORKFLOW') && + typeof deployment.run_duration === 'number' && + deployment.run_duration > 0 + ) { return deployment.run_duration; } - - + try { const conductedAt = new Date(deployment.conducted_at); const createdAt = new Date(deployment.created_at); @@ -36,14 +40,14 @@ const getDeploymentDurationInSeconds = (deployment: Deployment): number => { const durationMs = conductedAt.getTime() - createdAt.getTime(); return Math.max(0, Math.floor(durationMs / 1000)); } catch (e) { - console.error("Error calculating deployment duration", e); + console.error('Error calculating deployment duration', e); return 0; } }; -const getDeploymentDurationInHours = (deployment: Deployment): number => { +const getDeploymentDurationInMinutes = (deployment: Deployment): number => { const seconds = getDeploymentDurationInSeconds(deployment); - return +(seconds / 3600).toFixed(2); + return +(seconds / 60).toFixed(2); }; export const calculateDeploymentTrends = ( @@ -60,13 +64,15 @@ export const calculateDeploymentTrends = ( } // Filter valid deployments early - const validDeployments = deployments.filter(dep => { - const hasValidDates = dep.conducted_at && new Date(dep.conducted_at).toString() !== 'Invalid Date'; + const validDeployments = deployments.filter((dep) => { + const hasValidDates = + dep.conducted_at && + new Date(dep.conducted_at).toString() !== 'Invalid Date'; if (dep.id.startsWith('WORKFLOW')) { return hasValidDates && typeof dep.run_duration === 'number'; } - + return hasValidDates; }); @@ -86,45 +92,53 @@ export const calculateDeploymentTrends = ( const firstHalf = sortedDeployments.slice(0, midpoint); const secondHalf = sortedDeployments.slice(midpoint); - // Calculate average duration for each half const getAvgDuration = (deps: Deployment[]) => { - const totalDuration = deps.reduce((sum, dep) => sum + getDeploymentDurationInSeconds(dep), 0); + const totalDuration = deps.reduce( + (sum, dep) => sum + getDeploymentDurationInSeconds(dep), + 0 + ); return deps.length > 0 ? totalDuration / deps.length : 0; }; const firstHalfAvgDuration = getAvgDuration(firstHalf); const secondHalfAvgDuration = getAvgDuration(secondHalf); - - const durationChange = firstHalfAvgDuration - ? ((secondHalfAvgDuration - firstHalfAvgDuration) / firstHalfAvgDuration) * 100 + + const durationChange = firstHalfAvgDuration + ? ((secondHalfAvgDuration - firstHalfAvgDuration) / firstHalfAvgDuration) * + 100 : 0; - + const avgDuration = getAvgDuration(sortedDeployments); const getAvgPrCount = (deps: Deployment[]): number => { if (!deps || deps.length === 0) return 0; - + // Filter deployments that have valid PR count data - const depsWithPrCount = deps.filter(dep => dep.pr_count >= 0); - + const depsWithPrCount = deps.filter((dep) => dep.pr_count >= 0); + + console.log('prCount', deployments[0]); + if (depsWithPrCount.length === 0) return 0; - - const deploymentsByDate = depsWithPrCount.reduce((acc, dep) => { - const date = new Date(dep.conducted_at).toLocaleDateString('en-US'); - if (!acc[date]) { - acc[date] = { totalPRs: 0, count: 0 }; - } - acc[date].totalPRs += dep.pr_count || 0; - acc[date].count++; - return acc; - }, {} as Record); - + + const deploymentsByDate = depsWithPrCount.reduce( + (acc, dep) => { + const date = new Date(dep.conducted_at).toLocaleDateString('en-US'); + if (!acc[date]) { + acc[date] = { totalPRs: 0, count: 0 }; + } + acc[date].totalPRs += dep.pr_count || 0; + acc[date].count++; + return acc; + }, + {} as Record + ); + const dailyTotals = Object.values(deploymentsByDate); - - const avgPrPerDay = dailyTotals.map(day => day.totalPRs / day.count); + + const avgPrPerDay = dailyTotals.map((day) => day.totalPRs / day.count); const totalAvgPr = avgPrPerDay.reduce((sum, avg) => sum + avg, 0); - + return avgPrPerDay.length > 0 ? totalAvgPr / avgPrPerDay.length : 0; }; @@ -139,7 +153,7 @@ export const calculateDeploymentTrends = ( return { durationTrend: { - value: avgDuration, + value: avgDuration / 60, change: durationChange, state: determineTrendState(durationChange, false) }, @@ -176,22 +190,24 @@ export const DeploymentTrendPill: FC<{ }> = ({ label, change, state }) => { const theme = useTheme(); - const text = ( - state === 'positive' ? 'Increasing ' + label : state === 'negative' ? 'Decreasing ' + label : 'Stable ' + label - ) + const text = + state === 'positive' + ? 'Increasing ' + label + : state === 'negative' + ? 'Decreasing ' + label + : 'Stable ' + label; const useMultiplierFormat = Math.abs(change) > 100; const formattedChange = useMultiplierFormat ? `${percentageToMultiplier(change)}` : `${Math.round(change)}%`; - const color = darken ( - state === 'positive' - ? theme.colors.success.main - : theme.colors.warning.main, - state === 'neutral' ? 0.5 : 0, - - ) + const color = darken( + state === 'positive' + ? theme.colors.success.main + : theme.colors.warning.main, + state === 'neutral' ? 0.5 : 0 + ); const icon = state === 'positive' ? ( @@ -215,10 +231,8 @@ export const DeploymentTrendPill: FC<{ > {text} - - - {formattedChange} - + + {formattedChange} {icon} @@ -241,14 +255,24 @@ export const DoraMetricsTrend: FC = () => { }, [allDeployments]); const chartData = useMemo(() => { - const validDeployments = allDeployments.filter(dep => { - const hasValidDates = dep.conducted_at && new Date(dep.conducted_at).toString() !== 'Invalid Date'; - + const validDeployments = allDeployments.filter((dep) => { + const hasValidDates = + dep.conducted_at && + new Date(dep.conducted_at).toString() !== 'Invalid Date'; + if (dep.id?.startsWith('WORKFLOW')) { - return hasValidDates && typeof dep.run_duration === 'number' && dep.run_duration >= 0; + return ( + hasValidDates && + typeof dep.run_duration === 'number' && + dep.run_duration >= 0 + ); } - - return hasValidDates && dep.created_at && new Date(dep.created_at).toString() !== 'Invalid Date'; + + return ( + hasValidDates && + dep.created_at && + new Date(dep.created_at).toString() !== 'Invalid Date' + ); }); if (!validDeployments.length) { @@ -256,45 +280,57 @@ export const DoraMetricsTrend: FC = () => { } const sortedDeployments = [...validDeployments].sort( - (a, b) => new Date(a.conducted_at).getTime() - new Date(b.conducted_at).getTime() + (a, b) => + new Date(a.conducted_at).getTime() - new Date(b.conducted_at).getTime() ); - const deploymentsByDate = sortedDeployments.reduce((acc, deployment) => { - const date = new Date(deployment.conducted_at).toLocaleDateString('en-US', { - day: 'numeric', - month: 'short' - }); - - if (!acc[date]) { - acc[date] = { - deployments: [], - totalDuration: 0, - totalPRs: 0, - prDeploymentCount: 0 - }; - } - - const durationInHours = getDeploymentDurationInHours(deployment); - acc[date].deployments.push(deployment); - acc[date].totalDuration += durationInHours; - - if (deployment.pr_count >= 0) { - acc[date].totalPRs += deployment.pr_count || 0; - acc[date].prDeploymentCount++; - } - - return acc; - }, {} as Record); + const deploymentsByDate = sortedDeployments.reduce( + (acc, deployment) => { + const date = new Date(deployment.conducted_at).toLocaleDateString( + 'en-US', + { + day: 'numeric', + month: 'short' + } + ); + + if (!acc[date]) { + acc[date] = { + deployments: [], + totalDuration: 0, + totalPRs: 0, + prDeploymentCount: 0 + }; + } + + const durationInMinutes = getDeploymentDurationInMinutes(deployment); + acc[date].deployments.push(deployment); + acc[date].totalDuration += durationInMinutes; + + if (deployment.pr_count >= 0) { + acc[date].totalPRs += deployment.pr_count || 0; + acc[date].prDeploymentCount++; + } + + return acc; + }, + {} as Record< + string, + { + deployments: Deployment[]; + totalDuration: number; + totalPRs: number; + prDeploymentCount: number; + } + > + ); const dates = Object.keys(deploymentsByDate); - const durations = dates.map(date => deploymentsByDate[date].totalDuration); - - const prCounts = dates.map(date => { + const durations = dates.map( + (date) => deploymentsByDate[date].totalDuration + ); + + const prCounts = dates.map((date) => { const { totalPRs, prDeploymentCount } = deploymentsByDate[date]; return prDeploymentCount > 0 ? totalPRs / prDeploymentCount : 0; }); @@ -305,11 +341,11 @@ export const DoraMetricsTrend: FC = () => { const series: ChartSeries = [ { type: 'bar', - label: 'Deployment Duration (hours)', + label: 'Deployment Duration (minutes)', data: durations, yAxisID: 'y', - borderColor: theme.colors.success.main, - order: 0 + order: 0, + color: 'white' }, { type: 'bar', @@ -319,7 +355,8 @@ export const DoraMetricsTrend: FC = () => { backgroundColor: theme.colors.info.main, borderWidth: 2, tension: 0.4, - order: 1 + order: 1, + color: 'white' } ]; @@ -361,12 +398,12 @@ export const DoraMetricsTrend: FC = () => { position: 'left', title: { display: true, - text: 'Duration (hours)', + text: 'Duration (minutes)', color: theme.colors.success.main }, ticks: { color: theme.colors.success.main, - callback: (value) => value + 'h' + callback: (value) => value + 'm' }, max: chartData.yAxisMax, grid: { @@ -402,7 +439,7 @@ export const DoraMetricsTrend: FC = () => { const label = context.dataset.label || ''; const value = context.parsed.y; if (label.includes('Duration')) { - return `${label}: ${value.toFixed(2)}h`; + return `${label}: ${value.toFixed(2)}m`; } return `${label}: ${value.toFixed(0)}`; } From 208faed2556dd8c84d72ca00131e7d065a1bb0f1 Mon Sep 17 00:00:00 2001 From: harshit078 Date: Fri, 30 May 2025 12:03:16 +0530 Subject: [PATCH 6/9] Added state to redirect the user to managa teams if PR_Merge --- .../DeploymentInsightsOverlay.tsx | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/web-server/src/content/PullRequests/DeploymentInsightsOverlay.tsx b/web-server/src/content/PullRequests/DeploymentInsightsOverlay.tsx index 88bb28084..0b1fe9368 100644 --- a/web-server/src/content/PullRequests/DeploymentInsightsOverlay.tsx +++ b/web-server/src/content/PullRequests/DeploymentInsightsOverlay.tsx @@ -31,6 +31,9 @@ import { useDispatch, useSelector } from '@/store'; import { Deployment, PR, RepoWorkflowExtended } from '@/types/resources'; import { percent } from '@/utils/datatype'; import { depFn } from '@/utils/fn'; +import Link from 'next/link'; +import { ROUTES } from '@/constants/routes'; +import { DeploymentSources } from '@/types/resources'; import { DeploymentItem } from './DeploymentItem'; @@ -195,6 +198,10 @@ export const DeploymentInsightsOverlay = () => { const dateRangeLabel = useCurrentDateRangeReactNode(); + // Determine if the selected repository uses PR_MERGE as its deployment source + const currentBaseRepo = selectedRepo.value ? teamDeployments.repos_map[selectedRepo.value] : null; + const isPRMergeSource = currentBaseRepo?.deployment_type === DeploymentSources.PR_MERGE; + if (!team) return Please select a team first...; return ( @@ -348,11 +355,26 @@ export const DeploymentInsightsOverlay = () => { />
- - - - - + {!isPRMergeSource ? ( + <> + + + + + + + ) : ( + + + Deployment trends are only available for repos with workflows as source.{' '} + + + Go to settings → + + + + + )}
) : ( <> From 1f84876d7a692d3dd1c7f271ed84b6b2c1bba0a7 Mon Sep 17 00:00:00 2001 From: harshit078 Date: Fri, 30 May 2025 15:36:54 +0530 Subject: [PATCH 7/9] Fixed CI failing test --- .../DoraMetrics/DoraMetricsDuration.tsx | 71 ++++++++++++------- .../DeploymentInsightsOverlay.tsx | 49 +++++++------ 2 files changed, 73 insertions(+), 47 deletions(-) diff --git a/web-server/src/content/DoraMetrics/DoraMetricsDuration.tsx b/web-server/src/content/DoraMetrics/DoraMetricsDuration.tsx index 05b95f439..938fe3324 100644 --- a/web-server/src/content/DoraMetrics/DoraMetricsDuration.tsx +++ b/web-server/src/content/DoraMetrics/DoraMetricsDuration.tsx @@ -1,6 +1,12 @@ -import { Card, Chip, Tooltip, useTheme } from '@mui/material'; +import { + CheckCircleRounded, + AccessTimeRounded, + OpenInNew, + Code, + ArrowForwardIosRounded +} from '@mui/icons-material'; +import { Card, Tooltip, useTheme } from '@mui/material'; import { FC, useMemo } from 'react'; -import { CheckCircleRounded, AccessTimeRounded, OpenInNew, Code, ArrowForwardIosRounded } from '@mui/icons-material'; import { FlexBox } from '@/components/FlexBox'; import { Line } from '@/components/Text'; @@ -9,7 +15,6 @@ import { getDurationString, isoDateString } from '@/utils/date'; type DeploymentCardType = 'Longest' | 'Shortest'; - interface DeploymentCardProps { deployment: Deployment; type: DeploymentCardType; @@ -19,7 +24,6 @@ interface DoraMetricsDurationProps { deployments: Deployment[]; } - const formatDeploymentDate = (dateString: string): string => { if (!dateString) return 'Unknown Date'; @@ -62,14 +66,16 @@ const DeploymentCard: FC = ({ deployment }) => { width: '20vw', maxWidth: '30vw', border: `1px solid ${theme.palette.primary.light}`, - borderRadius: 2, + borderRadius: 2 }} > - + - Run On {formatDeploymentDate(deployment.conducted_at)} + + Run On {formatDeploymentDate(deployment.conducted_at)} + = ({ deployment }) => { position: 'absolute', right: 0, top: '35%', - bottom: '35%', + bottom: '35%' }} fontSize="medium" /> @@ -115,14 +121,18 @@ const DeploymentCard: FC = ({ deployment }) => { ); }; -export const DoraMetricsDuration: FC = ({ deployments }) => { +export const DoraMetricsDuration: FC = ({ + deployments +}) => { const { longestDeployment, shortestDeployment } = useMemo(() => { if (!Array.isArray(deployments) || !deployments.length) { return { longestDeployment: null, shortestDeployment: null }; } const validDeployments = deployments - .filter((d): d is Deployment => Boolean(d.conducted_at && typeof d.run_duration === 'number')) + .filter((d): d is Deployment => + Boolean(d.conducted_at && typeof d.run_duration === 'number') + ) .filter((d) => d.run_duration >= 0); if (!validDeployments.length) { @@ -130,13 +140,22 @@ export const DoraMetricsDuration: FC = ({ deployments } // Function to calculate Longest and shortest deployments - const { longest, shortest } = validDeployments.reduce((acc, current) => ({ - longest: !acc.longest || current.run_duration > acc.longest.run_duration ? current : acc.longest, - shortest: !acc.shortest || current.run_duration < acc.shortest.run_duration ? current : acc.shortest - }), { - longest: null as Deployment | null, - shortest: null as Deployment | null - }); + const { longest, shortest } = validDeployments.reduce( + (acc, current) => ({ + longest: + !acc.longest || current.run_duration > acc.longest.run_duration + ? current + : acc.longest, + shortest: + !acc.shortest || current.run_duration < acc.shortest.run_duration + ? current + : acc.shortest + }), + { + longest: null as Deployment | null, + shortest: null as Deployment | null + } + ); return { longestDeployment: longest, shortestDeployment: shortest }; }, [deployments]); @@ -145,19 +164,17 @@ export const DoraMetricsDuration: FC = ({ deployments - Longest Deployment - + + Longest Deployment + + - Shortest Deployment - + + Shortest Deployment + + diff --git a/web-server/src/content/PullRequests/DeploymentInsightsOverlay.tsx b/web-server/src/content/PullRequests/DeploymentInsightsOverlay.tsx index 0b1fe9368..9ac41f4a2 100644 --- a/web-server/src/content/PullRequests/DeploymentInsightsOverlay.tsx +++ b/web-server/src/content/PullRequests/DeploymentInsightsOverlay.tsx @@ -1,10 +1,10 @@ import { ArrowDownwardRounded } from '@mui/icons-material'; import { Card, Divider, useTheme, Collapse, Box } from '@mui/material'; +import Link from 'next/link'; import pluralize from 'pluralize'; import { ascend, descend, groupBy, path, prop, sort } from 'ramda'; import { FC, useCallback, useEffect, useMemo, useState } from 'react'; -import { DoraMetricsTrend } from '../DoraMetrics/DoraMetricsTrend'; -import { DoraMetricsDuration } from '../DoraMetrics/DoraMetricsDuration'; + import { FlexBox } from '@/components/FlexBox'; import { MiniButton } from '@/components/MiniButton'; import { MiniCircularLoader } from '@/components/MiniLoader'; @@ -13,6 +13,7 @@ import { PullRequestsTableMini } from '@/components/PRTableMini/PullRequestsTabl import Scrollbar from '@/components/Scrollbar'; import { Tabs, TabItem } from '@/components/Tabs'; import { Line } from '@/components/Text'; +import { ROUTES } from '@/constants/routes'; import { FetchState } from '@/constants/ui-states'; import { useAuth } from '@/hooks/useAuth'; import { useBoolState, useEasyState } from '@/hooks/useEasyState'; @@ -29,14 +30,15 @@ import { } from '@/slices/dora_metrics'; import { useDispatch, useSelector } from '@/store'; import { Deployment, PR, RepoWorkflowExtended } from '@/types/resources'; +import { DeploymentSources } from '@/types/resources'; import { percent } from '@/utils/datatype'; import { depFn } from '@/utils/fn'; -import Link from 'next/link'; -import { ROUTES } from '@/constants/routes'; -import { DeploymentSources } from '@/types/resources'; import { DeploymentItem } from './DeploymentItem'; +import { DoraMetricsDuration } from '../DoraMetrics/DoraMetricsDuration'; +import { DoraMetricsTrend } from '../DoraMetrics/DoraMetricsTrend'; + enum DepStatusFilter { All, Pass, @@ -199,8 +201,11 @@ export const DeploymentInsightsOverlay = () => { const dateRangeLabel = useCurrentDateRangeReactNode(); // Determine if the selected repository uses PR_MERGE as its deployment source - const currentBaseRepo = selectedRepo.value ? teamDeployments.repos_map[selectedRepo.value] : null; - const isPRMergeSource = currentBaseRepo?.deployment_type === DeploymentSources.PR_MERGE; + const currentBaseRepo = selectedRepo.value + ? teamDeployments.repos_map[selectedRepo.value] + : null; + const isPRMergeSource = + currentBaseRepo?.deployment_type === DeploymentSources.PR_MERGE; if (!team) return Please select a team first...; @@ -226,10 +231,11 @@ export const DeploymentInsightsOverlay = () => { px={1} py={1 / 2} corner={theme.spacing(1)} - border={`1px solid ${repo.id === selectedRepo.value + border={`1px solid ${ + repo.id === selectedRepo.value ? theme.colors.info.main : theme.colors.secondary.light - }`} + }`} pointer bgcolor={ repo.id === selectedRepo.value @@ -241,9 +247,7 @@ export const DeploymentInsightsOverlay = () => { ? theme.colors.info.main : undefined } - fontWeight={ - repo.id === selectedRepo.value ? 'bold' : undefined - } + fontWeight={repo.id === selectedRepo.value ? 'bold' : undefined} onClick={() => { selectedRepo.set(repo.id as ID); }} @@ -297,7 +301,7 @@ export const DeploymentInsightsOverlay = () => { {activeTab === TabKeys.ANALYTICS ? ( - + @@ -338,10 +342,7 @@ export const DeploymentInsightsOverlay = () => { { ) : ( - Deployment trends are only available for repos with workflows as source.{' '} + Deployment trends are only available for repos with + workflows as source.{' '} - + Go to settings → @@ -379,7 +388,7 @@ export const DeploymentInsightsOverlay = () => { ) : ( <> - + From 0ed9d1bba960efe199f5ad56fdb247023e028ba0 Mon Sep 17 00:00:00 2001 From: harshit078 Date: Fri, 6 Jun 2025 19:54:12 +0530 Subject: [PATCH 8/9] Fixed esling check --- web-server/src/content/DoraMetrics/DoraMetricsTrend.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web-server/src/content/DoraMetrics/DoraMetricsTrend.tsx b/web-server/src/content/DoraMetrics/DoraMetricsTrend.tsx index 8fecf1a90..bbd678b84 100644 --- a/web-server/src/content/DoraMetrics/DoraMetricsTrend.tsx +++ b/web-server/src/content/DoraMetrics/DoraMetricsTrend.tsx @@ -194,8 +194,8 @@ export const DeploymentTrendPill: FC<{ state === 'positive' ? 'Increasing ' + label : state === 'negative' - ? 'Decreasing ' + label - : 'Stable ' + label; + ? 'Decreasing ' + label + : 'Stable ' + label; const useMultiplierFormat = Math.abs(change) > 100; const formattedChange = useMultiplierFormat From e90e189b90c94d8dd58ba73e6b7dc3182432497c Mon Sep 17 00:00:00 2001 From: harshit078 Date: Fri, 13 Jun 2025 17:18:47 +0530 Subject: [PATCH 9/9] fix: Removed stable logic and updated negative sign in Increasing --- .../content/DoraMetrics/DoraMetricsTrend.tsx | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/web-server/src/content/DoraMetrics/DoraMetricsTrend.tsx b/web-server/src/content/DoraMetrics/DoraMetricsTrend.tsx index bbd678b84..abc22cc98 100644 --- a/web-server/src/content/DoraMetrics/DoraMetricsTrend.tsx +++ b/web-server/src/content/DoraMetrics/DoraMetricsTrend.tsx @@ -191,16 +191,16 @@ export const DeploymentTrendPill: FC<{ const theme = useTheme(); const text = - state === 'positive' - ? 'Increasing ' + label - : state === 'negative' - ? 'Decreasing ' + label - : 'Stable ' + label; + state === 'neutral' + ? label + : state === 'positive' + ? 'Increasing ' + label + : 'Decreasing ' + label; const useMultiplierFormat = Math.abs(change) > 100; const formattedChange = useMultiplierFormat - ? `${percentageToMultiplier(change)}` - : `${Math.round(change)}%`; + ? `${percentageToMultiplier(Math.abs(change))}` + : `${Math.abs(Math.round(change))}%`; const color = darken( state === 'positive' @@ -220,8 +220,7 @@ export const DeploymentTrendPill: FC<{ return ( { Deployment Trend - +