Skip to content

Commit

Permalink
feat(issue-details): Improved UI for viewing object/array values (#66153
Browse files Browse the repository at this point in the history
)

Fixes #58573,
#48480

Previously, objects/arrays were only collapsible after they reached
maxDefaultDepth. The major change here is that now each level is
collapsible

- Each level is now always collapsible. Before, objects/arrays were only
collapsible after a certain depth
- Arrays/objects begin collapsed when there are more than 5 items, in
addition to when they are at a certain depth
- Changed the appearance and location of the toggle buttons to better
match commonly-used json viewers
- Added `n items` preview text for collapsed objects/arrays (which is
clickable!)
  • Loading branch information
malwilley committed Mar 6, 2024
1 parent 06e64b8 commit bf0ebfe
Show file tree
Hide file tree
Showing 7 changed files with 275 additions and 144 deletions.
1 change: 1 addition & 0 deletions static/app/components/events/eventExtraData/index.spec.tsx
Expand Up @@ -178,6 +178,7 @@ describe('EventExtraData', function () {
},
});

await userEvent.click(screen.getByRole('button', {name: 'Expand'}));
expect(await screen.findAllByText(/redacted/)).toHaveLength(10);

await userEvent.hover(screen.getAllByText(/redacted/)[0]);
Expand Down
Expand Up @@ -174,6 +174,8 @@ describe('Request entry', function () {

expect(screen.getAllByText(/redacted/)).toHaveLength(5);

// Expand two levels down
await userEvent.click(await screen.findByLabelText('Expand'));
await userEvent.click(await screen.findByLabelText('Expand'));

expect(screen.getAllByText(/redacted/)).toHaveLength(7);
Expand Down
112 changes: 112 additions & 0 deletions static/app/components/structuredEventData/collapsibleValue.tsx
@@ -0,0 +1,112 @@
import {Children, useState} from 'react';
import {css} from '@emotion/react';
import styled from '@emotion/styled';

import {Button} from 'sentry/components/button';
import {IconChevron} from 'sentry/icons';
import {t, tn} from 'sentry/locale';
import {space} from 'sentry/styles/space';

type CollapsibleValueProps = {
children: React.ReactNode;
closeTag: string;
depth: number;
maxDefaultDepth: number;
openTag: string;
prefix?: React.ReactNode;
};

const MAX_ITEMS_BEFORE_AUTOCOLLAPSE = 5;

export function CollapsibleValue({
children,
openTag,
closeTag,
prefix = null,
depth,
maxDefaultDepth,
}: CollapsibleValueProps) {
const numChildren = Children.count(children);
const [isExpanded, setIsExpanded] = useState(
numChildren <= MAX_ITEMS_BEFORE_AUTOCOLLAPSE && depth < maxDefaultDepth
);

const shouldShowToggleButton = numChildren > 0;
const isBaseLevel = depth === 0;

// Toggle buttons get placed to the left of the open tag, but if this is the
// base level there is no room for it. So we add padding in this case.
const baseLevelPadding = isBaseLevel && shouldShowToggleButton;

return (
<CollapsibleDataContainer baseLevelPadding={baseLevelPadding}>
{numChildren > 0 ? (
<ToggleButton
size="zero"
aria-label={isExpanded ? t('Collapse') : t('Expand')}
onClick={() => setIsExpanded(oldValue => !oldValue)}
icon={
<IconChevron direction={isExpanded ? 'down' : 'right'} legacySize="10px" />
}
borderless
baseLevelPadding={baseLevelPadding}
/>
) : null}
{prefix}
<span>{openTag}</span>
{shouldShowToggleButton && !isExpanded ? (
<NumItemsButton size="zero" onClick={() => setIsExpanded(true)}>
{tn('%s item', '%s items', numChildren)}
</NumItemsButton>
) : null}
{shouldShowToggleButton && isExpanded ? (
<IndentedValues>{children}</IndentedValues>
) : null}
<span>{closeTag}</span>
</CollapsibleDataContainer>
);
}

const CollapsibleDataContainer = styled('span')<{baseLevelPadding: boolean}>`
position: relative;
${p =>
p.baseLevelPadding &&
css`
display: block;
padding-left: ${space(3)};
`}
`;

const IndentedValues = styled('div')`
padding-left: ${space(1.5)};
`;

const NumItemsButton = styled(Button)`
background: none;
border: none;
padding: 0 2px;
border-radius: 2px;
font-weight: normal;
box-shadow: none;
font-size: ${p => p.theme.fontSizeSmall};
color: ${p => p.theme.subText};
margin: 0 ${space(0.5)};
`;

const ToggleButton = styled(Button)<{baseLevelPadding: boolean}>`
position: absolute;
left: -${space(3)};
top: 2px;
border-radius: 2px;
align-items: center;
justify-content: center;
background: none;
border: none;
${p =>
p.baseLevelPadding &&
css`
left: 0;
`}
`;
40 changes: 39 additions & 1 deletion static/app/components/structuredEventData/index.spec.tsx
@@ -1,4 +1,4 @@
import {render, screen, within} from 'sentry-test/reactTestingLibrary';
import {render, screen, userEvent, within} from 'sentry-test/reactTestingLibrary';

import StructuredEventData from 'sentry/components/structuredEventData';

Expand Down Expand Up @@ -60,4 +60,42 @@ describe('ContextData', function () {
).toBeInTheDocument();
});
});

describe('collpasible values', function () {
it('auto-collapses objects/arrays with more than 5 items', async function () {
render(
<StructuredEventData
data={{
one: {one_child: 'one_child_value'},
two: {
two_1: 'two_child_value',
two_2: 2,
two_3: 3,
two_4: 4,
two_5: 5,
two_6: 6,
},
}}
/>
);

expect(screen.getByText('one_child_value')).toBeInTheDocument();
expect(screen.queryByText('two_child_value')).not.toBeInTheDocument();

// Click the "6 items" button to expand the object
await userEvent.click(screen.getByRole('button', {name: '6 items'}));
expect(screen.getByText('two_child_value')).toBeInTheDocument();
});
});

it('auto-collapses objects/arrays after max depth', async function () {
render(<StructuredEventData data={[1, [2, 3]]} maxDefaultDepth={1} />);

expect(screen.getByText('1')).toBeInTheDocument();
expect(screen.queryByText('2')).not.toBeInTheDocument();

// Click the "2 items" button to expand the array
await userEvent.click(screen.getByRole('button', {name: '2 items'}));
expect(screen.getByText('3')).toBeInTheDocument();
});
});
4 changes: 2 additions & 2 deletions static/app/components/structuredEventData/index.stories.tsx
Expand Up @@ -18,8 +18,8 @@ export default storyBook(StructuredEventData, story => {
<StructuredEventData data={100} />
<StructuredEventData data={null} />
<StructuredEventData data={false} />
<StructuredEventData data={{foo: 'bar'}} />
<StructuredEventData data={['one', 2, {three: {four: 'five'}}]} />
<StructuredEventData data={{foo: 'bar', arr: [1, 2, 3, 4, 5, 6]}} />
<StructuredEventData data={['one', 2, null]} />
</Fragment>
);
});
Expand Down

0 comments on commit bf0ebfe

Please sign in to comment.