Skip to content

Commit a200a20

Browse files
DominikB2014claude
andauthored
feat(assets): Add image preview to assets summary details widget (#109329)
Renders sample images inside the details widget on the frontend assets summary prebuilt dashboard when the viewed span is an image type (detected by `span.op === resource.img` or by matching the description's file extension against known image extensions). The `SampleImages` component was previously unusable in this context because `isSettingsLoading` was stuck at `true` — the project settings query was disabled since no `projectId` was passed. Fixed by adding `project.id` to `DefaultDetailWidgetFields` so the project can be resolved and the query enabled. Adds a `noVisualizationPadding` prop to `SampleImages` that strips the `ChartPanel` border/padding when rendered inside a widget card. Also threads the prop down to `ImageWrapper` to conditionally remove its `padding-top`. <img width="1447" height="394" alt="image" src="https://github.com/user-attachments/assets/d968a19d-56bf-4122-a0da-1467990b1c35" /> Refs BROWSE-362 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent c32ff96 commit a200a20

File tree

5 files changed

+71
-23
lines changed

5 files changed

+71
-23
lines changed

static/app/views/dashboards/utils/prebuiltConfigs/frontendAssets/frontendAssetsSummary.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const ASSET_DESCRIPTION_WIDGET: Widget = {
2222
SpanFields.SPAN_GROUP,
2323
SpanFields.SPAN_DESCRIPTION,
2424
SpanFields.SPAN_CATEGORY,
25+
SpanFields.PROJECT_ID,
2526
] satisfies DefaultDetailWidgetFields[],
2627
aggregates: [],
2728
columns: [
@@ -30,6 +31,7 @@ const ASSET_DESCRIPTION_WIDGET: Widget = {
3031
SpanFields.SPAN_GROUP,
3132
SpanFields.SPAN_DESCRIPTION,
3233
SpanFields.SPAN_CATEGORY,
34+
SpanFields.PROJECT_ID,
3335
] satisfies DefaultDetailWidgetFields[],
3436
fieldAliases: [],
3537
conditions: '',
@@ -41,8 +43,8 @@ const ASSET_DESCRIPTION_WIDGET: Widget = {
4143
layout: {
4244
x: 0,
4345
y: 0,
44-
minH: 1,
45-
h: 1,
46+
minH: 2,
47+
h: 2,
4648
w: 6,
4749
},
4850
};
@@ -146,7 +148,7 @@ const SECOND_ROW_WIDGETS = spaceWidgetsEquallyOnRow(
146148
],
147149
},
148150
],
149-
1,
151+
2,
150152
{h: 1, minH: 1}
151153
);
152154

@@ -210,7 +212,7 @@ const THIRD_ROW_WIDGETS = spaceWidgetsEquallyOnRow(
210212
],
211213
},
212214
],
213-
1
215+
3
214216
);
215217

