InstanceStore.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  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 { observable, runInAction, computed, action } from 'mobx'
  16. import type { Instance } from '../types/Instance'
  17. import type { Endpoint } from '../types/Endpoint'
  18. import InstanceSource from '../sources/InstanceSource'
  19. import ApiCaller from '../utils/ApiCaller'
  20. import configLoader from '../utils/Config'
  21. class InstanceStore {
  22. @observable instancesLoading = false
  23. @observable instancesPerPage = 6
  24. @observable currentPage = 1
  25. @observable searchChunksLoading = false
  26. @observable searchedInstances: Instance[] = []
  27. @observable backgroundInstances: Instance[] = []
  28. @observable backgroundChunksLoading = false
  29. @observable searching = false
  30. @observable searchNotFound = false
  31. @observable reloading = false
  32. @observable instancesDetails: Instance[] = []
  33. @observable loadingInstancesDetails = true
  34. @observable instancesDetailsCount = 0
  35. @observable instancesDetailsRemaining = 0
  36. @observable searchText = ''
  37. @computed get instances(): Instance[] {
  38. if (this.searchText && this.searchedInstances.length > 0) {
  39. return this.searchedInstances
  40. }
  41. return this.backgroundInstances
  42. }
  43. @computed get chunksLoading(): boolean {
  44. if (this.searchText) {
  45. return this.searchChunksLoading
  46. }
  47. return this.backgroundChunksLoading
  48. }
  49. lastEndpointId: string
  50. reqId: number
  51. @action async loadInstancesInChunks(options: {
  52. endpoint: Endpoint,
  53. vmsPerPage?: number,
  54. reload?: boolean,
  55. env?: any,
  56. useCache?: boolean,
  57. }) {
  58. let { endpoint, vmsPerPage, reload, env, useCache } = options
  59. vmsPerPage = vmsPerPage || 6
  60. ApiCaller.cancelRequests(`${endpoint.id}-chunk`)
  61. this.backgroundInstances = []
  62. if (reload) {
  63. this.reloading = true
  64. } else {
  65. this.instancesLoading = true
  66. }
  67. this.backgroundChunksLoading = true
  68. this.lastEndpointId = endpoint.id
  69. let chunkSize = configLoader.config.instancesListBackgroundLoading
  70. let chunkCount = Math.max(chunkSize[endpoint.type] || chunkSize.default, vmsPerPage)
  71. let loadNextChunk = async (lastEndpointId?: string) => {
  72. let currentEndpointId = endpoint.id
  73. let instances = await InstanceSource.loadInstancesChunk(currentEndpointId, chunkCount, lastEndpointId, `${endpoint.id}-chunk`, undefined, env, useCache)
  74. if (currentEndpointId !== this.lastEndpointId) {
  75. return
  76. }
  77. let shouldContinue = this.loadInstancesInChunksSuccess(instances, chunkCount, reload)
  78. if (shouldContinue) {
  79. loadNextChunk(instances[instances.length - 1].id)
  80. }
  81. }
  82. loadNextChunk()
  83. }
  84. @action loadInstancesInChunksSuccess(instances: Instance[], chunkCount: number, reload?: boolean): boolean {
  85. this.backgroundInstances = [...this.backgroundInstances, ...instances]
  86. if (reload) {
  87. this.reloading = false
  88. }
  89. this.instancesLoading = false
  90. if (instances.length < chunkCount) {
  91. this.backgroundChunksLoading = false
  92. return false
  93. }
  94. return true
  95. }
  96. @action async loadInstances(endpointId: string): Promise<void> {
  97. this.instancesLoading = true
  98. this.lastEndpointId = endpointId
  99. try {
  100. let instances = await InstanceSource.loadInstances(endpointId, true)
  101. if (endpointId !== this.lastEndpointId) {
  102. return
  103. }
  104. this.loadInstancesSuccess(instances)
  105. } catch (ex) {
  106. if (endpointId !== this.lastEndpointId) {
  107. return
  108. }
  109. runInAction(() => { this.instancesLoading = false })
  110. throw ex
  111. }
  112. }
  113. @action loadInstancesSuccess(instances: Instance[]) {
  114. this.backgroundInstances = instances
  115. this.instancesLoading = false
  116. }
  117. @action async searchInstances(endpoint: Endpoint, searchText: string) {
  118. ApiCaller.cancelRequests(`${endpoint.id}-chunk-search`)
  119. this.searchText = searchText
  120. this.searchNotFound = false
  121. if (!searchText) {
  122. this.currentPage = 1
  123. this.searchedInstances = []
  124. return
  125. }
  126. if (!this.backgroundChunksLoading) {
  127. this.searchedInstances = this.backgroundInstances
  128. .filter(i => (i.instance_name || i.name).toLowerCase().indexOf(searchText.toLowerCase()) > -1)
  129. this.searchNotFound = Boolean(this.searchedInstances.length === 0)
  130. this.currentPage = 1
  131. return
  132. }
  133. this.searching = true
  134. this.searchChunksLoading = true
  135. let chunkSize = configLoader.config.instancesListBackgroundLoading
  136. let chunkCount = Math.max(chunkSize[endpoint.type] || chunkSize.default, this.instancesPerPage)
  137. let loadNextChunk = async (lastEndpointId?: string) => {
  138. let instances = await InstanceSource.loadInstancesChunk(
  139. endpoint.id,
  140. chunkCount,
  141. lastEndpointId,
  142. `${endpoint.id}-chunk-search`,
  143. searchText
  144. )
  145. if (this.searching) {
  146. runInAction(() => {
  147. this.currentPage = 1
  148. this.searchedInstances = []
  149. })
  150. }
  151. let shouldContinue = this.searchInstancesSuccess(instances, chunkCount)
  152. if (shouldContinue) {
  153. loadNextChunk(instances[instances.length - 1].id)
  154. }
  155. }
  156. loadNextChunk()
  157. }
  158. @action searchInstancesSuccess(instances: Instance[], chunkCount: number): boolean {
  159. this.searchedInstances = [...this.searchedInstances, ...instances]
  160. this.searching = false
  161. this.searchNotFound = Boolean(this.searchedInstances.length === 0)
  162. if (instances.length < chunkCount) {
  163. this.searchChunksLoading = false
  164. return false
  165. }
  166. return true
  167. }
  168. @action reloadInstances(endpoint: Endpoint, chunkSize?: number, env?: any) {
  169. this.searchNotFound = false
  170. this.searchText = ''
  171. this.currentPage = 1
  172. this.loadInstancesInChunks({ endpoint, vmsPerPage: chunkSize, reload: true, env })
  173. }
  174. @action cancelIntancesChunksLoading() {
  175. ApiCaller.cancelRequests(`${this.lastEndpointId}-chunk`)
  176. this.lastEndpointId = ''
  177. this.searchNotFound = false
  178. this.searchText = ''
  179. this.currentPage = 1
  180. }
  181. @action setPage(page: number) {
  182. this.currentPage = page
  183. }
  184. @action updateInstancesPerPage(instancesPerPage: number) {
  185. this.currentPage = 1
  186. this.instancesPerPage = instancesPerPage
  187. }
  188. @action async loadInstancesDetailsBulk(
  189. instanceInfos: {
  190. endpointId: string,
  191. instanceNames: string[],
  192. env?: ?any,
  193. }[]
  194. ) {
  195. this.reqId = !this.reqId ? 1 : this.reqId + 1
  196. this.instancesDetails = []
  197. this.loadingInstancesDetails = true
  198. InstanceSource.cancelInstancesDetailsRequests(this.reqId - 1)
  199. try {
  200. await Promise.all(instanceInfos.map(async i => {
  201. await Promise.all(i.instanceNames.map(async name => {
  202. let instanceDetails = await InstanceSource.loadInstanceDetails({
  203. endpointId: i.endpointId,
  204. instanceName: name,
  205. reqId: this.reqId,
  206. quietError: false,
  207. env: i.env,
  208. cache: true,
  209. })
  210. runInAction(() => {
  211. this.instancesDetails = this.instancesDetails.filter(i => (i.name || i.instance_name || '') !== name)
  212. this.instancesDetails.push(instanceDetails.instance)
  213. this.instancesDetails.sort(n => (n.name || n.instance_name || '')
  214. .localeCompare(n.name || n.instance_name || ''))
  215. })
  216. }))
  217. }))
  218. } finally {
  219. this.loadingInstancesDetails = false
  220. }
  221. }
  222. @action async loadInstancesDetails(opts: {
  223. endpointId: string,
  224. instancesInfo: Instance[],
  225. cache?: boolean,
  226. quietError?: boolean,
  227. env?: any,
  228. targetProvider: string,
  229. }): Promise<void> {
  230. let { endpointId, instancesInfo, cache, quietError, env, targetProvider } = opts
  231. // Use reqId to be able to uniquely identify the request so all but the latest request can be igonred and canceled
  232. this.reqId = !this.reqId ? 1 : this.reqId + 1
  233. InstanceSource.cancelInstancesDetailsRequests(this.reqId - 1)
  234. instancesInfo.sort((a, b) => (a.instance_name || a.name).localeCompare(b.instance_name || b.name))
  235. let count = instancesInfo.length
  236. this.loadingInstancesDetails = true
  237. this.instancesDetails = []
  238. this.loadingInstancesDetails = true
  239. this.instancesDetailsCount = count
  240. this.instancesDetailsRemaining = count
  241. await new Promise(resolve => {
  242. Promise.all(instancesInfo.map(async instanceInfo => {
  243. try {
  244. let resp: { instance: Instance, reqId: number } =
  245. await InstanceSource.loadInstanceDetails({
  246. endpointId,
  247. instanceName: instanceInfo.instance_name || instanceInfo.name,
  248. targetProvider,
  249. reqId: this.reqId,
  250. quietError,
  251. env,
  252. cache,
  253. })
  254. if (resp.reqId !== this.reqId) {
  255. return
  256. }
  257. runInAction(() => {
  258. this.instancesDetailsRemaining -= 1
  259. this.loadingInstancesDetails = this.instancesDetailsRemaining > 0
  260. if (this.instancesDetails.find(i => i.id === resp.instance.id)) {
  261. this.instancesDetails = this.instancesDetails.filter(i => i.id !== resp.instance.id)
  262. }
  263. })
  264. runInAction(() => {
  265. this.instancesDetails = [
  266. ...this.instancesDetails,
  267. resp.instance,
  268. ]
  269. this.instancesDetails.sort((a, b) => (a.instance_name || a.name).localeCompare((b.instance_name || b.name)))
  270. })
  271. if (this.instancesDetailsRemaining === 0) {
  272. resolve()
  273. }
  274. } catch (err) {
  275. runInAction(() => {
  276. this.instancesDetailsRemaining -= 1
  277. this.loadingInstancesDetails = this.instancesDetailsRemaining > 0
  278. })
  279. if (!err || err.reqId !== this.reqId) {
  280. return
  281. }
  282. if (count === 0) {
  283. resolve()
  284. }
  285. }
  286. }))
  287. })
  288. }
  289. @action clearInstancesDetails() {
  290. this.instancesDetails = []
  291. this.loadingInstancesDetails = false
  292. this.instancesDetailsCount = 0
  293. this.instancesDetailsRemaining = 0
  294. }
  295. }
  296. export default new InstanceStore()