AddCloudConnection.js 18 KB

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