Migrate Images plugin to ant.design

Summary: {gif:xcsasxxe}

Reviewed By: mweststrate

Differential Revision: D28463799

fbshipit-source-id: 280eaaf0ad5858b3507055a278d1f98fd5668fd0
This commit is contained in:
Mathias Fleig Mortensen
2021-05-18 04:12:21 -07:00
committed by Facebook GitHub Bot
parent 6e206fc054
commit 996e8ab87c
4 changed files with 187 additions and 389 deletions

View File

@@ -10,23 +10,22 @@
import {CacheInfo, ImageId, ImageData, ImagesList} from './api'; import {CacheInfo, ImageId, ImageData, ImagesList} from './api';
import {ImageEventWithId} from './index'; import {ImageEventWithId} from './index';
import {styled, Layout, Toolbar, theme} from 'flipper-plugin';
import { import {
Toolbar,
Button, Button,
Spacer, Switch,
colors, Empty,
FlexBox, Skeleton,
FlexRow, Typography,
FlexColumn, Image,
LoadingIndicator, Row,
styled, Col,
ToggleButton, Badge,
Text, } from 'antd';
} from 'flipper';
import MultipleSelect from './MultipleSelect'; import MultipleSelect from './MultipleSelect';
import {ImagesMap} from './ImagePool'; import {ImagesMap} from './ImagePool';
import {clipboard} from 'electron'; import React, {PureComponent} from 'react';
import React, {ChangeEvent, KeyboardEvent, PureComponent} from 'react'; import {DeleteFilled} from '@ant-design/icons';
function formatMB(bytes: number) { function formatMB(bytes: number) {
return Math.floor(bytes / (1024 * 1024)) + 'MB'; return Math.floor(bytes / (1024 * 1024)) + 'MB';
@@ -42,26 +41,16 @@ type ToggleProps = {
toggled: boolean; toggled: boolean;
}; };
const ToolbarToggleButton = styled(ToggleButton)(() => ({
alignSelf: 'center',
marginRight: 4,
minWidth: 30,
}));
const ToggleLabel = styled(Text)(() => ({
whiteSpace: 'nowrap',
}));
function Toggle(props: ToggleProps) { function Toggle(props: ToggleProps) {
return ( return (
<> <>
<ToolbarToggleButton <Switch
onClick={() => { onClick={() => {
props.onClick && props.onClick(!props.toggled); props.onClick && props.onClick(!props.toggled);
}} }}
toggled={props.toggled} checked={props.toggled}
/> />
<ToggleLabel>{props.label}</ToggleLabel> <Typography.Text>{props.label}</Typography.Text>
</> </>
); );
} }
@@ -104,39 +93,11 @@ export default class ImagesCacheOverview extends PureComponent<
size: 150, size: 150,
}; };
static Container = styled(FlexColumn)({
backgroundColor: colors.white,
});
static Content = styled(FlexColumn)({
flex: 1,
overflow: 'auto',
});
static Empty = styled(FlexBox)({
alignItems: 'center',
height: '100%',
justifyContent: 'center',
width: '100%',
});
onImageSelected = (selectedImage: ImageId) => { onImageSelected = (selectedImage: ImageId) => {
this.setState({selectedImage}); this.setState({selectedImage});
this.props.onImageSelected(selectedImage); this.props.onImageSelected(selectedImage);
}; };
onKeyDown = (e: KeyboardEvent) => {
const selectedImage = this.state.selectedImage;
const imagesMap = this.props.imagesMap;
if (selectedImage) {
if ((e.ctrlKey || e.metaKey) && e.key === 'c') {
clipboard.writeText(String(imagesMap[selectedImage]));
e.preventDefault();
}
}
};
onEnableDebugOverlayToggled = () => { onEnableDebugOverlayToggled = () => {
this.props.onEnableDebugOverlay(!this.props.isDebugOverlayEnabled); this.props.onEnableDebugOverlay(!this.props.isDebugOverlayEnabled);
}; };
@@ -145,9 +106,6 @@ export default class ImagesCacheOverview extends PureComponent<
this.props.onEnableAutoRefresh(!this.props.isAutoRefreshEnabled); this.props.onEnableAutoRefresh(!this.props.isAutoRefreshEnabled);
}; };
onChangeSize = (e: ChangeEvent<HTMLInputElement>) =>
this.setState({size: parseInt(e.target.value, 10)});
onSurfaceOptionsChange = (selectedItem: string, checked: boolean) => { onSurfaceOptionsChange = (selectedItem: string, checked: boolean) => {
const {allSurfacesOption, surfaceOptions} = this.props; const {allSurfacesOption, surfaceOptions} = this.props;
const selectedSurfaces = new Set([...this.props.selectedSurfaces]); const selectedSurfaces = new Set([...this.props.selectedSurfaces]);
@@ -190,12 +148,11 @@ export default class ImagesCacheOverview extends PureComponent<
) > 0; ) > 0;
return ( return (
<ImagesCacheOverview.Container <Layout.Top>
grow <Toolbar>
onKeyDown={this.onKeyDown} <Button
tabIndex={0}> icon={<DeleteFilled></DeleteFilled>}
<Toolbar position="top"> onClick={this.props.onTrimMemory}>
<Button icon="trash" onClick={this.props.onTrimMemory}>
Trim Memory Trim Memory
</Button> </Button>
<Button onClick={this.props.onRefresh}>Refresh</Button> <Button onClick={this.props.onRefresh}>Refresh</Button>
@@ -230,21 +187,13 @@ export default class ImagesCacheOverview extends PureComponent<
onClick={this.props.onShowDiskImages} onClick={this.props.onShowDiskImages}
label="Show Disk Images" label="Show Disk Images"
/> />
<Spacer />
<input
type="range"
onChange={this.onChangeSize}
min={50}
max={150}
value={this.state.size}
/>
</Toolbar> </Toolbar>
{!hasImages ? ( {!hasImages ? (
<ImagesCacheOverview.Empty> <Layout.Container pad>
<LoadingIndicator size={50} /> <Empty />
</ImagesCacheOverview.Empty> </Layout.Container>
) : ( ) : (
<ImagesCacheOverview.Content> <Layout.ScrollContainer>
{this.props.images.map((data: CacheInfo, index: number) => { {this.props.images.map((data: CacheInfo, index: number) => {
const maxSize = data.maxSizeBytes; const maxSize = data.maxSizeBytes;
const subtitle = maxSize const subtitle = maxSize
@@ -263,15 +212,14 @@ export default class ImagesCacheOverview extends PureComponent<
onImageSelected={this.onImageSelected} onImageSelected={this.onImageSelected}
selectedImage={this.state.selectedImage} selectedImage={this.state.selectedImage}
imagesMap={this.props.imagesMap} imagesMap={this.props.imagesMap}
size={this.state.size}
events={this.props.events} events={this.props.events}
onClear={onClear} onClear={onClear}
/> />
); );
})} })}
</ImagesCacheOverview.Content> </Layout.ScrollContainer>
)} )}
</ImagesCacheOverview.Container> </Layout.Top>
); );
} }
} }
@@ -284,7 +232,6 @@ class ImageGrid extends PureComponent<{
onImageSelected: (image: ImageId) => void; onImageSelected: (image: ImageId) => void;
onClear: (() => void) | undefined; onClear: (() => void) | undefined;
imagesMap: ImagesMap; imagesMap: ImagesMap;
size: number;
events: Array<ImageEventWithId>; events: Array<ImageEventWithId>;
}> { }> {
static Content = styled.div({ static Content = styled.div({
@@ -298,31 +245,48 @@ class ImageGrid extends PureComponent<{
return null; 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 ( return (
<> <Layout.Container gap>
<ImageGridHeader <ImageGridHeader
key="header" key="header"
title={this.props.title} title={this.props.title}
subtitle={this.props.subtitle} subtitle={this.props.subtitle}
onClear={this.props.onClear} onClear={this.props.onClear}
/> />
<ImageGrid.Content key="content">
{images.map((imageId) => ( <Layout.Container pad>
{imageRows.map((row, rowIndex) => (
<Layout.Container pad key={rowIndex}>
<Row key={rowIndex} align={'middle'} gutter={[8, 24]}>
{row.map((imageId, colIndex) => (
<Col key={colIndex} span={24 / ROW_SIZE}>
<ImageItem <ImageItem
imageId={imageId} imageId={imageId}
image={this.props.imagesMap[imageId]} image={this.props.imagesMap[imageId]}
key={imageId} key={imageId}
selected={selectedImage != null && selectedImage === imageId} selected={
selectedImage != null && selectedImage === imageId
}
onSelected={onImageSelected} onSelected={onImageSelected}
size={this.props.size}
numberOfRequests={ numberOfRequests={
this.props.events.filter((e) => e.imageIds.includes(imageId)) this.props.events.filter((e) =>
.length e.imageIds.includes(imageId),
).length
} }
/> />
</Col>
))} ))}
</ImageGrid.Content> </Row>
</> </Layout.Container>
))}
</Layout.Container>
</Layout.Container>
); );
} }
} }
@@ -332,56 +296,23 @@ class ImageGridHeader extends PureComponent<{
subtitle: string; subtitle: string;
onClear: (() => void) | undefined; onClear: (() => void) | undefined;
}> { }> {
static Container = styled(FlexRow)({
color: colors.dark70,
paddingTop: 10,
paddingBottom: 10,
marginLeft: 15,
marginRight: 15,
marginBottom: 15,
borderBottom: `1px solid ${colors.light10}`,
flexShrink: 0,
alignItems: 'center',
position: 'sticky',
top: 0,
left: 0,
right: 0,
backgroundColor: colors.white,
zIndex: 3,
});
static Heading = styled.span({
fontSize: 22,
fontWeight: 600,
});
static Subtitle = styled.span({ static Subtitle = styled.span({
fontSize: 22, fontSize: 22,
fontWeight: 300, fontWeight: 300,
marginLeft: 15,
});
static ClearButton = styled(Button)({
alignSelf: 'center',
height: 30,
marginLeft: 'auto',
width: 100,
}); });
render() { render() {
return ( return (
<ImageGridHeader.Container> <Layout.Horizontal gap pad grow borderBottom>
<ImageGridHeader.Heading>{this.props.title}</ImageGridHeader.Heading> <Typography.Title>{this.props.title}</Typography.Title>
<ImageGridHeader.Subtitle> <ImageGridHeader.Subtitle>
{this.props.subtitle} {this.props.subtitle}
</ImageGridHeader.Subtitle> </ImageGridHeader.Subtitle>
<Spacer />
{this.props.onClear ? ( {this.props.onClear ? (
<ImageGridHeader.ClearButton onClick={this.props.onClear}> <Button onClick={this.props.onClear}>Clear Cache</Button>
Clear Cache
</ImageGridHeader.ClearButton>
) : null} ) : null}
</ImageGridHeader.Container> </Layout.Horizontal>
); );
} }
} }
@@ -394,36 +325,16 @@ class ImageItem extends PureComponent<{
size: number; size: number;
numberOfRequests: number; numberOfRequests: number;
}> { }> {
static Container = styled(FlexBox)<{size: number}>((props) => ({ static defaultProps = {
float: 'left', size: 150,
alignItems: 'center', };
justifyContent: 'center',
flexShrink: 0,
height: props.size,
width: props.size,
borderRadius: 4,
marginRight: 15,
marginBottom: 15,
backgroundColor: colors.light02,
}));
static Image = styled.img({
borderRadius: 4,
maxHeight: '100%',
maxWidth: '100%',
objectFit: 'contain',
});
static Loading = styled.span({
padding: '0 0',
});
static SelectedHighlight = styled.div<{selected: boolean}>((props) => ({ static SelectedHighlight = styled.div<{selected: boolean}>((props) => ({
borderColor: colors.highlight, borderColor: theme.primaryColor,
borderStyle: 'solid', borderStyle: 'solid',
borderWidth: props.selected ? 3 : 0, borderWidth: props.selected ? 3 : 0,
borderRadius: 4, borderRadius: 4,
boxShadow: props.selected ? `inset 0 0 0 1px ${colors.white}` : 'none', boxShadow: props.selected ? `inset 0 0 0 1px ${theme.white}` : 'none',
bottom: 0, bottom: 0,
left: 0, left: 0,
position: 'absolute', position: 'absolute',
@@ -431,10 +342,12 @@ class ImageItem extends PureComponent<{
top: 0, top: 0,
})); }));
static HoverOverlay = styled(FlexColumn)<{selected: boolean; size: number}>( static HoverOverlay = styled(Layout.Container)<{
(props) => ({ selected: boolean;
size: number;
}>((props) => ({
alignItems: 'center', alignItems: 'center',
backgroundColor: colors.whiteAlpha80, backgroundColor: 'rgba(255,255,255,0.8)',
bottom: props.selected ? 4 : 0, bottom: props.selected ? 4 : 0,
fontSize: props.size > 100 ? 16 : 11, fontSize: props.size > 100 ? 16 : 11,
justifyContent: 'center', justifyContent: 'center',
@@ -448,8 +361,7 @@ class ImageItem extends PureComponent<{
'&:hover': { '&:hover': {
opacity: 1, opacity: 1,
}, },
}), }));
);
static MemoryLabel = styled.span({ static MemoryLabel = styled.span({
fontWeight: 600, fontWeight: 600,
@@ -460,25 +372,13 @@ class ImageItem extends PureComponent<{
fontWeight: 300, fontWeight: 300,
}); });
static Events = styled.div({ static EventBadge = styled(Badge)({
position: 'absolute', position: 'absolute',
top: -5, top: 0,
right: -5, right: 0,
color: colors.white, zIndex: 1,
backgroundColor: colors.highlight,
fontWeight: 600,
borderRadius: 10,
fontSize: '0.85em',
zIndex: 2,
lineHeight: '20px',
width: 20,
textAlign: 'center',
}); });
static defaultProps = {
size: 150,
};
onClick = () => { onClick = () => {
this.props.onSelected(this.props.imageId); this.props.onSelected(this.props.imageId);
}; };
@@ -487,14 +387,14 @@ class ImageItem extends PureComponent<{
const {image, selected, size, numberOfRequests} = this.props; const {image, selected, size, numberOfRequests} = this.props;
return ( return (
<ImageItem.Container onClick={this.onClick} size={size}> <Layout.Container onClick={this.onClick} gap>
{numberOfRequests > 0 && image != null && ( {numberOfRequests > 0 && image != null && (
<ImageItem.Events>{numberOfRequests}</ImageItem.Events> <ImageItem.EventBadge count={numberOfRequests}></ImageItem.EventBadge>
)} )}
{image != null ? ( {image != null ? (
<ImageItem.Image src={image.data} /> <Image src={image.data} preview={false} />
) : ( ) : (
<LoadingIndicator size={25} /> <Skeleton.Image />
)} )}
<ImageItem.SelectedHighlight selected={selected} /> <ImageItem.SelectedHighlight selected={selected} />
{image != null && ( {image != null && (
@@ -507,7 +407,7 @@ class ImageItem extends PureComponent<{
</ImageItem.SizeLabel> </ImageItem.SizeLabel>
</ImageItem.HoverOverlay> </ImageItem.HoverOverlay>
)} )}
</ImageItem.Container> </Layout.Container>
); );
} }
} }

View File

@@ -9,20 +9,15 @@
import {ImageData} from './api'; import {ImageData} from './api';
import {ImageEventWithId} from './index'; import {ImageEventWithId} from './index';
import React, {Component} from 'react';
import { import {
Component, Layout,
ContextMenu, theme,
DataDescription,
Text,
Panel,
ManagedDataInspector,
FlexColumn,
FlexRow,
colors,
styled, styled,
} from 'flipper'; DataDescription,
import React from 'react'; Panel,
import {clipboard, MenuItemConstructorOptions} from 'electron'; DataInspector,
} from 'flipper-plugin';
type ImagesSidebarProps = { type ImagesSidebarProps = {
image: ImageData; image: ImageData;
@@ -32,10 +27,10 @@ type ImagesSidebarProps = {
type ImagesSidebarState = {}; type ImagesSidebarState = {};
const DataDescriptionKey = styled.span({ const DataDescriptionKey = styled.span({
color: colors.grapeDark1, color: theme.textColorPrimary,
}); });
const WordBreakFlexColumn = styled(FlexColumn)({ const WordBreakFlexColumn = styled(Layout.Container)({
wordBreak: 'break-all', wordBreak: 'break-all',
}); });
@@ -62,37 +57,22 @@ export default class ImagesSidebar extends Component<
return null; return null;
} }
const contextMenuItems: MenuItemConstructorOptions[] = [
{
label: 'Copy URI',
click: () => clipboard.writeText(this.props.image.uri!),
},
];
return ( return (
<Panel heading="Sources" floating={false}> <Panel title="Sources" pad>
<FlexRow>
<FlexColumn>
<DataDescriptionKey>URI</DataDescriptionKey>
</FlexColumn>
<FlexColumn>
<span key="sep">:&nbsp;</span>
</FlexColumn>
<WordBreakFlexColumn> <WordBreakFlexColumn>
<ContextMenu component="span" items={contextMenuItems}> <span>
URI<span key="sep">: </span>
<DataDescription <DataDescription
type="string" type="string"
value={this.props.image.uri} value={this.props.image.uri}
setValue={null} setValue={null}
/> />
</ContextMenu> </span>
</WordBreakFlexColumn> </WordBreakFlexColumn>
</FlexRow>
</Panel> </Panel>
); );
} }
} }
class EventDetails extends Component<{ class EventDetails extends Component<{
event: ImageEventWithId; event: ImageEventWithId;
}> { }> {
@@ -100,11 +80,11 @@ class EventDetails extends Component<{
const {event} = this.props; const {event} = this.props;
return ( return (
<Panel heading={<RequestHeader event={event} />} floating={false} padded> <Panel title={requestHeader(event)} pad>
<p> <p>
<DataDescriptionKey>Attribution</DataDescriptionKey> <DataDescriptionKey>Attribution</DataDescriptionKey>
<span key="sep">: </span> <span key="sep">: </span>
<ManagedDataInspector data={event.attribution} /> <DataInspector data={event.attribution} />
</p> </p>
<p> <p>
<DataDescriptionKey>Time start</DataDescriptionKey> <DataDescriptionKey>Time start</DataDescriptionKey>
@@ -163,24 +143,11 @@ class EventDetails extends Component<{
} }
} }
class RequestHeader extends Component<{ function requestHeader(event: ImageEventWithId) {
event: ImageEventWithId; const date = new Date(event.startTime);
}> { const dateString = `${date.toTimeString().split(' ')[0]}.${(
dateString = (timestamp: number) => {
const date = new Date(timestamp);
return `${date.toTimeString().split(' ')[0]}.${(
'000' + date.getMilliseconds() '000' + date.getMilliseconds()
).substr(-3)}`; ).substr(-3)}`;
};
render() { return (event.viewport ? 'Request' : 'Prefetch') + ' at ' + dateString;
const {event} = this.props;
const durationMs = event.endTime - event.startTime;
return (
<Text>
{event.viewport ? 'Request' : 'Prefetch'} at{' '}
{this.dateString(event.startTime)} ({durationMs}ms)
</Text>
);
}
} }

View File

@@ -7,105 +7,47 @@
* @format * @format
*/ */
import {Block, Button, colors, FlexColumn, styled, Glyph} from 'flipper'; import React, {Component} from 'react';
import React, {ChangeEvent, Component} from 'react'; import {Layout} from 'flipper-plugin';
import {Button, Menu, Checkbox, Dropdown} from 'antd';
import {DownOutlined} from '@ant-design/icons';
import type {CheckboxChangeEvent} from 'antd/lib/checkbox';
const Container = styled(Block)({ export default class MultipleSelect extends Component<{
position: 'relative',
marginLeft: '10px',
});
const List = styled(FlexColumn)<{visibleList: boolean}>((props) => ({
display: props.visibleList ? 'flex' : 'none',
position: 'absolute',
top: '32px',
left: 0,
zIndex: 4,
width: 'auto',
minWidth: '200px',
backgroundColor: colors.white,
borderWidth: '1px',
borderStyle: 'solid',
borderColor: colors.macOSTitleBarButtonBorderBottom,
borderRadius: 4,
}));
const ListItem = styled.label({
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
width: '100%',
color: colors.light50,
fontSize: '11px',
padding: '0 5px',
'&:hover': {
backgroundColor: colors.macOSTitleBarButtonBackgroundActiveHighlight,
},
});
const Checkbox = styled.input({
display: 'inline-block',
marginRight: 5,
verticalAlign: 'middle',
});
const StyledGlyph = styled(Glyph)({
marginLeft: '4px',
});
type State = {
visibleList: boolean;
};
export default class MultipleSelect extends Component<
{
selected: Set<string>; selected: Set<string>;
options: Set<string>; options: Set<string>;
onChange: (selectedItem: string, checked: boolean) => void;
label: string; label: string;
}, onChange: (selectedItem: string, checked: boolean) => void;
State }> {
> { handleOnChange = (option: string, event: CheckboxChangeEvent) => {
state = { this.props.onChange(option, event.target.checked);
visibleList: false,
}; };
handleOnChange = (event: ChangeEvent<HTMLInputElement>) => { menu = () => {
const { return (
target: {value, checked}, <Menu>
} = event; {Array.from(this.props.options).map((option, index) => (
this.props.onChange(value, checked); <Menu.Item key={index} disabled>
<Checkbox
onChange={(e) => this.handleOnChange(option, e)}
checked={this.props.selected.has(option)}
/>{' '}
{option}
</Menu.Item>
))}
</Menu>
);
}; };
toggleList = () => this.setState({visibleList: !this.state.visibleList});
render() { render() {
const {selected, label, options} = this.props;
const {visibleList} = this.state;
const icon = visibleList ? 'chevron-up' : 'chevron-down';
return ( return (
<Container> <Layout.Container>
<Button onClick={this.toggleList}> <Dropdown overlay={this.menu}>
{label} <StyledGlyph name={icon} /> <Button>
Surfaces <DownOutlined />
</Button> </Button>
<List visibleList={visibleList}> </Dropdown>
{Array.from(options).map((option, index) => ( </Layout.Container>
<ListItem key={index}>
<Checkbox
onChange={this.handleOnChange}
checked={selected.has(option)}
value={option}
type="checkbox"
/>
{option}
</ListItem>
))}
</List>
</Container>
); );
} }
} }

View File

@@ -17,19 +17,21 @@ import {
AndroidCloseableReferenceLeakEvent, AndroidCloseableReferenceLeakEvent,
CacheInfo, CacheInfo,
} from './api'; } from './api';
import {Fragment} from 'react';
import {ImagesMap} from './ImagePool'; import {ImagesMap} from './ImagePool';
import {PluginClient, createState, usePlugin, useValue} from 'flipper-plugin'; import {
PluginClient,
createState,
usePlugin,
useValue,
DetailSidebar,
Layout,
} from 'flipper-plugin';
import React from 'react'; import React from 'react';
import ImagesCacheOverview from './ImagesCacheOverview'; import ImagesCacheOverview from './ImagesCacheOverview';
import { import {isProduction} from 'flipper';
FlexRow,
Text, import {Typography} from 'antd';
DetailSidebar,
colors,
styled,
isProduction,
} from 'flipper';
import ImagesSidebar from './ImagesSidebar'; import ImagesSidebar from './ImagesSidebar';
import ImagePool from './ImagePool'; import ImagePool from './ImagePool';
@@ -38,18 +40,6 @@ export type AllImageEventsInfo = {
events: Array<ImageEventWithId>; events: Array<ImageEventWithId>;
}; };
const EmptySidebar = styled(FlexRow)({
alignItems: 'center',
justifyContent: 'center',
color: colors.light30,
padding: 15,
fontSize: 16,
});
export const InlineFlexRow = styled(FlexRow)({
display: 'inline-block',
});
const surfaceDefaultText = 'SELECT ALL SURFACES'; const surfaceDefaultText = 'SELECT ALL SURFACES';
const debugLog = (...args: any[]) => { const debugLog = (...args: any[]) => {
@@ -113,18 +103,17 @@ export function plugin(client: PluginClient<Events, Methods>) {
id: event.identityHashCode, id: event.identityHashCode,
title: `Leaked CloseableReference: ${event.className}`, title: `Leaked CloseableReference: ${event.className}`,
message: ( message: (
<Fragment> <Layout.Container>
<InlineFlexRow> <Typography.Text>CloseableReference leaked for </Typography.Text>
CloseableReference leaked for <Text code>{event.className}</Text> <Typography.Text code>{event.className}</Typography.Text>
<Typography.Text>
(identity hashcode: {event.identityHashCode}). (identity hashcode: {event.identityHashCode}).
</InlineFlexRow> </Typography.Text>
<InlineFlexRow> <Typography.Text strong>Stacktrace:</Typography.Text>
<Text bold>Stacktrace:</Text> <Typography.Text code>
</InlineFlexRow> {event.stacktrace || '<unavailable>'}
<InlineFlexRow> </Typography.Text>
<Text code>{event.stacktrace || '<unavailable>'}</Text> </Layout.Container>
</InlineFlexRow>
</Fragment>
), ),
severity: 'error', severity: 'error',
category: 'closeablereference_leak', category: 'closeablereference_leak',
@@ -465,11 +454,11 @@ function Sidebar() {
if (currentSelectedImage == null) { if (currentSelectedImage == null) {
return ( return (
<EmptySidebar grow> <Layout.Container pad>
<Text align="center"> <Typography.Text>
Select an image to see the events associated with it. Select an image to see the events associated with it.
</Text> </Typography.Text>
</EmptySidebar> </Layout.Container>
); );
} }