index.tsx 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766
  1. import {Fragment} from 'react';
  2. import {forceCheck} from 'react-lazyload';
  3. import type {RouteComponentProps} from 'react-router';
  4. import styled from '@emotion/styled';
  5. import pick from 'lodash/pick';
  6. import {fetchTagValues} from 'sentry/actionCreators/tags';
  7. import type {Client} from 'sentry/api';
  8. import {Alert} from 'sentry/components/alert';
  9. import GuideAnchor from 'sentry/components/assistant/guideAnchor';
  10. import EmptyMessage from 'sentry/components/emptyMessage';
  11. import FloatingFeedbackWidget from 'sentry/components/feedback/widget/floatingFeedbackWidget';
  12. import * as Layout from 'sentry/components/layouts/thirds';
  13. import ExternalLink from 'sentry/components/links/externalLink';
  14. import LoadingIndicator from 'sentry/components/loadingIndicator';
  15. import NoProjectMessage from 'sentry/components/noProjectMessage';
  16. import {DatePageFilter} from 'sentry/components/organizations/datePageFilter';
  17. import {EnvironmentPageFilter} from 'sentry/components/organizations/environmentPageFilter';
  18. import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
  19. import PageFiltersContainer from 'sentry/components/organizations/pageFilters/container';
  20. import {ProjectPageFilter} from 'sentry/components/organizations/projectPageFilter';
  21. import Pagination from 'sentry/components/pagination';
  22. import Panel from 'sentry/components/panels/panel';
  23. import SmartSearchBar from 'sentry/components/smartSearchBar';
  24. import {ItemType} from 'sentry/components/smartSearchBar/types';
  25. import {getRelativeSummary} from 'sentry/components/timeRangeSelector/utils';
  26. import {DEFAULT_STATS_PERIOD} from 'sentry/constants';
  27. import {ALL_ACCESS_PROJECTS} from 'sentry/constants/pageFilters';
  28. import {releaseHealth} from 'sentry/data/platformCategories';
  29. import {IconSearch} from 'sentry/icons';
  30. import {t} from 'sentry/locale';
  31. import ProjectsStore from 'sentry/stores/projectsStore';
  32. import {space} from 'sentry/styles/space';
  33. import type {
  34. AvatarProject,
  35. Organization,
  36. PageFilters,
  37. Project,
  38. Release,
  39. Tag,
  40. } from 'sentry/types';
  41. import {ReleaseStatus} from 'sentry/types';
  42. import {trackAnalytics} from 'sentry/utils/analytics';
  43. import {SEMVER_TAGS} from 'sentry/utils/discover/fields';
  44. import Projects from 'sentry/utils/projects';
  45. import routeTitleGen from 'sentry/utils/routeTitle';
  46. import withApi from 'sentry/utils/withApi';
  47. import withOrganization from 'sentry/utils/withOrganization';
  48. import withPageFilters from 'sentry/utils/withPageFilters';
  49. import withProjects from 'sentry/utils/withProjects';
  50. import DeprecatedAsyncView from 'sentry/views/deprecatedAsyncView';
  51. import Header from '../components/header';
  52. import ReleaseFeedbackBanner from '../components/releaseFeedbackBanner';
  53. import ReleaseArchivedNotice from '../detail/overview/releaseArchivedNotice';
  54. import {isMobileRelease} from '../utils';
  55. import {fetchThresholdStatuses} from '../utils/fetchThresholdStatus';
  56. import type {
  57. Threshold,
  58. ThresholdQuery,
  59. ThresholdStatus,
  60. ThresholdStatusesQuery,
  61. } from '../utils/types';
  62. import ReleaseCard from './releaseCard';
  63. import ReleasesAdoptionChart from './releasesAdoptionChart';
  64. import ReleasesDisplayOptions, {ReleasesDisplayOption} from './releasesDisplayOptions';
  65. import ReleasesPromo from './releasesPromo';
  66. import ReleasesRequest from './releasesRequest';
  67. import ReleasesSortOptions, {ReleasesSortOption} from './releasesSortOptions';
  68. import ReleasesStatusOptions, {ReleasesStatusOption} from './releasesStatusOptions';
  69. type RouteParams = {
  70. orgId: string;
  71. };
  72. type Props = RouteComponentProps<RouteParams, {}> & {
  73. api: Client;
  74. organization: Organization;
  75. projects: Project[];
  76. selection: PageFilters;
  77. };
  78. type State = {
  79. releases: Release[];
  80. thresholds: Threshold[];
  81. thresholdStatuses?: {[key: string]: ThresholdStatus[]};
  82. } & DeprecatedAsyncView['state'];
  83. class ReleasesList extends DeprecatedAsyncView<Props, State> {
  84. shouldReload = true;
  85. shouldRenderBadRequests = true;
  86. hasV2ReleaseUIEnabled =
  87. this.props.organization.features.includes('releases-v2-internal') ||
  88. this.props.organization.features.includes('releases-v2') ||
  89. this.props.organization.features.includes('releases-v2-st');
  90. getTitle() {
  91. return routeTitleGen(t('Releases'), this.props.organization.slug, false);
  92. }
  93. getEndpoints(): ReturnType<DeprecatedAsyncView['getEndpoints']> {
  94. const {organization, location, selection} = this.props;
  95. const {statsPeriod} = location.query;
  96. const activeSort = this.getSort();
  97. const activeStatus = this.getStatus();
  98. const query = {
  99. ...pick(location.query, ['project', 'environment', 'cursor', 'query', 'sort']),
  100. summaryStatsPeriod: statsPeriod,
  101. per_page: 20,
  102. flatten: activeSort === ReleasesSortOption.DATE ? 0 : 1,
  103. adoptionStages: 1,
  104. status:
  105. activeStatus === ReleasesStatusOption.ARCHIVED
  106. ? ReleaseStatus.ARCHIVED
  107. : ReleaseStatus.ACTIVE,
  108. };
  109. const endpoints: ReturnType<DeprecatedAsyncView['getEndpoints']> = [
  110. [
  111. 'releases', // stateKey
  112. `/organizations/${organization.slug}/releases/`, // endpoint
  113. {query}, // params
  114. {disableEntireQuery: true}, // options - prevent cursor from being passed into query
  115. ],
  116. ];
  117. if (this.hasV2ReleaseUIEnabled) {
  118. // prefetch all thresholds so we know whether to expect a threshold or not
  119. const thresholdQuery: ThresholdQuery = {};
  120. if (selection.projects.length) {
  121. thresholdQuery.project = selection.projects;
  122. } else {
  123. thresholdQuery.project = [ALL_ACCESS_PROJECTS];
  124. }
  125. if (selection.environments.length) {
  126. thresholdQuery.environment = selection.environments;
  127. }
  128. endpoints.push([
  129. 'thresholds',
  130. `/organizations/${organization.slug}/release-thresholds/`,
  131. {query: thresholdQuery},
  132. {disableEntireQuery: true}, // options to prevent cursor from being passed
  133. ]);
  134. }
  135. return endpoints;
  136. }
  137. componentDidUpdate(prevProps: Props, prevState: State) {
  138. super.componentDidUpdate(prevProps, prevState);
  139. if (prevState.releases !== this.state.releases) {
  140. /**
  141. * Manually trigger checking for elements in viewport.
  142. * Helpful when LazyLoad components enter the viewport without resize or scroll events,
  143. * https://github.com/twobin/react-lazyload#forcecheck
  144. *
  145. * HealthStatsCharts are being rendered only when they are scrolled into viewport.
  146. * This is how we re-check them without scrolling once releases change as this view
  147. * uses shouldReload=true and there is no reloading happening.
  148. */
  149. forceCheck();
  150. if (this.hasV2ReleaseUIEnabled) {
  151. // Refetch new threshold statuses if new releases are fetched
  152. this.fetchThresholdStatuses();
  153. }
  154. }
  155. }
  156. fetchThresholdStatuses() {
  157. const {selection, organization, api} = this.props;
  158. const {releases} = this.state;
  159. if (releases.length < 1) {
  160. return;
  161. }
  162. // Grab earliest release and latest release - then fetch all statuses within
  163. const fuzzSec = 30;
  164. const initialRelease = releases[0];
  165. let start = new Date(new Date(initialRelease.dateCreated).getTime() - fuzzSec * 1000);
  166. let end = new Date(new Date(initialRelease.dateCreated).getTime() + fuzzSec * 1000);
  167. const releaseVersions: string[] = [];
  168. releases.forEach(release => {
  169. const created = new Date(release.dateCreated);
  170. if (created < start) {
  171. start = created;
  172. }
  173. if (created > end) {
  174. end = created;
  175. }
  176. releaseVersions.push(release.version);
  177. });
  178. const query: ThresholdStatusesQuery = {
  179. start: start.toISOString(),
  180. end: end.toISOString(),
  181. release: releaseVersions,
  182. };
  183. if (selection.projects.length) {
  184. query.projectSlug = this.getSelectedProjectSlugs();
  185. }
  186. if (selection.environments.length) {
  187. query.environment = selection.environments;
  188. }
  189. fetchThresholdStatuses(organization, api, query).then(thresholdStatuses => {
  190. this.setState({thresholdStatuses});
  191. });
  192. }
  193. getQuery() {
  194. const {query} = this.props.location.query;
  195. return typeof query === 'string' ? query : undefined;
  196. }
  197. getSort(): ReleasesSortOption {
  198. const {environments} = this.props.selection;
  199. const {sort} = this.props.location.query;
  200. // Require 1 environment for date adopted
  201. if (sort === ReleasesSortOption.ADOPTION && environments.length !== 1) {
  202. return ReleasesSortOption.DATE;
  203. }
  204. const sortExists = Object.values(ReleasesSortOption).includes(sort);
  205. if (sortExists) {
  206. return sort;
  207. }
  208. return ReleasesSortOption.DATE;
  209. }
  210. getDisplay(): ReleasesDisplayOption {
  211. const {display} = this.props.location.query;
  212. switch (display) {
  213. case ReleasesDisplayOption.USERS:
  214. return ReleasesDisplayOption.USERS;
  215. default:
  216. return ReleasesDisplayOption.SESSIONS;
  217. }
  218. }
  219. getStatus(): ReleasesStatusOption {
  220. const {status} = this.props.location.query;
  221. switch (status) {
  222. case ReleasesStatusOption.ARCHIVED:
  223. return ReleasesStatusOption.ARCHIVED;
  224. default:
  225. return ReleasesStatusOption.ACTIVE;
  226. }
  227. }
  228. getSelectedProject(): Project | undefined {
  229. const {selection, projects} = this.props;
  230. const selectedProjectId =
  231. selection.projects && selection.projects.length === 1 && selection.projects[0];
  232. return projects?.find(p => p.id === `${selectedProjectId}`);
  233. }
  234. getSelectedProjectSlugs(): string[] {
  235. const {selection, projects} = this.props;
  236. const projIdSet = new Set(selection.projects);
  237. return projects.reduce((result: string[], proj) => {
  238. if (projIdSet.has(Number(proj.id))) {
  239. result.push(proj.slug);
  240. }
  241. return result;
  242. }, []);
  243. }
  244. get projectHasSessions() {
  245. return this.getSelectedProject()?.hasSessions ?? null;
  246. }
  247. getThresholdsForRelease(release: Release): Threshold[] {
  248. if (!this.hasV2ReleaseUIEnabled) {
  249. return [];
  250. }
  251. const {thresholds} = this.state;
  252. const lastDeploy = release.lastDeploy;
  253. const projectSlugs = release.projects.map(p => p.slug);
  254. return thresholds.filter(
  255. threshold =>
  256. projectSlugs.includes(threshold.project.slug) &&
  257. lastDeploy?.environment === threshold.environment?.name
  258. );
  259. }
  260. handleSearch = (query: string) => {
  261. const {location, router} = this.props;
  262. router.push({
  263. ...location,
  264. query: {...location.query, cursor: undefined, query},
  265. });
  266. };
  267. handleSortBy = (sort: string) => {
  268. const {location, router} = this.props;
  269. router.push({
  270. ...location,
  271. query: {...location.query, cursor: undefined, sort},
  272. });
  273. };
  274. handleDisplay = (display: string) => {
  275. const {location, router} = this.props;
  276. let sort = location.query.sort;
  277. if (
  278. sort === ReleasesSortOption.USERS_24_HOURS &&
  279. display === ReleasesDisplayOption.SESSIONS
  280. ) {
  281. sort = ReleasesSortOption.SESSIONS_24_HOURS;
  282. } else if (
  283. sort === ReleasesSortOption.SESSIONS_24_HOURS &&
  284. display === ReleasesDisplayOption.USERS
  285. ) {
  286. sort = ReleasesSortOption.USERS_24_HOURS;
  287. } else if (
  288. sort === ReleasesSortOption.CRASH_FREE_USERS &&
  289. display === ReleasesDisplayOption.SESSIONS
  290. ) {
  291. sort = ReleasesSortOption.CRASH_FREE_SESSIONS;
  292. } else if (
  293. sort === ReleasesSortOption.CRASH_FREE_SESSIONS &&
  294. display === ReleasesDisplayOption.USERS
  295. ) {
  296. sort = ReleasesSortOption.CRASH_FREE_USERS;
  297. }
  298. router.push({
  299. ...location,
  300. query: {...location.query, cursor: undefined, display, sort},
  301. });
  302. };
  303. handleStatus = (status: string) => {
  304. const {location, router} = this.props;
  305. router.push({
  306. ...location,
  307. query: {...location.query, cursor: undefined, status},
  308. });
  309. };
  310. trackAddReleaseHealth = () => {
  311. const {organization, selection} = this.props;
  312. if (organization.id && selection.projects[0]) {
  313. trackAnalytics('releases_list.click_add_release_health', {
  314. organization,
  315. project_id: selection.projects[0],
  316. });
  317. }
  318. };
  319. tagValueLoader = (key: string, search: string) => {
  320. const {location, organization} = this.props;
  321. const {project: projectId} = location.query;
  322. return fetchTagValues({
  323. api: this.api,
  324. orgSlug: organization.slug,
  325. tagKey: key,
  326. search,
  327. projectIds: projectId ? [projectId] : undefined,
  328. endpointParams: location.query,
  329. });
  330. };
  331. getTagValues = async (tag: Tag, currentQuery: string): Promise<string[]> => {
  332. const values = await this.tagValueLoader(tag.key, currentQuery);
  333. return values.map(({value}) => value);
  334. };
  335. shouldShowLoadingIndicator() {
  336. const {loading, releases, reloading} = this.state;
  337. return (loading && !reloading) || (loading && !releases?.length);
  338. }
  339. renderLoading() {
  340. return this.renderBody();
  341. }
  342. renderError() {
  343. return this.renderBody();
  344. }
  345. get shouldShowQuickstart() {
  346. const {releases} = this.state;
  347. const selectedProject = this.getSelectedProject();
  348. const hasReleasesSetup = selectedProject?.features.includes('releases');
  349. return !releases?.length && !hasReleasesSetup && selectedProject;
  350. }
  351. renderEmptyMessage() {
  352. const {location} = this.props;
  353. const {statsPeriod, start, end} = location.query;
  354. const searchQuery = this.getQuery();
  355. const activeSort = this.getSort();
  356. const activeStatus = this.getStatus();
  357. const selectedPeriod =
  358. !!start && !!end
  359. ? t('time range')
  360. : getRelativeSummary(statsPeriod || DEFAULT_STATS_PERIOD).toLowerCase();
  361. if (searchQuery?.length) {
  362. return (
  363. <Panel>
  364. <EmptyMessage icon={<IconSearch size="xl" />} size="large">{`${t(
  365. 'There are no releases that match'
  366. )}: '${searchQuery}'.`}</EmptyMessage>
  367. </Panel>
  368. );
  369. }
  370. if (activeSort === ReleasesSortOption.USERS_24_HOURS) {
  371. return (
  372. <Panel>
  373. <EmptyMessage icon={<IconSearch size="xl" />} size="large">
  374. {t(
  375. 'There are no releases with active user data (users in the last 24 hours).'
  376. )}
  377. </EmptyMessage>
  378. </Panel>
  379. );
  380. }
  381. if (activeSort === ReleasesSortOption.SESSIONS_24_HOURS) {
  382. return (
  383. <Panel>
  384. <EmptyMessage icon={<IconSearch size="xl" />} size="large">
  385. {t(
  386. 'There are no releases with active session data (sessions in the last 24 hours).'
  387. )}
  388. </EmptyMessage>
  389. </Panel>
  390. );
  391. }
  392. if (
  393. activeSort === ReleasesSortOption.BUILD ||
  394. activeSort === ReleasesSortOption.SEMVER
  395. ) {
  396. return (
  397. <Panel>
  398. <EmptyMessage icon={<IconSearch size="xl" />} size="large">
  399. {t('There are no releases with semantic versioning.')}
  400. </EmptyMessage>
  401. </Panel>
  402. );
  403. }
  404. if (activeSort !== ReleasesSortOption.DATE) {
  405. return (
  406. <Panel>
  407. <EmptyMessage icon={<IconSearch size="xl" />} size="large">
  408. {`${t('There are no releases with data in the')} ${selectedPeriod}.`}
  409. </EmptyMessage>
  410. </Panel>
  411. );
  412. }
  413. if (activeStatus === ReleasesStatusOption.ARCHIVED) {
  414. return (
  415. <Panel>
  416. <EmptyMessage icon={<IconSearch size="xl" />} size="large">
  417. {t('There are no archived releases.')}
  418. </EmptyMessage>
  419. </Panel>
  420. );
  421. }
  422. return (
  423. <Panel>
  424. <EmptyMessage icon={<IconSearch size="xl" />} size="large">
  425. {`${t('There are no releases with data in the')} ${selectedPeriod}.`}
  426. </EmptyMessage>
  427. </Panel>
  428. );
  429. }
  430. renderHealthCta() {
  431. const {organization} = this.props;
  432. const {releases} = this.state;
  433. const selectedProject = this.getSelectedProject();
  434. if (!selectedProject || this.projectHasSessions !== false || !releases?.length) {
  435. return null;
  436. }
  437. return (
  438. <Projects orgId={organization.slug} slugs={[selectedProject.slug]}>
  439. {({projects, initiallyLoaded, fetchError}) => {
  440. const project: AvatarProject | undefined =
  441. projects?.length === 1 ? projects.at(0) : undefined;
  442. const projectCanHaveReleases =
  443. project?.platform && releaseHealth.includes(project.platform);
  444. if (!initiallyLoaded || fetchError || !projectCanHaveReleases) {
  445. return null;
  446. }
  447. return (
  448. <Alert type="info" showIcon>
  449. <AlertText>
  450. <div>
  451. {t(
  452. 'To track user adoption, crash rates, session data and more, add Release Health to your current setup.'
  453. )}
  454. </div>
  455. <ExternalLink
  456. href="https://docs.sentry.io/product/releases/setup/#release-health"
  457. onClick={this.trackAddReleaseHealth}
  458. >
  459. {t('Add Release Health')}
  460. </ExternalLink>
  461. </AlertText>
  462. </Alert>
  463. );
  464. }}
  465. </Projects>
  466. );
  467. }
  468. renderInnerBody(
  469. activeDisplay: ReleasesDisplayOption,
  470. showReleaseAdoptionStages: boolean
  471. ) {
  472. const {location, selection, organization, router} = this.props;
  473. const {releases, reloading, releasesPageLinks, thresholdStatuses} = this.state;
  474. const selectedProject = this.getSelectedProject();
  475. const hasReleasesSetup = selectedProject?.features.includes('releases');
  476. if (this.shouldShowLoadingIndicator()) {
  477. return <LoadingIndicator />;
  478. }
  479. if (!releases?.length && hasReleasesSetup) {
  480. return this.renderEmptyMessage();
  481. }
  482. if (this.shouldShowQuickstart) {
  483. return <ReleasesPromo organization={organization} project={selectedProject!} />;
  484. }
  485. return (
  486. <ReleasesRequest
  487. releases={releases.map(({version}) => version)}
  488. organization={organization}
  489. selection={selection}
  490. location={location}
  491. display={[this.getDisplay()]}
  492. releasesReloading={reloading}
  493. healthStatsPeriod={location.query.healthStatsPeriod}
  494. >
  495. {({isHealthLoading, getHealthData}) => {
  496. const singleProjectSelected =
  497. selection.projects?.length === 1 &&
  498. selection.projects[0] !== ALL_ACCESS_PROJECTS;
  499. // TODO: project specific chart should live on the project details page.
  500. const isMobileProject =
  501. selectedProject?.platform && isMobileRelease(selectedProject.platform);
  502. return (
  503. <Fragment>
  504. {singleProjectSelected && this.projectHasSessions && isMobileProject && (
  505. <ReleasesAdoptionChart
  506. organization={organization}
  507. selection={selection}
  508. location={location}
  509. router={router}
  510. activeDisplay={activeDisplay}
  511. />
  512. )}
  513. {releases.map((release, index) => (
  514. <ReleaseCard
  515. key={`${release.projects[0].slug}-${release.version}`}
  516. activeDisplay={activeDisplay}
  517. release={release}
  518. organization={organization}
  519. location={location}
  520. selection={selection}
  521. reloading={reloading}
  522. showHealthPlaceholders={isHealthLoading}
  523. isTopRelease={index === 0}
  524. getHealthData={getHealthData}
  525. showReleaseAdoptionStages={showReleaseAdoptionStages}
  526. thresholds={this.getThresholdsForRelease(release)}
  527. thresholdStatuses={thresholdStatuses || {}}
  528. />
  529. ))}
  530. <Pagination pageLinks={releasesPageLinks} />
  531. </Fragment>
  532. );
  533. }}
  534. </ReleasesRequest>
  535. );
  536. }
  537. renderBody() {
  538. const {organization, selection, router} = this.props;
  539. const {releases, reloading, error} = this.state;
  540. const activeSort = this.getSort();
  541. const activeStatus = this.getStatus();
  542. const activeDisplay = this.getDisplay();
  543. const hasAnyMobileProject = selection.projects
  544. .map(id => `${id}`)
  545. .map(ProjectsStore.getById)
  546. .some(project => project?.platform && isMobileRelease(project.platform));
  547. const showReleaseAdoptionStages =
  548. hasAnyMobileProject && selection.environments.length === 1;
  549. const hasReleasesSetup = releases && releases.length > 0;
  550. return (
  551. <PageFiltersContainer showAbsolute={false}>
  552. <NoProjectMessage organization={organization}>
  553. <Header
  554. router={router}
  555. hasV2ReleaseUIEnabled={this.hasV2ReleaseUIEnabled}
  556. organization={organization}
  557. />
  558. <Layout.Body>
  559. <Layout.Main fullWidth>
  560. {organization.features.includes('releases-v2-banner') && (
  561. <ReleaseFeedbackBanner />
  562. )}
  563. {this.renderHealthCta()}
  564. <ReleasesPageFilterBar condensed>
  565. <GuideAnchor target="release_projects">
  566. <ProjectPageFilter />
  567. </GuideAnchor>
  568. <EnvironmentPageFilter />
  569. <DatePageFilter
  570. disallowArbitraryRelativeRanges
  571. menuFooterMessage={t(
  572. 'Changing this date range will recalculate the release metrics.'
  573. )}
  574. />
  575. </ReleasesPageFilterBar>
  576. {this.shouldShowQuickstart ? null : (
  577. <SortAndFilterWrapper>
  578. <GuideAnchor
  579. target="releases_search"
  580. position="bottom"
  581. disabled={!hasReleasesSetup}
  582. >
  583. <StyledSmartSearchBar
  584. searchSource="releases"
  585. query={this.getQuery()}
  586. placeholder={t('Search by version, build, package, or stage')}
  587. hasRecentSearches={false}
  588. supportedTags={{
  589. ...SEMVER_TAGS,
  590. release: {
  591. key: 'release',
  592. name: 'release',
  593. },
  594. }}
  595. maxMenuHeight={500}
  596. supportedTagType={ItemType.PROPERTY}
  597. onSearch={this.handleSearch}
  598. onGetTagValues={this.getTagValues}
  599. />
  600. </GuideAnchor>
  601. <ReleasesStatusOptions
  602. selected={activeStatus}
  603. onSelect={this.handleStatus}
  604. />
  605. <ReleasesSortOptions
  606. selected={activeSort}
  607. selectedDisplay={activeDisplay}
  608. onSelect={this.handleSortBy}
  609. environments={selection.environments}
  610. />
  611. <ReleasesDisplayOptions
  612. selected={activeDisplay}
  613. onSelect={this.handleDisplay}
  614. />
  615. </SortAndFilterWrapper>
  616. )}
  617. {!reloading &&
  618. activeStatus === ReleasesStatusOption.ARCHIVED &&
  619. !!releases?.length && <ReleaseArchivedNotice multi />}
  620. {error
  621. ? super.renderError()
  622. : this.renderInnerBody(activeDisplay, showReleaseAdoptionStages)}
  623. <FloatingFeedbackWidget />
  624. </Layout.Main>
  625. </Layout.Body>
  626. </NoProjectMessage>
  627. </PageFiltersContainer>
  628. );
  629. }
  630. }
  631. const AlertText = styled('div')`
  632. display: flex;
  633. align-items: flex-start;
  634. justify-content: flex-start;
  635. gap: ${space(2)};
  636. > *:nth-child(1) {
  637. flex: 1;
  638. }
  639. flex-direction: column;
  640. @media (min-width: ${p => p.theme.breakpoints.medium}) {
  641. flex-direction: row;
  642. }
  643. `;
  644. const ReleasesPageFilterBar = styled(PageFilterBar)`
  645. margin-bottom: ${space(2)};
  646. `;
  647. const SortAndFilterWrapper = styled('div')`
  648. display: grid;
  649. grid-template-columns: 1fr repeat(3, max-content);
  650. gap: ${space(2)};
  651. margin-bottom: ${space(2)};
  652. @media (max-width: ${p => p.theme.breakpoints.medium}) {
  653. grid-template-columns: repeat(3, 1fr);
  654. & > div {
  655. width: auto;
  656. }
  657. }
  658. @media (max-width: ${p => p.theme.breakpoints.small}) {
  659. grid-template-columns: minmax(0, 1fr);
  660. }
  661. `;
  662. const StyledSmartSearchBar = styled(SmartSearchBar)`
  663. @media (max-width: ${p => p.theme.breakpoints.medium}) {
  664. grid-column: 1 / -1;
  665. }
  666. `;
  667. export default withApi(withProjects(withOrganization(withPageFilters(ReleasesList))));