Skip to content

Commit be31b59

Browse files
authored
feat(ui): Split fetching of releases into two network requests (#18795)
Releases endpoint can be called with health: 1 parameter which will return releases with their health data. This request can take significantly longer (especially for big orgs) compared to health: 0. We decided to call health: 0 on the releases page and then fetch health: 1 lazily, showing placeholders in the meantime. We will skip the two-phased load for sorting other than date created (default) since in those cases we already need to go to the session store anyways (which takes here the long time).
1 parent c6e037b commit be31b59

File tree

8 files changed

+242
-58
lines changed

8 files changed

+242
-58
lines changed

src/sentry/static/sentry/app/types/index.tsx

+14-5
Original file line numberDiff line numberDiff line change
@@ -880,7 +880,17 @@ export type UserReport = {
880880
email: string;
881881
};
882882

883-
export type Release = {
883+
export type Release = BaseRelease &
884+
ReleaseData & {
885+
projects: ReleaseProject[];
886+
};
887+
888+
export type ReleaseWithHealth = BaseRelease &
889+
ReleaseData & {
890+
projects: Required<ReleaseProject>[];
891+
};
892+
893+
type ReleaseData = {
884894
commitCount: number;
885895
data: {};
886896
lastDeploy?: Deploy;
@@ -891,11 +901,10 @@ export type Release = {
891901
authors: User[];
892902
owner?: any; // TODO(ts)
893903
newGroups: number;
894-
projects: ReleaseProject[];
895904
versionInfo: VersionInfo;
896-
} & BaseRelease;
905+
};
897906

898-
export type BaseRelease = {
907+
type BaseRelease = {
899908
dateReleased: string;
900909
url: string;
901910
dateCreated: string;
@@ -911,7 +920,7 @@ export type ReleaseProject = {
911920
platform: string;
912921
platforms: string[];
913922
newGroups: number;
914-
healthData: Health;
923+
healthData?: Health;
915924
};
916925

917926
export type ReleaseMeta = {

src/sentry/static/sentry/app/views/releasesV2/detail/index.tsx

+4-4
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ import styled from '@emotion/styled';
66
import {t} from 'app/locale';
77
import {
88
Organization,
9-
Release,
109
ReleaseProject,
1110
ReleaseMeta,
1211
Deploy,
1312
GlobalSelection,
13+
ReleaseWithHealth,
1414
} from 'app/types';
1515
import AsyncView from 'app/views/asyncView';
1616
import GlobalSelectionHeader from 'app/components/organizations/globalSelectionHeader';
@@ -31,8 +31,8 @@ import ReleaseHeader from './releaseHeader';
3131
import PickProjectToContinue from './pickProjectToContinue';
3232

3333
type ReleaseContext = {
34-
release: Release;
35-
project: ReleaseProject;
34+
release: ReleaseWithHealth;
35+
project: Required<ReleaseProject>;
3636
deploys: Deploy[];
3737
releaseMeta: ReleaseMeta;
3838
};
@@ -50,7 +50,7 @@ type Props = RouteComponentProps<RouteParams, {}> & {
5050
};
5151

5252
type State = {
53-
release: Release;
53+
release: ReleaseWithHealth;
5454
deploys: Deploy[];
5555
} & AsyncView['state'];
5656

src/sentry/static/sentry/app/views/releasesV2/detail/overview/projectReleaseDetails.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@ import styled from '@emotion/styled';
33

44
import {t} from 'app/locale';
55
import space from 'app/styles/space';
6-
import {Release} from 'app/types';
6+
import {ReleaseWithHealth} from 'app/types';
77
import Version from 'app/components/version';
88
import TimeSince from 'app/components/timeSince';
99
import DateTime from 'app/components/dateTime';
1010

1111
import {SectionHeading, Wrapper} from './styles';
1212

1313
type Props = {
14-
release: Release;
14+
release: ReleaseWithHealth;
1515
};
1616

1717
const ProjectReleaseDetails = ({release}: Props) => {

src/sentry/static/sentry/app/views/releasesV2/detail/releaseHeader.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ type Props = {
2626
location: Location;
2727
orgId: string;
2828
release: Release;
29-
project: ReleaseProject;
29+
project: Required<ReleaseProject>;
3030
releaseMeta: ReleaseMeta;
3131
};
3232

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import React from 'react';
2+
import styled from '@emotion/styled';
3+
4+
import space from 'app/styles/space';
5+
import {t, tct} from 'app/locale';
6+
import Button from 'app/components/button';
7+
8+
type DefaultProps = {
9+
maxVisibleItems: number;
10+
fadeHeight: string;
11+
};
12+
13+
type Props = DefaultProps & {
14+
children: React.ReactNode[];
15+
className?: string;
16+
};
17+
18+
type State = {
19+
collapsed: boolean;
20+
};
21+
22+
// TODO(matej): refactor to reusable component
23+
24+
class clippedHealthRows extends React.Component<Props, State> {
25+
static defaultProps: DefaultProps = {
26+
maxVisibleItems: 5,
27+
fadeHeight: '40px',
28+
};
29+
30+
state: State = {
31+
collapsed: true,
32+
};
33+
34+
reveal = () => {
35+
this.setState({collapsed: false});
36+
};
37+
38+
collapse = () => {
39+
this.setState({collapsed: true});
40+
};
41+
42+
render() {
43+
const {children, maxVisibleItems, fadeHeight, className} = this.props;
44+
const {collapsed} = this.state;
45+
46+
return (
47+
<Wrapper className={className}>
48+
{children.map((item, index) => {
49+
if (!collapsed || index < maxVisibleItems) {
50+
return item;
51+
}
52+
53+
if (index === maxVisibleItems) {
54+
return (
55+
<ShowMoreWrapper fadeHeight={fadeHeight} key="show-more">
56+
<Button
57+
onClick={this.reveal}
58+
priority="primary"
59+
size="xsmall"
60+
data-test-id="show-more"
61+
>
62+
{tct('Show [numberOfFrames] More', {
63+
numberOfFrames: children.length - maxVisibleItems,
64+
})}
65+
</Button>
66+
</ShowMoreWrapper>
67+
);
68+
}
69+
return null;
70+
})}
71+
72+
{!collapsed && children.length > maxVisibleItems && (
73+
<CollapseWrapper>
74+
<Button
75+
onClick={this.collapse}
76+
priority="primary"
77+
size="xsmall"
78+
data-test-id="collapse"
79+
>
80+
{t('Collapse')}
81+
</Button>
82+
</CollapseWrapper>
83+
)}
84+
</Wrapper>
85+
);
86+
}
87+
}
88+
89+
const Wrapper = styled('div')`
90+
position: relative;
91+
`;
92+
93+
const ShowMoreWrapper = styled('div')<{fadeHeight: string}>`
94+
position: absolute;
95+
left: 0;
96+
right: 0;
97+
bottom: 0;
98+
background-image: linear-gradient(180deg, hsla(0, 0%, 100%, 0.15) 0, #fff);
99+
background-repeat: repeat-x;
100+
text-align: center;
101+
display: flex;
102+
align-items: center;
103+
justify-content: center;
104+
border-bottom: ${space(1)} solid #fff;
105+
border-top: ${space(1)} solid transparent;
106+
height: ${p => p.fadeHeight};
107+
`;
108+
109+
const CollapseWrapper = styled('div')`
110+
text-align: center;
111+
padding: ${space(0.25)} 0 ${space(1)} 0;
112+
`;
113+
114+
export default clippedHealthRows;

src/sentry/static/sentry/app/views/releasesV2/list/index.tsx

+45-17
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,18 @@ import withOrganization from 'app/utils/withOrganization';
1616
import withGlobalSelection from 'app/utils/withGlobalSelection';
1717
import LoadingIndicator from 'app/components/loadingIndicator';
1818
import LightWeightNoProjectMessage from 'app/components/lightWeightNoProjectMessage';
19-
import IntroBanner from 'app/views/releasesV2/list/introBanner';
2019
import {PageContent, PageHeader} from 'app/styles/organization';
2120
import EmptyStateWarning from 'app/components/emptyStateWarning';
22-
import ReleaseCard from 'app/views/releasesV2/list/releaseCard';
2321
import GlobalSelectionHeader from 'app/components/organizations/globalSelectionHeader';
2422
import {getRelativeSummary} from 'app/components/organizations/timeRangeSelector/utils';
2523
import {DEFAULT_STATS_PERIOD} from 'app/constants';
2624
import {defined} from 'app/utils';
2725

2826
import ReleaseListSortOptions from './releaseListSortOptions';
2927
import ReleasePromo from './releasePromo';
28+
import IntroBanner from './introBanner';
3029
import SwitchReleasesButton from '../utils/switchReleasesButton';
30+
import ReleaseCard from './releaseCard';
3131

3232
type RouteParams = {
3333
orgId: string;
@@ -38,7 +38,10 @@ type Props = RouteComponentProps<RouteParams, {}> & {
3838
selection: GlobalSelection;
3939
};
4040

41-
type State = {releases: Release[]} & AsyncView['state'];
41+
type State = {
42+
releases: Release[];
43+
loadingHealth: boolean;
44+
} & AsyncView['state'];
4245

4346
class ReleasesList extends AsyncView<Props, State> {
4447
shouldReload = true;
@@ -47,15 +50,10 @@ class ReleasesList extends AsyncView<Props, State> {
4750
return routeTitleGen(t('Releases'), this.props.organization.slug, false);
4851
}
4952

50-
getDefaultState() {
51-
return {
52-
...super.getDefaultState(),
53-
};
54-
}
55-
56-
getEndpoints(): [string, string, {}][] {
53+
getEndpoints() {
5754
const {organization, location} = this.props;
58-
const {statsPeriod, sort} = location.query;
55+
const {statsPeriod} = location.query;
56+
const sort = this.getSort();
5957

6058
const query = {
6159
...pick(location.query, [
@@ -68,15 +66,43 @@ class ReleasesList extends AsyncView<Props, State> {
6866
'healthStat',
6967
]),
7068
summaryStatsPeriod: statsPeriod,
71-
per_page: 50,
69+
per_page: 25,
7270
health: 1,
73-
flatten: !sort || sort === 'date' ? 0 : 1,
71+
flatten: sort === 'date' ? 0 : 1,
7472
};
7573

76-
return [['releases', `/organizations/${organization.slug}/releases/`, {query}]];
74+
const endpoints: ReturnType<AsyncView['getEndpoints']> = [
75+
['releasesWithHealth', `/organizations/${organization.slug}/releases/`, {query}],
76+
];
77+
78+
// when sorting by date we fetch releases without health and then fetch health lazily
79+
if (sort === 'date') {
80+
endpoints.push([
81+
'releasesWithoutHealth',
82+
`/organizations/${organization.slug}/releases/`,
83+
{query: {...query, health: 0}},
84+
]);
85+
}
86+
87+
return endpoints;
88+
}
89+
90+
onRequestSuccess({stateKey, data, jqXHR}) {
91+
const {remainingRequests} = this.state;
92+
93+
// make sure there's no withHealth/withoutHealth race condition and set proper loading state
94+
if (stateKey === 'releasesWithHealth' || remainingRequests === 1) {
95+
this.setState({
96+
reloading: false,
97+
loading: false,
98+
loadingHealth: stateKey === 'releasesWithoutHealth',
99+
releases: data,
100+
releasesPageLinks: jqXHR?.getResponseHeader('Link'),
101+
});
102+
}
77103
}
78104

79-
componentDidUpdate(prevProps, prevState) {
105+
componentDidUpdate(prevProps: Props, prevState: State) {
80106
super.componentDidUpdate(prevProps, prevState);
81107

82108
if (prevState.releases !== this.state.releases) {
@@ -176,7 +202,7 @@ class ReleasesList extends AsyncView<Props, State> {
176202

177203
renderInnerBody() {
178204
const {location, selection, organization} = this.props;
179-
const {releases, reloading} = this.state;
205+
const {releases, reloading, loadingHealth} = this.state;
180206

181207
if (this.shouldShowLoadingIndicator()) {
182208
return <LoadingIndicator />;
@@ -194,12 +220,14 @@ class ReleasesList extends AsyncView<Props, State> {
194220
selection={selection}
195221
reloading={reloading}
196222
key={`${release.version}-${release.projects[0].slug}`}
223+
showHealthPlaceholders={loadingHealth}
197224
/>
198225
));
199226
}
200227

201228
renderBody() {
202229
const {organization} = this.props;
230+
const {releasesPageLinks} = this.state;
203231

204232
return (
205233
<GlobalSelectionHeader
@@ -229,7 +257,7 @@ class ReleasesList extends AsyncView<Props, State> {
229257

230258
{this.renderInnerBody()}
231259

232-
<Pagination pageLinks={this.state.releasesPageLinks} />
260+
<Pagination pageLinks={releasesPageLinks} />
233261

234262
{!this.shouldShowLoadingIndicator() && (
235263
<SwitchReleasesButton version="1" orgId={organization.id} />

src/sentry/static/sentry/app/views/releasesV2/list/releaseCard.tsx

+10-1
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,17 @@ type Props = {
2828
location: Location;
2929
selection: GlobalSelection;
3030
reloading: boolean;
31+
showHealthPlaceholders: boolean;
3132
};
3233

33-
const ReleaseCard = ({release, orgSlug, location, selection, reloading}: Props) => {
34+
const ReleaseCard = ({
35+
release,
36+
orgSlug,
37+
location,
38+
reloading,
39+
selection,
40+
showHealthPlaceholders,
41+
}: Props) => {
3442
const {version, commitCount, lastDeploy, authors, dateCreated} = release;
3543

3644
return (
@@ -112,6 +120,7 @@ const ReleaseCard = ({release, orgSlug, location, selection, reloading}: Props)
112120
release={release}
113121
orgSlug={orgSlug}
114122
location={location}
123+
showPlaceholders={showHealthPlaceholders}
115124
selection={selection}
116125
/>
117126
</StyledPanel>

0 commit comments

Comments
 (0)