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

Support notifications in the Console #6479

Merged
merged 72 commits into from
Feb 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
700634c
console: Add notifications sdk
ryaplots Aug 9, 2023
1519bfe
console: Add notifications store
ryaplots Aug 9, 2023
93f6e1e
console: Add notification view
ryaplots Aug 9, 2023
c6c6503
console: Add notifications container, layout and logics
ryaplots Aug 9, 2023
f4e207d
console: Add pagination
ryaplots Aug 10, 2023
3f9343a
console: Add fetch notifications every 5 minutes
ryaplots Aug 10, 2023
4beed75
console: Add archive and bg color
ryaplots Aug 11, 2023
0e19f26
console: Add notification content
ryaplots Aug 17, 2023
41f910f
console: Add notification preview
ryaplots Aug 17, 2023
4437d90
console: Refactor unseen handling
ryaplots Aug 17, 2023
a515671
console: Refactor notifications and apply styling fixes
ryaplots Aug 18, 2023
7cfe33d
console: Add archive view
ryaplots Aug 21, 2023
3acc0d9
console: Add responsiveness
ryaplots Aug 22, 2023
7bbad4b
console: Add e2e tests
ryaplots Aug 22, 2023
a0ba6e4
console: Add data-test-id
ryaplots Aug 23, 2023
504bac4
console: Add notifications to shared messages
ryaplots Aug 23, 2023
a568a6e
console: Add message
ryaplots Aug 23, 2023
7c21f7c
console: Fix spelling
ryaplots Aug 23, 2023
8ba03c2
console: Solve repetition
ryaplots Aug 30, 2023
c8f5164
console: Apply various fixes
ryaplots Sep 7, 2023
474918b
console: Deal with responsiveness
ryaplots Sep 7, 2023
64814e9
console: Fix styling
ryaplots Sep 7, 2023
608cd0a
console: Remove unnecessary requests
ryaplots Sep 7, 2023
807eb51
console: Fix marking all as read
ryaplots Sep 7, 2023
d8a33a0
console: Use react-window-infinite-loader and implement loader list
ryaplots Sep 26, 2023
0ad2b85
console: Update e2e tests
ryaplots Sep 27, 2023
e1691b7
console: Adjust notification UI to new console
ryaplots Jan 9, 2024
7357075
console: Fix notification issues
ryaplots Jan 12, 2024
e2d6aa5
console: Use resetloadMoreItemsCache to update the list
ryaplots Jan 12, 2024
e1cc8a9
console: Add notifications dropdown
ryaplots Jan 16, 2024
9be0e84
console: Fix autoscroll loading
kschiffer Jan 16, 2024
8cc06e3
console: Fix styling and logic
ryaplots Jan 16, 2024
d80b80c
console: Adjust dropdown style
ryaplots Jan 16, 2024
2aae6c9
console: Remove unnecessary styles
ryaplots Jan 17, 2024
474c626
console: Adjust styling
ryaplots Jan 17, 2024
6b7d986
console: Refactor css
ryaplots Jan 17, 2024
02b1bbc
console: Fix e2e tests
ryaplots Jan 18, 2024
176b529
console: Fix notifications bugs
ryaplots Jan 18, 2024
97ba2fd
console: Fix dropdown
ryaplots Jan 23, 2024
6580a5b
console: Update notifications stylling
ryaplots Jan 23, 2024
c9e18e7
console: Allow accessing a specific notification through lcation
ryaplots Jan 23, 2024
e023501
console: Handle resize
ryaplots Jan 23, 2024
455e432
console: Fix linting
ryaplots Jan 24, 2024
7763a80
console: Apply notifications diff
ryaplots Jan 29, 2024
9396296
console: Add notificatin dot
ryaplots Jan 29, 2024
a6ce019
console: Fetch most recent notifications in init.js
ryaplots Jan 29, 2024
06a42ed
console: Change date on small screen
ryaplots Jan 29, 2024
1aee0cf
console: Apply diff chnages
ryaplots Jan 29, 2024
cf95eef
console: Use id in the url of the notifications
ryaplots Jan 30, 2024
b610595
console: Content responsiveness
ryaplots Jan 30, 2024
6a7ce9f
console: Fix browser console errors
ryaplots Jan 30, 2024
35ed94c
console: Add search to link
ryaplots Jan 30, 2024
441186a
console: Fix link
ryaplots Feb 1, 2024
1f80b37
console: Apply notification retrieval and selection fixes
kschiffer Feb 1, 2024
8af9859
console: Make notification titles more concise
kschiffer Feb 1, 2024
6ff1483
console: Fix reducer and archived message
ryaplots Feb 1, 2024
fe66b7f
console: Correctly update seen/unseen status onClick
ryaplots Feb 1, 2024
aca2d1b
console: Fix collaborator link
ryaplots Feb 1, 2024
e17957c
console: Fix dropdown color
ryaplots Feb 1, 2024
6e2bfbf
console: Update items after status chnage
ryaplots Feb 1, 2024
dd1c6fc
console: Fix navigate after archive on small screen
ryaplots Feb 1, 2024
8b5b35e
console: Update state periodically
ryaplots Feb 2, 2024
c7350a7
console: Fix get notification speriodically
ryaplots Feb 2, 2024
f0426d0
console: Use useEffect to update notification status
ryaplots Feb 5, 2024
c90ea0d
console: Fix deps
ryaplots Feb 5, 2024
c5782be
console: Fix deps
ryaplots Feb 5, 2024
34c8478
console: Fix conflict resolution
ryaplots Feb 5, 2024
35c67e7
console: Fix code quality errors
ryaplots Feb 5, 2024
e69a435
console: Fix small screen notifications status bug
ryaplots Feb 5, 2024
1e68b6c
console: Fix button alert styling
kschiffer Feb 6, 2024
4d1ec94
console: Fix periodical notification updates
kschiffer Feb 6, 2024
c9145bb
console: Fix interval time
ryaplots Feb 7, 2024
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
78 changes: 78 additions & 0 deletions cypress/e2e/console/notifications/notifications.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright © 2023 The Things Network Foundation, The Things Industries B.V.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// 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.

