projectSourceMapsArtifacts.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. import {Fragment, useCallback} from 'react';
  2. import {RouteComponentProps} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import {Role} from 'sentry/components/acl/role';
  5. import {Button} from 'sentry/components/button';
  6. import FileSize from 'sentry/components/fileSize';
  7. import Link from 'sentry/components/links/link';
  8. import Pagination from 'sentry/components/pagination';
  9. import {PanelTable} from 'sentry/components/panels';
  10. import SearchBar from 'sentry/components/searchBar';
  11. import Tag from 'sentry/components/tag';
  12. import TimeSince from 'sentry/components/timeSince';
  13. import {Tooltip} from 'sentry/components/tooltip';
  14. import {IconClock, IconDownload} from 'sentry/icons';
  15. import {t, tct} from 'sentry/locale';
  16. import {space} from 'sentry/styles/space';
  17. import {Artifact, DebugIdBundleArtifact, Project} from 'sentry/types';
  18. import {useApiQuery} from 'sentry/utils/queryClient';
  19. import {decodeScalar} from 'sentry/utils/queryString';
  20. import useApi from 'sentry/utils/useApi';
  21. import useOrganization from 'sentry/utils/useOrganization';
  22. import {normalizeUrl} from 'sentry/utils/withDomainRequired';
  23. import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
  24. import {DebugIdBundlesTags} from 'sentry/views/settings/projectSourceMaps/debugIdBundlesTags';
  25. enum DebugIdBundleArtifactType {
  26. INVALID = 0,
  27. SOURCE = 1,
  28. MINIFIED_SOURCE = 2,
  29. SOURCE_MAP = 3,
  30. INDEXED_RAM_BUNDLE = 4,
  31. }
  32. const debugIdBundleTypeLabels = {
  33. [DebugIdBundleArtifactType.INVALID]: t('Invalid'),
  34. [DebugIdBundleArtifactType.SOURCE]: t('Source'),
  35. [DebugIdBundleArtifactType.MINIFIED_SOURCE]: t('Minified'),
  36. [DebugIdBundleArtifactType.SOURCE_MAP]: t('Source Map'),
  37. [DebugIdBundleArtifactType.INDEXED_RAM_BUNDLE]: t('Indexed RAM Bundle'),
  38. };
  39. function ArtifactsTableRow({
  40. name,
  41. downloadRole,
  42. downloadUrl,
  43. size,
  44. orgSlug,
  45. artifactColumnDetails,
  46. }: {
  47. artifactColumnDetails: React.ReactNode;
  48. downloadRole: string;
  49. downloadUrl: string;
  50. name: string;
  51. orgSlug: string;
  52. size: number;
  53. }) {
  54. return (
  55. <Fragment>
  56. <ArtifactColumn>
  57. <Name>{name || `(${t('empty')})`}</Name>
  58. {artifactColumnDetails}
  59. </ArtifactColumn>
  60. <SizeColumn>
  61. <FileSize bytes={size} />
  62. </SizeColumn>
  63. <ActionsColumn>
  64. <Role role={downloadRole}>
  65. {({hasRole}) => {
  66. return (
  67. <Tooltip
  68. title={tct(
  69. 'Artifacts can only be downloaded by users with organization [downloadRole] role[orHigher]. This can be changed in [settingsLink:Debug Files Access] settings.',
  70. {
  71. downloadRole,
  72. orHigher: downloadRole !== 'owner' ? ` ${t('or higher')}` : '',
  73. settingsLink: <Link to={`/settings/${orgSlug}/#debugFilesRole`} />,
  74. }
  75. )}
  76. disabled={hasRole}
  77. isHoverable
  78. >
  79. <Button
  80. size="sm"
  81. icon={<IconDownload size="sm" />}
  82. disabled={!hasRole}
  83. href={downloadUrl}
  84. title={hasRole ? t('Download Artifact') : undefined}
  85. aria-label={t('Download Artifact')}
  86. />
  87. </Tooltip>
  88. );
  89. }}
  90. </Role>
  91. </ActionsColumn>
  92. </Fragment>
  93. );
  94. }
  95. type Props = RouteComponentProps<
  96. {bundleId: string; orgId: string; projectId: string},
  97. {}
  98. > & {
  99. project: Project;
  100. };
  101. export function ProjectSourceMapsArtifacts({params, location, router, project}: Props) {
  102. const api = useApi();
  103. const organization = useOrganization();
  104. // query params
  105. const query = decodeScalar(location.query.query);
  106. const cursor = location.query.cursor ?? '';
  107. // endpoints
  108. const artifactsEndpoint = `/projects/${organization.slug}/${
  109. project.slug
  110. }/releases/${encodeURIComponent(params.bundleId)}/files/`;
  111. const debugIdBundlesArtifactsEndpoint = `/projects/${organization.slug}/${
  112. project.slug
  113. }/artifact-bundles/${encodeURIComponent(params.bundleId)}/files/`;
  114. // debug id bundles tab url
  115. const debugIdsUrl = normalizeUrl(
  116. `/settings/${organization.slug}/projects/${project.slug}/source-maps/artifact-bundles/${params.bundleId}/`
  117. );
  118. const tabDebugIdBundlesActive = location.pathname === debugIdsUrl;
  119. const {
  120. data: artifactsData,
  121. getResponseHeader: artifactsHeaders,
  122. isLoading: artifactsLoading,
  123. } = useApiQuery<Artifact[]>(
  124. [
  125. artifactsEndpoint,
  126. {
  127. query: {query, cursor},
  128. },
  129. ],
  130. {
  131. staleTime: 0,
  132. keepPreviousData: true,
  133. enabled: !tabDebugIdBundlesActive,
  134. }
  135. );
  136. const {
  137. data: debugIdBundlesArtifactsData,
  138. getResponseHeader: debugIdBundlesArtifactsHeaders,
  139. isLoading: debugIdBundlesArtifactsLoading,
  140. } = useApiQuery<DebugIdBundleArtifact>(
  141. [
  142. debugIdBundlesArtifactsEndpoint,
  143. {
  144. query: {query, cursor},
  145. },
  146. ],
  147. {
  148. staleTime: 0,
  149. keepPreviousData: true,
  150. enabled: tabDebugIdBundlesActive,
  151. }
  152. );
  153. const handleSearch = useCallback(
  154. (newQuery: string) => {
  155. router.push({
  156. ...location,
  157. query: {...location.query, cursor: undefined, query: newQuery},
  158. });
  159. },
  160. [router, location]
  161. );
  162. return (
  163. <Fragment>
  164. <SettingsPageHeader
  165. title={tabDebugIdBundlesActive ? t('Artifact Bundle') : t('Release Bundle')}
  166. subtitle={
  167. <VersionAndDetails>
  168. {params.bundleId}
  169. {tabDebugIdBundlesActive && (
  170. <DebugIdBundlesTags
  171. dist={debugIdBundlesArtifactsData?.dist}
  172. release={debugIdBundlesArtifactsData?.release}
  173. loading={debugIdBundlesArtifactsLoading}
  174. />
  175. )}
  176. </VersionAndDetails>
  177. }
  178. />
  179. <SearchBarWithMarginBottom
  180. placeholder={
  181. tabDebugIdBundlesActive ? t('Filter by Path or ID') : t('Filter by Path')
  182. }
  183. onSearch={handleSearch}
  184. query={query}
  185. />
  186. <StyledPanelTable
  187. headers={[
  188. t('Artifact'),
  189. <SizeColumn key="file-size">{t('File Size')}</SizeColumn>,
  190. '',
  191. ]}
  192. emptyMessage={
  193. query
  194. ? t('No artifacts match your search query.')
  195. : tabDebugIdBundlesActive
  196. ? t('There are no artifacts in this bundle.')
  197. : t('There are no artifacts in this archive.')
  198. }
  199. isEmpty={
  200. (tabDebugIdBundlesActive
  201. ? debugIdBundlesArtifactsData?.files ?? []
  202. : artifactsData ?? []
  203. ).length === 0
  204. }
  205. isLoading={
  206. tabDebugIdBundlesActive ? debugIdBundlesArtifactsLoading : artifactsLoading
  207. }
  208. >
  209. {tabDebugIdBundlesActive
  210. ? (debugIdBundlesArtifactsData?.files ?? []).map(data => {
  211. const downloadUrl = `${api.baseUrl}/projects/${organization.slug}/${
  212. project.slug
  213. }/artifact-bundles/${encodeURIComponent(params.bundleId)}/files/${
  214. data.id
  215. }/?download=1`;
  216. return (
  217. <ArtifactsTableRow
  218. key={data.id}
  219. size={data.fileSize}
  220. name={data.filePath}
  221. downloadRole={organization.debugFilesRole}
  222. downloadUrl={downloadUrl}
  223. orgSlug={organization.slug}
  224. artifactColumnDetails={
  225. <DebugIdAndFileTypeWrapper>
  226. <div>{data.debugId}</div>
  227. <div>{debugIdBundleTypeLabels[data.fileType]}</div>
  228. </DebugIdAndFileTypeWrapper>
  229. }
  230. />
  231. );
  232. })
  233. : artifactsData?.map(data => {
  234. const downloadUrl = `${api.baseUrl}/projects/${organization.slug}/${
  235. project.slug
  236. }/releases/${encodeURIComponent(params.bundleId)}/files/${
  237. data.id
  238. }/?download=1`;
  239. return (
  240. <ArtifactsTableRow
  241. key={data.id}
  242. size={data.size}
  243. name={data.name}
  244. downloadRole={organization.debugFilesRole}
  245. downloadUrl={downloadUrl}
  246. orgSlug={organization.slug}
  247. artifactColumnDetails={
  248. <TimeAndDistWrapper>
  249. <TimeWrapper>
  250. <IconClock size="sm" />
  251. <TimeSince date={data.dateCreated} />
  252. </TimeWrapper>
  253. <StyledTag
  254. type={data.dist ? 'info' : undefined}
  255. tooltipText={data.dist ? undefined : t('No distribution set')}
  256. >
  257. {data.dist ?? t('none')}
  258. </StyledTag>
  259. </TimeAndDistWrapper>
  260. }
  261. />
  262. );
  263. })}
  264. </StyledPanelTable>
  265. <Pagination
  266. pageLinks={
  267. tabDebugIdBundlesActive
  268. ? debugIdBundlesArtifactsHeaders?.('Link') ?? ''
  269. : artifactsHeaders?.('Link') ?? ''
  270. }
  271. />
  272. </Fragment>
  273. );
  274. }
  275. const StyledPanelTable = styled(PanelTable)`
  276. grid-template-columns: minmax(220px, 1fr) minmax(120px, max-content) minmax(
  277. 74px,
  278. max-content
  279. );
  280. `;
  281. const Column = styled('div')`
  282. display: flex;
  283. align-items: center;
  284. overflow: hidden;
  285. `;
  286. const ActionsColumn = styled(Column)`
  287. justify-content: flex-end;
  288. `;
  289. const SearchBarWithMarginBottom = styled(SearchBar)`
  290. margin-bottom: ${space(3)};
  291. `;
  292. const ArtifactColumn = styled('div')`
  293. overflow-wrap: break-word;
  294. word-break: break-all;
  295. line-height: 140%;
  296. `;
  297. const Name = styled('div')`
  298. display: flex;
  299. justify-content: flex-start;
  300. align-items: center;
  301. `;
  302. const SizeColumn = styled('div')`
  303. display: flex;
  304. justify-content: flex-end;
  305. text-align: right;
  306. align-items: center;
  307. color: ${p => p.theme.subText};
  308. `;
  309. const TimeAndDistWrapper = styled('div')`
  310. width: 100%;
  311. display: flex;
  312. margin-top: ${space(1)};
  313. align-items: center;
  314. `;
  315. const TimeWrapper = styled('div')`
  316. display: grid;
  317. gap: ${space(0.5)};
  318. grid-template-columns: min-content 1fr;
  319. font-size: ${p => p.theme.fontSizeMedium};
  320. align-items: center;
  321. color: ${p => p.theme.subText};
  322. `;
  323. const StyledTag = styled(Tag)`
  324. margin-left: ${space(1)};
  325. `;
  326. const DebugIdAndFileTypeWrapper = styled('div')`
  327. font-size: ${p => p.theme.fontSizeSmall};
  328. color: ${p => p.theme.subText};
  329. `;
  330. const VersionAndDetails = styled('div')`
  331. display: flex;
  332. flex-direction: column;
  333. gap: ${space(1)};
  334. word-break: break-word;
  335. `;