/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @format */ import {CacheInfo, ImageId, ImageData, ImagesList, ImagesMap} from './api'; import {ImageEventWithId} from './index'; import {styled, Layout, Toolbar, theme} from 'flipper-plugin'; import { Button, Switch, Empty, Skeleton, Typography, Image, Row, Col, Badge, } from 'antd'; import MultipleSelect from './MultipleSelect'; import React, {PureComponent} from 'react'; import {DeleteFilled} from '@ant-design/icons'; function toMB(bytes: number) { return Math.floor(bytes / (1024 * 1024)); } export function toKB(bytes: number) { return Math.floor(bytes / 1024); } export function formatMB(bytes: number) { return toMB(bytes) + 'MB'; } export function formatKB(bytes: number) { return Math.floor(bytes / 1024) + 'KB'; } type ToggleProps = { label: string; onClick?: (newValue: boolean) => void; toggled: boolean; }; function Toggle(props: ToggleProps) { return ( <> { props.onClick && props.onClick(!props.toggled); }} checked={props.toggled} /> {props.label} ); } type ImagesCacheOverviewProps = { onColdStartChange: (checked: boolean) => void; coldStartFilter: boolean; allSurfacesOption: string; surfaceOptions: Set; selectedSurfaces: Set; onChangeSurface: (key: Set) => void; images: ImagesList; onClear: (type: string) => void; onTrimMemory: () => void; onRefresh: () => void; onEnableDebugOverlay: (enabled: boolean) => void; isDebugOverlayEnabled: boolean; onEnableAutoRefresh: (enabled: boolean) => void; isAutoRefreshEnabled: boolean; onImageSelected: (selectedImage: ImageId) => void; imagesMap: ImagesMap; events: Array; onTrackLeaks: (enabled: boolean) => void; isLeakTrackingEnabled: boolean; onShowDiskImages: (enabled: boolean) => void; showDiskImages: boolean; }; type ImagesCacheOverviewState = { selectedImage: ImageId | null; size: number; }; export default class ImagesCacheOverview extends PureComponent< ImagesCacheOverviewProps, ImagesCacheOverviewState > { state = { selectedImage: null, size: 150, }; onImageSelected = (selectedImage: ImageId) => { this.setState({selectedImage}); this.props.onImageSelected(selectedImage); }; onEnableDebugOverlayToggled = () => { this.props.onEnableDebugOverlay(!this.props.isDebugOverlayEnabled); }; onEnableAutoRefreshToggled = () => { this.props.onEnableAutoRefresh(!this.props.isAutoRefreshEnabled); }; onSurfaceOptionsChange = (selectedItem: string, checked: boolean) => { const {allSurfacesOption, surfaceOptions} = this.props; const selectedSurfaces = new Set([...this.props.selectedSurfaces]); if (checked && selectedItem === allSurfacesOption) { this.props.onChangeSurface(surfaceOptions); return; } if (!checked && selectedSurfaces.size === 1) { return; } if (selectedItem !== allSurfacesOption) { selectedSurfaces.delete(allSurfacesOption); if (checked) { selectedSurfaces.add(selectedItem); } else { selectedSurfaces.delete(selectedItem); } } if ( surfaceOptions.size - selectedSurfaces.size === 1 && !selectedSurfaces.has(allSurfacesOption) ) { selectedSurfaces.add(allSurfacesOption); } this.props.onChangeSurface(selectedSurfaces); }; render() { const hasImages = this.props.images.reduce( (c, cacheInfo) => c + cacheInfo.imageIds.length, 0, ) > 0; return ( {!hasImages ? ( ) : ( {this.props.images.map((data: CacheInfo, index: number) => { const maxSize = data.maxSizeBytes; const subtitle = maxSize ? formatMB(data.sizeBytes) + ' / ' + formatMB(maxSize) : formatMB(data.sizeBytes); const onClear = data.clearKey !== undefined ? () => this.props.onClear(data.clearKey as string) : undefined; return ( ); })} )} ); } } class ImageGrid extends PureComponent<{ title: string; subtitle: string; images: Array; selectedImage: ImageId | null; onImageSelected: (image: ImageId) => void; onClear: (() => void) | undefined; imagesMap: ImagesMap; events: Array; }> { static Content = styled.div({ paddingLeft: 15, }); render() { const {images, onImageSelected, selectedImage} = this.props; if (images.length === 0) { return null; } const ROW_SIZE = 6; const imageRows = Array(Math.ceil(images.length / ROW_SIZE)) .fill(0) .map((_, index) => index * ROW_SIZE) .map((begin) => images.slice(begin, begin + ROW_SIZE)); return ( {imageRows.map((row, rowIndex) => ( {row.map((imageId, colIndex) => ( e.imageIds.includes(imageId), ).length } /> ))} ))} ); } } class ImageGridHeader extends PureComponent<{ title: string; subtitle: string; onClear: (() => void) | undefined; }> { static Subtitle = styled.span({ fontSize: 22, fontWeight: 300, }); render() { return ( {this.props.title} {this.props.subtitle} {this.props.onClear ? ( ) : null} ); } } class ImageItem extends PureComponent<{ imageId: ImageId; image: ImageData; selected: boolean; onSelected: (image: ImageId) => void; size: number; numberOfRequests: number; }> { static defaultProps = { size: 150, }; static SelectedHighlight = styled.div<{selected: boolean}>((props) => ({ borderColor: theme.primaryColor, borderStyle: 'solid', borderWidth: props.selected ? 3 : 0, borderRadius: 4, boxShadow: props.selected ? `inset 0 0 0 1px ${theme.white}` : 'none', bottom: 0, left: 0, position: 'absolute', right: 0, top: 0, })); static HoverOverlay = styled(Layout.Container)<{ selected: boolean; size: number; }>((props) => ({ alignItems: 'center', backgroundColor: 'rgba(255,255,255,0.8)', bottom: props.selected ? 4 : 0, fontSize: props.size > 100 ? 16 : 11, justifyContent: 'center', left: props.selected ? 4 : 0, opacity: 0, position: 'absolute', right: props.selected ? 4 : 0, top: props.selected ? 4 : 0, overflow: 'hidden', transition: '.1s opacity', '&:hover': { opacity: 1, }, })); static MemoryLabel = styled.span({ fontWeight: 600, marginBottom: 6, }); static SizeLabel = styled.span({ fontWeight: 300, }); static EventBadge = styled(Badge)({ position: 'absolute', top: 0, right: 0, zIndex: 1, }); onClick = () => { this.props.onSelected(this.props.imageId); }; render() { const {image, selected, size, numberOfRequests} = this.props; return ( {numberOfRequests > 0 && image != null && ( )} {image != null ? ( ) : ( )} {image != null && ( {formatKB(image.sizeBytes)} {image.width}×{image.height} )} ); } }