Browse Source

ref(source-maps): Add more UI for debug Ids - (#45844)

Priscila Oliveira 2 years ago
parent
commit
26ab25300f

+ 46 - 11
static/app/routes.tsx

@@ -513,17 +513,52 @@ function buildRoutes() {
             };
           })}
         />
-        <Route
-          path="debug-id-bundles/"
-          component={make(async () => {
-            const {ProjectSourceMapsContainer} = await import(
-              'sentry/views/settings/projectSourceMaps'
-            );
-            return {
-              default: ProjectSourceMapsContainer,
-            };
-          })}
-        />
+        <Route path="debug-id-bundles/">
+          <IndexRoute
+            component={make(async () => {
+              const {ProjectSourceMapsContainer} = await import(
+                'sentry/views/settings/projectSourceMaps'
+              );
+              return {
+                default: ProjectSourceMapsContainer,
+              };
+            })}
+          />
+          <Route
+            path=":bundleId/"
+            component={make(async () => {
+              const {ProjectSourceMapsContainer} = await import(
+                'sentry/views/settings/projectSourceMaps'
+              );
+              return {
+                default: ProjectSourceMapsContainer,
+              };
+            })}
+          />
+        </Route>
+        <Route path="release-bundles/">
+          <IndexRoute
+            component={make(async () => {
+              const {ProjectSourceMapsContainer} = await import(
+                'sentry/views/settings/projectSourceMaps'
+              );
+              return {
+                default: ProjectSourceMapsContainer,
+              };
+            })}
+          />
+          <Route
+            path=":bundleId/"
+            component={make(async () => {
+              const {ProjectSourceMapsContainer} = await import(
+                'sentry/views/settings/projectSourceMaps'
+              );
+              return {
+                default: ProjectSourceMapsContainer,
+              };
+            })}
+          />
+        </Route>
         <Route
           path=":name/"
           name={t('Archive')}

+ 84 - 7
static/app/views/settings/projectSourceMaps/index.tsx

@@ -1,23 +1,100 @@
+import {Fragment} from 'react';
 import {RouteComponentProps} from 'react-router';
 
+import ExternalLink from 'sentry/components/links/externalLink';
+import ListLink from 'sentry/components/links/listLink';
+import NavTabs from 'sentry/components/navTabs';
+import {t, tct} from 'sentry/locale';
 import {Project} from 'sentry/types';
 import useOrganization from 'sentry/utils/useOrganization';
+import {normalizeUrl} from 'sentry/utils/withDomainRequired';
+import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
+import TextBlock from 'sentry/views/settings/components/text/textBlock';
+import ProjectSourceMapsDetail from 'sentry/views/settings/projectSourceMaps/detail';
 import ProjectSourceMapsList from 'sentry/views/settings/projectSourceMaps/list';
-import {ProjectSourceMaps} from 'sentry/views/settings/projectSourceMaps/projectSourceMaps';
 
