diff --git a/static/app/routes.tsx b/static/app/routes.tsx index bc42a19f09af5e..615a5612f110f3 100644 --- a/static/app/routes.tsx +++ b/static/app/routes.tsx @@ -1108,12 +1108,16 @@ function buildRoutes() { /> import('app/views/releases/detail/commits')} + componentPromise={() => + import('app/views/releases/detail/commitsAndFiles/commits') + } component={SafeLazyLoad} /> import('app/views/releases/detail/filesChanged')} + componentPromise={() => + import('app/views/releases/detail/commitsAndFiles/filesChanged') + } component={SafeLazyLoad} /> diff --git a/static/app/utils/sessions.tsx b/static/app/utils/sessions.tsx index 8e4471ab70805e..c6f2ba5cf1437f 100644 --- a/static/app/utils/sessions.tsx +++ b/static/app/utils/sessions.tsx @@ -9,13 +9,17 @@ import { SIXTY_DAYS, THIRTY_DAYS, } from 'app/components/charts/utils'; +import {IconCheckmark, IconFire, IconWarning} from 'app/icons'; import {SessionApiResponse, SessionField, SessionStatus} from 'app/types'; import {SeriesDataUnit} from 'app/types/echarts'; import {defined, percent} from 'app/utils'; -import {Theme} from 'app/utils/theme'; +import {IconSize, Theme} from 'app/utils/theme'; import {getCrashFreePercent, getSessionStatusPercent} from 'app/views/releases/utils'; import {sessionTerm} from 'app/views/releases/utils/sessionTerm'; +const CRASH_FREE_DANGER_THRESHOLD = 98; +const CRASH_FREE_WARNING_THRESHOLD = 99.5; + export function getCount(groups: SessionApiResponse['groups'] = [], field: SessionField) { return groups.reduce((acc, group) => acc + group.totals[field], 0); } @@ -343,3 +347,15 @@ export function filterSessionsInTimeWindow( groups, }; } + +export function getCrashFreeIcon(crashFreePercent: number, iconSize: IconSize = 'sm') { + if (crashFreePercent < CRASH_FREE_DANGER_THRESHOLD) { + return ; + } + + if (crashFreePercent < CRASH_FREE_WARNING_THRESHOLD) { + return ; + } + + return ; +} diff --git a/static/app/views/projectDetail/missingFeatureButtons/missingReleasesButtons.tsx b/static/app/views/projectDetail/missingFeatureButtons/missingReleasesButtons.tsx index 6198616dd89a70..4f357b90a58af4 100644 --- a/static/app/views/projectDetail/missingFeatureButtons/missingReleasesButtons.tsx +++ b/static/app/views/projectDetail/missingFeatureButtons/missingReleasesButtons.tsx @@ -6,7 +6,7 @@ import FeatureTourModal from 'app/components/modals/featureTourModal'; import {t} from 'app/locale'; import {Organization} from 'app/types'; import {trackAnalyticsEvent} from 'app/utils/analytics'; -import {RELEASES_TOUR_STEPS} from 'app/views/releases/list/releasePromo'; +import {RELEASES_TOUR_STEPS} from 'app/views/releases/list/releasesPromo'; const DOCS_URL = 'https://docs.sentry.io/product/releases/'; const DOCS_HEALTH_URL = 'https://docs.sentry.io/product/releases/health/'; diff --git a/static/app/views/projectDetail/projectLatestReleases.tsx b/static/app/views/projectDetail/projectLatestReleases.tsx index c2d2d6dc3e759e..2048618e1d4120 100644 --- a/static/app/views/projectDetail/projectLatestReleases.tsx +++ b/static/app/views/projectDetail/projectLatestReleases.tsx @@ -18,7 +18,7 @@ import overflowEllipsis from 'app/styles/overflowEllipsis'; import space from 'app/styles/space'; import {Organization, Release} from 'app/types'; import {analytics} from 'app/utils/analytics'; -import {RELEASES_TOUR_STEPS} from 'app/views/releases/list/releasePromo'; +import {RELEASES_TOUR_STEPS} from 'app/views/releases/list/releasesPromo'; import MissingReleasesButtons from './missingFeatureButtons/missingReleasesButtons'; import {SectionHeadingLink, SectionHeadingWrapper, SidebarSection} from './styles'; diff --git a/static/app/views/releases/detail/commits.tsx b/static/app/views/releases/detail/commitsAndFiles/commits.tsx similarity index 99% rename from static/app/views/releases/detail/commits.tsx rename to static/app/views/releases/detail/commitsAndFiles/commits.tsx index bab4038a9ef2c7..229e64780edef9 100644 --- a/static/app/views/releases/detail/commits.tsx +++ b/static/app/views/releases/detail/commitsAndFiles/commits.tsx @@ -13,9 +13,10 @@ import {formatVersion} from 'app/utils/formatters'; import routeTitleGen from 'app/utils/routeTitle'; import AsyncView from 'app/views/asyncView'; +import {getCommitsByRepository, getQuery, getReposToRender} from '../utils'; + import EmptyState from './emptyState'; import RepositorySwitcher from './repositorySwitcher'; -import {getCommitsByRepository, getQuery, getReposToRender} from './utils'; import withReleaseRepos from './withReleaseRepos'; type Props = RouteComponentProps<{orgId: string; release: string}, {}> & { diff --git a/static/app/views/releases/detail/emptyState.tsx b/static/app/views/releases/detail/commitsAndFiles/emptyState.tsx similarity index 100% rename from static/app/views/releases/detail/emptyState.tsx rename to static/app/views/releases/detail/commitsAndFiles/emptyState.tsx diff --git a/static/app/views/releases/detail/filesChanged.tsx b/static/app/views/releases/detail/commitsAndFiles/filesChanged.tsx similarity index 99% rename from static/app/views/releases/detail/filesChanged.tsx rename to static/app/views/releases/detail/commitsAndFiles/filesChanged.tsx index 92c4545bab327a..b6d934b733e556 100644 --- a/static/app/views/releases/detail/filesChanged.tsx +++ b/static/app/views/releases/detail/commitsAndFiles/filesChanged.tsx @@ -14,9 +14,10 @@ import {formatVersion} from 'app/utils/formatters'; import routeTitleGen from 'app/utils/routeTitle'; import AsyncView from 'app/views/asyncView'; +import {getFilesByRepository, getQuery, getReposToRender} from '../utils'; + import EmptyState from './emptyState'; import RepositorySwitcher from './repositorySwitcher'; -import {getFilesByRepository, getQuery, getReposToRender} from './utils'; import withReleaseRepos from './withReleaseRepos'; type Props = RouteComponentProps<{orgId: string; release: string}, {}> & { diff --git a/static/app/views/releases/detail/repositorySwitcher.tsx b/static/app/views/releases/detail/commitsAndFiles/repositorySwitcher.tsx similarity index 100% rename from static/app/views/releases/detail/repositorySwitcher.tsx rename to static/app/views/releases/detail/commitsAndFiles/repositorySwitcher.tsx diff --git a/static/app/views/releases/detail/withReleaseRepos.tsx b/static/app/views/releases/detail/commitsAndFiles/withReleaseRepos.tsx similarity index 99% rename from static/app/views/releases/detail/withReleaseRepos.tsx rename to static/app/views/releases/detail/commitsAndFiles/withReleaseRepos.tsx index 5df1b5508959d7..c2888a5af8a7c2 100644 --- a/static/app/views/releases/detail/withReleaseRepos.tsx +++ b/static/app/views/releases/detail/commitsAndFiles/withReleaseRepos.tsx @@ -17,7 +17,7 @@ import withOrganization from 'app/utils/withOrganization'; import withRepositories from 'app/utils/withRepositories'; import EmptyMessage from 'app/views/settings/components/emptyMessage'; -import {ReleaseContext} from '.'; +import {ReleaseContext} from '..'; // These props are required when using this HoC type DependentProps = RouteComponentProps<{orgId: string; release: string}, {}>; diff --git a/static/app/views/releases/detail/releaseActions.tsx b/static/app/views/releases/detail/header/releaseActions.tsx similarity index 99% rename from static/app/views/releases/detail/releaseActions.tsx rename to static/app/views/releases/detail/header/releaseActions.tsx index 50d811b9191963..4ec38ab6f96f44 100644 --- a/static/app/views/releases/detail/releaseActions.tsx +++ b/static/app/views/releases/detail/header/releaseActions.tsx @@ -21,7 +21,7 @@ import {Organization, Release, ReleaseMeta} from 'app/types'; import {trackAnalyticsEvent} from 'app/utils/analytics'; import {formatVersion} from 'app/utils/formatters'; -import {isReleaseArchived} from '../utils'; +import {isReleaseArchived} from '../../utils'; type Props = { location: Location; diff --git a/static/app/views/releases/detail/releaseHeader.tsx b/static/app/views/releases/detail/header/releaseHeader.tsx similarity index 100% rename from static/app/views/releases/detail/releaseHeader.tsx rename to static/app/views/releases/detail/header/releaseHeader.tsx diff --git a/static/app/views/releases/detail/index.tsx b/static/app/views/releases/detail/index.tsx index e8fd3623174e06..e2ea5e94789d6a 100644 --- a/static/app/views/releases/detail/index.tsx +++ b/static/app/views/releases/detail/index.tsx @@ -34,7 +34,7 @@ import AsyncView from 'app/views/asyncView'; import {getReleaseBounds, ReleaseBounds} from '../utils'; -import ReleaseHeader from './releaseHeader'; +import ReleaseHeader from './header/releaseHeader'; type ReleaseContextType = { release: ReleaseWithHealth; diff --git a/static/app/views/releases/detail/overview/index.tsx b/static/app/views/releases/detail/overview/index.tsx index 7e4ad7b832d7f9..b3038cd2ff0b9f 100644 --- a/static/app/views/releases/detail/overview/index.tsx +++ b/static/app/views/releases/detail/overview/index.tsx @@ -43,16 +43,16 @@ import {TrendChangeType, TrendView} from 'app/views/performance/trends/types'; import {getReleaseParams, isReleaseArchived, ReleaseBounds} from '../../utils'; import {ReleaseContext} from '..'; -import CommitAuthorBreakdown from './commitAuthorBreakdown'; -import Deploys from './deploys'; -import Issues from './issues'; -import OtherProjects from './otherProjects'; -import ProjectReleaseDetails from './projectReleaseDetails'; -import ReleaseAdoption from './releaseAdoption'; +import CommitAuthorBreakdown from './sidebar/commitAuthorBreakdown'; +import Deploys from './sidebar/deploys'; +import OtherProjects from './sidebar/otherProjects'; +import ProjectReleaseDetails from './sidebar/projectReleaseDetails'; +import ReleaseAdoption from './sidebar/releaseAdoption'; +import ReleaseStats from './sidebar/releaseStats'; +import TotalCrashFreeUsers from './sidebar/totalCrashFreeUsers'; import ReleaseArchivedNotice from './releaseArchivedNotice'; import ReleaseComparisonChart from './releaseComparisonChart'; -import ReleaseStats from './releaseStats'; -import TotalCrashFreeUsers from './totalCrashFreeUsers'; +import ReleaseIssues from './releaseIssues'; const RELEASE_PERIOD_KEY = 'release'; @@ -374,7 +374,7 @@ class ReleaseOverview extends AsyncView { /> )} - void; }; -class Issues extends Component { +class ReleaseIssues extends Component { static defaultProps = defaultProps; state: State = this.getInitialState(); @@ -426,4 +426,4 @@ const StyledPagination = styled(Pagination)` margin: 0; `; -export default withApi(withOrganization(Issues)); +export default withApi(withOrganization(ReleaseIssues)); diff --git a/static/app/views/releases/detail/overview/commitAuthorBreakdown.tsx b/static/app/views/releases/detail/overview/sidebar/commitAuthorBreakdown.tsx similarity index 98% rename from static/app/views/releases/detail/overview/commitAuthorBreakdown.tsx rename to static/app/views/releases/detail/overview/sidebar/commitAuthorBreakdown.tsx index cd6943095cb0e7..d3df41c0eed688 100644 --- a/static/app/views/releases/detail/overview/commitAuthorBreakdown.tsx +++ b/static/app/views/releases/detail/overview/sidebar/commitAuthorBreakdown.tsx @@ -11,7 +11,7 @@ import {Commit, User} from 'app/types'; import {percent} from 'app/utils'; import {userDisplayName} from 'app/utils/formatters'; -import {SectionHeading, Wrapper} from './styles'; +import {SectionHeading, Wrapper} from '../styles'; type GroupedAuthorCommits = { [key: string]: {author: User | undefined; commitCount: number}; diff --git a/static/app/views/releases/detail/overview/deploys.tsx b/static/app/views/releases/detail/overview/sidebar/deploys.tsx similarity index 96% rename from static/app/views/releases/detail/overview/deploys.tsx rename to static/app/views/releases/detail/overview/sidebar/deploys.tsx index e25bc9ca281943..2f4909bf81b419 100644 --- a/static/app/views/releases/detail/overview/deploys.tsx +++ b/static/app/views/releases/detail/overview/sidebar/deploys.tsx @@ -7,7 +7,7 @@ import {t} from 'app/locale'; import space from 'app/styles/space'; import {Deploy} from 'app/types'; -import {SectionHeading, Wrapper} from './styles'; +import {SectionHeading, Wrapper} from '../styles'; type Props = { version: string; diff --git a/static/app/views/releases/detail/overview/otherProjects.tsx b/static/app/views/releases/detail/overview/sidebar/otherProjects.tsx similarity index 71% rename from static/app/views/releases/detail/overview/otherProjects.tsx rename to static/app/views/releases/detail/overview/sidebar/otherProjects.tsx index 77cff72faaed36..fcbe80ef725497 100644 --- a/static/app/views/releases/detail/overview/otherProjects.tsx +++ b/static/app/views/releases/detail/overview/sidebar/otherProjects.tsx @@ -4,13 +4,12 @@ import {Location} from 'history'; import Button from 'app/components/button'; import Collapsible from 'app/components/collapsible'; import IdBadge from 'app/components/idBadge'; -import {tn} from 'app/locale'; +import {extractSelectionParameters} from 'app/components/organizations/globalSelectionHeader/utils'; +import {t, tn} from 'app/locale'; import space from 'app/styles/space'; import {Organization, ReleaseProject} from 'app/types'; -import ProjectLink from '../../list/releaseHealth/projectLink'; - -import {SectionHeading, Wrapper} from './styles'; +import {SectionHeading, Wrapper} from '../styles'; type Props = { projects: ReleaseProject[]; @@ -44,12 +43,21 @@ function OtherProjects({projects, location, version, organization}: Props) { {projects.map(project => ( - + ))} diff --git a/static/app/views/releases/detail/overview/projectReleaseDetails.tsx b/static/app/views/releases/detail/overview/sidebar/projectReleaseDetails.tsx similarity index 97% rename from static/app/views/releases/detail/overview/projectReleaseDetails.tsx rename to static/app/views/releases/detail/overview/sidebar/projectReleaseDetails.tsx index ed6b0e1539e974..25b52e1f3d9920 100644 --- a/static/app/views/releases/detail/overview/projectReleaseDetails.tsx +++ b/static/app/views/releases/detail/overview/sidebar/projectReleaseDetails.tsx @@ -10,7 +10,7 @@ import Version from 'app/components/version'; import {t, tn} from 'app/locale'; import {ReleaseMeta, ReleaseWithHealth} from 'app/types'; -import {SectionHeading, Wrapper} from './styles'; +import {SectionHeading, Wrapper} from '../styles'; type Props = { release: ReleaseWithHealth; diff --git a/static/app/views/releases/detail/overview/releaseAdoption.tsx b/static/app/views/releases/detail/overview/sidebar/releaseAdoption.tsx similarity index 95% rename from static/app/views/releases/detail/overview/releaseAdoption.tsx rename to static/app/views/releases/detail/overview/sidebar/releaseAdoption.tsx index 44d00f4a74fb35..5833cf9671126d 100644 --- a/static/app/views/releases/detail/overview/releaseAdoption.tsx +++ b/static/app/views/releases/detail/overview/sidebar/releaseAdoption.tsx @@ -23,13 +23,15 @@ import { SessionField, } from 'app/types'; import {getAdoptionSeries, getCount} from 'app/utils/sessions'; -import {isProjectMobileForReleases} from 'app/views/releases/list'; -import {ADOPTION_STAGE_LABELS} from 'app/views/releases/list/releaseHealth/content'; -import {getReleaseBounds, getReleaseParams} from '../../utils'; -import {generateReleaseMarkLines, releaseMarkLinesLabels} from '../utils'; - -import {Wrapper} from './styles'; +import { + ADOPTION_STAGE_LABELS, + getReleaseBounds, + getReleaseParams, + isMobileRelease, +} from '../../../utils'; +import {generateReleaseMarkLines, releaseMarkLinesLabels} from '../../utils'; +import {Wrapper} from '../styles'; type Props = { release: ReleaseWithHealth; @@ -204,14 +206,13 @@ function ReleaseComparisonChart({ releaseBounds: getReleaseBounds(release), }); - const isMobileProject = isProjectMobileForReleases(project.platform); const adoptionStage = release.adoptionStages?.[project.slug]?.stage; const adoptionStageLabel = ADOPTION_STAGE_LABELS[adoptionStage]; const multipleEnvironments = environment.length === 0 || environment.length > 1; return ( - {isMobileProject && ( + {isMobileRelease(project.platform) && ( , activeRepository?: Reposi return [activeRepository.name]; } -/** - * Get high level transaction information for this release - */ -export function getReleaseEventView( - selection: GlobalSelection, - version: string, - _organization: Organization -): EventView { - const {projects, environments, datetime} = selection; - const {start, end, period} = datetime; - - const discoverQuery = { - id: undefined, - version: 2, - name: `${t('Release Apdex')}`, - fields: ['apdex()'], - query: new MutableSearch([ - `release:${version}`, - 'event.type:transaction', - 'count():>0', - ]).formatString(), - range: period, - environment: environments, - projects, - start: start ? getUtcDateString(start) : undefined, - end: end ? getUtcDateString(end) : undefined, - } as const; - - return EventView.fromSavedQuery(discoverQuery); -} - export const releaseComparisonChartLabels = { [ReleaseComparisonChartType.CRASH_FREE_SESSIONS]: t('Crash Free Session Rate'), [ReleaseComparisonChartType.HEALTHY_SESSIONS]: t('Healthy'), @@ -273,7 +236,7 @@ export function generateReleaseMarkLines( ); } - if (!isSingleEnv || !isProjectMobileForReleases(project.platform)) { + if (!isSingleEnv || !isMobileRelease(project.platform)) { // for now want to show marklines only on mobile platforms with single environment selected return markLines; } diff --git a/static/app/views/releases/list/crashFree.tsx b/static/app/views/releases/list/crashFree.tsx deleted file mode 100644 index 92a67e2642ddee..00000000000000 --- a/static/app/views/releases/list/crashFree.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import styled from '@emotion/styled'; - -import {IconCheckmark, IconFire, IconWarning} from 'app/icons'; -import overflowEllipsis from 'app/styles/overflowEllipsis'; -import space from 'app/styles/space'; -import {defined} from 'app/utils'; -import {IconSize} from 'app/utils/theme'; - -import {displayCrashFreePercent, releaseDisplayLabel} from '../utils'; - -import {DisplayOption} from './utils'; - -const CRASH_FREE_DANGER_THRESHOLD = 98; -const CRASH_FREE_WARNING_THRESHOLD = 99.5; - -const getIcon = (percent: number, iconSize: IconSize) => { - if (percent < CRASH_FREE_DANGER_THRESHOLD) { - return ; - } - - if (percent < CRASH_FREE_WARNING_THRESHOLD) { - return ; - } - - return ; -}; - -type Props = { - percent: number; - iconSize?: IconSize; - /** - * If provided there will be a label next to percentage - */ - displayOption?: DisplayOption; -}; - -const CrashFree = ({percent, iconSize = 'sm', displayOption}: Props) => { - return ( - - {getIcon(percent, iconSize)} - - {displayCrashFreePercent(percent)}{' '} - {defined(displayOption) && releaseDisplayLabel(displayOption, 2)} - - - ); -}; - -const Wrapper = styled('div')` - display: inline-grid; - grid-auto-flow: column; - grid-column-gap: ${space(1)}; - align-items: center; - vertical-align: middle; -`; - -const CrashFreePercent = styled('div')` - ${overflowEllipsis}; - font-variant-numeric: tabular-nums; -`; - -export default CrashFree; diff --git a/static/app/views/releases/list/healthStatsChart.tsx b/static/app/views/releases/list/healthStatsChart.tsx deleted file mode 100644 index e0b160bd1e926a..00000000000000 --- a/static/app/views/releases/list/healthStatsChart.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import LazyLoad from 'react-lazyload'; -import {useTheme} from '@emotion/react'; - -import MiniBarChart from 'app/components/charts/miniBarChart'; -import {tn} from 'app/locale'; -import {Series} from 'app/types/echarts'; - -import {DisplayOption} from './utils'; - -type Props = { - activeDisplay: DisplayOption; - data: Series[]; - height?: number; -}; - -function HealthStatsChart({activeDisplay, data, height = 24}: Props) { - const theme = useTheme(); - - const formatTooltip = (value: number) => { - const suffix = - activeDisplay === DisplayOption.USERS - ? tn('user', 'users', value) - : tn('session', 'sessions', value); - - return `${value.toLocaleString()} ${suffix}`; - }; - - return ( - - - - ); -} - -export default HealthStatsChart; diff --git a/static/app/views/releases/list/index.tsx b/static/app/views/releases/list/index.tsx index 4f88632a8b02bf..cf51ad026dc071 100644 --- a/static/app/views/releases/list/index.tsx +++ b/static/app/views/releases/list/index.tsx @@ -20,7 +20,7 @@ import SearchBar from 'app/components/searchBar'; import SmartSearchBar from 'app/components/smartSearchBar'; import {DEFAULT_STATS_PERIOD, RELEASE_ADOPTION_STAGES} from 'app/constants'; import {ALL_ACCESS_PROJECTS} from 'app/constants/globalSelectionHeader'; -import {desktop, mobile, PlatformKey, releaseHealth} from 'app/data/platformCategories'; +import {releaseHealth} from 'app/data/platformCategories'; import {IconInfo} from 'app/icons'; import {t} from 'app/locale'; import ProjectsStore from 'app/stores/projectsStore'; @@ -43,15 +43,15 @@ import withProjects from 'app/utils/withProjects'; import AsyncView from 'app/views/asyncView'; import ReleaseArchivedNotice from '../detail/overview/releaseArchivedNotice'; +import {isMobileRelease} from '../utils'; -import ReleaseAdoptionChart from './releaseAdoptionChart'; import ReleaseCard from './releaseCard'; -import ReleaseDisplayOptions from './releaseDisplayOptions'; -import ReleaseListRequest from './releaseListRequest'; -import ReleaseListSortOptions from './releaseListSortOptions'; -import ReleaseListStatusOptions from './releaseListStatusOptions'; -import ReleasePromo from './releasePromo'; -import {DisplayOption, SortOption, StatusOption} from './utils'; +import ReleasesAdoptionChart from './releasesAdoptionChart'; +import ReleasesDisplayOptions, {ReleasesDisplayOption} from './releasesDisplayOptions'; +import ReleasesPromo from './releasesPromo'; +import ReleasesRequest from './releasesRequest'; +import ReleasesSortOptions, {ReleasesSortOption} from './releasesSortOptions'; +import ReleasesStatusOptions, {ReleasesStatusOption} from './releasesStatusOptions'; const supportedTags = { 'release.version': { @@ -78,9 +78,6 @@ const supportedTags = { }, }; -export const isProjectMobileForReleases = (projectPlatform: PlatformKey) => - ([...mobile, ...desktop] as string[]).includes(projectPlatform); - type RouteParams = { orgId: string; }; @@ -113,10 +110,10 @@ class ReleasesList extends AsyncView { ...pick(location.query, ['project', 'environment', 'cursor', 'query', 'sort']), summaryStatsPeriod: statsPeriod, per_page: 20, - flatten: activeSort === SortOption.DATE ? 0 : 1, + flatten: activeSort === ReleasesSortOption.DATE ? 0 : 1, adoptionStages: 1, status: - activeStatus === StatusOption.ARCHIVED + activeStatus === ReleasesStatusOption.ARCHIVED ? ReleaseStatus.Archived : ReleaseStatus.Active, }; @@ -156,42 +153,42 @@ class ReleasesList extends AsyncView { return typeof query === 'string' ? query : undefined; } - getSort(): SortOption { + getSort(): ReleasesSortOption { const {environments} = this.props.selection; const {sort} = this.props.location.query; // Require 1 environment for date adopted - if (sort === SortOption.ADOPTION && environments.length !== 1) { - return SortOption.DATE; + if (sort === ReleasesSortOption.ADOPTION && environments.length !== 1) { + return ReleasesSortOption.DATE; } - const sortExists = Object.values(SortOption).includes(sort); + const sortExists = Object.values(ReleasesSortOption).includes(sort); if (sortExists) { return sort; } - return SortOption.DATE; + return ReleasesSortOption.DATE; } - getDisplay(): DisplayOption { + getDisplay(): ReleasesDisplayOption { const {display} = this.props.location.query; switch (display) { - case DisplayOption.USERS: - return DisplayOption.USERS; + case ReleasesDisplayOption.USERS: + return ReleasesDisplayOption.USERS; default: - return DisplayOption.SESSIONS; + return ReleasesDisplayOption.SESSIONS; } } - getStatus(): StatusOption { + getStatus(): ReleasesStatusOption { const {status} = this.props.location.query; switch (status) { - case StatusOption.ARCHIVED: - return StatusOption.ARCHIVED; + case ReleasesStatusOption.ARCHIVED: + return ReleasesStatusOption.ARCHIVED; default: - return StatusOption.ACTIVE; + return ReleasesStatusOption.ACTIVE; } } @@ -229,14 +226,26 @@ class ReleasesList extends AsyncView { const {location, router} = this.props; let sort = location.query.sort; - if (sort === SortOption.USERS_24_HOURS && display === DisplayOption.SESSIONS) - sort = SortOption.SESSIONS_24_HOURS; - else if (sort === SortOption.SESSIONS_24_HOURS && display === DisplayOption.USERS) - sort = SortOption.USERS_24_HOURS; - else if (sort === SortOption.CRASH_FREE_USERS && display === DisplayOption.SESSIONS) - sort = SortOption.CRASH_FREE_SESSIONS; - else if (sort === SortOption.CRASH_FREE_SESSIONS && display === DisplayOption.USERS) - sort = SortOption.CRASH_FREE_USERS; + if ( + sort === ReleasesSortOption.USERS_24_HOURS && + display === ReleasesDisplayOption.SESSIONS + ) + sort = ReleasesSortOption.SESSIONS_24_HOURS; + else if ( + sort === ReleasesSortOption.SESSIONS_24_HOURS && + display === ReleasesDisplayOption.USERS + ) + sort = ReleasesSortOption.USERS_24_HOURS; + else if ( + sort === ReleasesSortOption.CRASH_FREE_USERS && + display === ReleasesDisplayOption.SESSIONS + ) + sort = ReleasesSortOption.CRASH_FREE_SESSIONS; + else if ( + sort === ReleasesSortOption.CRASH_FREE_SESSIONS && + display === ReleasesDisplayOption.USERS + ) + sort = ReleasesSortOption.CRASH_FREE_USERS; router.push({ ...location, @@ -314,7 +323,7 @@ class ReleasesList extends AsyncView { ); } - if (activeSort === SortOption.USERS_24_HOURS) { + if (activeSort === ReleasesSortOption.USERS_24_HOURS) { return ( {t('There are no releases with active user data (users in the last 24 hours).')} @@ -322,7 +331,7 @@ class ReleasesList extends AsyncView { ); } - if (activeSort === SortOption.SESSIONS_24_HOURS) { + if (activeSort === ReleasesSortOption.SESSIONS_24_HOURS) { return ( {t( @@ -332,7 +341,10 @@ class ReleasesList extends AsyncView { ); } - if (activeSort === SortOption.BUILD || activeSort === SortOption.SEMVER) { + if ( + activeSort === ReleasesSortOption.BUILD || + activeSort === ReleasesSortOption.SEMVER + ) { return ( {t('There are no releases with semantic versioning.')} @@ -340,7 +352,7 @@ class ReleasesList extends AsyncView { ); } - if (activeSort !== SortOption.DATE) { + if (activeSort !== ReleasesSortOption.DATE) { const relativePeriod = getRelativeSummary( statsPeriod || DEFAULT_STATS_PERIOD ).toLowerCase(); @@ -352,7 +364,7 @@ class ReleasesList extends AsyncView { ); } - if (activeStatus === StatusOption.ARCHIVED) { + if (activeStatus === ReleasesStatusOption.ARCHIVED) { return ( {t('There are no archived releases.')} @@ -361,7 +373,7 @@ class ReleasesList extends AsyncView { } return ( - p !== ALL_ACCESS_PROJECTS)[0]} /> @@ -411,7 +423,10 @@ class ReleasesList extends AsyncView { ); } - renderInnerBody(activeDisplay: DisplayOption, showReleaseAdoptionStages: boolean) { + renderInnerBody( + activeDisplay: ReleasesDisplayOption, + showReleaseAdoptionStages: boolean + ) { const {location, selection, organization, router} = this.props; const {releases, reloading, releasesPageLinks} = this.state; @@ -424,7 +439,7 @@ class ReleasesList extends AsyncView { } return ( - version)} organization={organization} selection={selection} @@ -439,14 +454,13 @@ class ReleasesList extends AsyncView { selection.projects[0] !== ALL_ACCESS_PROJECTS; const selectedProject = this.getSelectedProject(); const isMobileProject = - selectedProject?.platform && - isProjectMobileForReleases(selectedProject.platform); + selectedProject?.platform && isMobileRelease(selectedProject.platform); return ( {singleProjectSelected && this.projectHasSessions && isMobileProject && ( - { ); }} - + ); } @@ -492,7 +506,7 @@ class ReleasesList extends AsyncView { const hasAnyMobileProject = selection.projects .map(id => `${id}`) .map(ProjectsStore.getById) - .some(project => project?.platform && isProjectMobileForReleases(project.platform)); + .some(project => project?.platform && isMobileRelease(project.platform)); const showReleaseAdoptionStages = hasReleaseStages && hasAnyMobileProject && selection.environments.length === 1; const hasReleasesSetup = releases && releases.length > 0; @@ -544,18 +558,18 @@ class ReleasesList extends AsyncView { /> )} - - - @@ -563,7 +577,7 @@ class ReleasesList extends AsyncView { {!reloading && - activeStatus === StatusOption.ARCHIVED && + activeStatus === ReleasesStatusOption.ARCHIVED && !!releases?.length && } {error diff --git a/static/app/views/releases/list/releaseAdoption.tsx b/static/app/views/releases/list/releaseAdoption.tsx deleted file mode 100644 index f3b1666f8db744..00000000000000 --- a/static/app/views/releases/list/releaseAdoption.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import {useTheme} from '@emotion/react'; -import styled from '@emotion/styled'; - -import Count from 'app/components/count'; -import ProgressBar from 'app/components/progressBar'; -import TextOverflow from 'app/components/textOverflow'; -import Tooltip from 'app/components/tooltip'; -import {t} from 'app/locale'; -import space from 'app/styles/space'; - -import {releaseDisplayLabel} from '../utils'; - -import {DisplayOption} from './utils'; - -type Props = { - adoption: number; - releaseCount: number; - projectCount: number; - displayOption: DisplayOption; - withLabels?: boolean; -}; - -function ReleaseAdoption({ - adoption, - releaseCount, - projectCount, - displayOption, - withLabels, -}: Props) { - const theme = useTheme(); - return ( -
- {withLabels && ( - - - /{' '} - {releaseDisplayLabel(displayOption, projectCount)} - - - {!adoption ? 0 : adoption < 1 ? '<1' : Math.round(adoption)}% - - )} - - - - - <Dot color={theme.progressBar} /> - {t('This Release')} - - - {' '} - {releaseDisplayLabel(displayOption, releaseCount)} - - - - - <Dot color={theme.progressBackground} /> - {t('Total Project')} - - - {' '} - {releaseDisplayLabel(displayOption, projectCount)} - - - - - - - } - > - - - - -
- ); -} - -const Labels = styled('div')` - display: grid; - grid-gap: ${space(1)}; - grid-template-columns: 1fr max-content; -`; - -const TooltipWrapper = styled('div')` - padding: ${space(0.75)}; - font-size: ${p => p.theme.fontSizeMedium}; - line-height: 21px; - font-weight: normal; -`; - -const TooltipRow = styled('div')` - display: grid; - grid-template-columns: auto auto; - grid-gap: ${space(3)}; - justify-content: space-between; - padding-bottom: ${space(0.25)}; -`; - -const Title = styled('div')` - text-align: left; -`; - -const Dot = styled('div')<{color: string}>` - display: inline-block; - margin-right: ${space(0.75)}; - border-radius: 10px; - width: 10px; - height: 10px; - background-color: ${p => p.color}; -`; - -const Value = styled('div')` - color: ${p => p.theme.gray300}; - text-align: right; -`; -const Divider = styled('div')` - border-top: 1px solid ${p => p.theme.gray400}; - margin: ${space(0.75)} -${space(2)} ${space(1)}; -`; - -const Time = styled('div')` - color: ${p => p.theme.gray300}; - text-align: center; -`; - -const ProgressBarWrapper = styled('div')` - /* A bit of padding makes hovering for tooltip easier */ - padding: ${space(0.5)} 0; -`; - -export default ReleaseAdoption; diff --git a/static/app/views/releases/list/releaseCard.tsx b/static/app/views/releases/list/releaseCard.tsx deleted file mode 100644 index 8cdcbca5a001eb..00000000000000 --- a/static/app/views/releases/list/releaseCard.tsx +++ /dev/null @@ -1,169 +0,0 @@ -import styled from '@emotion/styled'; -import {Location} from 'history'; - -import GuideAnchor from 'app/components/assistant/guideAnchor'; -import GlobalSelectionLink from 'app/components/globalSelectionLink'; -import {Panel} from 'app/components/panels'; -import ReleaseStats from 'app/components/releaseStats'; -import TextOverflow from 'app/components/textOverflow'; -import TimeSince from 'app/components/timeSince'; -import Version from 'app/components/version'; -import overflowEllipsis from 'app/styles/overflowEllipsis'; -import space from 'app/styles/space'; -import {GlobalSelection, Organization, Release} from 'app/types'; - -import ReleaseHealth from './releaseHealth'; -import {ReleaseListRequestRenderProps} from './releaseListRequest'; -import {DisplayOption} from './utils'; - -function getReleaseProjectId(release: Release, selection: GlobalSelection) { - // if a release has only one project - if (release.projects.length === 1) { - return release.projects[0].id; - } - - // if only one project is selected in global header and release has it (second condition will prevent false positives like -1) - if ( - selection.projects.length === 1 && - release.projects.map(p => p.id).includes(selection.projects[0]) - ) { - return selection.projects[0]; - } - - // project selector on release detail page will pick it up - return undefined; -} - -type Props = { - release: Release; - organization: Organization; - activeDisplay: DisplayOption; - location: Location; - selection: GlobalSelection; - reloading: boolean; - showHealthPlaceholders: boolean; - isTopRelease: boolean; - getHealthData: ReleaseListRequestRenderProps['getHealthData']; - showReleaseAdoptionStages: boolean; -}; - -const ReleaseCard = ({ - release, - organization, - activeDisplay, - location, - reloading, - selection, - showHealthPlaceholders, - isTopRelease, - getHealthData, - showReleaseAdoptionStages, -}: Props) => { - const {version, commitCount, lastDeploy, dateCreated, versionInfo} = release; - - return ( - - - - - - - - - - - {commitCount > 0 && } - - - {versionInfo?.package && ( - {versionInfo.package} - )} - - {lastDeploy?.dateFinished && ` \u007C ${lastDeploy.environment}`} - - - - - - - - ); -}; - -const VersionWrapper = styled('div')` - display: flex; - align-items: center; -`; - -const StyledVersion = styled(Version)` - ${overflowEllipsis}; -`; - -const StyledPanel = styled(Panel)<{reloading: number}>` - opacity: ${p => (p.reloading ? 0.5 : 1)}; - pointer-events: ${p => (p.reloading ? 'none' : 'auto')}; - - @media (min-width: ${p => p.theme.breakpoints[1]}) { - display: flex; - } -`; - -const ReleaseInfo = styled('div')` - padding: ${space(1.5)} ${space(2)}; - flex-shrink: 0; - - @media (min-width: ${p => p.theme.breakpoints[1]}) { - border-right: 1px solid ${p => p.theme.border}; - min-width: 260px; - width: 22%; - max-width: 300px; - } -`; - -const ReleaseInfoSubheader = styled('div')` - font-size: ${p => p.theme.fontSizeSmall}; - color: ${p => p.theme.gray400}; -`; - -const PackageName = styled(TextOverflow)` - font-size: ${p => p.theme.fontSizeMedium}; - color: ${p => p.theme.textColor}; -`; - -const ReleaseProjects = styled('div')` - border-top: 1px solid ${p => p.theme.border}; - flex-grow: 1; - display: grid; - - @media (min-width: ${p => p.theme.breakpoints[1]}) { - border-top: none; - } -`; - -const ReleaseInfoHeader = styled('div')` - font-size: ${p => p.theme.fontSizeExtraLarge}; - display: grid; - grid-template-columns: minmax(0, 1fr) max-content; - grid-gap: ${space(2)}; - align-items: center; -`; - -export default ReleaseCard; diff --git a/static/app/views/releases/list/releaseCard/index.tsx b/static/app/views/releases/list/releaseCard/index.tsx new file mode 100644 index 00000000000000..59b2ad37726a0c --- /dev/null +++ b/static/app/views/releases/list/releaseCard/index.tsx @@ -0,0 +1,412 @@ +import {Component} from 'react'; +import styled from '@emotion/styled'; +import color from 'color'; +import {Location} from 'history'; +import partition from 'lodash/partition'; + +import GuideAnchor from 'app/components/assistant/guideAnchor'; +import Button from 'app/components/button'; +import Collapsible from 'app/components/collapsible'; +import GlobalSelectionLink from 'app/components/globalSelectionLink'; +import {Panel, PanelHeader} from 'app/components/panels'; +import TextOverflow from 'app/components/textOverflow'; +import TimeSince from 'app/components/timeSince'; +import Tooltip from 'app/components/tooltip'; +import Version from 'app/components/version'; +import {t, tct, tn} from 'app/locale'; +import overflowEllipsis from 'app/styles/overflowEllipsis'; +import space from 'app/styles/space'; +import {GlobalSelection, Organization, Release} from 'app/types'; + +import {ReleasesDisplayOption} from '../releasesDisplayOptions'; +import {ReleasesRequestRenderProps} from '../releasesRequest'; + +import ReleaseCardCommits from './releaseCardCommits'; +import ReleaseCardProjectRow from './releaseCardProjectRow'; +import ReleaseCardStatsPeriod from './releaseCardStatsPeriod'; + +function getReleaseProjectId(release: Release, selection: GlobalSelection) { + // if a release has only one project + if (release.projects.length === 1) { + return release.projects[0].id; + } + + // if only one project is selected in global header and release has it (second condition will prevent false positives like -1) + if ( + selection.projects.length === 1 && + release.projects.map(p => p.id).includes(selection.projects[0]) + ) { + return selection.projects[0]; + } + + // project selector on release detail page will pick it up + return undefined; +} + +type Props = { + release: Release; + organization: Organization; + activeDisplay: ReleasesDisplayOption; + location: Location; + selection: GlobalSelection; + reloading: boolean; + showHealthPlaceholders: boolean; + isTopRelease: boolean; + getHealthData: ReleasesRequestRenderProps['getHealthData']; + showReleaseAdoptionStages: boolean; +}; + +class ReleaseCard extends Component { + shouldComponentUpdate(nextProps: Props) { + // we don't want project health rows to reorder/jump while the whole card is loading + if (this.props.reloading && nextProps.reloading) { + return false; + } + + return true; + } + + render() { + const { + release, + organization, + activeDisplay, + location, + reloading, + selection, + showHealthPlaceholders, + isTopRelease, + getHealthData, + showReleaseAdoptionStages, + } = this.props; + const {version, commitCount, lastDeploy, dateCreated, versionInfo} = release; + + // sort health rows inside release card alphabetically by project name, + // show only the ones that are selected in global header + const [projectsToShow, projectsToHide] = partition( + release.projects.sort((a, b) => a.slug.localeCompare(b.slug)), + p => + // do not filter for My Projects & All Projects + selection.projects.length > 0 && !selection.projects.includes(-1) + ? selection.projects.includes(p.id) + : true + ); + + function getHiddenProjectsTooltip() { + const limitedProjects = projectsToHide.map(p => p.slug).slice(0, 5); + const remainderLength = projectsToHide.length - limitedProjects.length; + + if (remainderLength) { + limitedProjects.push(tn('and %s more', 'and %s more', remainderLength)); + } + + return limitedProjects.join(', '); + } + + return ( + + + + + + + + + + + {commitCount > 0 && ( + + )} + + + {versionInfo?.package && ( + {versionInfo.package} + )} + + {lastDeploy?.dateFinished && ` \u007C ${lastDeploy.environment}`} + + + + + + + {t('Project Name')} + {showReleaseAdoptionStages && ( + {t('Adoption Stage')} + )} + + {t('Adoption')} + + + {t('Crash Free Rate')} + {t('Crashes')} + {t('New Issues')} + + + + + ( + + + + )} + collapseButton={({onCollapse}) => ( + + + + )} + > + {projectsToShow.map((project, index) => ( + + ))} + + + + {projectsToHide.length > 0 && ( + + + + {projectsToHide.length === 1 + ? tct('[number:1] hidden project', {number: }) + : tct('[number] hidden projects', { + number: {projectsToHide.length}, + })} + + + + )} + + + ); + } +} + +const VersionWrapper = styled('div')` + display: flex; + align-items: center; +`; + +const StyledVersion = styled(Version)` + ${overflowEllipsis}; +`; + +const StyledPanel = styled(Panel)<{reloading: number}>` + opacity: ${p => (p.reloading ? 0.5 : 1)}; + pointer-events: ${p => (p.reloading ? 'none' : 'auto')}; + + @media (min-width: ${p => p.theme.breakpoints[1]}) { + display: flex; + } +`; + +const ReleaseInfo = styled('div')` + padding: ${space(1.5)} ${space(2)}; + flex-shrink: 0; + + @media (min-width: ${p => p.theme.breakpoints[1]}) { + border-right: 1px solid ${p => p.theme.border}; + min-width: 260px; + width: 22%; + max-width: 300px; + } +`; + +const ReleaseInfoSubheader = styled('div')` + font-size: ${p => p.theme.fontSizeSmall}; + color: ${p => p.theme.gray400}; +`; + +const PackageName = styled(TextOverflow)` + font-size: ${p => p.theme.fontSizeMedium}; + color: ${p => p.theme.textColor}; +`; + +const ReleaseProjects = styled('div')` + border-top: 1px solid ${p => p.theme.border}; + flex-grow: 1; + display: grid; + + @media (min-width: ${p => p.theme.breakpoints[1]}) { + border-top: none; + } +`; + +const ReleaseInfoHeader = styled('div')` + font-size: ${p => p.theme.fontSizeExtraLarge}; + display: grid; + grid-template-columns: minmax(0, 1fr) max-content; + grid-gap: ${space(2)}; + align-items: center; +`; + +const ReleaseProjectsHeader = styled(PanelHeader)` + border-top-left-radius: 0; + padding: ${space(1.5)} ${space(2)}; + font-size: ${p => p.theme.fontSizeSmall}; +`; + +const ProjectRows = styled('div')` + position: relative; +`; + +const ExpandButtonWrapper = styled('div')` + position: absolute; + width: 100%; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; + background-image: linear-gradient( + 180deg, + ${p => color(p.theme.background).alpha(0).string()} 0, + ${p => p.theme.background} + ); + background-repeat: repeat-x; + border-bottom: ${space(1)} solid ${p => p.theme.background}; + border-top: ${space(1)} solid transparent; + border-bottom-right-radius: ${p => p.theme.borderRadius}; + @media (max-width: ${p => p.theme.breakpoints[1]}) { + border-bottom-left-radius: ${p => p.theme.borderRadius}; + } +`; + +const CollapseButtonWrapper = styled('div')` + display: flex; + align-items: center; + justify-content: center; + height: 41px; +`; + +export const ReleaseProjectsLayout = styled('div')<{showReleaseAdoptionStages?: boolean}>` + display: grid; + grid-template-columns: 1fr 1.4fr 0.6fr 0.7fr; + + grid-column-gap: ${space(1)}; + align-items: center; + width: 100%; + + @media (min-width: ${p => p.theme.breakpoints[0]}) { + grid-template-columns: 1fr 1fr 1fr 0.5fr 0.5fr 0.5fr; + } + + @media (min-width: ${p => p.theme.breakpoints[1]}) { + grid-template-columns: 1fr 1fr 1fr 0.5fr 0.5fr 0.5fr; + } + + @media (min-width: ${p => p.theme.breakpoints[3]}) { + ${p => + p.showReleaseAdoptionStages + ? ` + grid-template-columns: 1fr 0.7fr 1fr 1fr 0.7fr 0.7fr 0.5fr; + ` + : ` + grid-template-columns: 1fr 1fr 1fr 0.7fr 0.7fr 0.5fr; + `} + } +`; + +export const ReleaseProjectColumn = styled('div')` + ${overflowEllipsis}; + line-height: 20px; +`; + +export const NewIssuesColumn = styled(ReleaseProjectColumn)` + font-variant-numeric: tabular-nums; + + @media (min-width: ${p => p.theme.breakpoints[0]}) { + text-align: right; + } +`; + +export const AdoptionColumn = styled(ReleaseProjectColumn)` + display: none; + font-variant-numeric: tabular-nums; + + @media (min-width: ${p => p.theme.breakpoints[0]}) { + display: flex; + /* Chart tooltips need overflow */ + overflow: visible; + } + + & > * { + flex: 1; + } +`; + +export const AdoptionStageColumn = styled(ReleaseProjectColumn)` + display: none; + font-variant-numeric: tabular-nums; + + @media (min-width: ${p => p.theme.breakpoints[3]}) { + display: flex; + + /* Need to show the edges of the tags */ + overflow: visible; + } +`; + +export const CrashFreeRateColumn = styled(ReleaseProjectColumn)` + font-variant-numeric: tabular-nums; + + @media (min-width: ${p => p.theme.breakpoints[0]}) { + text-align: center; + } + + @media (min-width: ${p => p.theme.breakpoints[3]}) { + text-align: right; + } +`; + +export const CrashesColumn = styled(ReleaseProjectColumn)` + display: none; + font-variant-numeric: tabular-nums; + + @media (min-width: ${p => p.theme.breakpoints[0]}) { + display: block; + text-align: right; + } +`; + +const HiddenProjectsMessage = styled('div')` + display: flex; + align-items: center; + font-size: ${p => p.theme.fontSizeSmall}; + padding: 0 ${space(2)}; + border-top: 1px solid ${p => p.theme.border}; + overflow: hidden; + height: 24px; + line-height: 24px; + color: ${p => p.theme.gray300}; + background-color: ${p => p.theme.backgroundSecondary}; + border-bottom-right-radius: ${p => p.theme.borderRadius}; + @media (max-width: ${p => p.theme.breakpoints[1]}) { + border-bottom-left-radius: ${p => p.theme.borderRadius}; + } +`; + +export default ReleaseCard; diff --git a/static/app/components/releaseStats.tsx b/static/app/views/releases/list/releaseCard/releaseCardCommits.tsx similarity index 91% rename from static/app/components/releaseStats.tsx rename to static/app/views/releases/list/releaseCard/releaseCardCommits.tsx index 1f1c1a7f74e1d0..07fcce847f5bf6 100644 --- a/static/app/components/releaseStats.tsx +++ b/static/app/views/releases/list/releaseCard/releaseCardCommits.tsx @@ -10,7 +10,7 @@ type Props = { withHeading: boolean; }; -const ReleaseStats = ({release, withHeading = true}: Props) => { +const ReleaseCardCommits = ({release, withHeading = true}: Props) => { const commitCount = release.commitCount || 0; const authorCount = (release.authors && release.authors.length) || 0; if (commitCount === 0) { @@ -42,4 +42,4 @@ const ReleaseSummaryHeading = styled('div')` margin-bottom: ${space(0.5)}; `; -export default ReleaseStats; +export default ReleaseCardCommits; diff --git a/static/app/views/releases/list/releaseCard/releaseCardProjectRow.tsx b/static/app/views/releases/list/releaseCard/releaseCardProjectRow.tsx new file mode 100644 index 00000000000000..d0f9ce9c6a271c --- /dev/null +++ b/static/app/views/releases/list/releaseCard/releaseCardProjectRow.tsx @@ -0,0 +1,258 @@ +import LazyLoad from 'react-lazyload'; +import {useTheme} from '@emotion/react'; +import styled from '@emotion/styled'; +import {Location} from 'history'; + +import GuideAnchor from 'app/components/assistant/guideAnchor'; +import Button from 'app/components/button'; +import MiniBarChart from 'app/components/charts/miniBarChart'; +import Count from 'app/components/count'; +import GlobalSelectionLink from 'app/components/globalSelectionLink'; +import ProjectBadge from 'app/components/idBadge/projectBadge'; +import Link from 'app/components/links/link'; +import NotAvailable from 'app/components/notAvailable'; +import {extractSelectionParameters} from 'app/components/organizations/globalSelectionHeader/utils'; +import {PanelItem} from 'app/components/panels'; +import Placeholder from 'app/components/placeholder'; +import Tag from 'app/components/tag'; +import Tooltip from 'app/components/tooltip'; +import {t, tn} from 'app/locale'; +import overflowEllipsis from 'app/styles/overflowEllipsis'; +import space from 'app/styles/space'; +import {Organization, Release, ReleaseProject} from 'app/types'; +import {defined} from 'app/utils'; +import {getCrashFreeIcon} from 'app/utils/sessions'; + +import { + ADOPTION_STAGE_LABELS, + displayCrashFreePercent, + getReleaseNewIssuesUrl, + getReleaseUnhandledIssuesUrl, + isMobileRelease, +} from '../../utils'; +import {ReleasesDisplayOption} from '../releasesDisplayOptions'; +import {ReleasesRequestRenderProps} from '../releasesRequest'; + +import { + AdoptionColumn, + AdoptionStageColumn, + CrashesColumn, + CrashFreeRateColumn, + NewIssuesColumn, + ReleaseProjectColumn, + ReleaseProjectsLayout, +} from '.'; + +type Props = { + index: number; + organization: Organization; + project: ReleaseProject; + location: Location; + getHealthData: ReleasesRequestRenderProps['getHealthData']; + releaseVersion: string; + activeDisplay: ReleasesDisplayOption; + showPlaceholders: boolean; + showReleaseAdoptionStages: boolean; + isTopRelease: boolean; + adoptionStages?: Release['adoptionStages']; +}; + +function ReleaseCardProjectRow({ + index, + project, + organization, + location, + getHealthData, + releaseVersion, + activeDisplay, + showPlaceholders, + showReleaseAdoptionStages, + isTopRelease, + adoptionStages, +}: Props) { + const theme = useTheme(); + const {id, newGroups} = project; + + const crashCount = getHealthData.getCrashCount( + releaseVersion, + id, + ReleasesDisplayOption.SESSIONS + ); + const crashFreeRate = getHealthData.getCrashFreeRate(releaseVersion, id, activeDisplay); + const get24hCountByProject = getHealthData.get24hCountByProject(id, activeDisplay); + const timeSeries = getHealthData.getTimeSeries(releaseVersion, id, activeDisplay); + const adoption = getHealthData.getAdoption(releaseVersion, id, activeDisplay); + + const adoptionStage = + showReleaseAdoptionStages && + adoptionStages?.[project.slug] && + adoptionStages?.[project.slug].stage; + + const adoptionStageLabel = + Boolean(get24hCountByProject && adoptionStage && isMobileRelease(project.platform)) && + ADOPTION_STAGE_LABELS[adoptionStage]; + + return ( + + + + + + + {showReleaseAdoptionStages && ( + + {adoptionStageLabel ? ( + + + {adoptionStageLabel.name} + + + ) : ( + + )} + + )} + + + {showPlaceholders ? ( + + ) : ( + + {adoption ? Math.round(adoption) : '0'}% + + { + const suffix = + activeDisplay === ReleasesDisplayOption.USERS + ? tn('user', 'users', value) + : tn('session', 'sessions', value); + + return `${value.toLocaleString()} ${suffix}`; + }} + colors={[theme.purple300, theme.gray200]} + /> + + + )} + + + + {showPlaceholders ? ( + + ) : defined(crashFreeRate) ? ( + + {getCrashFreeIcon(crashFreeRate)} + {displayCrashFreePercent(crashFreeRate)} + + ) : ( + + )} + + + + {showPlaceholders ? ( + + ) : defined(crashCount) ? ( + + + + + + ) : ( + + )} + + + + + + + + + + + + + + + + + + ); +} + +export default ReleaseCardProjectRow; + +const ProjectRow = styled(PanelItem)` + padding: ${space(1)} ${space(2)}; + @media (min-width: ${p => p.theme.breakpoints[1]}) { + font-size: ${p => p.theme.fontSizeMedium}; + } +`; + +const StyledPlaceholder = styled(Placeholder)` + height: 15px; + display: inline-block; + position: relative; + top: ${space(0.25)}; +`; + +const AdoptionWrapper = styled('span')` + flex: 1; + display: inline-grid; + grid-template-columns: 30px 1fr; + grid-gap: ${space(1)}; + align-items: center; + + /* Chart tooltips need overflow */ + overflow: visible; +`; + +const CrashFreeWrapper = styled('div')` + display: inline-grid; + grid-auto-flow: column; + grid-column-gap: ${space(1)}; + align-items: center; + vertical-align: middle; +`; + +const ViewColumn = styled('div')` + ${overflowEllipsis}; + line-height: 20px; + text-align: right; +`; diff --git a/static/app/views/releases/list/healthStatsPeriod.tsx b/static/app/views/releases/list/releaseCard/releaseCardStatsPeriod.tsx similarity index 93% rename from static/app/views/releases/list/healthStatsPeriod.tsx rename to static/app/views/releases/list/releaseCard/releaseCardStatsPeriod.tsx index 383d86754acd34..fdb81e1d027794 100644 --- a/static/app/views/releases/list/healthStatsPeriod.tsx +++ b/static/app/views/releases/list/releaseCard/releaseCardStatsPeriod.tsx @@ -12,7 +12,7 @@ type Props = { selection: GlobalSelection; }; -const HealthStatsPeriod = ({location, selection}: Props) => { +const ReleaseCardStatsPeriod = ({location, selection}: Props) => { const activePeriod = location.query.healthStatsPeriod || HealthStatsPeriodOption.TWENTY_FOUR_HOURS; const {pathname, query} = location; @@ -66,4 +66,4 @@ const Period = styled(Link)<{selected: boolean}>` } `; -export default withGlobalSelection(HealthStatsPeriod); +export default withGlobalSelection(ReleaseCardStatsPeriod); diff --git a/static/app/views/releases/list/releaseDisplayOptions.tsx b/static/app/views/releases/list/releaseDisplayOptions.tsx deleted file mode 100644 index 457f389d2ae8ce..00000000000000 --- a/static/app/views/releases/list/releaseDisplayOptions.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import styled from '@emotion/styled'; - -import {t} from 'app/locale'; - -import ReleaseListDropdown from './releaseListDropdown'; -import {DisplayOption} from './utils'; - -const displayOptions = { - [DisplayOption.SESSIONS]: {label: t('Sessions')}, - [DisplayOption.USERS]: {label: t('Users')}, -}; - -type Props = { - selected: DisplayOption; - onSelect: (key: string) => void; -}; - -function ReleaseListDisplayOptions({selected, onSelect}: Props) { - return ( - - ); -} - -export default ReleaseListDisplayOptions; - -const StyledReleaseListDropdown = styled(ReleaseListDropdown)` - z-index: 1; - @media (max-width: ${p => p.theme.breakpoints[2]}) { - order: 3; - } -`; diff --git a/static/app/views/releases/list/releaseHealth/content.tsx b/static/app/views/releases/list/releaseHealth/content.tsx deleted file mode 100644 index e67f37f0a19996..00000000000000 --- a/static/app/views/releases/list/releaseHealth/content.tsx +++ /dev/null @@ -1,427 +0,0 @@ -import {Fragment} from 'react'; -import styled from '@emotion/styled'; -import color from 'color'; -import {Location} from 'history'; - -import GuideAnchor from 'app/components/assistant/guideAnchor'; -import Button from 'app/components/button'; -import Collapsible from 'app/components/collapsible'; -import Count from 'app/components/count'; -import GlobalSelectionLink from 'app/components/globalSelectionLink'; -import ProjectBadge from 'app/components/idBadge/projectBadge'; -import ExternalLink from 'app/components/links/externalLink'; -import Link from 'app/components/links/link'; -import NotAvailable from 'app/components/notAvailable'; -import {PanelItem} from 'app/components/panels'; -import Placeholder from 'app/components/placeholder'; -import Tag from 'app/components/tag'; -import Tooltip from 'app/components/tooltip'; -import {t, tct} from 'app/locale'; -import overflowEllipsis from 'app/styles/overflowEllipsis'; -import space from 'app/styles/space'; -import {Organization, Release, ReleaseProject} from 'app/types'; -import {defined} from 'app/utils'; -import {Theme} from 'app/utils/theme'; -import {isProjectMobileForReleases} from 'app/views/releases/list'; - -import {getReleaseNewIssuesUrl, getReleaseUnhandledIssuesUrl} from '../../utils'; -import CrashFree from '../crashFree'; -import HealthStatsChart from '../healthStatsChart'; -import HealthStatsPeriod from '../healthStatsPeriod'; -import {ReleaseListRequestRenderProps} from '../releaseListRequest'; -import {DisplayOption} from '../utils'; - -import Header from './header'; -import ProjectLink from './projectLink'; - -const adoptionStagesLink = ( - -); - -export const ADOPTION_STAGE_LABELS: Record< - string, - {name: string; tooltipTitle: JSX.Element; type: keyof Theme['tag']} -> = { - low_adoption: { - name: t('Low Adoption'), - tooltipTitle: tct( - 'This release has a low percentage of sessions compared to other releases in this project. [link:Learn more]', - {link: adoptionStagesLink} - ), - type: 'warning', - }, - adopted: { - name: t('Adopted'), - tooltipTitle: tct( - 'This release has a high percentage of sessions compared to other releases in this project. [link:Learn more]', - {link: adoptionStagesLink} - ), - type: 'success', - }, - replaced: { - name: t('Replaced'), - tooltipTitle: tct( - 'This release was previously Adopted, but now has a lower level of sessions compared to other releases in this project. [link:Learn more]', - {link: adoptionStagesLink} - ), - type: 'default', - }, -}; - -type Props = { - projects: Array; - releaseVersion: Release['version']; - organization: Organization; - activeDisplay: DisplayOption; - location: Location; - showPlaceholders: boolean; - isTopRelease: boolean; - getHealthData: ReleaseListRequestRenderProps['getHealthData']; - showReleaseAdoptionStages: boolean; - adoptionStages?: Release['adoptionStages']; -}; - -const Content = ({ - projects, - showReleaseAdoptionStages, - adoptionStages, - releaseVersion, - location, - organization, - activeDisplay, - showPlaceholders, - isTopRelease, - getHealthData, -}: Props) => ( - -
- - {t('Project Name')} - {showReleaseAdoptionStages && ( - {t('Adoption Stage')} - )} - - {t('Adoption')} - - - {t('Crash Free Rate')} - {t('Crashes')} - {t('New Issues')} - -
- - - ( - - - - )} - collapseButton={({onCollapse}) => ( - - - - )} - > - {projects.map((project, index) => { - const {id, slug, newGroups} = project; - - const crashCount = getHealthData.getCrashCount( - releaseVersion, - id, - DisplayOption.SESSIONS - ); - const crashFreeRate = getHealthData.getCrashFreeRate( - releaseVersion, - id, - activeDisplay - ); - const get24hCountByProject = getHealthData.get24hCountByProject( - id, - activeDisplay - ); - const timeSeries = getHealthData.getTimeSeries( - releaseVersion, - id, - activeDisplay - ); - const adoption = getHealthData.getAdoption(releaseVersion, id, activeDisplay); - - const adoptionStage = - showReleaseAdoptionStages && - adoptionStages?.[project.slug] && - adoptionStages?.[project.slug].stage; - - const isMobileProject = isProjectMobileForReleases(project.platform); - const adoptionStageLabel = - Boolean(get24hCountByProject && adoptionStage && isMobileProject) && - ADOPTION_STAGE_LABELS[adoptionStage]; - - return ( - - - - - - - {showReleaseAdoptionStages && ( - - {adoptionStageLabel ? ( - - - - {adoptionStageLabel.name} - - - - ) : ( - - )} - - )} - - - {showPlaceholders ? ( - - ) : ( - - {adoption ? Math.round(adoption) : '0'}% - - - )} - - - - {showPlaceholders ? ( - - ) : defined(crashFreeRate) ? ( - - ) : ( - - )} - - - - {showPlaceholders ? ( - - ) : defined(crashCount) ? ( - - - - - - ) : ( - - )} - - - - - - - - - - - - - - - - - - ); - })} - - -
-); - -export default Content; - -const ProjectRows = styled('div')` - position: relative; -`; - -const ExpandButtonWrapper = styled('div')` - position: absolute; - width: 100%; - bottom: 0; - display: flex; - align-items: center; - justify-content: center; - background-image: linear-gradient( - 180deg, - ${p => color(p.theme.background).alpha(0).string()} 0, - ${p => p.theme.background} - ); - background-repeat: repeat-x; - border-bottom: ${space(1)} solid ${p => p.theme.background}; - border-top: ${space(1)} solid transparent; - border-bottom-right-radius: ${p => p.theme.borderRadius}; - @media (max-width: ${p => p.theme.breakpoints[1]}) { - border-bottom-left-radius: ${p => p.theme.borderRadius}; - } -`; - -const CollapseButtonWrapper = styled('div')` - display: flex; - align-items: center; - justify-content: center; - height: 41px; -`; - -const ProjectRow = styled(PanelItem)` - padding: ${space(1)} ${space(2)}; - @media (min-width: ${p => p.theme.breakpoints[1]}) { - font-size: ${p => p.theme.fontSizeMedium}; - } -`; - -const Layout = styled('div')<{showReleaseAdoptionStages?: boolean}>` - display: grid; - grid-template-columns: 1fr 1.4fr 0.6fr 0.7fr; - - grid-column-gap: ${space(1)}; - align-items: center; - width: 100%; - - @media (min-width: ${p => p.theme.breakpoints[0]}) { - grid-template-columns: 1fr 1fr 1fr 0.5fr 0.5fr 0.5fr; - } - - @media (min-width: ${p => p.theme.breakpoints[1]}) { - grid-template-columns: 1fr 1fr 1fr 0.5fr 0.5fr 0.5fr; - } - - @media (min-width: ${p => p.theme.breakpoints[3]}) { - ${p => - p.showReleaseAdoptionStages - ? ` - grid-template-columns: 1fr 0.7fr 1fr 1fr 0.7fr 0.7fr 0.5fr; - ` - : ` - grid-template-columns: 1fr 1fr 1fr 0.7fr 0.7fr 0.5fr; - `} - } -`; - -const Column = styled('div')` - ${overflowEllipsis}; - line-height: 20px; -`; - -const NewIssuesColumn = styled(Column)` - font-variant-numeric: tabular-nums; - - @media (min-width: ${p => p.theme.breakpoints[0]}) { - text-align: right; - } -`; - -const AdoptionColumn = styled(Column)` - display: none; - font-variant-numeric: tabular-nums; - - @media (min-width: ${p => p.theme.breakpoints[0]}) { - display: flex; - /* Chart tooltips need overflow */ - overflow: visible; - } - - & > * { - flex: 1; - } -`; - -const AdoptionStageColumn = styled(Column)` - display: none; - font-variant-numeric: tabular-nums; - - @media (min-width: ${p => p.theme.breakpoints[3]}) { - display: flex; - - /* Need to show the edges of the tags */ - overflow: visible; - } -`; - -const AdoptionWrapper = styled('span')` - flex: 1; - display: inline-grid; - grid-template-columns: 30px 1fr; - grid-gap: ${space(1)}; - align-items: center; - - /* Chart tooltips need overflow */ - overflow: visible; -`; - -const CrashFreeRateColumn = styled(Column)` - font-variant-numeric: tabular-nums; - - @media (min-width: ${p => p.theme.breakpoints[0]}) { - text-align: center; - } - - @media (min-width: ${p => p.theme.breakpoints[3]}) { - text-align: right; - } -`; - -const CrashesColumn = styled(Column)` - display: none; - font-variant-numeric: tabular-nums; - - @media (min-width: ${p => p.theme.breakpoints[0]}) { - display: block; - text-align: right; - } -`; - -const ViewColumn = styled(Column)` - text-align: right; -`; - -const StyledPlaceholder = styled(Placeholder)` - height: 15px; - display: inline-block; - position: relative; - top: ${space(0.25)}; -`; diff --git a/static/app/views/releases/list/releaseHealth/header.tsx b/static/app/views/releases/list/releaseHealth/header.tsx deleted file mode 100644 index b367093d4fcdd3..00000000000000 --- a/static/app/views/releases/list/releaseHealth/header.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import styled from '@emotion/styled'; - -import {PanelHeader} from 'app/components/panels'; -import space from 'app/styles/space'; - -const Header = styled(PanelHeader)` - border-top-left-radius: 0; - padding: ${space(1.5)} ${space(2)}; - font-size: ${p => p.theme.fontSizeSmall}; -`; - -export default Header; diff --git a/static/app/views/releases/list/releaseHealth/index.tsx b/static/app/views/releases/list/releaseHealth/index.tsx deleted file mode 100644 index f639dc8cff57ac..00000000000000 --- a/static/app/views/releases/list/releaseHealth/index.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import {Component, Fragment} from 'react'; -import styled from '@emotion/styled'; -import {Location} from 'history'; -import partition from 'lodash/partition'; - -import TextOverflow from 'app/components/textOverflow'; -import Tooltip from 'app/components/tooltip'; -import {tct, tn} from 'app/locale'; -import space from 'app/styles/space'; -import {GlobalSelection, Organization, Release} from 'app/types'; - -import {ReleaseListRequestRenderProps} from '../releaseListRequest'; -import {DisplayOption} from '../utils'; - -import Content from './content'; - -type Props = { - release: Release; - organization: Organization; - activeDisplay: DisplayOption; - location: Location; - showPlaceholders: boolean; - selection: GlobalSelection; - reloading: boolean; - isTopRelease: boolean; - getHealthData: ReleaseListRequestRenderProps['getHealthData']; - showReleaseAdoptionStages: boolean; -}; - -class ReleaseHealth extends Component { - shouldComponentUpdate(nextProps: Props) { - // we don't want project health rows to reorder/jump while the whole card is loading - if (this.props.reloading && nextProps.reloading) { - return false; - } - - return true; - } - - render() { - const { - release, - organization, - activeDisplay, - location, - showPlaceholders, - selection, - isTopRelease, - getHealthData, - showReleaseAdoptionStages, - } = this.props; - - // sort health rows inside release card alphabetically by project name, - // show only the ones that are selected in global header - const [projectsToShow, projectsToHide] = partition( - release.projects.sort((a, b) => a.slug.localeCompare(b.slug)), - p => - // do not filter for My Projects & All Projects - selection.projects.length > 0 && !selection.projects.includes(-1) - ? selection.projects.includes(p.id) - : true - ); - - function getHiddenProjectsTooltip() { - const limitedProjects = projectsToHide.map(p => p.slug).slice(0, 5); - const remainderLength = projectsToHide.length - limitedProjects.length; - - if (remainderLength) { - limitedProjects.push(tn('and %s more', 'and %s more', remainderLength)); - } - - return limitedProjects.join(', '); - } - - return ( - - - - {projectsToHide.length > 0 && ( - - - - {projectsToHide.length === 1 - ? tct('[number:1] hidden project', {number: }) - : tct('[number] hidden projects', { - number: {projectsToHide.length}, - })} - - - - )} - - ); - } -} - -const HiddenProjectsMessage = styled('div')` - display: flex; - align-items: center; - font-size: ${p => p.theme.fontSizeSmall}; - padding: 0 ${space(2)}; - border-top: 1px solid ${p => p.theme.border}; - overflow: hidden; - height: 24px; - line-height: 24px; - color: ${p => p.theme.gray300}; - background-color: ${p => p.theme.backgroundSecondary}; - border-bottom-right-radius: ${p => p.theme.borderRadius}; - @media (max-width: ${p => p.theme.breakpoints[1]}) { - border-bottom-left-radius: ${p => p.theme.borderRadius}; - } -`; - -export default ReleaseHealth; diff --git a/static/app/views/releases/list/releaseHealth/issuesQuantity.tsx b/static/app/views/releases/list/releaseHealth/issuesQuantity.tsx deleted file mode 100644 index 655d8b23b8710c..00000000000000 --- a/static/app/views/releases/list/releaseHealth/issuesQuantity.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import styled from '@emotion/styled'; - -import Count from 'app/components/count'; -import Link from 'app/components/links/link'; -import Tooltip from 'app/components/tooltip'; -import {t, tn} from 'app/locale'; -import overflowEllipsis from 'app/styles/overflowEllipsis'; -import space from 'app/styles/space'; - -import {getReleaseNewIssuesUrl} from '../../utils'; - -type Props = { - orgSlug: string; - newGroups: number; - projectId: number; - releaseVersion: string; - isCompact?: boolean; -}; - -const IssuesQuantity = ({ - orgSlug, - newGroups, - projectId, - releaseVersion, - isCompact = false, -}: Props) => ( - - - {isCompact ? ( - - - {tn('issue', 'issues', newGroups)} - - ) : ( - - )} - - -); - -export default IssuesQuantity; - -const Issues = styled('div')` - display: grid; - grid-gap: ${space(0.5)}; - grid-template-columns: auto max-content; - justify-content: flex-end; - align-items: center; - text-align: end; -`; - -// overflowEllipsis is useful if the count's value is over 1000000000 -const StyledCount = styled(Count)` - ${overflowEllipsis} -`; diff --git a/static/app/views/releases/list/releaseHealth/projectLink.tsx b/static/app/views/releases/list/releaseHealth/projectLink.tsx deleted file mode 100644 index 5b93c2bff0eec2..00000000000000 --- a/static/app/views/releases/list/releaseHealth/projectLink.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import {Location} from 'history'; - -import Button from 'app/components/button'; -import {extractSelectionParameters} from 'app/components/organizations/globalSelectionHeader/utils'; -import {t} from 'app/locale'; -import {ReleaseProject} from 'app/types'; - -type Props = { - orgSlug: string; - releaseVersion: string; - project: ReleaseProject; - location: Location; -}; - -const ProjectLink = ({orgSlug, releaseVersion, project, location}: Props) => ( - -); - -export default ProjectLink; diff --git a/static/app/views/releases/list/releaseListSortOptions.tsx b/static/app/views/releases/list/releaseListSortOptions.tsx deleted file mode 100644 index deed8c8401577d..00000000000000 --- a/static/app/views/releases/list/releaseListSortOptions.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import {ComponentProps} from 'react'; -import styled from '@emotion/styled'; - -import {t} from 'app/locale'; -import {Organization} from 'app/types'; - -import ReleaseListDropdown from './releaseListDropdown'; -import {DisplayOption, SortOption} from './utils'; - -type Props = { - selected: SortOption; - selectedDisplay: DisplayOption; - onSelect: (key: string) => void; - organization: Organization; - environments: string[]; -}; - -function ReleaseListSortOptions({ - selected, - selectedDisplay, - onSelect, - organization, - environments, -}: Props) { - const sortOptions = { - [SortOption.DATE]: {label: t('Date Created')}, - [SortOption.SESSIONS]: {label: t('Total Sessions')}, - ...(selectedDisplay === DisplayOption.USERS - ? { - [SortOption.USERS_24_HOURS]: {label: t('Active Users')}, - [SortOption.CRASH_FREE_USERS]: {label: t('Crash Free Users')}, - } - : { - [SortOption.SESSIONS_24_HOURS]: {label: t('Active Sessions')}, - [SortOption.CRASH_FREE_SESSIONS]: {label: t('Crash Free Sessions')}, - }), - } as ComponentProps['options']; - - if (organization.features.includes('semver')) { - sortOptions[SortOption.BUILD] = {label: t('Build Number')}; - sortOptions[SortOption.SEMVER] = {label: t('Semantic Version')}; - } - - if (organization.features.includes('release-adoption-stage')) { - const isDisabled = environments.length !== 1; - sortOptions[SortOption.ADOPTION] = { - label: t('Date Adopted'), - disabled: isDisabled, - tooltip: isDisabled - ? t('Select one environment to use this sort option.') - : undefined, - }; - } - - return ( - - ); -} - -export default ReleaseListSortOptions; - -const StyledReleaseListDropdown = styled(ReleaseListDropdown)` - z-index: 2; - @media (max-width: ${p => p.theme.breakpoints[2]}) { - order: 2; - } -`; diff --git a/static/app/views/releases/list/releaseListStatusOptions.tsx b/static/app/views/releases/list/releaseListStatusOptions.tsx deleted file mode 100644 index 3fdff73ba5acd4..00000000000000 --- a/static/app/views/releases/list/releaseListStatusOptions.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import styled from '@emotion/styled'; - -import {t} from 'app/locale'; - -import ReleaseListDropdown from './releaseListDropdown'; -import {StatusOption} from './utils'; - -const options = { - [StatusOption.ACTIVE]: {label: t('Active')}, - [StatusOption.ARCHIVED]: {label: t('Archived')}, -}; - -type Props = { - selected: StatusOption; - onSelect: (key: string) => void; -}; - -function ReleaseListStatusOptions({selected, onSelect}: Props) { - return ( - - ); -} - -export default ReleaseListStatusOptions; - -const StyledReleaseListDropdown = styled(ReleaseListDropdown)` - z-index: 3; - @media (max-width: ${p => p.theme.breakpoints[2]}) { - order: 1; - } -`; diff --git a/static/app/views/releases/list/releaseAdoptionChart.tsx b/static/app/views/releases/list/releasesAdoptionChart.tsx similarity index 96% rename from static/app/views/releases/list/releaseAdoptionChart.tsx rename to static/app/views/releases/list/releasesAdoptionChart.tsx index f3af90d51afa0c..fd85ae5ae63963 100644 --- a/static/app/views/releases/list/releaseAdoptionChart.tsx +++ b/static/app/views/releases/list/releasesAdoptionChart.tsx @@ -39,19 +39,20 @@ import {formatVersion} from 'app/utils/formatters'; import {decodeScalar} from 'app/utils/queryString'; import {getAdoptionSeries, getCount} from 'app/utils/sessions'; import withApi from 'app/utils/withApi'; -import {sessionDisplayToField} from 'app/views/releases/list/releaseListRequest'; -import {DisplayOption} from 'app/views/releases/list/utils'; +import {sessionDisplayToField} from 'app/views/releases/list/releasesRequest'; + +import {ReleasesDisplayOption} from './releasesDisplayOptions'; type Props = { api: Client; organization: Organization; selection: GlobalSelection; - activeDisplay: DisplayOption; + activeDisplay: ReleasesDisplayOption; location: Location; router: InjectedRouter; }; -class ReleaseAdoptionChart extends Component { +class ReleasesAdoptionChart extends Component { // needs to have different granularity, that's why we use custom getInterval instead of getSessionsInterval getInterval() { const {organization, location} = this.props; @@ -279,7 +280,9 @@ class ReleaseAdoptionChart extends Component { {tct('Total [display]', { display: - activeDisplay === DisplayOption.USERS ? 'Users' : 'Sessions', + activeDisplay === ReleasesDisplayOption.USERS + ? 'Users' + : 'Sessions', })} @@ -295,7 +298,7 @@ class ReleaseAdoptionChart extends Component { } } -export default withApi(ReleaseAdoptionChart); +export default withApi(ReleasesAdoptionChart); const ChartHeader = styled(HeaderTitleLegend)` margin-bottom: ${space(1)}; diff --git a/static/app/views/releases/list/releasesDisplayOptions.tsx b/static/app/views/releases/list/releasesDisplayOptions.tsx new file mode 100644 index 00000000000000..c9a3bfc40c023d --- /dev/null +++ b/static/app/views/releases/list/releasesDisplayOptions.tsx @@ -0,0 +1,40 @@ +import styled from '@emotion/styled'; + +import {t} from 'app/locale'; + +import ReleasesDropdown from './releasesDropdown'; + +export enum ReleasesDisplayOption { + USERS = 'users', + SESSIONS = 'sessions', +} + +const displayOptions = { + [ReleasesDisplayOption.SESSIONS]: {label: t('Sessions')}, + [ReleasesDisplayOption.USERS]: {label: t('Users')}, +}; + +type Props = { + selected: ReleasesDisplayOption; + onSelect: (key: string) => void; +}; + +function ReleasesDisplayOptions({selected, onSelect}: Props) { + return ( + + ); +} + +export default ReleasesDisplayOptions; + +const StyledReleasesDropdown = styled(ReleasesDropdown)` + z-index: 1; + @media (max-width: ${p => p.theme.breakpoints[2]}) { + order: 3; + } +`; diff --git a/static/app/views/releases/list/releaseListDropdown.tsx b/static/app/views/releases/list/releasesDropdown.tsx similarity index 95% rename from static/app/views/releases/list/releaseListDropdown.tsx rename to static/app/views/releases/list/releasesDropdown.tsx index 400f1a6a9f1225..ba464174c8ffbb 100644 --- a/static/app/views/releases/list/releaseListDropdown.tsx +++ b/static/app/views/releases/list/releasesDropdown.tsx @@ -19,7 +19,7 @@ type Props = { className?: string; }; -const ReleaseListDropdown = ({ +const ReleasesDropdown = ({ label: prefix, options, selected, @@ -58,4 +58,4 @@ const ReleaseListDropdown = ({ ); }; -export default ReleaseListDropdown; +export default ReleasesDropdown; diff --git a/static/app/views/releases/list/releasePromo.tsx b/static/app/views/releases/list/releasesPromo.tsx similarity index 98% rename from static/app/views/releases/list/releasePromo.tsx rename to static/app/views/releases/list/releasesPromo.tsx index 33a71a0c3988b5..6475c07a3e25e5 100644 --- a/static/app/views/releases/list/releasePromo.tsx +++ b/static/app/views/releases/list/releasesPromo.tsx @@ -82,7 +82,7 @@ type Props = { projectId?: number; }; -class ReleasePromo extends Component { +class ReleasesPromo extends Component { componentDidMount() { const {organization, projectId} = this.props; @@ -156,4 +156,4 @@ const ButtonList = styled(ButtonBar)` grid-template-columns: repeat(auto-fit, minmax(130px, max-content)); `; -export default ReleasePromo; +export default ReleasesPromo; diff --git a/static/app/views/releases/list/releaseListRequest.tsx b/static/app/views/releases/list/releasesRequest.tsx similarity index 90% rename from static/app/views/releases/list/releaseListRequest.tsx rename to static/app/views/releases/list/releasesRequest.tsx index 7fb1e60c007703..7279f3c89f32ef 100644 --- a/static/app/views/releases/list/releaseListRequest.tsx +++ b/static/app/views/releases/list/releasesRequest.tsx @@ -30,7 +30,7 @@ import withApi from 'app/utils/withApi'; import {getCrashFreePercent} from '../utils'; -import {DisplayOption} from './utils'; +import {ReleasesDisplayOption} from './releasesDisplayOptions'; function omitIgnoredProps(props: Props) { return omit(props, [ @@ -71,30 +71,30 @@ export function reduceTimeSeriesGroups( return acc; } -export function sessionDisplayToField(display: DisplayOption) { +export function sessionDisplayToField(display: ReleasesDisplayOption) { switch (display) { - case DisplayOption.USERS: + case ReleasesDisplayOption.USERS: return SessionField.USERS; - case DisplayOption.SESSIONS: + case ReleasesDisplayOption.SESSIONS: default: return SessionField.SESSIONS; } } -export type ReleaseListRequestRenderProps = { +export type ReleasesRequestRenderProps = { isHealthLoading: boolean; errored: boolean; - getHealthData: ReturnType; + getHealthData: ReturnType; }; type Props = { api: Client; releases: string[]; organization: Organization; - children: (renderProps: ReleaseListRequestRenderProps) => React.ReactNode; + children: (renderProps: ReleasesRequestRenderProps) => React.ReactNode; selection: GlobalSelection; location: Location; - display: DisplayOption[]; + display: ReleasesDisplayOption[]; defaultStatsPeriod?: string; releasesReloading?: boolean; healthStatsPeriod?: HealthStatsPeriodOption; @@ -111,7 +111,7 @@ type State = { totalCountByProjectInPeriod: SessionApiResponse | null; }; -class ReleaseListRequest extends React.Component { +class ReleasesRequest extends React.Component { state: State = { loading: false, errored: false, @@ -340,7 +340,7 @@ class ReleaseListRequest extends React.Component { }; }; - getCrashCount = (version: string, project: number, display: DisplayOption) => { + getCrashCount = (version: string, project: number, display: ReleasesDisplayOption) => { const {statusCountByReleaseInPeriod} = this.state; const field = sessionDisplayToField(display); @@ -352,7 +352,11 @@ class ReleaseListRequest extends React.Component { )?.totals[field]; }; - getCrashFreeRate = (version: string, project: number, display: DisplayOption) => { + getCrashFreeRate = ( + version: string, + project: number, + display: ReleasesDisplayOption + ) => { const {statusCountByReleaseInPeriod} = this.state; const field = sessionDisplayToField(display); @@ -367,7 +371,11 @@ class ReleaseListRequest extends React.Component { : getCrashFreePercent(100 - percent(crashedCount ?? 0, totalCount ?? 0)); }; - get24hCountByRelease = (version: string, project: number, display: DisplayOption) => { + get24hCountByRelease = ( + version: string, + project: number, + display: ReleasesDisplayOption + ) => { const {totalCountByReleaseIn24h} = this.state; const field = sessionDisplayToField(display); @@ -379,7 +387,7 @@ class ReleaseListRequest extends React.Component { getPeriodCountByRelease = ( version: string, project: number, - display: DisplayOption + display: ReleasesDisplayOption ) => { const {totalCountByReleaseInPeriod} = this.state; const field = sessionDisplayToField(display); @@ -389,7 +397,7 @@ class ReleaseListRequest extends React.Component { ?.reduce((acc, group) => acc + group.totals[field], 0); }; - get24hCountByProject = (project: number, display: DisplayOption) => { + get24hCountByProject = (project: number, display: ReleasesDisplayOption) => { const {totalCountByProjectIn24h} = this.state; const field = sessionDisplayToField(display); @@ -398,7 +406,7 @@ class ReleaseListRequest extends React.Component { ?.reduce((acc, group) => acc + group.totals[field], 0); }; - getPeriodCountByProject = (project: number, display: DisplayOption) => { + getPeriodCountByProject = (project: number, display: ReleasesDisplayOption) => { const {totalCountByProjectInPeriod} = this.state; const field = sessionDisplayToField(display); @@ -407,7 +415,7 @@ class ReleaseListRequest extends React.Component { ?.reduce((acc, group) => acc + group.totals[field], 0); }; - getTimeSeries = (version: string, project: number, display: DisplayOption) => { + getTimeSeries = (version: string, project: number, display: ReleasesDisplayOption) => { const {healthStatsPeriod} = this.props; if (healthStatsPeriod === HealthStatsPeriodOption.AUTO) { return this.getPeriodTimeSeries(version, project, display); @@ -416,7 +424,11 @@ class ReleaseListRequest extends React.Component { return this.get24hTimeSeries(version, project, display); }; - get24hTimeSeries = (version: string, project: number, display: DisplayOption) => { + get24hTimeSeries = ( + version: string, + project: number, + display: ReleasesDisplayOption + ) => { const {totalCountByReleaseIn24h, totalCountByProjectIn24h} = this.state; const field = sessionDisplayToField(display); @@ -449,7 +461,11 @@ class ReleaseListRequest extends React.Component { ]; }; - getPeriodTimeSeries = (version: string, project: number, display: DisplayOption) => { + getPeriodTimeSeries = ( + version: string, + project: number, + display: ReleasesDisplayOption + ) => { const {statusCountByReleaseInPeriod, statusCountByProjectInPeriod} = this.state; const field = sessionDisplayToField(display); @@ -482,7 +498,7 @@ class ReleaseListRequest extends React.Component { ]; }; - getAdoption = (version: string, project: number, display: DisplayOption) => { + getAdoption = (version: string, project: number, display: ReleasesDisplayOption) => { const {healthStatsPeriod} = this.props; const countByRelease = ( @@ -513,4 +529,4 @@ class ReleaseListRequest extends React.Component { } } -export default withApi(ReleaseListRequest); +export default withApi(ReleasesRequest); diff --git a/static/app/views/releases/list/releasesSortOptions.tsx b/static/app/views/releases/list/releasesSortOptions.tsx new file mode 100644 index 00000000000000..d20e8a0a005675 --- /dev/null +++ b/static/app/views/releases/list/releasesSortOptions.tsx @@ -0,0 +1,84 @@ +import {ComponentProps} from 'react'; +import styled from '@emotion/styled'; + +import {t} from 'app/locale'; +import {Organization} from 'app/types'; + +import {ReleasesDisplayOption} from './releasesDisplayOptions'; +import ReleasesDropdown from './releasesDropdown'; + +export enum ReleasesSortOption { + CRASH_FREE_USERS = 'crash_free_users', + CRASH_FREE_SESSIONS = 'crash_free_sessions', + USERS_24_HOURS = 'users_24h', + SESSIONS_24_HOURS = 'sessions_24h', + SESSIONS = 'sessions', + DATE = 'date', + BUILD = 'build', + SEMVER = 'semver', + ADOPTION = 'adoption', +} + +type Props = { + selected: ReleasesSortOption; + selectedDisplay: ReleasesDisplayOption; + onSelect: (key: string) => void; + organization: Organization; + environments: string[]; +}; + +function ReleasesSortOptions({ + selected, + selectedDisplay, + onSelect, + organization, + environments, +}: Props) { + const sortOptions = { + [ReleasesSortOption.DATE]: {label: t('Date Created')}, + [ReleasesSortOption.SESSIONS]: {label: t('Total Sessions')}, + ...(selectedDisplay === ReleasesDisplayOption.USERS + ? { + [ReleasesSortOption.USERS_24_HOURS]: {label: t('Active Users')}, + [ReleasesSortOption.CRASH_FREE_USERS]: {label: t('Crash Free Users')}, + } + : { + [ReleasesSortOption.SESSIONS_24_HOURS]: {label: t('Active Sessions')}, + [ReleasesSortOption.CRASH_FREE_SESSIONS]: {label: t('Crash Free Sessions')}, + }), + } as ComponentProps['options']; + + if (organization.features.includes('semver')) { + sortOptions[ReleasesSortOption.BUILD] = {label: t('Build Number')}; + sortOptions[ReleasesSortOption.SEMVER] = {label: t('Semantic Version')}; + } + + if (organization.features.includes('release-adoption-stage')) { + const isDisabled = environments.length !== 1; + sortOptions[ReleasesSortOption.ADOPTION] = { + label: t('Date Adopted'), + disabled: isDisabled, + tooltip: isDisabled + ? t('Select one environment to use this sort option.') + : undefined, + }; + } + + return ( + + ); +} + +export default ReleasesSortOptions; + +const StyledReleasesDropdown = styled(ReleasesDropdown)` + z-index: 2; + @media (max-width: ${p => p.theme.breakpoints[2]}) { + order: 2; + } +`; diff --git a/static/app/views/releases/list/releasesStatusOptions.tsx b/static/app/views/releases/list/releasesStatusOptions.tsx new file mode 100644 index 00000000000000..79a9f2636b7005 --- /dev/null +++ b/static/app/views/releases/list/releasesStatusOptions.tsx @@ -0,0 +1,40 @@ +import styled from '@emotion/styled'; + +import {t} from 'app/locale'; + +import ReleasesDropdown from './releasesDropdown'; + +export enum ReleasesStatusOption { + ACTIVE = 'active', + ARCHIVED = 'archived', +} + +const options = { + [ReleasesStatusOption.ACTIVE]: {label: t('Active')}, + [ReleasesStatusOption.ARCHIVED]: {label: t('Archived')}, +}; + +type Props = { + selected: ReleasesStatusOption; + onSelect: (key: string) => void; +}; + +function ReleasesStatusOptions({selected, onSelect}: Props) { + return ( + + ); +} + +export default ReleasesStatusOptions; + +const StyledReleasesDropdown = styled(ReleasesDropdown)` + z-index: 3; + @media (max-width: ${p => p.theme.breakpoints[2]}) { + order: 1; + } +`; diff --git a/static/app/views/releases/list/utils.tsx b/static/app/views/releases/list/utils.tsx deleted file mode 100644 index f236079a2bd0bc..00000000000000 --- a/static/app/views/releases/list/utils.tsx +++ /dev/null @@ -1,21 +0,0 @@ -export enum SortOption { - CRASH_FREE_USERS = 'crash_free_users', - CRASH_FREE_SESSIONS = 'crash_free_sessions', - USERS_24_HOURS = 'users_24h', - SESSIONS_24_HOURS = 'sessions_24h', - SESSIONS = 'sessions', - DATE = 'date', - BUILD = 'build', - SEMVER = 'semver', - ADOPTION = 'adoption', -} - -export enum DisplayOption { - USERS = 'users', - SESSIONS = 'sessions', -} - -export enum StatusOption { - ACTIVE = 'active', - ARCHIVED = 'archived', -} diff --git a/static/app/views/releases/utils/index.tsx b/static/app/views/releases/utils/index.tsx index d9009b187fb3c8..799c12b040b532 100644 --- a/static/app/views/releases/utils/index.tsx +++ b/static/app/views/releases/utils/index.tsx @@ -4,15 +4,16 @@ import round from 'lodash/round'; import moment from 'moment'; import {DateTimeObject} from 'app/components/charts/utils'; +import ExternalLink from 'app/components/links/externalLink'; import {getParams} from 'app/components/organizations/globalSelectionHeader/getParams'; import {PAGE_URL_PARAM, URL_PARAM} from 'app/constants/globalSelectionHeader'; -import {tn} from 'app/locale'; +import {desktop, mobile, PlatformKey} from 'app/data/platformCategories'; +import {t, tct} from 'app/locale'; import {Release, ReleaseStatus} from 'app/types'; +import {Theme} from 'app/utils/theme'; import {MutableSearch} from 'app/utils/tokenizeSearch'; import {IssueSortOptions} from 'app/views/issueList/utils'; -import {DisplayOption} from '../list/utils'; - export const CRASH_FREE_DECIMAL_THRESHOLD = 95; export const roundDuration = (seconds: number) => { @@ -64,17 +65,6 @@ export const displaySessionStatusPercent = (percent: number, absolute = true) => return `${getSessionStatusPercent(percent, absolute).toLocaleString()}\u0025`; }; -export const displayCrashFreeDiff = ( - diffPercent: number, - crashFreePercent?: number | null -) => - `${Math.abs( - round( - diffPercent, - crashFreePercent && crashFreePercent > CRASH_FREE_DECIMAL_THRESHOLD ? 3 : 0 - ) - ).toLocaleString()}\u0025`; - export const getReleaseNewIssuesUrl = ( orgSlug: string, projectId: string | number | null, @@ -137,14 +127,6 @@ export const getReleaseHandledIssuesUrl = ( export const isReleaseArchived = (release: Release) => release.status === ReleaseStatus.Archived; -export function releaseDisplayLabel(displayOption: DisplayOption, count?: number | null) { - if (displayOption === DisplayOption.USERS) { - return tn('user', 'users', count); - } - - return tn('session', 'sessions', count); -} - export type ReleaseBounds = {releaseStart?: string | null; releaseEnd?: string | null}; export function getReleaseBounds(release?: Release): ReleaseBounds { @@ -211,3 +193,40 @@ export function getReleaseParams({location, releaseBounds}: GetReleaseParams) { return params; } + +const adoptionStagesLink = ( + +); + +export const ADOPTION_STAGE_LABELS: Record< + string, + {name: string; tooltipTitle: JSX.Element; type: keyof Theme['tag']} +> = { + low_adoption: { + name: t('Low Adoption'), + tooltipTitle: tct( + 'This release has a low percentage of sessions compared to other releases in this project. [link:Learn more]', + {link: adoptionStagesLink} + ), + type: 'warning', + }, + adopted: { + name: t('Adopted'), + tooltipTitle: tct( + 'This release has a high percentage of sessions compared to other releases in this project. [link:Learn more]', + {link: adoptionStagesLink} + ), + type: 'success', + }, + replaced: { + name: t('Replaced'), + tooltipTitle: tct( + 'This release was previously Adopted, but now has a lower level of sessions compared to other releases in this project. [link:Learn more]', + {link: adoptionStagesLink} + ), + type: 'default', + }, +}; + +export const isMobileRelease = (releaseProjectPlatform: PlatformKey) => + ([...mobile, ...desktop] as string[]).includes(releaseProjectPlatform); diff --git a/tests/js/spec/views/releases/detail/releaseActions.spec.jsx b/tests/js/spec/views/releases/detail/header/releaseActions.spec.jsx similarity index 98% rename from tests/js/spec/views/releases/detail/releaseActions.spec.jsx rename to tests/js/spec/views/releases/detail/header/releaseActions.spec.jsx index c874aaff00a54c..518f4c34fb8ad9 100644 --- a/tests/js/spec/views/releases/detail/releaseActions.spec.jsx +++ b/tests/js/spec/views/releases/detail/header/releaseActions.spec.jsx @@ -4,7 +4,7 @@ import {mountWithTheme} from 'sentry-test/enzyme'; import {initializeOrg} from 'sentry-test/initializeOrg'; import {mountGlobalModal} from 'sentry-test/modal'; -import ReleaseActions from 'app/views/releases/detail/releaseActions'; +import ReleaseActions from 'app/views/releases/detail/header/releaseActions'; describe('ReleaseActions', function () { const {organization} = initializeOrg(); diff --git a/tests/js/spec/views/releases/detail/overview/issues.spec.jsx b/tests/js/spec/views/releases/detail/overview/releaseIssues.spec.jsx similarity index 94% rename from tests/js/spec/views/releases/detail/overview/issues.spec.jsx rename to tests/js/spec/views/releases/detail/overview/releaseIssues.spec.jsx index 4bc26db5a5ae0e..33dbb31f4cf3d2 100644 --- a/tests/js/spec/views/releases/detail/overview/issues.spec.jsx +++ b/tests/js/spec/views/releases/detail/overview/releaseIssues.spec.jsx @@ -1,9 +1,9 @@ import {mountWithTheme} from 'sentry-test/enzyme'; -import Issues from 'app/views/releases/detail/overview/issues'; +import ReleaseIssues from 'app/views/releases/detail/overview/releaseIssues'; import {getReleaseBounds} from 'app/views/releases/utils'; -describe('Release Issues', function () { +describe('ReleaseIssues', function () { let newIssuesEndpoint, resolvedIssuesEndpoint, unhandledIssuesEndpoint, @@ -67,9 +67,9 @@ describe('Release Issues', function () { }; it('shows an empty state', async function () { - const wrapper = mountWithTheme(); + const wrapper = mountWithTheme(); const wrapper2 = mountWithTheme( - + ); await tick(); @@ -100,7 +100,7 @@ describe('Release Issues', function () { }); it('filters the issues', async function () { - const wrapper = mountWithTheme(); + const wrapper = mountWithTheme(); const filterOptions = wrapper.find('ButtonBar Button'); @@ -121,7 +121,7 @@ describe('Release Issues', function () { }); it('renders link to Issues', function () { - const wrapper = mountWithTheme(); + const wrapper = mountWithTheme(); expect(wrapper.find('Link[data-test-id="issues-button"]').prop('to')).toEqual({ pathname: `/organizations/${props.organization.slug}/issues/`, diff --git a/tests/js/spec/views/releases/list/index.spec.jsx b/tests/js/spec/views/releases/list/index.spec.jsx index 3a22852808590d..e3032f2482cb87 100644 --- a/tests/js/spec/views/releases/list/index.spec.jsx +++ b/tests/js/spec/views/releases/list/index.spec.jsx @@ -3,7 +3,9 @@ import {initializeOrg} from 'sentry-test/initializeOrg'; import ProjectsStore from 'app/stores/projectsStore'; import ReleasesList from 'app/views/releases/list/'; -import {DisplayOption, SortOption, StatusOption} from 'app/views/releases/list/utils'; +import {ReleasesDisplayOption} from 'app/views/releases/list/releasesDisplayOptions'; +import {ReleasesSortOption} from 'app/views/releases/list/releasesSortOptions'; +import {ReleasesStatusOption} from 'app/views/releases/list/releasesStatusOptions'; describe('ReleasesList', function () { const {organization, routerContext, router} = initializeOrg(); @@ -22,10 +24,10 @@ describe('ReleasesList', function () { location: { query: { query: 'derp', - sort: SortOption.SESSIONS, + sort: ReleasesSortOption.SESSIONS, healthStatsPeriod: '24h', somethingBad: 'XXX', - status: StatusOption.ACTIVE, + status: ReleasesStatusOption.ACTIVE, }, }, }; @@ -82,7 +84,7 @@ describe('ReleasesList', function () { expect(items.at(1).text()).toContain('1.0.1'); expect(items.at(1).find('AdoptionColumn').at(1).text()).toContain('0%'); expect(items.at(2).text()).toContain('af4f231ec9a8'); - expect(items.at(2).find('Header').text()).toContain('Project'); + expect(items.at(2).find('ReleaseProjectsHeader').text()).toContain('Project'); }); it('displays the right empty state', function () { @@ -98,7 +100,7 @@ describe('ReleasesList', function () { routerContext ); expect(wrapper.find('StyledPanel')).toHaveLength(0); - expect(wrapper.find('ReleasePromo').text()).toContain('Demystify Releases'); + expect(wrapper.find('ReleasesPromo').text()).toContain('Demystify Releases'); location = {query: {statsPeriod: '30d'}}; wrapper = mountWithTheme( @@ -106,7 +108,7 @@ describe('ReleasesList', function () { routerContext ); expect(wrapper.find('StyledPanel')).toHaveLength(0); - expect(wrapper.find('ReleasePromo').text()).toContain('Demystify Releases'); + expect(wrapper.find('ReleasesPromo').text()).toContain('Demystify Releases'); location = {query: {query: 'abc'}}; wrapper = mountWithTheme( @@ -117,7 +119,7 @@ describe('ReleasesList', function () { "There are no releases that match: 'abc'." ); - location = {query: {sort: SortOption.SESSIONS, statsPeriod: '7d'}}; + location = {query: {sort: ReleasesSortOption.SESSIONS, statsPeriod: '7d'}}; wrapper = mountWithTheme( , routerContext @@ -126,7 +128,7 @@ describe('ReleasesList', function () { 'There are no releases with data in the last 7 days.' ); - location = {query: {sort: SortOption.USERS_24_HOURS, statsPeriod: '7d'}}; + location = {query: {sort: ReleasesSortOption.USERS_24_HOURS, statsPeriod: '7d'}}; wrapper = mountWithTheme( , routerContext @@ -135,7 +137,7 @@ describe('ReleasesList', function () { 'There are no releases with active user data (users in the last 24 hours).' ); - location = {query: {sort: SortOption.SESSIONS_24_HOURS, statsPeriod: '7d'}}; + location = {query: {sort: ReleasesSortOption.SESSIONS_24_HOURS, statsPeriod: '7d'}}; wrapper = mountWithTheme( , routerContext @@ -144,7 +146,7 @@ describe('ReleasesList', function () { 'There are no releases with active session data (sessions in the last 24 hours).' ); - location = {query: {sort: SortOption.BUILD}}; + location = {query: {sort: ReleasesSortOption.BUILD}}; wrapper = mountWithTheme( , routerContext @@ -195,12 +197,12 @@ describe('ReleasesList', function () { '/organizations/org-slug/releases/', expect.objectContaining({ query: expect.objectContaining({ - sort: SortOption.SESSIONS, + sort: ReleasesSortOption.SESSIONS, }), }) ); - const sortDropdown = wrapper.find('ReleaseListSortOptions'); + const sortDropdown = wrapper.find('ReleasesSortOptions'); const sortByOptions = sortDropdown.find('DropdownItem span'); const dateCreatedOption = sortByOptions.at(0); @@ -214,7 +216,7 @@ describe('ReleasesList', function () { expect(router.push).toHaveBeenCalledWith({ query: expect.objectContaining({ - sort: SortOption.DATE, + sort: ReleasesSortOption.DATE, }), }); }); @@ -228,17 +230,17 @@ describe('ReleasesList', function () { wrapper = mountWithTheme( , routerContext ); - const sortDropdown = wrapper.find('ReleaseListSortOptions'); + const sortDropdown = wrapper.find('ReleasesSortOptions'); expect(sortDropdown.find('ButtonLabel').text()).toBe('Sort ByDate Created'); }); it('display the right Crash Free column', async function () { - const displayDropdown = wrapper.find('ReleaseListDisplayOptions'); + const displayDropdown = wrapper.find('ReleasesDisplayOptions'); const activeDisplay = displayDropdown.find('DropdownButton button'); expect(activeDisplay.text()).toEqual('DisplaySessions'); @@ -258,28 +260,31 @@ describe('ReleasesList', function () { expect(router.push).toHaveBeenCalledWith({ query: expect.objectContaining({ - display: DisplayOption.USERS, + display: ReleasesDisplayOption.USERS, }), }); }); it('displays archived releases', function () { const archivedWrapper = mountWithTheme( - , + , routerContext ); expect(endpointMock).toHaveBeenLastCalledWith( '/organizations/org-slug/releases/', expect.objectContaining({ - query: expect.objectContaining({status: StatusOption.ARCHIVED}), + query: expect.objectContaining({status: ReleasesStatusOption.ARCHIVED}), }) ); expect(archivedWrapper.find('ReleaseArchivedNotice').exists()).toBeTruthy(); const statusOptions = archivedWrapper - .find('ReleaseListStatusOptions') + .find('ReleasesStatusOptions') .first() .find('DropdownItem span'); const statusActiveOption = statusOptions.at(0); @@ -292,7 +297,7 @@ describe('ReleasesList', function () { statusActiveOption.simulate('click'); expect(router.push).toHaveBeenLastCalledWith({ query: expect.objectContaining({ - status: StatusOption.ACTIVE, + status: ReleasesStatusOption.ACTIVE, }), }); @@ -301,7 +306,7 @@ describe('ReleasesList', function () { statusArchivedOption.simulate('click'); expect(router.push).toHaveBeenLastCalledWith({ query: expect.objectContaining({ - status: StatusOption.ARCHIVED, + status: ReleasesStatusOption.ARCHIVED, }), }); }); @@ -389,14 +394,14 @@ describe('ReleasesList', function () { const healthSection = mountWithTheme( , routerContext - ).find('ReleaseHealth'); + ).find('ReleaseProjects'); const hiddenProjectsMessage = healthSection.find('HiddenProjectsMessage'); expect(hiddenProjectsMessage.text()).toBe('2 hidden projects'); expect(hiddenProjectsMessage.find('Tooltip').prop('title')).toBe('test, test3'); - expect(healthSection.find('ProjectRow').length).toBe(1); + expect(healthSection.find('ReleaseCardProjectRow').length).toBe(1); expect(healthSection.find('ProjectBadge').text()).toBe('test2'); }); @@ -409,11 +414,11 @@ describe('ReleasesList', function () { const healthSection = mountWithTheme( , routerContext - ).find('ReleaseHealth'); + ).find('ReleaseProjects'); expect(healthSection.find('HiddenProjectsMessage').exists()).toBeFalsy(); - expect(healthSection.find('ProjectRow').length).toBe(1); + expect(healthSection.find('ReleaseCardProjectRow').length).toBe(1); }); it('autocompletes semver search tag', async function () { diff --git a/tests/js/spec/views/releases/list/releaseAdoption.spec.jsx b/tests/js/spec/views/releases/list/releaseAdoption.spec.jsx deleted file mode 100644 index fa58e9251c3beb..00000000000000 --- a/tests/js/spec/views/releases/list/releaseAdoption.spec.jsx +++ /dev/null @@ -1,60 +0,0 @@ -import {mountWithTheme} from 'sentry-test/enzyme'; - -import ReleaseAdoption from 'app/views/releases/list/releaseAdoption'; - -describe('ReleasesList > ReleaseAdoption', function () { - it('renders', function () { - const wrapper = mountWithTheme( - - ); - - expect(wrapper.find('Labels').exists()).toBeFalsy(); - expect(wrapper.find('ProgressBar').prop('value')).toBe(10); - - const tooltipContent = mountWithTheme(wrapper.find('Tooltip').prop('title')); - - expect(tooltipContent.find('Title').at(0).text()).toBe('This Release'); - expect(tooltipContent.find('Value').at(0).text()).toBe('100 sessions'); - expect(tooltipContent.find('Title').at(1).text()).toBe('Total Project'); - expect(tooltipContent.find('Value').at(1).text()).toBe('1k sessions'); - }); - - it('renders with users', function () { - const wrapper = mountWithTheme( - - ); - - const tooltipContent = mountWithTheme(wrapper.find('Tooltip').prop('title')); - - expect(tooltipContent.find('Value').at(0).text()).toBe('1 user'); - }); - - it('renders with labels', function () { - const wrapper = mountWithTheme( - - ); - expect(wrapper.find('Labels').text()).toBe('1/1 session100%'); - - const wrapper2 = mountWithTheme( - - ); - expect(wrapper2.find('Labels').text()).toBe('100/1k users10%'); - }); -}); diff --git a/tests/js/spec/views/releases/list/releaseListRequest.spec.tsx b/tests/js/spec/views/releases/list/releasesRequest.spec.tsx similarity index 92% rename from tests/js/spec/views/releases/list/releaseListRequest.spec.tsx rename to tests/js/spec/views/releases/list/releasesRequest.spec.tsx index 6db6e85b116c71..7e19b219b1f017 100644 --- a/tests/js/spec/views/releases/list/releaseListRequest.spec.tsx +++ b/tests/js/spec/views/releases/list/releasesRequest.spec.tsx @@ -4,10 +4,10 @@ import {mountWithTheme} from 'sentry-test/enzyme'; import {initializeOrg} from 'sentry-test/initializeOrg'; import {HealthStatsPeriodOption} from 'app/types'; -import ReleaseListRequest from 'app/views/releases/list/releaseListRequest'; -import {DisplayOption} from 'app/views/releases/list/utils'; +import {ReleasesDisplayOption} from 'app/views/releases/list/releasesDisplayOptions'; +import ReleasesRequest from 'app/views/releases/list/releasesRequest'; -describe('ReleaseListRequest', function () { +describe('ReleasesRequest', function () { const {organization, routerContext, router} = initializeOrg(); const projectId = 123; const selection = { @@ -219,7 +219,7 @@ describe('ReleaseListRequest', function () { let healthData; const wrapper = mountWithTheme( - {({getHealthData}) => { healthData = getHealthData; return null; }} - , + , routerContext ); @@ -250,31 +250,31 @@ describe('ReleaseListRequest', function () { healthData.getCrashCount( '7a82c130be9143361f20bc77252df783cf91e4fc', projectId, - DisplayOption.SESSIONS + ReleasesDisplayOption.SESSIONS ) ).toBe(492); expect( healthData.getCrashFreeRate( '7a82c130be9143361f20bc77252df783cf91e4fc', projectId, - DisplayOption.SESSIONS + ReleasesDisplayOption.SESSIONS ) ).toBe(99.76); expect( healthData.get24hCountByRelease( '7a82c130be9143361f20bc77252df783cf91e4fc', projectId, - DisplayOption.SESSIONS + ReleasesDisplayOption.SESSIONS ) ).toBe(219826); - expect(healthData.get24hCountByProject(projectId, DisplayOption.SESSIONS)).toBe( - 835965 - ); + expect( + healthData.get24hCountByProject(projectId, ReleasesDisplayOption.SESSIONS) + ).toBe(835965); expect( healthData.getTimeSeries( '7a82c130be9143361f20bc77252df783cf91e4fc', projectId, - DisplayOption.SESSIONS + ReleasesDisplayOption.SESSIONS ) ).toEqual([ { @@ -341,7 +341,7 @@ describe('ReleaseListRequest', function () { healthData.getAdoption( '7a82c130be9143361f20bc77252df783cf91e4fc', projectId, - DisplayOption.SESSIONS + ReleasesDisplayOption.SESSIONS ) ).toBe(26.29607698886915); @@ -349,31 +349,31 @@ describe('ReleaseListRequest', function () { healthData.getCrashCount( 'e102abb2c46e7fe8686441091005c12aed90da99', projectId, - DisplayOption.SESSIONS + ReleasesDisplayOption.SESSIONS ) ).toBe(5); expect( healthData.getCrashFreeRate( 'e102abb2c46e7fe8686441091005c12aed90da99', projectId, - DisplayOption.SESSIONS + ReleasesDisplayOption.SESSIONS ) ).toBe(99.921); expect( healthData.get24hCountByRelease( 'e102abb2c46e7fe8686441091005c12aed90da99', projectId, - DisplayOption.SESSIONS + ReleasesDisplayOption.SESSIONS ) ).toBe(6320); - expect(healthData.get24hCountByProject(projectId, DisplayOption.SESSIONS)).toBe( - 835965 - ); + expect( + healthData.get24hCountByProject(projectId, ReleasesDisplayOption.SESSIONS) + ).toBe(835965); expect( healthData.getTimeSeries( 'e102abb2c46e7fe8686441091005c12aed90da99', projectId, - DisplayOption.SESSIONS + ReleasesDisplayOption.SESSIONS ) ).toEqual([ { @@ -440,7 +440,7 @@ describe('ReleaseListRequest', function () { healthData.getAdoption( 'e102abb2c46e7fe8686441091005c12aed90da99', projectId, - DisplayOption.SESSIONS + ReleasesDisplayOption.SESSIONS ) ).toBe(0.7560125124855706); @@ -451,7 +451,7 @@ describe('ReleaseListRequest', function () { let healthData; const wrapper = mountWithTheme( - {({getHealthData}) => { healthData = getHealthData; return null; }} - , + , routerContext ); @@ -482,29 +482,31 @@ describe('ReleaseListRequest', function () { healthData.getCrashCount( '7a82c130be9143361f20bc77252df783cf91e4fc', projectId, - DisplayOption.SESSIONS + ReleasesDisplayOption.SESSIONS ) ).toBe(492); expect( healthData.getCrashFreeRate( '7a82c130be9143361f20bc77252df783cf91e4fc', projectId, - DisplayOption.USERS + ReleasesDisplayOption.USERS ) ).toBe(99.908); expect( healthData.get24hCountByRelease( '7a82c130be9143361f20bc77252df783cf91e4fc', projectId, - DisplayOption.USERS + ReleasesDisplayOption.USERS ) ).toBe(56826); - expect(healthData.get24hCountByProject(projectId, DisplayOption.USERS)).toBe(140965); + expect(healthData.get24hCountByProject(projectId, ReleasesDisplayOption.USERS)).toBe( + 140965 + ); expect( healthData.getTimeSeries( '7a82c130be9143361f20bc77252df783cf91e4fc', projectId, - DisplayOption.USERS + ReleasesDisplayOption.USERS ) ).toEqual([ { @@ -571,7 +573,7 @@ describe('ReleaseListRequest', function () { healthData.getAdoption( '7a82c130be9143361f20bc77252df783cf91e4fc', projectId, - DisplayOption.USERS + ReleasesDisplayOption.USERS ) ).toBe(40.31213421771362); @@ -579,29 +581,31 @@ describe('ReleaseListRequest', function () { healthData.getCrashCount( 'e102abb2c46e7fe8686441091005c12aed90da99', projectId, - DisplayOption.SESSIONS + ReleasesDisplayOption.SESSIONS ) ).toBe(5); expect( healthData.getCrashFreeRate( 'e102abb2c46e7fe8686441091005c12aed90da99', projectId, - DisplayOption.USERS + ReleasesDisplayOption.USERS ) ).toBe(99.87); expect( healthData.get24hCountByRelease( 'e102abb2c46e7fe8686441091005c12aed90da99', projectId, - DisplayOption.USERS + ReleasesDisplayOption.USERS ) ).toBe(850); - expect(healthData.get24hCountByProject(projectId, DisplayOption.USERS)).toBe(140965); + expect(healthData.get24hCountByProject(projectId, ReleasesDisplayOption.USERS)).toBe( + 140965 + ); expect( healthData.getTimeSeries( 'e102abb2c46e7fe8686441091005c12aed90da99', projectId, - DisplayOption.USERS + ReleasesDisplayOption.USERS ) ).toEqual([ { @@ -668,7 +672,7 @@ describe('ReleaseListRequest', function () { healthData.getAdoption( 'e102abb2c46e7fe8686441091005c12aed90da99', projectId, - DisplayOption.USERS + ReleasesDisplayOption.USERS ) ).toBe(0.6029865569467598); }); @@ -677,7 +681,7 @@ describe('ReleaseListRequest', function () { let healthData; const wrapper = mountWithTheme( - @@ -697,7 +701,7 @@ describe('ReleaseListRequest', function () { healthData = getHealthData; return null; }} - , + , routerContext ); @@ -709,7 +713,7 @@ describe('ReleaseListRequest', function () { healthData.getTimeSeries( '7a82c130be9143361f20bc77252df783cf91e4fc', projectId, - DisplayOption.SESSIONS + ReleasesDisplayOption.SESSIONS ) ).toEqual([ { @@ -756,7 +760,7 @@ describe('ReleaseListRequest', function () { healthData.getAdoption( '7a82c130be9143361f20bc77252df783cf91e4fc', projectId, - DisplayOption.SESSIONS + ReleasesDisplayOption.SESSIONS ) ).toBe(26.29607698886915);