index.tsx 9.0 KB

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