diff --git a/src-docs/src/components/guide_components.scss b/src-docs/src/components/guide_components.scss index 73cbebe954d..b147bd55ced 100644 --- a/src-docs/src/components/guide_components.scss +++ b/src-docs/src/components/guide_components.scss @@ -241,6 +241,85 @@ $guideZLevelHighest: $euiZLevel9 + 1000; top: 0; } +.guideDemo__notificationEventCircleIndicator { + display: inline-block; + width: $euiSize; + height: $euiSize; + background: $euiColorPrimary; + color: $euiColorEmptyShade; + font-size: $euiSizeM; + line-height: $euiSize; + text-align: center; + border-radius: 50%; +} + +.guideDemo__notificationEventHighlight { + background: transparentize($euiColorPrimary, .9); + border-radius: $euiBorderRadiusSmall; + padding: $euiSizeXS; +} + +.guideDemo__notificationEvent { + @include euiFontSizeS; + display: flex; + flex-direction: column; + + > * { + display: flex; + } + + .guideDemo__notificationEventCircleIndicator { + margin-right: $euiSizeXS; + } +} + +.guideDemo__notificationEventTopRow { + display: flex; + + > * { + position: relative; + margin-right: $euiSizeXS; + + &:last-child { + margin-right: 0; + } + } + + .guideDemo__notificationEventIcon { + margin: 0 $euiSizeXS; + } + + .guideDemo__notificationEventCircleIndicator { + position: absolute; + top: -$euiSizeS; + left: 0; + } +} + +.guideDemo__notificationEventMeta { + display: flex; + width: 100%; + margin: 0 $euiSizeS; + + .euiNotificationEventMeta { + width: 100%; + } +} + +.guideDemo__notificationEventSections { + margin-left: $euiSizeXL + $euiSizeXS; + display: flex; + flex-direction: column; + + > * { + margin-top: $euiSizeS; + } +} + +.guideDemo__notificationEventBadge { + flex: 1; +} + .euiDataGridRowCell--favoriteFranchise { background: transparentize($color: #800080, $amount: .95) !important; } @@ -259,6 +338,7 @@ $guideZLevelHighest: $euiZLevel9 + 1000; color: $euiColorAccentText; } + @import '../views/guidelines/index'; @import 'guide_section/index'; @import 'guide_rule/index'; diff --git a/src-docs/src/routes.js b/src-docs/src/routes.js index 7941d8cf446..5300239c182 100644 --- a/src-docs/src/routes.js +++ b/src-docs/src/routes.js @@ -145,7 +145,7 @@ import { ModalExample } from './views/modal/modal_example'; import { MutationObserverExample } from './views/mutation_observer/mutation_observer_example'; -import { NotificationEventsExample } from './views/notification_events/notification_events_example'; +import { NotificationEventExample } from './views/notification_event/notification_event_example'; import { OutsideClickDetectorExample } from './views/outside_click_detector/outside_click_detector_example'; @@ -393,7 +393,7 @@ const navigation = [ ImageExample, ListGroupExample, LoadingExample, - NotificationEventsExample, + NotificationEventExample, ProgressExample, StatExample, TextExample, diff --git a/src-docs/src/views/notification_event/notification_event.js b/src-docs/src/views/notification_event/notification_event.js new file mode 100644 index 00000000000..afecb67d6f3 --- /dev/null +++ b/src-docs/src/views/notification_event/notification_event.js @@ -0,0 +1,53 @@ +import React, { useState } from 'react'; +import { EuiPanel } from '../../../../src/components/panel'; +import { EuiContextMenuItem } from '../../../../src/components/context_menu'; +import { EuiNotificationEvent } from '../../../../src/components/notification/notification_event'; + +export default () => { + const [isRead, setIsRead] = useState(false); + + const onRead = (id, isRead) => { + setIsRead(!isRead); + }; + + const onOpenContextMenu = (id) => { + return [ + onRead(id, isRead)}> + {isRead ? 'Mark as unread' : 'Mark as read'} + , + + {}}> + View messages like this + , + + {}}> + Don’t notify me about this + , + ]; + }; + + return ( + + {}} + onClickTitle={() => {}} + /> + + ); +}; diff --git a/src-docs/src/views/notification_event/notification_event_example.js b/src-docs/src/views/notification_event/notification_event_example.js new file mode 100644 index 00000000000..48361ffc4f3 --- /dev/null +++ b/src-docs/src/views/notification_event/notification_event_example.js @@ -0,0 +1,246 @@ +import React, { Fragment } from 'react'; +import { renderToHtml } from '../../services'; +import { GuideSectionTypes } from '../../components'; +import { Link } from 'react-router-dom'; +import { EuiNotificationEventMeta } from '../../../../src/components/notification/notification_event_meta'; +import { + EuiNotificationEvent, + EuiText, + EuiContextMenuItem, + EuiSpacer, + EuiCode, + EuiAccordion, + EuiCodeBlock, + EuiButtonEmpty as EuiPrimaryActionProps, +} from '../../../../src/components'; +import NotificationEventPropsMethods from './notification_event_props_methods'; + +import NotificationEvent from './notification_event'; +const notificationEventSource = require('!!raw-loader!./notification_event'); +const notificationEventHtml = renderToHtml(NotificationEvent); + +import NotificationEventFlexible from './notification_event_flexible'; +const notificationEventFlexibleSource = require('!!raw-loader!./notification_event_flexible'); +const notificationEventFlexibleHtml = renderToHtml(NotificationEventFlexible); + +import NotificationsFeed from './notifications_feed'; +const notificationsFeedSource = require('!!raw-loader!./notifications_feed'); +const notificationsFeedHtml = renderToHtml(NotificationsFeed); + +const notificationEventSnippet = ``; + +const notificationEventFeedSnippet = `// we're looping through an array of objects to render multiple EuiNotificationEvent +const notificationEvents = events.map((event) => ( + +)); + +// the multiple EuiNotificationEvent should live inside the same container +
+ {notificationEvents} +
+`; + +export const NotificationEventExample = { + title: 'Notification event', + beta: true, + isNew: true, + intro: ( + +

+ Use EuiNotificationEvent to display notifications about + new events in your product like alerts, support, or news. This component + is meant to live inside a{' '} + + EuiFlyout + {' '} + so that users can quickly be informed or take action. +

+
+ ), + sections: [ + { + source: [ + { + type: GuideSectionTypes.JS, + code: notificationEventSource, + }, + { + type: GuideSectionTypes.HTML, + code: notificationEventHtml, + }, + ], + props: { + EuiNotificationEvent, + EuiNotificationEventMeta, + EuiContextMenuItem, + EuiPrimaryActionProps, + }, + snippet: notificationEventSnippet, + demo: , + }, + { + source: [ + { + type: GuideSectionTypes.JS, + code: notificationEventFlexibleSource, + }, + { + type: GuideSectionTypes.HTML, + code: notificationEventFlexibleHtml, + }, + ], + title: 'A flexible component', + text: ( + <> + +

+ The EuiNotificationEvent takes into account that + an event can be purely informative or actionable. It is flexible + and adapts the design according to the props passed. +

+
+ + + + Code snippet}> + + + {notificationEventSnippet} + + + + +
    +
  • + isRead: Shows a button that indicates the + current isRead state of the event. Use{' '} + onRead to allow users to toggle between read + and unread states. +
  • +
  • + iconType: Display an icon or logo to help + users quickly identify where the event originated. +
  • +
  • + type (required): Shows inside a badge + denoting what type of event it is. Use in conjunction with{' '} + severity and badgeColor to + indicate the level of urgency. +
  • +
  • + time (required): Indicates the time the event + was received. It is recommended to display a relative time + format like '2 hours ago'. +
  • +
  • + onContextMenu: Use this prop when you have + multiple events and you need to add individual actions to each + event. You can add filters based on the event type or a more + descriptive read/unread actions as an alternative to the read + indicator. +
  • +
  • + title (required): The title of the + notification event. It should be descriptive enough so that + users don't need to navigate away. But use it in + conjunction with an onClickTitle to direct + users to the respective app in case they need more information + about the notification. +
  • +
  • + messages: Provides more details about the + event. You can provide a single message or multiple messages if + the event executes in various steps. +
  • +
  • + primaryAction: Use this prop in conjunction + with onClickPrimaryAction to provide a call + to action, like download a report or link to a page where an + action is required. Most of the time, the clickable title is + enough. +
  • +
+
+ + +

+ The following demo shows how you can combine different props to + create different types of events like a report, alert, or simply + news. +

+
+ + ), + props: { + EuiNotificationEvent, + EuiNotificationEventMeta, + EuiContextMenuItem, + EuiPrimaryActionProps, + }, + snippet: notificationEventSnippet, + demo: , + }, + { + source: [ + { + type: GuideSectionTypes.JS, + code: notificationsFeedSource, + }, + { + type: GuideSectionTypes.HTML, + code: notificationsFeedHtml, + }, + ], + title: 'Notifications feed', + text: ( +

+ You can create a notifications feed by rendering multiple{' '} + EuiNotificationEvent. These components should live + inside a container without other components on the same level. This + way, we ensure that feed styles are applied correctly. Consuming + applications should implement all the logic to filter and save + read/unread states. +

+ ), + props: { + EuiNotificationEvent, + EuiNotificationEventMeta, + EuiContextMenuItem, + EuiPrimaryActionProps, + }, + snippet: notificationEventFeedSnippet, + demo: , + }, + ], +}; diff --git a/src-docs/src/views/notification_event/notification_event_flexible.js b/src-docs/src/views/notification_event/notification_event_flexible.js new file mode 100644 index 00000000000..42a2fd29534 --- /dev/null +++ b/src-docs/src/views/notification_event/notification_event_flexible.js @@ -0,0 +1,146 @@ +import React, { useState } from 'react'; +import { EuiPanel } from '../../../../src/components/panel'; +import { EuiSpacer } from '../../../../src/components/spacer'; +import { EuiButtonGroup } from '../../../../src/components/button'; +import { EuiContextMenuItem } from '../../../../src/components/context_menu'; +import { EuiNotificationEvent } from '../../../../src/components/notification/notification_event'; + +const notificationEventsData = [ + { + id: 'report', + type: 'Report', + iconType: 'logoKibana', + iconAriaLabel: 'Kibana', + time: '1 min ago', + title: '[Error Monitoring Report] is generated', + primaryAction: 'Download', + primaryActionProps: { + iconType: 'download', + }, + messages: ['The reported was generated at 17:12:16 GMT+4'], + isRead: false, + }, + { + id: 'alert', + type: 'Alert', + iconType: 'logoMaps', + severity: 'Warning', + iconAriaLabel: 'Maps', + badgeColor: 'warning', + time: '2 min ago', + title: '[Maps] Geo Alert', + messages: [ + 'The request completed at 12:32:33 GMT+4', + 'The request completed at 12:32:33 GMT+4', + 'A background request started at 12:32:33 GMT+4', + ], + isRead: false, + }, + + { + id: 'news', + type: 'News', + iconType: 'logoElastic', + iconAriaLabel: 'Elastic', + time: '3 min ago', + badgeColor: 'accent', + title: 'Search more, spend less', + messages: [ + 'Retain and search more data with searchable snapshots on low-cost object stores + a new cold data tier in 7.11.', + ], + isRead: false, + primaryAction: 'View and go', + }, +]; + +export default () => { + const [event, setEvent] = useState(notificationEventsData[0]); + + const onRead = (id, isRead) => { + const nextState = { ...event, isRead: !isRead }; + + setEvent(nextState); + }; + + const onOpenContextMenu = (id) => { + const { isRead } = event; + + return [ + onRead(id, isRead)}> + {isRead ? 'Mark as unread' : 'Mark as read'} + , + + {}}> + View messages like this + , + + {}}> + Don’t notify me about this + , + ]; + }; + + const [toggleIdSelected, setToggleIdSelected] = useState('reportButton'); + + const toggleButtons = [ + { + id: 'reportButton', + label: 'Report', + }, + { + id: 'alertButton', + label: 'Alert', + }, + { + id: 'newsButton', + label: 'News', + }, + ]; + + const onChangeButtonGroup = (optionId) => { + setToggleIdSelected(optionId); + const eventId = optionId.replace('Button', ''); + const event = notificationEventsData.find((event) => event.id === eventId); + setEvent(event); + }; + + return ( + <> + + + + {}} + onClickTitle={event.id !== 'news' ? () => {} : undefined} + /> + + + ); +}; diff --git a/src-docs/src/views/notification_event/notification_event_props_methods.js b/src-docs/src/views/notification_event/notification_event_props_methods.js new file mode 100644 index 00000000000..ebf8fb0ce40 --- /dev/null +++ b/src-docs/src/views/notification_event/notification_event_props_methods.js @@ -0,0 +1,73 @@ +import React from 'react'; +import { EuiPanel } from '../../../../src/components/panel'; +import { EuiBadge } from '../../../../src/components/badge'; +import { EuiButtonIcon } from '../../../../src/components/button'; +import { EuiIcon } from '../../../../src/components/icon'; + +const CircleIndicator = ({ name }) => ( + {name} +); + +export default () => { + return ( + +
+
+
+ +
+ +
+ + +
+ +
+ + + type: severity + +
+ +
+ + time +
+ +
+ +
+
+
+
+ title +
+
+ messages +
+
+ primaryAction +
+
+
+
+ ); +}; diff --git a/src-docs/src/views/notification_event/notifications_feed.js b/src-docs/src/views/notification_event/notifications_feed.js new file mode 100644 index 00000000000..a1e3c398626 --- /dev/null +++ b/src-docs/src/views/notification_event/notifications_feed.js @@ -0,0 +1,166 @@ +import React, { useState } from 'react'; +import { EuiPanel } from '../../../../src/components/panel'; +import { EuiSpacer } from '../../../../src/components/spacer'; +import { EuiButton } from '../../../../src/components/button'; +import { EuiContextMenuItem } from '../../../../src/components/context_menu'; +import { EuiNotificationEvent } from '../../../../src/components/notification/notification_event'; + +const notificationEventsData = [ + { + id: 'alert-01', + type: 'Alert', + severity: 'Warning', + iconType: 'logoMaps', + iconAriaLabel: 'Maps', + badgeColor: 'warning', + time: '1 min ago', + title: '[Maps] Geo Alert', + messages: [ + 'The request completed at 12:32:33 GMT+4', + 'The request completed at 12:32:33 GMT+4', + 'A background request started at 12:32:33 GMT+4', + ], + isRead: false, + }, + { + id: 'report-01', + type: 'Report', + iconType: 'logoKibana', + iconAriaLabel: 'Kibana', + time: '3 min ago', + title: '[Error Monitoring Report] is generated', + primaryAction: 'Download', + primaryActionProps: { + iconType: 'download', + }, + messages: [ + 'The reported was generated at 17:12:16 GMT+4 and due to an error it was was generated again at 17:13:17 GMT+4', + ], + isRead: false, + }, + { + id: 'news-01', + type: 'News', + iconType: 'logoElastic', + iconAriaLabel: 'Elastic', + time: '6 min ago', + badgeColor: 'accent', + title: 'Search more, spend less', + messages: [ + 'Retain and search more data with searchable snapshots on low-cost object stores + a new cold data tier in 7.11.', + ], + isRead: false, + primaryAction: 'View and go', + }, + { + id: 'alert-02', + type: 'Alert', + severity: 'Critical', + iconType: 'logoKibana', + iconAriaLabel: 'Kibana', + badgeColor: 'danger', + time: '8 min ago', + title: 'Index Threshold Alert', + messages: [ + '[prod-server-001] is above 300', + '[prod-server-001] is above 700', + ], + isRead: false, + }, + { + id: 'background-search-01', + type: 'Background Search', + iconType: 'logoKibana', + iconAriaLabel: 'Kibana', + time: '10 min ago', + title: '[Flights] Flight Count and Average Ticket Price', + messages: ['The request completed at 12:32:33 GMT+4'], + isRead: false, + }, +]; + +export default () => { + const [events, setEvents] = useState(notificationEventsData); + + const onRead = (id, isRead) => { + const nextState = events.map((event) => { + return event.id === id ? { ...event, isRead: !isRead } : event; + }); + + setEvents(nextState); + }; + + const onFilterByType = (type) => { + const nextState = events.filter((event) => type.includes(event.type)); + + setEvents(nextState); + }; + + const onOpenContextMenu = (id) => { + const { isRead, type } = events.find(({ id: eventId }) => eventId === id); + + return [ + onRead(id, isRead)}> + {isRead ? 'Mark as unread' : 'Mark as read'} + , + + onFilterByType(type)}> + View messages like this + , + + {}}> + Don’t notify me about this + , + ]; + }; + + const onResetData = () => { + setEvents(notificationEventsData); + }; + + const notificationEvents = events.map((event) => { + // we want to make the news title unclickable + const onClickTitle = event.type === 'News' ? undefined : () => {}; + + return ( + {}} + onClickTitle={onClickTitle} + /> + ); + }); + + return ( + <> + + Reset data + + + + {notificationEvents} + + + ); +}; diff --git a/src-docs/src/views/notification_events/notification_events.js b/src-docs/src/views/notification_events/notification_events.js deleted file mode 100644 index a56b472bcc7..00000000000 --- a/src-docs/src/views/notification_events/notification_events.js +++ /dev/null @@ -1,109 +0,0 @@ -import React, { useState } from 'react'; - -import { - EuiPanel, - EuiSpacer, - EuiTitle, - EuiContextMenuItem, -} from '../../../../src/components'; - -import { EuiNotificationEventMeta } from '../../../../src/components/notification/notification_event_meta'; - -export default () => { - const [isRead, setIsRead] = useState(false); - - const onRead = () => { - setIsRead(!isRead); - }; - - const panelStyle = { maxWidth: '400px' }; - - const contextMenuItems = [ - - Mark as read - , - - {}}> - View messages like this - , - - {}}> - Don’t notify me about this - , - ]; - - return ( - <> - -

EuiNotificationEventMeta

-
- - -

All props

-
- - - - - - - -

All props

-
- - - - - - - -

No Severity

-
- - - 2 min ago} - isRead={isRead} - onRead={onRead} - eventName="event-03" - /> - - - - -

Only required props

-
- - - 2 min ago} - eventName="event-04" - /> - - - ); -}; diff --git a/src-docs/src/views/notification_events/notification_events_example.js b/src-docs/src/views/notification_events/notification_events_example.js deleted file mode 100644 index c6e67967f23..00000000000 --- a/src-docs/src/views/notification_events/notification_events_example.js +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; -import { renderToHtml } from '../../services'; -import { GuideSectionTypes } from '../../components'; -import { EuiNotificationEventMeta } from '../../../../src/components/notification/notification_event_meta'; -import { EuiContextMenuItem } from '../../../../src/components/context_menu/'; -import { notificationEventReadButtonConfig } from './playground'; - -import Notification from './notification_events'; -const notificationSource = require('!!raw-loader!./notification_events'); -const notificationHtml = renderToHtml(Notification); - -export const NotificationEventsExample = { - title: 'Notification events', - isNew: true, - sections: [ - { - source: [ - { - type: GuideSectionTypes.JS, - code: notificationSource, - }, - { - type: GuideSectionTypes.HTML, - code: notificationHtml, - }, - ], - props: { - EuiNotificationEventMeta, - EuiContextMenuItem, - }, - demo: , - }, - ], - playground: notificationEventReadButtonConfig, -}; diff --git a/src-docs/src/views/notification_events/playground.js b/src-docs/src/views/notification_events/playground.js deleted file mode 100644 index 2dc66be45ce..00000000000 --- a/src-docs/src/views/notification_events/playground.js +++ /dev/null @@ -1,41 +0,0 @@ -import { PropTypes } from 'react-view'; -import { - propUtilityForPlayground, - simulateFunction, - dummyFunction, -} from '../../services/playground'; -import { EuiNotificationEventReadButton } from '../../../../src/components/'; - -export const notificationEventReadButtonConfig = () => { - const docgenInfo = Array.isArray(EuiNotificationEventReadButton.__docgenInfo) - ? EuiNotificationEventReadButton.__docgenInfo[0] - : EuiNotificationEventReadButton.__docgenInfo; - - const propsToUse = propUtilityForPlayground(docgenInfo.props); - - propsToUse.eventName = { - ...propsToUse.eventName, - value: 'alert-critical-01', - type: PropTypes.String, - }; - - propsToUse.onClick = simulateFunction(propsToUse.onClick, true); - - return { - config: { - componentName: 'EuiNotificationEventReadButton', - props: propsToUse, - scope: { - EuiNotificationEventReadButton, - }, - customProps: { - onClick: dummyFunction, - }, - imports: { - '@elastic/eui': { - named: ['EuiNotificationEventReadButton'], - }, - }, - }, - }; -}; diff --git a/src/components/index.js b/src/components/index.js index 721188f758c..3fe0d871a88 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -241,7 +241,7 @@ export { export { EuiMutationObserver } from './observer/mutation_observer'; -export { EuiNotificationEventReadButton } from './notification'; +export { EuiNotificationEvent } from './notification'; export { EuiOutsideClickDetector } from './outside_click_detector'; diff --git a/src/components/notification/__snapshots__/notification_event.test.tsx.snap b/src/components/notification/__snapshots__/notification_event.test.tsx.snap new file mode 100644 index 00000000000..3a84cb5dbe1 --- /dev/null +++ b/src/components/notification/__snapshots__/notification_event.test.tsx.snap @@ -0,0 +1,827 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`EuiNotificationEvent is rendered 1`] = ` +
+
+
+
+ + + + Alert + + + +
+
+ + 1 min ago + +
+
+

+ title +

+
+
+
+

+ message +

+
+
+
+
+
+`; + +exports[`EuiNotificationEvent props badgeColor is rendered 1`] = ` +
+
+
+
+ + + + Alert + + + +
+
+ + 1 min ago + +
+
+

+ title +

+
+
+
+

+ message +

+
+
+
+
+
+`; + +exports[`EuiNotificationEvent props contextMenuItems are rendered 1`] = ` +
+
+
+ + + +
+
+
+`; + +exports[`EuiNotificationEvent props headingLevel is rendered 1`] = ` +
+
+
+
+ + + + Alert + + + +
+
+ + 1 min ago + +
+
+

+ title +

+
+
+
+

+ message +

+
+
+
+
+
+`; + +exports[`EuiNotificationEvent props iconAriaLabel is rendered 1`] = ` +
+
+
+
+ + + + + Alert + + + +
+
+ + 1 min ago + +
+
+

+ title +

+
+
+
+

+ message +

+
+
+
+
+
+`; + +exports[`EuiNotificationEvent props iconType is rendered 1`] = ` +
+
+
+
+
+
+ + 1 min ago + +
+
+

+ title +

+
+
+
+

+ message +

+
+
+
+
+
+`; + +exports[`EuiNotificationEvent props isRead is rendered 1`] = ` +
+
+ +
+
+
+
+ + + + Alert + + + +
+
+ + 1 min ago + +
+
+

+ title +

+
+
+
+

+ message +

+
+
+
+
+
+`; + +exports[`EuiNotificationEvent props multiple messages are rendered 1`] = ` +
+
+
+
+ + + + Alert + + + +
+
+ + 1 min ago + +
+
+

+ title +

+
+
+
+

+ message 1 +

+
+
+
+
+ +
+
+
+
+
+
+
+

+ message 2 +

+
+
+
+
+

+ message 3 +

+
+
+
+
+
+
+
+
+
+
+`; + +exports[`EuiNotificationEvent props primaryAction is rendered 1`] = ` +
+
+
+
+ + + + Alert + + + +
+
+ + 1 min ago + +
+
+

+ title +

+
+
+
+

+ message +

+
+
+
+
+ +
+
+
+`; + +exports[`EuiNotificationEvent props primaryActionProps is rendered 1`] = ` +
+
+
+
+ + + + Alert + + + +
+
+ + 1 min ago + +
+
+

+ title +

+
+
+
+

+ message +

+
+
+
+
+ +
+
+
+`; + +exports[`EuiNotificationEvent props severity is rendered 1`] = ` +
+
+
+
+ + + + Alert: severity + + + +
+
+ + 1 min ago + +
+
+

+ title +

+
+
+
+

+ message +

+
+
+
+
+
+`; diff --git a/src/components/notification/__snapshots__/notification_event_meta.test.tsx.snap b/src/components/notification/__snapshots__/notification_event_meta.test.tsx.snap index 8d31cfe6106..7cbbfdf0fcb 100644 --- a/src/components/notification/__snapshots__/notification_event_meta.test.tsx.snap +++ b/src/components/notification/__snapshots__/notification_event_meta.test.tsx.snap @@ -132,19 +132,6 @@ exports[`EuiNotificationEventMeta props isRead is rendered 1`] = `
- @@ -182,6 +169,7 @@ exports[`EuiNotificationEventMeta props logoCloud is rendered 1`] = ` > } - badgeColor="secondary" eventName="eventName" + badgeColor="secondary" /> ); @@ -79,6 +83,7 @@ describe('EuiNotificationEventMeta', () => { test('logoCloud is rendered', () => { const component = render( 2 min ago} iconType="logoCloud" @@ -104,16 +109,17 @@ describe('EuiNotificationEventMeta', () => { const component = mount( 2 min ago} iconType="logoCloud" - contextMenuItems={contextMenuItems} eventName="eventName" + onOpenContextMenu={() => contextMenuItems} /> ); expect(component.find(EuiContextMenuPanel)).toHaveLength(0); - findTestSubject(component, 'notificationEventMetaButton').simulate( + findTestSubject(component, 'id-notificationEventMetaButton').simulate( 'click' ); expect(component.find(EuiContextMenuPanel)).toHaveLength(1); @@ -123,26 +129,4 @@ describe('EuiNotificationEventMeta', () => { ).toMatchSnapshot(); }); }); - - describe('behavior', () => { - it('triggers the onRead callback', () => { - const onRead = jest.fn(); - - const component = mount( - 2 min ago} - isRead={true} - onRead={onRead} - /> - ); - - findTestSubject(component, 'notificationEventReadButton').simulate( - 'click' - ); - - expect(onRead).toHaveBeenCalledTimes(1); - }); - }); }); diff --git a/src/components/notification/notification_event_meta.tsx b/src/components/notification/notification_event_meta.tsx index 62820af60b9..7ddb4f59f2e 100644 --- a/src/components/notification/notification_event_meta.tsx +++ b/src/components/notification/notification_event_meta.tsx @@ -17,28 +17,35 @@ * under the License. */ -import React, { FunctionComponent, useState, ReactNode } from 'react'; +import React, { + FunctionComponent, + useState, + ReactNode, + ReactElement, +} from 'react'; import classNames from 'classnames'; import { EuiIcon, IconType } from '../icon'; import { EuiBadge, EuiBadgeProps } from '../badge'; import { EuiPopover } from '../popover'; import { EuiButtonIcon } from '../button'; -import { EuiContextMenuPanel, EuiContextMenuPanelProps } from '../context_menu'; -import { EuiI18n } from '../i18n'; import { - EuiNotificationEventReadButton, - EuiNotificationEventReadButtonProps, -} from './notification_event_read_button'; + EuiContextMenuItem, + EuiContextMenuItemProps, + EuiContextMenuPanel, +} from '../context_menu'; +import { EuiI18n } from '../i18n'; import { htmlIdGenerator } from '../../services'; -export type EuiNotificationEventMetaProps = Omit< - EuiNotificationEventReadButtonProps, - 'isRead' | 'onClick' -> & { +export type EuiNotificationEventMetaProps = { + id: string; /** * Type of event (e.g. "Alert", "Cloud", etc..). Shows inside a badge. */ type: string; + /** + * A unique, human-friendly name for the event to be used in aria attributes (e.g. "alert-critical-01", "cloud-no-severity-12", etc..). + */ + eventName: string; /** * Shows an indicator of the read state of the event. Leave as `undefined` to hide the indicator. */ @@ -47,33 +54,29 @@ export type EuiNotificationEventMetaProps = Omit< * Type of severity (e.g. "Critical", "Warning", etc..). Shows as a text after the `type` following the format "Alert: Critical". */ severity?: string; - /** * Accepts either our palette colors (primary, secondary ..etc) or a hex value `#FFFFFF`, `#000`. */ badgeColor?: EuiBadgeProps['color']; - /** * The icon used to visually represent this data type. Accepts any `EuiIcon IconType`. */ iconType?: IconType; - /** * Specify an `aria-label` for the icon. * If no `aria-label` is passed we assume the icon is purely decorative. */ iconAriaLabel?: string; - /** * Indicates when the event was received. */ time: ReactNode; - /** - * An array of context menu items. See #EuiContextMenuItem + * Necessary to trigger `onOpenContextMenu` from #EuiNotificationEvent */ - contextMenuItems?: EuiContextMenuPanelProps['items']; - + onOpenContextMenu?: () => Array< + ReactElement + >; /** * Applies an `onClick` handler to the `read` indicator. */ @@ -81,45 +84,49 @@ export type EuiNotificationEventMetaProps = Omit< }; export const EuiNotificationEventMeta: FunctionComponent = ({ - isRead, + id, iconType, type, time, badgeColor = 'hollow', - onRead, severity, - contextMenuItems = [], eventName, iconAriaLabel, + onOpenContextMenu, }) => { const [isPopoverOpen, setIsPopoverOpen] = useState(false); const classes = classNames('euiNotificationEventMeta', { - 'euiNotificationEventMeta--hasContextMenu': contextMenuItems.length > 0, + 'euiNotificationEventMeta--hasContextMenu': onOpenContextMenu, }); - const id = htmlIdGenerator()(); + const [contextMenuItems, setContextMenuItems] = useState< + ReturnType> + >([]); - const onMarkAsRead = () => { - onRead?.(); - }; + const randomPopoverId = htmlIdGenerator()(); const ariaAttribute = iconAriaLabel ? { 'aria-label': iconAriaLabel } : { 'aria-hidden': true }; + const onOpenPopover = () => { + setIsPopoverOpen(!isPopoverOpen); + if (onOpenContextMenu) { + setContextMenuItems(onOpenContextMenu()); + } + }; + return (
- {typeof isRead === 'boolean' && ( - )} - {iconType && } - {type && ( {time}
- {contextMenuItems.length > 0 && ( + {onOpenContextMenu && (
( setIsPopoverOpen(!isPopoverOpen)} - data-test-subj="notificationEventMetaButton" + onClick={onOpenPopover} + data-test-subj={`${id}-notificationEventMetaButton`} /> )} } closePopover={() => setIsPopoverOpen(false)}> - + {/* The EuiContextMenu is wrapped with a div so it closes after an item is clicked */} +
setIsPopoverOpen(false)}> + +
)} diff --git a/src/components/notification/notification_event_read_button.test.tsx b/src/components/notification/notification_event_read_button.test.tsx index f2592a9667d..ccccb98639c 100644 --- a/src/components/notification/notification_event_read_button.test.tsx +++ b/src/components/notification/notification_event_read_button.test.tsx @@ -26,6 +26,7 @@ describe('EuiNotificationEventReadButton', () => { test('is rendered', () => { const component = render( {}} @@ -38,6 +39,7 @@ describe('EuiNotificationEventReadButton', () => { test('renders isRead to false', () => { const component = render( {}} @@ -51,6 +53,7 @@ describe('EuiNotificationEventReadButton', () => { const handler = jest.fn(); const component = mount( & { + id: string; /** * Shows an indicator of the read state of the event */ @@ -41,6 +42,7 @@ export type EuiNotificationEventReadButtonProps = Omit< }; export const EuiNotificationEventReadButton: FunctionComponent = ({ + id, isRead, onClick, eventName, @@ -50,32 +52,43 @@ export const EuiNotificationEventReadButton: FunctionComponent );