Browse Source

ref(issue-details): Clean up event package module (#79615)

This module is ugly and long, and clipped box doesn't make sense for the
streamlined UI since the sections are foldable. Did a quick column
layout, doing tags +1 for the count. Also setting it to collapse by
default.

**Before:**
<img width="1234" alt="image"
src="https://github.com/user-attachments/assets/01ca5075-c820-4c74-94da-8d828a950d97">

**After**
<img width="1299" alt="image"
src="https://github.com/user-attachments/assets/ab9b7c6f-db2c-41bd-a8ad-78622222ac69">
Leander Rodrigues 4 months ago
parent
commit
6178f0f44c

+ 69 - 18
static/app/components/events/packageData.spec.tsx

@@ -1,5 +1,8 @@
 import {DataScrubbingRelayPiiConfigFixture} from 'sentry-fixture/dataScrubbingRelayPiiConfig';
 import {EventFixture} from 'sentry-fixture/event';
+import {LocationFixture} from 'sentry-fixture/locationFixture';
+import {OrganizationFixture} from 'sentry-fixture/organization';
+import {RouterFixture} from 'sentry-fixture/routerFixture';
 
 import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
 import {textWithMarkupMatcher} from 'sentry-test/utils';
@@ -7,30 +10,78 @@ import {textWithMarkupMatcher} from 'sentry-test/utils';
 import {EventPackageData} from 'sentry/components/events/packageData';
 
 describe('EventPackageData', function () {
-  it('display redacted data', async function () {
-    const event = EventFixture({
+  const event = EventFixture({
+    packages: {
+      certifi: '',
+      pip: '18.0',
+      python: '2.7.15',
+      'sentry-sdk': '0.3.1',
+      setuptools: '40.0.0',
+      urllib3: '1.23',
+      wheel: '0.31.1',
+      wsgiref: '0.1.2',
+    },
+    _meta: {
       packages: {
-        certifi: '',
-        pip: '18.0',
-        python: '2.7.15',
-        'sentry-sdk': '0.3.1',
-        setuptools: '40.0.0',
-        urllib3: '1.23',
-        wheel: '0.31.1',
-        wsgiref: '0.1.2',
-      },
-      _meta: {
-        packages: {
-          certifi: {'': {rem: [['organization:1', 'x']]}},
-        },
+        certifi: {'': {rem: [['organization:1', 'x']]}},
       },
+    },
+  });
+  const organization = OrganizationFixture({
+    relayPiiConfig: JSON.stringify(DataScrubbingRelayPiiConfigFixture()),
+  });
+
+  it('changes section title depending on the platform', function () {
+    render(<EventPackageData event={event} />, {
+      organization,
+      router: RouterFixture({
+        location: LocationFixture({query: {streamline: '1'}}),
+      }),
     });
+    expect(screen.getByText('Packages')).toBeInTheDocument();
+    render(<EventPackageData event={{...event, platform: 'csharp'}} />, {
+      organization,
+      router: RouterFixture({
+        location: LocationFixture({query: {streamline: '1'}}),
+      }),
+    });
+    expect(screen.getByText('Assemblies')).toBeInTheDocument();
+    render(<EventPackageData event={{...event, platform: 'java'}} />, {
+      organization,
+      router: RouterFixture({
+        location: LocationFixture({query: {streamline: '1'}}),
+      }),
+    });
+    expect(screen.getByText('Dependencies')).toBeInTheDocument();
+  });
 
+  it('displays all the data in column format', async function () {
     render(<EventPackageData event={event} />, {
-      organization: {
-        relayPiiConfig: JSON.stringify(DataScrubbingRelayPiiConfigFixture()),
-      },
+      organization,
+      router: RouterFixture({
+        location: LocationFixture({query: {streamline: '1'}}),
+      }),
     });
+    // Should be collapsed by default
+    expect(screen.queryByText(/python/)).not.toBeInTheDocument();
+    // Displays when open
+    await userEvent.click(screen.getByText('Packages'));
+    expect(screen.getByText(/python/)).toBeInTheDocument();
+    expect(screen.getByText(event?.packages?.python as string)).toBeInTheDocument();
+    // Respects _meta annotations
+    expect(screen.getByText(/redacted/)).toBeInTheDocument();
+    await userEvent.hover(screen.getByText(/redacted/));
+    expect(
+      await screen.findByText(
+        textWithMarkupMatcher(
+          "Removed because of the data scrubbing rule [Mask] [Credit card numbers] from [$message] in your organization's settings"
+        )
+      )
+    ).toBeInTheDocument();
+  });
+
+  it('display redacted data', async function () {
+    render(<EventPackageData event={event} />, {organization});
 
     expect(screen.getByText(/redacted/)).toBeInTheDocument();
 

+ 67 - 6
static/app/components/events/packageData.tsx

@@ -1,17 +1,27 @@
+import {useRef} from 'react';
+import styled from '@emotion/styled';
+
 import ClippedBox from 'sentry/components/clippedBox';
 import ErrorBoundary from 'sentry/components/errorBoundary';
+import {useIssueDetailsColumnCount} from 'sentry/components/events/eventTags/util';
 import KeyValueList from 'sentry/components/events/interfaces/keyValueList';
+import KeyValueData from 'sentry/components/keyValueData';
 import {t} from 'sentry/locale';
+import {space} from 'sentry/styles/space';
 import type {Event} from 'sentry/types/event';
 import {isEmptyObject} from 'sentry/utils/object/isEmptyObject';
 import {SectionKey} from 'sentry/views/issueDetails/streamline/context';
 import {InterimSection} from 'sentry/views/issueDetails/streamline/interimSection';
+import {useHasStreamlinedUI} from 'sentry/views/issueDetails/utils';
 
 type Props = {
   event: Event;
 };
 
 export function EventPackageData({event}: Props) {
+  const containerRef = useRef<HTMLDivElement>(null);
+  const hasStreamlinedUI = useHasStreamlinedUI();
+  const columnCount = useIssueDetailsColumnCount(containerRef) + 1;
   let longKeys: boolean, title: string;
 
   const packages = Object.entries(event.packages || {}).map(([key, value]) => ({
@@ -39,13 +49,64 @@ export function EventPackageData({event}: Props) {
     return null;
   }
 
+  const componentItems = packages.map((item, i) => (
+    <KeyValueData.Content
+      key={`content-card-${item.key}-${i}`}
+      item={item}
+      meta={item.meta}
+    />
+  ));
+
+  const columns: React.ReactNode[] = [];
+  const columnSize = Math.ceil(componentItems.length / columnCount);
+  for (let i = 0; i < componentItems.length; i += columnSize) {
+    columns.push(
+      <Column key={`highlight-column-${i}`}>
+        {componentItems.slice(i, i + columnSize)}
+      </Column>
+    );
+  }
+
   return (
-    <InterimSection title={title} type={SectionKey.PACKAGES}>
-      <ClippedBox>
-        <ErrorBoundary mini>
-          <KeyValueList data={packages} longKeys={longKeys} />
-        </ErrorBoundary>
-      </ClippedBox>
+    <InterimSection
+      title={title}
+      type={SectionKey.PACKAGES}
+      ref={containerRef}
+      initialCollapse
+    >
+      {hasStreamlinedUI ? (
+        <ColumnsContainer columnCount={columnCount}>{columns}</ColumnsContainer>
+      ) : (
+        <ClippedBox>
+          <ErrorBoundary mini>
+            <KeyValueList data={packages} longKeys={longKeys} />
+          </ErrorBoundary>
+        </ClippedBox>
+      )}
     </InterimSection>
   );
 }
+
+export const ColumnsContainer = styled('div')<{columnCount: number}>`
+  display: grid;
+  align-items: start;
+  grid-template-columns: repeat(${p => p.columnCount}, 1fr);
+`;
+
+export const Column = styled('div')`
+  display: grid;
+  grid-template-columns: fit-content(65%) 1fr;
+  font-size: ${p => p.theme.fontSizeSmall};
+  &:first-child {
+    margin-left: -${space(1)};
+  }
+  &:not(:first-child) {
+    border-left: 1px solid ${p => p.theme.innerBorder};
+    padding-left: ${space(2)};
+    margin-left: -1px;
+  }
+  &:not(:last-child) {
+    border-right: 1px solid ${p => p.theme.innerBorder};
+    padding-right: ${space(2)};
+  }
+`;