Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
92 changes: 92 additions & 0 deletions static/app/components/preprod/preprodBuildsSearchControls.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import {OverlayTrigger} from '@sentry/scraps/overlayTrigger';

import {CompactSelect, type SelectOption} from 'sentry/components/core/compactSelect';
import {Container, Flex} from 'sentry/components/core/layout';
import {PreprodBuildsDisplay} from 'sentry/components/preprod/preprodBuildsDisplay';
import {PreprodSearchBar} from 'sentry/components/preprod/preprodSearchBar';
import {t} from 'sentry/locale';
import useOrganization from 'sentry/utils/useOrganization';

const displaySelectOptions: Array<SelectOption<PreprodBuildsDisplay>> = [
{value: PreprodBuildsDisplay.SIZE, label: t('Size')},
{value: PreprodBuildsDisplay.DISTRIBUTION, label: t('Distribution')},
];

interface PreprodBuildsSearchControlsProps {
/**
* Current display mode value from URL query
*/
display: PreprodBuildsDisplay;
/**
* Initial search query value
*/
initialQuery: string;
/**
* Called when display mode changes
*/
onDisplayChange: (display: PreprodBuildsDisplay) => void;
/**
* Project IDs to filter search attributes
*/
projects: number[];
/**
* Called on every keystroke (for controlled input with debounce)
*/
onChange?: (query: string, state: {queryIsValid: boolean}) => void;
/**
* Called when search is submitted (e.g., on Enter)
*/
onSearch?: (query: string) => void;
}

