WizardInstances.jsx 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  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, { css } from 'styled-components'
  16. import PropTypes from 'prop-types'
  17. import {
  18. Checkbox,
  19. SearchInput,
  20. ReloadButton,
  21. Arrow,
  22. StatusIcon,
  23. StatusImage,
  24. Button,
  25. } from 'components'
  26. import Palette from '../../styleUtils/Palette'
  27. import StyleProps from '../../styleUtils/StyleProps'
  28. import instanceImage from './images/instance.svg'
  29. import bigInstanceImage from './images/instance-big.svg'
  30. const Wrapper = styled.div`
  31. width: 100%;
  32. display: flex;
  33. flex-direction: column;
  34. `
  35. const LoadingWrapper = styled.div`
  36. margin-top: 32px;
  37. display: flex;
  38. flex-direction: column;
  39. align-items: center;
  40. `
  41. const InstancesWrapper = styled.div`
  42. margin-left: -32px;
  43. flex-grow: 1;
  44. overflow: auto;
  45. `
  46. const InstanceContent = styled.div`
  47. display: flex;
  48. align-items: center;
  49. width: 100%;
  50. padding: 8px 16px;
  51. margin-left: 16px;
  52. border-top: 1px solid ${Palette.grayscale[1]};
  53. transition: background ${StyleProps.animations.swift};
  54. &:hover {
  55. background: ${Palette.grayscale[1]};
  56. }
  57. `
  58. const CheckboxStyled = styled(Checkbox) `
  59. opacity: 0;
  60. transition: all ${StyleProps.animations.swift};
  61. `
  62. const Instance = styled.div`
  63. display: flex;
  64. align-items: center;
  65. position: relative;
  66. cursor: pointer;
  67. ${CheckboxStyled} {
  68. ${props => props.selected ? 'opacity: 1;' : ''}
  69. }
  70. &:hover ${CheckboxStyled} {
  71. opacity: 1;
  72. }
  73. &:last-child ${InstanceContent} {
  74. border-bottom: 1px solid ${Palette.grayscale[1]};
  75. }
  76. `
  77. const LoadingText = styled.div`
  78. margin-top: 38px;
  79. font-size: 18px;
  80. `
  81. const Image = styled.div`
  82. width: 48px;
  83. height: 48px;
  84. background: url('${instanceImage}') center no-repeat;
  85. `
  86. const Label = styled.div`
  87. flex-grow: 1;
  88. white-space: nowrap;
  89. text-overflow: ellipsis;
  90. overflow: hidden;
  91. margin: 0 16px;
  92. `
  93. const Details = styled.div`
  94. font-size: 14px;
  95. color: ${Palette.grayscale[4]};
  96. `
  97. const FiltersWrapper = styled.div`
  98. padding: 8px 0 0 8px;
  99. min-height: 24px;
  100. margin-bottom: 16px;
  101. display: flex;
  102. justify-content: space-between;
  103. `
  104. const FilterInfo = styled.div`
  105. display: flex;
  106. color: ${Palette.grayscale[4]};
  107. `
  108. const SelectionInfo = styled.div``
  109. const FilterSeparator = styled.div`
  110. margin: 0 14px 0 16px;
  111. `
  112. const Pagination = styled.div`
  113. display: flex;
  114. justify-content: center;
  115. margin: 32px 0 16px 0;
  116. flex-shrink: 0;
  117. `
  118. const Page = styled.div`
  119. width: 30px;
  120. height: 30px;
  121. display: flex;
  122. justify-content: center;
  123. align-items: center;
  124. border: 1px solid ${Palette.grayscale[3]};
  125. cursor: ${props => props.disabled ? 'default' : 'pointer'};
  126. ${props => props.previous ? css`
  127. border-top-left-radius: ${StyleProps.borderRadius};
  128. border-bottom-left-radius: ${StyleProps.borderRadius};
  129. padding-top: 2px;
  130. height: 28px;
  131. ` : ''}
  132. ${props => props.number ? css`
  133. border-top: 1px solid ${Palette.grayscale[3]};
  134. border-bottom: 1px solid ${Palette.grayscale[3]};
  135. border-left: 1px solid white;
  136. border-right: 1px solid white;
  137. cursor: default;
  138. ` : ''}
  139. ${props => props.next ? css`
  140. border-top-right-radius: ${StyleProps.borderRadius};
  141. border-bottom-right-radius: ${StyleProps.borderRadius};
  142. ` : ''}
  143. `
  144. const Reloading = styled.div`
  145. margin: 32px auto 0 auto;
  146. flex-grow: 1;
  147. `
  148. const SearchNotFound = styled.div`
  149. display: flex;
  150. flex-direction: column;
  151. align-items: center;
  152. ${props => props.marginTop ? 'margin-top: 64px;' : ''}
  153. * {
  154. margin-bottom: 42px;
  155. &:last-child {
  156. margin-bottom: 0;
  157. }
  158. }
  159. `
  160. const SearchNotFoundText = styled.div`
  161. font-size: 18px;
  162. `
  163. const SearchNotFoundSubtitle = styled.div`
  164. color: ${Palette.grayscale[4]};
  165. margin-top: -32px;
  166. `
  167. const BigInstanceImage = styled.div`
  168. ${StyleProps.exactSize('96px')}
  169. background: url('${bigInstanceImage}') center no-repeat;
  170. `
  171. class WizardInstances extends React.Component {
  172. static propTypes = {
  173. instances: PropTypes.array,
  174. selectedInstances: PropTypes.array,
  175. currentPage: PropTypes.number,
  176. loading: PropTypes.bool,
  177. searching: PropTypes.bool,
  178. searchNotFound: PropTypes.bool,
  179. loadingPage: PropTypes.bool,
  180. hasNextPage: PropTypes.bool,
  181. reloading: PropTypes.bool,
  182. onSearchInputChange: PropTypes.func,
  183. onNextPageClick: PropTypes.func,
  184. onPreviousPageClick: PropTypes.func,
  185. onReloadClick: PropTypes.func,
  186. onInstanceClick: PropTypes.func,
  187. }
  188. constructor() {
  189. super()
  190. this.state = {
  191. searchText: '',
  192. }
  193. }
  194. handleSeachInputChange(searchText) {
  195. clearTimeout(this.timeout)
  196. this.timeout = setTimeout(() => {
  197. this.setState({ searchText })
  198. this.props.onSearchInputChange(searchText)
  199. }, 500)
  200. }
  201. areNoInstances() {
  202. return !this.props.loading && !this.props.searchNotFound && !this.props.reloading && this.props.instances.length === 0
  203. }
  204. renderNoInstances() {
  205. if (!this.areNoInstances()) {
  206. return null
  207. }
  208. return (
  209. <SearchNotFound marginTop>
  210. <BigInstanceImage />
  211. <SearchNotFoundText>It seems like you don’t have any Instances in this Endpoint</SearchNotFoundText>
  212. <SearchNotFoundSubtitle>You can retry the search or choose another Endpoint</SearchNotFoundSubtitle>
  213. <Button hollow onClick={() => { this.props.onReloadClick(this.state.searchText) }}>Retry Search</Button>
  214. </SearchNotFound>
  215. )
  216. }
  217. renderSearchNotFound() {
  218. if (!this.props.searchNotFound) {
  219. return null
  220. }
  221. return (
  222. <SearchNotFound>
  223. <StatusImage status="ERROR" />
  224. <SearchNotFoundText>Your search returned no results</SearchNotFoundText>
  225. <Button hollow onClick={() => { this.props.onReloadClick(this.state.searchText) }}>Retry</Button>
  226. </SearchNotFound>
  227. )
  228. }
  229. renderReloading() {
  230. if (!this.props.reloading) {
  231. return null
  232. }
  233. return (
  234. <Reloading>
  235. <StatusImage loading />
  236. </Reloading>
  237. )
  238. }
  239. renderLoading() {
  240. if (!this.props.loading) {
  241. return null
  242. }
  243. return (
  244. <LoadingWrapper>
  245. <StatusImage loading />
  246. <LoadingText>Loading instances...</LoadingText>
  247. </LoadingWrapper>
  248. )
  249. }
  250. renderInstances() {
  251. if (this.props.loading || this.props.searchNotFound || this.props.reloading || this.areNoInstances()) {
  252. return null
  253. }
  254. return (
  255. <InstancesWrapper>
  256. {this.props.instances.map(instance => {
  257. let selected = Boolean(this.props.selectedInstances && this.props.selectedInstances.find(i => i.id === instance.id))
  258. let flavorName = instance.flavor_name ? ` | ${instance.flavor_name}` : ''
  259. return (
  260. <Instance
  261. key={instance.id}
  262. onClick={() => { this.props.onInstanceClick(instance) }}
  263. selected={selected}
  264. >
  265. <CheckboxStyled checked={selected} onChange={() => {}} />
  266. <InstanceContent>
  267. <Image />
  268. <Label>{instance.instance_name}</Label>
  269. <Details>{`${instance.num_cpu} vCPU | ${instance.memory_mb} MB RAM${flavorName}`}</Details>
  270. </InstanceContent>
  271. </Instance>
  272. )
  273. })}
  274. </InstancesWrapper>
  275. )
  276. }
  277. renderFilters() {
  278. if (this.props.loading || this.areNoInstances()) {
  279. return null
  280. }
  281. let count = this.props.selectedInstances ? this.props.selectedInstances.length : 0
  282. let plural = count === 1 ? '' : 's'
  283. return (
  284. <FiltersWrapper>
  285. <SearchInput
  286. alwaysOpen
  287. onChange={searchText => { this.handleSeachInputChange(searchText) }}
  288. loading={this.props.searching}
  289. placeholder="Search VMs"
  290. />
  291. <FilterInfo>
  292. <SelectionInfo>{count} instance{plural} selected</SelectionInfo>
  293. <FilterSeparator>|</FilterSeparator>
  294. <ReloadButton onClick={() => { this.props.onReloadClick(this.state.searchText) }} />
  295. </FilterInfo>
  296. </FiltersWrapper>
  297. )
  298. }
  299. renderPagination() {
  300. if (this.props.loading || this.props.searchNotFound || this.props.reloading || this.areNoInstances()) {
  301. return null
  302. }
  303. let areAllDisabled = this.props.searching || this.props.loadingPage
  304. let isPreviousDisabled = this.props.currentPage === 1 || areAllDisabled
  305. let isNextDisabled = !this.props.hasNextPage || areAllDisabled
  306. return (
  307. <Pagination>
  308. <Page
  309. previous
  310. disabled={isPreviousDisabled}
  311. onClick={() => { if (!isPreviousDisabled) { this.props.onPreviousPageClick() } }}
  312. >
  313. <Arrow orientation="left" disabled={isPreviousDisabled} />
  314. </Page>
  315. <Page number>
  316. {this.props.loadingPage ? <StatusIcon status="RUNNING" secondary /> : this.props.currentPage}
  317. </Page>
  318. <Page
  319. next
  320. onClick={() => { if (!isNextDisabled) { this.props.onNextPageClick(this.state.searchText) } }}
  321. disabled={isNextDisabled}
  322. >
  323. <Arrow disabled={isNextDisabled} />
  324. </Page>
  325. </Pagination>
  326. )
  327. }
  328. render() {
  329. return (
  330. <Wrapper>
  331. {this.renderFilters()}
  332. {this.renderLoading()}
  333. {this.renderReloading()}
  334. {this.renderSearchNotFound()}
  335. {this.renderInstances()}
  336. {this.renderPagination()}
  337. {this.renderNoInstances()}
  338. </Wrapper>
  339. )
  340. }
  341. }
  342. export default WizardInstances