Files
2024-02-15 17:01:50 +00:00

236 lines
6.2 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {Fragment} from 'react';
import {css} from '@emotion/react';
import styled from '@emotion/styled';
import moment from 'moment';
import {BarChart} from 'sentry/components/charts/barChart';
import Count from 'sentry/components/count';
import ProjectBadge from 'sentry/components/idBadge/projectBadge';
import Link from 'sentry/components/links/link';
import LoadingError from 'sentry/components/loadingError';
import PanelTable from 'sentry/components/panels/panelTable';
import Placeholder from 'sentry/components/placeholder';
import TimeSince from 'sentry/components/timeSince';
import {IconArrow} from 'sentry/icons';
import {t} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import type {Group, Organization} from 'sentry/types';
import {getTitle} from 'sentry/utils/events';
import {useApiQuery} from 'sentry/utils/queryClient';
interface TeamIssuesAgeProps {
organization: Organization;
teamSlug: string;
}
/**
* takes "< 1 hour" and returns a datetime of 1 hour ago
*/
function parseBucket(bucket: string): number {
if (bucket === '> 1 year') {
return moment().subtract(1, 'y').subtract(1, 'd').valueOf();
}
const [_, num, unit] = bucket.split(' ');
return moment()
.subtract(num, unit as any)
.valueOf();
}
const bucketLabels = {
'< 1 hour': t('1 hour'),
'< 4 hour': t('4 hours'),
'< 12 hour': t('12 hours'),
'< 1 day': t('1 day'),
'< 1 week': t('1 week'),
'< 4 week': t('1 month'),
'< 24 week': t('6 months'),
'< 1 year': t('1 year'),
'> 1 year': t('> 1 year'),
};
function TeamIssuesAge({organization, teamSlug}: TeamIssuesAgeProps) {
const {
data: oldestIssues,
isLoading: isOldestIssuesLoading,
isError: isOldestIssuesError,
refetch: refetchOldestIssues,
} = useApiQuery<Group[]>(
[
`/teams/${organization.slug}/${teamSlug}/issues/old/`,
{
query: {
limit: 7,
},
},
],
{staleTime: 5000}
);
const {
data: unresolvedIssueAge,
isLoading: isUnresolvedIssueAgeLoading,
isError: isUnresolvedIssueAgeError,
refetch: refetchUnresolvedIssueAge,
} = useApiQuery<Record<string, number>>(
[`/teams/${organization.slug}/${teamSlug}/unresolved-issue-age/`],
{staleTime: 5000}
);
const isLoading = isOldestIssuesLoading || isUnresolvedIssueAgeLoading;
if (isOldestIssuesError || isUnresolvedIssueAgeError) {
return (
<LoadingError
onRetry={() => {
refetchOldestIssues();
refetchUnresolvedIssueAge();
}}
/>
);
}
const seriesData = Object.entries(unresolvedIssueAge ?? {})
.map(([bucket, value]) => ({
name: bucket,
value,
}))
.sort((a, b) => parseBucket(b.name) - parseBucket(a.name));
return (
<div>
<ChartWrapper>
{isLoading && <Placeholder height="200px" />}
{!isLoading && (
<BarChart
style={{height: 190}}
legend={{right: 3, top: 0}}
yAxis={{minInterval: 1}}
xAxis={{
type: 'category',
min: 0,
axisLabel: {
showMaxLabel: true,
showMinLabel: true,
formatter: (bucket: string) => {
return bucketLabels[bucket] ?? bucket;
},
},
}}
series={[
{
seriesName: t('Unresolved Issues'),
silent: true,
data: seriesData,
barCategoryGap: '5%',
},
]}
/>
)}
</ChartWrapper>
<StyledPanelTable
isEmpty={!oldestIssues || oldestIssues?.length === 0}
emptyMessage={t('No unresolved issues for this team’s projects')}
headers={[
t('Oldest Issues'),
<RightAligned key="events">{t('Events')}</RightAligned>,
<RightAligned key="users">{t('Users')}</RightAligned>,
<RightAligned key="age">
{t('Age')} <IconArrow direction="down" size="xs" color="gray300" />
</RightAligned>,
]}
isLoading={isLoading}
>
{oldestIssues?.map(issue => {
const {title} = getTitle(issue, organization?.features, false);
return (
<Fragment key={issue.id}>
<ProjectTitleContainer>
<ShadowlessProjectBadge
disableLink
hideName
avatarSize={18}
project={issue.project}
/>
<TitleOverflow>
<Link
to={{
pathname: `/organizations/${organization.slug}/issues/${issue.id}/`,
}}
>
{title}
</Link>
</TitleOverflow>
</ProjectTitleContainer>
<RightAligned>
<Count value={issue.count} />
</RightAligned>
<RightAligned>
<Count value={issue.userCount} />
</RightAligned>
<RightAligned>
<TimeSince date={issue.firstSeen} />
</RightAligned>
</Fragment>
);
})}
</StyledPanelTable>
</div>
);
}
export default TeamIssuesAge;
const ChartWrapper = styled('div')`
padding: ${space(2)} ${space(2)} 0 ${space(2)};
border-bottom: 1px solid ${p => p.theme.border};
`;
const StyledPanelTable = styled(PanelTable)`
grid-template-columns: 1fr 0.15fr 0.15fr 0.25fr;
white-space: nowrap;
margin-bottom: 0;
border: 0;
font-size: ${p => p.theme.fontSizeMedium};
box-shadow: unset;
> * {
padding: ${space(1)} ${space(2)};
}
${p =>
p.isEmpty &&
css`
& > div:last-child {
padding: 48px ${space(2)};
}
`}
`;
const RightAligned = styled('span')`
display: flex;
align-items: center;
justify-content: flex-end;
`;
const ProjectTitleContainer = styled('div')`
${p => p.theme.overflowEllipsis};
display: flex;
align-items: center;
`;
const TitleOverflow = styled('div')`
${p => p.theme.overflowEllipsis};
`;
const ShadowlessProjectBadge = styled(ProjectBadge)`
display: inline-flex;
align-items: center;
margin-right: ${space(1)};
* > img {
box-shadow: none;
}
`;