Browse Source

ref(source-maps): Update release + dist render UI (#50412)

Priscila Oliveira 1 year ago
parent
commit
80fc2f24b3

+ 10 - 0
fixtures/js-stubs/sourceMapsDebugIDBundles.ts

@@ -6,6 +6,16 @@ export function SourceMapsDebugIDBundles(
   return [
     {
       bundleId: 'b916a646-2c6b-4e45-af4c-409830a44e0e',
+      associations: [
+        {
+          release: 'v2.0',
+          dist: null,
+        },
+        {
+          release: 'frontend@2e318148eac9298ec04a662ae32b4b093b027f0a',
+          dist: ['android', 'iOS'],
+        },
+      ],
       release: null,
       dist: null,
       fileCount: 39,

+ 12 - 2
fixtures/js-stubs/sourceMapsDebugIDBundlesArtifacts.ts

@@ -5,8 +5,18 @@ export function SourceMapsDebugIDBundlesArtifacts(
 ): DebugIdBundleArtifact {
   return {
     bundleId: '7227e105-744e-4066-8c69-3e5e344723fc',
-    release: '2.0',
-    dist: 'android',
+    release: null,
+    dist: null,
+    associations: [
+      {
+        release: 'v2.0',
+        dist: null,
+      },
+      {
+        release: 'frontend@2e318148eac9298ec04a662ae32b4b093b027f0a',
+        dist: ['android', 'iOS'],
+      },
+    ],
     files: [
       {
         id: 'ZmlsZXMvXy9fL21haW4uanM=',

+ 9 - 3
static/app/components/clippedBox.tsx

@@ -4,7 +4,7 @@ import {css} from '@emotion/react';
 import styled from '@emotion/styled';
 import color from 'color';
 
-import {Button} from 'sentry/components/button';
+import {Button, ButtonProps} from 'sentry/components/button';
 import {t} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
 
@@ -22,6 +22,10 @@ type DefaultProps = {
 type Props = {
   clipFlex: number;
   clipHeight: number;
+  /**
+   * Used to customize the button
+   */
+  buttonProps?: Partial<ButtonProps>;
   children?: React.ReactNode;
   className?: string;
   /**
@@ -134,7 +138,8 @@ class ClippedBox extends PureComponent<Props, State> {
 
   render() {
     const {isClipped, isRevealed} = this.state;
-    const {title, children, clipHeight, btnText, className, clipFade} = this.props;
+    const {title, children, clipHeight, btnText, className, clipFade, buttonProps} =
+      this.props;
 
     const showMoreButton = (
       <Button
@@ -142,6 +147,7 @@ class ClippedBox extends PureComponent<Props, State> {
         priority="primary"
         size="xs"
         aria-label={btnText ?? t('Show More')}
+        {...buttonProps}
       >
         {btnText}
       </Button>
@@ -192,7 +198,7 @@ const Title = styled('h5')`
   margin-bottom: ${space(1)};
 `;
 
-const ClipFade = styled('div')`
+export const ClipFade = styled('div')`
   position: absolute;
   left: 0;
   right: 0;

+ 11 - 0
static/app/types/sourceMaps.ts

@@ -1,13 +1,23 @@
+export type DebugIdBundleAssociation = {
+  dist: string[] | string | null;
+  release: string;
+};
+
 export type DebugIdBundle = {
+  associations: DebugIdBundleAssociation[];
   bundleId: string;
   date: string;
+  // TODO(Pri): Remove this type once fully transitioned to associations.
   dist: string | null;
   fileCount: number;
+  // TODO(Pri): Remove this type once fully transitioned to associations.
   release: string | null;
 };
 
 export type DebugIdBundleArtifact = {
+  associations: DebugIdBundleAssociation[];
   bundleId: string;
+  // TODO(Pri): Remove this type once fully transitioned to associations.
   dist: string | null;
   files: {
     debugId: string;
@@ -16,5 +26,6 @@ export type DebugIdBundleArtifact = {
     fileType: number;
     id: string;
   }[];
+  // TODO(Pri): Remove this type once fully transitioned to associations.
   release: string | null;
 };

+ 124 - 0
static/app/views/settings/projectSourceMaps/associations.tsx

@@ -0,0 +1,124 @@
+import {Link} from 'react-router';
+import styled from '@emotion/styled';
+
+import ClippedBox, {ClipFade} from 'sentry/components/clippedBox';
+import {CopyToClipboardButton} from 'sentry/components/copyToClipboardButton';
+import {Hovercard} from 'sentry/components/hovercard';
+import Placeholder from 'sentry/components/placeholder';
+import TextOverflow from 'sentry/components/textOverflow';
+import {t, tct, tn} from 'sentry/locale';
+import {space} from 'sentry/styles/space';
+import {DebugIdBundleAssociation} from 'sentry/types/sourceMaps';
+import useOrganization from 'sentry/utils/useOrganization';
+
+function AssociationsBody({associations}: {associations: DebugIdBundleAssociation[]}) {
+  const organization = useOrganization();
+
+  return (
+    <ClippedBoxWithoutPadding
+      clipHeight={210}
+      btnText={t('+ %s more', associations.length - associations.slice(0, 4).length)}
+      buttonProps={{
+        priority: 'default',
+        borderless: true,
+      }}
+    >
+      <NumericList>
+        {associations.map(({release, dist}) => (
+          <li key={release}>
+            <ReleaseContent>
+              <ReleaseLink
+                to={`/organizations/${organization.slug}/releases/${release}/`}
+              >
+                <TextOverflow>{release}</TextOverflow>
+              </ReleaseLink>
+              <CopyToClipboardButton
+                text={release}
+                borderless
+                size="zero"
+                iconSize="sm"
+              />
+            </ReleaseContent>
+            {!dist?.length ? (
+              <NoAssociations>
+                {t('No dists associated with this release')}
+              </NoAssociations>
+            ) : (
+              tct('Dist: [dist]', {
+                dist: typeof dist === 'string' ? dist : dist.join(', '),
+              })
+            )}
+          </li>
+        ))}
+      </NumericList>
+    </ClippedBoxWithoutPadding>
+  );
+}
+
+type Props = {
+  associations?: DebugIdBundleAssociation[];
+  loading?: boolean;
+};
+
+export function Associations({associations = [], loading}: Props) {
+  if (loading) {
+    return <Placeholder width="200px" height="20px" />;
+  }
+
+  if (!associations.length) {
+    return (
+      <NoAssociations>{t('No releases associated with this bundle')}</NoAssociations>
+    );
+  }
+
+  return (
+    <div>
+      <WiderHovercard
+        position="right"
+        body={<AssociationsBody associations={associations} />}
+        header={t('Releases')}
+        displayTimeout={0}
+        showUnderline
+      >
+        {tn('%s Release', '%s Releases', associations.length)}
+      </WiderHovercard>{' '}
+      {t('associated')}
+    </div>
+  );
+}
+
+const NoAssociations = styled('div')`
+  color: ${p => p.theme.disabled};
+`;
+
+const ReleaseContent = styled('div')`
+  display: grid;
+  grid-template-columns: 1fr max-content;
+  gap: ${space(1)};
+  align-items: center;
+`;
+
+const ReleaseLink = styled(Link)`
+  overflow: hidden;
+`;
+
+// TODO(ui): Add a native numeric list to the List component
+const NumericList = styled('ol')`
+  display: flex;
+  flex-direction: column;
+  gap: ${space(0.5)};
+  margin: 0;
+`;
+
+const WiderHovercard = styled(Hovercard)`
+  width: 320px;
+`;
+
+const ClippedBoxWithoutPadding = styled(ClippedBox)`
+  padding: 0;
+  ${ClipFade} {
+    background: ${p => p.theme.background};
+    border-bottom: 0;
+    padding: 0;
+  }
+`;

+ 12 - 1
static/app/views/settings/projectSourceMaps/projectSourceMaps.spec.tsx

@@ -6,6 +6,7 @@ import {
   userEvent,
   waitFor,
 } from 'sentry-test/reactTestingLibrary';
+import {textWithMarkupMatcher} from 'sentry-test/utils';
 
 import {ProjectSourceMaps} from 'sentry/views/settings/projectSourceMaps/projectSourceMaps';
 
@@ -276,6 +277,16 @@ describe('ProjectSourceMaps', function () {
       expect(screen.getByText('Mar 8, 2023 9:53 AM UTC')).toBeInTheDocument();
       // Delete button
       expect(screen.getByRole('button', {name: 'Remove All Artifacts'})).toBeEnabled();
+
+      // Release information
+      expect(
+        await screen.findByText(textWithMarkupMatcher('2 Releases associated'))
+      ).toBeInTheDocument();
+      await userEvent.hover(screen.getByText('2 Releases'));
+      expect(
+        await screen.findByText('frontend@2e318148eac9298ec04a662ae32b4b093b027f0a')
+      ).toBeInTheDocument();
+
       // Click on bundle id
       await userEvent.click(
         screen.getByRole('link', {name: 'b916a646-2c6b-4e45-af4c-409830a44e0e'})
@@ -347,7 +358,7 @@ describe('ProjectSourceMaps', function () {
       );
 
       expect(
-        await screen.findByText('No debug ID bundles found for this project.')
+        await screen.findByText('No artifact bundles found for this project.')
       ).toBeInTheDocument();
     });
   });

+ 10 - 3
static/app/views/settings/projectSourceMaps/projectSourceMaps.tsx

@@ -32,6 +32,7 @@ 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 {Associations} from 'sentry/views/settings/projectSourceMaps/associations';
 import {DebugIdBundlesTags} from 'sentry/views/settings/projectSourceMaps/debugIdBundlesTags';
 
 enum SortBy {
@@ -321,12 +322,12 @@ export function ProjectSourceMaps({location, router, project}: Props) {
           query
             ? tct('No [tabName] match your search query.', {
                 tabName: tabDebugIdBundlesActive
-                  ? t('debug ID bundles')
+                  ? t('artifact bundles')
                   : t('release bundles'),
               })
             : tct('No [tabName] found for this project.', {
                 tabName: tabDebugIdBundlesActive
-                  ? t('debug ID bundles')
+                  ? t('artifact bundles')
                   : t('release bundles'),
               })
         }
@@ -349,7 +350,13 @@ export function ProjectSourceMaps({location, router, project}: Props) {
                   project.slug
                 }/source-maps/artifact-bundles/${encodeURIComponent(data.bundleId)}`}
                 idColumnDetails={
-                  <DebugIdBundlesTags dist={data.dist} release={data.release} />
+                  // TODO(Pri): Move the loading to the component once fully transitioned to associations.
+                  !debugIdBundlesLoading &&
+                  (data.associations ? (
+                    <Associations associations={data.associations} />
+                  ) : (
+                    <DebugIdBundlesTags dist={data.dist} release={data.release} />
+                  ))
                 }
               />
             ))

+ 11 - 5
static/app/views/settings/projectSourceMaps/projectSourceMapsArtifacts.spec.tsx

@@ -1,5 +1,6 @@
 import {initializeOrg} from 'sentry-test/initializeOrg';
 import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
+import {textWithMarkupMatcher} from 'sentry-test/utils';
 
 import ConfigStore from 'sentry/stores/configStore';
 import {ProjectSourceMapsArtifacts} from 'sentry/views/settings/projectSourceMaps/projectSourceMapsArtifacts';
@@ -202,14 +203,14 @@ describe('ProjectSourceMapsArtifacts', function () {
       expect(
         screen.getByText('7227e105-744e-4066-8c69-3e5e344723fc')
       ).toBeInTheDocument();
-      // Chips
-      await userEvent.hover(await screen.findByText('2.0'));
+
+      // Release information
       expect(
-        await screen.findByText('Associated with release "2.0"')
+        await screen.findByText(textWithMarkupMatcher('2 Releases associated'))
       ).toBeInTheDocument();
-      await userEvent.hover(await screen.findByText('android'));
+      await userEvent.hover(screen.getByText('2 Releases'));
       expect(
-        await screen.findByText('Associated with distribution "android"')
+        await screen.findByText('frontend@2e318148eac9298ec04a662ae32b4b093b027f0a')
       ).toBeInTheDocument();
 
       // Search bar
@@ -269,6 +270,11 @@ describe('ProjectSourceMapsArtifacts', function () {
       expect(
         await screen.findByText('There are no artifacts in this bundle.')
       ).toBeInTheDocument();
+
+      // TODO(Pri): Uncomment once fully transitioned to associations.
+      // expect(
+      //   screen.getByText('No releases associated with this bundle')
+      // ).toBeInTheDocument();
     });
   });
 });

+ 12 - 7
static/app/views/settings/projectSourceMaps/projectSourceMapsArtifacts.tsx

@@ -22,6 +22,7 @@ import useApi from 'sentry/utils/useApi';
 import useOrganization from 'sentry/utils/useOrganization';
 import {normalizeUrl} from 'sentry/utils/withDomainRequired';
 import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
+import {Associations} from 'sentry/views/settings/projectSourceMaps/associations';
 import {DebugIdBundlesTags} from 'sentry/views/settings/projectSourceMaps/debugIdBundlesTags';
 
 enum DebugIdBundleArtifactType {
@@ -180,13 +181,17 @@ export function ProjectSourceMapsArtifacts({params, location, router, project}:
         subtitle={
           <VersionAndDetails>
             {params.bundleId}
-            {tabDebugIdBundlesActive && (
-              <DebugIdBundlesTags
-                dist={debugIdBundlesArtifactsData?.dist}
-                release={debugIdBundlesArtifactsData?.release}
-                loading={debugIdBundlesArtifactsLoading}
-              />
-            )}
+            {tabDebugIdBundlesActive &&
+              // TODO(Pri): Move the loading to the component once fully transitioned to associations.
+              !debugIdBundlesArtifactsLoading &&
+              (debugIdBundlesArtifactsData?.associations ? (
+                <Associations associations={debugIdBundlesArtifactsData?.associations} />
+              ) : (
+                <DebugIdBundlesTags
+                  dist={debugIdBundlesArtifactsData?.dist}
+                  release={debugIdBundlesArtifactsData?.release}
+                />
+              ))}
           </VersionAndDetails>
         }
       />