import { generateCollaborator } from '../../../support/utils'

describe('Notifications', () => {
const collabUserId = 'test-collab-user'
const collabUser = {
ids: { user_id: collabUserId },
primary_email_address: '[email protected]',
password: 'ABCDefg123!',
password_confirm: 'ABCDefg123!',
}
const application = { ids: { application_id: 'test-application' } }
const userCollaborator = generateCollaborator('applications', 'user')
const apiKeyName = 'api-test-key'
const apiKey = {
name: apiKeyName,
rights: ['RIGHT_APPLICATION_ALL'],
}

before(() => {
cy.dropAndSeedDatabase()
cy.createUser(collabUser)
cy.createApplication(application, 'admin')
cy.createCollaborator('applications', application.ids.application_id, userCollaborator)
cy.createApiKey('applications', application.ids.application_id, apiKey, key => {
Cypress.config('apiKeyId', key.id)
})
})

beforeEach(() => {
cy.loginConsole({ user_id: 'admin', password: 'admin' })
cy.visit(`${Cypress.config('consoleRootPath')}/notifications`)
})

it('succeeds showing a list of notifications', () => {
cy.findByText('Notifications').should('be.visible')
cy.get(`[data-test-id="notification-list-item"]`).should('be.visible')
})

it('succeeds opening a notification', () => {
cy.findByText(/A collaborator of your application has been added or updated/).click()
cy.findByRole('button', { name: /Archive/ }).should('be.visible')
})

it('succeeds archiving and unarchiving a notification', () => {
cy.findByText(/A collaborator of your application has been added or updated/).click()
cy.findByRole('button', { name: /Archive/ }).click()
cy.findByText(/See archived messages/).click()
cy.findByText(/A collaborator of your application has been added or updated/).should(
'be.visible',
)
cy.findByText(/A collaborator of your application has been added or updated/).click()
cy.findByRole('button', { name: /Unarchive/ }).click()
cy.findByText(/See all messages/).click()
cy.findByText(/A collaborator of your application has been added or updated/).should(
'be.visible',
)
})

it('succeeds marking all notifications as read', () => {
cy.findByTestId('total-unseen-notifications').should('be.visible')
cy.findByRole('button', { name: /Mark all as read/ }).click()
cy.findByTestId('total-unseen-notifications').should('not.exist')
})
})
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@
"react-toastify": "^9.1.3",
"react-virtualized-auto-sizer": "^1.0.21",
"react-window": "^1.8.10",
"react-window-infinite-loader": "^1.0.9",
"redux": "^4.2.1",
"redux-actions": "^2.6.5",
"redux-logic": "^5.0.1",
Expand Down
5 changes: 4 additions & 1 deletion pkg/webui/account/containers/collaborators-table/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,10 @@ const CollaboratorsTable = props => {
return (
<span>
{collaboratorId}{' '}
<Message className="c-text-neutral-light" content={sharedMessages.currentUserIndicator} />
<Message
className="c-text-neutral-light"
content={sharedMessages.currentUserIndicator}
/>
</span>
)
}
Expand Down
11 changes: 11 additions & 0 deletions pkg/webui/components/button/button.styl
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,17 @@
&:only-child
margin-right: - $cs.xxs

