NotificationDropdown.tsx 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. /*
  2. Copyright (C) 2017 Cloudbase Solutions SRL
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <http://www.gnu.org/licenses/>.
  13. */
  14. import React from 'react'
  15. import { Link } from 'react-router-dom'
  16. import { observer } from 'mobx-react'
  17. import styled, { css } from 'styled-components'
  18. import autobind from 'autobind-decorator'
  19. import Palette from '../../styleUtils/Palette'
  20. import StyleProps from '../../styleUtils/StyleProps'
  21. import type { NotificationItemData } from '../../../@types/NotificationItem'
  22. import StatusIcon from '../../atoms/StatusIcon'
  23. import bellImage from './images/bell'
  24. import loadingImage from './images/loading'
  25. const Wrapper = styled.div<any>`
  26. cursor: pointer;
  27. position: relative;
  28. `
  29. const Icon = styled.div<any>`
  30. position: relative;
  31. transition: all ${StyleProps.animations.swift};
  32. width: 32px;
  33. height: 32px;
  34. display: flex;
  35. align-items: center;
  36. justify-content: center;
  37. &:hover {
  38. opacity: 0.9;
  39. }
  40. `
  41. const BellIcon = styled.div<any>`
  42. width: 12px;
  43. height: 17px;
  44. `
  45. const bellBadgePostion = css`
  46. position: absolute;
  47. top: 6px;
  48. right: 9px;
  49. `
  50. const Badge = styled.div<any>`
  51. ${props => (props.isBellBadge ? bellBadgePostion : '')}
  52. background: ${Palette.primary};
  53. border-radius: 50%;
  54. width: 6px;
  55. height: 6px;
  56. text-align: center;
  57. `
  58. const List = styled.div<any>`
  59. cursor: pointer;
  60. background: ${Palette.grayscale[1]};
  61. border-radius: ${StyleProps.borderRadius};
  62. width: 272px;
  63. position: absolute;
  64. right: 0;
  65. top: 45px;
  66. z-index: 10;
  67. ${StyleProps.boxShadow}
  68. `
  69. const ListItemCss = css`
  70. display: flex;
  71. border-bottom: 1px solid ${Palette.grayscale[0]};
  72. padding: 8px;
  73. transition: all ${StyleProps.animations.swift};
  74. justify-content: space-between;
  75. text-decoration: none;
  76. color: inherit;
  77. &:first-child {
  78. position: relative;
  79. border-top-left-radius: ${StyleProps.borderRadius};
  80. border-top-right-radius: ${StyleProps.borderRadius};
  81. &:after {
  82. content: ' ';
  83. position: absolute;
  84. width: 10px;
  85. height: 10px;
  86. background: ${Palette.grayscale[1]};
  87. border: 1px solid ${Palette.grayscale[1]};
  88. border-color: transparent transparent ${Palette.grayscale[1]} ${Palette.grayscale[1]};
  89. transform: rotate(135deg);
  90. right: 10px;
  91. top: -6px;
  92. transition: all ${StyleProps.animations.swift};
  93. }
  94. }
  95. &:last-child {
  96. border-color: transparent;
  97. border-bottom-left-radius: ${StyleProps.borderRadius};
  98. border-bottom-right-radius: ${StyleProps.borderRadius};
  99. }
  100. `
  101. const ListItemNoLink = styled.div<any>`
  102. ${ListItemCss}
  103. cursor: default;
  104. `
  105. const ListItem = styled(Link)`
  106. ${ListItemCss}
  107. &:hover {
  108. background: ${Palette.grayscale[0]};
  109. }
  110. &:first-child {
  111. &:hover:after {
  112. background: ${Palette.grayscale[0]};
  113. border-color: transparent transparent ${Palette.grayscale[0]} ${Palette.grayscale[0]};
  114. }
  115. }
  116. `
  117. export const InfoColumn = styled.div<any>`
  118. display: flex;
  119. flex-direction: column;
  120. `
  121. export const BadgeColumn = styled.div<any>`
  122. display: flex;
  123. align-items: center;
  124. margin: 0 8px;
  125. `
  126. export const MainItemInfo = styled.div<any>`
  127. display: flex;
  128. align-items: center;
  129. margin-right: -8px;
  130. & > div {
  131. margin-right: 8px;
  132. }
  133. `
  134. export const ItemReplicaBadge = styled.div<any>`
  135. background: 'white';
  136. color: #7F8795;
  137. font-size: 9px;
  138. ${StyleProps.exactWidth('13px')}
  139. ${StyleProps.exactHeight('10px')}
  140. display: flex;
  141. justify-content: center;
  142. align-items: center;
  143. font-weight: 500;
  144. border-radius: 2px;
  145. border: 1px solid #7F8795;
  146. `
  147. export const ItemTitle = styled.div<any>`
  148. ${props => (props.nowrap ? 'white-space: nowrap;' : 'word-break: break-word;')}
  149. overflow: hidden;
  150. text-overflow: ellipsis;
  151. `
  152. export const ItemDescription = styled.div<any>`
  153. color: ${Palette.grayscale[5]};
  154. font-size: 10px;
  155. margin-top: 8px;
  156. `
  157. const NoItems = styled.div<any>`
  158. text-align: center;
  159. width: 100%;
  160. `
  161. const Loading = styled.div<any>`
  162. position: absolute;
  163. top: 0;
  164. left: 0;
  165. width: 32px;
  166. height: 32px;
  167. animation: rotate 3s linear infinite;
  168. @keyframes rotate {
  169. from {transform: rotate(0deg);}
  170. to {transform: rotate(360deg);}
  171. }
  172. `
  173. export type Props = {
  174. white?: boolean,
  175. items: NotificationItemData[],
  176. onClose: () => void,
  177. }
  178. type State = {
  179. showDropdownList: boolean,
  180. }
  181. const testId = 'notificationDropdown'
  182. @observer
  183. class NotificationDropdown extends React.Component<Props, State> {
  184. state = {
  185. showDropdownList: false,
  186. }
  187. itemMouseDown: boolean | undefined
  188. componentDidMount() {
  189. window.addEventListener('mousedown', this.handlePageClick, false)
  190. }
  191. componentWillUnmount() {
  192. window.removeEventListener('mousedown', this.handlePageClick, false)
  193. }
  194. handleItemClick() {
  195. this.setState({ showDropdownList: false })
  196. this.props.onClose()
  197. }
  198. @autobind
  199. handlePageClick() {
  200. if (!this.itemMouseDown) {
  201. if (this.state.showDropdownList) {
  202. this.props.onClose()
  203. }
  204. this.setState({ showDropdownList: false })
  205. }
  206. }
  207. handleButtonClick() {
  208. if (this.state.showDropdownList) {
  209. this.props.onClose()
  210. }
  211. this.setState(prevState => ({ showDropdownList: !prevState.showDropdownList }))
  212. }
  213. renderNoItems() {
  214. if (!this.state.showDropdownList || (this.props.items && this.props.items.length > 0)) {
  215. return null
  216. }
  217. return (
  218. <List>
  219. <ListItemNoLink
  220. onMouseDown={() => { this.itemMouseDown = true }}
  221. onMouseUp={() => { this.itemMouseDown = false }}
  222. >
  223. <NoItems data-test-id={`${testId}-noItems`}>There are no notifications</NoItems>
  224. </ListItemNoLink>
  225. </List>
  226. )
  227. }
  228. renderList() {
  229. if (!this.state.showDropdownList || !this.props.items || this.props.items.length === 0) {
  230. return null
  231. }
  232. const list = (
  233. <List>
  234. {this.props.items.map(item => {
  235. const executionsPath = item.status === 'RUNNING' ? item.type === 'replica' ? '/executions' : item.type === 'migration' ? '/tasks' : '' : ''
  236. return (
  237. <ListItem
  238. key={item.id}
  239. onMouseDown={() => { this.itemMouseDown = true }}
  240. onMouseUp={() => { this.itemMouseDown = false }}
  241. onClick={() => { this.handleItemClick() }}
  242. to={`/${item.type}s/${item.id}${executionsPath}`}
  243. >
  244. <InfoColumn>
  245. <MainItemInfo>
  246. <StatusIcon data-test-id={`${testId}-${item.id}-status`} status={item.status} hollow />
  247. <ItemReplicaBadge
  248. type={item.type}
  249. data-test-id={`${testId}-${item.id}-type`}
  250. >{item.type === 'replica' ? 'RE' : 'MI'}
  251. </ItemReplicaBadge>
  252. <ItemTitle data-test-id={`${testId}-${item.id}-name`}>{item.name}</ItemTitle>
  253. </MainItemInfo>
  254. <ItemDescription data-test-id={`${testId}-${item.id}-description`}>{item.description}</ItemDescription>
  255. </InfoColumn>
  256. {item.unseen ? <BadgeColumn data-test-id={`${testId}-${item.id}-badge`}><Badge /></BadgeColumn> : null}
  257. </ListItem>
  258. )
  259. })}
  260. </List>
  261. )
  262. return list
  263. }
  264. renderBell() {
  265. const isLoading = Boolean(this.props.items.find(i => i.status === 'RUNNING'))
  266. return (
  267. <Icon
  268. data-test-id={`${testId}-button`}
  269. onMouseDown={() => { this.itemMouseDown = true }}
  270. onMouseUp={() => { this.itemMouseDown = false }}
  271. onClick={() => this.handleButtonClick()}
  272. >
  273. <BellIcon
  274. dangerouslySetInnerHTML={{ __html: bellImage(this.props.white ? 'white' : Palette.grayscale[2]) }}
  275. />
  276. {this.props.items.find(i => i.unseen) ? <Badge data-test-id={`${testId}-bell-badge`} isBellBadge /> : null}
  277. {isLoading ? (
  278. <Loading
  279. data-test-id={`${testId}-bell-loading`}
  280. dangerouslySetInnerHTML={{ __html: loadingImage(this.props.white) }}
  281. />
  282. ) : null}
  283. </Icon>
  284. )
  285. }
  286. render() {
  287. return (
  288. <Wrapper>
  289. {this.renderBell()}
  290. {this.renderList()}
  291. {this.renderNoItems()}
  292. </Wrapper>
  293. )
  294. }
  295. }
  296. export default NotificationDropdown