2
0

AddCloudConnection.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625
  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. /* eslint-disable dot-notation */
  15. import React, { PropTypes } from 'react';
  16. import withStyles from 'isomorphic-style-loader/lib/withStyles';
  17. import s from './AddCloudConnection.scss';
  18. import Reflux from 'reflux';
  19. import ConnectionsStore from '../../stores/ConnectionsStore';
  20. import ConnectionsActions from '../../actions/ConnectionsActions';
  21. import NotificationActions from '../../actions/NotificationActions';
  22. import Dropdown from '../NewDropdown';
  23. import Switch from '../Switch'
  24. import LoadingIcon from "../LoadingIcon/LoadingIcon";
  25. import ValidateEndpoint from '../ValidateEndpoint';
  26. const title = 'Add Cloud Endpoint';
  27. class AddCloudConnection extends Reflux.Component {
  28. static contextTypes = {
  29. onSetTitle: PropTypes.func.isRequired,
  30. };
  31. static defaultProps = {
  32. cloud: null,
  33. connection: null,
  34. type: "new"
  35. }
  36. constructor(props) {
  37. super(props)
  38. this.store = ConnectionsStore
  39. this.state = {
  40. type: props.type, // type of operation: new/edit
  41. connection: props.connection, // connection object (on edit)
  42. connectionName: "", // connection name field
  43. description: "", // connection description field
  44. currentCloud: this.props.cloud, // chosen cloud - if adding a new endpoint
  45. currentCloudData: null, // endpoint field data
  46. validateEndpoint: false, // holds the endpoint object when validation
  47. requiredFields: [], // array that holds all the endpoint required fields - used for field validation
  48. cloudFormsSubmitted: false // flag that indicates if the form has been submitted - used for field validation
  49. }
  50. }
  51. componentWillMount() {
  52. super.componentWillMount.call(this)
  53. this.context.onSetTitle(title);
  54. if (this.state.currentCloudData == null) {
  55. this.setState({ currentCloudData: {} })
  56. }
  57. }
  58. componentDidMount() {
  59. if (this.state.connection) {
  60. this.state.allClouds.forEach(item => {
  61. if (item.name === this.state.connection.type) {
  62. let credentials = this.state.connection.credentials
  63. let newCredentials = {}
  64. for (let i in credentials) {
  65. if (typeof credentials[i] == "object") {
  66. newCredentials['login_type'] = i
  67. // credentials['user_credentials'] = {}
  68. for (let j in credentials[i]) {
  69. // credentials['user_credentials'][j] = credentials[i][j]
  70. newCredentials[j] = credentials[i][j]
  71. }
  72. } else if (typeof credentials[i] === 'boolean') {
  73. newCredentials[i] = credentials[i]
  74. } else {
  75. newCredentials[i] = credentials[i] + ""
  76. }
  77. }
  78. this.setState({
  79. currentCloudData: newCredentials,
  80. connectionName: this.state.connection.name,
  81. description: this.state.connection.description
  82. }, () => {
  83. this.chooseCloud(item)
  84. })
  85. }
  86. })
  87. } else if (this.props.cloud) {
  88. this.chooseCloud(this.props.cloud)
  89. }
  90. // Fixes an issue with focus when multiple modals are rendered and escape key is not captured.
  91. // Test with adding cloud connection from wizard.
  92. setTimeout(() => { this.rootDiv.focus() }, 100)
  93. }
  94. /**
  95. * Function called upon saving an endpoint - handles both new and edit operations
  96. */
  97. handleSave() {
  98. let valid = true
  99. let requiredFields = this.state.requiredFields
  100. for (let i in this.state.currentCloudData) {
  101. if (requiredFields.indexOf(i) > -1 && !this.state.currentCloudData[i]) {
  102. valid = false
  103. }
  104. }
  105. requiredFields.forEach((field) => {
  106. if (!this.state.currentCloudData[field]) {
  107. valid = false
  108. }
  109. })
  110. if (this.state.connectionName.trim().length == 0) {
  111. valid = false
  112. }
  113. if (!valid) {
  114. NotificationActions.notify("Please fill all required fields", "error")
  115. this.setState({ cloudFormsSubmitted: true })
  116. } else {
  117. let credentials = Object.assign({}, this.state.currentCloudData)
  118. for (let key in credentials) {
  119. if (credentials[key].label) {
  120. credentials[key] = credentials[key].value
  121. }
  122. let field = this.state.currentCloud.endpoint.fields.find(function findByName(f) { return f.name == this }, key);
  123. if (!field || !field.dataType) {
  124. continue;
  125. }
  126. // Convert datatype
  127. switch (field.dataType) {
  128. case 'boolean':
  129. credentials[key] = (credentials[key] === true ||
  130. ((typeof credentials[key] === 'string' || credentials[key] instanceof String) &&
  131. credentials[key].toLowerCase() == "true"));
  132. break;
  133. case 'integer':
  134. let value = parseInt(credentials[key], 10);
  135. if (value.toString() != credentials[key]) {
  136. valid = false;
  137. NotificationActions.notify('"' + key + '" needs to be an integer', 'error');
  138. }
  139. credentials[key] = value;
  140. break;
  141. default:
  142. // retain original value
  143. break;
  144. }
  145. }
  146. if (!valid) {
  147. return;
  148. }
  149. // If there's a switch radio, create a hierarchical structure with the selected radio as the root.
  150. this.state.currentCloud.endpoint.fields.forEach(field => {
  151. if (field.type === 'switch-radio') {
  152. credentials[credentials[field.name]] = {}
  153. field.options.forEach(fieldOptions => {
  154. if (fieldOptions.value === credentials[field.name]) {
  155. fieldOptions.fields.forEach(fieldOptionField => {
  156. credentials[credentials[field.name]][fieldOptionField.name] = credentials[fieldOptionField.name]
  157. })
  158. }
  159. })
  160. }
  161. })
  162. // If endpoint is new
  163. if (this.state.type == "new") {
  164. ConnectionsActions.newEndpoint({
  165. name: this.state.connectionName,
  166. description: this.state.description,
  167. type: this.state.currentCloud.name,
  168. connection_info: credentials
  169. }, (response) => {
  170. this.setState({
  171. validateEndpoint: response.data.endpoint,
  172. type: "edit",
  173. connection: response.data.endpoint
  174. })
  175. })
  176. this.props.addHandle(this.state.connectionName);
  177. } else { // If editing an endpoint
  178. ConnectionsActions.editEndpoint(this.state.connection, {
  179. name: this.state.connectionName,
  180. description: this.state.description,
  181. connection_info: credentials
  182. }, (response) => {
  183. this.setState({
  184. validateEndpoint: response.data.endpoint,
  185. type: "edit",
  186. connection: response.data.endpoint
  187. })
  188. this.props.updateHandle({
  189. name: this.state.connectionName,
  190. description: this.state.description
  191. })
  192. })
  193. }
  194. }
  195. }
  196. /**
  197. * Handles change `name` property
  198. * @param e
  199. */
  200. handleChangeName(e) {
  201. this.setState({ connectionName: e.target.value })
  202. }
  203. /**
  204. * Handles change `description` property
  205. * @param e
  206. */
  207. handleChangeDescription(e) {
  208. this.setState({ description: e.target.value })
  209. }
  210. /**
  211. * Handler to choose the cloud which the endpoint will be assigned to
  212. * @param cloud
  213. */
  214. chooseCloud(cloud) {
  215. let currentCloudData = {}
  216. if (this.state.currentCloudData !== null) {
  217. currentCloudData = this.state.currentCloudData
  218. }
  219. let requiredFields = []
  220. cloud.endpoint.fields.forEach(field => {
  221. if (typeof currentCloudData[field.name] == "undefined") {
  222. if (typeof field.defaultValue === 'undefined') {
  223. currentCloudData[field.name] = "";
  224. } else {
  225. currentCloudData[field.name] = field.defaultValue.toString();
  226. }
  227. }
  228. if (field.required) {
  229. requiredFields.push(field.name)
  230. }
  231. })
  232. this.setState({
  233. currentCloud: cloud,
  234. currentCloudData: currentCloudData,
  235. requiredFields: requiredFields
  236. }, this.setDefaultValues)
  237. }
  238. /**
  239. * Function that goes back from endpoint validation to edit mode
  240. */
  241. backToEdit() {
  242. this.setState({ validateEndpoint: null })
  243. }
  244. /**
  245. * Handles back operation when adding a new endpoint and want to switch cloud. Resets all previous cloud data.
  246. */
  247. handleBack() {
  248. this.setState({
  249. currentCloudData: null,
  250. currentCloud: null,
  251. requiredFields: null,
  252. connectionName: "",
  253. description: null
  254. })
  255. }
  256. /**
  257. * Sets default values for cloud fields
  258. */
  259. setDefaultValues() {
  260. this.state.currentCloud.endpoint.fields.forEach(field => {
  261. let currentCloudData = this.state.currentCloudData
  262. switch (field.type) {
  263. case 'switch':
  264. if (field.default && typeof currentCloudData[field.name] == "undefined") {
  265. currentCloudData[field.name] = field.default
  266. this.setState({ currentCloudData: currentCloudData })
  267. }
  268. break
  269. case 'switch-radio':
  270. field.options.forEach(option => {
  271. if (option.default && !currentCloudData[field.name]) {
  272. currentCloudData[field.name] = option.value
  273. this.setRadioRequiredFields(field, option.value)
  274. this.setState({ currentCloudData: currentCloudData })
  275. }
  276. }, this)
  277. break;
  278. case 'text':
  279. if (field.default && typeof currentCloudData[field.name] == "undefined") {
  280. currentCloudData[field.name] = field.default
  281. this.setState({ currentCloudData: currentCloudData })
  282. }
  283. break
  284. default:
  285. break;
  286. }
  287. }, this)
  288. }
  289. /**
  290. * Checks whether the field is valid. Only goes through validation if field is required
  291. * @param field
  292. * @returns {boolean}
  293. */
  294. isValid(field) {
  295. if (field.required && this.state.cloudFormsSubmitted) {
  296. if (this.state.currentCloudData[field.name]) {
  297. return !(this.state.currentCloudData[field.name] && this.state.currentCloudData[field.name].length == 0);
  298. } else {
  299. return false
  300. }
  301. } else {
  302. return true
  303. }
  304. }
  305. /**
  306. * Dinamically change the required fields affected by the current radio selection
  307. * @param field
  308. * @param currentValue
  309. */
  310. setRadioRequiredFields(field, currentValue) {
  311. let requiredFields = this.state.requiredFields || [];
  312. // Remove fields set by previous radio change
  313. field.options.forEach(option => {
  314. option.fields.forEach(f => {
  315. requiredFields = requiredFields.filter(rf => rf !== f.name)
  316. })
  317. })
  318. field.options.forEach(option => {
  319. if (option.value === currentValue) {
  320. option.fields.forEach(optionField => {
  321. if (optionField.required) {
  322. requiredFields.push(optionField.name);
  323. }
  324. })
  325. }
  326. })
  327. this.setState({ requiredFields: requiredFields });
  328. }
  329. /**
  330. * Handles cancel edit/add endpoint
  331. */
  332. handleCancel() {
  333. this.props.closeHandle();
  334. }
  335. /**
  336. * Handler to change the endpoint field
  337. * @param e
  338. * @param field
  339. */
  340. handleCloudFieldChange(e, field) {
  341. let currentCloudData = this.state.currentCloudData
  342. if (field.type == 'dropdown') {
  343. currentCloudData[field.name] = e.value
  344. } else if (field.type === 'switch') {
  345. currentCloudData[field.name] = e.target.checked
  346. } else {
  347. currentCloudData[field.name] = e.target.value
  348. }
  349. if (field.type === 'switch-radio') {
  350. this.setRadioRequiredFields(field, e.target.value)
  351. }
  352. this.setState({ currentCloudData: currentCloudData })
  353. }
  354. /**
  355. * Renders the cloud list
  356. * @returns {XML}
  357. */
  358. renderCloudList() {
  359. let clouds = this.state.allClouds.map((cloud, index) => {
  360. let colorType = ""
  361. if (cloud.credentials != null && cloud.credentials.length != 0) {
  362. colorType = ""
  363. }
  364. return (
  365. <div className={s.cloudContainer} key={"cloudImage_" + index}>
  366. <div
  367. className={s.cloudImage + " icon large-cloud " + cloud.name + " " + colorType}
  368. onClick={() => this.chooseCloud(cloud)}
  369. ></div>
  370. </div>
  371. )
  372. }, this)
  373. return (
  374. <div className={s.container}>
  375. <div className={s.cloudList}>
  376. {clouds}
  377. </div>
  378. <div className={s.buttons}>
  379. <button className={s.centerBtn + " gray"} onClick={(e) => this.handleCancel(e)}>Cancel</button>
  380. </div>
  381. </div>
  382. )
  383. }
  384. /**
  385. * Renders individual cloud fields
  386. * @param field
  387. * @returns {XML}
  388. */
  389. renderField(field) {
  390. let returnValue
  391. switch (field.type) {
  392. case "text":
  393. returnValue = (
  394. <div className={"form-group " + (this.isValid(field) ? "" : s.error)} key={"cloudField_" + field.name}>
  395. <div className="input-label">
  396. {field.label + (field.required ? " *" : "")}
  397. </div>
  398. <input
  399. type="text"
  400. placeholder={field.label + (field.required ? " *" : "")}
  401. onChange={(e) => this.handleCloudFieldChange(e, field)}
  402. value={this.state.currentCloudData[field.name]}
  403. />
  404. </div>
  405. )
  406. break;
  407. case "password":
  408. returnValue = (
  409. <div className={"form-group " + (this.isValid(field) ? "" : s.error)} key={"cloudField_" + field.name}>
  410. <div className="input-label">
  411. {field.label + (field.required ? " *" : "")}
  412. </div>
  413. <input
  414. type="password"
  415. placeholder={field.label + (field.required ? " *" : "")}
  416. onChange={(e) => this.handleCloudFieldChange(e, field)}
  417. value={this.state.currentCloudData[field.name]}
  418. />
  419. </div>
  420. )
  421. break;
  422. case 'switch':
  423. returnValue = (
  424. <div
  425. className="form-group"
  426. key={"cloudField_" + field.name}
  427. >
  428. <div className="input-label">
  429. {field.label + (field.required ? " *" : "")}
  430. </div>
  431. <Switch
  432. className={s.switchButton}
  433. labelClassName={s.switchLabel}
  434. checked={this.state.currentCloudData[field.name] === true}
  435. onChange={(e) => this.handleCloudFieldChange(e, field)}
  436. checkedLabel="Yes"
  437. uncheckedLabel="No"
  438. />
  439. </div>
  440. )
  441. break
  442. case "dropdown":
  443. returnValue = (
  444. <div className={"form-group " + (this.isValid(field) ? "" : s.error)} key={"cloudField_" + field.name}>
  445. <div className="input-label">
  446. {field.label + (field.required ? " *" : "")}
  447. </div>
  448. <Dropdown
  449. options={field.options}
  450. onChange={(e) => this.handleCloudFieldChange(e, field)}
  451. placeholder="Choose a value"
  452. value={field.options.find(function findOption(option) { return option.value == this},
  453. this.state.currentCloudData[field.name])}
  454. />
  455. </div>
  456. )
  457. break;
  458. case "switch-radio":
  459. let fields = ""
  460. field.options.forEach((option) => {
  461. if (option.value == this.state.currentCloudData[field.name]) {
  462. fields = option.fields.map((optionField) => this.renderField(optionField))
  463. }
  464. })
  465. let radioOptions = field.options.map((option, key) => (
  466. <div key={"radio_option_" + key} className={s.radioOption}>
  467. <input
  468. type="radio"
  469. value={option.value}
  470. id={option.name}
  471. checked={option.value == this.state.currentCloudData[field.name]}
  472. onChange={(e) => this.handleCloudFieldChange(e, field)}
  473. /> <label htmlFor={option.name}>{option.label}</label>
  474. </div>
  475. )
  476. )
  477. returnValue = (
  478. <div key={"cloudField_" + field.name}>
  479. <div className="form-group switch-radio" key={"cloudField_" + field.name}>
  480. { radioOptions }
  481. </div>
  482. <div></div>
  483. <div className={s.cloudFields + ' ' + s.radioFields}>
  484. {fields}
  485. </div>
  486. </div>
  487. )
  488. break;
  489. default:
  490. break
  491. }
  492. return returnValue
  493. }
  494. /**
  495. * Renders the new/edit endpoint form
  496. * @param cloud
  497. * @returns {XML}
  498. */
  499. renderCloudFields(cloud) {
  500. let fields = cloud.endpoint.fields.map(field => this.renderField(field), this)
  501. return (
  502. <div className={s.container}>
  503. <div className={s.cloudImage}>
  504. <div className={" icon large-cloud " + this.state.currentCloud.name}></div>
  505. </div>
  506. <div className={s.cloudFields + (cloud.endpoint.fields.length > 6 ? " " + s.larger : "")}>
  507. <div className={"form-group " + (this.state.cloudFormsSubmitted &&
  508. this.state.connectionName.trim().length == 0 ? s.error : "")}
  509. >
  510. <div className="input-label">
  511. Endpoint Name *
  512. </div>
  513. <input
  514. type="text"
  515. placeholder="Endpoint Name *"
  516. onChange={(e) => this.handleChangeName(e)}
  517. value={this.state.connectionName}
  518. />
  519. </div>
  520. <div className="form-group">
  521. <div className="input-label">
  522. Endpoint Description
  523. </div>
  524. <input
  525. type="text"
  526. placeholder="Endpoint Description"
  527. onChange={(e) => this.handleChangeDescription(e)}
  528. value={this.state.description}
  529. ></input>
  530. </div>
  531. {fields}
  532. </div>
  533. <div className={s.buttons}>
  534. {(this.state.type == "new" && this.props.cloud == null) ? (
  535. <button className={s.leftBtn + " gray"} onClick={(e) => this.handleBack(e)}>Back</button>
  536. ) : (
  537. <button className={s.leftBtn + " gray"} onClick={(e) => this.handleCancel(e)}>Cancel</button>
  538. )}
  539. <button className={s.rightBtn} onClick={(e) => this.handleSave(e)}>Save</button>
  540. </div>
  541. </div>
  542. )
  543. }
  544. render() {
  545. let modalBody
  546. if (this.state.validateEndpoint) {
  547. modalBody = (
  548. <ValidateEndpoint
  549. closeHandle={this.props.closeHandle}
  550. endpoint={this.state.validateEndpoint}
  551. backHandle={(e) => this.backToEdit(e)}
  552. />
  553. )
  554. } else if (this.state.currentCloud == null) {
  555. if (this.state.allClouds) {
  556. modalBody = this.renderCloudList()
  557. } else {
  558. modalBody = <LoadingIcon />
  559. }
  560. } else {
  561. modalBody = this.renderCloudFields(this.state.currentCloud)
  562. }
  563. return (
  564. <div tabIndex="0" className={s.root} ref={rootDiv => { this.rootDiv = rootDiv }}>
  565. <div className={s.header}>
  566. <h3>{title}</h3>
  567. </div>
  568. {modalBody}
  569. </div>
  570. );
  571. }
  572. }
  573. export default withStyles(AddCloudConnection, s);