&.with-alert:after
content: ''
position: absolute
top: 4px
left: 17px
border: 3px solid var(--c-border-neutral-min)
border-radius: 100%
size: 13px
box-sizing: border-box
background-color: var(--c-bg-brand-normal)

.expand-icon
color: var(--c-icon-neutral-light)
font-size: 1.285rem
Expand Down
25 changes: 23 additions & 2 deletions pkg/webui/components/button/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@ import { useIntl } from 'react-intl'
import Link from '@ttn-lw/components/link'
import Spinner from '@ttn-lw/components/spinner'
import Icon from '@ttn-lw/components/icon'
import Dropdown from '@ttn-lw/components/dropdown'
import Status from '@ttn-lw/components/status'

import Message from '@ttn-lw/lib/components/message'

import combineRefs from '@ttn-lw/lib/combine-refs'
import PropTypes from '@ttn-lw/lib/prop-types'

import Dropdown from '../dropdown'

import style from './button.styl'

const filterDataProps = props =>
Expand All @@ -50,6 +52,7 @@ const assembleClassnames = ({
dropdownItems,
className,
error,
withAlert,
}) =>
classnames(style.button, {
[className]: !Boolean(dropdownItems), // If there are dropdown items, the button is wrapped in a div with the className.
Expand All @@ -65,6 +68,7 @@ const assembleClassnames = ({
[style.onlyIcon]: icon !== undefined && !message,
[style.withDropdown]: Boolean(dropdownItems),
[style.error]: error && !busy,
[style.withAlert]: withAlert,
})

const buttonChildren = props => {
Expand Down Expand Up @@ -171,16 +175,28 @@ Button.defaultProps = {
}

const LinkButton = props => {
const { disabled, titleMessage } = props
const { disabled, titleMessage, onClick, value } = props
const buttonClassNames = assembleClassnames(props)
const { to } = props

const handleClick = useCallback(
evt => {
// Passing a value to the onClick handler is useful for components that
// are rendered multiple times, e.g. in a list. The value can be used to
// identify the component that was clicked.
onClick(evt, value)
},
[onClick, value],
)

return (
<Link
className={buttonClassNames}
to={to}
disabled={disabled}
title={titleMessage}
children={buttonChildren(props)}
onClick={handleClick}
/>
)
}
Expand Down Expand Up @@ -291,10 +307,15 @@ Button.defaultProps = {
}

LinkButton.propTypes = {
onClick: PropTypes.func,
...commonPropTypes,
...Link.propTypes,
}

LinkButton.defaultProps = {
onClick: () => null,
}

Button.Link = LinkButton
Button.Link.displayName = 'Button.Link'

Expand Down
9 changes: 7 additions & 2 deletions pkg/webui/components/dropdown/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -188,18 +188,23 @@ DropdownItem.defaultProps = {
submenuItems: undefined,
}

const DropdownHeaderItem = ({ title }) => (
<li className={style.dropdownHeaderItem}>
const DropdownHeaderItem = ({ title, className }) => (
<li className={classnames(style.dropdownHeaderItem, className)}>
<span>
<Message content={title} />
</span>
</li>
)

DropdownHeaderItem.propTypes = {
className: PropTypes.string,
title: PropTypes.message.isRequired,
}

DropdownHeaderItem.defaultProps = {
className: undefined,
}

Dropdown.Item = DropdownItem
Dropdown.HeaderItem = DropdownHeaderItem
Dropdown.Attached = AttachedDropdown
Expand Down
5 changes: 5 additions & 0 deletions pkg/webui/components/header/header.styl
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@
+media-query($bp.xs)
padding: 0 $cs.xs

.notifications-dropdown
max-width: 31rem
width: 31rem // Avoids flash of small container when dropdown is opened
padding: 0

.logo
height: $cs.l
+media-query($bp.xs)
Expand Down
16 changes: 15 additions & 1 deletion pkg/webui/components/header/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@ const Header = ({
addDropdownItems,
starDropdownItems,
profileDropdownItems,
notificationsDropdownItems,
user,
onMenuClick,
showNotificationDot,
...rest
}) => (
<header {...rest} className={classnames(className, style.container)}>
Expand All @@ -49,7 +51,14 @@ const Header = ({
dropdownPosition="below left"
className="xs:d-none"
/>
<Button secondary icon="inbox" dropdownItems={<></>} dropdownPosition="below left" />
<Button
secondary
icon="inbox"
dropdownItems={notificationsDropdownItems}
dropdownClassName={style.notificationsDropdown}
dropdownPosition="below left"
withAlert={showNotificationDot}
/>
<ProfileDropdown
brandLogo={brandLogo}
data-test-id="profile-dropdown"
Expand All @@ -73,10 +82,14 @@ Header.propTypes = {
brandLogo: imgPropType,
/** The classname applied to the component. */
className: PropTypes.string,
/** The dropdown items when the notifications button is clicked. */
notificationsDropdownItems: PropTypes.node.isRequired,
/** A handler for when the menu button is clicked. */
onMenuClick: PropTypes.func.isRequired,
/** The dropdown items when the profile button is clicked. */
profileDropdownItems: PropTypes.node.isRequired,
/** Whether to show a notification dot. */
showNotificationDot: PropTypes.bool,
/** The dropdown items when the star button is clicked. */
starDropdownItems: PropTypes.node.isRequired,
/**
Expand All @@ -90,6 +103,7 @@ Header.defaultProps = {
className: undefined,
user: undefined,
brandLogo: undefined,
showNotificationDot: false,
}

export default Header
6 changes: 3 additions & 3 deletions pkg/webui/components/spinner/spinner.styl
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ $xs = $cs.m
.center
&:not(.inline)
position: absolute
top: ($l * -1) // To achieve visual centering.
top: 0
left: 0
right: 0
bottom: 0
Expand All @@ -83,11 +83,11 @@ $xs = $cs.m

&.small
height: $s
top: ($s * -1) // To achieve visual centering.
top: 0

&.micro
height: $xs
top: ($xs * -1) // To achieve visual centering.
top: 0

.small .spinner
width: $s
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,11 @@ const ApplicationGeneralSettingsForm = ({
isResctrictedUser={isResctrictedUser}
userId={userId}
/>
<Message content={m.techContactDescription} component="p" className="mt-cs-xs c-text-neutral-light" />
<Message
content={m.techContactDescription}
component="p"
className="mt-cs-xs c-text-neutral-light"
/>
<SubmitBar>
<Form.Submit component={SubmitButton} message={sharedMessages.saveChanges} />
<Require featureCheck={mayDeleteApplication}>
Expand Down
33 changes: 33 additions & 0 deletions pkg/webui/console/components/notifications/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright © 2023 The Things Network Foundation, The Things Industries B.V.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// 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.

import ApiKeyCreated from './templates/api-key-created'
import ApiKeyChanged from './templates/api-key-changed'
import ClientRequested from './templates/client-requested'
import CollaboratorChanged from './templates/collaborator-changed'
import EntityStateChanged from './templates/entity-state-changed'
import PasswordChanged from './templates/password-changed'
import UserRequested from './templates/user-requested'

const notificationMap = {
api_key_created: ApiKeyCreated,
api_key_changed: ApiKeyChanged,
client_requested: ClientRequested,
collaborator_changed: CollaboratorChanged,
entity_state_changed: EntityStateChanged,
password_changed: PasswordChanged,
user_requested: UserRequested,
}

export default notificationMap
Loading
Loading