123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425 |
- import {Fragment, useCallback} from 'react';
- import type {RouteComponentProps} from 'react-router';
- import styled from '@emotion/styled';
- import {Role} from 'sentry/components/acl/role';
- import Tag from 'sentry/components/badge/tag';
- import {LinkButton} from 'sentry/components/button';
- import FileSize from 'sentry/components/fileSize';
- import Link from 'sentry/components/links/link';
- import Pagination from 'sentry/components/pagination';
- import Panel from 'sentry/components/panels/panel';
- import {PanelTable} from 'sentry/components/panels/panelTable';
- import SearchBar from 'sentry/components/searchBar';
- import TimeSince from 'sentry/components/timeSince';
- import {Tooltip} from 'sentry/components/tooltip';
- import {IconClock, IconDownload} from 'sentry/icons';
- import {t, tct} from 'sentry/locale';
- import {space} from 'sentry/styles/space';
- import type {Project} from 'sentry/types/project';
- import type {Artifact} from 'sentry/types/release';
- import type {DebugIdBundleArtifact} from 'sentry/types/sourceMaps';
- import {useApiQuery} from 'sentry/utils/queryClient';
- import {decodeScalar} from 'sentry/utils/queryString';
- import normalizeUrl from 'sentry/utils/url/normalizeUrl';
- import useApi from 'sentry/utils/useApi';
- import useOrganization from 'sentry/utils/useOrganization';
- import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
- import {DebugIdBundleDeleteButton} from 'sentry/views/settings/projectSourceMaps/debugIdBundleDeleteButton';
- import {DebugIdBundleDetails} from 'sentry/views/settings/projectSourceMaps/debugIdBundleDetails';
- import {useDeleteDebugIdBundle} from 'sentry/views/settings/projectSourceMaps/useDeleteDebugIdBundle';
- enum DebugIdBundleArtifactType {
- INVALID = 0,
- SOURCE = 1,
- MINIFIED_SOURCE = 2,
- SOURCE_MAP = 3,
- INDEXED_RAM_BUNDLE = 4,
- }
- const debugIdBundleTypeLabels = {
- [DebugIdBundleArtifactType.INVALID]: t('Invalid'),
- [DebugIdBundleArtifactType.SOURCE]: t('Source'),
- [DebugIdBundleArtifactType.MINIFIED_SOURCE]: t('Minified'),
- [DebugIdBundleArtifactType.SOURCE_MAP]: t('Source Map'),
- [DebugIdBundleArtifactType.INDEXED_RAM_BUNDLE]: t('Indexed RAM Bundle'),
- };
- function ArtifactsTableRow({
- name,
- downloadRole,
- downloadUrl,
- size,
- type,
- orgSlug,
- artifactColumnDetails,
- }: {
- artifactColumnDetails: React.ReactNode;
- downloadRole: string;
- downloadUrl: string;
- name: string;
- orgSlug: string;
- size: number;
- type?: string;
- }) {
- return (
- <Fragment>
- <ArtifactColumn>
- <Name>{name || `(${t('empty')})`}</Name>
- {artifactColumnDetails}
- </ArtifactColumn>
- {type && <TypeColumn>{type}</TypeColumn>}
- <SizeColumn>
- <FileSize bytes={size} />
- </SizeColumn>
- <ActionsColumn>
- <Role role={downloadRole}>
- {({hasRole}) => {
- return (
- <Tooltip
- title={tct(
- 'Artifacts can only be downloaded by users with organization [downloadRole] role[orHigher]. This can be changed in [settingsLink:Debug Files Access] settings.',
- {
- downloadRole,
- orHigher: downloadRole !== 'owner' ? ` ${t('or higher')}` : '',
- settingsLink: <Link to={`/settings/${orgSlug}/#debugFilesRole`} />,
- }
- )}
- disabled={hasRole}
- isHoverable
- >
- <LinkButton
- size="sm"
- icon={<IconDownload size="sm" />}
- disabled={!hasRole}
- href={downloadUrl}
- title={hasRole ? t('Download Artifact') : undefined}
- aria-label={t('Download Artifact')}
- />
- </Tooltip>
- );
- }}
- </Role>
- </ActionsColumn>
- </Fragment>
- );
- }
- type Props = RouteComponentProps<
- {bundleId: string; orgId: string; projectId: string},
- {}
- > & {
- project: Project;
- };
- export function ProjectSourceMapsArtifacts({params, location, router, project}: Props) {
- const api = useApi();
- const organization = useOrganization();
- // query params
- const query = decodeScalar(location.query.query);
- const cursor = location.query.cursor ?? '';
- // endpoints
- const artifactsEndpoint = `/projects/${organization.slug}/${
- project.slug
- }/releases/${encodeURIComponent(params.bundleId)}/files/`;
- const debugIdBundlesArtifactsEndpoint = `/projects/${organization.slug}/${
- project.slug
- }/artifact-bundles/${encodeURIComponent(params.bundleId)}/files/`;
- // debug id bundles tab url
- const debugIdsUrl = normalizeUrl(
- `/settings/${organization.slug}/projects/${project.slug}/source-maps/artifact-bundles/${params.bundleId}/`
- );
- const tabDebugIdBundlesActive = location.pathname === debugIdsUrl;
- const {
- data: artifactsData,
- getResponseHeader: artifactsHeaders,
- isPending: artifactsLoading,
- } = useApiQuery<Artifact[]>(
- [
- artifactsEndpoint,
- {
- query: {query, cursor},
- },
- ],
- {
- staleTime: 0,
- keepPreviousData: true,
- enabled: !tabDebugIdBundlesActive,
- }
- );
- const {
- data: debugIdBundlesArtifactsData,
- getResponseHeader: debugIdBundlesArtifactsHeaders,
- isPending: debugIdBundlesArtifactsLoading,
- } = useApiQuery<DebugIdBundleArtifact>(
- [
- debugIdBundlesArtifactsEndpoint,
- {
- query: {query, cursor},
- },
- ],
- {
- staleTime: 0,
- keepPreviousData: true,
- enabled: tabDebugIdBundlesActive,
- }
- );
- const {mutate: deleteDebugIdArtifacts} = useDeleteDebugIdBundle({
- onSuccess: () =>
- router.push(
- `/settings/${organization.slug}/projects/${project.slug}/source-maps/artifact-bundles/`
- ),
- });
- const handleDeleteDebugIdBundle = useCallback(() => {
- if (!debugIdBundlesArtifactsData) {
- return;
- }
- deleteDebugIdArtifacts({
- projectSlug: project.slug,
- bundleId: debugIdBundlesArtifactsData.bundleId,
- });
- }, [debugIdBundlesArtifactsData, deleteDebugIdArtifacts, project.slug]);
- const handleSearch = useCallback(
- (newQuery: string) => {
- router.push({
- ...location,
- query: {...location.query, cursor: undefined, query: newQuery},
- });
- },
- [router, location]
- );
- return (
- <Fragment>
- <SettingsPageHeader
- title={tabDebugIdBundlesActive ? params.bundleId : t('Release Bundle')}
- action={
- tabDebugIdBundlesActive && (
- <DebugIdBundleDeleteButton size="sm" onDelete={handleDeleteDebugIdBundle} />
- )
- }
- subtitle={
- !tabDebugIdBundlesActive && (
- <VersionAndDetails>{params.bundleId}</VersionAndDetails>
- )
- }
- />
- {tabDebugIdBundlesActive && debugIdBundlesArtifactsData && (
- <DetailsPanel>
- <DebugIdBundleDetails debugIdBundle={debugIdBundlesArtifactsData} />
- </DetailsPanel>
- )}
- <SearchBarWithMarginBottom
- placeholder={
- tabDebugIdBundlesActive ? t('Filter by Path or ID') : t('Filter by Path')
- }
- onSearch={handleSearch}
- query={query}
- />
- <StyledPanelTable
- hasTypeColumn={tabDebugIdBundlesActive}
- headers={[
- t('Artifact'),
- ...(tabDebugIdBundlesActive
- ? [<TypeColumn key="type">{t('Type')}</TypeColumn>]
- : []),
- <SizeColumn key="file-size">{t('File Size')}</SizeColumn>,
- '',
- ]}
- emptyMessage={
- query
- ? t('No artifacts match your search query.')
- : tabDebugIdBundlesActive
- ? t('There are no artifacts in this bundle.')
- : t('There are no artifacts in this archive.')
- }
- isEmpty={
- (tabDebugIdBundlesActive
- ? debugIdBundlesArtifactsData?.files ?? []
- : artifactsData ?? []
- ).length === 0
- }
- isLoading={
- tabDebugIdBundlesActive ? debugIdBundlesArtifactsLoading : artifactsLoading
- }
- >
- {tabDebugIdBundlesActive
- ? (debugIdBundlesArtifactsData?.files ?? []).map(data => {
- const downloadUrl = `${api.baseUrl}/projects/${organization.slug}/${
- project.slug
- }/artifact-bundles/${encodeURIComponent(params.bundleId)}/files/${
- data.id
- }/?download=1`;
- return (
- <ArtifactsTableRow
- key={data.id}
- size={data.fileSize}
- name={data.filePath}
- type={debugIdBundleTypeLabels[data.fileType]}
- downloadRole={organization.debugFilesRole}
- downloadUrl={downloadUrl}
- orgSlug={organization.slug}
- artifactColumnDetails={
- <Fragment>
- {data.sourcemap ? (
- <div>
- <SubText>{t('Sourcemap Reference:')}</SubText> {data.sourcemap}
- </div>
- ) : null}
- {data.debugId ? (
- <div>
- <SubText>{t('Debug ID:')}</SubText> {data.debugId}
- </div>
- ) : null}
- </Fragment>
- }
- />
- );
- })
- : artifactsData?.map(data => {
- const downloadUrl = `${api.baseUrl}/projects/${organization.slug}/${
- project.slug
- }/releases/${encodeURIComponent(params.bundleId)}/files/${
- data.id
- }/?download=1`;
- return (
- <ArtifactsTableRow
- key={data.id}
- size={data.size}
- name={data.name}
- downloadRole={organization.debugFilesRole}
- downloadUrl={downloadUrl}
- orgSlug={organization.slug}
- artifactColumnDetails={
- <TimeAndDistWrapper>
- <TimeWrapper>
- <IconClock size="sm" />
- <TimeSince date={data.dateCreated} />
- </TimeWrapper>
- <StyledTag
- type={data.dist ? 'info' : undefined}
- tooltipText={data.dist ? undefined : t('No distribution set')}
- >
- {data.dist ?? t('none')}
- </StyledTag>
- </TimeAndDistWrapper>
- }
- />
- );
- })}
- </StyledPanelTable>
- <Pagination
- pageLinks={
- tabDebugIdBundlesActive
- ? debugIdBundlesArtifactsHeaders?.('Link') ?? ''
- : artifactsHeaders?.('Link') ?? ''
- }
- />
- </Fragment>
- );
- }
- const StyledPanelTable = styled(PanelTable)<{hasTypeColumn: boolean}>`
- grid-template-columns: minmax(220px, 1fr) minmax(120px, max-content) minmax(
- 74px,
- max-content
- );
- ${p =>
- p.hasTypeColumn &&
- `
- grid-template-columns:
- minmax(220px, 1fr) minmax(120px, max-content) minmax(120px, max-content)
- minmax(74px, max-content);
- `}
- `;
- const Column = styled('div')`
- display: flex;
- align-items: center;
- overflow: hidden;
- `;
- const ActionsColumn = styled(Column)`
- justify-content: flex-end;
- `;
- const SearchBarWithMarginBottom = styled(SearchBar)`
- margin-bottom: ${space(3)};
- `;
- const DetailsPanel = styled(Panel)`
- padding: ${space(1)} ${space(2)};
- `;
- const ArtifactColumn = styled('div')`
- overflow-wrap: break-word;
- word-break: break-all;
- line-height: 140%;
- display: flex;
- flex-direction: column;
- justify-content: center;
- `;
- const Name = styled('div')`
- display: flex;
- justify-content: flex-start;
- align-items: center;
- `;
- const TypeColumn = styled('div')`
- display: flex;
- justify-content: flex-end;
- text-align: right;
- align-items: center;
- color: ${p => p.theme.subText};
- `;
- const SizeColumn = styled('div')`
- display: flex;
- justify-content: flex-end;
- text-align: right;
- align-items: center;
- color: ${p => p.theme.subText};
- `;
- const TimeAndDistWrapper = styled('div')`
- width: 100%;
- display: flex;
- margin-top: ${space(1)};
- align-items: center;
- `;
- const TimeWrapper = styled('div')`
- display: grid;
- gap: ${space(0.5)};
- grid-template-columns: min-content 1fr;
- font-size: ${p => p.theme.fontSizeMedium};
- align-items: center;
- color: ${p => p.theme.subText};
- `;
- const StyledTag = styled(Tag)`
- margin-left: ${space(1)};
- `;
- const SubText = styled('span')`
- color: ${p => p.theme.subText};
- `;
- const VersionAndDetails = styled('div')`
- display: flex;
- flex-direction: column;
- gap: ${space(1)};
- word-break: break-word;
- `;
|