index.tsx 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. import {Fragment, useCallback, useState} from 'react';
  2. import type {RouteComponentProps} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import {
  5. addErrorMessage,
  6. addLoadingMessage,
  7. addSuccessMessage,
  8. } from 'sentry/actionCreators/indicator';
  9. import Checkbox from 'sentry/components/checkbox';
  10. import LoadingError from 'sentry/components/loadingError';
  11. import LoadingIndicator from 'sentry/components/loadingIndicator';
  12. import Pagination from 'sentry/components/pagination';
  13. import {PanelTable} from 'sentry/components/panels/panelTable';
  14. import SearchBar from 'sentry/components/searchBar';
  15. import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
  16. import {t} from 'sentry/locale';
  17. import {space} from 'sentry/styles/space';
  18. import type {BuiltinSymbolSource, CustomRepo, DebugFile} from 'sentry/types/debugFiles';
  19. import type {Organization} from 'sentry/types/organization';
  20. import type {Project} from 'sentry/types/project';
  21. import {
  22. type ApiQueryKey,
  23. useApiQuery,
  24. useMutation,
  25. useQueryClient,
  26. } from 'sentry/utils/queryClient';
  27. import {decodeScalar} from 'sentry/utils/queryString';
  28. import type RequestError from 'sentry/utils/requestError/requestError';
  29. import routeTitleGen from 'sentry/utils/routeTitle';
  30. import useApi from 'sentry/utils/useApi';
  31. import {useNavigate} from 'sentry/utils/useNavigate';
  32. import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
  33. import TextBlock from 'sentry/views/settings/components/text/textBlock';
  34. import PermissionAlert from 'sentry/views/settings/project/permissionAlert';
  35. import DebugFileRow from './debugFileRow';
  36. import Sources from './sources';
  37. type Props = RouteComponentProps<{projectId: string}, {}> & {
  38. organization: Organization;
  39. project: Project;
  40. };
  41. function makeDebugFilesQueryKey({
  42. orgSlug,
  43. projectSlug,
  44. query,
  45. }: {
  46. orgSlug: string;
  47. projectSlug: string;
  48. query: string;
  49. }): ApiQueryKey {
  50. return [
  51. `/projects/${orgSlug}/${projectSlug}/files/dsyms/`,
  52. {
  53. query: {query},
  54. },
  55. ];
  56. }
  57. function makeSymbolSourcesQueryKey({orgSlug}: {orgSlug: string}): ApiQueryKey {
  58. return [`/organizations/${orgSlug}/builtin-symbol-sources/`];
  59. }
  60. function ProjectDebugSymbols({organization, project, location, router, params}: Props) {
  61. const navigate = useNavigate();
  62. const api = useApi();
  63. const queryClient = useQueryClient();
  64. const [showDetails, setShowDetails] = useState(false);
  65. const query = decodeScalar(location.query.query, '');
  66. const hasSymbolSourcesFeatureFlag = organization.features.includes('symbol-sources');
  67. const {
  68. data: debugFiles,
  69. getResponseHeader: getDebugFilesResponseHeader,
  70. isLoading: isLoadingDebugFiles,
  71. isLoadingError: isLoadingErrorDebugFiles,
  72. refetch: refetchDebugFiles,
  73. } = useApiQuery<DebugFile[] | null>(
  74. makeDebugFilesQueryKey({
  75. projectSlug: params.projectId,
  76. orgSlug: organization.slug,
  77. query,
  78. }),
  79. {
  80. staleTime: 0,
  81. retry: false,
  82. }
  83. );
  84. const {
  85. data: builtinSymbolSources,
  86. isLoading: isLoadingSymbolSources,
  87. isError: isErrorSymbolSources,
  88. refetch: refetchSymbolSources,
  89. } = useApiQuery<BuiltinSymbolSource[] | null>(
  90. makeSymbolSourcesQueryKey({orgSlug: organization.slug}),
  91. {
  92. staleTime: 0,
  93. enabled: hasSymbolSourcesFeatureFlag,
  94. retry: 0,
  95. }
  96. );
  97. const handleSearch = useCallback(
  98. (value: string) => {
  99. navigate({
  100. ...location,
  101. query: {...location.query, cursor: undefined, query: !value ? undefined : value},
  102. });
  103. },
  104. [navigate, location]
  105. );
  106. const {mutate: handleDeleteDebugFile} = useMutation<unknown, RequestError, string>({
  107. mutationFn: (id: string) => {
  108. return api.requestPromise(
  109. `/projects/${organization.slug}/${params.projectId}/files/dsyms/?id=${id}`,
  110. {
  111. method: 'DELETE',
  112. }
  113. );
  114. },
  115. onMutate: () => {
  116. addLoadingMessage('Deleting debug file');
  117. },
  118. onSuccess: () => {
  119. addSuccessMessage('Successfully deleted debug file');
  120. // invalidate debug files query
  121. queryClient.invalidateQueries(
  122. makeDebugFilesQueryKey({
  123. projectSlug: params.projectId,
  124. orgSlug: organization.slug,
  125. query,
  126. })
  127. );
  128. // invalidate symbol sources query
  129. queryClient.invalidateQueries(
  130. makeSymbolSourcesQueryKey({
  131. orgSlug: organization.slug,
  132. })
  133. );
  134. },
  135. onError: () => {
  136. addErrorMessage('Failed to delete debug file');
  137. },
  138. });
  139. return (
  140. <SentryDocumentTitle title={routeTitleGen(t('Debug Files'), params.projectId, false)}>
  141. <SettingsPageHeader title={t('Debug Information Files')} />
  142. <TextBlock>
  143. {t(`
  144. Debug information files are used to convert addresses and minified
  145. function names from native crash reports into function names and
  146. locations.
  147. `)}
  148. </TextBlock>
  149. {organization.features.includes('symbol-sources') && (
  150. <Fragment>
  151. <PermissionAlert project={project} />
  152. {isLoadingSymbolSources ? (
  153. <LoadingIndicator />
  154. ) : isErrorSymbolSources ? (
  155. <LoadingError
  156. onRetry={refetchSymbolSources}
  157. message={t('There was an error loading repositories.')}
  158. />
  159. ) : (
  160. <Sources
  161. api={api}
  162. location={location}
  163. router={router}
  164. project={project}
  165. organization={organization}
  166. customRepositories={
  167. (project.symbolSources
  168. ? JSON.parse(project.symbolSources)
  169. : []) as CustomRepo[]
  170. }
  171. builtinSymbolSources={project.builtinSymbolSources ?? []}
  172. builtinSymbolSourceOptions={builtinSymbolSources ?? []}
  173. />
  174. )}
  175. </Fragment>
  176. )}
  177. {isLoadingDebugFiles ? (
  178. <LoadingIndicator />
  179. ) : isLoadingErrorDebugFiles ? (
  180. <LoadingError
  181. onRetry={refetchDebugFiles}
  182. message={t('There was an error loading debug information files.')}
  183. />
  184. ) : (
  185. <Fragment>
  186. <Wrapper>
  187. <TextBlock noMargin>{t('Uploaded debug information files')}</TextBlock>
  188. <Filters>
  189. <Label>
  190. <Checkbox
  191. checked={showDetails}
  192. onChange={e => {
  193. setShowDetails((e.target as HTMLInputElement).checked);
  194. }}
  195. />
  196. {t('show details')}
  197. </Label>
  198. <SearchBar
  199. placeholder={t('Search DIFs')}
  200. onSearch={handleSearch}
  201. query={query}
  202. />
  203. </Filters>
  204. </Wrapper>
  205. <StyledPanelTable
  206. headers={[
  207. t('Debug ID'),
  208. t('Information'),
  209. <Actions key="actions">{t('Actions')}</Actions>,
  210. ]}
  211. emptyMessage={
  212. query
  213. ? t('There are no debug symbols that match your search.')
  214. : t('There are no debug symbols for this project.')
  215. }
  216. isEmpty={debugFiles?.length === 0}
  217. isLoading={isLoadingDebugFiles}
  218. >
  219. {!debugFiles?.length
  220. ? null
  221. : debugFiles.map(debugFile => {
  222. const downloadUrl = `${api.baseUrl}/projects/${organization.slug}/${params.projectId}/files/dsyms/?id=${debugFile.id}`;
  223. return (
  224. <DebugFileRow
  225. debugFile={debugFile}
  226. showDetails={showDetails}
  227. downloadUrl={downloadUrl}
  228. downloadRole={organization.debugFilesRole}
  229. onDelete={handleDeleteDebugFile}
  230. key={debugFile.id}
  231. orgSlug={organization.slug}
  232. project={project}
  233. />
  234. );
  235. })}
  236. </StyledPanelTable>
  237. <Pagination pageLinks={getDebugFilesResponseHeader?.('Link')} />
  238. </Fragment>
  239. )}
  240. </SentryDocumentTitle>
  241. );
  242. }
  243. const StyledPanelTable = styled(PanelTable)`
  244. grid-template-columns: 37% 1fr auto;
  245. `;
  246. const Actions = styled('div')`
  247. text-align: right;
  248. `;
  249. const Wrapper = styled('div')`
  250. display: grid;
  251. grid-template-columns: auto 1fr;
  252. gap: ${space(4)};
  253. align-items: center;
  254. margin-top: ${space(4)};
  255. margin-bottom: ${space(1)};
  256. @media (max-width: ${p => p.theme.breakpoints.small}) {
  257. display: block;
  258. }
  259. `;
  260. const Filters = styled('div')`
  261. display: grid;
  262. grid-template-columns: min-content minmax(200px, 400px);
  263. align-items: center;
  264. justify-content: flex-end;
  265. gap: ${space(2)};
  266. @media (max-width: ${p => p.theme.breakpoints.small}) {
  267. grid-template-columns: min-content 1fr;
  268. }
  269. `;
  270. const Label = styled('label')`
  271. font-weight: ${p => p.theme.fontWeightNormal};
  272. display: flex;
  273. align-items: center;
  274. margin-bottom: 0;
  275. white-space: nowrap;
  276. gap: ${space(1)};
  277. `;
  278. export default ProjectDebugSymbols;