Skip to content

Commit

Permalink
Allow blocking toast notifications ('do not disturb')
Browse files Browse the repository at this point in the history
Allow user to block system notifications via Account Settings:
1. each change to the snooze duration resets the snooze timer
2. notification settings are not persisted - page reload clears them.
3. only displaying is blocked - logs continue to be accumulated in the
   store (userMessages -> records)
4. after each refresh old logs are cleared. The treshold is linked with
   toast display time (2x display time).
  • Loading branch information
rszwajko committed Nov 26, 2020
1 parent 7abd886 commit ac910e4
Show file tree
Hide file tree
Showing 8 changed files with 97 additions and 16 deletions.
15 changes: 15 additions & 0 deletions src/actions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ import {
SET_WEBSOCKET,
SHOW_TOKEN_EXPIRED_MSG,
START_SCHEDULER_FIXED_DELAY,
START_SCHEDULER_FOR_RESUMING_NOTIFICATIONS,
STOP_SCHEDULER_FIXED_DELAY,
STOP_SCHEDULER_FOR_RESUMING_NOTIFICATIONS,
UPDATE_PAGING_DATA,
} from '_/constants'

Expand Down Expand Up @@ -69,6 +71,19 @@ export function stopSchedulerFixedDelay () {
return { type: STOP_SCHEDULER_FIXED_DELAY }
}

export function startSchedulerForResumingNotifications (delayInSeconds) {
return {
type: START_SCHEDULER_FOR_RESUMING_NOTIFICATIONS,
payload: {
delayInSeconds,
},
}
}

export function stopSchedulerForResumingNotifications () {
return { type: STOP_SCHEDULER_FOR_RESUMING_NOTIFICATIONS }
}

