Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
c8e5eeb
Crimes
gggritso Jan 13, 2026
8784687
refactor(dashboards): Extract URL+localStorage sync hook and update C…
gggritso Jan 14, 2026
70722ad
refactor(dashboards): Improve useSyncedQueryParamState API
gggritso Jan 14, 2026
42c5b57
refactor(dashboards): Rename effectiveSortBy to releaseFilterSortBy
gggritso Jan 14, 2026
7f7bfb8
refactor(dashboards): Move getReleasesSortBy validation to filtersBar
gggritso Jan 14, 2026
a2edb17
fix(useSyncedQueryParamState): Fix URL/localStorage fallback chain an…
gggritso Jan 14, 2026
0a864e1
refactor(dashboards): Replace ReleasesProvider with useReleases hook
gggritso Jan 14, 2026
609c3fc
refactor(dashboards): Remove ReleasesProvider from widget builder
gggritso Jan 14, 2026
45491cd
fix(dashboards): Make release sort selector match release selector vi…
gggritso Jan 14, 2026
44dd23b
fix(dashboards): Remove chevron from release sort selector
gggritso Jan 14, 2026
5f00f64
fix(dashboards): Use correct prop to hide chevron in sort selector
gggritso Jan 14, 2026
7161672
refactor(dashboards): Remove default exports in favor of named exports
gggritso Jan 14, 2026
0b35772
refactor(dashboards): Simplify ReleasesSortSelect props and reorganiz…
gggritso Jan 14, 2026
cb2cde7
style(dashboards): Remove horizontal padding from sort selector button
gggritso Jan 14, 2026
b1cb01e
Revert "style(dashboards): Remove horizontal padding from sort select…
gggritso Jan 14, 2026
1023e2c
style(dashboards): Use muted variant for sort icon to match chevrons
gggritso Jan 14, 2026
30767ad
Merge branch 'master' into georgegritsouk/dain-1111-allow-custom-sort…
gggritso Jan 20, 2026
d533af3
refactor(dashboards): Remove text from sort selector, show icon only
gggritso Jan 20, 2026
abf3413
refactor(dashboards): Rename sortReleasesBy to releaseFilterSort quer…
gggritso Jan 20, 2026
d42f430
Merge branch 'master' into georgegritsouk/dain-1111-allow-custom-sort…
gggritso Jan 20, 2026
d692150
Merge branch 'master' into georgegritsouk/dain-1111-allow-custom-sort…
gggritso Jan 20, 2026
88bc3ff
test(dashboards): Fix releasesSelectControl tests to use useReleases …
gggritso Jan 20, 2026
1531d00
ref(dashboards): Modernize release fetching with React Query hook
gggritso Jan 21, 2026
fdcf325
test(dashboards): Mock network instead of hook in ReleasesSelectContr…
gggritso Jan 21, 2026
3b8b93b
refactor(dashboards): Use getApiUrl and remove unused ReleasesProvider
gggritso Jan 21, 2026
9b8099f
test(dashboards): Fix release search mock to match all query params
gggritso Jan 21, 2026
d195ea5
Merge remote-tracking branch 'origin/georgegritsouk/dain-1163-moderni…
gggritso Jan 21, 2026
13df807
refactor(dashboards): Inline URL+localStorage sync and fix TypeScript…
gggritso Jan 21, 2026
c95209d
refactor(dashboards): Change WidgetBuilderFilterBar to default export
gggritso Jan 21, 2026
e1974d9
Merge remote-tracking branch 'origin/master' into georgegritsouk/dain…
gggritso Jan 23, 2026
1393e0b
refactor(dashboards): Remove localStorage sync for release sort
gggritso Jan 23, 2026
8ebdfbf
Merge branch 'master' into georgegritsouk/dain-1111-allow-custom-sort…
gggritso Jan 23, 2026
5682ab8
refactor(dashboards): Remove Insights-specific transaction.op filter
gggritso Jan 23, 2026
81dfc25
ref(dashboards): Consolidate release sort options and improve typing
gggritso Jan 23, 2026
c640bfb
ref(dashboards): Improve type safety for release sort callbacks
gggritso Jan 23, 2026
7b9afa4
ref(dashboards): Clean up release sorting implementation
gggritso Jan 23, 2026
c0be0eb
ref(dashboards): Address code review feedback for release sorting
gggritso Jan 23, 2026
591790c
ref(dashboards): Code review improvements for release sorting
gggritso Jan 23, 2026
572093a
Merge branch 'master' into georgegritsouk/dain-1111-allow-custom-sort…
gggritso Jan 26, 2026
4d13d88
Propagate disabled property
gggritso Jan 26, 2026
6a6d7c0
fix(dashboards): Fix stale metrics data when changing release sort
gggritso Jan 26, 2026
a02a07a
ref(dashboards): Use useQueries combine for stable metrics results
gggritso Jan 26, 2026
f004341
ref(dashboards): Improve useReleases robustness and code quality
gggritso Jan 26, 2026
21e6612
Improve dropdown layout
gggritso Jan 26, 2026
410db28
fix(dashboards): Fix release dropdown grid layout with missing counts
gggritso Jan 26, 2026
9f32963
Reduce width of release sort selector
gggritso Jan 26, 2026
b02ea76
fix(dashboards): Reset ADOPTION sort when environment requirement not…
gggritso Jan 27, 2026
f2c4dba
ref(dashboards): Simplify RELEASES_SORT_OPTIONS structure
gggritso Jan 29, 2026
c15f63e
ref(dashboards): Add DEFAULT_RELEASES_SORT constant
gggritso Jan 29, 2026
1c49750
Remove unused export
gggritso Jan 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions static/app/constants/releases.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import {t} from 'sentry/locale';

