Browse Source

feat(interface-breadcrumb): Add breadcrumb eventlink (#28889)

Priscila Oliveira 3 years ago
parent
commit
e67ff3e32b

+ 6 - 2
static/app/components/events/eventEntries.tsx

@@ -1,4 +1,4 @@
-import {memo, useEffect, useState} from 'react';
+import React, {memo, useEffect, useState} from 'react';
 import styled from '@emotion/styled';
 import * as Sentry from '@sentry/react';
 import {Location} from 'history';
@@ -55,7 +55,7 @@ const MINIFIED_DATA_JAVA_EVENT_REGEX_MATCH =
 
 type ProGuardErrors = Array<Error>;
 
-type Props = {
+type Props = Pick<React.ComponentProps<typeof EventEntry>, 'route' | 'router'> & {
   /**
    * The organization can be the shared view on a public issue view.
    */
@@ -81,6 +81,8 @@ const EventEntries = memo(
     event,
     group,
     className,
+    router,
+    route,
     isShare = false,
     showExampleCommit = false,
     showTagSummary = true,
@@ -320,6 +322,8 @@ const EventEntries = memo(
             organization={organization}
             event={definedEvent}
             entry={entry}
+            route={route}
+            router={router}
           />
         </ErrorBoundary>
       ));

+ 12 - 2
static/app/components/events/eventEntry.tsx

@@ -15,7 +15,7 @@ import Threads from 'app/components/events/interfaces/threads';
 import {Group, Organization, Project, SharedViewOrganization} from 'app/types';
 import {Entry, EntryType, Event, EventTransaction} from 'app/types/event';
 
-type Props = {
+type Props = Pick<React.ComponentProps<typeof Breadcrumbs>, 'route' | 'router'> & {
   entry: Entry;
   projectSlug: Project['slug'];
   event: Event;
@@ -23,7 +23,15 @@ type Props = {
   group?: Group;
 };
 
-function EventEntry({entry, projectSlug, event, organization, group}: Props) {
+function EventEntry({
+  entry,
+  projectSlug,
+  event,
+  organization,
+  group,
+  route,
+  router,
+}: Props) {
   const hasHierarchicalGrouping =
     !!organization.features?.includes('grouping-stacktrace-ui') &&
     !!(event.metadata.current_tree_label || event.metadata.finest_tree_label);
@@ -87,6 +95,8 @@ function EventEntry({entry, projectSlug, event, organization, group}: Props) {
           data={data}
           organization={organization as Organization}
           event={event}
+          router={router}
+          route={route}
         />
       );
     }

+ 25 - 20
static/app/components/events/interfaces/breadcrumbs/breadcrumb/data/default.tsx

@@ -15,26 +15,33 @@ type Props = {
   breadcrumb: BreadcrumbTypeDefault | BreadcrumbTypeNavigation;
   event: Event;
   orgSlug: Organization['slug'];
+  linkedEvent?: React.ReactElement;
 };
 
-const Default = ({breadcrumb, event, orgSlug, searchTerm}: Props) => (
-  <Summary kvData={breadcrumb.data}>
-    {breadcrumb?.message && (
-      <AnnotatedText
-        value={
-          <FormatMessage
-            searchTerm={searchTerm}
-            event={event}
-            orgSlug={orgSlug}
-            breadcrumb={breadcrumb}
-            message={breadcrumb.message}
-          />
-        }
-        meta={getMeta(breadcrumb, 'message')}
-      />
-    )}
-  </Summary>
-);
+function Default({breadcrumb, event, orgSlug, searchTerm, linkedEvent}: Props) {
+  const {message} = breadcrumb;
+  return (
+    <Summary kvData={breadcrumb.data}>
+      {linkedEvent}
+      {message && (
+        <AnnotatedText
+          value={
+            <FormatMessage
+              searchTerm={searchTerm}
+              event={event}
+              orgSlug={orgSlug}
+              breadcrumb={breadcrumb}
+              message={message}
+            />
+          }
+          meta={getMeta(breadcrumb, 'message')}
+        />
+      )}
+    </Summary>
+  );
+}
+
+export default Default;
 
 function isEventId(maybeEventId: string): boolean {
   // maybeEventId is an event id if it's a hex string of 32 characters long
@@ -80,5 +87,3 @@ const FormatMessage = withProjects(function FormatMessageInner({
 
   return content;
 });
-
-export default Default;

+ 7 - 5
static/app/components/events/interfaces/breadcrumbs/breadcrumb/data/exception.tsx

@@ -11,14 +11,16 @@ import Summary from './summary';
 type Props = {
   searchTerm: string;
   breadcrumb: BreadcrumbTypeDefault;
+  linkedEvent?: React.ReactElement;
 };
 
-const Exception = ({breadcrumb, searchTerm}: Props) => {
-  const {data} = breadcrumb;
+function Exception({breadcrumb, searchTerm, linkedEvent}: Props) {
+  const {data, message} = breadcrumb;
   const dataValue = data?.value;
 
   return (
     <Summary kvData={omit(data, ['type', 'value'])}>
+      {linkedEvent}
       {data?.type && (
         <AnnotatedText
           value={
@@ -39,14 +41,14 @@ const Exception = ({breadcrumb, searchTerm}: Props) => {
           meta={getMeta(data, 'value')}
         />
       )}
-      {breadcrumb?.message && (
+      {message && (
         <AnnotatedText
-          value={<Highlight text={searchTerm}>{breadcrumb.message}</Highlight>}
+          value={<Highlight text={searchTerm}>{message}</Highlight>}
           meta={getMeta(breadcrumb, 'message')}
         />
       )}
     </Summary>
   );
-};
+}
 
 export default Exception;

+ 4 - 2
static/app/components/events/interfaces/breadcrumbs/breadcrumb/data/http.tsx

@@ -13,9 +13,10 @@ import Summary from './summary';
 type Props = {
   searchTerm: string;
   breadcrumb: BreadcrumbTypeHTTP;
+  linkedEvent?: React.ReactElement;
 };
 
-const Http = ({breadcrumb, searchTerm}: Props) => {
+function Http({breadcrumb, searchTerm, linkedEvent}: Props) {
   const {data} = breadcrumb;
 
   const renderUrl = (url: any) => {
@@ -41,6 +42,7 @@ const Http = ({breadcrumb, searchTerm}: Props) => {
 
   return (
     <Summary kvData={omit(data, ['method', 'url', 'status_code'])}>
+      {linkedEvent}
       {data?.method && (
         <AnnotatedText
           value={
@@ -67,6 +69,6 @@ const Http = ({breadcrumb, searchTerm}: Props) => {
       )}
     </Summary>
   );
-};
+}
 
 export default Http;

+ 24 - 5
static/app/components/events/interfaces/breadcrumbs/breadcrumb/data/index.tsx

@@ -5,24 +5,42 @@ import {Event} from 'app/types/event';
 import Default from './default';
 import Exception from './exception';
 import Http from './http';
+import LinkedEvent from './linkedEvent';
 
-type Props = {
+type Props = Pick<React.ComponentProps<typeof LinkedEvent>, 'route' | 'router'> & {
   searchTerm: string;
   breadcrumb: RawCrumb;
   event: Event;
   orgSlug: Organization['slug'];
 };
 
-const Data = ({breadcrumb, event, orgSlug, searchTerm}: Props) => {
+function Data({breadcrumb, event, orgSlug, searchTerm, route, router}: Props) {
+  const linkedEvent = breadcrumb.event_id ? (
+    <LinkedEvent
+      orgSlug={orgSlug}
+      eventId={breadcrumb.event_id}
+      route={route}
+      router={router}
+    />
+  ) : undefined;
+
   if (breadcrumb.type === BreadcrumbType.HTTP) {
-    return <Http breadcrumb={breadcrumb} searchTerm={searchTerm} />;
+    return (
+      <Http breadcrumb={breadcrumb} searchTerm={searchTerm} linkedEvent={linkedEvent} />
+    );
   }
 
   if (
     breadcrumb.type === BreadcrumbType.WARNING ||
     breadcrumb.type === BreadcrumbType.ERROR
   ) {
-    return <Exception breadcrumb={breadcrumb} searchTerm={searchTerm} />;
+    return (
+      <Exception
+        breadcrumb={breadcrumb}
+        searchTerm={searchTerm}
+        linkedEvent={linkedEvent}
+      />
+    );
   }
 
   return (
@@ -31,8 +49,9 @@ const Data = ({breadcrumb, event, orgSlug, searchTerm}: Props) => {
       orgSlug={orgSlug}
       breadcrumb={breadcrumb}
       searchTerm={searchTerm}
+      linkedEvent={linkedEvent}
     />
   );
-};
+}
 
 export default Data;

+ 119 - 0
static/app/components/events/interfaces/breadcrumbs/breadcrumb/data/linkedEvent.tsx

@@ -0,0 +1,119 @@
+import {useEffect, useState} from 'react';
+import {InjectedRouter, PlainRoute} from 'react-router';
+import styled from '@emotion/styled';
+import * as Sentry from '@sentry/react';
+
+import {addErrorMessage} from 'app/actionCreators/indicator';
+import ProjectBadge from 'app/components/idBadge/projectBadge';
+import Placeholder from 'app/components/placeholder';
+import ShortId from 'app/components/shortId';
+import {t} from 'app/locale';
+import space from 'app/styles/space';
+import {EventIdResponse, Group, Organization, Project} from 'app/types';
+import useApi from 'app/utils/useApi';
+import useSessionStorage from 'app/utils/useSessionStorage';
+
+type StoredLinkedEvent = {
+  shortId: string;
+  groupId: string;
+  project: Project;
+  orgSlug: Organization['slug'];
+};
+
+type Props = {
+  orgSlug: Organization['slug'];
+  eventId: string;
+  router: InjectedRouter;
+  route: PlainRoute;
+};
+
+function LinkedEvent({orgSlug, eventId, route, router}: Props) {
+  const [storedLinkedEvent, setStoredLinkedEvent, removeStoredLinkedEvent] =
+    useSessionStorage<undefined | StoredLinkedEvent>(eventId);
+
+  const [eventIdLookup, setEventIdLookup] = useState<undefined | EventIdResponse>();
+
+  const api = useApi();
+
+  useEffect(() => {
+    fetchEventById();
+    router.setRouteLeaveHook(route, onRouteLeave);
+  }, []);
+
+  useEffect(() => {
+    fetchIssueByGroupId();
+  }, [eventIdLookup]);
+
+  function onRouteLeave() {
+    removeStoredLinkedEvent();
+  }
+
+  async function fetchEventById() {
+    if (!!storedLinkedEvent) {
+      return;
+    }
+
+    try {
+      const response = await api.requestPromise(
+        `/organizations/${orgSlug}/eventids/${eventId}/`
+      );
+
+      setEventIdLookup(response);
+    } catch (error) {
+      addErrorMessage(
+        t('An error occured while fetching the data of the breadcrumb event link')
+      );
+      Sentry.captureException(error);
+      // do nothing. The link won't be displayed
+    }
+  }
+
+  async function fetchIssueByGroupId() {
+    if (!!storedLinkedEvent || !eventIdLookup) {
+      return;
+    }
+
+    try {
+      const response = await api.requestPromise(
+        `/organizations/${orgSlug}/issues/${eventIdLookup.groupId}/`
+      );
+
+      const {project, shortId} = response as Group;
+      const {groupId} = eventIdLookup;
+      setStoredLinkedEvent({shortId, project, groupId, orgSlug});
+    } catch (error) {
+      addErrorMessage(
+        t('An error occured while fetching the data of the breadcrumb event link')
+      );
+      Sentry.captureException(error);
+      // do nothing. The link won't be displayed
+    }
+  }
+
+  if (!storedLinkedEvent) {
+    return <StyledPlaceholder height="16px" width="109px" />;
+  }
+
+  const {shortId, project, groupId} = storedLinkedEvent;
+
+  return (
+    <StyledShortId
+      shortId={shortId}
+      avatar={<ProjectBadge project={project} avatarSize={16} hideName />}
+      to={`/${orgSlug}/${project.slug}/issues/${groupId}/events/${eventId}/`}
+    />
+  );
+}
+
+export default LinkedEvent;
+
+const StyledShortId = styled(ShortId)`
+  font-weight: 700;
+  display: inline-grid;
+  margin-right: ${space(1)};
+`;
+
+const StyledPlaceholder = styled(Placeholder)`
+  display: inline-flex;
+  margin-right: ${space(1)};
+`;

+ 3 - 1
static/app/components/events/interfaces/breadcrumbs/breadcrumb/data/summary.tsx

@@ -59,9 +59,11 @@ const ContextDataWrapper = styled('div')`
 `;
 
 const StyledCode = styled('code')`
-  line-height: 26px;
   font-size: inherit;
   white-space: pre-wrap;
   background: none;
   padding: 0;
+  > * {
+    vertical-align: middle;
+  }
 `;

+ 7 - 2
static/app/components/events/interfaces/breadcrumbs/breadcrumb/index.tsx

@@ -1,4 +1,4 @@
-import {memo} from 'react';
+import React, {memo} from 'react';
 import {css} from '@emotion/react';
 import styled from '@emotion/styled';
 
@@ -13,7 +13,7 @@ import Level from './level';
 import Time from './time';
 import Type from './type';
 
-type Props = {
+type Props = Pick<React.ComponentProps<typeof Data>, 'route' | 'router'> & {
   breadcrumb: Crumb;
   event: Event;
   orgSlug: Organization['slug'];
@@ -37,6 +37,8 @@ const Breadcrumb = memo(function Breadcrumb({
   onLoad,
   scrollbarSize,
   style,
+  route,
+  router,
   ['data-test-id']: dataTestId,
 }: Props) {
   const {type, description, color, level, category, timestamp} = breadcrumb;
@@ -57,6 +59,8 @@ const Breadcrumb = memo(function Breadcrumb({
         orgSlug={orgSlug}
         breadcrumb={breadcrumb}
         searchTerm={searchTerm}
+        route={route}
+        router={router}
       />
       <div>
         <Level level={level} searchTerm={searchTerm} />
@@ -101,6 +105,7 @@ const Wrapper = styled('div')<{error: boolean; scrollbarSize: number}>`
       :nth-child(5n-2) {
         grid-row: 2/2;
         grid-column: 2/-1;
+        padding-top: 0;
         padding-right: ${space(2)};
       }
 

+ 11 - 1
static/app/components/events/interfaces/breadcrumbs/breadcrumbs.tsx

@@ -26,7 +26,13 @@ const cache = new CellMeasurerCache({
 
 type Props = Pick<
   React.ComponentProps<typeof Breadcrumb>,
-  'event' | 'orgSlug' | 'searchTerm' | 'relativeTime' | 'displayRelativeTime'
+  | 'event'
+  | 'orgSlug'
+  | 'searchTerm'
+  | 'relativeTime'
+  | 'displayRelativeTime'
+  | 'router'
+  | 'route'
 > & {
   breadcrumbs: Crumb[];
   onSwitchTimeFormat: () => void;
@@ -45,6 +51,8 @@ function Breadcrumbs({
   event,
   relativeTime,
   emptyMessage,
+  route,
+  router,
 }: Props) {
   const [scrollToIndex, setScrollToIndex] = useState<number | undefined>(undefined);
   const [scrollbarSize, setScrollbarSize] = useState(0);
@@ -107,6 +115,8 @@ function Breadcrumbs({
                 ? scrollbarSize
                 : 0
             }
+            router={router}
+            route={route}
           />
         )}
       </CellMeasurer>

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