Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Impl [Notifications] Add abilities to notification #2880

Draft
wants to merge 4 commits into
base: development
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/common/Download/DownloadContainer.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ const DownloadContainer = () => {
className="download-container"
data-testid="download-container"
style={{ ...defaultStyle, ...transitionStyles[state] }}
ref={nodeRef}
>
<div className="download-container__header">Downloads</div>
<button className="notification__button-close" onClick={handleCancel}>
Expand Down
6 changes: 6 additions & 0 deletions src/common/Download/downloadContainer.scss
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,10 @@
}
}
}

.notification__button-close {
position: absolute;
top: 6px;
right: 6px;
}
}
74 changes: 55 additions & 19 deletions src/common/Notifications/Notification.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,14 @@ illegal under applicable law, and the grant of the foregoing license
under the Apache 2.0 license is conditioned upon your compliance with
such restriction.
*/
import React, { useMemo, useRef } from 'react'
import React, { useEffect, useMemo, useRef } from 'react'
import { useDispatch } from 'react-redux'
import { Transition } from 'react-transition-group'
import { inRange } from 'lodash'
import PropTypes from 'prop-types'
import classnames from 'classnames'

import { useTimeout } from '../../hooks/useTimeout'

import { removeNotification } from '../../reducers/notificationReducer'

Expand All @@ -33,11 +36,16 @@ import { ReactComponent as UnsuccessAlert } from 'igz-controls/images/unsuccess_

import './notification.scss'

