123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277 |
- 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<Props, State> {
- 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<DeprecatedAsyncView['getEndpoints']> {
- const {organization, params, location} = this.props;
- const {builtinSymbolSources} = this.state || {};
- const {query} = location.query;
- const endpoints: ReturnType<DeprecatedAsyncView['getEndpoints']> = [
- [
- '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 (
- <DebugFileRow
- debugFile={debugFile}
- showDetails={showDetails}
- downloadUrl={downloadUrl}
- downloadRole={organization.debugFilesRole}
- onDelete={this.handleDelete}
- key={debugFile.id}
- orgSlug={organization.slug}
- project={project}
- />
- );
- });
- }
- renderBody() {
- const {organization, project, router, location} = this.props;
- const {loading, showDetails, builtinSymbolSources, debugFiles, debugFilesPageLinks} =
- this.state;
- return (
- <Fragment>
- <SettingsPageHeader title={t('Debug Information Files')} />
- <TextBlock>
- {t(`
- Debug information files are used to convert addresses and minified
- function names from native crash reports into function names and
- locations.
- `)}
- </TextBlock>
- {organization.features.includes('symbol-sources') && (
- <Fragment>
- <PermissionAlert project={project} />
- <Sources
- api={this.api}
- location={location}
- router={router}
- project={project}
- organization={organization}
- customRepositories={
- (project.symbolSources
- ? JSON.parse(project.symbolSources)
- : []) as CustomRepo[]
- }
- builtinSymbolSources={project.builtinSymbolSources ?? []}
- builtinSymbolSourceOptions={builtinSymbolSources ?? []}
- isLoading={loading}
- />
- </Fragment>
- )}
- <Wrapper>
- <TextBlock noMargin>{t('Uploaded debug information files')}</TextBlock>
- <Filters>
- <Label>
- <Checkbox
- checked={showDetails}
- onChange={e => {
- this.setState({showDetails: (e.target as HTMLInputElement).checked});
- }}
- />
- {t('show details')}
- </Label>
- <SearchBar
- placeholder={t('Search DIFs')}
- onSearch={this.handleSearch}
- query={this.getQuery()}
- />
- </Filters>
- </Wrapper>
- <StyledPanelTable
- headers={[
- t('Debug ID'),
- t('Information'),
- <Actions key="actions">{t('Actions')}</Actions>,
- ]}
- emptyMessage={this.getEmptyMessage()}
- isEmpty={debugFiles?.length === 0}
- isLoading={loading}
- >
- {this.renderDebugFiles()}
- </StyledPanelTable>
- <Pagination pageLinks={debugFilesPageLinks} />
- </Fragment>
- );
- }
- }
- 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;
|