diff --git a/src/NotificationsHub.js b/src/NotificationsHub.js index eafc75572..d437ea1ab 100644 --- a/src/NotificationsHub.js +++ b/src/NotificationsHub.js @@ -7,12 +7,13 @@ import type {SearchableProps, FlipperBasePlugin, Device} from 'flipper'; import type {PluginNotification} from './reducers/notifications'; -import {selectPlugin} from './reducers/connections'; +import type Logger from './fb-stubs/Logger'; import { FlipperDevicePlugin, Searchable, Button, + ButtonGroup, FlexBox, FlexColumn, FlexRow, @@ -29,7 +30,9 @@ import PropTypes from 'prop-types'; import { clearAllNotifications, updatePluginBlacklist, + updateCategoryBlacklist, } from './reducers/notifications'; +import {selectPlugin} from './reducers/connections'; import {createPaste, textContent} from './utils/index'; export default class Notifications extends FlipperDevicePlugin<{}> { @@ -57,19 +60,30 @@ export default class Notifications extends FlipperDevicePlugin<{}> { }; render() { + const { + blacklistedPlugins, + blacklistedCategories, + } = this.context.store.getState().notifications; return ( ({ + logger={this.props.logger} + defaultFilters={[ + ...blacklistedPlugins.map(value => ({ value, invertible: false, type: 'exclude', key: 'plugin', - }))} + })), + ...blacklistedCategories.map(value => ({ + value, + invertible: false, + type: 'exclude', + key: 'category', + })), + ]} actions={ @@ -85,14 +99,17 @@ type Props = {| activeNotifications: Array, invalidatedNotifications: Array, blacklistedPlugins: Array, + blacklistedCategories: Array, onClear: () => void, updatePluginBlacklist: (blacklist: Array) => mixed, + updateCategoryBlacklist: (blacklist: Array) => mixed, selectPlugin: ({ selectedPlugin: ?string, selectedApp: ?string, deepLinkPayload?: ?string, }) => mixed, selectedID: ?string, + logger: Logger, |}; type State = {| @@ -131,12 +148,6 @@ const NoContent = styled(FlexColumn)({ }); class NotificationsTable extends Component { - static getDerivedStateFromProps(props: Props): State { - return { - selectedNotification: props.selectedID, - }; - } - contextMenuItems = [{label: 'Clear all', click: this.props.onClear}]; state: State = { selectedNotification: this.props.selectedID, @@ -149,10 +160,27 @@ class NotificationsTable extends Component { .filter(f => f.type === 'exclude' && f.key.toLowerCase() === 'plugin') .map(f => String(f.value)), ); + + this.props.updateCategoryBlacklist( + this.props.filters + .filter( + f => f.type === 'exclude' && f.key.toLowerCase() === 'category', + ) + .map(f => String(f.value)), + ); + } + + if ( + this.props.selectedID && + prevProps.selectedID !== this.props.selectedID + ) { + this.setState({ + selectedNotification: this.props.selectedID, + }); } } - onHide = (pluginId: string) => { + onHidePlugin = (pluginId: string) => { // add filter to searchbar this.props.addFilter({ value: pluginId, @@ -165,17 +193,43 @@ class NotificationsTable extends Component { ); }; + onHideCategory = (category: string) => { + // add filter to searchbar + this.props.addFilter({ + value: category, + type: 'exclude', + key: 'category', + invertible: false, + }); + this.props.updatePluginBlacklist( + this.props.blacklistedCategories.concat(category), + ); + }; + getFilter = (): ((n: PluginNotification) => boolean) => ( n: PluginNotification, ) => { const searchTerm = this.props.searchTerm.toLowerCase(); - const blacklist = new Set( + + // filter plugins + const blacklistedPlugins = new Set( this.props.blacklistedPlugins.map(p => p.toLowerCase()), ); - if (blacklist.has(n.pluginId.toLowerCase())) { + if (blacklistedPlugins.has(n.pluginId.toLowerCase())) { return false; } + // filter categories + const {category} = n.notification; + if (category) { + const blacklistedCategories = new Set( + this.props.blacklistedCategories.map(p => p.toLowerCase()), + ); + if (blacklistedCategories.has(category.toLowerCase())) { + return false; + } + } + if (searchTerm.length === 0) { return true; } else if (n.notification.title.toLowerCase().indexOf(searchTerm) > -1) { @@ -192,19 +246,27 @@ class NotificationsTable extends Component { render() { const activeNotifications = this.props.activeNotifications .filter(this.getFilter()) - .map((n: PluginNotification) => ( - - this.setState({selectedNotification: n.notification.id}) - } - onClear={this.props.onClear} - onHide={() => this.onHide(n.pluginId)} - selectPlugin={this.props.selectPlugin} - /> - )) + .map((n: PluginNotification) => { + const {category} = n.notification; + + return ( + + this.setState({selectedNotification: n.notification.id}) + } + onClear={this.props.onClear} + onHidePlugin={() => this.onHidePlugin(n.pluginId)} + onHideCategory={ + category ? () => this.onHideCategory(category) : undefined + } + selectPlugin={this.props.selectPlugin} + logger={this.props.logger} + /> + ); + }) .reverse(); const invalidatedNotifications = this.props.invalidatedNotifications @@ -255,14 +317,17 @@ const ConnectedNotificationsTable = connect( activeNotifications, invalidatedNotifications, blacklistedPlugins, + blacklistedCategories, }, }) => ({ activeNotifications, invalidatedNotifications, blacklistedPlugins, + blacklistedCategories, }), { updatePluginBlacklist, + updateCategoryBlacklist, selectPlugin, }, )(Searchable(NotificationsTable)); @@ -348,7 +413,7 @@ const NotificationButton = styled('div')({ borderRadius: 4, textAlign: 'center', padding: 4, - width: 55, + width: 80, marginBottom: 4, opacity: 0, transition: '0.15s opacity', @@ -365,8 +430,9 @@ const NotificationButton = styled('div')({ type ItemProps = { ...PluginNotification, - onClick?: () => mixed, - onHide?: () => mixed, + onHighlight?: () => mixed, + onHidePlugin?: () => mixed, + onHideCategory?: () => mixed, isSelected?: boolean, inactive?: boolean, selectPlugin?: ({ @@ -374,18 +440,29 @@ type ItemProps = { selectedApp: ?string, deepLinkPayload?: ?string, }) => mixed, + logger?: Logger, }; -class NotificationItem extends Component { +type ItemState = {| + reportedNotHelpful: boolean, +|}; + +class NotificationItem extends Component { constructor(props: ItemProps) { super(props); const plugin = plugins.find(p => p.id === props.pluginId); const items = []; - if (props.onHide && plugin) { + if (props.onHidePlugin && plugin) { items.push({ label: `Hide ${plugin.title} plugin`, - click: this.props.onHide, + click: this.props.onHidePlugin, + }); + } + if (props.onHideCategory) { + items.push({ + label: 'Hide Similar', + click: this.props.onHideCategory, }); } items.push( @@ -397,6 +474,7 @@ class NotificationItem extends Component { this.plugin = plugin; } + state = {reportedNotHelpful: false}; plugin: ?Class>; contextMenuItems; deepLinkButton = React.createRef(); @@ -429,8 +507,37 @@ class NotificationItem extends Component { } }; + reportNotUseful = (e: UIEvent) => { + e.preventDefault(); + e.stopPropagation(); + if (this.props.logger) { + this.props.logger.track( + 'usage', + 'notification-not-useful', + this.props.notification, + ); + } + this.setState({reportedNotHelpful: true}); + }; + + onHide = (e: UIEvent) => { + e.preventDefault(); + e.stopPropagation(); + if (this.props.onHideCategory) { + this.props.onHideCategory(); + } else if (this.props.onHidePlugin) { + this.props.onHidePlugin(); + } + }; + render() { - const {notification, isSelected, inactive, onHide} = this.props; + const { + notification, + isSelected, + inactive, + onHidePlugin, + onHideCategory, + } = this.props; const {action} = notification; return ( @@ -438,7 +545,7 @@ class NotificationItem extends Component { data-role="notification" component={NotificationBox} severity={notification.severity} - onClick={this.props.onClick} + onClick={this.props.onHighlight} isSelected={isSelected} inactive={inactive} items={this.contextMenuItems}> @@ -449,7 +556,7 @@ class NotificationItem extends Component { {!inactive && isSelected && this.plugin && - (action || onHide) && ( + (action || onHidePlugin || onHideCategory) && ( {action && ( @@ -457,9 +564,16 @@ class NotificationItem extends Component { Open in {this.plugin.title} )} - {onHide && ( - - )} + + {onHideCategory && ( + + )} + {onHidePlugin && ( + + )} + {notification.timestamp @@ -478,10 +592,14 @@ class NotificationItem extends Component { Open )} - {onHide && ( - + {this.state.reportedNotHelpful ? ( + Hide + ) : ( + + Not helpful + )} )} diff --git a/src/plugins/network/index.js b/src/plugins/network/index.js index 81c366279..cf97b7bef 100644 --- a/src/plugins/network/index.js +++ b/src/plugins/network/index.js @@ -143,9 +143,11 @@ export default class extends FlipperPlugin { persistedState: PersistedState, ): Array => { const responses = persistedState ? persistedState.responses || [] : []; + // $FlowFixMe Object.values returns Array, but we know it is Array + const r: Array = Object.values(responses); + return ( - // $FlowFixMe Object.values returns Array, but we know it is Array - (Object.values(responses): Array) + r // Show error messages for all status codes indicating a client or server error .filter((response: Response) => response.status >= 400) .map((response: Response) => ({ @@ -155,7 +157,7 @@ export default class extends FlipperPlugin { '(URL missing)'}" failed. ${response.reason}`, severity: 'error', timestamp: response.timestamp, - category: response.status, + category: `HTTP${response.status}`, action: response.id, })) );