216218
const ASSETS_TABLE_WIDGET: Widget = {

static/app/views/dashboards/widgets/detailsWidget/detailsWidgetVisualization.spec.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ import {ProjectFixture} from 'sentry-fixture/project';
66
import {render, screen, waitForElementToBeRemoved} from 'sentry-test/reactTestingLibrary';
77

88
import {DetailsWidgetVisualization} from 'sentry/views/dashboards/widgets/detailsWidget/detailsWidgetVisualization';
9+
import type {DefaultDetailWidgetFields} from 'sentry/views/dashboards/widgets/detailsWidget/types';
10+
import type {SpanResponse} from 'sentry/views/insights/types';
11+
12+
type TestSpan = Pick<SpanResponse, DefaultDetailWidgetFields>;
913

1014
describe('DetailsWidgetVisualization', () => {
1115
beforeEach(() => {
@@ -50,12 +54,13 @@ describe('DetailsWidgetVisualization', () => {
5054
});
5155

5256
it('renders a span', () => {
53-
const span = {
57+
const span: TestSpan = {
5458
id: '123',
5559
['span.op']: 'span_op',
5660
['span.description']: 'span_description',
5761
['span.group']: 'span_group',
5862
['span.category']: 'span_category',
63+
['project.id']: 1,
5964
};
6065
render(<DetailsWidgetVisualization span={span} />);
6166
expect(
@@ -64,12 +69,13 @@ describe('DetailsWidgetVisualization', () => {
6469
});
6570

6671
it('renders a db span', async () => {
67-
const span = {
72+
const span: TestSpan = {
6873
id: '123',
6974
['span.op']: 'db',
7075
['span.description']: 'SELECT * FROM users',
7176
['span.group']: 'span_group',
7277
['span.category']: 'db',
78+
['project.id']: 1,
7379
};
7480
render(<DetailsWidgetVisualization span={span} />);
7581

@@ -81,12 +87,13 @@ describe('DetailsWidgetVisualization', () => {
8187
});
8288

8389
it('renders an http domain status link', async () => {
84-
const span = {
90+
const span: TestSpan = {
8591
id: '123',
8692
['span.op']: 'http',
8793
['span.description']: 'GET /users',
8894
['span.group']: 'span_group',
8995
['span.category']: 'http',
96+
['project.id']: 1,
9097
};
9198
render(<DetailsWidgetVisualization span={span} />);
9299

static/app/views/dashboards/widgets/detailsWidget/detailsWidgetVisualization.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import {Container, Flex} from '@sentry/scraps/layout';
55
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
66
import {AutoSizedText} from 'sentry/views/dashboards/widgetCard/autoSizedText';
77
import type {DefaultDetailWidgetFields} from 'sentry/views/dashboards/widgets/detailsWidget/types';
8+
import SampleImages from 'sentry/views/insights/browser/resources/components/sampleImages';
9+
import {IMAGE_FILE_EXTENSIONS} from 'sentry/views/insights/browser/resources/constants';
10+
import {ResourceSpanOps} from 'sentry/views/insights/browser/resources/types';
811
import {DatabaseSpanDescription} from 'sentry/views/insights/common/components/spanDescription';
912
import {useSpans} from 'sentry/views/insights/common/queries/useDiscover';
1013
import {resolveSpanModule} from 'sentry/views/insights/common/utils/resolveSpanModule';
@@ -49,12 +52,38 @@ export function DetailsWidgetVisualization(props: DetailsWidgetVisualizationProp
4952
}
5053

5154
if (moduleName === ModuleName.RESOURCE) {
55+
const isImage =
56+
spanOp === ResourceSpanOps.IMAGE ||
57+
IMAGE_FILE_EXTENSIONS.includes(
58+
(spanDescription.split('?')[0] ?? '').split('.').pop()?.toLowerCase() ?? ''
59+
);
60+
61+
if (isImage) {
62+
const projectId = span[SpanFields.PROJECT_ID]
63+
? Number(span[SpanFields.PROJECT_ID])
64+
: undefined;
65+
return <ResourceImageVisualization spanGroup={spanGroup} projectId={projectId} />;
66+
}
67+
5268
return <Container padding="md xl">{spanDescription}</Container>;
5369
}
5470

5571
return <Wrapper>{`${spanOp} - ${spanDescription}`}</Wrapper>;
5672
}
5773

74+
function ResourceImageVisualization(props: {
75+
spanGroup: string;
76+
projectId?: number;
77+
}): React.ReactNode {
78+
const {spanGroup, projectId} = props;
79+
80+
return (
81+
<Container padding="0 xl" overflow="auto" height="100%">
82+
<SampleImages groupId={spanGroup} projectId={projectId} noVisualizationPadding />
83+
</Container>
84+
);
85+
}
86+
5887
function HttpSpanVisualization(props: {
5988
spanDescription: string;
6089
spanId: string;

static/app/views/dashboards/widgets/detailsWidget/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ export type DefaultDetailWidgetFields =
55
| SpanFields.SPAN_GROUP
66
| SpanFields.SPAN_DESCRIPTION
77
| SpanFields.ID
8-
| SpanFields.SPAN_CATEGORY;
8+
| SpanFields.SPAN_CATEGORY
9+
| SpanFields.PROJECT_ID;

static/app/views/insights/browser/resources/components/sampleImages.tsx

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import type {SpanResponse} from 'sentry/views/insights/types';
2424
import {SpanFields} from 'sentry/views/insights/types';
2525
import {usePerformanceGeneralProjectSettings} from 'sentry/views/performance/utils';
2626

27-
type Props = {groupId: string; projectId?: number};
27+
type Props = {groupId: string; noVisualizationPadding?: boolean; projectId?: number};
2828

2929
export const LOCAL_STORAGE_SHOW_LINKS = 'performance-resources-images-showLinks';
3030

@@ -38,7 +38,7 @@ const {
3838
const imageWidth = '200px';
3939
const imageHeight = '180px';
4040

41-
function SampleImages({groupId, projectId}: Props) {
41+
function SampleImages({groupId, projectId, noVisualizationPadding}: Props) {
4242
const [showLinks, setShowLinks] = useLocalStorageState(LOCAL_STORAGE_SHOW_LINKS, false);
4343
const filters = useResourceModuleFilters();
4444
const [showImages, setShowImages] = useState(showLinks);
@@ -83,17 +83,24 @@ function SampleImages({groupId, projectId}: Props) {
8383
setShowImages(true);
8484
};
8585

86+
const body = (
87+
<SampleImagesChartPanelBody
88+
onClickShowLinks={handleClickOnlyShowLinks}
89+
images={filteredResources}
90+
isLoadingImages={isLoadingImages}
91+
isSettingsLoading={isSettingsLoading}
92+
isImagesEnabled={isImagesEnabled}
93+
showImages={showImages || isImagesEnabled}
94+
noVisualizationPadding={noVisualizationPadding}
95+
/>
96+
);
97+
98+
if (noVisualizationPadding) {
99+
return body;
100+
}
101+
86102
return (
87-
<ChartPanel title={showImages ? t('Largest Images') : undefined}>
88-
<SampleImagesChartPanelBody
89-
onClickShowLinks={handleClickOnlyShowLinks}
90-
images={filteredResources}
91-
isLoadingImages={isLoadingImages}
92-
isSettingsLoading={isSettingsLoading}
93-
isImagesEnabled={isImagesEnabled}
94-
showImages={showImages || isImagesEnabled}
95-
/>
96-
</ChartPanel>
103+
<ChartPanel title={showImages ? t('Largest Images') : undefined}>{body}</ChartPanel>
97104
);
98105
}
99106

@@ -111,6 +118,7 @@ function SampleImagesChartPanelBody(props: {
111118
isLoadingImages: boolean;
112119
isSettingsLoading: boolean;
113120
showImages: boolean;
121+
noVisualizationPadding?: boolean;
114122
onClickShowLinks?: () => void;
115123
}) {
116124
const {
@@ -120,6 +128,7 @@ function SampleImagesChartPanelBody(props: {
120128
showImages,
121129
isImagesEnabled,
122130
isSettingsLoading,
131+
noVisualizationPadding,
123132
} = props;
124133

125134
const hasImages = images.length > 0;
@@ -147,7 +156,7 @@ function SampleImagesChartPanelBody(props: {
147156
}
148157

149158
return (
150-
<ImageWrapper>
159+
<ImageWrapper noVisualizationPadding={noVisualizationPadding}>
151160
{images.map(resource => {
152161
const hasRawDomain = Boolean(resource[RAW_DOMAIN]);
153162
const isRelativeUrl = resource[SPAN_DESCRIPTION].startsWith('/');
@@ -299,10 +308,10 @@ const getFileNameFromDescription = (description: string) => {
299308
return url.pathname.split('/').pop() ?? '';
300309
};
301310

302-
const ImageWrapper = styled('div')`
311+
const ImageWrapper = styled('div')<{noVisualizationPadding?: boolean}>`
303312
display: grid;
304313
grid-template-columns: repeat(auto-fill, ${imageWidth});
305-
padding-top: ${space(2)};
314+
padding-top: ${p => (p.noVisualizationPadding ? 0 : space(2))};
306315
gap: 30px;
307316
`;
308317

0 commit comments

Comments
 (0)