Browse Source

style(most-helpful-event): Style and text updates (#53943)

Closes https://github.com/getsentry/sentry/issues/53738

## Before
<img width="350" alt="Screenshot 2023-08-01 at 10 05 08 AM"
src="https://github.com/getsentry/sentry/assets/22582037/5308f6cd-4640-4952-abc5-1821f4e0d395"
>

## After
<img width="250" alt="Screenshot 2023-08-01 at 9 39 47 AM"
src="https://github.com/getsentry/sentry/assets/22582037/0ccd2e1a-0cfe-41bb-86e1-18be67209f40">

## Before
<img width="350" alt="Screenshot 2023-08-01 at 1 20 43 PM"
src="https://github.com/getsentry/sentry/assets/22582037/5d7cd714-c80d-4c87-be15-055c136791f4">

## After
<img width="300" alt="Screenshot 2023-08-01 at 1 21 41 PM"
src="https://github.com/getsentry/sentry/assets/22582037/dae11de9-7222-48a3-978c-3037d92aee65">

---------

Co-authored-by: Malachi Willey <malwilley@gmail.com>
Julia Hoge 1 year ago
parent
commit
f04fd28a1c

+ 16 - 0
static/app/icons/iconJson.tsx

@@ -0,0 +1,16 @@
+import {forwardRef} from 'react';
+
+import {SvgIcon, SVGIconProps} from './svgIcon';
+
+const IconJson = forwardRef<SVGSVGElement, SVGIconProps>((props, ref) => {
+  return (
+    <SvgIcon {...props} ref={ref}>
+      <path d="m5.1,15.23h-.84c-1.44,0-2.62-1.18-2.62-2.62v-2.06c0-.4-.2-.77-.54-.99l-.29-.19c-.47-.3-.75-.82-.75-1.38s.28-1.07.75-1.38l.3-.19c.33-.22.54-.58.54-.98v-2.06C1.64,1.95,2.81.77,4.26.77h.84c.39,0,.7.32.7.7s-.32.7-.7.7h-.84c-.67,0-1.21.54-1.21,1.21v2.06c0,.88-.44,1.69-1.18,2.17l-.3.19c-.09.06-.11.15-.11.2s.01.14.11.2l.29.19c.74.48,1.18,1.29,1.18,2.17v2.06c0,.67.54,1.21,1.21,1.21h.84c.39,0,.7.32.7.7s-.32.7-.7.7Z" />
+      <path d="m11.74,15.23h-.84c-.39,0-.7-.32-.7-.7s.32-.7.7-.7h.84c.67,0,1.21-.54,1.21-1.21v-2.06c0-.88.44-1.69,1.18-2.17l.3-.19c.09-.06.11-.15.11-.2s-.01-.14-.11-.2l-.29-.19c-.74-.48-1.18-1.29-1.18-2.17v-2.06c0-.67-.54-1.21-1.21-1.21h-.84c-.39,0-.7-.32-.7-.7s.32-.7.7-.7h.84c1.44,0,2.62,1.18,2.62,2.62v2.06c0,.4.2.77.54.99l.29.19c.47.3.75.82.75,1.38s-.28,1.07-.75,1.38l-.3.19c-.33.22-.54.58-.54.98v2.06c0,1.44-1.17,2.62-2.62,2.62Z" />
+    </SvgIcon>
+  );
+});
+
+IconJson.displayName = 'IconJson';
+
+export {IconJson};

+ 1 - 0
static/app/icons/index.tsx

@@ -49,6 +49,7 @@ export {IconInfo} from './iconInfo';
 export {IconInput} from './iconInput';
 export {IconIssues} from './iconIssues';
 export {IconJira} from './iconJira';
+export {IconJson} from './iconJson';
 export {IconLab} from './iconLab';
 export {IconLaptop} from './iconLaptop';
 export {IconLightning} from './iconLightning';

+ 48 - 26
static/app/views/issueDetails/groupEventCarousel.spec.tsx

@@ -33,41 +33,63 @@ describe('GroupEventCarousel', () => {
     window.open = jest.fn();
   });
 
-  it('can use event dropdown to navigate events', async () => {
-    // Because it isn't rendered on smaller screens
-    jest.spyOn(useMedia, 'default').mockReturnValue(true);
-
-    render(<GroupEventCarousel {...defaultProps} />, {
-      organization: TestStubs.Organization({
-        features: [
-          'issue-details-most-helpful-event',
-          'issue-details-most-helpful-event-ui',
-        ],
-      }),
+  describe('recommended event ui', () => {
+    const orgWithRecommendedEvent = TestStubs.Organization({
+      features: [
+        'issue-details-most-helpful-event',
+        'issue-details-most-helpful-event-ui',
+      ],
     });
 
-    await userEvent.click(screen.getByRole('button', {name: /recommended event/i}));
-    await userEvent.click(screen.getByRole('option', {name: /oldest event/i}));
+    it('can navigate to the oldest event', async () => {
+      jest.spyOn(useMedia, 'default').mockReturnValue(true);
+
+      render(<GroupEventCarousel {...defaultProps} />, {
+        organization: orgWithRecommendedEvent,
+      });
+
+      await userEvent.click(screen.getByRole('button', {name: /recommended/i}));
+      await userEvent.click(screen.getByRole('option', {name: /oldest/i}));
 
-    expect(browserHistory.push).toHaveBeenCalledWith({
-      pathname: '/organizations/org-slug/issues/group-id/events/oldest/',
-      query: {referrer: 'oldest-event'},
+      expect(browserHistory.push).toHaveBeenCalledWith({
+        pathname: '/organizations/org-slug/issues/group-id/events/oldest/',
+        query: {referrer: 'oldest-event'},
+      });
     });
 
-    await userEvent.click(screen.getByRole('button', {name: /oldest event/i}));
-    await userEvent.click(screen.getByRole('option', {name: /latest event/i}));
+    it('can navigate to the latest event', async () => {
+      jest.spyOn(useMedia, 'default').mockReturnValue(true);
 
-    expect(browserHistory.push).toHaveBeenCalledWith({
-      pathname: '/organizations/org-slug/issues/group-id/events/oldest/',
-      query: {referrer: 'oldest-event'},
+      render(<GroupEventCarousel {...defaultProps} />, {
+        organization: orgWithRecommendedEvent,
+      });
+
+      await userEvent.click(screen.getByRole('button', {name: /recommended/i}));
+      await userEvent.click(screen.getByRole('option', {name: /latest/i}));
+
+      expect(browserHistory.push).toHaveBeenCalledWith({
+        pathname: '/organizations/org-slug/issues/group-id/events/latest/',
+        query: {referrer: 'latest-event'},
+      });
     });
 
-    await userEvent.click(screen.getByRole('button', {name: /latest event/i}));
-    await userEvent.click(screen.getByRole('option', {name: /recommended event/i}));
+    it('can navigate to the recommended event', async () => {
+      jest.spyOn(useMedia, 'default').mockReturnValue(true);
+
+      render(<GroupEventCarousel {...defaultProps} />, {
+        organization: orgWithRecommendedEvent,
+        router: {
+          params: {eventId: 'latest'},
+        },
+      });
+
+      await userEvent.click(screen.getByRole('button', {name: /latest/i}));
+      await userEvent.click(screen.getByRole('option', {name: /recommended/i}));
 
-    expect(browserHistory.push).toHaveBeenCalledWith({
-      pathname: '/organizations/org-slug/issues/group-id/events/recommended/',
-      query: {referrer: 'recommended-event'},
+      expect(browserHistory.push).toHaveBeenCalledWith({
+        pathname: '/organizations/org-slug/issues/group-id/events/recommended/',
+        query: {referrer: 'recommended-event'},
+      });
     });
   });
 

+ 79 - 16
static/app/views/issueDetails/groupEventCarousel.tsx

@@ -8,11 +8,15 @@ import {Button, ButtonProps} from 'sentry/components/button';
 import {CompactSelect} from 'sentry/components/compactSelect';
 import DateTime from 'sentry/components/dateTime';
 import {DropdownMenu} from 'sentry/components/dropdownMenu';
+import FeatureBadge from 'sentry/components/featureBadge';
+import TimeSince from 'sentry/components/timeSince';
 import {Tooltip} from 'sentry/components/tooltip';
 import {
   IconChevron,
   IconCopy,
   IconEllipsis,
+  IconJson,
+  IconLink,
   IconNext,
   IconOpen,
   IconPrevious,
@@ -46,6 +50,11 @@ type GroupEventCarouselProps = {
   projectSlug: string;
 };
 
+type GroupEventNavigationProps = {
+  group: Group;
+  relativeTime: string;
+};
+
 type EventNavigationButtonProps = {
   disabled: boolean;
   group: Group;
@@ -59,19 +68,13 @@ enum EventNavDropdownOption {
   RECOMMENDED = 'recommended',
   LATEST = 'latest',
   OLDEST = 'oldest',
+  CUSTOM = 'custom',
   ALL = 'all',
 }
 
 const BUTTON_SIZE = 'sm';
 const BUTTON_ICON_SIZE = 'sm';
 
-const EVENT_NAV_DROPDOWN_OPTIONS = [
-  {value: EventNavDropdownOption.RECOMMENDED, label: 'Recommended Event'},
-  {value: EventNavDropdownOption.LATEST, label: 'Latest Event'},
-  {value: EventNavDropdownOption.OLDEST, label: 'Oldest Event'},
-  {options: [{value: EventNavDropdownOption.ALL, label: 'View All Events'}]},
-];
-
 const makeBaseEventsPath = ({
   organization,
   group,
@@ -112,7 +115,7 @@ function EventNavigationButton({
   );
 }
 
-function EventNavigationDropdown({group}: {group: Group}) {
+function EventNavigationDropdown({group, relativeTime}: GroupEventNavigationProps) {
   const location = useLocation();
   const params = useParams<{eventId?: string}>();
   const theme = useTheme();
@@ -141,13 +144,54 @@ function EventNavigationDropdown({group}: {group: Group}) {
   };
 
   const selectedValue = getSelectedOption();
+  const eventNavDropdownOptions = [
+    {
+      value: EventNavDropdownOption.RECOMMENDED,
+      label: (
+        <div>
+          {t('Recommended')}
+          <FeatureBadge type="new" />
+        </div>
+      ),
+      textValue: t('Recommended'),
+      details: t('Event with the most context'),
+    },
+    {
+      value: EventNavDropdownOption.LATEST,
+      label: t('Latest'),
+      details: t('Last seen event in this issue'),
+    },
+    {
+      value: EventNavDropdownOption.OLDEST,
+      label: t('Oldest'),
+      details: t('First seen event in this issue'),
+    },
+    ...(!selectedValue
+      ? [
+          {
+            value: EventNavDropdownOption.CUSTOM,
+            label: t('Custom Selection'),
+          },
+        ]
+      : []),
+    {
+      options: [{value: EventNavDropdownOption.ALL, label: 'View All Events'}],
+    },
+  ];
 
   return (
     <CompactSelect
       size="sm"
-      options={EVENT_NAV_DROPDOWN_OPTIONS}
-      value={selectedValue}
-      triggerLabel={!selectedValue ? 'Navigate Events' : undefined}
+      options={eventNavDropdownOptions}
+      value={!selectedValue ? EventNavDropdownOption.CUSTOM : selectedValue}
+      triggerLabel={
+        !selectedValue ? (
+          <TimeSince date={relativeTime} disabledAbsoluteTooltip />
+        ) : selectedValue === EventNavDropdownOption.RECOMMENDED ? (
+          t('Recommended')
+        ) : undefined
+      }
+      menuWidth={232}
       onChange={selectedOption => {
         switch (selectedOption.value) {
           case EventNavDropdownOption.RECOMMENDED:
@@ -337,20 +381,39 @@ export function GroupEventCarousel({event, group, projectSlug}: GroupEventCarous
           ]}
         />
         {xlargeViewport && (
-          <Button size={BUTTON_SIZE} onClick={copyLink}>
-            Copy Link
+          <Button
+            title={
+              isHelpfulEventUiEnabled ? t('Copy link to this issue event') : undefined
+            }
+            size={BUTTON_SIZE}
+            onClick={copyLink}
+            aria-label={t('Copy Link')}
+            icon={isHelpfulEventUiEnabled ? <IconLink /> : undefined}
+          >
+            {!isHelpfulEventUiEnabled && 'Copy Link'}
           </Button>
         )}
         {xlargeViewport && (
           <Button
+            title={isHelpfulEventUiEnabled ? t('View JSON') : undefined}
             size={BUTTON_SIZE}
-            icon={<IconOpen size={BUTTON_ICON_SIZE} />}
             onClick={downloadJson}
+            aria-label={t('View JSON')}
+            icon={
+              isHelpfulEventUiEnabled ? (
+                <IconJson />
+              ) : (
+                <IconOpen size={BUTTON_ICON_SIZE} />
+              )
+            }
           >
-            JSON
+            {!isHelpfulEventUiEnabled && 'JSON'}
           </Button>
         )}
-        <EventNavigationDropdown group={group} />
+        <EventNavigationDropdown
+          group={group}
+          relativeTime={event.dateCreated ?? event.dateReceived}
+        />
         <NavButtons>
           {!isHelpfulEventUiEnabled && (
             <EventNavigationButton