Add State to Notification Component

Summary:
This diff covers notification items to show data stored in the state as well as add button functionality to plugins.

This diff doesn't cover notification search, clear, and setting, which will be covered in next diffs

Reviewed By: mweststrate

Differential Revision: D24986742

fbshipit-source-id: d42cfe8112881a7f0d2158cbce267a4d7c344305
This commit is contained in:
Chaiwat Ekkaewnumchai
2020-11-17 05:36:47 -08:00
committed by Facebook GitHub Bot
parent 41c7e023a9
commit 2c9219f99a

View File

@@ -7,9 +7,9 @@
* @format * @format
*/ */
import React from 'react'; import React, {useCallback, useMemo} from 'react';
import {Layout, theme} from 'flipper-plugin'; import {Layout, theme} from 'flipper-plugin';
import {styled} from '../../ui'; import {styled, Glyph} from '../../ui';
import {Input, Typography, Button, Collapse} from 'antd'; import {Input, Typography, Button, Collapse} from 'antd';
import { import {
DownOutlined, DownOutlined,
@@ -20,53 +20,23 @@ import {
DeleteOutlined, DeleteOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import {LeftSidebar, SidebarTitle} from '../LeftSidebar'; import {LeftSidebar, SidebarTitle} from '../LeftSidebar';
import {PluginNotification} from '../../reducers/notifications'; import {Notification as NotificationData} from '../../plugin';
import {useStore, useDispatch} from '../../utils/useStore';
import {ClientQuery} from '../../Client';
import {deconstructClientId} from '../../utils/clientUtils';
import {selectPlugin} from '../../reducers/connections';
type NotificationExtra = {
onOpen: () => void;
clientName: string | undefined;
appName: string | undefined;
pluginName: string;
iconName: string | null | undefined;
};
type PluginNotification = NotificationData & NotificationExtra;
const {Title, Text, Paragraph} = Typography; const {Title, Text, Paragraph} = Typography;
// NOTE: remove after the component link to state
const notificationExample: Array<PluginNotification> = [
{
notification: {
id: 'testid_0',
title: `
CRASH: FATAL EXCEPTION:
mainReason: java.lang.RuntimeException: Artificially triggered crash from Flipper sample app
`,
message:
'very very very very very very very very very very very very very very very very very very very very very long',
severity: 'error',
},
pluginId: 'testPluginId',
client: 'iPortaldroid',
},
{
notification: {
id: 'testid_1',
title: `CRASH: FATAL EXCEPTION:
mainReason: java.lang.RuntimeException: Artificially triggered crash from Flipper sample app
`,
message: `FATAL EXCEPTION: main`,
severity: 'error',
},
pluginId: 'testPluginId',
client: 'iPortaldroid',
},
{
notification: {
id: 'testid_2',
action: '1',
title: `CRASH: FATAL EXCEPTION: mainReason: java.lang.RuntimeException: Artificially triggered`,
message: `Callstack: FATAL EXCEPTION: main Process: com.facebook.flipper.sample, PID: 1646 java.lang.RuntimeException: Artificially triggered crash from Flipper sample app at com.facebook.flipper.sample.RootComponentSpec`,
severity: 'error',
category:
'java.lang.RuntimeException: Artificially triggered crash from Flipper sample app',
},
pluginId: 'CrashReporter',
client: 'emulator-5554',
},
];
const CollapseContainer = styled.div({ const CollapseContainer = styled.div({
'.ant-collapse-ghost .ant-collapse-item': { '.ant-collapse-ghost .ant-collapse-item': {
'& > .ant-collapse-header': { '& > .ant-collapse-header': {
@@ -119,27 +89,39 @@ function DetailCollapse({detail}: {detail: string | React.ReactNode}) {
} }
function NotificationEntry({notification}: {notification: PluginNotification}) { function NotificationEntry({notification}: {notification: PluginNotification}) {
const {notification: content, pluginId, client} = notification; const {
// TODO: figure out how to transform app name to icon onOpen,
const icon = React.createElement(ExclamationCircleOutlined, { message,
style: {color: theme.primaryColor}, title,
}); clientName,
appName,
pluginName,
iconName,
} = notification;
const icon = iconName ? (
<Glyph name={iconName} size={16} color={theme.primaryColor} />
) : (
<ExclamationCircleOutlined style={{color: theme.primaryColor}} />
);
return ( return (
<Layout.Container gap="small" pad="medium"> <Layout.Container gap="small" pad="medium">
<Layout.Horizontal gap="tiny" center> <Layout.Horizontal gap="tiny" center>
{icon} {icon}
<Text>{pluginId}</Text> <Text style={{fontSize: theme.fontSize.smallBody}}>{pluginName}</Text>
</Layout.Horizontal> </Layout.Horizontal>
<Title level={4} ellipsis={{rows: 2}}> <Title level={4} ellipsis={{rows: 2}}>
{content.title} {title}
</Title> </Title>
<Text type="secondary" style={{fontSize: theme.fontSize.smallBody}}> <Text type="secondary" style={{fontSize: theme.fontSize.smallBody}}>
{client} {clientName && appName
? `${clientName}/${appName}`
: clientName ?? appName ?? 'Not Connected'}
</Text> </Text>
<Button style={{width: 'fit-content'}} size="small"> <Button style={{width: 'fit-content'}} size="small" onClick={onOpen}>
Open Open {pluginName}
</Button> </Button>
<DetailCollapse detail={content.message} /> <DetailCollapse detail={message} />
</Layout.Container> </Layout.Container>
); );
} }
@@ -154,7 +136,7 @@ function NotificationList({
<Layout.Container> <Layout.Container>
{notifications.map((notification) => ( {notifications.map((notification) => (
<NotificationEntry <NotificationEntry
key={notification.notification.id} key={notification.id}
notification={notification} notification={notification}
/> />
))} ))}
@@ -164,6 +146,56 @@ function NotificationList({
} }
export function Notification() { export function Notification() {
const dispatch = useDispatch();
const clients = useStore((state) => state.connections.clients);
const getClientQuery = useCallback(
(id: string | null) =>
id !== null
? clients.reduce(
(query: ClientQuery | null, client) =>
client.id === id ? client.query : query,
null,
) ?? deconstructClientId(id)
: null,
[clients],
);
const clientPlugins = useStore((state) => state.plugins.clientPlugins);
const devicePlugins = useStore((state) => state.plugins.devicePlugins);
const getPlugin = useCallback(
(id: string) => clientPlugins.get(id) || devicePlugins.get(id),
[clientPlugins, devicePlugins],
);
const activeNotifications = useStore(
(state) => state.notifications.activeNotifications,
);
const displayedNotifications: Array<PluginNotification> = useMemo(
() =>
activeNotifications.map((noti) => {
const plugin = getPlugin(noti.pluginId);
const client = getClientQuery(noti.client);
return {
...noti.notification,
onOpen: () =>
dispatch(
selectPlugin({
selectedPlugin: noti.pluginId,
selectedApp: noti.client,
deepLinkPayload: noti.notification.action,
}),
),
clientName: client?.device_id,
appName: client?.app,
pluginName: plugin?.title ?? noti.pluginId,
iconName: plugin?.icon,
};
}),
[activeNotifications, getPlugin, getClientQuery, dispatch],
);
const actions = ( const actions = (
<div> <div>
<Layout.Horizontal gap="medium"> <Layout.Horizontal gap="medium">
@@ -181,7 +213,7 @@ export function Notification() {
<Input placeholder="Search..." prefix={<SearchOutlined />} /> <Input placeholder="Search..." prefix={<SearchOutlined />} />
</Layout.Container> </Layout.Container>
</Layout.Container> </Layout.Container>
<NotificationList notifications={notificationExample} /> <NotificationList notifications={displayedNotifications} />
</Layout.Top> </Layout.Top>
</LeftSidebar> </LeftSidebar>
); );