MigrationList.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  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 Reflux from 'reflux';
  16. import withStyles from 'isomorphic-style-loader/lib/withStyles';
  17. import Location from '../../core/Location';
  18. import Dropdown from '../NewDropdown';
  19. import UserIcon from '../UserIcon';
  20. import NotificationIcon from '../NotificationIcon';
  21. import SearchBox from '../SearchBox';
  22. import Moment from 'react-moment';
  23. import s from './MigrationList.scss';
  24. import MigrationStore from '../../stores/MigrationStore';
  25. import ProjectStore from '../../stores/MigrationStore';
  26. import MigrationActions from '../../actions/MigrationActions';
  27. import FilteredTable from '../FilteredTable';
  28. import TextTruncate from 'react-text-truncate';
  29. import LoadingIcon from "../LoadingIcon/LoadingIcon";
  30. import ConfirmationDialog from '../ConfirmationDialog'
  31. import ProjectsDropdown from '../ProjectsDropdown';
  32. const title = 'Migrations';
  33. const migrationTypes = [
  34. { label: "Replicas", type: "replica" },
  35. { label: "Migrations", type: "migration" },
  36. { label: "All", type: "all" }
  37. ]
  38. const statusTypes = [
  39. { label: "Running", type: "RUNNING" },
  40. { label: "Error", type: "ERROR" },
  41. { label: "Completed", type: "COMPLETED" },
  42. { label: "All", type: "all" }
  43. ]
  44. const migrationActions = [
  45. { label: "Execute", value: "execute" },
  46. { label: "Cancel", value: "cancel" },
  47. { label: "Delete", value: "delete" }
  48. ]
  49. class MigrationList extends Reflux.Component {
  50. constructor(props) {
  51. super(props)
  52. this.store = MigrationStore;
  53. this.state = {
  54. title: props.type == "migrations" ? "Migrations" : "Replicas",
  55. queryText: '',
  56. filterType: props.type == "migrations" ? "migration" : "replica",
  57. filterStatus: "all",
  58. currentProject: "My Project",
  59. searchMin: true,
  60. filteredData: [],
  61. confirmationDialog: {
  62. visible: false,
  63. message: "Are you sure?",
  64. onConfirm: null,
  65. onCancel: null
  66. }
  67. }
  68. }
  69. static contextTypes = {
  70. onSetTitle: PropTypes.func.isRequired
  71. };
  72. componentWillReceiveProps(newProps, oldProps) {
  73. this.setState({
  74. title: newProps.type == "migrations" ? "Migrations" : "Replicas",
  75. filterType: newProps.type == "migrations" ? "migration" : "replica"
  76. })
  77. }
  78. componentWillMount() {
  79. super.componentWillMount.call(this)
  80. this.context.onSetTitle(this.state.title);
  81. MigrationActions.loadMigrations()
  82. this.projects = [
  83. { label: "My Project", value: "Project1" },
  84. { label: "Project 2", value: "Project2" }
  85. ]
  86. }
  87. componentDidMount() {
  88. this.setState({ filteredData: this.state.migrations }) // eslint-disable-line react/no-did-mount-set-state
  89. }
  90. newMigration() {
  91. Location.push('/migrations/new')
  92. }
  93. migrationsSelected() {
  94. let count = 0
  95. let total = 0
  96. if (this.state.migrations) {
  97. count = this.migrationsSelectedCount()
  98. if (this.state.filterType == "all") {
  99. total = this.state.migrations.length
  100. } else {
  101. this.state.migrations.forEach(item => {
  102. if (item.type == this.state.filterType) {
  103. total++
  104. }
  105. })
  106. }
  107. }
  108. let term = "migration"
  109. if (this.state.filterType == "replica") term = "replica"
  110. return `${count} of ${total} ${term}(s) selected`;
  111. }
  112. migrationsSelectedCount() {
  113. let count = 0
  114. if (this.state.migrations) {
  115. this.state.migrations.forEach((item) => {
  116. if (item.selected) {
  117. if (this.state.filterType == "all") {
  118. count++
  119. } else {
  120. if (item.type == this.state.filterType && item.selected) {
  121. count++
  122. }
  123. }
  124. }
  125. })
  126. }
  127. return count
  128. }
  129. migrationDetail(e, item) {
  130. if (item.type == "migration") {
  131. Location.push('/migration/' + item.id + "/")
  132. } else {
  133. Location.push('/replica/' + item.id + "/")
  134. }
  135. }
  136. checkItem(e, itemRef) {
  137. let items = this.state.migrations
  138. items.forEach((item) => {
  139. if (item == itemRef) {
  140. item.selected = !item.selected
  141. }
  142. })
  143. this.setState({ migrations: items })
  144. }
  145. searchItem(queryText) {
  146. if (queryText.target) {
  147. this.setState({queryText: queryText.target.value })
  148. } else {
  149. this.setState({queryText: queryText })
  150. }
  151. }
  152. filterType(e, type) {
  153. this.setState({ filterType: type }, () => {
  154. this.searchItem({ target: { value: this.state.queryText } })
  155. })
  156. }
  157. filterStatus(e, status) {
  158. this.setState({ filterStatus: status }, () => {
  159. this.searchItem({ target: { value: this.state.queryText } })
  160. })
  161. }
  162. filterFn(item, queryText, filterType, filterStatus) {
  163. return (
  164. item.name.toLowerCase().indexOf(queryText.toLowerCase()) != -1 &&
  165. (filterType == "all" || filterType == item.type) &&
  166. (filterStatus == "all" || filterStatus == item.status)
  167. )
  168. }
  169. renderSearch(items) {
  170. if (items) {
  171. let output = items.map((item, index) => {
  172. let count = 0
  173. if (item.type == 'replica' && item.executions.length) {
  174. item.tasks = item.executions[item.executions.length - 1].tasks
  175. }
  176. if (!item.tasks) {
  177. item.tasks = []
  178. }
  179. item.tasks.forEach((task) => {
  180. if (task.status != "COMPLETED") count++
  181. })
  182. let tasksRemaining = count + " out of " + item.tasks.length
  183. return (
  184. <div className={"item " + (item.selected ? "selected" : "")} key={"vm_" + index}>
  185. <div className="checkbox-container">
  186. <input
  187. id={"vm_check_" + index}
  188. type="checkbox"
  189. checked={item.selected}
  190. onChange={(e) => this.checkItem(e, item)}
  191. className="checkbox-normal"
  192. />
  193. <label htmlFor={"vm_check_" + index}></label>
  194. </div>
  195. <span className="cell cell-icon" onClick={(e) => this.migrationDetail(e, item)}>
  196. <div className={"icon " + item.type}></div>
  197. <span className="details">
  198. {/*{item.name ? item.name : "N/A"}*/}
  199. <TextTruncate line={1} truncateText="..." text={item.name} />
  200. <span className={s.migrationStatus + " status-pill " + item.status}>{item.status}</span>
  201. </span>
  202. </span>
  203. <span className="cell" onClick={(e) => this.migrationDetail(e, item)}>
  204. <div className={s.cloudImage + " icon small-cloud " + item.origin_endpoint_type}></div>
  205. <span className={s.chevronRight}></span>
  206. <div className={s.cloudImage + " icon small-cloud " + item.destination_endpoint_type}></div>
  207. </span>
  208. <span className={"cell " + s.composite} onClick={(e) => this.migrationDetail(e, item)}>
  209. <span className={s.label}>Created</span>
  210. <span className={s.value}>
  211. <Moment format="MMM Do YYYY HH:ss" date={item.created_at} />
  212. </span>
  213. </span>
  214. {/*<span className={"cell " + s.composite} onClick={(e) => this.migrationDetail(e, item)}>
  215. <span className={s.label}>Notes</span>
  216. <TextTruncate line={2} truncateText="..." text={item.notes} />
  217. </span>*/}
  218. <span className={"cell " + s.composite} onClick={(e) => this.migrationDetail(e, item)}>
  219. <span className={s.label}>Tasks remaining</span>
  220. <span className={s.value}>{tasksRemaining}</span>
  221. </span>
  222. {/*<span className={"cell " + s.composite}>
  223. <span className={s.label}>Current instance</span>
  224. <span className={s.value}>{this.currentInstance(item)}</span>
  225. </span>*/}
  226. </div>
  227. )
  228. })
  229. return output
  230. } else {
  231. return (<div className="no-results">Your search returned no results</div>)
  232. }
  233. }
  234. onProjectChange(project) {
  235. // TODO: Move setstate from here
  236. //this.setState({ currentProject: project.value })
  237. }
  238. onMigrationActionChange(option) {
  239. switch (option.value) {
  240. case "delete":
  241. let deletedItems = [] // we put here the items for deletion
  242. this.state.migrations.forEach((item) => {
  243. if (item.selected) {
  244. if (this.state.filterType == "all") {
  245. deletedItems.push(item)
  246. } else {
  247. if (item.type == this.state.filterType) {
  248. deletedItems.push(item)
  249. }
  250. }
  251. }
  252. })
  253. this.setState({
  254. confirmationDialog: {
  255. visible: true,
  256. onConfirm: () => {
  257. this.setState({ confirmationDialog: { visible: false }})
  258. deletedItems.forEach(item => {
  259. MigrationActions.deleteMigration(item)
  260. })
  261. },
  262. onCancel: () => {
  263. this.setState({ confirmationDialog: { visible: false }})
  264. }
  265. }
  266. })
  267. break
  268. case "execute":
  269. this.state.migrations.forEach((item) => {
  270. if (item.selected) {
  271. if (this.state.filterType == "all") {
  272. MigrationActions.executeReplica(item)
  273. } else {
  274. if (item.type == this.state.filterType) {
  275. MigrationActions.executeReplica(item)
  276. }
  277. }
  278. }
  279. })
  280. break
  281. case "cancel":
  282. this.state.migrations.forEach((item) => {
  283. if (item.selected) {
  284. if (this.state.filterType == "all") {
  285. MigrationActions.cancelMigration(item)
  286. } else {
  287. if (item.type == this.state.filterType) {
  288. MigrationActions.cancelMigration(item)
  289. }
  290. }
  291. }
  292. })
  293. break
  294. default:
  295. break
  296. }
  297. }
  298. currentInstance(migration) {
  299. let instance = "N/A"
  300. /*migration.vms.forEach((item) => {
  301. if (item.selected) {
  302. instance = item.name
  303. }
  304. })*/
  305. return instance
  306. }
  307. refreshList() {
  308. MigrationActions.loadMigrations()
  309. }
  310. render() {
  311. let _this = this
  312. let itemStates = statusTypes.map((state, index) => (
  313. <a
  314. className={_this.state.filterStatus == state.type || (_this.state.filterStatus == null && state.type == "all") ?
  315. "selected" : ""}
  316. onClick={(e) => _this.filterStatus(e, state.type)} key={"status_" + index}
  317. >{state.label}</a>
  318. )
  319. )
  320. return (
  321. <div className={s.root}>
  322. <div className={s.container}>
  323. <div className={s.pageHeader}>
  324. <div className={s.top}>
  325. <h1>Coriolis {this.state.title}</h1>
  326. <div className={s.topActions}>
  327. {/*<Dropdown
  328. options={this.projects}
  329. onChange={(e) => this.onProjectChange(e)}
  330. placeholder="Select"
  331. value={this.state.currentProject}
  332. />*/}
  333. <ProjectsDropdown />
  334. <button onClick={this.newMigration}>New</button>
  335. <UserIcon />
  336. <NotificationIcon />
  337. </div>
  338. </div>
  339. <div className="filters">
  340. <div className="category-filter">
  341. {itemStates}
  342. </div>
  343. <div className={s.refreshBtn}>
  344. <div className="icon refresh" onClick={this.refreshList}></div>
  345. </div>
  346. <div className="name-filter">
  347. <SearchBox
  348. placeholder="Search"
  349. value={this.state.queryText}
  350. onChange={(e) => this.searchItem(e)}
  351. minimize={true} // eslint-disable-line react/jsx-boolean-value
  352. onClick={(e) => this.toggleSearch(e)}
  353. className={"searchBox " + (this.state.searchMin ? "minimize" : "")}
  354. />
  355. </div>
  356. <div className={s.bulkActions + (this.migrationsSelectedCount() === 0 ? " invisible": "")}>
  357. <div className={s.migrationsCount}>
  358. {this.migrationsSelected()}
  359. </div>
  360. <Dropdown
  361. options={migrationActions}
  362. onChange={(e) => this.onMigrationActionChange(e)}
  363. placeholder="More Actions"
  364. />
  365. </div>
  366. </div>
  367. </div>
  368. <div className={s.pageContent}>
  369. <FilteredTable
  370. items={this.state.migrations}
  371. filterFn={this.filterFn}
  372. queryText={this.state.queryText}
  373. filterType={this.state.filterType}
  374. filterStatus={this.state.filterStatus}
  375. renderSearch={(e) => this.renderSearch(e)}
  376. ></FilteredTable>
  377. </div>
  378. </div>
  379. <ConfirmationDialog
  380. visible={this.state.confirmationDialog.visible}
  381. message={this.state.confirmationDialog.message}
  382. onConfirm={(e) => this.state.confirmationDialog.onConfirm(e)}
  383. onCancel={(e) => this.state.confirmationDialog.onCancel(e)}
  384. />
  385. </div>
  386. );
  387. }
  388. }
  389. export default withStyles(MigrationList, s);