-type Props = RouteComponentProps<{projectId: string}, {}> & {
+import {ProjectSourceMaps} from './projectSourceMaps';
+import {ProjectSourceMapsArtifacts} from './projectSourceMapsArtifacts';
+
+type Props = RouteComponentProps<
+  {orgId: string; projectId: string; bundleId?: string; name?: string},
+  {}
+> & {
   children: React.ReactNode;
   project: Project;
 };
 
-export function ProjectSourceMapsContainer(props: Props) {
+export function ProjectSourceMapsContainer({params, location, ...props}: Props) {
   const organization = useOrganization();
-
   const sourceMapsDebugIds = organization.features.includes('source-maps-debug-ids');
 
-  if (sourceMapsDebugIds) {
-    return <ProjectSourceMaps {...props} />;
+  if (!sourceMapsDebugIds) {
+    if (params.name) {
+      return (
+        <ProjectSourceMapsDetail
+          {...props}
+          location={location}
+          params={{...params, name: params.name}}
+          organization={organization}
+        />
+      );
+    }
+    return (
+      <ProjectSourceMapsList
+        {...props}
+        location={location}
+        params={params}
+        organization={organization}
+      />
+    );
   }
 
-  return <ProjectSourceMapsList {...props} organization={organization} />;
+  const releaseBundlesUrl = normalizeUrl(
+    `/settings/${params.orgId}/projects/${params.projectId}/source-maps/release-bundles/${
+      params.bundleId ? `${params.bundleId}/` : ''
+    }`
+  );
+
+  const debugIdsUrl = normalizeUrl(
+    `/settings/${params.orgId}/projects/${
+      params.projectId
+    }/source-maps/debug-id-bundles/${params.bundleId ? `${params.bundleId}/` : ''}`
+  );
+
+  const tabDebugIdBundlesActive = location.pathname === debugIdsUrl;
+  const tab = tabDebugIdBundlesActive ? 'debug-id' : 'release';
+
+  return (
+    <Fragment>
+      <SettingsPageHeader title={t('Source Maps')} />
+      <TextBlock>
+        {tct(
+          `These source map archives help Sentry identify where to look when Javascript is minified. By providing this information, you can get better context for your stack traces when debugging. To learn more about source maps, [link: read the docs].`,
+          {
+            link: (
+              <ExternalLink href="https://docs.sentry.io/platforms/javascript/sourcemaps/" />
+            ),
+          }
+        )}
+      </TextBlock>
+      <NavTabs underlined>
+        <ListLink to={releaseBundlesUrl} index isActive={() => !tabDebugIdBundlesActive}>
+          {t('Release Bundles')}
+        </ListLink>
+        <ListLink to={debugIdsUrl} isActive={() => tabDebugIdBundlesActive}>
+          {t('Debug ID Bundles')}
+        </ListLink>
+      </NavTabs>
+      {params.bundleId ? (
+        <ProjectSourceMapsArtifacts
+          {...props}
+          tab={tab}
+          location={location}
+          params={{...params, bundleId: params.bundleId}}
+        />
+      ) : (
+        <ProjectSourceMaps {...props} tab={tab} location={location} params={params} />
+      )}
+    </Fragment>
+  );
 }

+ 19 - 42
static/app/views/settings/projectSourceMaps/projectSourceMaps.tsx

@@ -12,10 +12,7 @@ import {Button} from 'sentry/components/button';
 import Confirm from 'sentry/components/confirm';
 import Count from 'sentry/components/count';
 import DateTime from 'sentry/components/dateTime';
-import ExternalLink from 'sentry/components/links/externalLink';
 import Link from 'sentry/components/links/link';
-import ListLink from 'sentry/components/links/listLink';
-import NavTabs from 'sentry/components/navTabs';
 import Pagination from 'sentry/components/pagination';
 import {PanelTable} from 'sentry/components/panels';
 import SearchBar from 'sentry/components/searchBar';
@@ -28,11 +25,8 @@ import {space} from 'sentry/styles/space';
 import {Project} from 'sentry/types';
 import {useQuery} from 'sentry/utils/queryClient';
 import {decodeScalar} from 'sentry/utils/queryString';
-import recreateRoute from 'sentry/utils/recreateRoute';
 import useApi from 'sentry/utils/useApi';
 import useOrganization from 'sentry/utils/useOrganization';
-import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
-import TextBlock from 'sentry/views/settings/components/text/textBlock';
 
 enum SORT_BY {
   ASC = 'date_added',
@@ -41,18 +35,18 @@ enum SORT_BY {
 
 type Props = RouteComponentProps<{}, {}> & {
   project: Project;
+  tab: 'release' | 'debug-id';
 };
 
-export function ProjectSourceMaps({routes, params, location, router, project}: Props) {
+export function ProjectSourceMaps({location, router, project, tab}: Props) {
   const api = useApi();
   const organization = useOrganization();
-  const baseUrl = recreateRoute('', {routes, params, stepBack: -1});
-  const tabDebugIdBundlesActive = location.pathname.endsWith('debug-id-bundles/');
   const query = decodeScalar(location.query.query);
   const sortBy = location.query.sort ?? SORT_BY.DESC;
   const cursor = location.query.cursor ?? '';
   const sourceMapsEndpoint = `/projects/${organization.slug}/${project.slug}/files/source-maps/`;
   const debugIdBundlesEndpoint = `/projects/${organization.slug}/${project.slug}/files/artifact-bundles/`;
+  const tabDebugIdBundlesActive = tab === 'debug-id';
 
   const {
     data: archivesData,
@@ -151,29 +145,6 @@ export function ProjectSourceMaps({routes, params, location, router, project}: P
 
   return (
     <Fragment>
-      <SettingsPageHeader title={t('Source Maps')} />
-      <TextBlock>
-        {tct(
-          `These source map archives help Sentry identify where to look when Javascript is minified. By providing this information, you can get better context for your stack traces when debugging. To learn more about source maps, [link: read the docs].`,
-          {
-            link: (
-              <ExternalLink href="https://docs.sentry.io/platforms/javascript/sourcemaps/" />
-            ),
-          }
-        )}
-      </TextBlock>
-      <NavTabs underlined>
-        <ListLink to={baseUrl} index isActive={() => !tabDebugIdBundlesActive}>
-          {t('Release Bundles')}
-        </ListLink>
-        <ListLink
-          to={`${baseUrl}debug-id-bundles/`}
-          index
-          isActive={() => tabDebugIdBundlesActive}
-        >
-          {t('Debug ID Bundles')}
-        </ListLink>
-      </NavTabs>
       <SearchBarWithMarginBottom
         placeholder={t('Search')}
         onSearch={handleSearch}
@@ -214,16 +185,20 @@ export function ProjectSourceMaps({routes, params, location, router, project}: P
         isEmpty={data.length === 0}
         isLoading={loading}
       >
-        {data.map(({name, date, fileCount}) => {
+        {data.map(({date, fileCount, ...d}) => {
+          const name = tabDebugIdBundlesActive ? d.bundleId : d.name;
+          const link = tabDebugIdBundlesActive
+            ? `/settings/${organization.slug}/projects/${
+                project.slug
+              }/source-maps/debug-id-bundles/${encodeURIComponent(name)}`
+            : `/settings/${organization.slug}/projects/${
+                project.slug
+              }/source-maps/release-bundles/${encodeURIComponent(name)}`;
           return (
             <Fragment key={name}>
               <Column>
                 <TextOverflow>
-                  <Link
-                    to={`/settings/${organization.slug}/projects/${
-                      project.slug
-                    }/source-maps/${encodeURIComponent(name)}`}
-                  >
+                  <Link to={link}>
                     <Version version={name} anchor={false} tooltipRawVersion truncate />
                   </Link>
                 </TextOverflow>
@@ -283,13 +258,11 @@ const StyledPanelTable = styled(PanelTable)`
   }
 `;
 
-const SearchBarWithMarginBottom = styled(SearchBar)`
-  margin-bottom: ${space(3)};
-`;
-
 const ArtifactsColumn = styled('div')`
   text-align: right;
   justify-content: flex-end;
+  align-items: center;
+  display: flex;
 `;
 
 const DateUploadedColumn = styled('div')`
@@ -307,3 +280,7 @@ const Column = styled('div')`
 const ActionsColumn = styled(Column)`
   justify-content: flex-end;
 `;
+
+const SearchBarWithMarginBottom = styled(SearchBar)`
+  margin-bottom: ${space(3)};
+`;

+ 248 - 0
static/app/views/settings/projectSourceMaps/projectSourceMapsArtifacts.tsx

@@ -0,0 +1,248 @@
+import {Fragment, useCallback} from 'react';
+import {RouteComponentProps} from 'react-router';
+import styled from '@emotion/styled';
+
+import {Role} from 'sentry/components/acl/role';
+import {Button} from 'sentry/components/button';
+import FileSize from 'sentry/components/fileSize';
+import Link from 'sentry/components/links/link';
+import Pagination from 'sentry/components/pagination';
+import {PanelTable} from 'sentry/components/panels';
+import SearchBar from 'sentry/components/searchBar';
+import Tag from 'sentry/components/tag';
+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 {Project} from 'sentry/types';
+import {useQuery} from 'sentry/utils/queryClient';
+import {decodeScalar} from 'sentry/utils/queryString';
+import useApi from 'sentry/utils/useApi';
+import useOrganization from 'sentry/utils/useOrganization';
+
+type Props = RouteComponentProps<{bundleId: string}, {}> & {
+  project: Project;
+  tab: 'release' | 'debug-id';
+};
+
+export function ProjectSourceMapsArtifacts({
+  params,
+  location,
+  router,
+  project,
+  tab,
+}: Props) {
+  const api = useApi();
+  const organization = useOrganization();
+  const tabDebugIdBundlesActive = tab === 'debug-id';
+  const query = decodeScalar(location.query.query);
+  const cursor = location.query.cursor ?? '';
+  const downloadRole = organization.debugFilesRole;
+  const artifactsEndpoint = `/projects/${organization.slug}/${
+    project.slug
+  }/releases/${encodeURIComponent(params.bundleId)}/files/`;
+  const debugIdBundlesEndpoint = ``;
+
+  const {data: artifactsData, isLoading: artifactsLoading} = useQuery(
+    [
+      artifactsEndpoint,
+      {
+        query: {query, cursor},
+      },
+    ],
+    () => {
+      return api.requestPromise(artifactsEndpoint, {
+        query: {query, cursor},
+        includeAllArgs: true,
+      });
+    },
+    {
+      staleTime: 0,
+      keepPreviousData: true,
+      enabled: !tabDebugIdBundlesActive,
+    }
+  );
+
+  const {data: debugIdBundlesData, isLoading: debugIdBundlesLoading} = useQuery(
+    [
+      debugIdBundlesEndpoint,
+      {
+        query: {query, cursor},
+      },
+    ],
+    () => {
+      return api.requestPromise(debugIdBundlesEndpoint, {
+        query: {query, cursor},
+        includeAllArgs: true,
+      });
+    },
+    {
+      staleTime: 0,
+      keepPreviousData: true,
+      enabled: tabDebugIdBundlesActive,
+    }
+  );
+
+  const data = tabDebugIdBundlesActive
+    ? debugIdBundlesData?.[0] ?? []
+    : artifactsData?.[0] ?? [];
+  const pageLinks = tabDebugIdBundlesActive
+    ? debugIdBundlesData?.[2]?.getResponseHeader('Link') ?? ''
+    : artifactsData?.[2]?.getResponseHeader('Link') ?? '';
+  const loading = tabDebugIdBundlesActive ? debugIdBundlesLoading : artifactsLoading;
+
+  const handleSearch = useCallback(
+    (newQuery: string) => {
+      router.push({
+        ...location,
+        query: {...location.query, cursor: undefined, query: newQuery},
+      });
+    },
+    [router, location]
+  );
+
+  return (
+    <Fragment>
+      <SearchBarWithMarginBottom
+        placeholder={t('Search')}
+        onSearch={handleSearch}
+        query={query}
+      />
+      <StyledPanelTable
+        headers={[
+          t('Artifact'),
+          <SizeColumn key="file-size">{t('File Size')}</SizeColumn>,
+          '',
+        ]}
+        emptyMessage={
+          query
+            ? t('No artifacts match your search query.')
+            : t('There are no artifacts in this archive.')
+        }
+        isEmpty={data.length === 0}
+        isLoading={loading}
+      >
+        {data.map(({id, size, name, dist, dateCreated}) => {
+          const downloadUrl = `${api.baseUrl}/projects/${organization.slug}/${
+            project.slug
+          }/releases/${encodeURIComponent(name)}/files/${id}/?download=1`;
+
+          return (
+            <Fragment key={id}>
+              <NameColumn>
+                <Name>{name || `(${t('empty')})`}</Name>
+                <TimeAndDistWrapper>
+                  <TimeWrapper>
+                    <IconClock size="sm" />
+                    <TimeSince date={dateCreated} />
+                  </TimeWrapper>
+                  <StyledTag
+                    type={dist ? 'info' : undefined}
+                    tooltipText={dist ? undefined : t('No distribution set')}
+                  >
+                    {dist ?? t('none')}
+                  </StyledTag>
+                </TimeAndDistWrapper>
+              </NameColumn>
+              <SizeColumn>
+                <FileSize bytes={size} />
+              </SizeColumn>
+              <ActionsColumn>
+                <Role role={downloadRole}>
+                  {({hasRole}) => (
+                    <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/${organization.slug}/#debugFilesRole`} />
+                          ),
+                        }
+                      )}
+                      disabled={hasRole}
+                      isHoverable
+                    >
+                      <Button
+                        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>
+          );
+        })}
+      </StyledPanelTable>
+      <Pagination pageLinks={pageLinks} />
+    </Fragment>
+  );
+}
+
+const StyledPanelTable = styled(PanelTable)`
+  grid-template-columns: minmax(220px, 1fr) 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 SizeColumn = styled('div')`
+  display: flex;
+  justify-content: flex-end;
+  text-align: right;
+  align-items: center;
+`;
+
+const Name = styled('div')`
+  padding-right: ${space(4)};
+  overflow-wrap: break-word;
+  word-break: break-all;
+`;
+
+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 NameColumn = styled('div')`
+  display: flex;
+  flex-direction: column;
+  align-items: flex-start;
+  justify-content: center;
+`;