import {Fragment} from 'react'; import {css, useTheme} from '@emotion/react'; import styled from '@emotion/styled'; import round from 'lodash/round'; import moment from 'moment-timezone'; import {LinkButton} from 'sentry/components/button'; import {BarChart} from 'sentry/components/charts/barChart'; import MarkLine from 'sentry/components/charts/components/markLine'; import type {DateTimeObject} from 'sentry/components/charts/utils'; import Link from 'sentry/components/links/link'; import LoadingError from 'sentry/components/loadingError'; import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse'; import {PanelTable} from 'sentry/components/panels/panelTable'; import Placeholder from 'sentry/components/placeholder'; import {IconArrow} from 'sentry/icons'; import {t, tct} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {Organization} from 'sentry/types/organization'; import type {Project} from 'sentry/types/project'; import toArray from 'sentry/utils/array/toArray'; import {useApiQuery} from 'sentry/utils/queryClient'; import type {ColorOrAlias} from 'sentry/utils/theme'; import {ProjectBadge, ProjectBadgeContainer} from './styles'; import {barAxisLabel, groupByTrend, sortSeriesByDay} from './utils'; interface TeamReleasesProps extends DateTimeObject { organization: Organization; projects: Project[]; teamSlug: string; } export type ProjectReleaseCount = { last_week_totals: Record; project_avgs: Record; release_counts: Record; }; function TeamReleases({ organization, projects, teamSlug, start, end, period, utc, }: TeamReleasesProps) { const theme = useTheme(); const datetime = {start, end, period, utc}; const { data: periodReleases, isPending: isPeriodReleasesLoading, isError: isPeriodReleasesError, refetch: refetchPeriodReleases, } = useApiQuery( [ `/teams/${organization.slug}/${teamSlug}/release-count/`, { query: { ...normalizeDateTimeParams(datetime), }, }, ], {staleTime: 5000} ); const { data: weekReleases, isPending: isWeekReleasesLoading, isError: isWeekReleasesError, refetch: refetchWeekReleases, } = useApiQuery( [ `/teams/${organization.slug}/${teamSlug}/release-count/`, { query: { statsPeriod: '7d', }, }, ], {staleTime: 5000} ); const isLoading = isPeriodReleasesLoading || isWeekReleasesLoading; if (isPeriodReleasesError || isWeekReleasesError) { return ( { refetchPeriodReleases(); refetchWeekReleases(); }} /> ); } function getReleaseCount(projectId: number, dataset: 'week' | 'period'): number | null { const releasesPeriod = dataset === 'week' ? weekReleases?.last_week_totals : periodReleases?.project_avgs; const count = releasesPeriod?.[projectId] ? Math.ceil(releasesPeriod?.[projectId]) : 0; return count; } function getTrend(projectId: number): number | null { const periodCount = getReleaseCount(projectId, 'period'); const weekCount = getReleaseCount(projectId, 'week'); if (periodCount === null || weekCount === null) { return null; } return weekCount - periodCount; } function renderReleaseCount(projectId: string, dataset: 'week' | 'period') { if (isLoading) { return (
); } const count = getReleaseCount(Number(projectId), dataset); if (count === null) { return '\u2014'; } return count; } function renderTrend(projectId: string) { if (isLoading) { return (
); } const trend = getTrend(Number(projectId)); if (trend === null) { return '\u2014'; } return ( = 0 ? 'successText' : 'errorText'}> {`${round(Math.abs(trend), 3)}`} = 0 ? 'up' : 'down'} size="xs" /> ); } const sortedProjects = projects .map(project => ({project, trend: getTrend(Number(project.id)) ?? 0})) .sort((a, b) => Math.abs(b.trend) - Math.abs(a.trend)); const groupedProjects = groupByTrend(sortedProjects); const data = Object.entries(periodReleases?.release_counts ?? {}).map( ([bucket, count]) => ({ value: Math.ceil(count), name: new Date(bucket).getTime(), }) ); const seriesData = sortSeriesByDay(data); const averageValues = Object.values(periodReleases?.project_avgs ?? {}); const projectAvgSum = averageValues.reduce( (total, currentData) => total + currentData, 0 ); const totalPeriodAverage = Math.ceil(projectAvgSum / averageValues.length); return (
{ // `seriesParams` can be an array or an object :/ const [series] = toArray(seriesParams); if (!series.data?.value) { return ''; } const dateFormat = 'MMM D'; const startDate = moment(series.data.value[0]).format(dateFormat); const endDate = moment(series.data.value[0]) .add(7, 'days') .format(dateFormat); return [ '
', `
${series.marker} ${series.seriesName} ${series.data.value[1]}
`, `
Last ${period} Average ${totalPeriodAverage}
`, '
', ``, '
', ].join(''); }, }} />
{t('Learn More')} } headers={[ t('Releases Per Project'), {tct('Last [period] Average', {period})} , {t('Last 7 Days')}, {t('Difference')}, ]} > {groupedProjects.map(({project}) => ( {renderReleaseCount(project.id, 'period')} {renderReleaseCount(project.id, 'week')} {renderTrend(project.id)} ))}
); } export default TeamReleases; const ChartWrapper = styled('div')` padding: ${space(2)} ${space(2)} 0 ${space(2)}; border-bottom: 1px solid ${p => p.theme.border}; `; const StyledPanelTable = styled(PanelTable)<{isEmpty: boolean}>` grid-template-columns: 1fr 0.2fr 0.2fr 0.2fr; white-space: nowrap; margin-bottom: 0; border: 0; font-size: ${p => p.theme.fontSizeMedium}; box-shadow: unset; & > div { padding: ${space(1)} ${space(2)}; } ${p => p.isEmpty && css` & > div:last-child { padding: 48px ${space(2)}; } `} `; const RightAligned = styled('span')` text-align: right; `; const ScoreWrapper = styled('div')` display: flex; align-items: center; justify-content: flex-end; text-align: right; `; const PaddedIconArrow = styled(IconArrow)` margin: 0 ${space(0.5)}; `; const SubText = styled('div')<{color: ColorOrAlias}>` color: ${p => p.theme[p.color]}; `;