/**
* Reusable search controls for preprod builds pages.
* Combines search bar with optional display mode toggle.
*/
export function PreprodBuildsSearchControls({
initialQuery,
display,
projects,
onChange,
onSearch,
onDisplayChange,
}: PreprodBuildsSearchControlsProps) {
const organization = useOrganization();
const hasDistributionFeature = organization.features.includes(
'preprod-build-distribution'
);

return (
<Flex
align={{xs: 'stretch', sm: 'center'}}
direction={{xs: 'column', sm: 'row'}}
gap="md"
wrap="wrap"
>
<Container flex="1">
<PreprodSearchBar
initialQuery={initialQuery}
onChange={onChange}
onSearch={onSearch}
projects={projects}
/>
</Container>
{hasDistributionFeature && (
<Container maxWidth="200px">
<CompactSelect
options={displaySelectOptions}
value={display}
onChange={option => onDisplayChange(option.value)}
trigger={triggerProps => (
<OverlayTrigger.Button
{...triggerProps}
prefix={t('Display')}
style={{width: '100%', zIndex: 1}}
/>
)}
/>
</Container>
)}
</Flex>
);
}
11 changes: 6 additions & 5 deletions static/app/components/preprod/preprodSearchBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,6 @@ interface PreprodSearchBarProps {
* When true, parens and logical operators (AND, OR) will be marked as invalid.
*/
disallowLogicalOperators?: boolean;
/**
* List of attribute keys to hide from the search bar. Defaults to HIDDEN_PREPROD_ATTRIBUTES.
*/
hiddenKeys?: string[];
onChange?: (query: string, state: {queryIsValid: boolean}) => void;
onSearch?: (query: string) => void;
Expand Down Expand Up @@ -80,11 +77,15 @@ export function PreprodSearchBar({
useTraceItemAttributesWithConfig(traceItemAttributeConfig, 'number', hiddenKeys);

const filteredStringAttributes = useMemo(
() => filterAttributes(stringAttributes, allowedKeys),
() => ({
...filterAttributes(stringAttributes, allowedKeys),
}),
[stringAttributes, allowedKeys]
);
const filteredNumberAttributes = useMemo(
() => filterAttributes(numberAttributes, allowedKeys),
() => ({
...filterAttributes(numberAttributes, allowedKeys),
}),
[numberAttributes, allowedKeys]
);

Expand Down
65 changes: 0 additions & 65 deletions static/app/views/preprod/components/preprodBuildsSearchBar.tsx

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import {useCallback, useContext, useEffect, useMemo, useState} from 'react';

import {Container, Flex} from 'sentry/components/core/layout';
import {Container} from 'sentry/components/core/layout';
import * as Layout from 'sentry/components/layouts/thirds';
import LoadingError from 'sentry/components/loadingError';
import {
getPreprodBuildsDisplay,
PreprodBuildsDisplay,
} from 'sentry/components/preprod/preprodBuildsDisplay';
import {PreprodBuildsSearchControls} from 'sentry/components/preprod/preprodBuildsSearchControls';
import {PreprodBuildsTable} from 'sentry/components/preprod/preprodBuildsTable';
import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
import {t} from 'sentry/locale';
Expand All @@ -22,10 +23,8 @@ import {useLocation} from 'sentry/utils/useLocation';
import useOrganization from 'sentry/utils/useOrganization';
import {useParams} from 'sentry/utils/useParams';
import {formatVersion} from 'sentry/utils/versions/formatVersion';
import PreprodBuildsSearchBar from 'sentry/views/preprod/components/preprodBuildsSearchBar';
import {usePreprodBuildsAnalytics} from 'sentry/views/preprod/hooks/usePreprodBuildsAnalytics';
import type {BuildDetailsApiResponse} from 'sentry/views/preprod/types/buildDetailsTypes';
import type {ListBuildsApiResponse} from 'sentry/views/preprod/types/listBuildsTypes';
import {ReleaseContext} from 'sentry/views/releases/detail';

import {PreprodOnboarding} from './preprodOnboarding';
Expand Down Expand Up @@ -96,12 +95,11 @@ export default function PreprodBuilds() {
error: buildsError,
refetch,
getResponseHeader,
}: UseApiQueryResult<
ListBuildsApiResponse,
RequestError
> = useApiQuery<ListBuildsApiResponse>(
}: UseApiQueryResult<BuildDetailsApiResponse[], RequestError> = useApiQuery<
BuildDetailsApiResponse[]
>(
[
getApiUrl(`/organizations/$organizationIdOrSlug/preprodartifacts/list-builds/`, {
getApiUrl(`/organizations/$organizationIdOrSlug/builds/`, {
path: {organizationIdOrSlug: organization.slug},
}),
{query: queryParams},
Expand All @@ -112,7 +110,7 @@ export default function PreprodBuilds() {
}
);

const handleSearch = (query: string) => {
const handleSearch = (query: string, _state?: {queryIsValid: boolean}) => {
setLocalSearchQuery(query);
};

Expand All @@ -130,7 +128,7 @@ export default function PreprodBuilds() {
[location]
);

const builds = buildsData?.builds || [];
const builds = buildsData ?? [];
const pageLinks = getResponseHeader?.('Link') || null;

const hasSearchQuery = !!urlSearchQuery?.trim();
Expand Down Expand Up @@ -171,23 +169,13 @@ export default function PreprodBuilds() {
/>
{buildsError && <LoadingError onRetry={refetch} />}
<Container paddingBottom="md">
<Flex
align={{xs: 'stretch', sm: 'center'}}
direction={{xs: 'column', sm: 'row'}}
gap="md"
wrap="wrap"
>
<PreprodBuildsSearchBar
onChange={handleSearch}
query={localSearchQuery}
disabled={isLoadingBuilds}
displayOptions={
hasDistributionFeature
? {selected: activeDisplay, onSelect: handleDisplayChange}
: undefined
}
/>
</Flex>
<PreprodBuildsSearchControls
initialQuery={localSearchQuery}
display={activeDisplay}
projects={[Number(projectId)]}
onChange={handleSearch}
onDisplayChange={handleDisplayChange}
/>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Search bar won't sync with external URL changes

Medium Severity

The migration from SearchBar (controlled with query prop) to SearchQueryBuilder (uncontrolled with initialQuery prop) breaks synchronization when the URL changes externally. The old SearchBar had a useEffect that synced internal state with prop changes, but SearchQueryBuilder only uses initialQuery for initialization. When users navigate via browser back/forward buttons or receive a shared URL, the API will fetch the correct results but the search bar will display the stale query text, causing a confusing UI/data mismatch.

Additional Locations (1)

Fix in Cursor Fix in Web

Copy link
Contributor

@NicoHinderling NicoHinderling Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the URL parsing is working but admittedly the URL looks super weird

https://sentry-2ji6yy0i9.sentry.dev/explore/releases/?query=app_id%3A%EF%80%8DContains%EF%80%8Dcom.emergetools.hackernews&tab=mobile-builds

Image

lol not sure why that would happen

edit: tried to see if I could do a quick fix and i think we should just leave this as is 👍

</Container>
{showOnboarding ? (
<PreprodOnboarding projectPlatform={projectPlatform || null} />
Expand Down
Loading
Loading