1+ import { useContext , useRef , useState } from 'react' ;
12import { useTheme } from '@emotion/react' ;
23import 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' ;
57import 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' ;
611import { 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' ;
715import { formatBytesBase10 } from 'sentry/utils/bytes/formatBytesBase10' ;
16+ import { ChartRenderingContext } from 'sentry/views/insights/common/components/chart' ;
817import { getAppSizeCategoryInfo } from 'sentry/views/preprod/components/visualizations/appSizeTheme' ;
918import { TreemapType , type TreemapElement } from 'sentry/views/preprod/types/appSizeTypes' ;
19+ import { filterTreemapElement } from 'sentry/views/preprod/utils/treemapFiltering' ;
1020
1121interface 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
1676export 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