WizardInstances.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  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 Checkbox from '../../../ui/Checkbox/Checkbox'
  18. import ReloadButton from '../../../ui/ReloadButton/ReloadButton'
  19. import StatusImage from '../../../ui/StatusComponents/StatusImage/StatusImage'
  20. import Button from '../../../ui/Button/Button'
  21. import SearchInput from '../../../ui/SearchInput/SearchInput'
  22. import InfoIcon from '../../../ui/InfoIcon/InfoIcon'
  23. import Pagination from '../../../ui/Pagination/Pagination'
  24. import { ThemePalette, ThemeProps } from '../../../Theme'
  25. import type { Instance as InstanceType } from '../../../../@types/Instance'
  26. import instanceImage from './images/instance.svg'
  27. import bigInstanceImage from './images/instance-big.svg'
  28. const Wrapper = styled.div<any>`
  29. width: 100%;
  30. display: flex;
  31. flex-direction: column;
  32. `
  33. const LoadingWrapper = styled.div<any>`
  34. margin-top: 32px;
  35. display: flex;
  36. flex-direction: column;
  37. align-items: center;
  38. `
  39. const InstancesWrapper = styled.div<any>`
  40. margin-left: -32px;
  41. flex-grow: 1;
  42. overflow: auto;
  43. `
  44. const InstanceContent = styled.div<any>`
  45. display: flex;
  46. align-items: center;
  47. width: 100%;
  48. padding: 8px 16px;
  49. margin-left: 16px;
  50. border-top: 1px solid ${ThemePalette.grayscale[1]};
  51. transition: background ${ThemeProps.animations.swift};
  52. &:hover {
  53. background: ${ThemePalette.grayscale[1]};
  54. }
  55. `
  56. const CheckboxStyled = styled(Checkbox)`
  57. opacity: 0;
  58. transition: all ${ThemeProps.animations.swift};
  59. :focus {
  60. opacity: 1;
  61. }
  62. `
  63. const Instance = styled.div<any>`
  64. display: flex;
  65. align-items: center;
  66. position: relative;
  67. cursor: pointer;
  68. ${CheckboxStyled} {
  69. ${props => (props.selected ? 'opacity: 1;' : '')}
  70. }
  71. &:hover ${CheckboxStyled} {
  72. opacity: 1;
  73. }
  74. &:last-child ${InstanceContent} {
  75. border-bottom: 1px solid ${ThemePalette.grayscale[1]};
  76. }
  77. `
  78. const LoadingText = styled.div<any>`
  79. margin-top: 38px;
  80. font-size: 18px;
  81. `
  82. export const Image = styled.div<any>`
  83. ${ThemeProps.exactSize('48px')}
  84. background: url('${instanceImage}') center no-repeat;
  85. `
  86. const Label = styled.div<any>`
  87. flex-grow: 1;
  88. margin: 0 16px;
  89. display: flex;
  90. flex-direction: column;
  91. `
  92. const LabelTitle = styled.div``
  93. const LabelSubtitle = styled.div`
  94. color: ${ThemePalette.grayscale[4]};
  95. overflow-wrap: anywhere;
  96. `
  97. const Details = styled.div<any>`
  98. color: ${ThemePalette.grayscale[4]};
  99. min-width: 160px;
  100. text-align: right;
  101. `
  102. const FiltersWrapper = styled.div<any>`
  103. padding: 8px 0 0 8px;
  104. min-height: 24px;
  105. margin-bottom: 16px;
  106. display: flex;
  107. justify-content: space-between;
  108. `
  109. const SearchInputInfo = styled.div<any>`
  110. display: flex;
  111. align-items: center;
  112. `
  113. const FilterInfo = styled.div<any>`
  114. display: flex;
  115. color: ${ThemePalette.grayscale[4]};
  116. `
  117. const SelectionInfo = styled.div<any>``
  118. const FilterSeparator = styled.div<any>`
  119. margin: 0 14px 0 16px;
  120. `
  121. const Reloading = styled.div<any>`
  122. margin: 32px auto 0 auto;
  123. flex-grow: 1;
  124. `
  125. const SearchNotFound = styled.div<any>`
  126. display: flex;
  127. flex-direction: column;
  128. align-items: center;
  129. ${props => (props.marginTop ? 'margin-top: 64px;' : '')}
  130. > * {
  131. margin-bottom: 42px;
  132. }
  133. `
  134. const SearchNotFoundText = styled.div<any>`
  135. font-size: 18px;
  136. `
  137. const SearchNotFoundSubtitle = styled.div<any>`
  138. color: ${ThemePalette.grayscale[4]};
  139. margin-top: -32px;
  140. text-align: center;
  141. `
  142. const BigInstanceImage = styled.div<any>`
  143. ${ThemeProps.exactSize('96px')}
  144. background: url('${bigInstanceImage}') center no-repeat;
  145. `
  146. type Props = {
  147. instances: InstanceType[],
  148. selectedInstances?: InstanceType[] | null,
  149. currentPage: number,
  150. instancesPerPage: number,
  151. loading: boolean,
  152. chunksLoading: boolean,
  153. searching: boolean,
  154. searchNotFound: boolean,
  155. reloading: boolean,
  156. hasSourceOptions: boolean,
  157. searchText?: string,
  158. onSearchInputChange: (value: string) => void,
  159. onReloadClick: () => void,
  160. onInstanceClick: (instance: InstanceType) => void,
  161. onPageClick: (page: number) => void,
  162. }
  163. type State = {
  164. searchText: string,
  165. }
  166. @observer
  167. class WizardInstances extends React.Component<Props, State> {
  168. state = {
  169. searchText: '',
  170. }
  171. timeout!: number
  172. isCheckboxMouseDown: boolean = false
  173. componentWillUnmount() {
  174. this.props.onSearchInputChange('')
  175. }
  176. handleSeachInputChange(searchText: string) {
  177. clearTimeout(this.timeout)
  178. this.setState({ searchText })
  179. this.timeout = setTimeout(() => {
  180. this.props.onSearchInputChange(searchText)
  181. }, 500)
  182. }
  183. handlePreviousPageClick() {
  184. this.props.onPageClick(this.props.currentPage - 1)
  185. }
  186. handleNextPageClick() {
  187. this.props.onPageClick(this.props.currentPage + 1)
  188. }
  189. areNoInstances() {
  190. return !this.props.loading && !this.props.searchNotFound && !this.props.reloading
  191. && this.props.instances.length === 0 && !this.props.searching
  192. }
  193. renderNoInstances() {
  194. if (!this.areNoInstances()) {
  195. return null
  196. }
  197. let subtitle
  198. if (this.props.hasSourceOptions) {
  199. subtitle = (
  200. <SearchNotFoundSubtitle>
  201. Some platforms require pre-inputting parameters
  202. like location or resource containers for listing instances.
  203. <br />Please check that all of the options from the previous screen are correct.
  204. </SearchNotFoundSubtitle>
  205. )
  206. } else {
  207. subtitle = (
  208. <SearchNotFoundSubtitle>
  209. You can retry the search or choose another Endpoint
  210. </SearchNotFoundSubtitle>
  211. )
  212. }
  213. return (
  214. <SearchNotFound marginTop>
  215. <BigInstanceImage />
  216. <SearchNotFoundText>
  217. It seems like you don’t have any Instances in this Endpoint
  218. </SearchNotFoundText>
  219. {subtitle}
  220. <Button hollow onClick={() => { this.props.onReloadClick() }}>Retry Search</Button>
  221. </SearchNotFound>
  222. )
  223. }
  224. renderSearchNotFound() {
  225. if (!this.props.searchNotFound) {
  226. return null
  227. }
  228. let subtitle = null
  229. if (this.props.hasSourceOptions) {
  230. subtitle = (
  231. <SearchNotFoundSubtitle>
  232. Some platforms require pre-inputting parameters like location or resource containers for
  233. <br />listing instances.
  234. Please check that all of the options from the previous screen are correct.
  235. </SearchNotFoundSubtitle>
  236. )
  237. }
  238. return (
  239. <SearchNotFound>
  240. <StatusImage status="ERROR" />
  241. <SearchNotFoundText data-test-id="wInstances-notFoundText">Your search returned no results</SearchNotFoundText>
  242. {subtitle}
  243. <Button hollow onClick={() => { this.props.onReloadClick() }}>Retry</Button>
  244. </SearchNotFound>
  245. )
  246. }
  247. renderReloading() {
  248. if (!this.props.reloading) {
  249. return null
  250. }
  251. return (
  252. <Reloading>
  253. <StatusImage loading />
  254. </Reloading>
  255. )
  256. }
  257. renderLoading() {
  258. if (!this.props.loading) {
  259. return null
  260. }
  261. return (
  262. <LoadingWrapper>
  263. <StatusImage loading data-test-id="wInstances-loadingStatus" />
  264. <LoadingText>Loading instances...</LoadingText>
  265. </LoadingWrapper>
  266. )
  267. }
  268. renderInstances() {
  269. if (this.props.loading || this.props.searchNotFound
  270. || this.props.reloading || this.areNoInstances()) {
  271. return null
  272. }
  273. const startIdx = (this.props.currentPage - 1) * this.props.instancesPerPage
  274. const endIdx = startIdx + (this.props.instancesPerPage - 1)
  275. const filteredInstances = this.props.instances
  276. .filter((_, idx) => idx >= startIdx && idx <= endIdx)
  277. return (
  278. <InstancesWrapper>
  279. {filteredInstances.map(instance => {
  280. const selected = Boolean(this.props.selectedInstances
  281. && this.props.selectedInstances.find(i => i.id === instance.id))
  282. const flavorName = instance.flavor_name ? ` | ${instance.flavor_name}` : ''
  283. const instanceId = instance.instance_name || instance.id
  284. return (
  285. <Instance
  286. key={instance.id}
  287. onMouseDown={() => {
  288. if (!this.isCheckboxMouseDown) this.props.onInstanceClick(instance)
  289. }}
  290. selected={selected}
  291. data-test-id={`wInstances-item-${instance.id}`}
  292. >
  293. <CheckboxStyled
  294. checked={selected}
  295. onChange={() => { this.props.onInstanceClick(instance) }}
  296. onMouseDown={() => { this.isCheckboxMouseDown = true }}
  297. onMouseUp={() => { this.isCheckboxMouseDown = false }}
  298. />
  299. <InstanceContent data-test-id="wInstances-instanceItem">
  300. <Image />
  301. <Label>
  302. <LabelTitle>{instance.name}</LabelTitle>
  303. {instanceId !== instance.name ? (
  304. <LabelSubtitle>{instanceId}</LabelSubtitle>
  305. ) : null}
  306. </Label>
  307. <Details>{`${instance.num_cpu} vCPU | ${instance.memory_mb} MB RAM${flavorName}`}</Details>
  308. </InstanceContent>
  309. </Instance>
  310. )
  311. })}
  312. </InstancesWrapper>
  313. )
  314. }
  315. renderFilters() {
  316. if (this.props.loading || this.areNoInstances()) {
  317. return null
  318. }
  319. const count = this.props.selectedInstances ? this.props.selectedInstances.length : 0
  320. const plural = count === 1 ? '' : 's'
  321. return (
  322. <FiltersWrapper>
  323. <SearchInputInfo>
  324. <SearchInput
  325. alwaysOpen
  326. onChange={searchText => { this.handleSeachInputChange(searchText) }}
  327. value={this.state.searchText}
  328. loading={this.props.searching}
  329. placeholder="Search VMs"
  330. data-test-id="wInstances-searchInput"
  331. />
  332. {this.props.hasSourceOptions ? (
  333. <InfoIcon
  334. text="Some platforms require pre-inputting parameters like location or resource containers for listing instances. Please check that all of the options from the previous screen are correct."
  335. marginBottom={0}
  336. marginLeft={8}
  337. filled
  338. />
  339. ) : null}
  340. </SearchInputInfo>
  341. <FilterInfo>
  342. <SelectionInfo data-test-id="wInstances-selInfo">{count} instance{plural} selected</SelectionInfo>
  343. <FilterSeparator>|</FilterSeparator>
  344. <ReloadButton
  345. onClick={() => { this.props.onReloadClick() }}
  346. data-test-id="wInstances-reloadButton"
  347. />
  348. </FilterInfo>
  349. </FiltersWrapper>
  350. )
  351. }
  352. renderPagination() {
  353. if (this.props.loading || this.props.searchNotFound
  354. || this.props.reloading || this.areNoInstances()) {
  355. return null
  356. }
  357. const hasNextPage = this.props.currentPage
  358. * this.props.instancesPerPage < this.props.instances.length
  359. const areAllDisabled = this.props.searching
  360. const isPreviousDisabled = this.props.currentPage === 1 || areAllDisabled
  361. const isNextDisabled = !hasNextPage || areAllDisabled
  362. return (
  363. <Pagination
  364. style={{ margin: '32px 0 16px 0' }}
  365. previousDisabled={isPreviousDisabled}
  366. onPreviousClick={() => { this.handlePreviousPageClick() }}
  367. currentPage={this.props.currentPage}
  368. totalPages={Math.ceil(this.props.instances.length / this.props.instancesPerPage)}
  369. loading={this.props.chunksLoading}
  370. nextDisabled={isNextDisabled}
  371. onNextClick={() => { this.handleNextPageClick() }}
  372. />
  373. )
  374. }
  375. render() {
  376. return (
  377. <Wrapper>
  378. {this.renderFilters()}
  379. {this.renderLoading()}
  380. {this.renderReloading()}
  381. {this.renderSearchNotFound()}
  382. {this.renderInstances()}
  383. {this.renderPagination()}
  384. {this.renderNoInstances()}
  385. </Wrapper>
  386. )
  387. }
  388. }
  389. export default WizardInstances