import {Fragment} from 'react'; import type {RouteComponentProps} from 'react-router'; import styled from '@emotion/styled'; import {addErrorMessage} from 'sentry/actionCreators/indicator'; import Checkbox from 'sentry/components/checkbox'; import Pagination from 'sentry/components/pagination'; import {PanelTable} from 'sentry/components/panels/panelTable'; import SearchBar from 'sentry/components/searchBar'; import {t} from 'sentry/locale'; import ProjectsStore from 'sentry/stores/projectsStore'; import {space} from 'sentry/styles/space'; import type {BuiltinSymbolSource, CustomRepo, DebugFile} from 'sentry/types/debugFiles'; import type {Organization} from 'sentry/types/organization'; import type {Project} from 'sentry/types/project'; import routeTitleGen from 'sentry/utils/routeTitle'; import DeprecatedAsyncView from 'sentry/views/deprecatedAsyncView'; import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader'; import TextBlock from 'sentry/views/settings/components/text/textBlock'; import PermissionAlert from 'sentry/views/settings/project/permissionAlert'; import DebugFileRow from './debugFileRow'; import Sources from './sources'; type Props = RouteComponentProps<{projectId: string}, {}> & { organization: Organization; project: Project; }; type State = DeprecatedAsyncView['state'] & { debugFiles: DebugFile[] | null; project: Project; showDetails: boolean; builtinSymbolSources?: BuiltinSymbolSource[] | null; }; class ProjectDebugSymbols extends DeprecatedAsyncView { getTitle() { const {projectId} = this.props.params; return routeTitleGen(t('Debug Files'), projectId, false); } getDefaultState() { return { ...super.getDefaultState(), project: this.props.project, showDetails: false, }; } getEndpoints(): ReturnType { const {organization, params, location} = this.props; const {builtinSymbolSources} = this.state || {}; const {query} = location.query; const endpoints: ReturnType = [ [ 'debugFiles', `/projects/${organization.slug}/${params.projectId}/files/dsyms/`, { query: {query}, }, ], ]; if (!builtinSymbolSources && organization.features.includes('symbol-sources')) { endpoints.push([ 'builtinSymbolSources', `/organizations/${organization.slug}/builtin-symbol-sources/`, {}, ]); } return endpoints; } handleDelete = (id: string) => { const {organization, params} = this.props; this.setState({ loading: true, }); this.api.request( `/projects/${organization.slug}/${params.projectId}/files/dsyms/?id=${id}`, { method: 'DELETE', complete: () => this.fetchData(), } ); }; handleSearch = (query: string) => { const {location, router} = this.props; router.push({ ...location, query: {...location.query, cursor: undefined, query}, }); }; async fetchProject() { const {organization, params} = this.props; try { const updatedProject = await this.api.requestPromise( `/projects/${organization.slug}/${params.projectId}/` ); ProjectsStore.onUpdateSuccess(updatedProject); } catch { addErrorMessage(t('An error occurred while fetching project data')); } } getQuery() { const {query} = this.props.location.query; return typeof query === 'string' ? query : undefined; } getEmptyMessage() { if (this.getQuery()) { return t('There are no debug symbols that match your search.'); } return t('There are no debug symbols for this project.'); } renderLoading() { return this.renderBody(); } renderDebugFiles() { const {debugFiles, showDetails} = this.state; const {organization, params, project} = this.props; if (!debugFiles?.length) { return null; } return debugFiles.map(debugFile => { const downloadUrl = `${this.api.baseUrl}/projects/${organization.slug}/${params.projectId}/files/dsyms/?id=${debugFile.id}`; return ( ); }); } renderBody() { const {organization, project, router, location} = this.props; const {loading, showDetails, builtinSymbolSources, debugFiles, debugFilesPageLinks} = this.state; return ( {t(` Debug information files are used to convert addresses and minified function names from native crash reports into function names and locations. `)} {organization.features.includes('symbol-sources') && ( )} {t('Uploaded debug information files')} {t('Actions')}, ]} emptyMessage={this.getEmptyMessage()} isEmpty={debugFiles?.length === 0} isLoading={loading} > {this.renderDebugFiles()} ); } } const StyledPanelTable = styled(PanelTable)` grid-template-columns: 37% 1fr auto; `; const Actions = styled('div')` text-align: right; `; const Wrapper = styled('div')` display: grid; grid-template-columns: auto 1fr; gap: ${space(4)}; align-items: center; margin-top: ${space(4)}; margin-bottom: ${space(1)}; @media (max-width: ${p => p.theme.breakpoints.small}) { display: block; } `; const Filters = styled('div')` display: grid; grid-template-columns: min-content minmax(200px, 400px); align-items: center; justify-content: flex-end; gap: ${space(2)}; @media (max-width: ${p => p.theme.breakpoints.small}) { grid-template-columns: min-content 1fr; } `; const Label = styled('label')` font-weight: normal; display: flex; align-items: center; margin-bottom: 0; white-space: nowrap; gap: ${space(1)}; `; export default ProjectDebugSymbols;