EndpointsPage.jsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  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. // @flow
  15. import React from 'react'
  16. import styled from 'styled-components'
  17. import { observer } from 'mobx-react'
  18. import MainTemplate from '../../templates/MainTemplate'
  19. import Navigation from '../../organisms/Navigation'
  20. import FilterList from '../../organisms/FilterList'
  21. import PageHeader from '../../organisms/PageHeader'
  22. import EndpointListItem from '../../molecules/EndpointListItem'
  23. import AlertModal from '../../organisms/AlertModal'
  24. import Modal from '../../molecules/Modal'
  25. import ChooseProvider from '../../organisms/ChooseProvider'
  26. import Endpoint from '../../organisms/Endpoint'
  27. import type { Endpoint as EndpointType } from '../../../types/Endpoint'
  28. import endpointImage from './images/endpoint-large.svg'
  29. import projectStore from '../../../stores/ProjectStore'
  30. import userStore from '../../../stores/UserStore'
  31. import EndpointSource from '../../../sources/EndpointSource'
  32. import endpointStore from '../../../stores/EndpointStore'
  33. import migrationStore from '../../../stores/MigrationStore'
  34. import replicaStore from '../../../stores/ReplicaStore'
  35. import notificationStore from '../../../stores/NotificationStore'
  36. import providerStore from '../../../stores/ProviderStore'
  37. import LabelDictionary from '../../../utils/LabelDictionary'
  38. import { requestPollTimeout } from '../../../config.js'
  39. import EndpointDuplicateOptions from '../../organisms/EndpointDuplicateOptions'
  40. const Wrapper = styled.div``
  41. const BulkActions = [
  42. { label: 'Delete', value: 'delete' },
  43. { label: 'Duplicate', value: 'duplicate' },
  44. ]
  45. type State = {
  46. showDeleteEndpointsConfirmation: boolean,
  47. confirmationItems: ?EndpointType[],
  48. showChooseProviderModal: boolean,
  49. showEndpointModal: boolean,
  50. providerType: ?string,
  51. showEndpointsInUseModal: boolean,
  52. modalIsOpen: boolean,
  53. showDuplicateModal: boolean,
  54. duplicating: boolean,
  55. }
  56. @observer
  57. class EndpointsPage extends React.Component<{}, State> {
  58. state = {
  59. showDeleteEndpointsConfirmation: false,
  60. confirmationItems: null,
  61. showChooseProviderModal: false,
  62. showEndpointModal: false,
  63. providerType: null,
  64. showEndpointsInUseModal: false,
  65. modalIsOpen: false,
  66. showDuplicateModal: false,
  67. duplicating: false,
  68. }
  69. pollTimeout: TimeoutID
  70. stopPolling: boolean
  71. componentDidMount() {
  72. document.title = 'Coriolis Endpoints'
  73. projectStore.getProjects()
  74. this.stopPolling = false
  75. this.pollData(true)
  76. }
  77. componentWillUnmount() {
  78. clearTimeout(this.pollTimeout)
  79. this.stopPolling = true
  80. }
  81. getFilterItems() {
  82. let types = [{ label: 'All', value: 'all' }]
  83. endpointStore.endpoints.forEach(endpoint => {
  84. if (!types.find(t => t.value === endpoint.type)) {
  85. types.push({ label: LabelDictionary.get(endpoint.type), value: endpoint.type })
  86. }
  87. })
  88. return types
  89. }
  90. getEndpointUsage(endpoint: EndpointType) {
  91. let replicasCount = replicaStore.replicas.filter(
  92. r => r.origin_endpoint_id === endpoint.id || r.destination_endpoint_id === endpoint.id).length
  93. let migrationsCount = migrationStore.migrations.filter(
  94. r => r.origin_endpoint_id === endpoint.id || r.destination_endpoint_id === endpoint.id).length
  95. return { migrationsCount, replicasCount }
  96. }
  97. handleProjectChange() {
  98. endpointStore.getEndpoints({ showLoading: true })
  99. migrationStore.getMigrations()
  100. replicaStore.getReplicas()
  101. }
  102. handleReloadButtonClick() {
  103. projectStore.getProjects()
  104. endpointStore.getEndpoints({ showLoading: true })
  105. migrationStore.getMigrations()
  106. replicaStore.getReplicas()
  107. }
  108. handleItemClick(item: EndpointType) {
  109. window.location.href = `/#/endpoint/${item.id}`
  110. }
  111. handleActionChange(items: EndpointType[], action: string) {
  112. switch (action) {
  113. case 'delete': {
  114. let endpointsInUse = items.filter(endpoint => {
  115. const endpointUsage = this.getEndpointUsage(endpoint)
  116. return endpointUsage.migrationsCount > 0 || endpointUsage.replicasCount > 0
  117. })
  118. if (endpointsInUse.length > 0) {
  119. this.setState({ showEndpointsInUseModal: true })
  120. } else {
  121. this.setState({
  122. showDeleteEndpointsConfirmation: true,
  123. confirmationItems: items,
  124. })
  125. }
  126. break
  127. }
  128. case 'duplicate': {
  129. this.setState({
  130. confirmationItems: items,
  131. showDuplicateModal: true,
  132. modalIsOpen: true,
  133. })
  134. break
  135. }
  136. default: break
  137. }
  138. }
  139. handleDuplicate(projectId: string) {
  140. this.setState({ modalIsOpen: false, duplicating: true })
  141. let selectedProjectId = userStore.loggedUser ? userStore.loggedUser.project.id : ''
  142. let switchProject = projectId !== selectedProjectId
  143. let endpoints = []
  144. let items = this.state.confirmationItems || []
  145. Promise.all(items.map(endpoint => {
  146. return EndpointSource.getConnectionInfo(endpoint).then(connectionInfo => {
  147. endpoints.push({
  148. ...endpoint,
  149. connection_info: connectionInfo,
  150. name: `${endpoint.name}${!switchProject ? ' (copy)' : ''}`,
  151. })
  152. })
  153. })).then(() => {
  154. if (switchProject) {
  155. return userStore.switchProject(projectId).then(() => {
  156. this.handleProjectChange()
  157. })
  158. }
  159. return Promise.resolve()
  160. }).then(() => {
  161. return Promise.all(endpoints.map(endpoint => {
  162. return EndpointSource.add(endpoint, true)
  163. }).map((p: Promise<any>) => p.catch(e => e)))
  164. .then((results: (Endpoint | { status: string, data?: { description: string } })[]) => {
  165. let internalServerErrors = results.filter(r => r.status && r.status === 500)
  166. if (internalServerErrors.length > 0) {
  167. notificationStore.alert(`There was a problem duplicating ${internalServerErrors.length} endpoint${internalServerErrors.length > 1 ? 's' : ''}`, 'error')
  168. }
  169. let forbiddenErrors = results.filter(r => r.status && r.status === 403)
  170. if (forbiddenErrors.length > 0 && forbiddenErrors[0].data && forbiddenErrors[0].data.description) {
  171. notificationStore.alert(String(forbiddenErrors[0].data.description), 'error')
  172. }
  173. })
  174. }).catch(e => {
  175. if (e.data && e.data.description) {
  176. notificationStore.alert(e.data.description, 'error')
  177. }
  178. }).then(() => {
  179. this.pollData(true)
  180. this.setState({ showDuplicateModal: false, duplicating: false })
  181. })
  182. }
  183. handleCloseDeleteEndpointsConfirmation() {
  184. this.setState({
  185. showDeleteEndpointsConfirmation: false,
  186. confirmationItems: null,
  187. })
  188. }
  189. handleDeleteEndpointsConfirmation() {
  190. if (this.state.confirmationItems) {
  191. this.state.confirmationItems.forEach(endpoint => {
  192. endpointStore.delete(endpoint)
  193. })
  194. }
  195. this.handleCloseDeleteEndpointsConfirmation()
  196. }
  197. handleEmptyListButtonClick() {
  198. providerStore.loadProviders()
  199. this.setState({ showChooseProviderModal: true })
  200. }
  201. handleCloseChooseProviderModal() {
  202. this.setState({ showChooseProviderModal: false })
  203. }
  204. handleProviderClick(providerType: string) {
  205. this.setState({
  206. showChooseProviderModal: false,
  207. showEndpointModal: true,
  208. providerType,
  209. })
  210. }
  211. handleCloseEndpointModal() {
  212. this.setState({ showEndpointModal: false })
  213. }
  214. handleModalOpen() {
  215. this.setState({ modalIsOpen: true })
  216. }
  217. handleModalClose() {
  218. this.setState({ modalIsOpen: false }, () => {
  219. this.pollData()
  220. })
  221. }
  222. pollData(showLoading?: boolean = false) {
  223. if (this.state.modalIsOpen || this.stopPolling) {
  224. return
  225. }
  226. Promise.all([endpointStore.getEndpoints({ showLoading }), migrationStore.getMigrations(), replicaStore.getReplicas()]).then(() => {
  227. this.pollTimeout = setTimeout(() => { this.pollData() }, requestPollTimeout)
  228. })
  229. }
  230. itemFilterFunction(item: any, filterItem?: ?string, filterText?: string) {
  231. let endpoint: EndpointType = item
  232. if ((filterItem !== 'all' && (endpoint.type !== filterItem)) ||
  233. (endpoint.name.toLowerCase().indexOf(filterText || '') === -1 &&
  234. // $FlowIssue
  235. (!endpoint.description || endpoint.description.toLowerCase().indexOf(filterText) === -1))
  236. ) {
  237. return false
  238. }
  239. return true
  240. }
  241. render() {
  242. let items: any = endpointStore.endpoints
  243. let selectedProjectId = userStore.loggedUser ? userStore.loggedUser.project.id : ''
  244. return (
  245. <Wrapper>
  246. <MainTemplate
  247. navigationComponent={<Navigation currentPage="endpoints" />}
  248. listComponent={
  249. <FilterList
  250. filterItems={this.getFilterItems()}
  251. selectionLabel="endpoint"
  252. loading={endpointStore.loading}
  253. items={items}
  254. onItemClick={item => {
  255. let anyItem: any = item
  256. let endpoint: EndpointType = anyItem
  257. this.handleItemClick(endpoint)
  258. }}
  259. onReloadButtonClick={() => { this.handleReloadButtonClick() }}
  260. actions={BulkActions}
  261. onActionChange={(items, action) => {
  262. let anyItems: any = items
  263. let endpoints: EndpointType[] = anyItems
  264. this.handleActionChange(endpoints, action)
  265. }}
  266. itemFilterFunction={(...args) => this.itemFilterFunction(...args)}
  267. renderItemComponent={options =>
  268. (<EndpointListItem
  269. {...options}
  270. getUsage={endpoint => this.getEndpointUsage(endpoint)}
  271. />)
  272. }
  273. emptyListImage={endpointImage}
  274. emptyListMessage="You don’t have any Cloud Endpoints in this project."
  275. emptyListExtraMessage="A Cloud Endpoint is used for the source or target of a Replica/Migration."
  276. emptyListButtonLabel="Add Endpoint"
  277. onEmptyListButtonClick={() => { this.handleEmptyListButtonClick() }}
  278. />
  279. }
  280. headerComponent={
  281. <PageHeader
  282. title="Coriolis Endpoints"
  283. onProjectChange={() => { this.handleProjectChange() }}
  284. onModalOpen={() => { this.handleModalOpen() }}
  285. onModalClose={() => { this.handleModalClose() }}
  286. />
  287. }
  288. />
  289. <AlertModal
  290. isOpen={this.state.showDeleteEndpointsConfirmation}
  291. title="Delete Endpoints?"
  292. message="Are you sure you want to delete the selected endpoints?"
  293. extraMessage="Deleting a Coriolis Cloud Endpoint is permanent!"
  294. onConfirmation={() => { this.handleDeleteEndpointsConfirmation() }}
  295. onRequestClose={() => { this.handleCloseDeleteEndpointsConfirmation() }}
  296. />
  297. <Modal
  298. isOpen={this.state.showChooseProviderModal}
  299. title="New Cloud Endpoint"
  300. onRequestClose={() => { this.handleCloseChooseProviderModal() }}
  301. >
  302. <ChooseProvider
  303. onCancelClick={() => { this.handleCloseChooseProviderModal() }}
  304. providers={providerStore.providers}
  305. loading={providerStore.providersLoading}
  306. onProviderClick={providerName => { this.handleProviderClick(providerName) }}
  307. />
  308. </Modal>
  309. <Modal
  310. isOpen={this.state.showEndpointModal}
  311. title="New Cloud Endpoint"
  312. onRequestClose={() => { this.handleCloseEndpointModal() }}
  313. >
  314. <Endpoint
  315. type={this.state.providerType}
  316. onCancelClick={() => { this.handleCloseEndpointModal() }}
  317. />
  318. </Modal>
  319. <AlertModal
  320. type="error"
  321. isOpen={this.state.showEndpointsInUseModal}
  322. title="Endpoints are in use"
  323. message="Some of the selected endpoints can't be deleted because they are in use by replicas or migrations."
  324. extraMessage="You must first delete the replicas or migrations which use these endpoints."
  325. onRequestClose={() => { this.setState({ showEndpointsInUseModal: false }) }}
  326. />
  327. {this.state.showDuplicateModal ? (
  328. <Modal
  329. isOpen
  330. title="Duplicate Endpoint"
  331. onRequestClose={() => { this.setState({ showDuplicateModal: false }) }}
  332. >
  333. <EndpointDuplicateOptions
  334. duplicating={this.state.duplicating}
  335. projects={projectStore.projects}
  336. selectedProjectId={selectedProjectId}
  337. onCancelClick={() => { this.setState({ showDuplicateModal: false }) }}
  338. onDuplicateClick={projectId => { this.handleDuplicate(projectId) }}
  339. />
  340. </Modal>
  341. ) : null}
  342. </Wrapper>
  343. )
  344. }
  345. }
  346. export default EndpointsPage