index.tsx 5.6 KB

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