index.tsx 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. import {Fragment, useCallback, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
  4. import ExternalLink from 'sentry/components/links/externalLink';
  5. import Pagination from 'sentry/components/pagination';
  6. import {PanelTable} from 'sentry/components/panels/panelTable';
  7. import SearchBar from 'sentry/components/searchBar';
  8. import {t, tct} from 'sentry/locale';
  9. import type {DebugFile} from 'sentry/types/debugFiles';
  10. import type {RouteComponentProps} from 'sentry/types/legacyReactRouter';
  11. import type {Organization} from 'sentry/types/organization';
  12. import type {Project} from 'sentry/types/project';
  13. import type {DebugIdBundleAssociation} from 'sentry/types/sourceMaps';
  14. import type {ApiQueryKey} from 'sentry/utils/queryClient';
  15. import {useApiQuery, useQueries} from 'sentry/utils/queryClient';
  16. import useApi from 'sentry/utils/useApi';
  17. import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
  18. import TextBlock from 'sentry/views/settings/components/text/textBlock';
  19. import ProjectProguardRow from './projectProguardRow';
  20. export type ProjectProguardProps = RouteComponentProps<{projectId: string}, {}> & {
  21. organization: Organization;
  22. project: Project;
  23. };
  24. export type ProguardMappingAssociation = {
  25. releases: string[];
  26. };
  27. export type AssociatedMapping = {
  28. mapping?: DebugFile;
  29. releaseAssociation?: DebugIdBundleAssociation[];
  30. };
  31. function ProjectProguard({organization, location, router, params}: ProjectProguardProps) {
  32. const api = useApi();
  33. const {projectId} = params;
  34. const [loading, setLoading] = useState(false);
  35. const {
  36. data: mappings,
  37. isPending: dataLoading,
  38. getResponseHeader,
  39. refetch: fetchData,
  40. } = useApiQuery<DebugFile[]>(
  41. [
  42. `/projects/${organization.slug}/${projectId}/files/dsyms/`,
  43. {
  44. query: {
  45. query: location.query.query,
  46. file_formats: 'proguard',
  47. cursor: location.query.cursor,
  48. },
  49. },
  50. ],
  51. {
  52. staleTime: 0,
  53. }
  54. );
  55. const mappingsPageLinks = getResponseHeader?.('Link');
  56. const associationsResults = useQueries({
  57. queries:
  58. mappings?.map(mapping => {
  59. const queryKey = [
  60. `/projects/${organization.slug}/${projectId}/files/proguard-artifact-releases/`,
  61. {
  62. query: {
  63. proguard_uuid: mapping.uuid,
  64. },
  65. },
  66. ] as ApiQueryKey;
  67. return {
  68. queryKey,
  69. queryFn: () =>
  70. api.requestPromise(queryKey[0], {
  71. method: 'GET',
  72. query: queryKey[1]?.query,
  73. }) as Promise<ProguardMappingAssociation>,
  74. };
  75. }) ?? [],
  76. });
  77. const associationsFetched = associationsResults.every(result => result.isFetched);
  78. const handleSearch = useCallback(
  79. (query: string) => {
  80. router.push({
  81. ...location,
  82. query: {...location.query, cursor: undefined, query: query || undefined},
  83. });
  84. },
  85. [location, router]
  86. );
  87. const handleDelete = useCallback(
  88. async (id: string) => {
  89. setLoading(true);
  90. try {
  91. await api.requestPromise(
  92. `/projects/${
  93. organization.slug
  94. }/${projectId}/files/dsyms/?id=${encodeURIComponent(id)}`,
  95. {
  96. method: 'DELETE',
  97. }
  98. );
  99. setLoading(false);
  100. addSuccessMessage('Successfully deleted the mapping file');
  101. fetchData();
  102. } catch {
  103. setLoading(false);
  104. addErrorMessage('An error occurred while deleting the mapping file');
  105. }
  106. },
  107. [api, fetchData, organization.slug, projectId]
  108. );
  109. const query =
  110. typeof location.query.query === 'string' ? location.query.query : undefined;
  111. const isLoading = loading || dataLoading || !associationsFetched;
  112. return (
  113. <Fragment>
  114. <SettingsPageHeader
  115. title={t('ProGuard Mappings')}
  116. action={
  117. <SearchBar
  118. placeholder={t('Filter mappings')}
  119. onSearch={handleSearch}
  120. query={query}
  121. width="280px"
  122. />
  123. }
  124. />
  125. <TextBlock>
  126. {tct(
  127. `ProGuard mapping files are used to convert minified classes, methods and field names into a human readable format. To learn more about proguard mapping files, [link: read the docs].`,
  128. {
  129. link: (
  130. <ExternalLink href="https://docs.sentry.io/platforms/android/proguard/" />
  131. ),
  132. }
  133. )}
  134. </TextBlock>
  135. <StyledPanelTable
  136. headers={[t('Mapping'), <SizeColumn key="size">{t('File Size')}</SizeColumn>, '']}
  137. emptyMessage={
  138. query
  139. ? t('There are no mappings that match your search.')
  140. : t('There are no mappings for this project.')
  141. }
  142. isEmpty={mappings?.length === 0}
  143. isLoading={isLoading}
  144. >
  145. {!mappings?.length
  146. ? null
  147. : mappings.map((mapping, index) => {
  148. const downloadUrl = `${api.baseUrl}/projects/${
  149. organization.slug
  150. }/${projectId}/files/dsyms/?id=${encodeURIComponent(mapping.id)}`;
  151. return (
  152. <ProjectProguardRow
  153. mapping={mapping}
  154. associations={associationsResults[index].data}
  155. downloadUrl={downloadUrl}
  156. onDelete={handleDelete}
  157. key={mapping.id}
  158. orgSlug={organization.slug}
  159. />
  160. );
  161. })}
  162. </StyledPanelTable>
  163. <Pagination pageLinks={mappingsPageLinks} />
  164. </Fragment>
  165. );
  166. }
  167. export default ProjectProguard;
  168. const StyledPanelTable = styled(PanelTable)`
  169. grid-template-columns: minmax(220px, 1fr) max-content 120px;
  170. `;
  171. const SizeColumn = styled('div')`
  172. text-align: right;
  173. `;