diff --git a/web-server/pages/api/internal/team/[team_id]/dora_metrics.ts b/web-server/pages/api/internal/team/[team_id]/dora_metrics.ts index 09d3f52ef..7c01fb132 100644 --- a/web-server/pages/api/internal/team/[team_id]/dora_metrics.ts +++ b/web-server/pages/api/internal/team/[team_id]/dora_metrics.ts @@ -1,6 +1,7 @@ import { endOfDay, startOfDay } from 'date-fns'; import * as yup from 'yup'; +import { getTeamRepos } from '@/api/resources/team_repos'; import { Endpoint } from '@/api-helpers/global'; import { repoFiltersFromTeamProdBranches, @@ -17,8 +18,8 @@ import { } from '@/utils/cockpitMetricUtils'; import { isoDateString, getAggregateAndTrendsIntervalTime } from '@/utils/date'; -import { getAllTeamsReposProdBranchesForOrgAsMap } from './repo_branches'; import { getTeamLeadTimePRs } from './insights'; +import { getAllTeamsReposProdBranchesForOrgAsMap } from './repo_branches'; const pathSchema = yup.object().shape({ team_id: yup.string().uuid().required() @@ -86,7 +87,8 @@ endpoint.handle.GET(getSchema, async (req, res) => { meanTimeToRestoreResponse, changeFailureRateResponse, deploymentFrequencyResponse, - leadtimePrs + leadtimePrs, + teamRepos ] = await Promise.all([ fetchLeadTimeStats({ teamId, @@ -148,10 +150,10 @@ endpoint.handle.GET(getSchema, async (req, res) => { }), getTeamLeadTimePRs(teamId, from_date, to_date, prFilters).then( (r) => r.data - ) + ), + getTeamRepos(teamId) ]); - console.log('🚀 ~ endpoint.handle.GET ~ leadTimeResponse:', leadTimeResponse); return res.send({ lead_time_stats: leadTimeResponse.lead_time_stats, lead_time_trends: leadTimeResponse.lead_time_trends, @@ -167,7 +169,8 @@ endpoint.handle.GET(getSchema, async (req, res) => { deploymentFrequencyResponse.deployment_frequency_stats, deployment_frequency_trends: deploymentFrequencyResponse.deployment_frequency_trends, - lead_time_prs: leadtimePrs + lead_time_prs: leadtimePrs, + assigned_repos: teamRepos } as TeamDoraMetricsApiResponseType); }); diff --git a/web-server/pages/dora-metrics/index.tsx b/web-server/pages/dora-metrics/index.tsx index 25b41ae9c..1cf07c2f7 100644 --- a/web-server/pages/dora-metrics/index.tsx +++ b/web-server/pages/dora-metrics/index.tsx @@ -22,6 +22,7 @@ function Page() { } pageTitle="DORA metrics" isLoading={isLoading} + teamDateSelectorMode="single" > diff --git a/web-server/src/components/PageHeader.tsx b/web-server/src/components/PageHeader.tsx index a66591307..db1957ebd 100644 --- a/web-server/src/components/PageHeader.tsx +++ b/web-server/src/components/PageHeader.tsx @@ -20,6 +20,7 @@ import { FlexBox, FlexBoxProps } from './FlexBox'; import { Hotkey } from './Hotkey'; import { Logo } from './Logo/Logo'; import { Tabs } from './Tabs'; +import { TeamSelector } from './TeamSelector/TeamSelector'; import { Line } from './Text'; type SubRoute = { @@ -104,6 +105,9 @@ export const PageHeader: FC< <> + {teamDateSelectorMode && ( + + )} {selectBranch && } {additionalFilters?.map((filter, i) => ( {filter} diff --git a/web-server/src/components/TeamSelector/TeamPopover.tsx b/web-server/src/components/TeamSelector/TeamPopover.tsx new file mode 100644 index 000000000..7a312ba6d --- /dev/null +++ b/web-server/src/components/TeamSelector/TeamPopover.tsx @@ -0,0 +1,535 @@ +import { + CheckCircleOutlineRounded, + RadioButtonUnchecked, + RadioButtonChecked, + EditRounded, + SearchRounded, + ClearRounded +} from '@mui/icons-material'; +import { + alpha, + Box, + Button, + CircularProgress, + Divider, + InputAdornment, + MenuItem, + Popover, + Stack, + SxProps, + TextField, + Typography, + useTheme +} from '@mui/material'; +import Link from 'next/link'; +import pluralize from 'pluralize'; +import { + FC, + useCallback, + MutableRefObject, + Dispatch, + SetStateAction +} from 'react'; +import { useDispatch } from 'react-redux'; + +import { FlexBox, FlexBoxProps } from '@/components/FlexBox'; +import Scrollbar from '@/components/Scrollbar'; +import { MenuListWrapperSecondary } from '@/components/Shared'; +import IntegrationsData from '@/components/TeamSelector/integrations-data.svg'; +import TeamData from '@/components/TeamSelector/team-data.svg'; +import { Line } from '@/components/Text'; +import { track } from '@/constants/events'; +import { ROUTES } from '@/constants/routes'; +import { useActiveRouteEvent } from '@/hooks/useActiveRouteEvent'; +import { BoolState, useBoolState, useEasyState } from '@/hooks/useEasyState'; +import { useSingleTeamConfig } from '@/hooks/useStateTeamConfig'; +import { appSlice, updateTeamMemberDataSetting } from '@/slices/app'; +import { useSelector } from '@/store'; +import { Team } from '@/types/api/teams'; +import { UserWithAvatar } from '@/types/resources'; +import { homogenize } from '@/utils/datatype'; +import { depFn } from '@/utils/fn'; + +import { defaultPopoverProps } from './defaultPopoverProps'; + +export const TeamPopover: FC<{ + teamElRef: MutableRefObject; + teamsPop: BoolState; + showAllTeams: boolean; + loadingTeams: boolean; + isSingleMode: boolean; + hideTeamMemberFilter: boolean; + setShowAllTeams: Dispatch>; + teams: Team[]; + apiTeams: Team[]; + usersMap: Record; + setProdBranchNamesByTeamId: (teamId: string) => void; + closeOnSelect?: boolean; +}> = ({ + teamElRef, + teamsPop, + showAllTeams, + setShowAllTeams, + apiTeams, + teams, + loadingTeams, + setProdBranchNamesByTeamId, + isSingleMode, + hideTeamMemberFilter, + closeOnSelect +}) => { + const theme = useTheme(); + const { team } = useSingleTeamConfig(); + + const updatingTeamMemberFilter = useBoolState(); + + const isRoleEng = false; + const activeRouteEvent = useActiveRouteEvent('APP_TEAM_CHANGE_SINGLE'); + const dispatch = useDispatch(); + + const teamSearchFilter = useEasyState(''); + + const toggleTeamMemberFilter = useCallback( + (enabled: boolean) => { + if (!team?.id) return; + + depFn(updatingTeamMemberFilter.trackAsync, async () => + dispatch( + updateTeamMemberDataSetting({ + teamId: team?.id, + enabled + }) + ) + ); + }, + [dispatch, team?.id, updatingTeamMemberFilter.trackAsync] + ); + + const listFilteredBySearch = apiTeams.filter((team) => + teamSearchFilter.value + ? homogenize(team.name).includes(homogenize(teamSearchFilter.value)) + : true + ); + + const teamReposMap = useSelector((s) => s.app.teamsProdBranchMap); + + return ( + + + + + + + + Currently showing all teams + + + {loadingTeams && } + + + + } + justifyContent="stretch" + alignItems="stretch" + spacing={0} + height="100%" + > + + 3 ? '270px' : undefined} + > + {apiTeams.length ? ( + <> + {Boolean(apiTeams.length > 4 || teamSearchFilter.value) && ( + <> + + + + ), + endAdornment: ( + + + + ) + }} + sx={{ mb: 1 / 2 }} + value={teamSearchFilter.value} + onChange={teamSearchFilter.eventHandler} + /> + + {apiTeams.length} {pluralize('team', apiTeams.length)}{' '} + present + {listFilteredBySearch.length !== apiTeams.length + ? ` (${listFilteredBySearch.length} shown)` + : ''} + + + )} + {listFilteredBySearch.map((apiTeam) => { + const selected = teams.some( + (team) => team.id === apiTeam.id + ); + + return ( + { + dispatch(appSlice.actions.setSingleTeam([apiTeam])); + setProdBranchNamesByTeamId(apiTeam.id); + track(activeRouteEvent, { team: apiTeam }); + if (closeOnSelect) { + depFn(teamsPop.false); + } + }} + > + + {selected ? ( + + {!isSingleMode ? ( + + ) : ( + + )} + + ) : ( + + )} + + + + + + + {apiTeam.name} + + {!isRoleEng && ( + e.stopPropagation()} + title={`Edit team: ${apiTeam.name}`} + tooltipPlacement="right" + > + + + + + )} + + {teamReposMap[apiTeam.id]?.length ? ( + + {teamReposMap[apiTeam.id]?.length}{' '} + {pluralize( + 'repo', + teamReposMap[apiTeam.id]?.length || 0 + )} + + ) : ( + + No repos + + )} + + + + ); + })} + + ) : !loadingTeams ? ( + + No teams to show + + + + + ) : ( + 'We getting your teams together, but someone seems missing 🤔' + )} + + + + + {!hideTeamMemberFilter && ( + <> + + + + + + + + + } + justifyContent="stretch" + alignItems="stretch" + spacing={0} + height="100%" + > + + + { + team.member_filter_enabled && + toggleTeamMemberFilter(false); + }} + > + + + + + + + By Codebases/Projects + + Data for this team will be shown{' '} + for all its codebases/projects + + + Great for viewing team analytics +
+ without onboarding all members +
+
+
+ { + !team.member_filter_enabled && + toggleTeamMemberFilter(true); + }} + > + + + + + + + By Contributions/Assignees + + Data for this team will be shown{' '} + only for team members present in it + + + Great for{' '} + + monorepos + + , and repos or projects with multi-team contributors + + + +
+
+
+
+ + )} +
+
+ ); +}; + +const dataFilterMenuItemSx: SxProps = { + maxWidth: '300px', + borderRadius: 1, + flex: 1, + alignItems: 'flex-start', + overflow: 'hidden' +}; + +const DataFilterRadio: FC<{ checked: boolean; saving?: boolean }> = ({ + checked, + saving +}) => { + return ( + + {saving ? ( + + ) : checked ? ( + + + + ) : ( + + )} + + ); +}; + +const DataFilterMenuItem: typeof FlexBox = (props: FlexBoxProps) => { + return ( + + ); +}; + +const CustomLoadingButton = () => { + const theme = useTheme(); + return ( + + + + ); +}; diff --git a/web-server/src/components/TeamSelector/TeamSelector.tsx b/web-server/src/components/TeamSelector/TeamSelector.tsx new file mode 100644 index 000000000..df5d3866c --- /dev/null +++ b/web-server/src/components/TeamSelector/TeamSelector.tsx @@ -0,0 +1,145 @@ +import { + KeyboardArrowDownRounded, + AdjustRounded, + GroupWorkRounded, + WorkspacesOutlined, + TerminalOutlined +} from '@mui/icons-material'; +import { Box } from '@mui/material'; +import { useRouter } from 'next/router'; +import { FC, useRef } from 'react'; + +import { useAuth } from '@/hooks/useAuth'; +import { useBoolState } from '@/hooks/useEasyState'; +import { useSingleTeamConfig } from '@/hooks/useStateTeamConfig'; + +import { DatePopover } from './DatePopover'; +import { TeamPopover } from './TeamPopover'; +import { useTeamSelectorSetup } from './useTeamSelectorSetup'; + +import { FlexBox } from '../FlexBox'; +import { HeaderBtn } from '../HeaderBtn'; +import { LightTooltip } from '../Shared'; + +export type TeamSelectorModes = + | 'single' + | 'multiple' + | 'date-only' + | 'single-only' + | 'multiple-only'; + +export const TeamSelector: FC<{ + mode?: TeamSelectorModes; + closeOnSelect?: boolean; +}> = ({ mode = 'single', closeOnSelect = false }) => { + const teamElRef = useRef(null); + const dateElRef = useRef(null); + const teamsPop = useBoolState(false); + const datesPop = useBoolState(false); + const { org } = useAuth(); + const { + teams, + apiTeams, + dateRangeLabel, + teamsLabel, + usersMap, + hideDateSelector, + hideTeamSelector, + loadingTeams, + setRange, + setShowAllTeams, + dateRange, + isSingleMode, + showAllTeams, + setProdBranchNamesByTeamId + } = useTeamSelectorSetup({ mode }); + + const { team } = useSingleTeamConfig(); + + const router = useRouter(); + const hideTeamMemberFilter = true; + + if (!org) return null; + + return ( + <> + + {!hideTeamSelector && ( + + + {!isSingleMode ? ( + + ) : ( + + )} + + {!hideTeamMemberFilter && ( + + {team?.member_filter_enabled ? ( + + ) : ( + + )} + + )} + + } + endIcon={} + onClick={teamsPop.true} + sx={{ + minWidth: '220px', + '> .MuiButton-endIcon': { marginLeft: 'auto' } + }} + > + {teamsLabel} + + )} + {!hideDateSelector && ( + } + onClick={datesPop.true} + sx={{ minWidth: '220px', justifyContent: 'space-between' }} + > + {dateRangeLabel} + + )} + + + + + ); +}; diff --git a/web-server/src/components/TeamSelector/useTeamSelectorSetup.tsx b/web-server/src/components/TeamSelector/useTeamSelectorSetup.tsx index e759e532e..fee5d515e 100644 --- a/web-server/src/components/TeamSelector/useTeamSelectorSetup.tsx +++ b/web-server/src/components/TeamSelector/useTeamSelectorSetup.tsx @@ -110,6 +110,7 @@ export const useTeamSelectorSetup = ({ mode }: UseTeamSelectorSetupArgs) => { () => payload?.teams || ([] as Team[]), [payload?.teams] ); + const usersMap = useMemo( () => payload?.users || ([] as unknown as FetchTeamsResponse['users']), [payload?.users] @@ -131,9 +132,11 @@ export const useTeamSelectorSetup = ({ mode }: UseTeamSelectorSetupArgs) => { const teamsLabel = teams.length ? !isSingleMode ? `${teams.length} ${pluralize('team', teams.length)} selected` - : `${teams[0]?.name} (${teams[0]?.member_ids.length} ${pluralize( - 'member', - teams[0]?.member_ids.length + : `${teams[0]?.name} (${ + teamReposProdBranchMap[teams[0]?.id]?.length || 0 + } ${pluralize( + 'repo', + teamReposProdBranchMap[teams[0]?.id]?.length || 0 )})` : `Select ${!isSingleMode ? plural('Team') : 'Team'}`; diff --git a/web-server/src/content/DoraMetrics/DoraCards/ChangeFailureRateCard.tsx b/web-server/src/content/DoraMetrics/DoraCards/ChangeFailureRateCard.tsx index f23e2c3dc..b3e044bb6 100644 --- a/web-server/src/content/DoraMetrics/DoraCards/ChangeFailureRateCard.tsx +++ b/web-server/src/content/DoraMetrics/DoraCards/ChangeFailureRateCard.tsx @@ -60,9 +60,7 @@ export const ChangeFailureRateCard = () => { IntegrationGroup.CODE ); - const isIncidentProviderIntegrationEnabled = integrationSet.has( - IntegrationGroup.INCIDENT - ); + const isIncidentProviderIntegrationEnabled = true; const canShowIncidentsData = isCodeProviderIntegrationEnabled && isIncidentProviderIntegrationEnabled; diff --git a/web-server/src/content/DoraMetrics/DoraCards/ChangeTimeCard.tsx b/web-server/src/content/DoraMetrics/DoraCards/ChangeTimeCard.tsx index 1b1a094cf..919b7b270 100644 --- a/web-server/src/content/DoraMetrics/DoraCards/ChangeTimeCard.tsx +++ b/web-server/src/content/DoraMetrics/DoraCards/ChangeTimeCard.tsx @@ -8,7 +8,6 @@ import { darken, List, ListItem, - Stack, useTheme } from '@mui/material'; import Link from 'next/link'; @@ -18,21 +17,20 @@ import { useMemo } from 'react'; import { Chart2, ChartOptions } from '@/components/Chart2'; import { FlexBox } from '@/components/FlexBox'; import { useOverlayPage } from '@/components/OverlayPageContext'; -import { MiniSwitch } from '@/components/Shared'; import { Line } from '@/components/Text'; import { track } from '@/constants/events'; import { ROUTES } from '@/constants/routes'; import { isRoleLessThanEM } from '@/constants/useRoute'; -import { getTrendsDataFromArray } from '@/content/Cockpit/codeMetrics/shared'; import { CardRoot, NoDataImg } from '@/content/DoraMetrics/DoraCards/sharedComponents'; import { usePropsForChangeTimeCard } from '@/content/DoraMetrics/DoraCards/sharedHooks'; import { useAuth } from '@/hooks/useAuth'; +import { useSelector } from '@/store'; import { ChangeTimeModes } from '@/types/resources'; -import { mergeDateValueTupleArray } from '@/utils/array'; -import { getDurationString } from '@/utils/date'; +import { merge } from '@/utils/datatype'; +import { getDurationString, getSortedDatesAsArrayFromMap } from '@/utils/date'; import { getDoraLink } from '../../PullRequests/DeploymentFrequencyGraph'; import { @@ -74,18 +72,21 @@ export const ChangeTimeCard = () => { const { reposCountWithWorkflowConfigured, - isActiveModeSwitchDisabled, isSufficientDataAvailable, - activeModePrevTrendsData, - activeModeCurrentTrendsData, activeModeProps, isAllAssignedReposHaveDeploymentsConfigured, allAssignedRepos, reposWithNoDeploymentsConfigured, - prevChangeTime, - toggleActiveModeValue + prevChangeTime } = usePropsForChangeTimeCard(); + const [currentLeadTimeTrendsData, prevLeadTimeTrendsData] = useSelector( + (s) => [ + s.doraMetrics.metrics_summary?.lead_time_trends.current, + s.doraMetrics.metrics_summary?.lead_time_trends.previous + ] + ); + const isCodeProviderIntegrationEnabled = true; const showClassificationBadge = @@ -102,27 +103,25 @@ export const ChangeTimeCard = () => { ); + const mergedLeadTimeTrends = merge( + currentLeadTimeTrendsData, + prevLeadTimeTrendsData + ); + const series = useMemo( () => [ { label: 'Lead Time', fill: 'start', - data: getTrendsDataFromArray( - mergeDateValueTupleArray( - activeModePrevTrendsData, - activeModeCurrentTrendsData - ) - ).map((point) => point || 0), + data: getSortedDatesAsArrayFromMap(mergedLeadTimeTrends).map( + (key) => mergedLeadTimeTrends[key].lead_time + ), backgroundColor: activeModeProps.backgroundColor, borderColor: alpha(activeModeProps.backgroundColor, 0.5), lineTension: 0.2 } ], - [ - activeModeCurrentTrendsData, - activeModePrevTrendsData, - activeModeProps.backgroundColor - ] + [activeModeProps.backgroundColor, mergedLeadTimeTrends] ); return ( @@ -350,78 +349,6 @@ export const ChangeTimeCard = () => { {' '} {'->'} - - - Cycle Time - - - {!reposCountWithWorkflowConfigured ? ( - - - No assigned repos have deployment workflow - configured. - - {!isEng && ( - - - - )} - - ) : ( - <> - - No Lead Time data available - - )} - - ) - } - darkTip - > - -
- - - Lead Time - - ) : isCodeProviderIntegrationEnabled ? ( diff --git a/web-server/src/content/DoraMetrics/DoraCards/MeanTimeToRestoreCard.tsx b/web-server/src/content/DoraMetrics/DoraCards/MeanTimeToRestoreCard.tsx index 57541a937..55f7e7866 100644 --- a/web-server/src/content/DoraMetrics/DoraCards/MeanTimeToRestoreCard.tsx +++ b/web-server/src/content/DoraMetrics/DoraCards/MeanTimeToRestoreCard.tsx @@ -11,9 +11,7 @@ import { CardRoot, NoDataImg } from '@/content/DoraMetrics/DoraCards/sharedComponents'; -import { useAuth } from '@/hooks/useAuth'; import { useDoraMetricsGraph } from '@/hooks/useDoraMetricsGraph'; -import { IntegrationGroup } from '@/types/resources'; import { getDurationString } from '@/utils/date'; import { NoIncidentsLabel } from './NoIncidentsLabel'; @@ -48,13 +46,11 @@ const chartOptions = { } as ChartOptions; export const MeanTimeToRestoreCard = () => { - const { integrationSet } = useAuth(); const { isNoDataAvailable, ...meanTimeToRestoreProps } = useMeanTimeToRestoreProps(); + const { trendsSeriesMap } = useDoraMetricsGraph(); - const isIncidentProviderIntegrationEnabled = integrationSet.has( - IntegrationGroup.INCIDENT - ); + const isIncidentProviderIntegrationEnabled = true; const canShowMTRData = !isNoDataAvailable && isIncidentProviderIntegrationEnabled; diff --git a/web-server/src/content/DoraMetrics/DoraCards/WeeklyDeliveryVolumeCard.tsx b/web-server/src/content/DoraMetrics/DoraCards/WeeklyDeliveryVolumeCard.tsx index 80e3ccf4a..7836f7d4d 100644 --- a/web-server/src/content/DoraMetrics/DoraCards/WeeklyDeliveryVolumeCard.tsx +++ b/web-server/src/content/DoraMetrics/DoraCards/WeeklyDeliveryVolumeCard.tsx @@ -19,6 +19,7 @@ import { } from '@/hooks/useStateTeamConfig'; import { useSelector } from '@/store'; import { IntegrationGroup } from '@/types/resources'; +import { merge } from '@/utils/datatype'; import { getSortedDatesAsArrayFromMap } from '@/utils/date'; import { useAvgWeeklyDeploymentFrequency } from './sharedHooks'; @@ -56,15 +57,16 @@ export const WeeklyDeliveryVolumeCard = () => { const { integrationSet } = useAuth(); const dateRangeLabel = useCurrentDateRangeLabel(); const deploymentFrequencyProps = useAvgWeeklyDeploymentFrequency(); - const deploymentsConfigured = useSelector( - (s) => s.doraMetrics.deploymentsConfigured - ); + const deploymentsConfigured = true; const isCodeProviderIntegrationEnabled = integrationSet.has( IntegrationGroup.CODE ); - const weekDeliveryVolumeData = useSelector( - (s) => s.doraMetrics.metrics_summary?.deployment_frequency_trends || {} + const weekDeliveryVolumeData = useSelector((s) => + merge( + s.doraMetrics.metrics_summary?.deployment_frequency_trends.current, + s.doraMetrics.metrics_summary?.deployment_frequency_trends.previous + ) ); const totalDeployments = useSelector( diff --git a/web-server/src/content/DoraMetrics/DoraCards/sharedHooks.tsx b/web-server/src/content/DoraMetrics/DoraCards/sharedHooks.tsx index 8ea4b06a0..e447f8df1 100644 --- a/web-server/src/content/DoraMetrics/DoraCards/sharedHooks.tsx +++ b/web-server/src/content/DoraMetrics/DoraCards/sharedHooks.tsx @@ -1,5 +1,6 @@ import { useCallback, useEffect, useMemo } from 'react'; +import { Row } from '@/constants/db'; import { ChangeTimeThresholds, updatedDeploymentFrequencyThresholds @@ -20,18 +21,18 @@ export const useMeanTimeToRestoreProps = () => { const meanTimeToRestore = useSelector( (s) => s.doraMetrics.metrics_summary?.mean_time_to_restore_stats.current - .time_to_restore_average + .mean_time_to_recovery ); const currAvgTimeToRestore = useSelector( (s) => s.doraMetrics.metrics_summary?.mean_time_to_restore_stats.current - .time_to_restore_average || 0 + .mean_time_to_recovery || 0 ); const prevAvgTimeToRestore = useSelector( (s) => s.doraMetrics.metrics_summary?.mean_time_to_restore_stats.previous - .time_to_restore_average || 0 + .mean_time_to_recovery || 0 ); const incidents = useSelector( @@ -78,7 +79,7 @@ export const useMeanTimeToRestoreProps = () => { export const useLeadTimeProps = () => { const leadTime = useSelector( - (s) => s.doraMetrics.metrics_summary?.lead_time_stats.current_average + (s) => s.doraMetrics.metrics_summary?.lead_time_stats.current.lead_time ); return useMemo(() => { @@ -107,9 +108,7 @@ export const useLeadTimeProps = () => { export const useDoraStats = () => { const { integrationSet } = useAuth(); const leadTimeProps = useLeadTimeProps(); - const depsConfigured = useSelector( - (s) => s.doraMetrics.deploymentsConfigured - ); + const depsConfigured = true; const { count: df } = useAvgWeeklyDeploymentFrequency(); const { count: cfr } = useChangeFailureRateProps(); const { count: mttr, isNoDataAvailable } = useMeanTimeToRestoreProps(); @@ -136,12 +135,10 @@ export const usePropsForChangeTimeCard = () => { const allAssignedRepos = useSelector( (s) => s.doraMetrics.allReposAssignedToTeam ); - const reposWithWorkflowConfigured = useSelector( - (s) => s.doraMetrics.workflowConfiguredRepos - ); const prevLeadTime = useSelector( - (s) => s.doraMetrics.metrics_summary?.lead_time_stats.previous_average || 0 + (s) => + s.doraMetrics.metrics_summary?.lead_time_stats.previous.lead_time || 0 ); const [currLeadTimeTrendsData, prevLeadTimeTrendsData] = useSelector((s) => [ @@ -171,26 +168,17 @@ export const usePropsForChangeTimeCard = () => { const activeModeCurrentTrendsData = currLeadTimeTrendsData; - const isAllAssignedReposHaveDeploymentsConfigured = useSelector( - (s) => - s.doraMetrics.allReposAssignedToTeam.length === - s.doraMetrics.workflowConfiguredRepos.length - ); + const isAllAssignedReposHaveDeploymentsConfigured = true; - const reposWithNoDeploymentsConfigured = useMemo(() => { - const workflowConfiguredRepoIdsSet = new Set( - reposWithWorkflowConfigured.map((r) => r.id) - ); - return allAssignedRepos.filter( - (r) => !workflowConfiguredRepoIdsSet.has(r.id) - ); - }, [allAssignedRepos, reposWithWorkflowConfigured]); + const reposWithNoDeploymentsConfigured = [] as (Row<'TeamRepos'> & + Row<'OrgRepo'>)[]; const isShowingLeadTime = true; const isShowingCycleTime = false; const reposCountWithWorkflowConfigured = - allAssignedRepos.length - reposWithNoDeploymentsConfigured.length; + Number(allAssignedRepos?.length) - + Number(reposWithNoDeploymentsConfigured?.length); const isActiveModeSwitchDisabled = false; @@ -220,18 +208,15 @@ export const useAvgWeeklyDeploymentFrequency = () => { let avgDeploymentFrequency = useSelector( (s) => s.doraMetrics.metrics_summary?.deployment_frequency_stats.current - .avg_deployment_frequency || 0 + .avg_daily_deployment_frequency || 0 ); let prevAvgDeploymentFrequency = useSelector( (s) => s.doraMetrics.metrics_summary?.deployment_frequency_stats.previous - .avg_deployment_frequency || 0 + .avg_daily_deployment_frequency || 0 ); - const interval = useSelector( - (s) => - s.doraMetrics.metrics_summary?.deployment_frequency_stats.current.duration - ); + const interval = 'week'; const metricInterval = useMemo(() => { return { diff --git a/web-server/src/content/DoraMetrics/DoraMetricsBody.tsx b/web-server/src/content/DoraMetrics/DoraMetricsBody.tsx index a26852912..f2eacea67 100644 --- a/web-server/src/content/DoraMetrics/DoraMetricsBody.tsx +++ b/web-server/src/content/DoraMetrics/DoraMetricsBody.tsx @@ -50,29 +50,26 @@ export const DoraMetricsBody = () => { !s.doraMetrics.metrics_summary?.change_failure_rate_stats.current .change_failure_rate && !s.doraMetrics.metrics_summary?.mean_time_to_restore_stats.current - .time_to_restore_average && - !s.doraMetrics.metrics_summary?.lead_time_stats.current_average && + .incident_count && + !s.doraMetrics.metrics_summary?.lead_time_stats.current.lead_time && !s.doraMetrics.metrics_summary?.deployment_frequency_stats.current - .avg_deployment_frequency + .avg_daily_deployment_frequency ); useEffect(() => { if (!singleTeamId) return; dispatch( fetchTeamDoraMetrics({ - org_id: orgId, - team_id: singleTeamId, - from_date: dates.start, - to_date: dates.end, + orgId, + teamId: singleTeamId, + fromDate: dates.start, + toDate: dates.end, branches: activeBranchMode === ActiveBranchMode.PROD ? null : activeBranchMode === ActiveBranchMode.ALL ? '^' - : branches, - manager_teams_array: [ - { team_ids: [singleTeamId], manager_id: team?.manager_id || null } - ] + : branches }) ); }, [ diff --git a/web-server/src/hooks/useDoraMetricsGraph/index.tsx b/web-server/src/hooks/useDoraMetricsGraph/index.tsx index 5fbdc999c..91243900e 100644 --- a/web-server/src/hooks/useDoraMetricsGraph/index.tsx +++ b/web-server/src/hooks/useDoraMetricsGraph/index.tsx @@ -1,10 +1,9 @@ import { lighten, rgbToHex } from '@mui/material'; import { useMemo } from 'react'; -import { getTrendsDataFromArray } from '@/content/Cockpit/codeMetrics/shared'; import { useSelector } from '@/store'; import { brandColors } from '@/theme/schemes/theme'; -import { mergeDateValueTupleArray } from '@/utils/array'; +import { merge } from '@/utils/datatype'; import { getSortedDatesAsArrayFromMap } from '@/utils/date'; export const useDoraMetricsGraph = () => { @@ -12,86 +11,83 @@ export const useDoraMetricsGraph = () => { (s) => s.doraMetrics.metrics_summary?.lead_time_trends ); - const meanTimeToRestoreTrends = useSelector( - (s) => s.doraMetrics.metrics_summary?.mean_time_to_restore_trends - ); - const changeFailureRateTrends = useSelector( - (s) => s.doraMetrics.metrics_summary?.change_failure_rate_trends - ); + const meanTimeToRestoreTrends = useSelector((s) => ({ + ...s.doraMetrics.metrics_summary?.mean_time_to_restore_trends.current, + ...s.doraMetrics.metrics_summary?.mean_time_to_restore_trends.previous + })); + + const changeFailureRateTrends = useSelector((s) => ({ + ...s.doraMetrics.metrics_summary?.change_failure_rate_trends.current, + ...s.doraMetrics.metrics_summary?.change_failure_rate_trends.previous + })); const activeTrends = leadTimeTrends; + const mergedLeadTimeTrends = merge( + leadTimeTrends?.current, + leadTimeTrends?.previous + ); if (!activeTrends) return { trendsSeriesMap: null, yAxisLabels: [] }; const yAxisLabels = useMemo(() => { - const sprintLabels = - mergeDateValueTupleArray( - activeTrends.previous?.breakdown.first_response_time, - activeTrends.current?.breakdown.first_response_time - ).map((s) => s[0]) || []; - return sprintLabels; + return getSortedDatesAsArrayFromMap({ + ...activeTrends.previous, + ...activeTrends.current + }); }, [activeTrends]); const firstCommitToOpenTrendsData = useMemo( () => - getTrendsDataFromArray( - mergeDateValueTupleArray( - leadTimeTrends.previous?.breakdown.first_commit_to_open, - leadTimeTrends.current?.breakdown.first_commit_to_open - ) - ), - [leadTimeTrends] + getSortedDatesAsArrayFromMap(mergedLeadTimeTrends).map((key) => ({ + x: key, + y: mergedLeadTimeTrends[key].first_commit_to_open ?? 0 + })), + [mergedLeadTimeTrends] ); const firstResponseTimeTrendsData = useMemo( () => - getTrendsDataFromArray( - mergeDateValueTupleArray( - activeTrends.previous?.breakdown.first_response_time, - activeTrends.current?.breakdown.first_response_time - ) - ), - [activeTrends] + getSortedDatesAsArrayFromMap(mergedLeadTimeTrends).map((key) => ({ + x: key, + y: mergedLeadTimeTrends[key].first_response_time ?? 0 + })), + [mergedLeadTimeTrends] ); const reworkTimeTrendsData = useMemo( () => - getTrendsDataFromArray( - mergeDateValueTupleArray( - activeTrends.previous?.breakdown.rework_time, - activeTrends.current?.breakdown.rework_time - ) - ), - [activeTrends] + getSortedDatesAsArrayFromMap(mergedLeadTimeTrends).map((key) => ({ + x: key, + y: mergedLeadTimeTrends[key].rework_time ?? 0 + })), + [mergedLeadTimeTrends] ); const mergeTimeTrendsData = useMemo( () => - getTrendsDataFromArray( - mergeDateValueTupleArray( - activeTrends.previous?.breakdown.merge_time, - activeTrends.current?.breakdown.merge_time - ) - ), - [activeTrends] + getSortedDatesAsArrayFromMap(mergedLeadTimeTrends).map((key) => ({ + x: key, + y: mergedLeadTimeTrends[key].merge_time ?? 0 + })), + [mergedLeadTimeTrends] ); const deployTimeTrendsData = useMemo( () => - getTrendsDataFromArray( - mergeDateValueTupleArray( - leadTimeTrends.previous?.breakdown.merge_time, - leadTimeTrends.current?.breakdown.merge_time - ) - ), - [leadTimeTrends] + getSortedDatesAsArrayFromMap(mergedLeadTimeTrends).map((key) => ({ + x: key, + y: mergedLeadTimeTrends[key].merge_to_deploy ?? 0 + })), + [mergedLeadTimeTrends] ); const changeFailureRateTrendsData = useMemo( () => getSortedDatesAsArrayFromMap(changeFailureRateTrends).map((key) => ({ x: key, - y: Number(changeFailureRateTrends[key].percentage.toFixed(2) ?? 0) + y: Number( + changeFailureRateTrends[key].change_failure_rate?.toFixed(2) ?? 0 + ) })), [changeFailureRateTrends] ); @@ -100,7 +96,7 @@ export const useDoraMetricsGraph = () => { () => getSortedDatesAsArrayFromMap(meanTimeToRestoreTrends).map((key) => ({ x: key, - y: meanTimeToRestoreTrends[key] ?? 0 + y: meanTimeToRestoreTrends[key].mean_time_to_recovery ?? 0 })), [meanTimeToRestoreTrends] ); @@ -147,28 +143,17 @@ export const useDoraMetricsGraph = () => { y: point || 0 })) }, - totalCycleTimeTrends: { - id: `Total Cycle Time`, - color: rgbToHex(lighten(brandColors.ticketState.todo, 0.5)), - data: firstResponseTimeTrendsData.map((point, index) => ({ - x: yAxisLabels[index], - y: - (point || 0) + - (reworkTimeTrendsData[index] || 0) + - (mergeTimeTrendsData[index] || 0) - })) - }, totalLeadTimeTrends: { id: `Total Lead Time`, color: rgbToHex(lighten(brandColors.ticketState.todo, 0.5)), data: firstCommitToOpenTrendsData.map((point, index) => ({ x: yAxisLabels[index], y: - (point || 0) + - (firstResponseTimeTrendsData[index] || 0) + - (reworkTimeTrendsData[index] || 0) + - (mergeTimeTrendsData[index] || 0) + - (deployTimeTrendsData[index] || 0) + (point?.y || 0) + + (firstResponseTimeTrendsData[index]?.y || 0) + + (reworkTimeTrendsData[index]?.y || 0) + + (mergeTimeTrendsData[index]?.y || 0) + + (deployTimeTrendsData[index]?.y || 0) })) }, meanTimeToRestoreTrends: [ diff --git a/web-server/src/slices/dora_metrics.ts b/web-server/src/slices/dora_metrics.ts index 386481542..e81170878 100644 --- a/web-server/src/slices/dora_metrics.ts +++ b/web-server/src/slices/dora_metrics.ts @@ -3,6 +3,7 @@ import { createAsyncThunk } from '@reduxjs/toolkit'; import { omit } from 'ramda'; import { handleApi } from '@/api-helpers/axios-api-instance'; +import { Row } from '@/constants/db'; import { StateFetchConfig } from '@/types/redux'; import { Deployment, @@ -10,10 +11,7 @@ import { TeamDeploymentsApiResponse, DeploymentWithIncidents, IncidentsWithDeploymentResponseType, - RepoWithSingleWorkflow, - ManagerTeamsMap, RepoFilterConfig, - TeamDeploymentsConfigured, IncidentApiResponseType, ChangeTimeModes } from '@/types/resources'; @@ -31,10 +29,7 @@ export type State = StateFetchConfig<{ | 'deploymentsConfiguredForAllRepos' | 'deploymentsConfigured' >; - allReposAssignedToTeam: RepoWithSingleWorkflow[]; - workflowConfiguredRepos: RepoWithSingleWorkflow[]; - deploymentsConfigured: TeamDeploymentsConfigured['deployments_configured']; - deploymentsConfiguredForAllRepos: TeamDeploymentsConfigured['deployments_configured_for_all_repos']; + allReposAssignedToTeam: (Row<'TeamRepos'> & Row<'OrgRepo'>)[]; all_deployments: DeploymentWithIncidents[]; resolved_incidents: IncidentsWithDeploymentResponseType[]; team_deployments: TeamDeploymentsApiResponse; @@ -49,9 +44,6 @@ const initialState: State = { activeChangeTimeMode: ChangeTimeModes.CYCLE_TIME, metrics_summary: null, allReposAssignedToTeam: [], - workflowConfiguredRepos: [], - deploymentsConfigured: false, - deploymentsConfiguredForAllRepos: false, all_deployments: [], resolved_incidents: [], team_deployments: { @@ -96,11 +88,7 @@ export const doraMetricsSlice = createSlice({ ], action.payload ); - state.allReposAssignedToTeam = action.payload.allReposAssignedToTeam; - state.workflowConfiguredRepos = action.payload.workflowConfiguredRepos; - state.deploymentsConfigured = action.payload.deploymentsConfigured; - state.deploymentsConfiguredForAllRepos = - action.payload.deploymentsConfiguredForAllRepos; + state.allReposAssignedToTeam = action.payload.assigned_repos; } ); addFetchCasesToReducer( @@ -138,18 +126,20 @@ type DoraMetricsApiParamsType = { export const fetchTeamDoraMetrics = createAsyncThunk( 'dora_metrics/fetchTeamDoraMetrics', - async ( - params: DoraMetricsApiParamsType & { - org_id: ID; - manager_teams_array: ManagerTeamsMap[]; - } - ) => { + async (params: { + teamId: ID; + orgId: ID; + fromDate: Date; + toDate: Date; + branches: string; + }) => { return await handleApi( - `internal/team/${params.team_id}/dora_metrics`, + `internal/team/${params.teamId}/dora_metrics`, { params: { - ...params, - manager_teams_array: JSON.stringify(params.manager_teams_array) + org_id: params.orgId, + from_date: params.fromDate, + to_date: params.toDate } } ); diff --git a/web-server/src/types/resources.ts b/web-server/src/types/resources.ts index 9e03fbf5a..27ce0d44e 100644 --- a/web-server/src/types/resources.ts +++ b/web-server/src/types/resources.ts @@ -538,11 +538,7 @@ export type ChangeFailureRateApiResponse = { export type ChangeFailureRateTrendsApiResponse = Record< DateString, - { - percentage: number; - failed_deployments: number; - total_deployments: number; - } + ChangeFailureRateApiResponse >; export type TeamDoraMetricsApiResponseType = { @@ -579,6 +575,7 @@ export type TeamDoraMetricsApiResponseType = { previous: DeploymentFrequencyTrends; }; lead_time_prs: PR[]; + assigned_repos: (Row<'TeamRepos'> & Row<'OrgRepo'>)[]; }; export enum ActiveBranchMode {