diff --git a/desktop/app/src/sandy-chrome/LeftRail.tsx b/desktop/app/src/sandy-chrome/LeftRail.tsx
index 9d2291c97..5107d477c 100644
--- a/desktop/app/src/sandy-chrome/LeftRail.tsx
+++ b/desktop/app/src/sandy-chrome/LeftRail.tsx
@@ -118,7 +118,10 @@ export function LeftRail({
}}
/>
} title="Plugin Manager" />
- } title="Notifications" />
+
state.notifications.activeNotifications.length,
+ );
+ return (
+ }
+ title="Notifications"
+ selected={toplevelSelection === 'notification'}
+ count={notificationCount}
+ onClick={() => setToplevelSelection('notification')}
+ />
+ );
+}
+
function DebugLogsButton({
toplevelSelection,
setToplevelSelection,
diff --git a/desktop/app/src/sandy-chrome/SandyApp.tsx b/desktop/app/src/sandy-chrome/SandyApp.tsx
index accc8731b..a879223e9 100644
--- a/desktop/app/src/sandy-chrome/SandyApp.tsx
+++ b/desktop/app/src/sandy-chrome/SandyApp.tsx
@@ -24,8 +24,13 @@ import {toggleLeftSidebarVisible} from '../reducers/application';
import {AppInspect} from './appinspect/AppInspect';
import PluginContainer from '../PluginContainer';
import {ContentContainer} from './ContentContainer';
+import {Notification} from './notification/Notification';
-export type ToplevelNavItem = 'appinspect' | 'flipperlogs' | undefined;
+export type ToplevelNavItem =
+ | 'appinspect'
+ | 'flipperlogs'
+ | 'notification'
+ | undefined;
export type ToplevelProps = {
toplevelSelection: ToplevelNavItem;
setToplevelSelection: (_newSelection: ToplevelNavItem) => void;
@@ -51,7 +56,8 @@ export function SandyApp({logger}: {logger: Logger}) {
const setToplevelSelection = useCallback(
(newSelection: ToplevelNavItem) => {
// toggle sidebar visibility if needed
- const hasLeftSidebar = newSelection === 'appinspect';
+ const hasLeftSidebar =
+ newSelection === 'appinspect' || newSelection === 'notification';
if (hasLeftSidebar) {
if (newSelection === toplevelSelection) {
dispatch(toggleLeftSidebarVisible());
@@ -76,10 +82,12 @@ export function SandyApp({logger}: {logger: Logger}) {
// eslint-disable-next-line
}, []);
- const leftMenuContent =
- leftSidebarVisible && toplevelSelection === 'appinspect' ? (
-
- ) : null;
+ const leftMenuContent = !leftSidebarVisible ? null : toplevelSelection ===
+ 'appinspect' ? (
+
+ ) : toplevelSelection === 'notification' ? (
+
+ ) : null;
return (
diff --git a/desktop/app/src/sandy-chrome/notification/Notification.tsx b/desktop/app/src/sandy-chrome/notification/Notification.tsx
new file mode 100644
index 000000000..5daad7784
--- /dev/null
+++ b/desktop/app/src/sandy-chrome/notification/Notification.tsx
@@ -0,0 +1,188 @@
+/**
+ * Copyright (c) Facebook, Inc. and its 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 React from 'react';
+import {Layout, styled} from '../../ui';
+import {Input, Typography, Button, Collapse} from 'antd';
+import {
+ DownOutlined,
+ UpOutlined,
+ SearchOutlined,
+ ExclamationCircleOutlined,
+ SettingOutlined,
+ DeleteOutlined,
+} from '@ant-design/icons';
+import {LeftSidebar, SidebarTitle} from '../LeftSidebar';
+import {PluginNotification} from '../../reducers/notifications';
+import {theme} from '../theme';
+
+const {Title, Text, Paragraph} = Typography;
+
+// NOTE: remove after the component link to state
+const notificationExample: Array = [
+ {
+ 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({
+ '.ant-collapse-ghost .ant-collapse-item': {
+ '& > .ant-collapse-header': {
+ paddingLeft: '16px',
+ },
+ '& > .ant-collapse-content > .ant-collapse-content-box': {
+ padding: 0,
+ },
+ },
+});
+
+function DetailCollapse({detail}: {detail: string | React.ReactNode}) {
+ const detailView =
+ typeof detail === 'string' ? (
+
+ {detail}
+
+ ) : (
+ detail
+ );
+ return (
+
+
+ isActive ? (
+
+ ) : (
+
+ )
+ }>
+
+ View detail
+
+ }>
+ {detailView}
+
+
+
+ );
+}
+
+function NotificationEntry({notification}: {notification: PluginNotification}) {
+ const {notification: content, pluginId, client} = notification;
+ // TODO: figure out how to transform app name to icon
+ const icon = React.createElement(ExclamationCircleOutlined, {
+ style: {color: theme.primaryColor},
+ });
+ return (
+
+
+ {icon}
+ {pluginId}
+
+
+ {content.title}
+
+
+ {client}
+
+
+
+
+ );
+}
+
+function NotificationList({
+ notifications,
+}: {
+ notifications: Array;
+}) {
+ return (
+
+
+ {notifications.map((notification) => (
+
+ ))}
+
+
+ );
+}
+
+export function Notification() {
+ const actions = (
+
+
+
+
+
+
+ );
+ return (
+
+
+
+ notifications
+
+ } />
+
+
+
+
+
+ );
+}