export enum ReleasesSortOption {
CRASH_FREE_USERS = 'crash_free_users',
CRASH_FREE_SESSIONS = 'crash_free_sessions',
Expand All @@ -9,3 +11,27 @@ export enum ReleasesSortOption {
SEMVER = 'semver',
ADOPTION = 'adoption',
}

/**
* Default sort option used when no valid sort is specified or when
* a sort option's requirements aren't met (e.g., ADOPTION requires exactly one environment).
*/
export const DEFAULT_RELEASES_SORT = ReleasesSortOption.DATE;

/**
* Sort options available for dashboard release filtering.
*
* Note: CRASH_FREE_USERS and CRASH_FREE_SESSIONS are intentionally excluded.
* These options are only shown in the releases list page where there's a
* "display mode" toggle (users vs sessions) that determines which one to show.
* See: static/app/views/releases/list/releasesSortOptions.tsx
*/
export const RELEASES_SORT_OPTIONS = {
[ReleasesSortOption.SESSIONS_24_HOURS]: t('Active Sessions'),
[ReleasesSortOption.USERS_24_HOURS]: t('Active Users'),
[ReleasesSortOption.ADOPTION]: t('Adoption'),
[ReleasesSortOption.BUILD]: t('Build Number'),
[ReleasesSortOption.DATE]: t('Date Created'),
[ReleasesSortOption.SEMVER]: t('Semantic Version'),
[ReleasesSortOption.SESSIONS]: t('Total Sessions'),
} as const;
61 changes: 61 additions & 0 deletions static/app/views/dashboards/components/releasesSortSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import {OverlayTrigger} from '@sentry/scraps/overlayTrigger';

import {CompactSelect} from 'sentry/components/core/compactSelect';
import {RELEASES_SORT_OPTIONS, ReleasesSortOption} from 'sentry/constants/releases';
import {IconSort} from 'sentry/icons';
import {t} from 'sentry/locale';
import usePageFilters from 'sentry/utils/usePageFilters';

interface ReleasesSortSelectProps {
onChange: (sortBy: ReleasesSortOption) => void;
sortBy: ReleasesSortOption;
disabled?: boolean;
}

export function ReleasesSortSelect({
sortBy,
onChange,
disabled,
}: ReleasesSortSelectProps) {
const {selection} = usePageFilters();
const {environments} = selection;
return (
<CompactSelect
disabled={disabled}
value={sortBy}
onChange={option => {
onChange(option.value);
}}
options={(
Object.keys(RELEASES_SORT_OPTIONS) as Array<keyof typeof RELEASES_SORT_OPTIONS>
).map(name => {
const filter = RELEASES_SORT_OPTIONS[name];
if (name !== ReleasesSortOption.ADOPTION) {
return {
label: filter,
value: name,
};
}

// Adoption sort requires exactly one environment because it calculates
// the percentage of sessions/users in that specific environment
const isNotSingleEnvironment = environments.length !== 1;
return {
label: filter,
value: name,
disabled: isNotSingleEnvironment,
tooltip: isNotSingleEnvironment
? t('Select one environment to use this sort option.')
: undefined,
};
})}
trigger={triggerProps => (
<OverlayTrigger.IconButton
{...triggerProps}
icon={<IconSort variant="muted" />}
aria-label={t('Sort Releases')}
/>
)}
/>
);
}
8 changes: 7 additions & 1 deletion static/app/views/dashboards/detail.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1531,7 +1531,7 @@ describe('Dashboards > Detail', () => {
],
});
// Mocked search results
MockApiClient.addMockResponse({
const searchMock = MockApiClient.addMockResponse({
url: '/organizations/org-slug/releases/',
body: [
ReleaseFixture({
Expand Down Expand Up @@ -1571,6 +1571,12 @@ describe('Dashboards > Detail', () => {

await userEvent.click(await screen.findByText('All Releases'));
await userEvent.type(screen.getByPlaceholderText('Search\u2026'), 's');

// Wait for debounce and search to complete
await waitFor(() => {
expect(searchMock).toHaveBeenCalled();
});

await userEvent.click(await screen.findByRole('option', {name: 'search-result'}));

// Validate that after search is cleared, search result still appears
Expand Down
45 changes: 36 additions & 9 deletions static/app/views/dashboards/filtersBar.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import {Fragment, useMemo, useState} from 'react';
import {Fragment, useEffect, useMemo, useState} from 'react';
import styled from '@emotion/styled';
import type {Location} from 'history';
import {createParser, useQueryState} from 'nuqs';

import {Button} from 'sentry/components/core/button';
import {ButtonBar} from 'sentry/components/core/button/buttonBar';
import {DatePageFilter} from 'sentry/components/organizations/datePageFilter';
import {EnvironmentPageFilter} from 'sentry/components/organizations/environmentPageFilter';
import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
import {ProjectPageFilter} from 'sentry/components/organizations/projectPageFilter';
import {
DEFAULT_RELEASES_SORT,
RELEASES_SORT_OPTIONS,
ReleasesSortOption,
} from 'sentry/constants/releases';
import {t} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import {DataCategory} from 'sentry/types/core';
Expand All @@ -17,6 +23,7 @@ import {trackAnalytics} from 'sentry/utils/analytics';
import {ToggleOnDemand} from 'sentry/utils/performance/contexts/onDemandControl';
import {useMaxPickableDays} from 'sentry/utils/useMaxPickableDays';
import useOrganization from 'sentry/utils/useOrganization';
import usePageFilters from 'sentry/utils/usePageFilters';
import {useUser} from 'sentry/utils/useUser';
import {useUserTeams} from 'sentry/utils/useUserTeams';
import AddFilter from 'sentry/views/dashboards/globalFilter/addFilter';
Expand All @@ -31,7 +38,7 @@ import {
} from 'sentry/views/dashboards/utils/prebuiltConfigs';

import {checkUserHasEditAccess} from './utils/checkUserHasEditAccess';
import ReleasesSelectControl from './releasesSelectControl';
import {SortableReleasesSelect} from './sortableReleasesSelect';
import type {
DashboardDetails,
DashboardFilters,
Expand Down Expand Up @@ -138,6 +145,18 @@ export default function FiltersBar({
// Calculate maxPickableDays based on the data categories
const maxPickableDaysOptions = useMaxPickableDays({dataCategories});

// Release sort state - validates and defaults to DATE via custom parser
const [releaseSort, setReleaseSort] = useQueryState('sortReleasesBy', parseReleaseSort);

// Reset sort to default if ADOPTION is selected but environment requirement isn't met
const {selection} = usePageFilters();
const {environments} = selection;
useEffect(() => {
if (releaseSort === ReleasesSortOption.ADOPTION && environments.length !== 1) {
setReleaseSort(DEFAULT_RELEASES_SORT);
}
}, [releaseSort, environments.length, setReleaseSort]);

const hasEditAccess = checkUserHasEditAccess(
currentUser,
userTeams,
Expand Down Expand Up @@ -204,19 +223,17 @@ export default function FiltersBar({
}}
/>
</PageFilterBar>
<ReleasesSelectControl
<SortableReleasesSelect
sortBy={releaseSort}
selectedReleases={selectedReleases}
isDisabled={isEditingDashboard}
handleChangeFilter={activeFilters => {
onDashboardFilterChange({
...activeFilters,
[DashboardFilterKeys.GLOBAL_FILTER]: activeGlobalFilters,
});
trackAnalytics('dashboards2.filter.change', {
organization,
filter_type: 'release',
});
}}
selectedReleases={selectedReleases}
isDisabled={isEditingDashboard}
onSortChange={setReleaseSort}
/>
{organization.features.includes('dashboards-global-filters') && (
<Fragment>
Expand Down Expand Up @@ -301,6 +318,16 @@ export default function FiltersBar({
);
}

const parseReleaseSort = createParser({
parse: (value: string): ReleasesSortOption => {
if (value in RELEASES_SORT_OPTIONS) {
return value as ReleasesSortOption;
}
return DEFAULT_RELEASES_SORT;
},
serialize: (value: ReleasesSortOption): string => value,
}).withDefault(DEFAULT_RELEASES_SORT);

const Wrapper = styled('div')`
display: flex;
flex-direction: row;
Expand Down
Loading
Loading