Browse Source

ref(event-tags): More UI Changes for Event Tags Tree (#66417)

Still working on this branch but refactoring to allow a few new UI
features:
- Filtering event tags in the tree
- Column layout (currently set to 2 columns)
- Context Summary moved away from Tags (into its own Data Section)

A few changes were made to tangential files to prevent things like prop
drilling location or organization.

For future PRS:
- Ensure filtering works (add grouping for common tags)
- Add dropdown for links on tags

Still todo:
- [x] Add tests for the new UI
- [x] Add screenshots/video


New UI

<img width="978" alt="image"
src="https://github.com/getsentry/sentry/assets/35509934/80381dd3-7124-4da7-87e4-75752fee4b4c">

Without Flag/QParam
<img width="978" alt="image"
src="https://github.com/getsentry/sentry/assets/35509934/67fc584f-795c-42f8-a389-35a74ed1aba6">
Leander Rodrigues 1 year ago
parent
commit
b109dee177

+ 4 - 0
static/app/components/events/contextSummary/index.tsx

@@ -51,6 +51,10 @@ type Props = {
 };
 
 function ContextSummary({event}: Props) {
+  if (objectIsEmpty(event.contexts)) {
+    return null;
+  }
+
   const filteredContexts = KNOWN_CONTEXTS.filter(makeContextFilter(event));
 
   // XXX: We want to have *at least* MIN_CONTEXTS, so we first find all the

+ 2 - 0
static/app/components/events/contextSummary/utils.tsx

@@ -11,6 +11,8 @@ const serverSideSdks = [
   'sentry.javascript.sveltekit',
 ];
 
+export const CONTEXT_DOCS_LINK = `https://docs.sentry.io/platform-redirect/?next=/enriching-events/context/`;
+
 function isServerSideRenderedEvent(event: Event) {
   return event.sdk && serverSideSdks.includes(event.sdk.name);
 }

+ 0 - 5
static/app/components/events/eventEntries.tsx

@@ -1,6 +1,5 @@
 import {Fragment} from 'react';
 import styled from '@emotion/styled';
-import type {Location} from 'history';
 
 import {CommitRow} from 'sentry/components/commitRow';
 import {EventEvidence} from 'sentry/components/events/eventEvidence';
@@ -39,7 +38,6 @@ import {SuspectCommits} from './suspectCommits';
 import {EventUserFeedback} from './userFeedback';
 
 type Props = {
-  location: Location;
   /**
    * The organization can be the shared view on a public issue view.
    */
@@ -55,7 +53,6 @@ type Props = {
 function EventEntries({
   organization,
   project,
-  location,
   event,
   group,
   className,
@@ -106,9 +103,7 @@ function EventEntries({
       {showTagSummary && (
         <EventTagsAndScreenshot
           event={event}
-          organization={organization as Organization}
           projectSlug={projectSlug}
-          location={location}
           isShare={isShare}
         />
       )}

+ 1 - 7
static/app/components/events/eventStatisticalDetector/eventComparison/eventDisplay.tsx

@@ -142,7 +142,6 @@ function EventDisplay({
   transaction,
   durationBaseline,
 }: EventDisplayProps) {
-  const location = useLocation();
   const organization = useOrganization();
   const [selectedEventId, setSelectedEventId] = useState<string>('');
 
@@ -282,12 +281,7 @@ function EventDisplay({
         </ComparisonContentWrapper>
       </div>
 
-      <EventTags
-        event={eventData}
-        organization={organization}
-        projectSlug={project.slug}
-        location={location}
-      />
+      <EventTags event={eventData} projectSlug={project.slug} />
     </EventDisplayContainer>
   );
 }

+ 16 - 39
static/app/components/events/eventTags/eventTagsTree.spec.tsx

@@ -47,32 +47,15 @@ describe('EventTagsTree', function () {
   const event = EventFixture({tags});
 
   it('avoids tag tree without query param', function () {
-    render(
-      <EventTags
-        organization={organization}
-        projectSlug={project.slug}
-        location={router.location}
-        event={event}
-      />,
-      {organization}
-    );
+    render(<EventTags projectSlug={project.slug} event={event} />, {organization});
     tags.forEach(({key: fullTagKey, value}) => {
       expect(screen.getByText(fullTagKey)).toBeInTheDocument();
       expect(screen.getByText(value)).toBeInTheDocument();
     });
   });
 
-  it('renders tag tree with query param', function () {
-    render(
-      <EventTags
-        organization={organization}
-        projectSlug={project.slug}
-        location={{...router.location, query: {tagsTree: '1'}}}
-        event={event}
-      />,
-      {organization}
-    );
-
+  /** Asserts that new tags view is rendering the appropriate data. Requires render() call prior. */
+  function assertNewTagsView() {
     tags.forEach(({value}) => {
       expect(screen.getByText(value)).toBeInTheDocument();
     });
@@ -84,30 +67,24 @@ describe('EventTagsTree', function () {
     treeBranchTags.forEach(tag => {
       expect(screen.getByText(tag)).toBeInTheDocument();
     });
+  }
+
+  it('renders tag tree with query param', function () {
+    router.location.query.tagsTree = '1';
+    render(<EventTags projectSlug={project.slug} event={event} />, {
+      organization,
+      router,
+    });
+
+    assertNewTagsView();
   });
 
   it("renders tag tree with the 'event-tags-tree-ui' feature", function () {
     const featuredOrganization = OrganizationFixture({features: ['event-tags-tree-ui']});
-    render(
-      <EventTags
-        organization={featuredOrganization}
-        projectSlug={project.slug}
-        location={router.location}
-        event={event}
-      />,
-      {organization: featuredOrganization}
-    );
-
-    tags.forEach(({value}) => {
-      expect(screen.getByText(value)).toBeInTheDocument();
-    });
-
-    pillOnlyTags.forEach(tag => {
-      expect(screen.queryByText(tag)).not.toBeInTheDocument();
+    render(<EventTags projectSlug={project.slug} event={event} />, {
+      organization: featuredOrganization,
     });
 
-    treeBranchTags.forEach(tag => {
-      expect(screen.getByText(tag)).toBeInTheDocument();
-    });
+    assertNewTagsView();
   });
 });

+ 159 - 90
static/app/components/events/eventTags/eventTagsTree.tsx

@@ -2,6 +2,7 @@ import {Fragment, useMemo} from 'react';
 import styled from '@emotion/styled';
 
 import EventTagsContent from 'sentry/components/events/eventTags/eventTagContent';
+import type {TagFilter} from 'sentry/components/events/eventTags/util';
 import {space} from 'sentry/styles/space';
 import type {EventTag} from 'sentry/types';
 import {generateQueryWithTag} from 'sentry/utils';
@@ -10,10 +11,12 @@ import useOrganization from 'sentry/utils/useOrganization';
 
 const MAX_TREE_DEPTH = 4;
 const INVALID_BRANCH_REGEX = /\.{2,}/;
+const COLUMN_COUNT = 2;
 
 interface TagTree {
   [key: string]: TagTreeContent;
 }
+
 interface TagTreeContent {
   subtree: TagTree;
   value: string;
@@ -22,6 +25,32 @@ interface TagTreeContent {
   originalTag?: EventTag;
 }
 
+interface TagTreeColumnData {
+  columns: React.ReactNode[];
+  runningTotal: number;
+  startIndex: number;
+}
+
+interface TagTreeRowProps {
+  content: TagTreeContent;
+  projectId: string;
+  projectSlug: string;
+  streamPath: string;
+  tagKey: string;
+  isEven?: boolean;
+  isLast?: boolean;
+  spacerCount?: number;
+}
+
+interface EventTagsTreeProps {
+  projectId: string;
+  projectSlug: string;
+  streamPath: string;
+  tagFilter: TagFilter;
+  tags: EventTag[];
+  meta?: Record<any, any>;
+}
+
 function addToTagTree(
   tree: TagTree,
   tag: EventTag,
@@ -57,16 +86,6 @@ function addToTagTree(
   return tree;
 }
 
-interface TagTreeRowProps {
-  content: TagTreeContent;
-  projectId: string;
-  projectSlug: string;
-  streamPath: string;
-  tagKey: string;
-  isLast?: boolean;
-  spacerCount?: number;
-}
-
 function TagTreeRow({
   content,
   tagKey,
@@ -74,119 +93,169 @@ function TagTreeRow({
   isLast = false,
   ...props
 }: TagTreeRowProps) {
-  const subtreeTags = Object.keys(content.subtree);
   const organization = useOrganization();
   const location = useLocation();
   const originalTag = content.originalTag;
 
   return (
-    <Fragment>
-      <TreeRow>
-        <TreeKeyTrunk spacerCount={spacerCount}>
-          {spacerCount > 0 && (
-            <Fragment>
-              <TreeSpacer spacerCount={spacerCount} isLast={isLast} />
-              <TreeBranchIcon />
-            </Fragment>
+    <TreeRow>
+      <TreeKeyTrunk spacerCount={spacerCount}>
+        {spacerCount > 0 && (
+          <Fragment>
+            <TreeSpacer spacerCount={spacerCount} isLast={isLast} />
+            <TreeBranchIcon />
+          </Fragment>
+        )}
+        <TreeKey>{tagKey}</TreeKey>
+      </TreeKeyTrunk>
+      <TreeValueTrunk>
+        <TreeValue>
+          {originalTag ? (
+            <EventTagsContent
+              tag={originalTag}
+              organization={organization}
+              query={generateQueryWithTag(
+                {...location.query, referrer: 'event-tags-tree'},
+                originalTag
+              )}
+              meta={content?.meta ?? {}}
+              {...props}
+            />
+          ) : (
+            content.value
           )}
-          <TreeKey>{tagKey}</TreeKey>
-        </TreeKeyTrunk>
-        <TreeValueTrunk>
-          <TreeValue>
-            {originalTag ? (
-              <EventTagsContent
-                tag={originalTag}
-                organization={organization}
-                query={generateQueryWithTag(
-                  {...location.query, referrer: 'event-tags-tree'},
-                  originalTag
-                )}
-                meta={content?.meta ?? {}}
-                {...props}
-              />
-            ) : (
-              content.value
-            )}
-          </TreeValue>
-        </TreeValueTrunk>
-      </TreeRow>
-      {subtreeTags.map((t, i) => (
-        <TagTreeRow
-          key={`${t}-${i}`}
-          tagKey={t}
-          content={content.subtree[t]}
-          spacerCount={spacerCount + 1}
-          isLast={i === subtreeTags.length - 1}
-          {...props}
-        />
-      ))}
-    </Fragment>
+        </TreeValue>
+      </TreeValueTrunk>
+    </TreeRow>
   );
 }
 
-interface EventTagsTreeProps {
-  projectId: string;
-  projectSlug: string;
-  streamPath: string;
-  tags: EventTag[];
-  meta?: Record<any, any>;
+/**
+ * Function to recursively create a flat list of all rows to be rendered for a given TagTree
+ * @param props The props for rendering the root of the TagTree
+ * @returns A list of TagTreeRow components to be rendered in this tree
+ */
+function getTagTreeRows({tagKey, content, spacerCount = 0, ...props}: TagTreeRowProps) {
+  const subtreeTags = Object.keys(content.subtree);
+  const subtreeRows = subtreeTags.reduce((rows, t, i) => {
+    const branchRows = getTagTreeRows({
+      ...props,
+      tagKey: t,
+      content: content.subtree[t],
+      spacerCount: spacerCount + 1,
+      isLast: i === subtreeTags.length - 1,
+    });
+    return rows.concat(branchRows);
+  }, []);
+
+  return [
+    <TagTreeRow
+      key={`${tagKey}-${spacerCount}`}
+      tagKey={tagKey}
+      content={content}
+      spacerCount={spacerCount}
+      {...props}
+    />,
+    ...subtreeRows,
+  ];
 }
 
-function createTagTreeItemData(
-  tags: EventTagsTreeProps['tags'],
-  meta: EventTagsTreeProps['meta']
-): [string, TagTreeContent][] {
-  const tagTree = tags.reduce<TagTree>(
-    (tree, tag, i) => addToTagTree(tree, tag, meta?.[i], tag),
-    {}
-  );
-  return Object.entries(tagTree);
+/**
+ * Component to render proportional columns for event tags. The columns will not separate
+ * branch tags from their roots, and attempt to be as evenly distributed as possible.
+ */
+function TagTreeColumns({meta, tags, ...props}: EventTagsTreeProps) {
+  const assembledColumns = useMemo(() => {
+    // Create the TagTree data structure using all the given tags
+    const tagTree = tags.reduce<TagTree>(
+      (tree, tag, i) => addToTagTree(tree, tag, meta?.[i], tag),
+      {}
+    );
+    // Create a list of TagTreeRow lists, containing every row to be rendered. They are grouped by
+    // root parent so that we do not split up roots/branches when forming columns
+    const tagTreeRowGroups: React.ReactNode[][] = Object.entries(tagTree).map(
+      ([tagKey, content]) => getTagTreeRows({tagKey, content, ...props})
+    );
+    // Get the total number of TagTreeRow components to be rendered, and a goal size for each column
+    const tagTreeRowTotal = tagTreeRowGroups.reduce(
+      (sum, group) => sum + group.length,
+      0
+    );
+    const columnRowGoal = tagTreeRowTotal / COLUMN_COUNT;
+
+    // Iterate through the row groups, splitting rows into columns when we exceed the goal size
+    const data = tagTreeRowGroups.reduce<TagTreeColumnData>(
+      ({startIndex, runningTotal, columns}, rowList, index) => {
+        runningTotal += rowList.length;
+        // When we reach the goal size wrap rows in a TreeColumn.
+        if (runningTotal > columnRowGoal) {
+          columns.push(
+            <TreeColumn key={columns.length}>
+              {tagTreeRowGroups.slice(startIndex, index)}
+            </TreeColumn>
+          );
+          runningTotal = 0;
+          startIndex = index;
+        }
+        // If it's the last entry, wrap the column
+        if (index === tagTreeRowGroups.length - 1) {
+          columns.push(
+            <TreeColumn key={columns.length}>
+              {tagTreeRowGroups.slice(startIndex)}
+            </TreeColumn>
+          );
+        }
+        return {startIndex, runningTotal, columns};
+      },
+      {startIndex: 0, runningTotal: 0, columns: []}
+    );
+
+    return data.columns;
+  }, [meta, tags, props]);
+
+  return <Fragment>{assembledColumns}</Fragment>;
 }
 
-function EventTagsTree({tags, meta, ...props}: EventTagsTreeProps) {
-  const tagTreeItemData = useMemo(() => createTagTreeItemData(tags, meta), [tags, meta]);
+function EventTagsTree(props: EventTagsTreeProps) {
   return (
     <TreeContainer>
-      <TreeGarden>
-        {tagTreeItemData.map(([tagKey, tagTreeContent]) => (
-          <TreeItem key={tagKey}>
-            <TagTreeRow
-              tagKey={tagKey}
-              content={tagTreeContent}
-              spacerCount={0}
-              {...props}
-            />
-          </TreeItem>
-        ))}
+      <TreeGarden columnCount={COLUMN_COUNT}>
+        <TagTreeColumns {...props} />
       </TreeGarden>
     </TreeContainer>
   );
 }
 
-const TreeContainer = styled('div')``;
+const TreeContainer = styled('div')`
+  margin-top: ${space(1.5)};
+`;
 
-const TreeGarden = styled('div')`
+const TreeGarden = styled('div')<{columnCount: number}>`
   display: grid;
   gap: 0 ${space(2)};
-  grid-template-columns: 200px 1fr;
+  grid-template-columns: repeat(${p => p.columnCount}, 1fr);
+  align-items: start;
 `;
 
-const TreeItem = styled('div')`
+const TreeColumn = styled('div')`
   display: grid;
-  grid-column: span 2;
-  grid-template-columns: subgrid;
-  background-color: ${p => p.theme.background};
-  padding: ${space(0.5)} ${space(0.75)};
-  :nth-child(odd) {
-    background-color: ${p => p.theme.backgroundSecondary};
+  grid-template-columns: minmax(auto, 150px) 1fr;
+  grid-column-gap: ${space(3)};
+  &:not(:first-child) {
+    border-left: 1px solid ${p => p.theme.gray200};
+    padding-left: ${space(2)};
   }
 `;
 
 const TreeRow = styled('div')`
-  border-radius: ${p => p.theme.borderRadius};
+  border-radius: ${space(0.5)};
+  padding: 0 ${space(1)};
   display: grid;
   grid-column: span 2;
   grid-template-columns: subgrid;
+  :nth-child(odd) {
+    background-color: ${p => p.theme.backgroundSecondary};
+  }
 `;
 
 const TreeSpacer = styled('div')<{isLast: boolean; spacerCount: number}>`

+ 8 - 31
static/app/components/events/eventTags/index.spec.tsx

@@ -15,21 +15,13 @@ describe('event tags', function () {
       },
     });
 
-    const {organization, project, router} = initializeOrg({
+    const {organization, project} = initializeOrg({
       organization: {
         relayPiiConfig: null,
       },
     });
 
-    render(
-      <EventTags
-        organization={organization}
-        projectSlug={project.slug}
-        location={router.location}
-        event={event}
-      />,
-      {organization}
-    );
+    render(<EventTags projectSlug={project.slug} event={event} />, {organization});
 
     await userEvent.hover(screen.getByText(/redacted/));
     expect(
@@ -60,21 +52,13 @@ describe('event tags', function () {
       },
     });
 
-    const {organization, project, router} = initializeOrg({
+    const {organization, project} = initializeOrg({
       organization: {
         relayPiiConfig: null,
       },
     });
 
-    render(
-      <EventTags
-        organization={organization}
-        projectSlug={project.slug}
-        location={router.location}
-        event={event}
-      />,
-      {organization}
-    );
+    render(<EventTags projectSlug={project.slug} event={event} />, {organization});
 
     expect(screen.getByText('device.family')).toBeInTheDocument();
     expect(screen.getByText('iOS')).toBeInTheDocument();
@@ -90,28 +74,21 @@ describe('event tags', function () {
       ) // Fall back case
     ).toBeInTheDocument(); // tooltip description
   });
-  it('transacation tag links to transaction overview', function () {
+
+  it('transaction tag links to transaction overview', function () {
     const tags = [{key: 'transaction', value: 'mytransaction'}];
 
     const event = EventFixture({
       tags,
     });
 
-    const {organization, project, router} = initializeOrg({
+    const {organization, project} = initializeOrg({
       organization: {
         relayPiiConfig: null,
       },
     });
 
-    render(
-      <EventTags
-        organization={organization}
-        projectSlug={project.slug}
-        location={router.location}
-        event={event}
-      />,
-      {organization}
-    );
+    render(<EventTags projectSlug={project.slug} event={event} />, {organization});
 
     expect(screen.getByText('mytransaction')).toBeInTheDocument();
     expect(screen.getByRole('link')).toHaveAttribute(

+ 10 - 9
static/app/components/events/eventTags/index.tsx

@@ -1,16 +1,17 @@
 import {useEffect} from 'react';
 import styled from '@emotion/styled';
 import * as Sentry from '@sentry/react';
-import type {Location} from 'history';
 
 import ClippedBox from 'sentry/components/clippedBox';
 import EventTagsTree from 'sentry/components/events/eventTags/eventTagsTree';
+import {TagFilter, useHasNewTagsUI} from 'sentry/components/events/eventTags/util';
 import Pills from 'sentry/components/pills';
-import type {Organization} from 'sentry/types';
 import type {Event} from 'sentry/types/event';
 import {defined, generateQueryWithTag} from 'sentry/utils';
 import {trackAnalytics} from 'sentry/utils/analytics';
 import {isMobilePlatform} from 'sentry/utils/platform';
+import {useLocation} from 'sentry/utils/useLocation';
+import useOrganization from 'sentry/utils/useOrganization';
 
 import {AnnotatedText} from '../meta/annotatedText';
 
@@ -18,14 +19,16 @@ import EventTagsPill from './eventTagsPill';
 
 type Props = {
   event: Event;
-  location: Location;
-  organization: Organization;
   projectSlug: string;
+  tagFilter?: TagFilter;
 };
 
 const IOS_DEVICE_FAMILIES = ['iPhone', 'iOS', 'iOS-Device'];
 
-export function EventTags({event, organization, projectSlug, location}: Props) {
+export function EventTags({event, projectSlug, tagFilter = TagFilter.ALL}: Props) {
+  const location = useLocation();
+  const organization = useOrganization();
+  const hasNewTagsUI = useHasNewTagsUI();
   const meta = event._meta?.tags;
   const projectId = event.projectID;
 
@@ -33,9 +36,6 @@ export function EventTags({event, organization, projectSlug, location}: Props) {
     ? event.tags?.filter(tag => tag.key !== 'device.class')
     : event.tags;
 
-  const shouldDisplayTagTree =
-    location.query.tagsTree || organization.features.includes('event-tags-tree-ui');
-
   useEffect(() => {
     if (
       organization.features.includes('device-classification') &&
@@ -101,13 +101,14 @@ export function EventTags({event, organization, projectSlug, location}: Props) {
   const streamPath = `/organizations/${orgSlug}/issues/`;
   return (
     <StyledClippedBox clipHeight={150}>
-      {shouldDisplayTagTree ? (
+      {hasNewTagsUI ? (
         <EventTagsTree
           tags={tags}
           meta={meta}
           projectSlug={projectSlug}
           projectId={projectId}
           streamPath={streamPath}
+          tagFilter={tagFilter}
         />
       ) : (
         <Pills>

+ 21 - 0
static/app/components/events/eventTags/util.tsx

@@ -0,0 +1,21 @@
+import {useLocation} from 'sentry/utils/useLocation';
+import useOrganization from 'sentry/utils/useOrganization';
+
+export const TAGS_DOCS_LINK = `https://docs.sentry.io/platform-redirect/?next=/enriching-events/tags`;
+
+export enum TagFilter {
+  ALL = 'All',
+  CUSTOM = 'Custom',
+  USER = 'User',
+  SYSTEM = 'System',
+  EVENT = 'Event',
+}
+
+export function useHasNewTagsUI() {
+  const location = useLocation();
+  const organization = useOrganization();
+  return (
+    location.query.tagsTree === '1' ||
+    organization.features.includes('event-tags-tree-ui')
+  );
+}

+ 87 - 23
static/app/components/events/eventTagsAndScreenshot/index.spec.tsx

@@ -1,5 +1,7 @@
 import {Fragment} from 'react';
 import {EventFixture} from 'sentry-fixture/event';
+import {OrganizationFixture} from 'sentry-fixture/organization';
+import {RouterFixture} from 'sentry-fixture/routerFixture';
 
 import {initializeOrg} from 'sentry-test/initializeOrg';
 import {
@@ -10,6 +12,7 @@ import {
   within,
 } from 'sentry-test/reactTestingLibrary';
 
+import {TagFilter} from 'sentry/components/events/eventTags/util';
 import {EventTagsAndScreenshot} from 'sentry/components/events/eventTagsAndScreenshot';
 import GlobalModal from 'sentry/components/globalModal';
 import type {EventAttachment} from 'sentry/types';
@@ -118,7 +121,7 @@ describe('EventTagsAndScreenshot', function () {
 
   const event = EventFixture({user});
 
-  const {organization, project, router} = initializeOrg({
+  const {organization, project} = initializeOrg({
     organization: {
       orgRole: 'member',
       attachmentsRole: 'member',
@@ -189,9 +192,7 @@ describe('EventTagsAndScreenshot', function () {
       render(
         <EventTagsAndScreenshot
           event={EventFixture({...event, tags, contexts})}
-          organization={organization}
           projectSlug={project.slug}
-          location={router.location}
         />,
         {organization}
       );
@@ -236,9 +237,7 @@ describe('EventTagsAndScreenshot', function () {
       render(
         <EventTagsAndScreenshot
           event={EventFixture({...event, tags, contexts})}
-          organization={organization}
           projectSlug={project.slug}
-          location={router.location}
           isShare
         />,
         {organization}
@@ -255,9 +254,7 @@ describe('EventTagsAndScreenshot', function () {
       render(
         <EventTagsAndScreenshot
           event={EventFixture({...event, tags, contexts})}
-          organization={organization}
           projectSlug={project.slug}
-          location={router.location}
           isShare
         />
       );
@@ -284,9 +281,7 @@ describe('EventTagsAndScreenshot', function () {
           <GlobalModal />
           <EventTagsAndScreenshot
             event={EventFixture({user: {}, contexts: {}})}
-            organization={organization}
             projectSlug={project.slug}
-            location={router.location}
           />
         </Fragment>,
         {organization}
@@ -339,9 +334,7 @@ describe('EventTagsAndScreenshot', function () {
       render(
         <EventTagsAndScreenshot
           event={EventFixture({...event, tags, contexts})}
-          organization={organization}
           projectSlug={project.slug}
-          location={router.location}
         />,
         {organization}
       );
@@ -396,9 +389,7 @@ describe('EventTagsAndScreenshot', function () {
       render(
         <EventTagsAndScreenshot
           event={EventFixture({...event, tags, contexts})}
-          organization={organization}
           projectSlug={project.slug}
-          location={router.location}
         />,
         {organization}
       );
@@ -439,9 +430,7 @@ describe('EventTagsAndScreenshot', function () {
       render(
         <EventTagsAndScreenshot
           event={EventFixture({...event, tags, contexts})}
-          organization={organization}
           projectSlug={project.slug}
-          location={router.location}
         />,
         {organization}
       );
@@ -473,9 +462,7 @@ describe('EventTagsAndScreenshot', function () {
       render(
         <EventTagsAndScreenshot
           event={EventFixture({...event, contexts})}
-          organization={organization}
           projectSlug={project.slug}
-          location={router.location}
         />,
         {organization}
       );
@@ -502,12 +489,7 @@ describe('EventTagsAndScreenshot', function () {
 
     it('has tags and attachments only', async function () {
       render(
-        <EventTagsAndScreenshot
-          event={{...event, tags}}
-          organization={organization}
-          projectSlug={project.slug}
-          location={router.location}
-        />,
+        <EventTagsAndScreenshot event={{...event, tags}} projectSlug={project.slug} />,
         {organization}
       );
 
@@ -531,4 +513,86 @@ describe('EventTagsAndScreenshot', function () {
       expect(tagsContainer.getAllByRole('listitem')).toHaveLength(tags.length);
     });
   });
+
+  describe("renders changes for 'event-tags-new-ui' flag", function () {
+    const featuredOrganization = OrganizationFixture({
+      features: ['event-attachments', 'event-tags-tree-ui'],
+    });
+    const router = RouterFixture({
+      location: {
+        query: {tagsTree: '1'},
+      },
+    });
+    function assertNewTagsView() {
+      expect(screen.getByText('Tags')).toBeInTheDocument();
+      // Ensure context isn't added in tag section
+      const contextItems = screen.queryByTestId('context-item');
+      expect(contextItems).not.toBeInTheDocument();
+      // Ensure tag filter appears
+      const tagsContainer = within(screen.getByTestId('event-tags'));
+      expect(tagsContainer.getAllByRole('radio')).toHaveLength(
+        Object.keys(TagFilter).length
+      );
+    }
+
+    function assertFlagAndQueryParamWork() {
+      const flaggedOrgTags = render(
+        <EventTagsAndScreenshot
+          event={EventFixture({...event, tags, contexts})}
+          projectSlug={project.slug}
+        />,
+        {organization: featuredOrganization}
+      );
+      assertNewTagsView();
+      flaggedOrgTags.unmount();
+
+      const flaggedOrgTagsAsShare = render(
+        <EventTagsAndScreenshot
+          event={EventFixture({...event, tags, contexts})}
+          projectSlug={project.slug}
+          isShare
+        />,
+        {organization: featuredOrganization}
+      );
+      assertNewTagsView();
+      flaggedOrgTagsAsShare.unmount();
+
+      const queryParamTags = render(
+        <EventTagsAndScreenshot
+          event={EventFixture({...event, tags, contexts})}
+          projectSlug={project.slug}
+        />,
+        {organization, router}
+      );
+      assertNewTagsView();
+      queryParamTags.unmount();
+
+      const queryParamTagsAsShare = render(
+        <EventTagsAndScreenshot
+          event={EventFixture({...event, tags, contexts})}
+          projectSlug={project.slug}
+          isShare
+        />,
+        {organization, router}
+      );
+      assertNewTagsView();
+      queryParamTagsAsShare.unmount();
+    }
+
+    it('no context, tags only', function () {
+      MockApiClient.addMockResponse({
+        url: `/projects/${organization.slug}/${project.slug}/events/${event.id}/attachments/`,
+        body: [],
+      });
+      assertFlagAndQueryParamWork();
+    });
+
+    it('no context, tags and screenshot', function () {
+      MockApiClient.addMockResponse({
+        url: `/projects/${organization.slug}/${project.slug}/events/${event.id}/attachments/`,
+        body: attachments,
+      });
+      assertFlagAndQueryParamWork();
+    });
+  });
 });

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