Repository: getsentry/sentry
Pull request: feat(replays): highlight console based on scrubber time
Closes #34844 
Added row highlights to the console screen so you can tell what logs are relevant based on hovered scrubber position.
https://user-images.githubusercontent.com/7014871/170291923-07cfaa7a-a7c9-4ed5-b312-7dd6b601796a.mov




<!--

  Sentry employees and contractors can delete or ignore the following.

-->

### Legal Boilerplate

Look, I get it. The entity doing business as "Sentry" was incorporated in the State of Delaware in 2015 as Functional Software, Inc. and is gonna need some rights from me in order to utilize my contributions in this here PR. So here's the deal: I retain all rights, title and interest in and to my contributions, and by keeping this boilerplate intact I confirm that Sentry can use, modify, copy, and redistribute my contributions, under Sentry's choice of terms.


Problem statement:
Console should highlight the most-recent row based on the current scrubber `hoverTime`
Like with the chapters component: when the scrubber is hovered we should highlight a row in the console that matches the hovered timestamp.

If there is no console message matching that exact timestamp, then we should highlight only console messages that are previous to the hovered-timestamp. 
ie: if you have console messages `A @ 0:10` and `B @ 0:20` and the hover position is at `0:19`, then the message `A @ 0:10` should be highlighted because it's the most-recent message before the timestamp `0:19`.

Hints from issue discussion:

Commit message: Merge branch 'master' into feat/replays/34844/console-row-highlight-impl

Patch to review:
diff --git a/static/app/views/replays/detail/console/consoleMessage.tsx b/static/app/views/replays/detail/console/consoleMessage.tsx
index 3f86d355201ba5..7c38a6d6fc77df 100644
--- a/static/app/views/replays/detail/console/consoleMessage.tsx
+++ b/static/app/views/replays/detail/console/consoleMessage.tsx
@@ -66,10 +66,16 @@ function MessageFormatter({breadcrumb}: MessageFormatterProps) {
 }
 
 interface ConsoleMessageProps extends MessageFormatterProps {
+  isActive: boolean;
   isLast: boolean;
   startTimestamp: number;
 }
