Просмотр исходного кода

feat(replay): Implement more Replay Details layouts for feedback (#38788)

The main one that's new is the `top` design from
https://github.com/getsentry/sentry/issues/37202

**"Top" (could be the new default & only layout)**
<img width="1312" alt="Screen Shot 2022-09-14 at 9 56 25 AM"
src="https://user-images.githubusercontent.com/187460/190216453-71a1d4e7-7885-4d86-a85c-b99bdd4a02ed.png">

**The original 'left' and 'right' layouts are also tweaked:**
the sidebar tabs are moved down below the video, and margin under all
tabs is reduced
<img width="1312" alt="Screen Shot 2022-09-14 at 11 49 33 AM"
src="https://user-images.githubusercontent.com/187460/190238252-269b64b4-175e-439a-9c34-84a1953ec491.png">



Also added are 2 more: no-video and only-video. They'd be cool when/if
two pages could talk to each other (and if we could hide the sentry
sidebar).

But also, I think I'd like the video-only layout during DEX to make it
easier to demo a 'full pii' capture. Not super critical though in any
way.

**No Video**
<img width="1312" alt="Screen Shot 2022-09-14 at 9 56 27 AM"
src="https://user-images.githubusercontent.com/187460/190216529-85a8adf6-252e-40fd-be6f-7027c8b049bc.png">

**Only Video**
<img width="1312" alt="Screen Shot 2022-09-14 at 9 56 34 AM"
src="https://user-images.githubusercontent.com/187460/190216545-662737ec-ff70-4a29-a26c-fc879ff871b4.png">


Fixes #37202
Ryan Albrecht 2 лет назад
Родитель
Сommit
3cb10faa1a

+ 41 - 2
static/app/utils/replays/hooks/useReplayLayout.tsx

@@ -8,6 +8,45 @@ import useUrlParams from 'sentry/utils/useUrlParams';
 import {getDefaultLayout} from 'sentry/views/replays/detail/layout/utils';
 
 export enum LayoutKey {
+  /**
+   * ### Top
+   *┌────────────────────┐
+   *│ Timeline           │
+   *├───────────┬────────┤
+   *│ Video     > Crumbs │
+   *│           >        │
+   *├^^^^^^^^^^^>        |
+   *│ Details   >        │
+   *│           >        │
+   *└───────────┴────────┘
+   */
+  top = 'top',
+  /**
+   * ### Top
+   *┌────────────────────┐
+   *│ Timeline           │
+   *├───────────┬────────┤
+   *│ Details   > Crumbs │
+   *│           >        │
+   *│           >        |
+   *│           >        │
+   *│           >        │
+   *└───────────┴────────┘
+   */
+  no_video = 'no_video',
+  /**
+   * ### Video Only
+   *┌────────────────────┐
+   *│ Timeline           │
+   *├────────────────────┤
+   *│                    │
+   *│                    |
+   *│       Video        │
+   *│                    │
+   *│                    │
+   *└────────────────────┘
+   */
+  video_only = 'video_only',
   /**
    * ### Topbar
    *┌────────────────────┐
@@ -30,7 +69,7 @@ export enum LayoutKey {
    * │        >          │
    * │^^^^^^^ >          |
    * │ Crumbs >          │
-   * │        >          │
+   * │ Tabs   >          │
    * └────────┴──────────┘
    */
   sidebar_left = 'sidebar_left',
@@ -43,7 +82,7 @@ export enum LayoutKey {
    * │          >        │
    * │          >^^^^^^^^┤
    * │          > Crumbs │
-   * │          >     
+   * │          > Tabs
    * └──────────┴────────┘
    */
   sidebar_right = 'sidebar_right',

+ 5 - 3
static/app/views/replays/detail/breadcrumbs/index.tsx

@@ -25,9 +25,11 @@ function CrumbPlaceholder({number}: {number: number}) {
   );
 }
 
-type Props = {};
+type Props = {
+  showTitle: boolean;
+};
 
-function Breadcrumbs({}: Props) {
+function Breadcrumbs({showTitle = true}: Props) {
   const {currentHoverTime, currentTime, replay} = useReplayContext();
 
   const replayRecord = replay?.getReplay();
@@ -85,7 +87,7 @@ function Breadcrumbs({}: Props) {
     <Panel>
       <FluidPanel
         bodyRef={crumbListContainerRef}
-        title={<PanelHeader>{t('Breadcrumbs')}</PanelHeader>}
+        title={showTitle ? <PanelHeader>{t('Breadcrumbs')}</PanelHeader> : undefined}
       >
         {content}
       </FluidPanel>

+ 3 - 3
static/app/views/replays/detail/focusTabs.tsx

@@ -15,15 +15,15 @@ const ReplayTabs: Record<TabKey, string> = {
   memory: t('Memory'),
 };
 
-type Props = {};
+type Props = {className?: string};
 
-function FocusTabs({}: Props) {
+function FocusTabs({className}: Props) {
   const {pathname, query} = useLocation();
   const {getActiveTab, setActiveTab} = useActiveReplayTab();
   const activeTab = getActiveTab();
 
   return (
-    <NavTabs underlined>
+    <NavTabs underlined className={className}>
       {Object.entries(ReplayTabs).map(([tab, label]) => (
         <li key={tab} className={activeTab === tab ? 'active' : ''}>
           <a

+ 19 - 18
static/app/views/replays/detail/layout/chooseLayout.tsx

@@ -1,47 +1,48 @@
+import {Fragment} from 'react';
+import styled from '@emotion/styled';
+
 import CompactSelect from 'sentry/components/forms/compactSelect';
-import {IconPanel} from 'sentry/icons';
 import {t} from 'sentry/locale';
+import space from 'sentry/styles/space';
 import useReplayLayout, {LayoutKey} from 'sentry/utils/replays/hooks/useReplayLayout';
 
 const layoutToLabel: Record<LayoutKey, string> = {
   topbar: t('Player Top'),
   sidebar_left: t('Player Left'),
   sidebar_right: t('Player Right'),
+  top: t('Top'),
+  no_video: t('Data Only'),
+  video_only: t('Video Only'),
 };
 
-const layoutToDir: Record<LayoutKey, string> = {
-  topbar: 'up',
-  sidebar_left: 'left',
-  sidebar_right: 'right',
-};
-
-function getLayoutIcon(layout: string) {
-  const dir = layout in layoutToDir ? layoutToDir[layout] : 'up';
-  return <IconPanel size="sm" direction={dir} />;
-}
-
 type Props = {};
 
 function ChooseLayout({}: Props) {
   const {getLayout, setLayout} = useReplayLayout();
 
+  const currentLabel = layoutToLabel[getLayout()];
   return (
     <CompactSelect
-      triggerProps={{
-        size: 'xs',
-        icon: getLayoutIcon(getLayout()),
-      }}
-      triggerLabel=""
+      triggerProps={{size: 'xs'}}
+      triggerLabel={
+        <Fragment>
+          Page Layout: <Current>{currentLabel}</Current>
+        </Fragment>
+      }
       value={getLayout()}
       placement="bottom right"
       onChange={opt => setLayout(opt?.value)}
       options={Object.entries(layoutToLabel).map(([value, label]) => ({
         value,
         label,
-        leadingItems: getLayoutIcon(value),
       }))}
     />
   );
 }
 
+const Current = styled('span')`
+  font-weight: normal;
+  padding-left: ${space(0.5)};
+`;
+
 export default ChooseLayout;

+ 117 - 54
static/app/views/replays/detail/layout/index.tsx

@@ -1,4 +1,3 @@
-import {Fragment} from 'react';
 import styled from '@emotion/styled';
 
 import ErrorBoundary from 'sentry/components/errorBoundary';
@@ -19,53 +18,118 @@ import TagPanel from 'sentry/views/replays/detail/tagPanel';
 
 const MIN_VIDEO_WIDTH = {px: 325};
 const MIN_CONTENT_WIDTH = {px: 325};
+const MIN_SIDEBAR_WIDTH = {px: 325};
 const MIN_VIDEO_HEIGHT = {px: 200};
 const MIN_CONTENT_HEIGHT = {px: 200};
-const MIN_CRUMBS_HEIGHT = {px: 200};
 
 type Props = {
   layout?: LayoutKey;
-  showCrumbs?: boolean;
-  showTimeline?: boolean;
-  showVideo?: boolean;
 };
 
-function ReplayLayout({
-  layout = LayoutKey.topbar,
-  showCrumbs = true,
-  showTimeline = true,
-  showVideo = true,
-}: Props) {
+function ReplayLayout({layout = LayoutKey.topbar}: Props) {
   const {ref: fullscreenRef, toggle: toggleFullscreen} = useFullscreen();
 
-  const timeline = showTimeline ? (
+  const timeline = (
     <ErrorBoundary mini>
       <ReplayTimeline />
     </ErrorBoundary>
-  ) : null;
+  );
 
-  const video = showVideo ? (
+  const video = (
     <VideoSection ref={fullscreenRef}>
       <ErrorBoundary mini>
         <ReplayView toggleFullscreen={toggleFullscreen} />
       </ErrorBoundary>
     </VideoSection>
-  ) : null;
+  );
 
-  const crumbs = showCrumbs ? (
-    <ErrorBoundary mini>
-      <Breadcrumbs />
-    </ErrorBoundary>
-  ) : null;
+  if (layout === 'video_only') {
+    return (
+      <BodyContent>
+        {timeline}
+        {video}
+      </BodyContent>
+    );
+  }
 
-  const content = (
+  const focusArea = (
     <ErrorBoundary mini>
-      <FluidPanel title={<FocusTabs />}>
+      <FluidPanel title={<SmallMarginFocusTabs />}>
         <FocusArea />
       </FluidPanel>
     </ErrorBoundary>
   );
 
+  if (layout === 'no_video') {
+    return (
+      <BodyContent>
+        {timeline}
+        <SplitPanel
+          key={layout}
+          left={{
+            content: focusArea,
+            default: '75%',
+            min: MIN_CONTENT_WIDTH,
+          }}
+          right={{
+            content: <SideCrumbsTags />,
+            min: MIN_SIDEBAR_WIDTH,
+          }}
+        />
+      </BodyContent>
+    );
+  }
+
+  if (layout === 'top') {
+    const mainSplit = (
+      <SplitPanel
+        key={layout + '_main'}
+        top={{
+          content: video,
+          default: '50%',
+          min: MIN_VIDEO_HEIGHT,
+        }}
+        bottom={{
+          content: focusArea,
+          min: MIN_CONTENT_HEIGHT,
+        }}
+      />
+    );
+
+    return (
+      <BodyContent>
+        {timeline}
+        <SplitPanel
+          key={layout}
+          left={{
+            content: mainSplit,
+            default: '75%',
+            min: MIN_CONTENT_WIDTH,
+          }}
+          right={{
+            content: <SideCrumbsTags />,
+            min: MIN_SIDEBAR_WIDTH,
+          }}
+        />
+      </BodyContent>
+    );
+  }
+
+  const sideVideoCrumbs = (
+    <SplitPanel
+      key={layout}
+      top={{
+        content: video,
+        default: '50%',
+        min: MIN_CONTENT_WIDTH,
+      }}
+      bottom={{
+        content: <SideCrumbsTags />,
+        min: MIN_SIDEBAR_WIDTH,
+      }}
+    />
+  );
+
   if (layout === 'sidebar_right') {
     return (
       <BodyContent>
@@ -73,13 +137,13 @@ function ReplayLayout({
         <SplitPanel
           key={layout}
           left={{
-            content,
+            content: focusArea,
             default: '60%',
             min: MIN_CONTENT_WIDTH,
           }}
           right={{
-            content: <SidebarContent video={video} crumbs={crumbs} />,
-            min: MIN_VIDEO_WIDTH,
+            content: sideVideoCrumbs,
+            min: MIN_SIDEBAR_WIDTH,
           }}
         />
       </BodyContent>
@@ -93,11 +157,11 @@ function ReplayLayout({
         <SplitPanel
           key={layout}
           left={{
-            content: <SidebarContent video={video} crumbs={crumbs} />,
-            min: MIN_VIDEO_WIDTH,
+            content: sideVideoCrumbs,
+            min: MIN_SIDEBAR_WIDTH,
           }}
           right={{
-            content,
+            content: focusArea,
             default: '60%',
             min: MIN_CONTENT_WIDTH,
           }}
@@ -107,6 +171,12 @@ function ReplayLayout({
   }
 
   // layout === 'topbar' or default
+  const crumbsWithTitle = (
+    <ErrorBoundary mini>
+      <Breadcrumbs showTitle />
+    </ErrorBoundary>
+  );
+
   return (
     <BodyContent>
       {timeline}
@@ -121,7 +191,7 @@ function ReplayLayout({
                 min: MIN_VIDEO_WIDTH,
               }}
               right={{
-                content: crumbs,
+                content: crumbsWithTitle,
               }}
             />
           ),
@@ -129,7 +199,7 @@ function ReplayLayout({
           min: MIN_VIDEO_HEIGHT,
         }}
         bottom={{
-          content,
+          content: focusArea,
           default: '60%',
           min: MIN_CONTENT_HEIGHT,
         }}
@@ -138,37 +208,23 @@ function ReplayLayout({
   );
 }
 
-function SidebarContent({video, crumbs}) {
-  const {getParamValue} = useUrlParams('t_side', 'video');
+function SideCrumbsTags() {
+  const {getParamValue} = useUrlParams('t_side', 'crumbs');
+  const sideTabs = <SmallMarginSideTabs />;
   if (getParamValue() === 'tags') {
     return (
-      <FluidPanel title={<SideTabs />}>
+      <FluidPanel title={sideTabs}>
         <TagPanel />
       </FluidPanel>
     );
   }
-  if (video && crumbs) {
-    return (
-      <FluidPanel title={<SideTabs />}>
-        <SplitPanel
-          top={{
-            content: video,
-            default: '55%',
-            min: MIN_VIDEO_HEIGHT,
-          }}
-          bottom={{
-            content: crumbs,
-            min: MIN_CRUMBS_HEIGHT,
-          }}
-        />
-      </FluidPanel>
-    );
-  }
+
   return (
-    <Fragment>
-      {video}
-      {crumbs}
-    </Fragment>
+    <FluidPanel title={sideTabs}>
+      <ErrorBoundary mini>
+        <Breadcrumbs showTitle={false} />
+      </ErrorBoundary>
+    </FluidPanel>
   );
 }
 
@@ -182,6 +238,13 @@ const BodyContent = styled('main')`
   padding: ${space(2)};
 `;
 
+const SmallMarginFocusTabs = styled(FocusTabs)`
+  margin-bottom: ${space(1)};
+`;
+const SmallMarginSideTabs = styled(SideTabs)`
+  margin-bottom: ${space(1)};
+`;
+
 const VideoSection = styled(FluidHeight)`
   height: 100%;
 

+ 1 - 1
static/app/views/replays/detail/page.tsx

@@ -32,8 +32,8 @@ function Page({children, crumbs, orgSlug, replayRecord}: Props) {
         <DetailsPageBreadcrumbs orgSlug={orgSlug} replayRecord={replayRecord} />
       </HeaderContent>
       <ButtonActionsWrapper>
-        <FeatureFeedback featureName="replay" buttonProps={{size: 'xs'}} />
         <ChooseLayout />
+        <FeatureFeedback featureName="replay" buttonProps={{size: 'xs'}} />
       </ButtonActionsWrapper>
 
       {replayRecord && crumbs ? (

+ 8 - 6
static/app/views/replays/detail/sideTabs.tsx

@@ -2,19 +2,21 @@ import NavTabs from 'sentry/components/navTabs';
 import {t} from 'sentry/locale';
 import useUrlParams from 'sentry/utils/useUrlParams';
 
-type Props = {};
-
 const TABS = {
-  video: t('Replay'),
+  crumbs: t('Breadcrumbs'),
   tags: t('Tags'),
 };
 
-function SideTabs({}: Props) {
-  const {getParamValue, setParamValue} = useUrlParams('t_side', 'video');
+type Props = {
+  className?: string;
+};
+
+function SideTabs({className}: Props) {
+  const {getParamValue, setParamValue} = useUrlParams('t_side', 'crumbs');
   const active = getParamValue();
 
   return (
-    <NavTabs underlined>
+    <NavTabs underlined className={className}>
       {Object.entries(TABS).map(([tab, label]) => {
         return (
           <li key={tab} className={active === tab ? 'active' : ''}>