AddCloudConnection.js 18 KB

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