const Notification = ({ notification, ...rest }) => {
const Notification = ({ notification, timeoutMs = 10000, ...rest }) => {
// rest is required for Transition
const dispatch = useDispatch()
const nodeRef = useRef()

const { pauseTimeout, resumeTimeout, cancelTimeout } = useTimeout(
() => handleRemoveNotification(notification.id),
timeoutMs
)

const defaultStyle = {
transform: 'translateY(130px)',
opacity: 0
Expand All @@ -61,38 +69,47 @@ const Notification = ({ notification, ...rest }) => {
)
const handleRemoveNotification = itemId => {
dispatch(removeNotification(itemId))
cancelTimeout()
}
const handleRetry = item => {
handleRemoveNotification(item.id)
cancelTimeout()
item.retry(item)
}

const progressbarClasses = classnames(
'notification__progress-bar',
isSuccessResponse ? 'notification__progress-bar-success' : 'notification__progress-bar-alert'
)

useEffect(() => {
const element = nodeRef.current

if (element) {
element.addEventListener('mouseenter', pauseTimeout)
element.addEventListener('mouseleave', resumeTimeout)
}

return () => {
if (element) {
element.removeEventListener('mouseenter', pauseTimeout)
element.removeEventListener('mouseleave', resumeTimeout)
}
}
}, [pauseTimeout, resumeTimeout])

return (
<Transition
nodeRef={nodeRef}
timeout={NOTIFICATION_DURATION}
onEntered={() => {
setTimeout(() => {
handleRemoveNotification(notification.id)
}, 10000)
}}
{...rest}
>
<Transition nodeRef={nodeRef} timeout={NOTIFICATION_DURATION} {...rest}>
{state => (
<div
className="notification"
style={{
...defaultStyle,
...transitionStyles[state]
}}
ref={nodeRef}
>
<div className="notification__body">
<button
className="notification__button-close"
onClick={() => handleRemoveNotification(notification.id)}
>
<CloseIcon />
</button>
<div
className={`notification__body__status notification__body__icon-${
isSuccessResponse ? 'success' : 'alert'
Expand All @@ -114,14 +131,33 @@ const Notification = ({ notification, ...rest }) => {
</div>
)}
</div>
<button
className="notification__button-close"
onClick={() => {
handleRemoveNotification(notification.id)
}}
>
<CloseIcon />
</button>
<div className="notification__progress-bar__wrapper">
<div className="notification__progress-bar__bg"></div>
<div
role="progressbar"
aria-hidden="false"
aria-label="notification timer"
className={progressbarClasses}
style={{ animationDuration: `${timeoutMs}ms` }}
></div>
</div>
</div>
)}
</Transition>
)
}

Notification.prototype = {
notification: PropTypes.object.isRequired
notification: PropTypes.object.isRequired,
timeoutMs: PropTypes.number
}

export default Notification
63 changes: 59 additions & 4 deletions src/common/Notifications/notification.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,65 @@
.notification {
position: relative;
z-index: 1000;
display: flex;
justify-content: space-between;
align-self: flex-end;
margin-right: 24px;
margin-bottom: 10px;
padding: 15px;
margin-bottom: 1rem;
padding: 8px;
color: $white;
background-color: $darkPurple;
border-radius: 5px;
box-shadow: $tooltipShadow;
cursor: pointer;
overflow: hidden;
opacity: 0;

&__progress-bar {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 5px;
z-index: 1;
opacity: 0.7;
transform-origin: left;
animation: notification__trackprogress-bar linear 1 forwards;
animation-play-state: running;

&__wrapper {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 5px;
}

&__bg {
width: 100%;
height: 100%;
opacity: 0.3;
}

&-success {
background-color: $brightTurquoise;
}

&-alert {
background-color: $burntSienna;
}
}

&:hover {
.notification__progress-bar {
animation-play-state: paused;
}
}

&__body {
display: flex;
align-items: center;
margin: 5px 20px 5px 0;
padding: 6px;

&__button-retry {
margin-left: 15px;
Expand All @@ -42,7 +88,7 @@
justify-content: center;
width: 24px;
height: 24px;
margin-right: 5px;
margin-right: 10px;
border-radius: 50%;
}

Expand All @@ -55,3 +101,12 @@
}
}
}

@keyframes notification__trackprogress-bar {
0% {
transform: scaleX(1);
}
100% {
transform: scaleX(0);
}
}
4 changes: 2 additions & 2 deletions src/common/Notifications/notifications.scss
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
.notifications-wrapper {
position: absolute;
right: 0;
bottom: 0;
right: 1.5em;
bottom: 1em;
display: flex;
flex: 1 1;
flex-direction: column;
Expand Down
123 changes: 123 additions & 0 deletions src/hooks/useTimeout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
Copyright 2019 Iguazio Systems Ltd.

Licensed under the Apache License, Version 2.0 (the "License") with
an addition restriction as set forth herein. You may not use this
file except in compliance with the License. You may obtain a copy of
the License at http://www.apache.org/licenses/LICENSE-2.0.

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied. See the License for the specific language governing
permissions and limitations under the License.

In addition, you may not use the software for any purposes that are
illegal under applicable law, and the grant of the foregoing license
under the Apache 2.0 license is conditioned upon your compliance with
such restriction.
*/

import { useCallback, useEffect, useRef } from 'react'

/**
* Set a timer, which executes a function once the timer expires. Exposes pause, resume and cancel
* callbacks to the consumers.
*
* @param fn Callback function to execute after the specified timeout
* @param ms Timeout in milliseconds after which to execute the callback
*/
export const useTimeout = (fn, ms) => {
// Indicates if timer is currently running
const isRunning = useRef(false)

// Number of milliseconds remaining before the timer runs out and callback is executed
const msRemaining = useRef(ms)

// Time when the most recent execution was requested (0 if time is not currently running)
const timeStarted = useRef(0)

// Timeout handle
const handle = useRef(0)

// Original callback function
const callback = useRef(fn)
ilan7empest marked this conversation as resolved.
Show resolved Hide resolved

/**
* Completely cancel the execution of the callback function
*/
const cancelTimeout = useCallback(() => {
// Ignore request if there is no active timeout to cancel
if (handle.current <= 0) {
return
}

// Mark timer as paused and reset all internal values to avoid being able to restart it
isRunning.current = false
msRemaining.current = 0
timeStarted.current = 0

clearTimeout(handle.current)
handle.current = 0
}, [])

/**
* Resume timeout countdown
*/
const resumeTimeout = useCallback(() => {
// Ignore request if time is already running or if there is no time remaining
if (isRunning.current || msRemaining.current <= 0) {
return
}

// Mark timer as running and record the current time for future calculations
isRunning.current = true
timeStarted.current = Date.now()

// Schedule timeout
handle.current = setTimeout(() => {
// Clear all internal values when the timer executes
cancelTimeout()

// Invoke the callback function
callback.current && callback.current()
}, msRemaining.current)
}, [cancelTimeout])

/**
* Pause current countdown
*/
const pauseTimeout = useCallback(() => {
// Ignore request if timer is not currently running
if (!isRunning.current) {
return
}

// Mark timer as paused, clear last time started and reduce the number of remaining
// milliseconds by the time that passed since the last "start" call
isRunning.current = false
msRemaining.current -= Date.now() - timeStarted.current
timeStarted.current = 0

// Clear timeout handle to avoid the function executing while timer is paused
clearTimeout(handle.current)
handle.current = 0
}, [])

// Update callback function reference when a new function is passed in
useEffect(() => {
callback.current = fn
}, [fn])

// Start the timer on mount and cancel it on unmount
useEffect(() => {
resumeTimeout()
return cancelTimeout
}, [cancelTimeout, resumeTimeout])

return {
cancelTimeout,
pauseTimeout,
resumeTimeout
}
}
Loading
Loading