diff --git a/ui/src/kapacitor/containers/TickscriptPage.js b/ui/src/kapacitor/containers/TickscriptPage.js index 8abf937aa2..1817804df2 100644 --- a/ui/src/kapacitor/containers/TickscriptPage.js +++ b/ui/src/kapacitor/containers/TickscriptPage.js @@ -44,7 +44,8 @@ class TickscriptPage extends Component { }) notify( 'warning', - 'Could not use logging, requires Kapacitor version 1.4' + 'Could not use logging, requires Kapacitor version 1.4', + {once: true} ) return } diff --git a/ui/src/localStorage.js b/ui/src/localStorage.js index 77cb2268f2..55c7aff557 100644 --- a/ui/src/localStorage.js +++ b/ui/src/localStorage.js @@ -52,6 +52,7 @@ export const saveToLocalStorage = ({ timeRange, dataExplorer, dashTimeV1: {ranges}, + dismissedNotifications, }) => { try { const appPersisted = Object.assign({}, {app: {persisted}}) @@ -66,6 +67,7 @@ export const saveToLocalStorage = ({ dataExplorer, VERSION, // eslint-disable-line no-undef dashTimeV1, + dismissedNotifications, }) ) } catch (err) { diff --git a/ui/src/shared/actions/notifications.js b/ui/src/shared/actions/notifications.js index deab1e28bb..23c830ce6d 100644 --- a/ui/src/shared/actions/notifications.js +++ b/ui/src/shared/actions/notifications.js @@ -1,4 +1,4 @@ -export function publishNotification(type, message) { +export function publishNotification(type, message, options = {once: false}) { // this validator is purely for development purposes. It might make sense to move this to a middleware. const validTypes = ['error', 'success', 'warning'] if (!validTypes.includes(type) || message === undefined) { @@ -10,6 +10,7 @@ export function publishNotification(type, message) { payload: { type, message, + once: options.once, }, } } diff --git a/ui/src/shared/components/Notifications.js b/ui/src/shared/components/Notifications.js index 7111e9f300..f305f52829 100644 --- a/ui/src/shared/components/Notifications.js +++ b/ui/src/shared/components/Notifications.js @@ -4,6 +4,8 @@ import {withRouter} from 'react-router' import {connect} from 'react-redux' import {bindActionCreators} from 'redux' +import {getNotificationID} from 'src/shared/reducers/notifications' + import { publishNotification as publishNotificationAction, dismissNotification as dismissNotificationAction, @@ -25,7 +27,10 @@ class Notifications extends Component { } renderNotification(type, message) { - if (!message) { + const isDismissed = this.props.dismissedNotifications[ + getNotificationID(message, type) + ] + if (!message || isDismissed) { return null } const cls = classnames('alert', { @@ -86,10 +91,12 @@ Notifications.propTypes = { error: string, warning: string, }), + dismissedNotifications: shape({}), } -const mapStateToProps = ({notifications}) => ({ +const mapStateToProps = ({notifications, dismissedNotifications}) => ({ notifications, + dismissedNotifications, }) const mapDispatchToProps = dispatch => ({ diff --git a/ui/src/shared/reducers/index.js b/ui/src/shared/reducers/index.js index cfb032a7de..b8824ab25d 100644 --- a/ui/src/shared/reducers/index.js +++ b/ui/src/shared/reducers/index.js @@ -2,7 +2,7 @@ import app from './app' import auth from './auth' import errors from './errors' import links from './links' -import notifications from './notifications' +import {notifications, dismissedNotifications} from './notifications' import sources from './sources' export default { @@ -11,5 +11,6 @@ export default { errors, links, notifications, + dismissedNotifications, sources, } diff --git a/ui/src/shared/reducers/notifications.js b/ui/src/shared/reducers/notifications.js index c4275fcd8a..6767a6f289 100644 --- a/ui/src/shared/reducers/notifications.js +++ b/ui/src/shared/reducers/notifications.js @@ -1,11 +1,7 @@ import u from 'updeep' +import _ from 'lodash' -function getInitialState() { - return {} -} -const initialState = getInitialState() - -const notificationsReducer = (state = initialState, action) => { +export const notifications = (state = {}, action) => { switch (action.type) { case 'NOTIFICATION_RECEIVED': { const {type, message} = action.payload @@ -16,11 +12,38 @@ const notificationsReducer = (state = initialState, action) => { return u(u.omit(type), state) } case 'ALL_NOTIFICATIONS_DISMISSED': { - return getInitialState() + // Reset to initial state + return {} } } return state } -export default notificationsReducer +export const getNotificationID = (message, type) => _.snakeCase(message) + type + +export const dismissedNotifications = (state = {}, action) => { + switch (action.type) { + case 'NOTIFICATION_RECEIVED': { + const {type, message, once} = action.payload + if (once) { + // Create a message ID in a deterministic way, also with its type + const messageID = getNotificationID(message, type) + if (state[messageID]) { + // Message action called with once option but we've already seen it + return state + } + // Message action called with once option and it's not present on + // the persisted state + return { + ...state, + [messageID]: true, + } + } + // Message action not called with once option + return state + } + } + + return state +}