-function ConsoleMessage({breadcrumb, isLast, startTimestamp = 0}: ConsoleMessageProps) {
+function ConsoleMessage({
+  breadcrumb,
+  isActive = false,
+  isLast,
+  startTimestamp = 0,
+}: ConsoleMessageProps) {
   const ICONS = {
     error: <IconClose isCircled size="xs" />,
     warning: <IconWarning size="xs" />,
@@ -84,7 +90,7 @@ function ConsoleMessage({breadcrumb, isLast, startTimestamp = 0}: ConsoleMessage
 
   return (
     <Fragment>
-      <Icon isLast={isLast} level={breadcrumb.level}>
+      <Icon isLast={isLast} level={breadcrumb.level} isActive={isActive}>
         {ICONS[breadcrumb.level]}
       </Icon>
       <Message isLast={isLast} level={breadcrumb.level}>
@@ -123,8 +129,10 @@ const ConsoleTimestamp = styled(Common)<{isLast: boolean; level: string}>`
   cursor: pointer;
 `;
 
-const Icon = styled(Common)`
+const Icon = styled(Common)<{isActive: boolean}>`
   padding: ${space(0.5)} ${space(1)};
+  border-left: 4px solid
+    ${p => (p.isActive ? `${p.theme.purple300}` : `${p.theme.background}`)};
 `;
 const Message = styled(Common)`
   padding: ${space(0.25)} 0;
diff --git a/static/app/views/replays/detail/console/index.tsx b/static/app/views/replays/detail/console/index.tsx
index 02e4eb1dfb0b9f..6108b6997d0839 100644
--- a/static/app/views/replays/detail/console/index.tsx
+++ b/static/app/views/replays/detail/console/index.tsx
@@ -4,6 +4,8 @@ import debounce from 'lodash/debounce';
 
 import CompactSelect from 'sentry/components/forms/compactSelect';
 import {Panel} from 'sentry/components/panels';
+import {useReplayContext} from 'sentry/components/replays/replayContext';
+import {relativeTimeInMs} from 'sentry/components/replays/utils';
 import SearchBar from 'sentry/components/searchBar';
 import {t} from 'sentry/locale';
 import space from 'sentry/styles/space';
@@ -22,6 +24,7 @@ const getDistinctLogLevels = breadcrumbs =>
   Array.from(new Set<string>(breadcrumbs.map(breadcrumb => breadcrumb.level)));
 
 function Console({breadcrumbs, startTimestamp = 0}: Props) {
+  const {currentHoverTime} = useReplayContext();
   const [searchTerm, setSearchTerm] = useState('');
   const [logLevel, setLogLevel] = useState<BreadcrumbLevelType[]>([]);
   const handleSearch = debounce(query => setSearchTerm(query), 150);
@@ -31,6 +34,20 @@ function Console({breadcrumbs, startTimestamp = 0}: Props) {
     [logLevel, searchTerm, breadcrumbs]
   );
 
+  const maxActiveConsoleIndex = useMemo(() => {
+    if (filteredBreadcrumbs.length <= 0) {
+      return -1;
+    }
+
+    const foundIndex = filteredBreadcrumbs.findIndex(
+      breadcrumb =>
+        relativeTimeInMs(breadcrumb.timestamp || '', startTimestamp) >=
+        (currentHoverTime || -1)
+    );
+
+    return foundIndex - 1;
+  }, [currentHoverTime, filteredBreadcrumbs, startTimestamp]);
+
   return (
     <Fragment>
       <ConsoleFilters>
@@ -55,6 +72,7 @@ function Console({breadcrumbs, startTimestamp = 0}: Props) {
           {filteredBreadcrumbs.map((breadcrumb, i) => {
             return (
               <ConsoleMessage
+                isActive={i <= maxActiveConsoleIndex}
                 startTimestamp={startTimestamp}
                 key={i}
                 isLast={i === breadcrumbs.length - 1}


Code review comments:
diff_hunk: @@ -123,8 +129,10 @@ const ConsoleTimestamp = styled(Common)<{isLast: boolean; level: string}>`
   cursor: pointer;
 `;
 
-const Icon = styled(Common)`
+const Icon = styled(Common)<{isActive: boolean}>`
   padding: ${space(0.5)} ${space(1)};
+  border-left: 4px solid
path: static/app/views/replays/detail/console/consoleMessage.tsx
text: @user1:
An issue with using a border for the active state is that the bottom border is affected when it is not active:

![image](https://user-images.githubusercontent.com/79684/170143461-7303af9e-3350-4119-b62e-66603eb17d0b.png)


(Also the row can be have different bg colors depending on log level).

@author:
Any ideas on alternatives? I initially thought the background color could be changed but I'd worry that interferes with the current styling of the row. 

@author:
I can have it be the color of the log type background if that works, then update it to the active color when the scrubber matches it.

@author:
What about this?
https://user-images.githubusercontent.com/7014871/170273649-a958dbeb-06dc-4b9d-9fde-2358d936b400.mov

diff_hunk: @@ -123,8 +129,10 @@ const ConsoleTimestamp = styled(Common)<{isLast: boolean; level: string}>`
   cursor: pointer;
 `;
 
-const Icon = styled(Common)`
+const Icon = styled(Common)<{isActive: boolean}>`
   padding: ${space(0.5)} ${space(1)};
+  border-left: 4px solid
+    ${p => (p.isActive ? `${p.theme.purple300}` : `${p.theme.background}`)};
path: static/app/views/replays/detail/console/consoleMessage.tsx
text: @user1:
Instead of directly using the colors, it's preferable to use an alias. So instead of `purple300`, we probably want something like `active` or `focus`.

diff_hunk: @@ -123,8 +129,10 @@ const ConsoleTimestamp = styled(Common)<{isLast: boolean; level: string}>`
   cursor: pointer;
 `;
 
-const Icon = styled(Common)`
+const Icon = styled(Common)<{isActive: boolean}>`
   padding: ${space(0.5)} ${space(1)};
+  border-left: 4px solid
+    ${p => (p.isActive ? `${p.theme.purple300}` : `${p.theme.background}`)};
path: static/app/views/replays/detail/console/consoleMessage.tsx
text: @user1:
@author I think you can do:
```suggestion
${p => (p.isActive ? p.theme.purple300 : p.theme.background)};
```