ChooseProvider.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  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 { observer } from 'mobx-react'
  16. import styled from 'styled-components'
  17. import notificationStore from '@src/stores/NotificationStore'
  18. import EndpointLogos from '@src/components/modules/EndpointModule/EndpointLogos'
  19. import Button from '@src/components/ui/Button'
  20. import StatusImage from '@src/components/ui/StatusComponents/StatusImage'
  21. import { ThemePalette, ThemeProps } from '@src/components/Theme'
  22. import FileUtils from '@src/utils/FileUtils'
  23. import configLoader from '@src/utils/Config'
  24. import type { FileContent } from '@src/utils/FileUtils'
  25. import type { Endpoint, MultiValidationItem } from '@src/@types/Endpoint'
  26. import { ProviderTypes } from '@src/@types/Providers'
  27. import { Region } from '@src/@types/Region'
  28. import MultipleUploadedEndpoints from './MultipleUploadedEndpoints'
  29. const Wrapper = styled.div<any>`
  30. display: flex;
  31. min-height: 0;
  32. padding: 22px 0 32px 0;
  33. text-align: center;
  34. `
  35. const Providers = styled.div`
  36. min-height: 0;
  37. display: flex;
  38. flex-direction: column;
  39. align-items: center;
  40. `
  41. const Logos = styled.div<any>`
  42. display: flex;
  43. flex-wrap: wrap;
  44. overflow: auto;
  45. min-height: 0;
  46. flex-grow: 1;
  47. `
  48. const Upload = styled.div<any>`
  49. border: 1px dashed ${props => (props.highlight ? ThemePalette.primary : 'white')};
  50. margin: 0 32px 16px 32px;
  51. padding: 16px;
  52. `
  53. const UploadMessage = styled.div<any>`
  54. color: ${ThemePalette.grayscale[3]};
  55. `
  56. const UploadMessageButton = styled.span`
  57. color: ${ThemePalette.primary};
  58. cursor: pointer;
  59. `
  60. const FakeFileInput = styled.input`
  61. position: absolute;
  62. opacity: 0;
  63. top: -99999px;
  64. `
  65. const EndpointLogosStyled = styled(EndpointLogos)`
  66. transform: scale(0.67);
  67. transition: all ${ThemeProps.animations.swift};
  68. cursor: pointer;
  69. &:hover {
  70. transform: scale(0.7);
  71. }
  72. `
  73. const LoadingWrapper = styled.div<any>`
  74. display: flex;
  75. flex-direction: column;
  76. align-items: center;
  77. margin: 32px 0;
  78. flex-grow: 1;
  79. `
  80. const LoadingText = styled.div<any>`
  81. font-size: 18px;
  82. margin-top: 32px;
  83. `
  84. type Props = {
  85. providers: ProviderTypes[],
  86. regions: Region[]
  87. onCancelClick: () => void,
  88. onProviderClick: (provider: ProviderTypes) => void,
  89. onUploadEndpoint: (endpoint: Endpoint) => void,
  90. loading: boolean,
  91. onValidateMultipleEndpoints: (endpoints: Endpoint[]) => void,
  92. onResizeUpdate?: () => void,
  93. multiValidating: boolean,
  94. multiValidation: MultiValidationItem[],
  95. onRemoveEndpoint: (endpoint: Endpoint) => void,
  96. onResetValidation: () => void,
  97. }
  98. type State = {
  99. highlightDropzone: boolean,
  100. multipleUploadedEndpoints: (Endpoint | string)[],
  101. invalidRegionsEndpointIds: { id: string, regions: string[] }[],
  102. }
  103. @observer
  104. class ChooseProvider extends React.Component<Props, State> {
  105. state: State = {
  106. highlightDropzone: false,
  107. multipleUploadedEndpoints: [],
  108. invalidRegionsEndpointIds: [],
  109. }
  110. fileInput: HTMLElement | null | undefined
  111. dragDropListeners: { type: string, listener: (e: any) => any }[] = []
  112. UNSAFE_componentWillMount() {
  113. setTimeout(() => { this.addDragAndDrop() }, 1000)
  114. }
  115. componentDidUpdate(_: Props, prevState: State) {
  116. if (prevState.multipleUploadedEndpoints.length !== this.state.multipleUploadedEndpoints.length
  117. && this.props.onResizeUpdate) {
  118. this.props.onResizeUpdate()
  119. }
  120. }
  121. componentWillUnmount() {
  122. this.removeDragDrop()
  123. }
  124. addDragAndDrop() {
  125. this.dragDropListeners = [{
  126. type: 'dragenter',
  127. listener: e => {
  128. this.setState({ highlightDropzone: true })
  129. e.dataTransfer.dropEffect = 'copy'
  130. e.preventDefault()
  131. },
  132. }, {
  133. type: 'dragover',
  134. listener: e => {
  135. e.dataTransfer.dropEffect = 'copy'
  136. e.preventDefault()
  137. },
  138. }, {
  139. type: 'dragleave',
  140. listener: e => {
  141. if (!e.clientX && !e.clientY) {
  142. this.setState({ highlightDropzone: false })
  143. }
  144. },
  145. }, {
  146. type: 'drop',
  147. listener: async e => {
  148. e.preventDefault()
  149. this.setState({ highlightDropzone: false })
  150. const filesContents = await FileUtils.readContentFromFileList(e.dataTransfer.files)
  151. if (filesContents.length === 1) {
  152. this.processOneFileContent(filesContents[0].content)
  153. } else {
  154. this.processMultipleFilesContents(filesContents)
  155. }
  156. },
  157. }]
  158. this.dragDropListeners.forEach(l => {
  159. window.addEventListener(l.type, l.listener)
  160. })
  161. }
  162. removeDragDrop() {
  163. this.dragDropListeners.forEach(l => {
  164. window.removeEventListener(l.type, l.listener)
  165. })
  166. this.dragDropListeners = []
  167. }
  168. parseEndpoint(content: string, skipAlert?: boolean): { endpoint: Endpoint, unidentRegions: string[] } {
  169. const endpoint: Endpoint = JSON.parse(content)
  170. if (!endpoint.name || !endpoint.type || !this.props.providers.find(p => p === endpoint.type)) {
  171. throw new Error()
  172. }
  173. delete (endpoint as any).id
  174. const unidentRegions: string[] = []
  175. if (endpoint.mapped_regions?.length) {
  176. endpoint.mapped_regions = endpoint.mapped_regions.map(nameId => {
  177. const region = this.props.regions.find(r => r.id === nameId || r.name === nameId)
  178. if (region) {
  179. return region.id
  180. }
  181. unidentRegions.push(nameId)
  182. return null
  183. }).filter((item: string | null): item is string => Boolean(item))
  184. if (unidentRegions.length && !skipAlert) {
  185. notificationStore.alert(`${unidentRegions.length} Coriolis Region${unidentRegions.length > 1 ? 's' : ''} couldn't be mapped`, 'warning')
  186. }
  187. }
  188. return { endpoint, unidentRegions }
  189. }
  190. processOneFileContent(content: string) {
  191. this.props.onResetValidation()
  192. try {
  193. const { endpoint } = this.parseEndpoint(content)
  194. this.chooseEndpoint(endpoint)
  195. } catch (err) {
  196. notificationStore.alert('Invalid .endpoint file', 'error')
  197. }
  198. }
  199. processMultipleFilesContents(filesContents: FileContent[]) {
  200. this.props.onResetValidation()
  201. const uniqueNames: { [prop: string]: number } = {}
  202. const invalidRegionsEndpointIds: { id: string, regions: string[] }[] = []
  203. const endpoints = filesContents.map(fileContent => {
  204. try {
  205. const { endpoint, unidentRegions } = this.parseEndpoint(fileContent.content, true)
  206. const key = `${endpoint.type}${endpoint.name}`
  207. if (uniqueNames[key] === undefined) {
  208. uniqueNames[key] = 0
  209. } else {
  210. uniqueNames[key] += 1
  211. endpoint.name = `${endpoint.name} (${uniqueNames[key]})`
  212. }
  213. if (unidentRegions.length) {
  214. invalidRegionsEndpointIds.push({ id: `${endpoint.type}${endpoint.name}`, regions: unidentRegions })
  215. }
  216. return endpoint
  217. } catch (err) {
  218. return fileContent.name
  219. }
  220. })
  221. const sortPriority = configLoader.config.providerSortPriority
  222. endpoints.sort((a, b) => {
  223. if (typeof a === 'string' && typeof b === 'string') {
  224. return a.localeCompare(b)
  225. }
  226. if (typeof a === 'string') {
  227. return 1
  228. }
  229. if (typeof b === 'string') {
  230. return -1
  231. }
  232. if (sortPriority[a.type] && sortPriority[b.type]) {
  233. return (sortPriority[a.type] - sortPriority[b.type]) || a.type.localeCompare(b.type)
  234. }
  235. if (sortPriority[a.type]) {
  236. return -1
  237. }
  238. if (sortPriority[b.type]) {
  239. return 1
  240. }
  241. return a.type.localeCompare(b.type)
  242. })
  243. this.setState({
  244. multipleUploadedEndpoints: endpoints,
  245. invalidRegionsEndpointIds,
  246. })
  247. }
  248. chooseEndpoint(endpoint: Endpoint) {
  249. this.props.onUploadEndpoint(endpoint)
  250. }
  251. async handleFileUpload(files: FileList | null) {
  252. const filesContents = await FileUtils.readContentFromFileList(files)
  253. if (filesContents.length === 1) {
  254. this.processOneFileContent(filesContents[0].content)
  255. } else {
  256. this.processMultipleFilesContents(filesContents)
  257. }
  258. }
  259. handleRemoveUploadedEndpoint(endpoint: Endpoint | string, isAdded: boolean) {
  260. this.setState(prevState => {
  261. const multipleUploadedEndpoints = prevState.multipleUploadedEndpoints.filter(e => {
  262. if (typeof e === 'string' && typeof endpoint === 'string') {
  263. return e !== endpoint
  264. }
  265. if (typeof e !== 'string' && typeof endpoint !== 'string') {
  266. return e.name !== endpoint.name || e.type !== endpoint.type
  267. }
  268. return true
  269. })
  270. if (isAdded && typeof endpoint !== 'string') {
  271. this.props.onRemoveEndpoint(endpoint)
  272. }
  273. return { multipleUploadedEndpoints }
  274. })
  275. }
  276. handleRegionsChange(endpoint: Endpoint, newRegions: string[]) {
  277. this.setState(prevState => ({
  278. multipleUploadedEndpoints: prevState.multipleUploadedEndpoints.map(stateEndpoint => {
  279. if (typeof stateEndpoint !== 'string' && `${stateEndpoint.type}${stateEndpoint.name}` === `${endpoint.type}${endpoint.name}`) {
  280. return {
  281. ...stateEndpoint,
  282. mapped_regions: newRegions,
  283. }
  284. }
  285. return stateEndpoint
  286. }),
  287. }))
  288. }
  289. renderMultipleUploadedEndpoints() {
  290. return (
  291. <MultipleUploadedEndpoints
  292. endpoints={this.state.multipleUploadedEndpoints}
  293. onBackClick={() => { this.setState({ multipleUploadedEndpoints: [] }) }}
  294. onRemove={(e, isAdded) => { this.handleRemoveUploadedEndpoint(e, isAdded) }}
  295. validating={this.props.multiValidating}
  296. multiValidation={this.props.multiValidation}
  297. invalidRegionsEndpointIds={this.state.invalidRegionsEndpointIds}
  298. regions={this.props.regions}
  299. onRegionsChange={(endpoint, newRegions) => { this.handleRegionsChange(endpoint, newRegions) }}
  300. onValidateClick={() => {
  301. this.props.onValidateMultipleEndpoints(this.state.multipleUploadedEndpoints.filter(e => typeof e !== 'string') as Endpoint[])
  302. }}
  303. onDone={this.props.onCancelClick}
  304. />
  305. )
  306. }
  307. renderLoading() {
  308. if (!this.props.loading) {
  309. return null
  310. }
  311. return (
  312. <LoadingWrapper>
  313. <StatusImage loading />
  314. <LoadingText>Loading providers ...</LoadingText>
  315. </LoadingWrapper>
  316. )
  317. }
  318. renderProviders() {
  319. if (this.props.loading) {
  320. return null
  321. }
  322. const UploadButton = (
  323. <UploadMessageButton
  324. onClick={() => { if (this.fileInput) this.fileInput.click() }}
  325. >upload
  326. </UploadMessageButton>
  327. )
  328. return (
  329. <Providers>
  330. <Logos>
  331. {this.props.providers.map(k => (
  332. <EndpointLogosStyled
  333. height={128}
  334. key={k}
  335. endpoint={k}
  336. data-test-id={`cProvider-endpointLogo-${k}`}
  337. onClick={() => { this.props.onProviderClick(k) }}
  338. />
  339. ))}
  340. </Logos>
  341. <Upload highlight={this.state.highlightDropzone}>
  342. <UploadMessage>
  343. You can
  344. &nbsp;{UploadButton}&nbsp;
  345. or drop multiple .endpoint and zipped .endpoint files.
  346. </UploadMessage>
  347. </Upload>
  348. <FakeFileInput
  349. type="file"
  350. ref={r => { this.fileInput = r }}
  351. accept=".endpoint,.zip"
  352. multiple
  353. onChange={e => { this.handleFileUpload(e.target.files) }}
  354. />
  355. <Button secondary onClick={this.props.onCancelClick} data-test-id="cProvider-cancelButton">Cancel</Button>
  356. </Providers>
  357. )
  358. }
  359. render() {
  360. return (
  361. <Wrapper>
  362. {this.state.multipleUploadedEndpoints.length === 0 ? this.renderProviders() : null}
  363. {this.renderLoading()}
  364. {this.state.multipleUploadedEndpoints.length > 0
  365. ? this.renderMultipleUploadedEndpoints() : null}
  366. </Wrapper>
  367. )
  368. }
  369. }
  370. export default ChooseProvider