export function setUserFilterPermission (filter) {
return {
type: SET_USER_FILTER_PERMISSION,
Expand Down
5 changes: 5 additions & 0 deletions src/actions/userMessages.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
ADD_USER_MESSAGE,
CLEAR_OLD_LOG_ENTRIES,
CLEAR_USER_MSGS,
DISMISS_EVENT,
DISMISS_USER_MSG,
Expand All @@ -23,6 +24,10 @@ export function clearUserMessages () {
return { type: CLEAR_USER_MSGS }
}

export function clearOldLogEntries () {
return { type: CLEAR_OLD_LOG_ENTRIES }
}

export function setNotificationNotified ({ eventId }) {
return {
type: SET_USERMSG_NOTIFIED,
Expand Down
36 changes: 21 additions & 15 deletions src/components/ToastNotifications.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { connect } from 'react-redux'

import { TimedToastNotification, ToastNotificationList } from 'patternfly-react'
import { setNotificationNotified } from '_/actions'
import AppConfiguration from '_/config'

import style from './sharedStyle.css'

Expand All @@ -13,31 +14,36 @@ function normalizeType (theType) {
return isExpected ? theType : 'warning'
}

const ToastNotifications = ({ userMessages, onDismissNotification }) => {
const ToastNotifications = ({ userMessages, showNotifications, onDismissNotification }) => {
return <ToastNotificationList>
{ userMessages.get('records').filter(r => !r.get('notified')).map(r =>
<TimedToastNotification
className={style['toast-margin-top']}
type={normalizeType(r.get('type'))}
onDismiss={() => onDismissNotification(r.get('id'))}
key={r.get('time')}
>
<span>
{r.get('message')}
</span>
</TimedToastNotification>
)}
{ showNotifications && userMessages.get('records')
.filter(r => !r.get('notified'))
.map(r =>
<TimedToastNotification
className={style['toast-margin-top']}
type={normalizeType(r.get('type'))}
onDismiss={() => onDismissNotification(r.get('id'))}
key={r.get('time')}
timerdelay={1000 * AppConfiguration.toastNotificationDisplayTimeInSec}
>
<span>
{r.get('message')}
</span>
</TimedToastNotification>
)}
</ToastNotificationList>
}

ToastNotifications.propTypes = {
userMessages: PropTypes.object.isRequired,
showNotifications: PropTypes.bool,
onDismissNotification: PropTypes.func.isRequired,
}

export default connect(
(state) => ({
userMessages: state.userMessages,
({ userMessages, options }) => ({
userMessages,
showNotifications: options.getIn(['global', 'showNotifications'], true),
}),
(dispatch) => ({
onDismissNotification: (eventId) => dispatch(setNotificationNotified({ eventId })),
Expand Down
1 change: 1 addition & 0 deletions src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const AppConfiguration = {
pageLimit: 20,
schedulerFixedDelayInSeconds: 60,
notificationSnoozeDurationInMinutes: 10,
toastNotificationDisplayTimeInSec: 8,

consoleClientResourcesURL: 'https://www.ovirt.org/documentation/admin-guide/virt/console-client-resources/',
cockpitPort: '9090',
Expand Down
3 changes: 3 additions & 0 deletions src/constants/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const CHANGE_PAGE = 'CHANGE_PAGE'
export const CHANGE_VM_CDROM = 'CHANGE_VM_CDROM'
export const CHECK_CONSOLE_IN_USE = 'CHECK_CONSOLE_IN_USE'
export const CHECK_TOKEN_EXPIRED = 'CHECK_TOKEN_EXPIRED'
export const CLEAR_OLD_LOG_ENTRIES = 'CLEAR_OLD_LOG_ENTRIES'
export const CLEAR_USER_MSGS = 'CLEAR_USER_MSGS'
export const COMPOSE_CREATE_VM = 'COMPOSE_CREATE_VM'
export const CREATE_DISK_FOR_VM = 'CREATE_DISK_FOR_VM'
Expand Down Expand Up @@ -125,8 +126,10 @@ export const NAVIGATE_TO_VM_DETAILS = 'NAVIGATE_TO_VM_DETAILS'
export const SHUTDOWN_VM = 'SHUTDOWN_VM'
export const START_POOL = 'START_POOL'
export const START_SCHEDULER_FIXED_DELAY = 'START_SCHEDULER_FIXED_DELAY'
export const START_SCHEDULER_FOR_RESUMING_NOTIFICATIONS = 'START_SCHEDULER_FOR_RESUMING_NOTIFICATIONS'
export const START_VM = 'START_VM'
export const STOP_SCHEDULER_FIXED_DELAY = 'STOP_SCHEDULER_FIXED_DELAY'
export const STOP_SCHEDULER_FOR_RESUMING_NOTIFICATIONS = 'STOP_SCHEDULER_FOR_RESUMING_NOTIFICATIONS'
export const SUSPEND_VM = 'SUSPEND_VM'
export const UPDATE_ICONS = 'UPDATE_ICONS'
export const UPDATE_PAGING_DATA = 'UPDATE_PAGING_DATA'
Expand Down
7 changes: 7 additions & 0 deletions src/reducers/userMessages.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Immutable from 'immutable'
import {
ADD_USER_MESSAGE,
CLEAR_OLD_LOG_ENTRIES,
DISMISS_USER_MSG,
FAILED_EXTERNAL_ACTION,
LOGIN_FAILED,
Expand All @@ -9,6 +10,7 @@ import {
} from '_/constants'
import { actionReducer } from './utils'
import uniqueId from 'lodash/uniqueId'
import AppConfiguration from '_/config'

/*flow-include
import type { FailedExternalAction } from '../actions/error'
Expand Down Expand Up @@ -75,6 +77,11 @@ const userMessages = actionReducer(initialState, {
[DISMISS_USER_MSG] (state, { payload: { eventId } }) {
return state.update('records', records => records.delete(state.get('records').findIndex(r => r.get('id') === eventId)))
},
[CLEAR_OLD_LOG_ENTRIES] (state) {
const tresholdAge = Date.now() - 2 * 1000 * AppConfiguration.toastNotificationDisplayTimeInSec
return state.update('records', records => records.filter(r => r.get('time') > tresholdAge))
},

})

export default userMessages
17 changes: 17 additions & 0 deletions src/sagas/background-refresh.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
resumeNotifications,
loadUserOptions,
} from './options'

Expand Down Expand Up @@ -60,6 +61,7 @@ function* refreshData ({ payload: { targetPage, ...otherPayload } }) {
if (refreshType) {
yield pagesRefreshers[refreshType](Object.assign({ id: targetPage.id }, otherPayload))
}
yield put(Actions.clearOldLogEntries())
console.info('refreshData() 🡒 finished')
}

Expand Down Expand Up @@ -263,6 +265,20 @@ function* schedulerWithFixedDelay ({
console.log(`⏰ schedulerWithFixedDelay[${myId}] 🡒 running after delay of: ${delayInSeconds}`)
}
}
let _SchedulerForNotificationsCount = 0
function* scheduleResumingNotifications ({ payload: { delayInSeconds } }) {
yield put(Actions.stopSchedulerForResumingNotifications())
const myId = _SchedulerForNotificationsCount++
console.log(`notification timer [${myId}] - delay [${delayInSeconds}] sec`)
const { stopped } = yield call(schedulerWaitFor, delayInSeconds, C.STOP_SCHEDULER_FOR_RESUMING_NOTIFICATIONS)
if (stopped) {
console.log(`notification timer [${myId}] - stopped`)
} else {
console.log(`notification timer [${myId}] - resume notifications`)
yield call(resumeNotifications)
}
}

/**
* When ovirt-web-ui is installed to ovirt-engine, a logout should push the user to the
* base ovirt welcome page. But when running in dev mode or via container, the logout
Expand All @@ -280,4 +296,5 @@ export default [
throttle(5000, C.REFRESH_DATA, refreshData),
takeLatest(C.CHANGE_PAGE, changePage),
takeEvery(C.LOGOUT, logoutAndCancelScheduler),
takeEvery(C.START_SCHEDULER_FOR_RESUMING_NOTIFICATIONS, scheduleResumingNotifications),
]
29 changes: 28 additions & 1 deletion src/sagas/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,16 @@ function* saveGlobalOptions ({ payload: { sshKey, showNotifications, notificatio
}

if (showNotifications !== undefined || notificationSnoozeDuration !== undefined) {

yield call(
updateNotifications,
{
current: yield select((state) => state.options.getIn(['global', 'showNotifications'])),
next: showNotifications,
},
{
current: yield select((state) => state.options.getIn(['global', 'notificationSnoozeDuration'])),
next: notificationSnoozeDuration,
})
}

yield put(
Expand All @@ -105,6 +114,24 @@ function* saveGlobalOptions ({ payload: { sshKey, showNotifications, notificatio
)
}

function* updateNotifications (show: {current: boolean, next?: boolean}, snooze: {current: number, next?: number}): any {
const snoozeDuration = snooze.next || snooze.current
const showNotifications = show.next === undefined ? show.current : show.next

yield put(A.setOption({ key: ['global', 'showNotifications'], value: showNotifications }))
yield put(A.setOption({ key: ['global', 'notificationSnoozeDuration'], value: snoozeDuration }))
if (showNotifications) {
yield put(A.stopSchedulerForResumingNotifications())
} else {
// minutes -> seconds
yield put(A.startSchedulerForResumingNotifications(snoozeDuration * 60))
}
}

export function* resumeNotifications (): any {
yield put(A.setOption({ key: ['global', 'showNotifications'], value: true }))
}

export function* loadUserOptions (): any {
const userId = yield select(state => state.config.getIn(['user', 'id']))
yield put(A.getSSHKey({ userId }))
Expand Down

0 comments on commit ac910e4

Please sign in to comment.