EndpointModal.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507
  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 styled from 'styled-components'
  16. import { observer } from 'mobx-react'
  17. import { observe } from 'mobx'
  18. import EndpointLogos from '../EndpointLogos/EndpointLogos'
  19. import StatusIcon from '../../../ui/StatusComponents/StatusIcon/StatusIcon'
  20. import CopyButton from '../../../ui/CopyButton/CopyButton'
  21. import StatusImage from '../../../ui/StatusComponents/StatusImage/StatusImage'
  22. import Button from '../../../ui/Button/Button'
  23. import LoadingButton from '../../../ui/LoadingButton/LoadingButton'
  24. import type { Endpoint as EndpointType } from '../../../../@types/Endpoint'
  25. import type { Field } from '../../../../@types/Field'
  26. import notificationStore from '../../../../stores/NotificationStore'
  27. import endpointStore from '../../../../stores/EndpointStore'
  28. import providerStore from '../../../../stores/ProviderStore'
  29. import ObjectUtils from '../../../../utils/ObjectUtils'
  30. import { ThemePalette } from '../../../Theme'
  31. import DomUtils from '../../../../utils/DomUtils'
  32. import { ContentPlugin } from '../../../../plugins'
  33. import DefaultContentPlugin from '../../../../plugins/default/ContentPlugin'
  34. import KeyboardManager from '../../../../utils/KeyboardManager'
  35. import { ProviderTypes } from '../../../../@types/Providers'
  36. const Wrapper = styled.div<any>`
  37. padding: 48px 0 32px 0;
  38. display: flex;
  39. align-items: center;
  40. flex-direction: column;
  41. min-height: 0;
  42. `
  43. const Status = styled.div<any>`
  44. display: flex;
  45. flex-direction: column;
  46. align-items: center;
  47. flex-shrink: 0;
  48. `
  49. const StatusHeader = styled.div<any>`
  50. display: flex;
  51. align-items: center;
  52. `
  53. const StatusMessage = styled.div<any>`
  54. margin-left: 8px;
  55. display: flex;
  56. align-items: center;
  57. line-height: 12px;
  58. `
  59. const ShowErrorButton = styled.span`
  60. font-size: 10px;
  61. color: ${ThemePalette.primary};
  62. margin-left: 8px;
  63. cursor: pointer;
  64. `
  65. const StatusError = styled.div<any>`
  66. max-width: 100%;
  67. margin: 16px 16px 0 16px;
  68. max-height: 140px;
  69. overflow: auto;
  70. cursor: pointer;
  71. &:hover > span {
  72. opacity: 1;
  73. }
  74. > span {
  75. background-position-y: 4px;
  76. margin-left: 4px;
  77. }
  78. `
  79. const Content = styled.div<any>`
  80. width: 100%;
  81. display: flex;
  82. flex-direction: column;
  83. min-height: 0;
  84. `
  85. const LoadingWrapper = styled.div<any>`
  86. display: flex;
  87. flex-direction: column;
  88. align-items: center;
  89. margin: 32px 0;
  90. `
  91. const LoadingText = styled.div<any>`
  92. font-size: 18px;
  93. margin-top: 32px;
  94. `
  95. const Buttons = styled.div<any>`
  96. display: flex;
  97. justify-content: space-between;
  98. margin-top: 32px;
  99. flex-shrink: 0;
  100. padding: 0 32px;
  101. `
  102. type Props = {
  103. type?: ProviderTypes | null,
  104. cancelButtonText: string,
  105. deleteOnCancel?: boolean,
  106. endpoint?: EndpointType | null,
  107. isNewEndpoint?: boolean,
  108. onCancelClick: (opts?: { autoClose?: boolean }) => void,
  109. onResizeUpdate?: (scrollableRef: HTMLElement, scrollOffset?: number) => void,
  110. }
  111. type State = {
  112. invalidFields: any[],
  113. validating: boolean,
  114. showErrorMessage: boolean,
  115. endpoint: EndpointType | null,
  116. isNew: boolean | null,
  117. }
  118. @observer
  119. class EndpointModal extends React.Component<Props, State> {
  120. static defaultProps = {
  121. cancelButtonText: 'Cancel',
  122. }
  123. state: State = {
  124. invalidFields: [],
  125. validating: false,
  126. showErrorMessage: false,
  127. endpoint: null,
  128. isNew: null,
  129. }
  130. scrollableRef!: HTMLElement
  131. closeTimeout: number | undefined
  132. contentPluginRef!: DefaultContentPlugin
  133. isValidateButtonEnabled: boolean = false
  134. providerStoreObserver!: () => void
  135. endpointValidationObserver!: () => void
  136. UNSAFE_componentWillMount() {
  137. this.UNSAFE_componentWillReceiveProps(this.props)
  138. this.providerStoreObserver = observe(providerStore, 'connectionInfoSchema', () => {
  139. if (this.props.onResizeUpdate) this.props.onResizeUpdate(this.scrollableRef)
  140. })
  141. this.endpointValidationObserver = observe(endpointStore, 'validation', () => {
  142. this.UNSAFE_componentWillReceiveProps(this.props)
  143. })
  144. }
  145. componentDidMount() {
  146. const loadSchema = async () => {
  147. if (!this.endpointType) {
  148. return
  149. }
  150. await providerStore.getConnectionInfoSchema(this.endpointType)
  151. this.fillRequiredDefaults()
  152. }
  153. loadSchema()
  154. KeyboardManager.onEnter('endpoint', () => {
  155. if (this.isValidateButtonEnabled) this.handleValidateClick()
  156. }, 2)
  157. }
  158. UNSAFE_componentWillReceiveProps(props: Props) {
  159. if (this.state.validating) {
  160. if (endpointStore.validation && !endpointStore.validation.valid) {
  161. this.setState({ validating: false })
  162. }
  163. }
  164. if (props.endpoint && endpointStore.connectionInfo) {
  165. const plugin: any = ContentPlugin.for(props.endpoint.type)
  166. this.setState(prevState => ({
  167. isNew: this.props.isNewEndpoint
  168. ? (prevState.isNew === null || prevState.isNew) : prevState.isNew,
  169. endpoint: {
  170. ...prevState.endpoint,
  171. ...ObjectUtils.flatten(props.endpoint || {},
  172. plugin.REQUIRES_PARENT_OBJECT_PATH),
  173. ...ObjectUtils.flatten(endpointStore.connectionInfo || {},
  174. plugin.REQUIRES_PARENT_OBJECT_PATH),
  175. },
  176. }))
  177. } else {
  178. this.setState(prevState => ({
  179. isNew: prevState.isNew === null || prevState.isNew,
  180. endpoint: {
  181. type: props.type,
  182. ...ObjectUtils.flatten(prevState.endpoint || {}),
  183. },
  184. }))
  185. }
  186. if (props.onResizeUpdate) props.onResizeUpdate(this.scrollableRef)
  187. }
  188. componentWillUnmount() {
  189. endpointStore.clearValidation()
  190. providerStore.clearConnectionInfoSchema()
  191. clearTimeout(this.closeTimeout)
  192. KeyboardManager.removeKeyDown('endpoint')
  193. this.providerStoreObserver()
  194. this.endpointValidationObserver()
  195. }
  196. get endpointType() {
  197. if (this.props.endpoint) {
  198. return this.props.endpoint.type
  199. }
  200. return this.props.type
  201. }
  202. getFieldValue(field: Field | null) {
  203. if (!field || !this.state.endpoint) {
  204. return ''
  205. }
  206. if (this.state.endpoint[field.name] != null) {
  207. return this.state.endpoint[field.name]
  208. }
  209. if (Object.keys(field).find(k => k === 'default')) {
  210. return field.default
  211. }
  212. if (field.type === 'integer') {
  213. return null
  214. }
  215. return ''
  216. }
  217. fillRequiredDefaults() {
  218. this.setState(prevState => {
  219. const endpoint: any = { ...prevState.endpoint }
  220. const requiredFieldsDefaults = providerStore.connectionInfoSchema
  221. .filter(f => f.required && f.default != null)
  222. requiredFieldsDefaults.forEach(f => {
  223. if (endpoint[f.name] == null) {
  224. endpoint[f.name] = f.default
  225. }
  226. })
  227. return { endpoint }
  228. })
  229. }
  230. handleFieldsChange(items: { field: Field, value: any }[]) {
  231. this.setState(prevState => {
  232. const endpoint: any = { ...prevState.endpoint }
  233. items.forEach(item => {
  234. let value = item.value
  235. if (item.field.type === 'array') {
  236. const arrayItems = endpoint[item.field.name] || []
  237. value = arrayItems.find((v: any) => v === item.value)
  238. ? arrayItems.filter((v: any) => v !== item.value) : [...arrayItems, item.value]
  239. }
  240. endpoint[item.field.name] = value
  241. })
  242. return { endpoint }
  243. })
  244. }
  245. handleValidateClick() {
  246. if (!this.highlightRequired()) {
  247. this.setState({ validating: true })
  248. notificationStore.alert('Saving endpoint ...')
  249. endpointStore.clearValidation()
  250. if (this.state.isNew) {
  251. this.add()
  252. } else {
  253. this.update()
  254. }
  255. } else {
  256. notificationStore.alert('Please fill all the required fields', 'error')
  257. }
  258. }
  259. handleShowErrorMessageClick() {
  260. this.setState(prevState => ({ showErrorMessage: !prevState.showErrorMessage }), () => {
  261. if (this.props.onResizeUpdate) this.props.onResizeUpdate(this.scrollableRef)
  262. })
  263. }
  264. handleCopyErrorMessageClick() {
  265. if (!endpointStore.validation) {
  266. return
  267. }
  268. const succesful = DomUtils.copyTextToClipboard(endpointStore.validation.message)
  269. if (succesful) {
  270. notificationStore.alert('The message has been copied to clipboard.')
  271. }
  272. }
  273. handleCancelClick() {
  274. if (this.props.deleteOnCancel && this.state.isNew === false) {
  275. endpointStore.delete(endpointStore.endpoints[0])
  276. }
  277. this.props.onCancelClick()
  278. }
  279. highlightRequired() {
  280. const invalidFields = this.contentPluginRef.findInvalidFields()
  281. this.setState({ invalidFields })
  282. return invalidFields.length > 0
  283. }
  284. async update() {
  285. const stateEndpoint = this.state.endpoint
  286. if (!stateEndpoint) {
  287. return
  288. }
  289. const endpoint = endpointStore.endpoints.find(e => e.id === stateEndpoint.id)
  290. if (!endpoint) {
  291. throw new Error('Endpoint not found in store')
  292. }
  293. await endpointStore.update(stateEndpoint)
  294. this.setState({ endpoint: ObjectUtils.flatten(endpoint) })
  295. notificationStore.alert('Validating endpoint ...')
  296. endpointStore.validate(endpoint)
  297. }
  298. async add() {
  299. if (!this.state.endpoint) {
  300. return
  301. }
  302. await endpointStore.add(this.state.endpoint)
  303. const endpoint = endpointStore.endpoints[0]
  304. this.setState({ isNew: false, endpoint: ObjectUtils.flatten(endpoint) })
  305. notificationStore.alert('Validating endpoint ...')
  306. endpointStore.validate(endpoint)
  307. }
  308. renderEndpointStatus() {
  309. const validation = endpointStore.validation
  310. if (!this.state.validating && !validation) {
  311. return null
  312. }
  313. let status = 'RUNNING'
  314. let message = 'Validating Endpoint ...'
  315. let error = null
  316. let showErrorButton = null
  317. if (validation) {
  318. if (validation.valid) {
  319. message = 'Endpoint is Valid'
  320. status = 'COMPLETED'
  321. } else {
  322. status = 'ERROR'
  323. message = 'Validation failed'
  324. if (validation.message) {
  325. showErrorButton = (
  326. <ShowErrorButton onClick={() => { this.handleShowErrorMessageClick() }}>
  327. {this.state.showErrorMessage ? 'Hide' : 'Show'} Error
  328. </ShowErrorButton>
  329. )
  330. error = this.state.showErrorMessage
  331. ? (
  332. <StatusError
  333. onClick={() => { this.handleCopyErrorMessageClick() }}
  334. >{validation.message}<CopyButton />
  335. </StatusError>
  336. ) : null
  337. }
  338. }
  339. }
  340. return (
  341. <Status data-test-id="endpointStatus">
  342. <StatusHeader>
  343. <StatusIcon status={status} />
  344. <StatusMessage>{message}{showErrorButton}</StatusMessage>
  345. </StatusHeader>
  346. {error}
  347. </Status>
  348. )
  349. }
  350. renderButtons() {
  351. this.isValidateButtonEnabled = true
  352. let actionButton = (
  353. <Button
  354. large
  355. onClick={() => this.handleValidateClick()}
  356. >Validate and save
  357. </Button>
  358. )
  359. let message = 'Validating Endpoint ...'
  360. if (this.state.validating || (endpointStore.validation && endpointStore.validation.valid)) {
  361. if (endpointStore.validation && endpointStore.validation.valid) {
  362. message = 'Saving ...'
  363. }
  364. this.isValidateButtonEnabled = false
  365. actionButton = <LoadingButton large>{message}</LoadingButton>
  366. }
  367. return (
  368. <Buttons>
  369. <Button
  370. large
  371. secondary
  372. onClick={() => {
  373. this.handleCancelClick()
  374. }}
  375. >{this.props.cancelButtonText}
  376. </Button>
  377. {actionButton}
  378. </Buttons>
  379. )
  380. }
  381. renderContent() {
  382. if (providerStore.connectionSchemaLoading || !this.endpointType) {
  383. return null
  384. }
  385. const contentElement: any = ContentPlugin.for(this.endpointType)
  386. return (
  387. <Content>
  388. {/* Fix browsers autofilling password fields */}
  389. <div style={{ position: 'absolute', left: '-10000px' }}>
  390. <input name="username" type="text" />
  391. <input name="password" type="password" />
  392. </div>
  393. {this.renderEndpointStatus()}
  394. {React.createElement(contentElement, {
  395. connectionInfoSchema: providerStore.connectionInfoSchema,
  396. validation: endpointStore.validation,
  397. invalidFields: this.state.invalidFields,
  398. validating: this.state.validating,
  399. disabled: this.state.validating,
  400. cancelButtonText: this.props.cancelButtonText,
  401. originalConnectionInfo: endpointStore.connectionInfo,
  402. getFieldValue: (field: Field | null) => this.getFieldValue(field),
  403. highlightRequired: () => { this.highlightRequired() },
  404. handleFieldChange: (field: Field | null, value: any) => {
  405. if (field) this.handleFieldsChange([{ field, value }])
  406. },
  407. handleFieldsChange: (fields: { field: Field; value: any }[]) => {
  408. this.handleFieldsChange(fields)
  409. },
  410. handleValidateClick: () => { this.handleValidateClick() },
  411. handleCancelClick: () => { this.handleCancelClick() },
  412. scrollableRef: (ref: HTMLElement) => { this.scrollableRef = ref },
  413. onRef: (ref: DefaultContentPlugin) => { this.contentPluginRef = ref },
  414. onResizeUpdate: (scrollOffset: number) => {
  415. if (this.props.onResizeUpdate) {
  416. this.props.onResizeUpdate(this.scrollableRef, scrollOffset)
  417. }
  418. },
  419. })}
  420. {this.renderButtons()}
  421. </Content>
  422. )
  423. }
  424. renderLoading() {
  425. if (!providerStore.connectionSchemaLoading) {
  426. return null
  427. }
  428. return (
  429. <LoadingWrapper>
  430. <StatusImage loading />
  431. <LoadingText>Loading connection schema ...</LoadingText>
  432. </LoadingWrapper>
  433. )
  434. }
  435. render() {
  436. if (endpointStore.validation && endpointStore.validation.valid
  437. && !this.closeTimeout) {
  438. this.closeTimeout = setTimeout(() => {
  439. this.props.onCancelClick({ autoClose: true })
  440. }, 2000)
  441. }
  442. return (
  443. <Wrapper>
  444. <EndpointLogos style={{ marginBottom: '16px' }} height={128} endpoint={this.endpointType} />
  445. {this.renderContent()}
  446. {this.renderLoading()}
  447. </Wrapper>
  448. )
  449. }
  450. }
  451. export default EndpointModal