Browse Source

ref(issue-details): UI Refactors for fold sections (#75400)

This PR fixes some of the UI bugs that were mentioned in
https://github.com/getsentry/sentry/pull/75169

Some of the things:

- Screenshots no longer renders alongside, they have their own section.
It doesn't have a hyper link heading, it has an action to view all
screenshots
- The grouping info section displays both the summary and details and
does not have a hide/show button
- The jump links work
- The event navigator has been moved inside this section with some
styling updates
- A non-functional input has been added for the event search with a date
picker
- DebugMeta (Images Loaded section) has not been touched since it'd
balloon the PR. It has to be refactored first in
https://github.com/getsentry/sentry/pull/75379

Here's a bunch of screenshots:


![image](https://github.com/user-attachments/assets/0d9c60f7-c5c7-45c7-94e0-758dec05d4de)

![image](https://github.com/user-attachments/assets/72413fa8-8432-41e3-be5b-c0a1bc3be539)

![Screenshot 2024-08-02 at 3 43
50 PM](https://github.com/user-attachments/assets/845e83f0-c2d2-45be-9b7b-4a8352c30459)
![Screenshot 2024-08-02 at 3 35
58 PM](https://github.com/user-attachments/assets/fc747518-dffc-4479-928a-77959ccefc1c)
![Screenshot 2024-08-02 at 3 32
15 PM](https://github.com/user-attachments/assets/aab91844-8f17-415f-bda8-f47fedad4031)
![Screenshot 2024-08-02 at 3 31
12 PM](https://github.com/user-attachments/assets/0bfa4906-de1d-4b2b-95ba-eb7842eda917)
![Screenshot 2024-08-02 at 3 33
07 PM](https://github.com/user-attachments/assets/f0f19d98-d8d2-4813-8011-837801ffab9b)


**todo**
- [x] Fix the Response section
- [x] Update the new user feedback section
- [ ] Fix thread selector section -> Gonna defer to another PR, it's
very woven into the existing stack trace stuff
- [x] Add screenshots
Leander Rodrigues 7 months ago
parent
commit
73d3f7ea54

+ 35 - 25
static/app/components/events/eventReplay/replayClipSection.tsx

@@ -20,6 +20,7 @@ import useOrganization from 'sentry/utils/useOrganization';
 import useRouter from 'sentry/utils/useRouter';
 import {FoldSectionKey} from 'sentry/views/issueDetails/streamline/foldSection';
 import {InterimSection} from 'sentry/views/issueDetails/streamline/interimSection';
+import {useHasStreamlinedUI} from 'sentry/views/issueDetails/utils';
 
 interface Props {
   event: Event;
@@ -36,6 +37,7 @@ const ReplayClipPreview = lazy(() => import('./replayClipPreview'));
 
 export function ReplayClipSection({event, group, replayId}: Props) {
   const organization = useOrganization();
+  const hasStreamlinedUI = useHasStreamlinedUI();
   const router = useRouter();
   const {getReplayCountForIssue} = useReplayCountForIssues();
 
@@ -79,6 +81,32 @@ export function ReplayClipSection({event, group, replayId}: Props) {
       </Fragment>
     ) : undefined;
 
+  const lazyReplay = (
+    <LazyLoad
+      analyticsContext="issue_details"
+      replaySlug={replayId}
+      orgSlug={organization.slug}
+      eventTimestampMs={eventTimestampMs}
+      fullReplayButtonProps={{
+        analyticsEventKey: 'issue_details.open_replay_details_clicked',
+        analyticsEventName: 'Issue Details: Open Replay Details Clicked',
+        analyticsParams: {
+          ...getAnalyticsDataForEvent(event),
+          ...getAnalyticsDataForGroup(group),
+          organization,
+        },
+      }}
+      loadingFallback={
+        <StyledNegativeSpaceContainer data-test-id="replay-loading-placeholder">
+          <LoadingIndicator />
+        </StyledNegativeSpaceContainer>
+      }
+      LazyComponent={ReplayClipPreview}
+      clipOffsets={REPLAY_CLIP_OFFSETS}
+      overlayContent={overlayContent}
+    />
+  );
+
   return (
     <ReplaySectionMinHeight
       title={t('Session Replay')}
@@ -87,31 +115,13 @@ export function ReplayClipSection({event, group, replayId}: Props) {
     >
       <ErrorBoundary mini>
         <ReplayGroupContextProvider groupId={group?.id} eventId={event.id}>
-          <ReactLazyLoad debounce={50} height={448} offset={0} once>
-            <LazyLoad
-              analyticsContext="issue_details"
-              replaySlug={replayId}
-              orgSlug={organization.slug}
-              eventTimestampMs={eventTimestampMs}
-              fullReplayButtonProps={{
-                analyticsEventKey: 'issue_details.open_replay_details_clicked',
-                analyticsEventName: 'Issue Details: Open Replay Details Clicked',
-                analyticsParams: {
-                  ...getAnalyticsDataForEvent(event),
-                  ...getAnalyticsDataForGroup(group),
-                  organization,
-                },
-              }}
-              loadingFallback={
-                <StyledNegativeSpaceContainer data-test-id="replay-loading-placeholder">
-                  <LoadingIndicator />
-                </StyledNegativeSpaceContainer>
-              }
-              LazyComponent={ReplayClipPreview}
-              clipOffsets={REPLAY_CLIP_OFFSETS}
-              overlayContent={overlayContent}
-            />
-          </ReactLazyLoad>
+          {hasStreamlinedUI ? (
+            lazyReplay
+          ) : (
+            <ReactLazyLoad debounce={50} height={448} offset={0} once>
+              {lazyReplay}
+            </ReactLazyLoad>
+          )}
         </ReplayGroupContextProvider>
       </ErrorBoundary>
     </ReplaySectionMinHeight>

+ 12 - 102
static/app/components/events/eventTagsAndScreenshot/index.tsx

@@ -1,26 +1,11 @@
-import {useState} from 'react';
 import styled from '@emotion/styled';
 
-import {
-  useDeleteEventAttachmentOptimistic,
-  useFetchEventAttachments,
-} from 'sentry/actionCreators/events';
-import {openModal} from 'sentry/actionCreators/modal';
+import {useFetchEventAttachments} from 'sentry/actionCreators/events';
+import {ScreenshotDataSection} from 'sentry/components/events/eventTagsAndScreenshot/screenshot/screenshotDataSection';
 import {DataSection} from 'sentry/components/events/styles';
-import Link from 'sentry/components/links/link';
-import {t, tn} from 'sentry/locale';
-import type {EventAttachment} from 'sentry/types/group';
-import {trackAnalytics} from 'sentry/utils/analytics';
-import {useLocation} from 'sentry/utils/useLocation';
 import useOrganization from 'sentry/utils/useOrganization';
-import {SCREENSHOT_TYPE} from 'sentry/views/issueDetails/groupEventAttachments/groupEventAttachmentsFilter';
-import {FoldSectionKey} from 'sentry/views/issueDetails/streamline/foldSection';
-import {InterimSection} from 'sentry/views/issueDetails/streamline/interimSection';
-import {Tab, TabPaths} from 'sentry/views/issueDetails/types';
 
-import Modal, {modalCss} from './screenshot/modal';
-import Screenshot from './screenshot';
-import Tags from './tags';
+import EventTagsDataSection from './tags';
 
 const SCREENSHOT_NAMES = [
   'screenshot.jpg',
@@ -31,12 +16,11 @@ const SCREENSHOT_NAMES = [
   'screenshot-2.png',
 ];
 
-type Props = React.ComponentProps<typeof Tags> & {
+type Props = React.ComponentProps<typeof EventTagsDataSection> & {
   isShare?: boolean;
 };
 
 export function EventTagsAndScreenshot({projectSlug, event, isShare = false}: Props) {
-  const location = useLocation();
   const organization = useOrganization();
   const {tags = []} = event;
   const {data: attachments} = useFetchEventAttachments(
@@ -47,103 +31,29 @@ export function EventTagsAndScreenshot({projectSlug, event, isShare = false}: Pr
     },
     {enabled: !isShare}
   );
-  const {mutate: deleteAttachment} = useDeleteEventAttachmentOptimistic();
   const screenshots =
     attachments?.filter(({name}) => SCREENSHOT_NAMES.includes(name)) ?? [];
 
-  const [screenshotInFocus, setScreenshotInFocus] = useState<number>(0);
-
   if (!tags.length && (isShare || !screenshots.length)) {
     return null;
   }
 
   const showScreenshot = !isShare && !!screenshots.length;
-  const screenshot = screenshots[screenshotInFocus];
   const showTags = !!tags.length;
 
-  const handleDeleteScreenshot = (attachmentId: string) => {
-    deleteAttachment({
-      orgSlug: organization.slug,
-      projectSlug,
-      eventId: event.id,
-      attachmentId,
-    });
-  };
-
-  function handleOpenVisualizationModal(
-    eventAttachment: EventAttachment,
-    downloadUrl: string
-  ) {
-    trackAnalytics('issue_details.issue_tab.screenshot_modal_opened', {
-      organization,
-    });
-    function handleDelete() {
-      trackAnalytics('issue_details.issue_tab.screenshot_modal_deleted', {
-        organization,
-      });
-      handleDeleteScreenshot(eventAttachment.id);
-    }
-
-    openModal(
-      modalProps => (
-        <Modal
-          {...modalProps}
-          event={event}
-          orgSlug={organization.slug}
-          projectSlug={projectSlug}
-          eventAttachment={eventAttachment}
-          downloadUrl={downloadUrl}
-          onDelete={handleDelete}
-          onDownload={() =>
-            trackAnalytics('issue_details.issue_tab.screenshot_modal_download', {
-              organization,
-            })
-          }
-          attachments={screenshots}
-          attachmentIndex={screenshotInFocus}
-        />
-      ),
-      {modalCss}
-    );
-  }
-
-  const screenshotLink = (
-    <Link
-      to={{
-        pathname: `${location.pathname}${TabPaths[Tab.ATTACHMENTS]}`,
-        query: {...location.query, types: SCREENSHOT_TYPE},
-      }}
-    >
-      {tn('Screenshot', 'Screenshots', screenshots.length)}
-    </Link>
-  );
-
   return (
     <Wrapper showScreenshot={showScreenshot} showTags={showTags}>
-      <div>{showTags && <Tags event={event} projectSlug={projectSlug} />}</div>
+      <div>
+        {showTags && <EventTagsDataSection event={event} projectSlug={projectSlug} />}
+      </div>
       {showScreenshot && (
         <div>
           <ScreenshotWrapper>
             <StyledScreenshotDataSection
-              title={screenshotLink}
-              showPermalink={false}
-              help={t('This image was captured around the time that the event occurred.')}
-              data-test-id="screenshot-data-section"
-              type={FoldSectionKey.SCREENSHOT}
-            >
-              <Screenshot
-                organization={organization}
-                eventId={event.id}
-                projectSlug={projectSlug}
-                screenshot={screenshot}
-                onDelete={handleDeleteScreenshot}
-                onNext={() => setScreenshotInFocus(screenshotInFocus + 1)}
-                onPrevious={() => setScreenshotInFocus(screenshotInFocus - 1)}
-                screenshotInFocus={screenshotInFocus}
-                totalScreenshots={screenshots.length}
-                openVisualizationModal={handleOpenVisualizationModal}
-              />
-            </StyledScreenshotDataSection>
+              event={event}
+              isShare={isShare}
+              projectSlug={projectSlug}
+            />
           </ScreenshotWrapper>
         </div>
       )}
@@ -170,7 +80,7 @@ const Wrapper = styled(DataSection)<{
   }
 `;
 
-const StyledScreenshotDataSection = styled(InterimSection)`
+const StyledScreenshotDataSection = styled(ScreenshotDataSection)`
   h3 a {
     color: ${p => p.theme.linkColor};
   }

+ 4 - 4
static/app/components/events/eventTagsAndScreenshot/screenshot/modal.tsx

@@ -14,8 +14,10 @@ import NotAvailable from 'sentry/components/notAvailable';
 import type {CursorHandler} from 'sentry/components/pagination';
 import {t, tct} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
-import type {EventAttachment, IssueAttachment, Organization, Project} from 'sentry/types';
 import type {Event} from 'sentry/types/event';
+import type {EventAttachment, IssueAttachment} from 'sentry/types/group';
+import type {Organization} from 'sentry/types/organization';
+import type {Project} from 'sentry/types/project';
 import {defined} from 'sentry/utils';
 import {formatBytesBase2} from 'sentry/utils/bytes/formatBytesBase2';
 import getDynamicText from 'sentry/utils/getDynamicText';
@@ -42,7 +44,7 @@ type Props = ModalRenderProps & {
   pageLinks?: string | null | undefined;
 };
 
-function Modal({
+export default function ScreenshotModal({
   eventAttachment,
   orgSlug,
   projectSlug,
@@ -234,8 +236,6 @@ function Modal({
   );
 }
 
-export default Modal;
-
 const StyledHeaderWrapper = styled('div')<{hasPagination: boolean}>`
   ${p =>
     p.hasPagination &&

+ 150 - 0
static/app/components/events/eventTagsAndScreenshot/screenshot/screenshotDataSection.tsx

@@ -0,0 +1,150 @@
+import {useState} from 'react';
+
+import {
+  useDeleteEventAttachmentOptimistic,
+  useFetchEventAttachments,
+} from 'sentry/actionCreators/events';
+import {openModal} from 'sentry/actionCreators/modal';
+import {LinkButton} from 'sentry/components/button';
+import Screenshot from 'sentry/components/events/eventTagsAndScreenshot/screenshot';
+import ScreenshotModal, {
+  modalCss,
+} from 'sentry/components/events/eventTagsAndScreenshot/screenshot/modal';
+import Link from 'sentry/components/links/link';
+import {t, tn} from 'sentry/locale';
+import type {Event} from 'sentry/types/event';
+import type {EventAttachment} from 'sentry/types/group';
+import type {Project} from 'sentry/types/project';
+import {trackAnalytics} from 'sentry/utils/analytics';
+import {useLocation} from 'sentry/utils/useLocation';
+import useOrganization from 'sentry/utils/useOrganization';
+import {SCREENSHOT_TYPE} from 'sentry/views/issueDetails/groupEventAttachments/groupEventAttachmentsFilter';
+import {FoldSectionKey} from 'sentry/views/issueDetails/streamline/foldSection';
+import {InterimSection} from 'sentry/views/issueDetails/streamline/interimSection';
+import {Tab, TabPaths} from 'sentry/views/issueDetails/types';
+import {useHasStreamlinedUI} from 'sentry/views/issueDetails/utils';
+
+const SCREENSHOT_NAMES = [
+  'screenshot.jpg',
+  'screenshot.png',
+  'screenshot-1.jpg',
+  'screenshot-1.png',
+  'screenshot-2.jpg',
+  'screenshot-2.png',
+];
+
+interface ScreenshotDataSectionProps {
+  event: Event;
+  projectSlug: Project['slug'];
+  isShare?: boolean;
+}
+
+export function ScreenshotDataSection({
+  event,
+  projectSlug,
+  isShare,
+  ...props
+}: ScreenshotDataSectionProps) {
+  const location = useLocation();
+  const organization = useOrganization();
+  const hasStreamlinedUI = useHasStreamlinedUI();
+  const {data: attachments} = useFetchEventAttachments(
+    {
+      orgSlug: organization.slug,
+      projectSlug,
+      eventId: event.id,
+    },
+    {enabled: !isShare}
+  );
+  const {mutate: deleteAttachment} = useDeleteEventAttachmentOptimistic();
+  const screenshots =
+    attachments?.filter(({name}) => SCREENSHOT_NAMES.includes(name)) ?? [];
+
+  const [screenshotInFocus, setScreenshotInFocus] = useState<number>(0);
+
+  const showScreenshot = !isShare && !!screenshots.length;
+  const screenshot = screenshots[screenshotInFocus];
+
+  const handleDeleteScreenshot = (attachmentId: string) => {
+    deleteAttachment({
+      orgSlug: organization.slug,
+      projectSlug,
+      eventId: event.id,
+      attachmentId,
+    });
+  };
+
+  function handleOpenVisualizationModal(
+    eventAttachment: EventAttachment,
+    downloadUrl: string
+  ) {
+    trackAnalytics('issue_details.issue_tab.screenshot_modal_opened', {
+      organization,
+    });
+    function handleDelete() {
+      trackAnalytics('issue_details.issue_tab.screenshot_modal_deleted', {
+        organization,
+      });
+      handleDeleteScreenshot(eventAttachment.id);
+    }
+
+    openModal(
+      modalProps => (
+        <ScreenshotModal
+          {...modalProps}
+          event={event}
+          orgSlug={organization.slug}
+          projectSlug={projectSlug}
+          eventAttachment={eventAttachment}
+          downloadUrl={downloadUrl}
+          onDelete={handleDelete}
+          onDownload={() =>
+            trackAnalytics('issue_details.issue_tab.screenshot_modal_download', {
+              organization,
+            })
+          }
+          attachments={screenshots}
+          attachmentIndex={screenshotInFocus}
+        />
+      ),
+      {modalCss}
+    );
+  }
+
+  const linkPath = {
+    pathname: `${location.pathname}${TabPaths[Tab.ATTACHMENTS]}`,
+    query: {...location.query, types: SCREENSHOT_TYPE},
+  };
+  const title = tn('Screenshot', 'Screenshots', screenshots.length);
+
+  return !showScreenshot ? null : (
+    <InterimSection
+      title={hasStreamlinedUI ? title : <Link to={linkPath}>{title}</Link>}
+      showPermalink={false}
+      help={t('This image was captured around the time that the event occurred.')}
+      data-test-id="screenshot-data-section"
+      type={FoldSectionKey.SCREENSHOT}
+      actions={
+        hasStreamlinedUI ? (
+          <LinkButton to={linkPath} size="xs">
+            {t('View All')}
+          </LinkButton>
+        ) : null
+      }
+      {...props}
+    >
+      <Screenshot
+        organization={organization}
+        eventId={event.id}
+        projectSlug={projectSlug}
+        screenshot={screenshot}
+        onDelete={handleDeleteScreenshot}
+        onNext={() => setScreenshotInFocus(screenshotInFocus + 1)}
+        onPrevious={() => setScreenshotInFocus(screenshotInFocus - 1)}
+        screenshotInFocus={screenshotInFocus}
+        totalScreenshots={screenshots.length}
+        openVisualizationModal={handleOpenVisualizationModal}
+      />
+    </InterimSection>
+  );
+}

+ 61 - 58
static/app/components/events/eventTagsAndScreenshot/tags.tsx

@@ -1,4 +1,4 @@
-import {useCallback, useMemo, useState} from 'react';
+import {forwardRef, useCallback, useMemo, useState} from 'react';
 import styled from '@emotion/styled';
 
 import ButtonBar from 'sentry/components/buttonBar';
@@ -24,68 +24,71 @@ type Props = {
   projectSlug: Project['slug'];
 };
 
-function Tags({event, projectSlug}: Props) {
-  const sentryTags = getSentryDefaultTags();
+export const EventTagsDataSection = forwardRef<HTMLElement, Props>(
+  function EventTagsDataSection({event, projectSlug}: Props, ref) {
+    const sentryTags = getSentryDefaultTags();
 
-  const [tagFilter, setTagFilter] = useState<TagFilter>(TagFilter.ALL);
-  const handleTagFilterChange = useCallback((value: TagFilter) => {
-    setTagFilter(value);
-  }, []);
-  const tags = useMemo(() => {
-    switch (tagFilter) {
-      case TagFilter.ALL:
-        return event.tags;
-      case TagFilter.CUSTOM:
-        return event.tags.filter(tag => !sentryTags.has(tag.key));
-      default:
-        return event.tags.filter(tag => TagFilterData[tagFilter].has(tag.key));
-    }
-  }, [tagFilter, event.tags, sentryTags]);
+    const [tagFilter, setTagFilter] = useState<TagFilter>(TagFilter.ALL);
+    const handleTagFilterChange = useCallback((value: TagFilter) => {
+      setTagFilter(value);
+    }, []);
+    const tags = useMemo(() => {
+      switch (tagFilter) {
+        case TagFilter.ALL:
+          return event.tags;
+        case TagFilter.CUSTOM:
+          return event.tags.filter(tag => !sentryTags.has(tag.key));
+        default:
+          return event.tags.filter(tag => TagFilterData[tagFilter].has(tag.key));
+      }
+    }, [tagFilter, event.tags, sentryTags]);
 
-  const availableFilters = useMemo(() => {
-    return Object.keys(TagFilterData).filter(filter => {
-      return event.tags.some(tag => TagFilterData[filter].has(tag.key));
-    });
-  }, [event.tags]);
+    const availableFilters = useMemo(() => {
+      return Object.keys(TagFilterData).filter(filter => {
+        return event.tags.some(tag => TagFilterData[filter].has(tag.key));
+      });
+    }, [event.tags]);
 
-  const actions = (
-    <ButtonBar gap={1}>
-      <SegmentedControl
-        size="xs"
-        aria-label={t('Filter tags')}
-        value={tagFilter}
-        onChange={handleTagFilterChange}
-      >
-        {[TagFilter.ALL, TagFilter.CUSTOM, ...availableFilters].map(v => (
-          <SegmentedControl.Item key={v}>{`${v}`}</SegmentedControl.Item>
-        ))}
-      </SegmentedControl>
-    </ButtonBar>
-  );
+    const actions = (
+      <ButtonBar gap={1}>
+        <SegmentedControl
+          size="xs"
+          aria-label={t('Filter tags')}
+          value={tagFilter}
+          onChange={handleTagFilterChange}
+        >
+          {[TagFilter.ALL, TagFilter.CUSTOM, ...availableFilters].map(v => (
+            <SegmentedControl.Item key={v}>{`${v}`}</SegmentedControl.Item>
+          ))}
+        </SegmentedControl>
+      </ButtonBar>
+    );
 
-  return (
-    <StyledEventDataSection
-      title={t('Tags')}
-      help={tct('The searchable tags associated with this event. [link:Learn more]', {
-        link: <ExternalLink openInNewTab href={TAGS_DOCS_LINK} />,
-      })}
-      isHelpHoverable
-      actions={actions}
-      data-test-id="event-tags"
-      guideTarget="tags"
-      type={FoldSectionKey.TAGS}
-    >
-      <EventTags
-        event={event}
-        projectSlug={projectSlug}
-        tagFilter={tagFilter}
-        filteredTags={tags ?? []}
-      />
-    </StyledEventDataSection>
-  );
-}
+    return (
+      <StyledEventDataSection
+        title={t('Tags')}
+        help={tct('The searchable tags associated with this event. [link:Learn more]', {
+          link: <ExternalLink openInNewTab href={TAGS_DOCS_LINK} />,
+        })}
+        isHelpHoverable
+        actions={actions}
+        data-test-id="event-tags"
+        guideTarget="tags"
+        type={FoldSectionKey.TAGS}
+        ref={ref}
+      >
+        <EventTags
+          event={event}
+          projectSlug={projectSlug}
+          tagFilter={tagFilter}
+          filteredTags={tags ?? []}
+        />
+      </StyledEventDataSection>
+    );
+  }
+);
 
-export default Tags;
+export default EventTagsDataSection;
 
 const StyledEventDataSection = styled(InterimSection)`
   padding: ${space(0.5)} ${space(2)} ${space(1)};

+ 14 - 4
static/app/components/events/groupingInfo/index.tsx

@@ -6,14 +6,16 @@ import LoadingError from 'sentry/components/loadingError';
 import LoadingIndicator from 'sentry/components/loadingIndicator';
 import {t} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
-import {EventGroupVariantType, IssueCategory} from 'sentry/types';
 import type {Event, EventGroupVariant} from 'sentry/types/event';
+import {EventGroupVariantType} from 'sentry/types/event';
 import type {Group} from 'sentry/types/group';
+import {IssueCategory} from 'sentry/types/group';
 import {useApiQuery} from 'sentry/utils/queryClient';
 import useOrganization from 'sentry/utils/useOrganization';
 import SectionToggleButton from 'sentry/views/issueDetails/sectionToggleButton';
 import {FoldSectionKey} from 'sentry/views/issueDetails/streamline/foldSection';
 import {InterimSection} from 'sentry/views/issueDetails/streamline/interimSection';
+import {useHasStreamlinedUI} from 'sentry/views/issueDetails/utils';
 
 import GroupingConfigSelect from './groupingConfigSelect';
 import GroupVariant from './groupingVariant';
@@ -119,6 +121,7 @@ export function EventGroupingInfo({
 }: GroupingInfoProps) {
   const organization = useOrganization();
   const [isOpen, setIsOpen] = useState(false);
+  const hasStreamlinedUI = useHasStreamlinedUI();
   const [configOverride, setConfigOverride] = useState<string | null>(null);
 
   const hasPerformanceGrouping =
@@ -148,15 +151,22 @@ export function EventGroupingInfo({
       )
     : [];
 
+  const openState = hasStreamlinedUI ? true : isOpen;
+
   return (
     <InterimSection
       title={t('Event Grouping Information')}
-      actions={<SectionToggleButton isExpanded={isOpen} onExpandChange={setIsOpen} />}
+      actions={
+        hasStreamlinedUI ? null : (
+          <SectionToggleButton isExpanded={isOpen} onExpandChange={setIsOpen} />
+        )
+      }
       type={FoldSectionKey.GROUPING_INFO}
     >
-      {!isOpen ? <GroupInfoSummary groupInfo={groupInfo} /> : null}
-      {isOpen ? (
+      {!openState ? <GroupInfoSummary groupInfo={groupInfo} /> : null}
+      {openState ? (
         <Fragment>
+          {hasStreamlinedUI ? <GroupInfoSummary groupInfo={groupInfo} /> : null}
           <ConfigHeader>
             <div>
               {showGroupingConfig && (

+ 2 - 2
static/app/components/events/interfaces/debugMeta/index.tsx

@@ -396,7 +396,7 @@ export function DebugMeta({data, projectSlug, groupId, event}: DebugMetaProps) {
       )}
       actions={actions}
     >
-      {!isOpen ? null : (
+      {isOpen || hasStreamlinedUI ? (
         <Fragment>
           <StyledSearchBarAction
             placeholder={t('Search images loaded')}
@@ -420,7 +420,7 @@ export function DebugMeta({data, projectSlug, groupId, event}: DebugMetaProps) {
             <div ref={panelTableRef}>{renderList(filteredImages)}</div>
           </StyledPanelTable>
         </Fragment>
-      )}
+      ) : null}
     </InterimSection>
   );
 }

+ 4 - 1
static/app/components/events/interfaces/request/index.tsx

@@ -18,6 +18,7 @@ import {defined} from 'sentry/utils';
 import {isUrl} from 'sentry/utils/string/isUrl';
 import {FoldSectionKey} from 'sentry/views/issueDetails/streamline/foldSection';
 import {InterimSection} from 'sentry/views/issueDetails/streamline/interimSection';
+import {useHasStreamlinedUI} from 'sentry/views/issueDetails/utils';
 
 import {RichHttpContentClippedBoxBodySection} from './richHttpContentClippedBoxBodySection';
 import {RichHttpContentClippedBoxKeyValueList} from './richHttpContentClippedBoxKeyValueList';
@@ -52,6 +53,7 @@ function RequestBodySection({data, event, meta}: RequestBodyProps) {
 }
 
 export function Request({data, event}: RequestProps) {
+  const hasStreamlinedUI = useHasStreamlinedUI();
   const entryIndex = event.entries.findIndex(entry => entry.type === EntryType.REQUEST);
   const meta = event._meta?.entries?.[entryIndex]?.data;
 
@@ -110,10 +112,11 @@ export function Request({data, event}: RequestProps) {
   return (
     <InterimSection
       type={FoldSectionKey.REQUEST}
-      title={title}
+      title={hasStreamlinedUI ? t('HTTP Request') : title}
       actions={actions}
       className="request"
     >
+      {hasStreamlinedUI && title}
       {view === 'curl' ? (
         <CodeSnippet language="bash">{getCurlCommand(data)}</CodeSnippet>
       ) : (

+ 14 - 1
static/app/components/events/viewHierarchy/index.tsx

@@ -13,6 +13,7 @@ import type {UseVirtualizedTreeProps} from 'sentry/utils/profiling/hooks/useVirt
 import {useVirtualizedTree} from 'sentry/utils/profiling/hooks/useVirtualizedTree/useVirtualizedTree';
 import type {VirtualizedTreeRenderedRow} from 'sentry/utils/profiling/hooks/useVirtualizedTree/virtualizedTreeUtils';
 import useOrganization from 'sentry/utils/useOrganization';
+import {useHasStreamlinedUI} from 'sentry/views/issueDetails/utils';
 
 import {DetailsPanel} from './detailsPanel';
 import {RenderingSystem} from './renderingSystem';
@@ -78,6 +79,7 @@ type ViewHierarchyProps = {
 
 function ViewHierarchy({viewHierarchy, project}: ViewHierarchyProps) {
   const organization = useOrganization();
+  const hasStreamlinedUI = useHasStreamlinedUI();
   const [scrollContainerRef, setScrollContainerRef] = useState<HTMLDivElement | null>(
     null
   );
@@ -188,7 +190,7 @@ function ViewHierarchy({viewHierarchy, project}: ViewHierarchyProps) {
     );
   }
 
-  return (
+  const viewHierarchyContent = (
     <Fragment>
       <RenderingSystem
         platform={project?.platform}
@@ -224,10 +226,21 @@ function ViewHierarchy({viewHierarchy, project}: ViewHierarchyProps) {
       </Content>
     </Fragment>
   );
+
+  return hasStreamlinedUI ? (
+    <Container>{viewHierarchyContent}</Container>
+  ) : (
+    viewHierarchyContent
+  );
 }
 
 export {ViewHierarchy};
 
+const Container = styled('div')`
+  position: relative;
+  margin-left: ${space(2)};
+`;
+
 const Content = styled('div')`
   display: flex;
   flex-direction: row;

+ 0 - 342
static/app/views/issueDetails/eventNavigation.tsx

@@ -1,342 +0,0 @@
-import styled from '@emotion/styled';
-import omit from 'lodash/omit';
-
-import {Button, LinkButton} from 'sentry/components/button';
-import ButtonBar from 'sentry/components/buttonBar';
-import {Chevron} from 'sentry/components/chevron';
-import {DropdownMenu} from 'sentry/components/dropdownMenu';
-import {TabList, Tabs} from 'sentry/components/tabs';
-import TimeSince from 'sentry/components/timeSince';
-import {IconChevron, IconCopy} from 'sentry/icons';
-import {t} from 'sentry/locale';
-import {space} from 'sentry/styles/space';
-import type {Event} from 'sentry/types/event';
-import type {Group} from 'sentry/types/group';
-import {defined} from 'sentry/utils';
-import {trackAnalytics} from 'sentry/utils/analytics';
-import {
-  getAnalyticsDataForEvent,
-  getAnalyticsDataForGroup,
-  getShortEventId,
-} from 'sentry/utils/events';
-import {getReplayIdFromEvent} from 'sentry/utils/replays/getReplayIdFromEvent';
-import useCopyToClipboard from 'sentry/utils/useCopyToClipboard';
-import {useLocation} from 'sentry/utils/useLocation';
-import useOrganization from 'sentry/utils/useOrganization';
-import {normalizeUrl} from 'sentry/utils/withDomainRequired';
-
-type EventNavigationProps = {
-  event: Event;
-  group: Group;
-};
-
-type SectionDefinition = {
-  condition: (event: Event) => boolean;
-  label: string;
-  section: string;
-};
-
-enum EventNavOptions {
-  RECOMMENDED = 'recommended',
-  LATEST = 'latest',
-  OLDEST = 'oldest',
-}
-
-const EventNavLabels = {
-  [EventNavOptions.RECOMMENDED]: t('Recommended Event'),
-  [EventNavOptions.OLDEST]: t('First Event'),
-  [EventNavOptions.LATEST]: t('Last Event'),
-};
-
-const eventDataSections: SectionDefinition[] = [
-  {section: 'event-highlights', label: t('Event Highlights'), condition: () => true},
-  {
-    section: 'stacktrace',
-    label: t('Stack Trace'),
-    condition: (event: Event) => event.entries.some(entry => entry.type === 'stacktrace'),
-  },
-  {
-    section: 'exception',
-    label: t('Exception'),
-    condition: (event: Event) => event.entries.some(entry => entry.type === 'exception'),
-  },
-  {
-    section: 'breadcrumbs',
-    label: t('Breadcrumbs'),
-    condition: (event: Event) =>
-      event.entries.some(entry => entry.type === 'breadcrumbs'),
-  },
-  {section: 'tags', label: t('Tags'), condition: (event: Event) => event.tags.length > 0},
-  {section: 'context', label: t('Context'), condition: (event: Event) => !!event.context},
-  {
-    section: 'user-feedback',
-    label: t('User Feedback'),
-    condition: (event: Event) => !!event.userReport,
-  },
-  {
-    section: 'replay',
-    label: t('Replay'),
-    condition: (event: Event) => !!getReplayIdFromEvent(event),
-  },
-];
-
-export default function EventNavigation({event, group}: EventNavigationProps) {
-  const location = useLocation();
-  const organization = useOrganization();
-
-  const hasPreviousEvent = defined(event.previousEventID);
-  const hasNextEvent = defined(event.nextEventID);
-
-  const baseEventsPath = `/organizations/${organization.slug}/issues/${group.id}/events/`;
-
-  const jumpToSections = eventDataSections.filter(eventSection =>
-    eventSection.condition(event)
-  );
-
-  const downloadJson = () => {
-    const host = organization.links.regionUrl;
-    const jsonUrl = `${host}/api/0/projects/${organization.slug}/${group.project.slug}/events/${event.id}/json/`;
-    window.open(jsonUrl);
-    trackAnalytics('issue_details.event_json_clicked', {
-      organization,
-      group_id: parseInt(`${event.groupID}`, 10),
-    });
-  };
-
-  const {onClick: copyLink} = useCopyToClipboard({
-    successMessage: t('Event URL copied to clipboard'),
-    text: window.location.origin + normalizeUrl(`${baseEventsPath}${event.id}/`),
-    onCopy: () =>
-      trackAnalytics('issue_details.copy_event_link_clicked', {
-        organization,
-        ...getAnalyticsDataForGroup(group),
-        ...getAnalyticsDataForEvent(event),
-      }),
-  });
-
-  const {onClick: copyEventId} = useCopyToClipboard({
-    successMessage: t('Event ID copied to clipboard'),
-    text: event.id,
-  });
-
-  return (
-    <div>
-      <EventNavigationWrapper>
-        <Tabs>
-          <TabList hideBorder variant="floating">
-            {Object.keys(EventNavLabels).map(label => {
-              return (
-                <TabList.Item
-                  to={{
-                    pathname: normalizeUrl(baseEventsPath + label + '/'),
-                    query: {...location.query, referrer: `${label}-event`},
-                  }}
-                  key={label}
-                >
-                  {EventNavLabels[label]}
-                </TabList.Item>
-              );
-            })}
-          </TabList>
-        </Tabs>
-        <NavigationWrapper>
-          <Navigation>
-            <LinkButton
-              title={'Previous Event'}
-              aria-label="Previous Event"
-              borderless
-              size="sm"
-              icon={<IconChevron direction="left" />}
-              disabled={!hasPreviousEvent}
-              to={{
-                pathname: `${baseEventsPath}${event.previousEventID}/`,
-                query: {...location.query, referrer: 'previous-event'},
-              }}
-            />
-            <LinkButton
-              title={'Next Event'}
-              aria-label="Next Event"
-              borderless
-              size="sm"
-              icon={<IconChevron direction="right" />}
-              disabled={!hasNextEvent}
-              to={{
-                pathname: `${baseEventsPath}${event.nextEventID}/`,
-                query: {...location.query, referrer: 'next-event'},
-              }}
-            />
-          </Navigation>
-          <LinkButton
-            to={{
-              pathname: normalizeUrl(
-                `/organizations/${organization.slug}/issues/${group.id}/events/`
-              ),
-              query: omit(location.query, 'query'),
-            }}
-            borderless
-            size="sm"
-          >
-            {t('View All Events')}
-          </LinkButton>
-        </NavigationWrapper>
-      </EventNavigationWrapper>
-      <Divider />
-      <EventInfoJumpToWrapper>
-        <EventInfo>
-          <EventIdInfo>
-            <EventTitle>{t('Event')}</EventTitle>
-            <Button
-              aria-label={t('Copy')}
-              borderless
-              onClick={copyEventId}
-              size="zero"
-              title={event.id}
-              tooltipProps={{overlayStyle: {maxWidth: 'max-content'}}}
-              translucentBorder
-            >
-              <EventId>
-                {getShortEventId(event.id)}
-                <CopyIconContainer>
-                  <IconCopy size="xs" />
-                </CopyIconContainer>
-              </EventId>
-            </Button>
-            <DropdownMenu
-              triggerProps={{
-                'aria-label': t('Event actions'),
-                icon: <Chevron direction="down" />,
-                size: 'zero',
-                borderless: true,
-                showChevron: false,
-              }}
-              position="bottom"
-              size="xs"
-              items={[
-                {
-                  key: 'copy-event-id',
-                  label: t('Copy Event ID'),
-                  onAction: copyEventId,
-                },
-                {
-                  key: 'copy-event-link',
-                  label: t('Copy Event Link'),
-                  onAction: copyLink,
-                },
-                {
-                  key: 'view-json',
-                  label: t('View JSON'),
-                  onAction: downloadJson,
-                },
-              ]}
-            />
-          </EventIdInfo>
-          <TimeSince date={event.dateCreated ?? event.dateReceived} />
-        </EventInfo>
-        <JumpTo>
-          <div>{t('Jump to:')}</div>
-          <ButtonBar>
-            {jumpToSections.map(jump => (
-              <StyledButton
-                key={jump.section}
-                onClick={() => {
-                  document
-                    .getElementById(jump.section)
-                    ?.scrollIntoView({behavior: 'smooth'});
-                }}
-                borderless
-                size="sm"
-              >
-                {jump.label}
-              </StyledButton>
-            ))}
-          </ButtonBar>
-        </JumpTo>
-      </EventInfoJumpToWrapper>
-    </div>
-  );
-}
-
-const EventNavigationWrapper = styled('div')`
-  display: flex;
-  justify-content: space-between;
-`;
-
-const NavigationWrapper = styled('div')`
-  display: flex;
-`;
-
-const Navigation = styled('div')`
-  display: flex;
-  border-right: 1px solid ${p => p.theme.gray100};
-`;
-
-const EventInfoJumpToWrapper = styled('div')`
-  display: flex;
-  gap: ${space(1)};
-  flex-direction: row;
-  justify-content: space-between;
-  align-items: center;
-`;
-
-const EventInfo = styled('div')`
-  display: flex;
-  gap: ${space(1)};
-  flex-direction: row;
-  align-items: center;
-`;
-
-const JumpTo = styled('div')`
-  display: flex;
-  gap: ${space(1)};
-  flex-direction: row;
-  align-items: center;
-  color: ${p => p.theme.gray300};
-`;
-
-const Divider = styled('hr')`
-  height: 1px;
-  width: 100%;
-  background: ${p => p.theme.border};
-  border: none;
-  margin-top: ${space(1)};
-  margin-bottom: ${space(1)};
-`;
-
-const EventIdInfo = styled('span')`
-  display: flex;
-  align-items: center;
-  gap: ${space(0.25)};
-`;
-
-const EventId = styled('span')`
-  position: relative;
-  font-weight: ${p => p.theme.fontWeightBold};
-  font-size: ${p => p.theme.fontSizeLarge};
-  text-decoration: underline;
-  text-decoration-color: ${p => p.theme.gray200};
-  &:hover {
-    > span {
-      display: flex;
-    }
-  }
-`;
-
-const StyledButton = styled(Button)`
-  color: ${p => p.theme.gray300};
-`;
-
-const CopyIconContainer = styled('span')`
-  display: none;
-  align-items: center;
-  padding: ${space(0.25)};
-  background: ${p => p.theme.background};
-  position: absolute;
-  right: 0;
-  top: 50%;
-  transform: translateY(-50%);
-`;
-
-const EventTitle = styled('div')`
-  font-weight: ${p => p.theme.fontWeightBold};
-  font-size: ${p => p.theme.fontSizeLarge};
-`;

Some files were not shown because too many files changed in this diff