AddCloudConnection.js 18 KB

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