ProjectMemberModal.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394
  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 { observer } from 'mobx-react'
  16. import styled from 'styled-components'
  17. import type { Field as FieldType } from '../../../../@types/Field'
  18. import type { User } from '../../../../@types/User'
  19. import type { Project, Role } from '../../../../@types/Project'
  20. import Button from '../../../ui/Button/Button'
  21. import Modal from '../../../ui/Modal/Modal'
  22. import FieldInput from '../../../ui/FieldInput/FieldInput'
  23. import ToggleButtonBar from '../../../ui/ToggleButtonBar/ToggleButtonBar'
  24. import AutocompleteDropdown from '../../../ui/Dropdowns/AutocompleteDropdown/AutocompleteDropdown'
  25. import { ThemePalette, ThemeProps } from '../../../Theme'
  26. import KeyboardManager from '../../../../utils/KeyboardManager'
  27. import userImage from './images/user.svg'
  28. const Wrapper = styled.div<any>`
  29. padding: 48px 0 32px 0;
  30. display: flex;
  31. flex-direction: column;
  32. min-height: 0;
  33. `
  34. const Image = styled.div<any>`
  35. ${ThemeProps.exactSize('96px')}
  36. background: url('${userImage}') center no-repeat;
  37. margin: 0 auto;
  38. `
  39. const ToggleButtonBarStyled = styled(ToggleButtonBar)`
  40. margin-top: 48px;
  41. `
  42. const Form = styled.div<any>`
  43. display: flex;
  44. justify-content: space-between;
  45. flex-wrap: wrap;
  46. margin-top: 32px;
  47. overflow: auto;
  48. padding: 0 32px;
  49. > div {
  50. margin-top: 16px;
  51. }
  52. `
  53. const FieldStyled = styled(FieldInput)`
  54. ${ThemeProps.exactWidth(`${ThemeProps.inputSizes.large.width}px`)}
  55. `
  56. const FormField = styled.div<any>``
  57. const FormLabel = styled.div<any>`
  58. font-size: 10px;
  59. font-weight: ${ThemeProps.fontWeights.medium};
  60. color: ${ThemePalette.grayscale[3]};
  61. text-transform: uppercase;
  62. margin-bottom: 2px;
  63. display: flex;
  64. align-items: center;
  65. `
  66. const Buttons = styled.div<any>`
  67. margin-top: 32px;
  68. display: flex;
  69. justify-content: space-between;
  70. padding: 0 32px;
  71. `
  72. type Props = {
  73. loading: boolean,
  74. users: User[],
  75. projects: Project[],
  76. onRequestClose: () => void,
  77. onAddClick: (user: User, isNew: boolean, roles: Role[]) => void,
  78. roles: Role[],
  79. }
  80. type State = {
  81. isNew: boolean,
  82. selectedUser?: User | null,
  83. username: string,
  84. description: string,
  85. email: string,
  86. projectId: string,
  87. password: string,
  88. confirmPassword: string,
  89. enabled: boolean,
  90. highlightFieldNames: string[],
  91. selectedRolesExisting: string[],
  92. selectedRolesNew: string[],
  93. }
  94. const testName = 'pmModal'
  95. @observer
  96. class ProjectMemberModal extends React.Component<Props, State> {
  97. state: State = {
  98. isNew: false,
  99. selectedUser: null,
  100. username: '',
  101. description: '',
  102. email: '',
  103. projectId: '',
  104. password: '',
  105. confirmPassword: '',
  106. enabled: true,
  107. highlightFieldNames: [],
  108. selectedRolesExisting: [],
  109. selectedRolesNew: [],
  110. }
  111. componentDidMount() {
  112. KeyboardManager.onEnter('projectMemberModal', () => {
  113. this.handleAddClick()
  114. })
  115. }
  116. componentWillUnmount() {
  117. KeyboardManager.removeKeyDown('projectMemberModal')
  118. }
  119. handleAddClick() {
  120. if (this.highlightFields()) {
  121. return
  122. }
  123. let user: User
  124. let roles = []
  125. if (this.state.isNew) {
  126. user = {
  127. id: '',
  128. project: { id: '', name: '' },
  129. project_id: this.state.projectId,
  130. email: this.state.email,
  131. name: this.state.username,
  132. description: this.state.description,
  133. password: this.state.password,
  134. enabled: this.state.enabled,
  135. }
  136. roles = this.state.selectedRolesNew
  137. } else if (this.state.selectedUser) {
  138. user = this.state.selectedUser
  139. roles = this.state.selectedRolesExisting
  140. } else {
  141. return
  142. }
  143. roles = roles.map(id => this.props.roles.find(r => r.id === id) || { id: 'undefined', name: '' })
  144. this.props.onAddClick(user, this.state.isNew, roles)
  145. }
  146. highlightFields(): boolean {
  147. const highlightFieldNames = []
  148. if (!this.state.isNew) {
  149. if (!this.state.selectedUser) {
  150. highlightFieldNames.push('selectedUser')
  151. }
  152. if (this.state.selectedRolesExisting.length === 0) {
  153. highlightFieldNames.push('rolesExisting')
  154. }
  155. if (highlightFieldNames.length > 0) {
  156. this.setState({ highlightFieldNames })
  157. return true
  158. }
  159. this.setState({ highlightFieldNames: [] })
  160. return false
  161. }
  162. if (!this.state.username) {
  163. highlightFieldNames.push('username')
  164. }
  165. if (!this.state.password) {
  166. highlightFieldNames.push('password')
  167. }
  168. if (this.state.password && this.state.password !== this.state.confirmPassword) {
  169. highlightFieldNames.push('confirm_password')
  170. }
  171. if (this.state.selectedRolesNew.length === 0) {
  172. highlightFieldNames.push('rolesNew')
  173. }
  174. if (highlightFieldNames.length > 0) {
  175. this.setState({ highlightFieldNames })
  176. return true
  177. }
  178. this.setState({ highlightFieldNames: [] })
  179. return false
  180. }
  181. renderToggleButton() {
  182. const items = [{
  183. value: 'existing',
  184. label: 'Existing',
  185. }, {
  186. value: 'new',
  187. label: 'New',
  188. }]
  189. return (
  190. <ToggleButtonBarStyled
  191. data-test-id={`${testName}-formToggle`}
  192. items={items}
  193. selectedValue={this.state.isNew ? 'new' : 'existing'}
  194. onChange={item => { this.setState({ isNew: item.value === 'new' }) }}
  195. />
  196. )
  197. }
  198. renderRolesField() {
  199. const selectedRoles = this.state.isNew
  200. ? this.state.selectedRolesNew : this.state.selectedRolesExisting
  201. const setSelectedRoles = (roles: string[]) => {
  202. if (this.state.isNew) {
  203. this.setState({ selectedRolesNew: roles })
  204. } else {
  205. this.setState({ selectedRolesExisting: roles })
  206. }
  207. }
  208. const highlighFieldName = this.state.isNew ? 'rolesNew' : 'rolesExisting'
  209. return (
  210. <FieldInput
  211. data-test-id={`${testName}-roles`}
  212. key="roles"
  213. name="role(s)"
  214. label="Role(s)"
  215. type="array"
  216. onChange={roleId => {
  217. if (selectedRoles.find(id => id === roleId)) {
  218. setSelectedRoles(selectedRoles.filter(r => r !== roleId))
  219. } else {
  220. setSelectedRoles([...selectedRoles, roleId])
  221. }
  222. }}
  223. value={selectedRoles}
  224. width={ThemeProps.inputSizes.large.width}
  225. layout="modal"
  226. disabled={this.props.loading}
  227. enum={this.props.roles.filter(r => r.name !== 'key-manager:service-admin').map(r => ({ name: r.name, id: r.id }))}
  228. required
  229. highlight={Boolean(this.state.highlightFieldNames.find(n => n === highlighFieldName))}
  230. noSelectionMessage="Choose role(s)"
  231. noItemsMessage="No available roles"
  232. />
  233. )
  234. }
  235. renderField(field: FieldType, value: any, onChange: (value: any) => void) {
  236. return (
  237. <FieldStyled
  238. data-test-id={`${testName}-field-${field.name}`}
  239. key={field.name}
  240. name={field.name}
  241. type={field.type || 'string'}
  242. value={value}
  243. label={field.label}
  244. onChange={onChange}
  245. width={ThemeProps.inputSizes.large.width}
  246. disabled={this.props.loading}
  247. enum={field.enum}
  248. password={field.name === 'password' || field.name === 'confirm_password'}
  249. required={field.required}
  250. highlight={Boolean(this.state.highlightFieldNames.find(n => n === field.name))}
  251. noSelectionMessage="Choose a project"
  252. noItemsMessage="No available members"
  253. />
  254. )
  255. }
  256. renderNewForm() {
  257. const userProjects = this.props.projects.map(p => ({ name: p.name, id: p.id }))
  258. const chooseProject: { name: string, id: string | null } = { name: 'Choose a project', id: null }
  259. const fields = [
  260. this.renderField(
  261. { name: 'username', label: 'Username', required: true },
  262. this.state.username,
  263. username => { this.setState({ username }) },
  264. ),
  265. this.renderField(
  266. { name: 'description', label: 'Description' },
  267. this.state.description,
  268. description => { this.setState({ description }) },
  269. ),
  270. this.renderField(
  271. {
  272. name: 'Primary Project',
  273. label: 'Primary Project',
  274. enum: [chooseProject].concat(userProjects),
  275. },
  276. this.state.projectId,
  277. projectId => { this.setState({ projectId }) },
  278. ),
  279. this.renderRolesField(),
  280. this.renderField(
  281. { name: 'password', label: 'Password', required: true },
  282. this.state.password,
  283. password => { this.setState({ password }) },
  284. ),
  285. this.renderField(
  286. { name: 'confirm_password', label: 'Confirm Password', required: true },
  287. this.state.confirmPassword,
  288. confirmPassword => { this.setState({ confirmPassword }) },
  289. ),
  290. this.renderField(
  291. { name: 'Email', label: 'Email' },
  292. this.state.email,
  293. email => { this.setState({ email }) },
  294. ),
  295. this.renderField(
  296. { name: 'Enabled', label: 'Enabled', type: 'boolean' },
  297. this.state.enabled,
  298. enabled => { this.setState({ enabled }) },
  299. ),
  300. ]
  301. return (
  302. <Form>
  303. {fields}
  304. </Form>
  305. )
  306. }
  307. renderExistingForm() {
  308. const users = this.props.users.map(u => ({ label: u.name, value: u.id }))
  309. return (
  310. <Form style={{ marginBottom: '80px' }}>
  311. <FormField>
  312. <FormLabel>
  313. Username
  314. </FormLabel>
  315. <AutocompleteDropdown
  316. data-test-id={`${testName}-users`}
  317. items={users}
  318. disabled={this.props.loading}
  319. selectedItem={this.state.selectedUser ? this.state.selectedUser.id : ''}
  320. highlight={Boolean(this.state.highlightFieldNames.find(n => n === 'selectedUser'))}
  321. onChange={item => {
  322. this.setState({ selectedUser: this.props.users.find(u => u.id === item.value) })
  323. }}
  324. required
  325. />
  326. </FormField>
  327. {this.renderRolesField()}
  328. </Form>
  329. )
  330. }
  331. renderForm() {
  332. if (this.state.isNew) {
  333. return this.renderNewForm()
  334. }
  335. return this.renderExistingForm()
  336. }
  337. render() {
  338. return (
  339. <Modal
  340. isOpen
  341. title="Add Project Member"
  342. onRequestClose={this.props.onRequestClose}
  343. >
  344. <Wrapper>
  345. <Image />
  346. {this.renderToggleButton()}
  347. {this.renderForm()}
  348. <Buttons>
  349. <Button
  350. secondary
  351. large
  352. onClick={this.props.onRequestClose}
  353. >Cancel
  354. </Button>
  355. <Button
  356. large
  357. disabled={this.props.loading}
  358. onClick={() => { this.handleAddClick() }}
  359. data-test-id={`${testName}-addButton`}
  360. >Add Member
  361. </Button>
  362. </Buttons>
  363. </Wrapper>
  364. </Modal>
  365. )
  366. }
  367. }
  368. export default ProjectMemberModal