index.tsx 5.5 KB

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