Skip to content

Commit cec9b04

Browse files
mtopo27NicoHinderling
authored andcommitted
feat(preprod): full-screen treemap (EME-290) (#100926)
Add full screen mode for treemap https://github.com/user-attachments/assets/f9047cc0-738d-4fe9-8e77-b859a1899080 ### Legal Boilerplate Look, I get it. The entity doing business as "Sentry" was incorporated in the State of Delaware in 2015 as Functional Software, Inc. and is gonna need some rights from me in order to utilize my contributions in this here PR. So here's the deal: I retain all rights, title and interest in and to my contributions, and by keeping this boilerplate intact I confirm that Sentry can use, modify, copy, and redistribute my contributions, under Sentry's choice of terms. --------- Co-authored-by: Nico Hinderling <[email protected]>
1 parent 08c9a83 commit cec9b04

File tree

4 files changed

+227
-29
lines changed

4 files changed

+227
-29
lines changed

static/app/actionCreators/modal.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -429,11 +429,15 @@ export async function openBulkEditMonitorsModal({onClose, ...options}: ModalOpti
429429
}
430430

431431
export async function openInsightChartModal(options: InsightChartModalOptions) {
432-
const {default: Modal, modalCss} = await import(
433-
'sentry/components/modals/insightChartModal'
434-
);
432+
const {
433+
default: Modal,
434+
modalCss,
435+
fullscreenModalCss,
436+
} = await import('sentry/components/modals/insightChartModal');
435437

436-
openModal(deps => <Modal {...deps} {...options} />, {modalCss});
438+
openModal(deps => <Modal {...deps} {...options} />, {
439+
modalCss: options.fullscreen ? fullscreenModalCss : modalCss,
440+
});
437441
}
438442

439443
export async function openAddTempestCredentialsModal(options: {

static/app/components/modals/insightChartModal.tsx

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ export type InsightChartModalOptions = {
1111
children: React.ReactNode;
1212
title: React.ReactNode;
1313
footer?: React.ReactNode;
14+
fullscreen?: boolean;
15+
height?: number;
1416
};
1517
type Props = ModalRenderProps & InsightChartModalOptions;
1618

@@ -20,32 +22,63 @@ export default function InsightChartModal({
2022
children,
2123
Footer,
2224
footer,
25+
fullscreen = false,
26+
height = 300,
2327
}: Props) {
2428
return (
2529
<Fragment>
26-
<Container>
30+
<Container fullscreen={fullscreen}>
2731
<Header closeButton>
2832
<h3>{title}</h3>
2933
</Header>
3034

31-
<ChartRenderingContext value={{height: 300, isFullscreen: true}}>
32-
{children}
33-
</ChartRenderingContext>
35+
<ContentArea fullscreen={fullscreen}>
36+
<ChartRenderingContext value={{height, isFullscreen: true}}>
37+
{children}
38+
</ChartRenderingContext>
39+
</ContentArea>
3440

3541
{footer && <Footer>{footer}</Footer>}
3642
</Container>
3743
</Fragment>
3844
);
3945
}
4046

41-
const Container = styled('div')<{height?: number | null}>`
42-
height: ${p => (p.height ? `${p.height}px` : 'auto')};
47+
const Container = styled('div')<{fullscreen?: boolean; height?: number | null}>`
48+
height: ${p =>
49+
p.fullscreen ? 'calc(100vh - 80px)' : p.height ? `${p.height}px` : 'auto'};
4350
position: relative;
4451
padding-bottom: ${space(3)};
4552
z-index: 1000;
53+
display: flex;
54+
flex-direction: column;
55+
`;
56+
57+
const ContentArea = styled('div')<{fullscreen?: boolean}>`
58+
${p =>
59+
p.fullscreen &&
60+
`
61+
flex: 1;
62+
min-height: 0;
63+
display: flex;
64+
flex-direction: column;
65+
`}
4666
`;
4767

4868
export const modalCss = css`
4969
width: 100%;
5070
max-width: 1200px;
5171
`;
72+
73+
export const fullscreenModalCss = css`
74+
width: calc(100vw - 80px);
75+
height: calc(100vh - 80px);
76+
max-width: calc(100vw - 80px);
77+
max-height: calc(100vh - 80px);
78+
79+
[role='document'] {
80+
height: 100%;
81+
display: flex;
82+
flex-direction: column;
83+
}
84+
`;

static/app/views/preprod/buildDetails/main/buildDetailsMainContent.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,8 @@ export function BuildDetailsMainContent(props: BuildDetailsMainContentProps) {
256256
<AppSizeTreemap
257257
root={filteredTreemapData.root}
258258
searchQuery={searchQuery || ''}
259+
unfilteredRoot={appSizeData.treemap.root}
260+
onSearchChange={value => setSearchQuery(value || undefined)}
259261
/>
260262
) : (
261263
<Alert type="info">No files found matching "{searchQuery}"</Alert>
@@ -265,7 +267,12 @@ export function BuildDetailsMainContent(props: BuildDetailsMainContentProps) {
265267
);
266268
} else {
267269
visualizationContent = filteredTreemapData ? (
268-
<AppSizeTreemap root={filteredTreemapData.root} searchQuery={searchQuery || ''} />
270+
<AppSizeTreemap
271+
root={filteredTreemapData.root}
272+
searchQuery={searchQuery || ''}
273+
unfilteredRoot={appSizeData.treemap.root}
274+
onSearchChange={value => setSearchQuery(value || undefined)}
275+
/>
269276
) : (
270277
<Alert type="info">No files found matching "{searchQuery}"</Alert>
271278
);

static/app/views/preprod/components/visualizations/appSizeTreemap.tsx

Lines changed: 172 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,105 @@
1+
import {useContext, useRef, useState} from 'react';
12
import {useTheme} from '@emotion/react';
23
import styled from '@emotion/styled';
3-
import type {TreemapSeriesOption, VisualMapComponentOption} from 'echarts';
4+
import type {ECharts, TreemapSeriesOption, VisualMapComponentOption} from 'echarts';
45

6+
import {openInsightChartModal} from 'sentry/actionCreators/modal';
57
import BaseChart, {type TooltipOption} from 'sentry/components/charts/baseChart';
8+
import {Button} from 'sentry/components/core/button';
9+
import {InputGroup} from 'sentry/components/core/input/inputGroup';
10+
import {Container, Flex} from 'sentry/components/core/layout';
611
import {Heading} from 'sentry/components/core/text';
12+
import {IconClose, IconContract, IconExpand, IconSearch} from 'sentry/icons';
13+
import {t} from 'sentry/locale';
14+
import {space} from 'sentry/styles/space';
715
import {formatBytesBase10} from 'sentry/utils/bytes/formatBytesBase10';
16+
import {ChartRenderingContext} from 'sentry/views/insights/common/components/chart';
817
import {getAppSizeCategoryInfo} from 'sentry/views/preprod/components/visualizations/appSizeTheme';
918
import {TreemapType, type TreemapElement} from 'sentry/views/preprod/types/appSizeTypes';
19+
import {filterTreemapElement} from 'sentry/views/preprod/utils/treemapFiltering';
1020

1121
interface AppSizeTreemapProps {
1222
root: TreemapElement | null;
1323
searchQuery: string;
24+
onSearchChange?: (query: string) => void;
25+
unfilteredRoot?: TreemapElement;
26+
}
27+
28+
function FullscreenModalContent({
29+
unfilteredRoot,
30+
initialSearch,
31+
onSearchChange,
32+
}: {
33+
initialSearch: string;
34+
unfilteredRoot: TreemapElement;
35+
onSearchChange?: (query: string) => void;
36+
}) {
37+
const [localSearch, setLocalSearch] = useState(initialSearch);
38+
const filteredRoot = filterTreemapElement(unfilteredRoot, localSearch, '');
39+
40+
const handleSearchChange = (value: string) => {
41+
setLocalSearch(value);
42+
onSearchChange?.(value);
43+
};
44+
45+
return (
46+
<Flex direction="column" gap="md" height="100%" width="100%">
47+
<InputGroup>
48+
<InputGroup.LeadingItems>
49+
<IconSearch />
50+
</InputGroup.LeadingItems>
51+
<InputGroup.Input
52+
placeholder="Search files"
53+
value={localSearch}
54+
onChange={e => handleSearchChange(e.target.value)}
55+
/>
56+
{localSearch && (
57+
<InputGroup.TrailingItems>
58+
<Button
59+
onClick={() => handleSearchChange('')}
60+
aria-label="Clear search"
61+
borderless
62+
size="zero"
63+
>
64+
<IconClose size="sm" />
65+
</Button>
66+
</InputGroup.TrailingItems>
67+
)}
68+
</InputGroup>
69+
<Container height="100%" width="100%" style={{flex: 1, minHeight: 0}}>
70+
<AppSizeTreemap root={filteredRoot} searchQuery={localSearch} />
71+
</Container>
72+
</Flex>
73+
);
1474
}
1575

1676
export function AppSizeTreemap(props: AppSizeTreemapProps) {
1777
const theme = useTheme();
18-
const {root} = props;
78+
const {root, searchQuery, unfilteredRoot, onSearchChange} = props;
1979
const appSizeCategoryInfo = getAppSizeCategoryInfo(theme);
80+
const renderingContext = useContext(ChartRenderingContext);
81+
const isFullscreen = renderingContext?.isFullscreen ?? false;
82+
const contextHeight = renderingContext?.height;
83+
const chartRef = useRef<ECharts | null>(null);
84+
const [isZoomed, setIsZoomed] = useState(false);
85+
86+
const handleChartReady = (chart: ECharts) => {
87+
chartRef.current = chart;
88+
};
89+
90+
const handleContainerMouseDown = () => {
91+
setIsZoomed(true);
92+
};
93+
94+
const handleRecenter = () => {
95+
if (chartRef.current) {
96+
chartRef.current.dispatchAction({
97+
type: 'treemapRootToNode',
98+
seriesIndex: 0,
99+
});
100+
setIsZoomed(false);
101+
}
102+
};
20103

21104
function convertToEChartsData(element: TreemapElement): any {
22105
const categoryInfo =
@@ -82,7 +165,7 @@ export function AppSizeTreemap(props: AppSizeTreemapProps) {
82165
// Empty state
83166
if (root === null) {
84167
return (
85-
<EmptyContainer>
168+
<Flex align="center" justify="center" height="100%">
86169
<Heading as="h4">
87170
No files match your search:{' '}
88171
<span
@@ -96,7 +179,7 @@ export function AppSizeTreemap(props: AppSizeTreemapProps) {
96179
{props.searchQuery}
97180
</span>
98181
</Heading>
99-
</EmptyContainer>
182+
</Flex>
100183
);
101184
}
102185

@@ -110,7 +193,7 @@ export function AppSizeTreemap(props: AppSizeTreemapProps) {
110193
animationEasing: 'quarticOut',
111194
animationDuration: 300,
112195
height: `calc(100% - 22px)`,
113-
width: `100%`,
196+
width: '100%',
114197
top: '22px',
115198
breadcrumb: {
116199
show: true,
@@ -224,21 +307,92 @@ export function AppSizeTreemap(props: AppSizeTreemapProps) {
224307
};
225308

226309
return (
227-
<BaseChart
228-
autoHeightResize
229-
renderer="canvas"
230-
xAxis={null}
231-
yAxis={null}
232-
series={series}
233-
visualMap={visualMap}
234-
tooltip={tooltip}
235-
/>
310+
<Container
311+
height="100%"
312+
width="100%"
313+
position="relative"
314+
onMouseDown={handleContainerMouseDown}
315+
>
316+
<BaseChart
317+
autoHeightResize
318+
height={contextHeight}
319+
renderer="canvas"
320+
xAxis={null}
321+
yAxis={null}
322+
series={series}
323+
visualMap={visualMap}
324+
tooltip={tooltip}
325+
onChartReady={handleChartReady}
326+
/>
327+
<ButtonContainer
328+
direction="row"
329+
position="absolute"
330+
onMouseDown={e => e.stopPropagation()}
331+
>
332+
<Button
333+
size="xs"
334+
aria-label={t('Recenter View')}
335+
title={t('Recenter')}
336+
borderless
337+
icon={<IconContract />}
338+
onClick={handleRecenter}
339+
disabled={!isZoomed}
340+
/>
341+
{!isFullscreen && (
342+
<Button
343+
size="xs"
344+
aria-label={t('Open Full-Screen View')}
345+
title={t('Fullscreen')}
346+
borderless
347+
icon={<IconExpand />}
348+
onClick={() => {
349+
openInsightChartModal({
350+
title: t('Size Analysis'),
351+
fullscreen: true,
352+
children: unfilteredRoot ? (
353+
<FullscreenModalContent
354+
unfilteredRoot={unfilteredRoot}
355+
initialSearch={searchQuery}
356+
onSearchChange={onSearchChange}
357+
/>
358+
) : (
359+
<Container height="100%" width="100%">
360+
<AppSizeTreemap root={root} searchQuery={searchQuery} />
361+
</Container>
362+
),
363+
});
364+
}}
365+
/>
366+
)}
367+
</ButtonContainer>
368+
</Container>
236369
);
237370
}
238371

239-
const EmptyContainer = styled('div')`
240-
display: flex;
372+
const ButtonContainer = styled(Flex)`
373+
top: 0px;
374+
right: 0;
375+
height: 20px;
241376
align-items: center;
242-
justify-content: center;
243-
height: 100%;
377+
gap: ${space(0.5)};
378+
z-index: 10;
379+
380+
button {
381+
display: flex;
382+
align-items: center;
383+
justify-content: center;
384+
color: ${p => p.theme.white};
385+
height: 22px;
386+
min-height: 20px;
387+
max-height: 20px;
388+
padding: 0 ${space(0.5)};
389+
background: rgba(0, 0, 0, 0.8);
390+
border-radius: ${p => p.theme.borderRadius};
391+
box-shadow: ${p => p.theme.dropShadowMedium};
392+
393+
&:hover {
394+
color: ${p => p.theme.white};
395+
background: rgba(0, 0, 0, 0.9);
396+
}
397+
}
244398
`;

0 commit comments

Comments
 (0)