-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(insights): Introduce sort by selector for releases #82302
Changes from 2 commits
5d2c93f
1bb7e0e
09200be
83010ca
e759a34
33db74b
1e23dc9
3bd0add
63adbb9
f536bf6
2cc8153
e2ec015
1e8b624
7d54fb5
c2d43bf
795676d
4f1c30c
8ea04e8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
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', | ||
Comment on lines
+2
to
+10
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @philipphofmann can you confirm that we would like to provide all these sort by options? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would remove crash-free-users and crash-free-session, as we are filtering releases for performance data, and I don't think that is relevant. I would keep the rest. |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,18 +1,27 @@ | ||
import {useState} from 'react'; | ||
import {useEffect, useState} from 'react'; | ||
import styled from '@emotion/styled'; | ||
import debounce from 'lodash/debounce'; | ||
|
||
import type {SelectOption} from 'sentry/components/compactSelect'; | ||
import {CompactSelect} from 'sentry/components/compactSelect'; | ||
import PageFilterBar from 'sentry/components/organizations/pageFilterBar'; | ||
import {DEFAULT_DEBOUNCE_DURATION} from 'sentry/constants'; | ||
import {ReleasesSortOption} from 'sentry/constants/releases'; | ||
import {IconReleases} from 'sentry/icons/iconReleases'; | ||
import {t, tn} from 'sentry/locale'; | ||
import {t, tct, tn} from 'sentry/locale'; | ||
import {space} from 'sentry/styles/space'; | ||
import {defined} from 'sentry/utils'; | ||
import {browserHistory} from 'sentry/utils/browserHistory'; | ||
import {getFormattedDate} from 'sentry/utils/dates'; | ||
import {decodeScalar} from 'sentry/utils/queryString'; | ||
import {useLocation} from 'sentry/utils/useLocation'; | ||
import {useNavigate} from 'sentry/utils/useNavigate'; | ||
import usePageFilters from 'sentry/utils/usePageFilters'; | ||
import { | ||
ReleasesSort, | ||
type ReleasesSortByOption, | ||
SORT_BY_OPTIONS, | ||
} from 'sentry/views/insights/common/components/releasesSort'; | ||
import { | ||
useReleases, | ||
useReleaseSelection, | ||
|
@@ -24,18 +33,24 @@ export const SECONDARY_RELEASE_ALIAS = 'R2'; | |
|
||
type Props = { | ||
selectorKey: string; | ||
sortBy: ReleasesSortByOption; | ||
selectorName?: string; | ||
selectorValue?: string; | ||
triggerLabelPrefix?: string; | ||
}; | ||
|
||
export function ReleaseSelector({selectorKey, selectorValue, triggerLabelPrefix}: Props) { | ||
export function ReleaseSelector({ | ||
selectorKey, | ||
selectorValue, | ||
triggerLabelPrefix, | ||
sortBy, | ||
}: Props) { | ||
const [searchTerm, setSearchTerm] = useState<string | undefined>(undefined); | ||
const {data, isLoading} = useReleases(searchTerm); | ||
const {data, isLoading} = useReleases({searchTerm, sortBy}); | ||
const {primaryRelease, secondaryRelease} = useReleaseSelection(); | ||
const location = useLocation(); | ||
|
||
const options: SelectOption<string>[] = []; | ||
const options: (SelectOption<string> & {count?: number})[] = []; | ||
if (defined(selectorValue)) { | ||
const index = data?.findIndex(({version}) => version === selectorValue); | ||
const selectedRelease = defined(index) ? data?.[index] : undefined; | ||
|
@@ -48,6 +63,7 @@ export function ReleaseSelector({selectorKey, selectorValue, triggerLabelPrefix} | |
|
||
options.push({ | ||
value: selectorValue, | ||
count: selectedReleaseSessionCount, | ||
label: selectorValue, | ||
details: ( | ||
<LabelDetails | ||
|
@@ -57,12 +73,14 @@ export function ReleaseSelector({selectorKey, selectorValue, triggerLabelPrefix} | |
), | ||
}); | ||
} | ||
|
||
data | ||
?.filter(({version}) => ![primaryRelease, secondaryRelease].includes(version)) | ||
.forEach(release => { | ||
const option = { | ||
value: release.version, | ||
label: release.version, | ||
count: release.count, | ||
details: ( | ||
<LabelDetails screenCount={release.count} dateCreated={release.dateCreated} /> | ||
), | ||
|
@@ -74,6 +92,11 @@ export function ReleaseSelector({selectorKey, selectorValue, triggerLabelPrefix} | |
? formatVersionAndCenterTruncate(selectorValue, 16) | ||
: selectorValue; | ||
|
||
const sortedOptions = | ||
sortBy === 'number_events' | ||
? options.sort((a, b) => (b.count ?? 0) - (a.count ?? 0)) | ||
: options; | ||
|
||
return ( | ||
<StyledCompactSelect | ||
triggerProps={{ | ||
|
@@ -89,8 +112,10 @@ export function ReleaseSelector({selectorKey, selectorValue, triggerLabelPrefix} | |
options={[ | ||
{ | ||
value: '_releases', | ||
label: t('Sorted by date created'), | ||
options, | ||
label: tct('Sorted by [sortBy]', { | ||
sortBy: SORT_BY_OPTIONS[sortBy].label, | ||
}), | ||
options: sortedOptions, | ||
}, | ||
]} | ||
onSearch={debounce(val => { | ||
|
@@ -134,8 +159,48 @@ function LabelDetails(props: LabelDetailsProps) { | |
); | ||
} | ||
|
||
function getSortRealesesBy( | ||
sort: ReleasesSortByOption, | ||
priscilawebdev marked this conversation as resolved.
Show resolved
Hide resolved
|
||
environments: string[] | ||
): ReleasesSortByOption { | ||
// Require 1 environment for date adopted | ||
if (sort === ReleasesSortOption.ADOPTION && environments.length !== 1) { | ||
return ReleasesSortOption.DATE; | ||
} | ||
|
||
const sortExists = Object.keys(SORT_BY_OPTIONS).includes(sort ?? ''); | ||
|
||
if (sortExists) { | ||
return sort; | ||
} | ||
|
||
priscilawebdev marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return ReleasesSortOption.DATE; | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you can find the same logic here |
||
export function ReleaseComparisonSelector() { | ||
const {primaryRelease, secondaryRelease} = useReleaseSelection(); | ||
const location = useLocation(); | ||
const navigate = useNavigate(); | ||
const {selection} = usePageFilters(); | ||
|
||
const sort = decodeScalar(location.query.sortReleasesBy) as ReleasesSortByOption; | ||
const sortReleasesBy = getSortRealesesBy(sort, selection.environments); | ||
|
||
useEffect(() => { | ||
if (sort !== sortReleasesBy) { | ||
navigate( | ||
{ | ||
...location, | ||
query: { | ||
...location.query, | ||
sortReleasesBy, | ||
}, | ||
}, | ||
{replace: true} | ||
); | ||
} | ||
}, [location, sortReleasesBy, navigate, sort]); | ||
|
||
return ( | ||
<StyledPageSelector condensed> | ||
<ReleaseSelector | ||
|
@@ -144,13 +209,28 @@ export function ReleaseComparisonSelector() { | |
selectorName={t('Release 1')} | ||
key="primaryRelease" | ||
triggerLabelPrefix={PRIMARY_RELEASE_ALIAS} | ||
sortBy={sortReleasesBy} | ||
/> | ||
<ReleaseSelector | ||
selectorKey="secondaryRelease" | ||
selectorName={t('Release 2')} | ||
selectorValue={secondaryRelease} | ||
key="secondaryRelease" | ||
triggerLabelPrefix={SECONDARY_RELEASE_ALIAS} | ||
sortBy={sortReleasesBy} | ||
/> | ||
<ReleasesSort | ||
sortBy={sortReleasesBy} | ||
environments={selection.environments} | ||
onChange={value => | ||
navigate({ | ||
...location, | ||
query: { | ||
...location.query, | ||
sortReleasesBy: value, | ||
}, | ||
}) | ||
} | ||
/> | ||
</StyledPageSelector> | ||
); | ||
|
@@ -166,7 +246,10 @@ const StyledPageSelector = styled(PageFilterBar)` | |
& > * { | ||
min-width: 135px; | ||
&:last-child { | ||
min-width: 135px; | ||
min-width: auto; | ||
> button[aria-haspopup] { | ||
padding-right: ${space(1.5)}; | ||
} | ||
} | ||
} | ||
`; | ||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,63 @@ | ||||||||||||||||||||||||
import {Button} from 'sentry/components/button'; | ||||||||||||||||||||||||
import {CompositeSelect} from 'sentry/components/compactSelect/composite'; | ||||||||||||||||||||||||
import {ReleasesSortOption} from 'sentry/constants/releases'; | ||||||||||||||||||||||||
import {IconSort} from 'sentry/icons'; | ||||||||||||||||||||||||
import {t} from 'sentry/locale'; | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
export const SORT_BY_OPTIONS = { | ||||||||||||||||||||||||
[ReleasesSortOption.SESSIONS_24_HOURS]: {label: t('Active Sessions')}, | ||||||||||||||||||||||||
[ReleasesSortOption.USERS_24_HOURS]: {label: t('Active Users')}, | ||||||||||||||||||||||||
[ReleasesSortOption.ADOPTION]: {label: t('Adoption')}, | ||||||||||||||||||||||||
[ReleasesSortOption.BUILD]: {label: t('Build Number')}, | ||||||||||||||||||||||||
[ReleasesSortOption.CRASH_FREE_SESSIONS]: {label: t('Crash Free Sessions')}, | ||||||||||||||||||||||||
[ReleasesSortOption.CRASH_FREE_USERS]: {label: t('Crash Free Users')}, | ||||||||||||||||||||||||
[ReleasesSortOption.DATE]: {label: t('Date Created')}, | ||||||||||||||||||||||||
['number_events']: {label: t('Number of Events')}, | ||||||||||||||||||||||||
[ReleasesSortOption.SEMVER]: {label: t('Semantic Version')}, | ||||||||||||||||||||||||
[ReleasesSortOption.SESSIONS]: {label: t('Total Sessions')}, | ||||||||||||||||||||||||
}; | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
export type ReleasesSortByOption = keyof typeof SORT_BY_OPTIONS; | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
interface Props { | ||||||||||||||||||||||||
environments: string[]; | ||||||||||||||||||||||||
onChange: (sortBy: string) => void; | ||||||||||||||||||||||||
sortBy: ReleasesSortByOption; | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
export function ReleasesSort({environments, sortBy, onChange}: Props) { | ||||||||||||||||||||||||
return ( | ||||||||||||||||||||||||
<CompositeSelect | ||||||||||||||||||||||||
trigger={triggerProps => ( | ||||||||||||||||||||||||
<Button | ||||||||||||||||||||||||
{...triggerProps} | ||||||||||||||||||||||||
size="xs" | ||||||||||||||||||||||||
icon={<IconSort />} | ||||||||||||||||||||||||
title={t('Sort Releases')} | ||||||||||||||||||||||||
aria-label={t('Sort Releases')} | ||||||||||||||||||||||||
/> | ||||||||||||||||||||||||
)} | ||||||||||||||||||||||||
> | ||||||||||||||||||||||||
<CompositeSelect.Region | ||||||||||||||||||||||||
label={t('Sort By')} | ||||||||||||||||||||||||
value={sortBy} | ||||||||||||||||||||||||
onChange={selection => { | ||||||||||||||||||||||||
onChange(selection.value); | ||||||||||||||||||||||||
}} | ||||||||||||||||||||||||
options={Object.entries(SORT_BY_OPTIONS).map(([name, filter]) => { | ||||||||||||||||||||||||
const disabled = | ||||||||||||||||||||||||
name === ReleasesSortOption.ADOPTION && environments.length !== 1; | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
return { | ||||||||||||||||||||||||
label: filter.label, | ||||||||||||||||||||||||
value: name, | ||||||||||||||||||||||||
disabled, | ||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unless we rename disabled to something that better expressed the intention, it's best to just inline it.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. but we are using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. in any case, I have updated the code |
||||||||||||||||||||||||
tooltip: disabled | ||||||||||||||||||||||||
? t('Select one environment to use this sort option.') | ||||||||||||||||||||||||
: undefined, | ||||||||||||||||||||||||
}; | ||||||||||||||||||||||||
})} | ||||||||||||||||||||||||
/> | ||||||||||||||||||||||||
</CompositeSelect> | ||||||||||||||||||||||||
); | ||||||||||||||||||||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this enum was just moved from
sentry/static/app/views/releases/list/releasesSortOptions.tsx
Lines 8 to 18 in 5078825