Skip to content

Commit 9725222

Browse files
authored
feat(preprod): Create minimal build list page (#97743)
<img width="1496" height="247" alt="Screenshot 2025-08-13 at 10 56 37 AM" src="https://github.com/user-attachments/assets/b554c6cb-4d6f-420f-bcbe-34e759b77e02" /> No sorting or filtering yet, figured we can add in the future
1 parent bfa4eea commit 9725222

File tree

3 files changed

+234
-2
lines changed

3 files changed

+234
-2
lines changed
Lines changed: 217 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,231 @@
1+
import React, {useState} from 'react';
2+
import {useTheme} from '@emotion/react';
3+
import styled from '@emotion/styled';
4+
import {PlatformIcon} from 'platformicons';
5+
6+
import {Button} from 'sentry/components/core/button';
7+
import {ButtonBar} from 'sentry/components/core/button/buttonBar';
8+
import InteractionStateLayer from 'sentry/components/core/interactionStateLayer';
9+
import {Flex} from 'sentry/components/core/layout';
10+
import {Link} from 'sentry/components/core/link';
111
import * as Layout from 'sentry/components/layouts/thirds';
212
import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
13+
import {SimpleTable} from 'sentry/components/tables/simpleTable';
14+
import TimeSince from 'sentry/components/timeSince';
15+
import {IconCheckmark, IconChevron, IconCommit} from 'sentry/icons';
16+
import {t} from 'sentry/locale';
17+
import {space} from 'sentry/styles/space';
18+
import {formatBytesBase10} from 'sentry/utils/bytes/formatBytesBase10';
19+
import {useApiQuery, type UseApiQueryResult} from 'sentry/utils/queryClient';
20+
import type RequestError from 'sentry/utils/requestError/requestError';
21+
import useOrganization from 'sentry/utils/useOrganization';
22+
import {useParams} from 'sentry/utils/useParams';
23+
import type {BuildDetailsApiResponse} from 'sentry/views/preprod/types/buildDetailsTypes';
24+
import type {ListBuildsApiResponse} from 'sentry/views/preprod/types/listBuildsTypes';
25+
import {getPlatformIconFromPlatform} from 'sentry/views/preprod/utils/labelUtils';
326

427
export default function BuildList() {
28+
const organization = useOrganization();
29+
const params = useParams<{projectId: string}>();
30+
const projectId = params.projectId;
31+
const [page, setPage] = useState(1);
32+
const theme = useTheme();
33+
34+
const buildsQuery: UseApiQueryResult<ListBuildsApiResponse, RequestError> =
35+
useApiQuery<ListBuildsApiResponse>(
36+
[
37+
`/projects/${organization.slug}/${projectId}/preprodartifacts/list-builds/`,
38+
{query: {page, per_page: 25}},
39+
],
40+
{
41+
staleTime: 0,
42+
enabled: !!projectId,
43+
}
44+
);
45+
46+
const {data: buildsData, isLoading, error} = buildsQuery;
47+
48+
const builds = buildsData?.builds || [];
49+
const pagination = buildsData?.pagination;
50+
let tableContent = null;
51+
if (isLoading) {
52+
tableContent = <SimpleTable.Empty>{t('Loading builds...')}</SimpleTable.Empty>;
53+
} else if (error) {
54+
tableContent = <SimpleTable.Empty>{t(`Error loading builds`)}</SimpleTable.Empty>;
55+
} else {
56+
if (builds.length === 0) {
57+
tableContent = <SimpleTable.Empty>{t('No builds found')}</SimpleTable.Empty>;
58+
} else {
59+
tableContent = (
60+
<React.Fragment>
61+
{builds.map((build: BuildDetailsApiResponse) => (
62+
<SimpleTable.Row key={build.id}>
63+
<Link
64+
to={`/organizations/${organization.slug}/preprod/${projectId}/${build.id}`}
65+
style={{
66+
display: 'contents',
67+
cursor: 'pointer',
68+
color: theme.textColor,
69+
}}
70+
>
71+
<InteractionStateLayer />
72+
<SimpleTable.RowCell justify="flex-start">
73+
<BuildInfo>
74+
<BuildName>
75+
<PlatformIcon
76+
platform={getPlatformIconFromPlatform(build.app_info.platform)}
77+
/>
78+
{build.app_info.name}
79+
</BuildName>
80+
<BuildDetails>{build.app_info.app_id}</BuildDetails>
81+
</BuildInfo>
82+
</SimpleTable.RowCell>
83+
84+
<SimpleTable.RowCell justify="flex-start">
85+
<BuildInfo>
86+
<BuildNumber>
87+
{build.app_info.version}
88+
<span>({build.app_info.build_number})</span>
89+
{build.state === 3 && <IconCheckmark size="sm" color="green300" />}
90+
</BuildNumber>
91+
<BuildDetails>
92+
<IconCommit size="xs" />
93+
<span>#{build.vcs_info.head_sha?.slice(0, 6) || 'N/A'}</span>
94+
<span>-</span>
95+
<span>{build.vcs_info.head_ref || 'main'}</span>
96+
</BuildDetails>
97+
</BuildInfo>
98+
</SimpleTable.RowCell>
99+
100+
<SimpleTable.RowCell>
101+
{build.size_info
102+
? formatBytesBase10(build.size_info.install_size_bytes)
103+
: '-'}
104+
</SimpleTable.RowCell>
105+
106+
<SimpleTable.RowCell>
107+
{build.size_info
108+
? formatBytesBase10(build.size_info.download_size_bytes)
109+
: '-'}
110+
</SimpleTable.RowCell>
111+
112+
<SimpleTable.RowCell>
113+
{build.app_info.date_added ? (
114+
<TimeSince date={build.app_info.date_added} unitStyle="short" />
115+
) : (
116+
'-'
117+
)}
118+
</SimpleTable.RowCell>
119+
</Link>
120+
</SimpleTable.Row>
121+
))}
122+
</React.Fragment>
123+
);
124+
}
125+
}
126+
5127
return (
6128
<SentryDocumentTitle title="Build list">
7129
<Layout.Page>
8-
<Layout.Header>Build list header</Layout.Header>
130+
<Layout.Header>
131+
<Layout.Title>Builds</Layout.Title>
132+
</Layout.Header>
9133

10134
<Layout.Body>
11-
<Layout.Main>Build list main content</Layout.Main>
135+
<Layout.Main fullWidth>
136+
<Flex direction="column" gap="md">
137+
<SimpleTableWithColumns>
138+
<SimpleTable.Header>
139+
<SimpleTable.HeaderCell>APP</SimpleTable.HeaderCell>
140+
<SimpleTable.HeaderCell>BUILD</SimpleTable.HeaderCell>
141+
<SimpleTable.HeaderCell>INSTALL SIZE</SimpleTable.HeaderCell>
142+
<SimpleTable.HeaderCell>DOWNLOAD SIZE</SimpleTable.HeaderCell>
143+
<SimpleTable.HeaderCell>CREATED</SimpleTable.HeaderCell>
144+
</SimpleTable.Header>
145+
146+
{tableContent}
147+
</SimpleTableWithColumns>
148+
149+
{pagination && (
150+
<Flex
151+
direction="row"
152+
gap="md"
153+
align="center"
154+
justify="end"
155+
data-test-id="pagination"
156+
>
157+
<PaginationCaption>
158+
Page {pagination.page + 1} of{' '}
159+
{Math.ceil(
160+
(typeof pagination.total_count === 'number'
161+
? pagination.total_count
162+
: 0) / pagination.per_page
163+
)}
164+
</PaginationCaption>
165+
<ButtonBar merged gap="0">
166+
<Button
167+
icon={<IconChevron direction="left" />}
168+
aria-label={t('Previous')}
169+
size="sm"
170+
disabled={!pagination.has_prev}
171+
onClick={() => setPage(pagination.prev || 1)}
172+
/>
173+
<Button
174+
icon={<IconChevron direction="right" />}
175+
aria-label={t('Next')}
176+
size="sm"
177+
disabled={!pagination.has_next}
178+
onClick={() => {
179+
setPage(pagination.next || pagination.page + 1);
180+
}}
181+
/>
182+
</ButtonBar>
183+
</Flex>
184+
)}
185+
</Flex>
186+
</Layout.Main>
12187
</Layout.Body>
13188
</Layout.Page>
14189
</SentryDocumentTitle>
15190
);
16191
}
192+
193+
const SimpleTableWithColumns = styled(SimpleTable)`
194+
grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
195+
`;
196+
197+
const BuildName = styled('div')`
198+
display: flex;
199+
align-items: center;
200+
gap: ${p => p.theme.space.sm};
201+
font-weight: ${p => p.theme.fontWeight.bold};
202+
font-size: ${p => p.theme.fontSize.lg};
203+
`;
204+
205+
const BuildInfo = styled('div')`
206+
display: flex;
207+
flex-direction: column;
208+
gap: ${p => p.theme.space.xs};
209+
`;
210+
211+
const BuildNumber = styled('div')`
212+
display: flex;
213+
align-items: center;
214+
gap: ${p => p.theme.space.xs};
215+
font-weight: ${p => p.theme.fontWeight.bold};
216+
font-size: ${p => p.theme.fontSize.lg};
217+
`;
218+
219+
const BuildDetails = styled('div')`
220+
display: flex;
221+
align-items: center;
222+
gap: ${p => p.theme.space.xs};
223+
font-size: ${p => p.theme.fontSize.sm};
224+
color: ${p => p.theme.subText};
225+
`;
226+
227+
const PaginationCaption = styled('span')`
228+
color: ${p => p.theme.subText};
229+
font-size: ${p => p.theme.fontSize.md};
230+
margin-right: ${space(2)};
231+
`;

static/app/views/preprod/types/buildDetailsTypes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type {Platform} from './sharedTypes';
22

33
export interface BuildDetailsApiResponse {
44
app_info: BuildDetailsAppInfo;
5+
id: string;
56
state: BuildDetailsState;
67
vcs_info: BuildDetailsVcsInfo;
78
size_info?: BuildDetailsSizeInfo;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type {BuildDetailsApiResponse} from './buildDetailsTypes';
2+
3+
interface PaginationInfo {
4+
has_next: boolean;
5+
has_prev: boolean;
6+
next: number | null;
7+
page: number;
8+
per_page: number;
9+
prev: number | null;
10+
total_count: number | string;
11+
}
12+
13+
export interface ListBuildsApiResponse {
14+
builds: BuildDetailsApiResponse[];
15+
pagination: PaginationInfo;
16+
}

0 commit comments

Comments
 (0)