index.tsx 5.7 KB

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