diff --git a/web-server/pages/api/internal/team/[team_id]/get_incidents.ts b/web-server/pages/api/internal/team/[team_id]/get_incidents.ts
index 50103720d..010c78022 100644
--- a/web-server/pages/api/internal/team/[team_id]/get_incidents.ts
+++ b/web-server/pages/api/internal/team/[team_id]/get_incidents.ts
@@ -2,11 +2,13 @@ import { endOfDay, startOfDay } from 'date-fns';
import { isNil, reject } from 'ramda';
import * as yup from 'yup';
-import { getTeamPrs } from '@/api/internal/team/[team_id]/insights';
-import { getTeamRevertedPrs } from '@/api/internal/team/[team_id]/revert_prs';
+import { getAllTeamsReposProdBranchesForOrgAsMap } from '@/api/internal/team/[team_id]/repo_branches';
import { handleRequest } from '@/api-helpers/axios';
import { Endpoint } from '@/api-helpers/global';
-import { updatePrFilterParams } from '@/api-helpers/team';
+import {
+ updatePrFilterParams,
+ repoFiltersFromTeamProdBranches
+} from '@/api-helpers/team';
import { mockDeploymentsWithIncidents } from '@/mocks/incidents';
import {
DeploymentWithIncidents,
@@ -22,7 +24,8 @@ const getSchema = yup.object().shape({
from_date: yup.date().required(),
to_date: yup.date().required(),
branches: yup.string().optional().nullable(),
- repo_filters: yup.mixed().optional().nullable()
+ repo_filters: yup.mixed().optional().nullable(),
+ org_id: yup.string().uuid().required()
});
const endpoint = new Endpoint(pathSchema);
@@ -36,7 +39,8 @@ endpoint.handle.GET(getSchema, async (req, res) => {
from_date: rawFromDate,
to_date: rawToDate,
branches,
- repo_filters
+ repo_filters,
+ org_id
} = req.payload;
const from_date = startOfDay(new Date(rawFromDate));
const to_date = endOfDay(new Date(rawToDate));
@@ -50,22 +54,32 @@ endpoint.handle.GET(getSchema, async (req, res) => {
{ branches, repo_filters }
);
- const [deploymentsWithIncident, summaryPrs, revertedPrs] = await Promise.all([
- getTeamIncidentsWithDeployment({ team_id, from_date, to_date }),
- getTeamPrs({
- team_id,
- branches,
- from_date: from_date,
- to_date: to_date,
- repo_filters
- }).then((r) => r.data),
- getTeamRevertedPrs({ ...params, team_id })
- ]);
+ const teamProdBranchesMap =
+ await getAllTeamsReposProdBranchesForOrgAsMap(org_id);
+ const teamRepoFiltersMap =
+ repoFiltersFromTeamProdBranches(teamProdBranchesMap);
+ const pr_filter = await updatePrFilterParams(
+ team_id,
+ {},
+ {
+ branches: branches,
+ repo_filters: !branches ? teamRepoFiltersMap[team_id] : null
+ }
+ ).then(({ pr_filter }) => ({
+ pr_filter
+ }));
+
+ const deploymentsWithIncident = await getTeamIncidentsWithDeployment({
+ team_id,
+ from_date,
+ to_date,
+ pr_filter
+ });
return res.send({
deployments_with_incidents: deploymentsWithIncident,
- summary_prs: summaryPrs,
- revert_prs: revertedPrs
+ summary_prs: [],
+ revert_prs: []
} as IncidentApiResponseType);
});
@@ -73,6 +87,7 @@ export const getTeamIncidentsWithDeployment = async (params: {
team_id: ID;
from_date: DateString | Date;
to_date: DateString | Date;
+ pr_filter: any;
}) => {
const [from_time, to_time] = getWeekStartAndEndInterval(
params.from_date,
@@ -84,7 +99,8 @@ export const getTeamIncidentsWithDeployment = async (params: {
{
params: reject(isNil, {
from_time,
- to_time
+ to_time,
+ pr_filter: params.pr_filter
})
}
);
diff --git a/web-server/src/components/OverlayComponents/ChangeFailureRate.tsx b/web-server/src/components/OverlayComponents/ChangeFailureRate.tsx
new file mode 100644
index 000000000..91421eb1f
--- /dev/null
+++ b/web-server/src/components/OverlayComponents/ChangeFailureRate.tsx
@@ -0,0 +1,5 @@
+import { FlexBox } from '../FlexBox';
+
+export const ChangeFailureRate = () => {
+ return ChangeFailureRate;
+};
diff --git a/web-server/src/components/PRTable/PullRequestsTable.tsx b/web-server/src/components/PRTable/PullRequestsTable.tsx
index d439deed7..364cb2654 100644
--- a/web-server/src/components/PRTable/PullRequestsTable.tsx
+++ b/web-server/src/components/PRTable/PullRequestsTable.tsx
@@ -35,9 +35,6 @@ import { PullRequestTableColumnSelector } from '@/components/PRTable/PullRequest
import { DarkTooltip, LightTooltip } from '@/components/Shared';
import { Line } from '@/components/Text';
import { SearchInput } from '@/components/TicketsTableAddons/SearchInput';
-import { Integration } from '@/constants/integrations';
-import { ROUTES } from '@/constants/routes';
-import { useAuth } from '@/hooks/useAuth';
import { EasyState, useEasyState } from '@/hooks/useEasyState';
import { useTableSort } from '@/hooks/useTableSort';
import { DEFAULT_PR_TABLE_COLUMN_STATE_MAP } from '@/slices/app';
@@ -65,8 +62,6 @@ export const PullRequestsTable: FC<
} & Omit
> = ({ propPrs, selectionMenu, selectedPrIds, isPrSelectionEnabled }) => {
const theme = useTheme();
- const { integrationSet } = useAuth();
- const hasBitbucket = integrationSet.has(Integration.BITBUCKET);
const prTableColumnConfig = useSelector(
(s) => s.app.prTableColumnsConfig || DEFAULT_PR_TABLE_COLUMN_STATE_MAP
);
@@ -113,7 +108,6 @@ export const PullRequestsTable: FC<
}
}, [page, pageCount]);
- const { orgId } = useAuth();
const enableCsv = true;
return (
@@ -306,12 +300,7 @@ export const PullRequestsTable: FC<
arrow
title={
-
- {hasBitbucket
- ? pr.author.linked_user?.name ||
- pr.author.username
- : `@${pr.author.username}`}
-
+ {`@${pr.author.username}`}
{!pr.author.linked_user && (
User not added to Middleware
@@ -327,18 +316,8 @@ export const PullRequestsTable: FC<
width="100%"
>
@@ -494,8 +469,6 @@ const PrChangesTooltip: FC<{ pr: PR }> = ({ pr }) => {
const PrReviewersCell: FC<{ pr: PR }> = ({ pr }) => {
const theme = useTheme();
- const { user } = useAuth();
- const hasBitbucket = user?.integrations?.bitbucket;
return (
= ({ pr }) => {
-
- {hasBitbucket
- ? reviewer.linked_user?.name || reviewer.username
- : `@${reviewer.username}`}
-
+ {`@${reviewer.username}`}
{!reviewer.linked_user && (
User not added to Middleware
@@ -522,17 +491,8 @@ const PrReviewersCell: FC<{ pr: PR }> = ({ pr }) => {
}
key={reviewer.username}
- component={hasBitbucket ? 'div' : Link}
- href={
- reviewer.linked_user?.id
- ? `${
- ROUTES.COLLABORATE.INSIGHTS.USER.add(reviewer.linked_user.id)
- .PATH
- }`
- : hasBitbucket
- ? undefined
- : getGHAvatar(reviewer.username)
- }
+ component={Link}
+ href={getGHAvatar(reviewer.username)}
target="_blank"
fontWeight={500}
display="flex"
@@ -546,7 +506,7 @@ const PrReviewersCell: FC<{ pr: PR }> = ({ pr }) => {
boxShadow: `0 0 0 2px ${theme.colors.info.main}`
}
)}
- src={hasBitbucket ? undefined : getGHAvatar(reviewer.username)}
+ src={getGHAvatar(reviewer.username)}
/>
))}
diff --git a/web-server/src/constants/overlays.ts b/web-server/src/constants/overlays.ts
index 164844a1f..ff364435c 100644
--- a/web-server/src/constants/overlays.ts
+++ b/web-server/src/constants/overlays.ts
@@ -20,5 +20,20 @@ export const overlaysImportMap = {
import('@/content/PullRequests/DeploymentInsightsOverlay').then((c) => ({
default: c.DeploymentInsightsOverlay
}))
+ ),
+ change_failure_rate: lazy(() =>
+ import('@/components/OverlayComponents/ChangeFailureRate').then((c) => ({
+ default: c.ChangeFailureRate
+ }))
+ ),
+ all_incidents: lazy(() =>
+ import('@/content/DoraMetrics/Incidents').then((c) => ({
+ default: c.AllIncidentsBody
+ }))
+ ),
+ resolved_incidents: lazy(() =>
+ import('@/content/DoraMetrics/ResolvedIncidents').then((c) => ({
+ default: c.ResolvedIncidentsBody
+ }))
)
};
diff --git a/web-server/src/content/DoraMetrics/DeploymentWithIncidentsMenuItem.tsx b/web-server/src/content/DoraMetrics/DeploymentWithIncidentsMenuItem.tsx
new file mode 100644
index 000000000..7806463de
--- /dev/null
+++ b/web-server/src/content/DoraMetrics/DeploymentWithIncidentsMenuItem.tsx
@@ -0,0 +1,122 @@
+import { AccessTimeRounded, ArrowForwardRounded } from '@mui/icons-material';
+import BugReportOutlinedIcon from '@mui/icons-material/BugReportOutlined';
+import NearbyErrorIcon from '@mui/icons-material/NearbyError';
+import { Card, useTheme } from '@mui/material';
+import { format } from 'date-fns';
+import Link from 'next/link';
+import pluralize from 'pluralize';
+import { FC } from 'react';
+import { FaExternalLinkAlt } from 'react-icons/fa';
+
+import { FlexBox } from '@/components/FlexBox';
+import { Line } from '@/components/Text';
+import { DeploymentWithIncidents } from '@/types/resources';
+import { getDurationString } from '@/utils/date';
+import { OPEN_IN_NEW_TAB_PROPS } from '@/utils/url';
+
+export const DeploymentWithIncidentsMenuItem: FC<{
+ deployment: DeploymentWithIncidents;
+ selected: boolean;
+ onSelect: (dep: DeploymentWithIncidents) => any;
+}> = ({ deployment, selected, onSelect }) => {
+ const theme = useTheme();
+ const incidents = deployment.incidents;
+
+ return (
+ onSelect(deployment)}
+ >
+
+
+
+
+
+ Run on{' '}
+ {format(new Date(deployment.conducted_at), 'do, MMM - hh:mmaaa')}
+
+ {deployment.html_url && (
+
+
+
+
+
+ )}
+
+
+
+
+
+ {incidents.length} {pluralize('incident', incidents.length)}
+
+
+ {deployment.head_branch}
+
+
+
+
+
+ {getDurationString(deployment.run_duration)}
+
+
+
+
+ {onSelect && (
+
+ )}
+
+
+ );
+};
diff --git a/web-server/src/content/DoraMetrics/DoraCards/ChangeFailureRateCard.tsx b/web-server/src/content/DoraMetrics/DoraCards/ChangeFailureRateCard.tsx
index b3e044bb6..57ade914c 100644
--- a/web-server/src/content/DoraMetrics/DoraCards/ChangeFailureRateCard.tsx
+++ b/web-server/src/content/DoraMetrics/DoraCards/ChangeFailureRateCard.tsx
@@ -5,6 +5,7 @@ import { useMemo } from 'react';
import { Chart2, ChartOptions } from '@/components/Chart2';
import { FlexBox } from '@/components/FlexBox';
+import { useOverlayPage } from '@/components/OverlayPageContext';
import { Line } from '@/components/Text';
import { track } from '@/constants/events';
import {
@@ -102,6 +103,8 @@ export const ChangeFailureRateCard = () => {
changeFailureRateProps.avgWeeklyDeploymentFrequency &&
(changeFailureRateProps.count || prevChangeFailureRate)
);
+
+ const { addPage } = useOverlayPage();
return (
@@ -207,7 +210,12 @@ export const ChangeFailureRateCard = () => {
track('DORA_METRICS_SEE_DETAILS_CLICKED', {
viewed: 'CFR'
});
- return console.error('OVERLAY PENDING');
+ addPage({
+ page: {
+ title: 'Deployments with incidents',
+ ui: 'all_incidents'
+ }
+ });
}}
color={changeFailureRateProps.color}
>
diff --git a/web-server/src/content/DoraMetrics/DoraCards/MeanTimeToRestoreCard.tsx b/web-server/src/content/DoraMetrics/DoraCards/MeanTimeToRestoreCard.tsx
index 55f7e7866..9cd05a720 100644
--- a/web-server/src/content/DoraMetrics/DoraCards/MeanTimeToRestoreCard.tsx
+++ b/web-server/src/content/DoraMetrics/DoraCards/MeanTimeToRestoreCard.tsx
@@ -5,6 +5,7 @@ import { useMemo } from 'react';
import { Chart2, ChartOptions } from '@/components/Chart2';
import { FlexBox } from '@/components/FlexBox';
+import { useOverlayPage } from '@/components/OverlayPageContext';
import { Line } from '@/components/Text';
import { track } from '@/constants/events';
import {
@@ -75,6 +76,8 @@ export const MeanTimeToRestoreCard = () => {
]
);
+ const { addPage } = useOverlayPage();
+
return (
@@ -172,7 +175,12 @@ export const MeanTimeToRestoreCard = () => {
track('DORA_METRICS_SEE_DETAILS_CLICKED', {
viewed: 'MTR'
});
- return console.error('OVERLAY PENDING');
+ addPage({
+ page: {
+ title: 'Resolved Incidents',
+ ui: 'resolved_incidents'
+ }
+ });
}}
color={meanTimeToRestoreProps.color}
>
diff --git a/web-server/src/content/DoraMetrics/Incidents.tsx b/web-server/src/content/DoraMetrics/Incidents.tsx
new file mode 100644
index 000000000..931fa2916
--- /dev/null
+++ b/web-server/src/content/DoraMetrics/Incidents.tsx
@@ -0,0 +1,500 @@
+import { AccessTimeRounded } from '@mui/icons-material';
+import BugReportOutlinedIcon from '@mui/icons-material/BugReportOutlined';
+import { Box, Card, Chip, Divider, Link, Paper, useTheme } from '@mui/material';
+import { format } from 'date-fns';
+import pluralize from 'pluralize';
+import { head } from 'ramda';
+import { FC, useCallback, useEffect, useMemo } from 'react';
+import { FaExternalLinkAlt } from 'react-icons/fa';
+
+import { EmptyState } from '@/components/EmptyState';
+import { FlexBox } from '@/components/FlexBox';
+import { MiniLoader } from '@/components/MiniLoader';
+import { useOverlayPage } from '@/components/OverlayPageContext';
+import Scrollbar from '@/components/Scrollbar';
+import { LightTooltip } from '@/components/Shared';
+import { SimpleAvatar } from '@/components/SimpleAvatar';
+import { Line } from '@/components/Text';
+import { TrendsLineChart } from '@/components/TrendsLineChart';
+import { FetchState } from '@/constants/ui-states';
+import { DeploymentWithIncidentsMenuItem } from '@/content/DoraMetrics/DeploymentWithIncidentsMenuItem';
+import { RevertedPrs } from '@/content/PullRequests/PrsReverted';
+import { useAuth } from '@/hooks/useAuth';
+import { useDoraMetricsGraph } from '@/hooks/useDoraMetricsGraph';
+import { useEasyState } from '@/hooks/useEasyState';
+import {
+ useCurrentDateRangeReactNode,
+ useSingleTeamConfig,
+ useStateBranchConfig
+} from '@/hooks/useStateTeamConfig';
+import { fetchAllDeploymentsWithIncidents } from '@/slices/dora_metrics';
+import { useDispatch, useSelector } from '@/store';
+import { PrUser, DeploymentWithIncidents } from '@/types/resources';
+import { getDurationString } from '@/utils/date';
+import { formatAsPercent } from '@/utils/stringFormatting';
+import { OPEN_IN_NEW_TAB_PROPS } from '@/utils/url';
+import { getGHAvatar } from '@/utils/user';
+
+import { IncidentItemIcon } from './IncidentsMenuItem';
+
+import { SubHeader } from '../../components/WrapperComponents';
+
+export const AllIncidentsBody = () => {
+ const dispatch = useDispatch();
+ const { orgId } = useAuth();
+ const branches = useStateBranchConfig();
+ const { singleTeamId, dates, team, singleTeamProdBranchesConfig } =
+ useSingleTeamConfig();
+ const { addPage } = useOverlayPage();
+ const dateRangeLabel = useCurrentDateRangeReactNode();
+ const isLoading = useSelector(
+ (s) => s.doraMetrics.requests?.all_deployments === FetchState.REQUEST
+ );
+
+ const allDeployments = useSelector(
+ (s) => s.doraMetrics.all_deployments || []
+ );
+ const allPrs = useSelector((s) => s.doraMetrics.summary_prs);
+ const revertedPrs = useSelector((s) => s.doraMetrics.revert_prs);
+
+ const selectedDeploymentId = useEasyState(null);
+ const setSelectedDeploymentId = useCallback(
+ (selectedDeployment: DeploymentWithIncidents) => {
+ selectedDeploymentId.set(selectedDeployment.id);
+ },
+ [selectedDeploymentId]
+ );
+
+ const selectedDeployment = useMemo(
+ () =>
+ allDeployments.find(
+ (deployment) => deployment.id === selectedDeploymentId.value
+ ),
+ [allDeployments, selectedDeploymentId]
+ );
+ const filteredDeployments = useMemo(
+ () => allDeployments.filter((deployment) => deployment.incidents.length),
+ [allDeployments]
+ );
+
+ const fetchAllIncidentDetails = useCallback(() => {
+ if (!singleTeamId || !dates.start || !dates.end) return;
+
+ dispatch(
+ fetchAllDeploymentsWithIncidents({
+ team_id: singleTeamId,
+ from_date: dates.start,
+ to_date: dates.end,
+ branches,
+ repo_filters: singleTeamProdBranchesConfig,
+ org_id: orgId
+ })
+ );
+ }, [
+ branches,
+ dates.end,
+ dates.start,
+ dispatch,
+ orgId,
+ singleTeamId,
+ singleTeamProdBranchesConfig
+ ]);
+
+ useEffect(() => {
+ fetchAllIncidentDetails();
+ }, [
+ branches,
+ dates.end,
+ dates.start,
+ dispatch,
+ fetchAllIncidentDetails,
+ orgId,
+ singleTeamId,
+ singleTeamProdBranchesConfig
+ ]);
+
+ const { trendsSeriesMap } = useDoraMetricsGraph();
+ const isTrendSeriesAvailable = head(
+ trendsSeriesMap?.changeFailureRateTrends || []
+ )?.data?.length;
+
+ if (isLoading) return ;
+ if (!allDeployments.length || !isTrendSeriesAvailable)
+ return (
+
+
+ No resolved incidents found for {team.name}{' '}
+ from {dateRangeLabel}
+
+
+ );
+
+ return (
+
+
+ Change failure rate, across weeks
+
+ {isTrendSeriesAvailable ? (
+
+
+
+ ) : (
+ Not enough data to show trends.
+ )}
+
+
+ {Boolean(revertedPrs.length) && (
+ <>
+
+
+
+
+ >
+ )}
+
+ Out of{' '}
+ {
+ addPage({
+ page: {
+ title: 'Deployments insights',
+ ui: 'deployment_freq'
+ }
+ });
+ }}
+ color="info"
+ >
+
+ {allDeployments.length} total{' '}
+ {pluralize('deployment', allDeployments.length)}
+
+ {' '}
+ from {dateRangeLabel} across all data sources,{' '}
+ {filteredDeployments.length}{' '}
+ {pluralize('deployment', filteredDeployments.length)} may have led to
+ possible incidents.
+
+
+
+
+
+
+
+
+ {filteredDeployments?.map((deployment) => {
+ return (
+
+ );
+ })}
+
+
+
+
+
+
+
+
+ );
+};
+
+const SelectedIncidentDetails: FC<{
+ deploymentDetails: DeploymentWithIncidents;
+}> = ({ deploymentDetails }) => {
+ const theme = useTheme();
+
+ const incidents = deploymentDetails?.incidents;
+ const isAssigned =
+ deploymentDetails?.event_actor?.username ||
+ deploymentDetails?.event_actor?.linked_user;
+
+ if (!deploymentDetails)
+ return (
+
+
+ Select an deployment on the left
+
+
+ to view details of possible incidents that it may have led to
+
+
+ );
+
+ return (
+
+ Selected Deployment
+
+ {Boolean(deploymentDetails.pr_count) && (
+
+
+
+ {deploymentDetails.pr_count}{' '}
+ {deploymentDetails.pr_count === 1 ? 'PR' : 'PRs'}
+
+
+ )}
+
+
+ Run on{' '}
+ {format(
+ new Date(deploymentDetails.conducted_at),
+ 'do, MMM - hh:mmaaa'
+ )}
+
+
+
+ by{' '}
+ {deploymentDetails.event_actor?.linked_user?.name ||
+ `@${deploymentDetails.event_actor.username}`}{' '}
+
+
+
+
+
+
+
+
+
+
+ {getDurationString(deploymentDetails.run_duration)}
+
+
+ }
+ />
+
+ {deploymentDetails.html_url && (
+
+
+
+
+
+ )}
+
+
+
+ {incidents.map((incident) => (
+
+
+
+ Incident details
+
+
+
+
+ {isAssigned ? (
+
+ Assignee{' '}
+
+
+ ) : (
+
+ Unassigned
+
+ )}
+
+
+ }
+ variant="filled"
+ />
+
+ {incident.status} on{' '}
+ {format(
+ new Date(
+ incident.resolved_date ||
+ incident.acknowledged_date ||
+ incident.creation_date
+ ),
+ 'do MMMM'
+ )}
+
+ }
+ >
+
+
+ {incident.status}
+
+
+
+
+ }
+ variant="filled"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+ {incident.title}
+
+
+
+ {incident.summary}
+
+
+ ))}
+
+ );
+};
+
+const IncidentUserAvatar: FC<{
+ userDetails: PrUser;
+ size?: number;
+}> = ({ userDetails, size }) => {
+ const { org } = useAuth();
+ const theme = useTheme();
+ const hasGithub = org.integrations.github;
+ return (
+
+ {`@${userDetails.username}`}
+ {!userDetails.linked_user && (
+
+ User not added to Middleware
+
+ )}
+
+ }
+ >
+
+
+
+
+
+
+ );
+};
diff --git a/web-server/src/content/DoraMetrics/IncidentsMenuItem.tsx b/web-server/src/content/DoraMetrics/IncidentsMenuItem.tsx
new file mode 100644
index 000000000..30c5c5eb2
--- /dev/null
+++ b/web-server/src/content/DoraMetrics/IncidentsMenuItem.tsx
@@ -0,0 +1,159 @@
+import {
+ ArrowForwardRounded,
+ CheckCircleOutlineRounded,
+ HowToRegRounded,
+ WarningAmberRounded
+} from '@mui/icons-material';
+import CalendarMonthIcon from '@mui/icons-material/CalendarMonth';
+import { Box, Card, useTheme } from '@mui/material';
+import { format } from 'date-fns';
+import { FC, useMemo } from 'react';
+import ClampLines from 'react-clamp-lines';
+
+import { FlexBox } from '@/components/FlexBox';
+import { Line } from '@/components/Text';
+import {
+ IncidentStatus,
+ IncidentsWithDeploymentResponseType
+} from '@/types/resources';
+import { getDurationString } from '@/utils/date';
+
+export const IncidentsMenuItem: FC<{
+ incident: IncidentsWithDeploymentResponseType;
+ selectedIncidentId: ID;
+ clickHandler: (incident: IncidentsWithDeploymentResponseType) => void;
+ showIcon?: boolean;
+}> = ({ incident, selectedIncidentId, clickHandler, showIcon = true }) => {
+ const theme = useTheme();
+ const isSelected = incident.id === selectedIncidentId;
+
+ const updatedDate =
+ incident.resolved_date ||
+ incident.acknowledged_date ||
+ incident.creation_date;
+
+ const resolutionTime = useMemo(
+ () =>
+ incident.creation_date && incident.resolved_date
+ ? getDurationString(
+ (new Date(incident.resolved_date).getTime() -
+ new Date(incident.creation_date).getTime()) /
+ 1e3
+ )
+ : null,
+ [incident.creation_date, incident.resolved_date]
+ );
+
+ return (
+ clickHandler(incident)}
+ >
+
+
+
+
+
+ {format(new Date(updatedDate), 'do MMMM')}
+
+
+
+
+
+
+
+
+ {showIcon && (
+
+ {incident.status} on{' '}
+ {format(
+ new Date(
+ incident.resolved_date ||
+ incident.acknowledged_date ||
+ incident.creation_date
+ ),
+ 'do MMMM'
+ )}
+
+ }
+ >
+ {incident.status}
+
+
+ )}
+ {resolutionTime && (
+
+
+ Created:{' '}
+ {format(
+ new Date(incident.creation_date),
+ "do MMM, yyyy 'at' hh:mm:ss a"
+ )}
+
+ Resolved:{' '}
+ {format(
+ new Date(incident.resolved_date),
+ "do MMM, yyyy 'at' hh:mm:ss a"
+ )}
+
+
+ }
+ >
+ Resolved in {resolutionTime}
+
+ )}
+
+
+
+ );
+};
+
+export const IncidentItemIcon = ({ status }: { status: string }) => {
+ if (status === IncidentStatus.TRIGGERED)
+ return ;
+ if (status === IncidentStatus.ACKNOWLEDGED)
+ return ;
+ return ;
+};
diff --git a/web-server/src/content/DoraMetrics/ResolvedIncidents.tsx b/web-server/src/content/DoraMetrics/ResolvedIncidents.tsx
new file mode 100644
index 000000000..6bece0378
--- /dev/null
+++ b/web-server/src/content/DoraMetrics/ResolvedIncidents.tsx
@@ -0,0 +1,274 @@
+import { Box, Card, Chip, Divider, Link, Paper, useTheme } from '@mui/material';
+import { format } from 'date-fns';
+import { head } from 'ramda';
+import { FC, useCallback, useEffect, useMemo } from 'react';
+import { FaExternalLinkAlt } from 'react-icons/fa';
+
+import { EmptyState } from '@/components/EmptyState';
+import { FlexBox } from '@/components/FlexBox';
+import { MiniLoader } from '@/components/MiniLoader';
+import Scrollbar from '@/components/Scrollbar';
+import { LightTooltip } from '@/components/Shared';
+import { SimpleAvatar } from '@/components/SimpleAvatar';
+import { Line } from '@/components/Text';
+import { TrendsLineChart } from '@/components/TrendsLineChart';
+import { FetchState } from '@/constants/ui-states';
+import { useAuth } from '@/hooks/useAuth';
+import { useDoraMetricsGraph } from '@/hooks/useDoraMetricsGraph';
+import { useEasyState } from '@/hooks/useEasyState';
+import {
+ useCurrentDateRangeReactNode,
+ useSingleTeamConfig
+} from '@/hooks/useStateTeamConfig';
+import { fetchAllResolvedIncidents } from '@/slices/dora_metrics';
+import { useDispatch, useSelector } from '@/store';
+import {
+ IncidentStatus,
+ IncidentsWithDeploymentResponseType
+} from '@/types/resources';
+import { OPEN_IN_NEW_TAB_PROPS } from '@/utils/url';
+
+import { IncidentsMenuItem, IncidentItemIcon } from './IncidentsMenuItem';
+
+import { SubHeader } from '../../components/WrapperComponents';
+
+export const ResolvedIncidentsBody = () => {
+ const dispatch = useDispatch();
+ const { orgId } = useAuth();
+ const { singleTeamId, dates, team } = useSingleTeamConfig();
+
+ const dateRangeLabel = useCurrentDateRangeReactNode();
+ const isLoading = useSelector(
+ (s) => s.doraMetrics.requests?.resolved_incidents === FetchState.REQUEST
+ );
+
+ const incidents = useSelector((s) => s.doraMetrics.resolved_incidents || []);
+
+ const selectedIncidentId = useEasyState(null);
+ const incidentFilter = useEasyState(null);
+ const setSelectedIncidentId = useCallback(
+ (selectedIncident: IncidentsWithDeploymentResponseType) => {
+ selectedIncidentId.set(selectedIncident.id);
+ },
+ [selectedIncidentId]
+ );
+ const selectedIncident = useMemo(
+ () =>
+ incidents.find((incident) => incident.id === selectedIncidentId.value),
+ [incidents, selectedIncidentId]
+ );
+
+ const filteredIncidents = useMemo(
+ () =>
+ incidents.filter(
+ (incident) =>
+ incident.status === incidentFilter.value || !incidentFilter.value
+ ),
+ [incidentFilter.value, incidents]
+ );
+
+ useEffect(() => {
+ dispatch(
+ fetchAllResolvedIncidents({
+ team_id: singleTeamId,
+ from_date: dates.start,
+ to_date: dates.end
+ })
+ );
+ }, [dates.end, dates.start, dispatch, orgId, singleTeamId]);
+
+ const { trendsSeriesMap } = useDoraMetricsGraph();
+ const isTrendsSeriesDataAvailable = head(
+ trendsSeriesMap.meanTimeToRestoreTrends
+ ).data.length;
+
+ if (isLoading || !team) return ;
+ if (!incidents.length)
+ return (
+
+
+ No resolved incidents found for {team.name}{' '}
+ from {dateRangeLabel}
+
+
+ );
+
+ return (
+
+
+ Mean time to recovery, across weeks
+
+ {isTrendsSeriesDataAvailable ? (
+
+
+
+ ) : (
+ Not enough data to show trends.
+ )}
+
+
+
+ List of all incidents resolved from {dateRangeLabel} across all data
+ sources.
+
+
+
+
+
+
+
+ {filteredIncidents?.map((incident) => {
+ return (
+
+ );
+ })}
+
+
+
+
+
+
+
+ );
+};
+
+const SelectedIncidentDetails: FC<{
+ incident: IncidentsWithDeploymentResponseType;
+}> = ({ incident }) => {
+ const theme = useTheme();
+ if (!incident)
+ return (
+
+
+ Select an incident on the left
+
+
+ to view details including in the possible deployment
+
+
+ );
+ const isAssigned =
+ incident.assigned_to.linked_user?.name || incident.assigned_to.username;
+ return (
+
+
+
+
+ Incident details
+
+
+
+ Assigned to{' '}
+ {incident.assigned_to.linked_user?.name ||
+ incident.assigned_to.username ||
+ 'No-one'}
+
+ }
+ >
+
+ {isAssigned ? (
+
+ Assignee{' '}
+
+
+ ) : (
+
+ Unassigned
+
+ )}
+
+
+ }
+ variant="filled"
+ />
+
+ {incident.status} on{' '}
+ {format(
+ new Date(
+ incident.resolved_date ||
+ incident.acknowledged_date ||
+ incident.creation_date
+ ),
+ 'do MMMM'
+ )}
+
+ }
+ >
+
+
+ {incident.status}
+
+
+
+
+ }
+ variant="filled"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+ {incident.title}
+
+
+
+ {incident.summary}
+
+
+
+ );
+};
diff --git a/web-server/src/content/PullRequests/PrsReverted.tsx b/web-server/src/content/PullRequests/PrsReverted.tsx
new file mode 100644
index 000000000..c3c8f138f
--- /dev/null
+++ b/web-server/src/content/PullRequests/PrsReverted.tsx
@@ -0,0 +1,144 @@
+import { CheckCircleOutlined, WarningAmberRounded } from '@mui/icons-material';
+import { emphasize, lighten } from '@mui/material';
+import { FC, useMemo, useCallback } from 'react';
+
+import { Chart2 } from '@/components/Chart2';
+import { FlexBox } from '@/components/FlexBox';
+import { PrTableWithPrExclusionMenu } from '@/components/PRTable/PrTableWithPrExclusionMenu';
+import { Line } from '@/components/Text';
+import { LineProps } from '@/components/Text/index';
+import { useModal } from '@/contexts/ModalContext';
+import { PR } from '@/types/resources';
+import { percent } from '@/utils/datatype';
+
+export const RevertedPrs: FC<{
+ id: string;
+ prs: PR[];
+ revertedPrs: PR[];
+ titleProps?: LineProps;
+ prUpdateCallback?: () => void;
+}> = ({ id, prs, revertedPrs, titleProps, prUpdateCallback }) => {
+ const { addModal } = useModal();
+
+ const series = useMemo(
+ () =>
+ [
+ {
+ data: revertedPrs.length,
+ color: emphasize('#F8BBD0', 0.1),
+ label: 'Reverted'
+ },
+ {
+ data: prs.length,
+ color: lighten('#80DEEA', 0.2),
+ label: 'Merged'
+ }
+ ].filter((serie) => serie.data),
+ [prs.length, revertedPrs.length]
+ );
+ const [revertedSeries, revertedColors, revertedLabels, revertedPrCount] =
+ useMemo(
+ () => [
+ series.map((s) => s.data),
+ series.map((s) => s.color),
+ series.map((s) => s.label),
+ revertedPrs.length
+ ],
+ [revertedPrs.length, series]
+ );
+
+ const openRevertedPrsModal = useCallback(async () => {
+ addModal({
+ title: `Reverted PRs`,
+ body: (
+
+ ),
+ showCloseIcon: true
+ });
+ }, [addModal, prUpdateCallback, revertedPrs]);
+
+ if (!prs.length) return null;
+
+ return (
+
+
+
+ PRs reverted
+
+
+ These were merged to mitigate bugs from recent PRs
+
+
+
+
+
+
+ {Boolean(revertedPrs.length) ? (
+
+
+
+ {prs.length === revertedPrs.length ? 'All ' : ''}
+ {revertedPrCount} {revertedPrCount > 1 ? 'PRs' : 'PR'}
+ {percent(revertedPrCount, prs.length)
+ ? ` (${percent(revertedPrCount, prs.length)}%)`
+ : ''}
+
+
+ reverted after merge
+
+
+ see details
+
+
+ ) : (
+
+
+
+ No {prs.length > 1 ? ' PRs' : ' PR'}
+
+
+ reverted after merge
+
+
+ )}
+
+
+ );
+};
diff --git a/web-server/src/hooks/useDoraMetricsGraph/index.tsx b/web-server/src/hooks/useDoraMetricsGraph/index.tsx
index d24e648ea..ca444e65d 100644
--- a/web-server/src/hooks/useDoraMetricsGraph/index.tsx
+++ b/web-server/src/hooks/useDoraMetricsGraph/index.tsx
@@ -27,12 +27,10 @@ export const useDoraMetricsGraph = () => {
leadTimeTrends?.previous
);
- if (!activeTrends) return { trendsSeriesMap: null, yAxisLabels: [] };
-
const yAxisLabels = useMemo(() => {
return getSortedDatesAsArrayFromMap({
- ...activeTrends.previous,
- ...activeTrends.current
+ ...(activeTrends?.previous || {}),
+ ...(activeTrends?.current || {})
});
}, [activeTrends]);
@@ -183,6 +181,8 @@ export const useDoraMetricsGraph = () => {
]
);
+ if (!activeTrends) return { trendsSeriesMap: null, yAxisLabels: [] };
+
return {
trendsSeriesMap,
yAxisLabels
diff --git a/web-server/src/slices/dora_metrics.ts b/web-server/src/slices/dora_metrics.ts
index a16c55f88..5f662fbe9 100644
--- a/web-server/src/slices/dora_metrics.ts
+++ b/web-server/src/slices/dora_metrics.ts
@@ -130,7 +130,7 @@ export const doraMetricsSlice = createSlice({
});
type DoraMetricsApiParamsType = {
- team_id: string;
+ team_id: ID;
from_date: Date;
to_date: Date;
branches?: string;
@@ -161,7 +161,7 @@ export const fetchTeamDoraMetrics = createAsyncThunk(
export const fetchAllDeploymentsWithIncidents = createAsyncThunk(
'dora_metrics/fetchAllIncidents',
- async (params: DoraMetricsApiParamsType) => {
+ async (params: DoraMetricsApiParamsType & { org_id: ID }) => {
return await handleApi(
`internal/team/${params.team_id}/get_incidents`,
{
diff --git a/web-server/src/utils/stringFormatting.ts b/web-server/src/utils/stringFormatting.ts
new file mode 100644
index 000000000..9bc08d28c
--- /dev/null
+++ b/web-server/src/utils/stringFormatting.ts
@@ -0,0 +1,37 @@
+import { DatumValue } from '@nivo/core';
+import pluralize from 'pluralize';
+export const trimWithEllipsis = (
+ text: string,
+ maxTextLength: number,
+ addInStart?: boolean
+) => {
+ const diff = text.length - maxTextLength;
+ if (diff <= 3) return text;
+ const textStr = addInStart
+ ? `...${text.slice(text.length - maxTextLength)}`
+ : `${text.slice(0, maxTextLength)}...`;
+ return textStr;
+};
+
+export const pluralizePrCount = (value: number) =>
+ `${value === 1 ? 'PR' : 'PRs'}`;
+
+export const formatAsPercent = (value: DatumValue) =>
+ value ? `${value}%` : `0%`;
+
+export const formatAsDeployment = (value: number) =>
+ value >= 1000
+ ? `${value / 1000}k Deps`
+ : `${value} ${pluralize('deps', value)}`;
+
+export const joinNames = (names: string[]): string => {
+ if (names.length === 0) {
+ return '';
+ } else if (names.length === 1) {
+ return names[0];
+ } else {
+ const lastNames = names.slice(-1);
+ const otherNames = names.slice(0, -1);
+ return `${otherNames.join(', ')} and ${lastNames[0]}`;
+ }
+};