InstanceStore.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491
  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 { observable, runInAction, computed, action } from "mobx";
  15. import type { Instance, InstanceBase } from "@src/@types/Instance";
  16. import type { Endpoint } from "@src/@types/Endpoint";
  17. import InstanceSource from "@src/sources/InstanceSource";
  18. import ApiCaller from "@src/utils/ApiCaller";
  19. import configLoader from "@src/utils/Config";
  20. import { ProviderTypes } from "@src/@types/Providers";
  21. import notificationStore from "./NotificationStore";
  22. class InstanceStore {
  23. @observable instancesLoading = false;
  24. @observable instancesPerPage = 6;
  25. @observable currentPage = 1;
  26. @observable searchChunksLoading = false;
  27. @observable searchedInstances: Instance[] = [];
  28. @observable backgroundInstances: Instance[] = [];
  29. @observable backgroundChunksLoading = false;
  30. @observable searching = false;
  31. @observable searchNotFound = false;
  32. @observable reloading = false;
  33. @observable instancesDetails: Instance[] = [];
  34. @observable loadingInstancesDetails = false;
  35. @observable instancesDetailsCount = 0;
  36. @observable instancesDetailsRemaining = 0;
  37. @observable searchText = "";
  38. @computed get instances(): Instance[] {
  39. if (this.searchText && this.searchedInstances.length > 0) {
  40. return this.searchedInstances;
  41. }
  42. return this.backgroundInstances;
  43. }
  44. @computed get chunksLoading(): boolean {
  45. if (this.searchText) {
  46. return this.searchChunksLoading;
  47. }
  48. return this.backgroundChunksLoading;
  49. }
  50. lastEndpointId?: string;
  51. reqId!: number;
  52. @action async loadInstancesInChunks(options: {
  53. endpoint: Endpoint;
  54. vmsPerPage?: number;
  55. reload?: boolean;
  56. env?: any;
  57. useCache?: boolean;
  58. }) {
  59. const { endpoint, vmsPerPage, reload, env, useCache } = options;
  60. const usableVmsPerPage = vmsPerPage || 6;
  61. ApiCaller.cancelRequests(`${endpoint.id}-chunk`);
  62. this.backgroundInstances = [];
  63. if (reload) {
  64. this.reloading = true;
  65. } else {
  66. this.instancesLoading = true;
  67. }
  68. this.backgroundChunksLoading = true;
  69. this.lastEndpointId = endpoint.id;
  70. const chunkSize = configLoader.config.instancesListBackgroundLoading;
  71. const chunkCount = Math.max(
  72. chunkSize[endpoint.type] || chunkSize.default,
  73. usableVmsPerPage
  74. );
  75. const loadNextChunk = async (lastEndpointId?: string) => {
  76. const currentEndpointId = endpoint.id;
  77. const [instances, invalidInstances] =
  78. await InstanceSource.loadInstancesChunk({
  79. endpointId: currentEndpointId,
  80. chunkSize: chunkCount,
  81. lastInstanceId: lastEndpointId,
  82. cancelId: `${endpoint.id}-chunk`,
  83. env,
  84. cache: useCache,
  85. });
  86. if (currentEndpointId !== this.lastEndpointId) {
  87. return;
  88. }
  89. if (invalidInstances.length) {
  90. notificationStore.alert(
  91. `There are one or more instances with invalid data (i.e. missing ID): ${invalidInstances
  92. .map(i => i.name || i.instance_name)
  93. .join(", ")}`,
  94. "error"
  95. );
  96. }
  97. const shouldContinue = this.loadInstancesInChunksSuccess({
  98. instances,
  99. instancesCount: instances.length + invalidInstances.length,
  100. chunkCount,
  101. reload,
  102. });
  103. if (shouldContinue) {
  104. await loadNextChunk(instances[instances.length - 1].id);
  105. }
  106. };
  107. await loadNextChunk();
  108. }
  109. @action loadInstancesInChunksSuccess(opts: {
  110. instances: Instance[];
  111. instancesCount: number;
  112. chunkCount: number;
  113. reload?: boolean;
  114. }): boolean {
  115. const { instances, instancesCount, chunkCount, reload } = opts;
  116. this.backgroundInstances = [...this.backgroundInstances, ...instances];
  117. if (reload) {
  118. this.reloading = false;
  119. }
  120. this.instancesLoading = false;
  121. if (instancesCount < chunkCount) {
  122. this.backgroundChunksLoading = false;
  123. return false;
  124. }
  125. return true;
  126. }
  127. @action async loadInstances(endpointId: string): Promise<void> {
  128. this.instancesLoading = true;
  129. this.lastEndpointId = endpointId;
  130. try {
  131. const instances = await InstanceSource.loadInstances(endpointId, true);
  132. if (endpointId !== this.lastEndpointId) {
  133. return;
  134. }
  135. this.loadInstancesSuccess(instances);
  136. } catch (ex) {
  137. if (endpointId !== this.lastEndpointId) {
  138. return;
  139. }
  140. runInAction(() => {
  141. this.instancesLoading = false;
  142. });
  143. throw ex;
  144. }
  145. }
  146. @action loadInstancesSuccess(instances: Instance[]) {
  147. this.backgroundInstances = instances;
  148. this.instancesLoading = false;
  149. }
  150. @action async searchInstances(endpoint: Endpoint, searchText: string) {
  151. ApiCaller.cancelRequests(`${endpoint.id}-chunk-search`);
  152. this.searchText = searchText;
  153. this.searchNotFound = false;
  154. if (!searchText) {
  155. this.currentPage = 1;
  156. this.searchedInstances = [];
  157. return;
  158. }
  159. if (!this.backgroundChunksLoading) {
  160. this.searchedInstances = this.backgroundInstances.filter(
  161. i =>
  162. (i.instance_name || i.name)
  163. .toLowerCase()
  164. .indexOf(searchText.toLowerCase()) > -1
  165. );
  166. this.searchNotFound = Boolean(this.searchedInstances.length === 0);
  167. this.currentPage = 1;
  168. return;
  169. }
  170. this.searching = true;
  171. this.searchChunksLoading = true;
  172. const chunkSize = configLoader.config.instancesListBackgroundLoading;
  173. const chunkCount = Math.max(
  174. chunkSize[endpoint.type] || chunkSize.default,
  175. this.instancesPerPage
  176. );
  177. const loadNextChunk = async (lastEndpointId?: string) => {
  178. const [instances, invalidInstances] =
  179. await InstanceSource.loadInstancesChunk({
  180. endpointId: endpoint.id,
  181. chunkSize: chunkCount,
  182. lastInstanceId: lastEndpointId,
  183. cancelId: `${endpoint.id}-chunk-search`,
  184. searchText,
  185. });
  186. if (this.searching) {
  187. runInAction(() => {
  188. this.currentPage = 1;
  189. this.searchedInstances = [];
  190. });
  191. }
  192. if (invalidInstances.length) {
  193. notificationStore.alert(
  194. `There are one or more instances with invalid data (i.e. missing ID): ${invalidInstances
  195. .map(i => i.name || i.instance_name)
  196. .join(", ")}`,
  197. "error"
  198. );
  199. }
  200. const shouldContinue = this.searchInstancesSuccess(
  201. instances,
  202. instances.length + invalidInstances.length,
  203. chunkCount
  204. );
  205. if (shouldContinue) {
  206. loadNextChunk(instances[instances.length - 1].id);
  207. }
  208. };
  209. loadNextChunk();
  210. }
  211. @action searchInstancesSuccess(
  212. instances: Instance[],
  213. instancesCount: number,
  214. chunkCount: number
  215. ): boolean {
  216. this.searchedInstances = [...this.searchedInstances, ...instances];
  217. this.searching = false;
  218. this.searchNotFound = Boolean(this.searchedInstances.length === 0);
  219. if (instancesCount < chunkCount) {
  220. this.searchChunksLoading = false;
  221. return false;
  222. }
  223. return true;
  224. }
  225. @action reloadInstances(endpoint: Endpoint, chunkSize?: number, env?: any) {
  226. this.searchNotFound = false;
  227. this.searchText = "";
  228. this.currentPage = 1;
  229. this.loadInstancesInChunks({
  230. endpoint,
  231. vmsPerPage: chunkSize,
  232. reload: true,
  233. env,
  234. });
  235. }
  236. @action cancelIntancesChunksLoading() {
  237. ApiCaller.cancelRequests(`${this.lastEndpointId}-chunk`);
  238. this.lastEndpointId = "";
  239. this.searchNotFound = false;
  240. this.searchText = "";
  241. this.currentPage = 1;
  242. }
  243. @action setPage(page: number) {
  244. this.currentPage = page;
  245. }
  246. @action updateInstancesPerPage(instancesPerPage: number) {
  247. this.currentPage = 1;
  248. this.instancesPerPage = instancesPerPage;
  249. }
  250. @action async loadInstancesDetailsBulk(
  251. instanceInfos: {
  252. endpointId: string;
  253. instanceIds: string[];
  254. env?: any;
  255. }[]
  256. ) {
  257. this.reqId = !this.reqId ? 1 : this.reqId + 1;
  258. this.instancesDetails = [];
  259. this.loadingInstancesDetails = true;
  260. InstanceSource.cancelInstancesDetailsRequests(this.reqId - 1);
  261. try {
  262. await Promise.all(
  263. instanceInfos.map(async i => {
  264. await Promise.all(
  265. i.instanceIds.map(async instanceId => {
  266. const instanceDetails = await InstanceSource.loadInstanceDetails({
  267. endpointId: i.endpointId,
  268. instanceId,
  269. reqId: this.reqId,
  270. quietError: false,
  271. env: i.env,
  272. cache: true,
  273. });
  274. const instance = instanceDetails.instance;
  275. if (!instance) {
  276. return;
  277. }
  278. runInAction(() => {
  279. this.instancesDetails = this.instancesDetails.filter(
  280. id => (id.instance_name || id.id) !== instanceId
  281. );
  282. this.instancesDetails.push(instance);
  283. this.instancesDetails = this.instancesDetails
  284. .slice()
  285. .sort(n => n.name.localeCompare(n.name));
  286. });
  287. })
  288. );
  289. })
  290. );
  291. } finally {
  292. this.loadingInstancesDetails = false;
  293. }
  294. }
  295. @action async addInstanceDetails(opts: {
  296. endpointId: string;
  297. instanceInfo: InstanceBase;
  298. cache?: boolean;
  299. quietError?: boolean;
  300. env?: any;
  301. targetProvider: ProviderTypes;
  302. }) {
  303. const { endpointId, instanceInfo, cache, quietError, env, targetProvider } =
  304. opts;
  305. this.loadingInstancesDetails = true;
  306. const resp = await InstanceSource.loadInstanceDetails({
  307. endpointId,
  308. instanceId: instanceInfo.instance_name || instanceInfo.id,
  309. targetProvider,
  310. reqId: this.reqId,
  311. quietError,
  312. env,
  313. cache,
  314. });
  315. const instance = resp.instance;
  316. if (!instance) {
  317. return;
  318. }
  319. runInAction(() => {
  320. this.loadingInstancesDetails = false;
  321. if (this.instancesDetails.find(i => i.id === instance.id)) {
  322. this.instancesDetails = this.instancesDetails.filter(
  323. i => i.id !== instance.id
  324. );
  325. }
  326. this.instancesDetails = [...this.instancesDetails, instance];
  327. this.instancesDetails = this.instancesDetails
  328. .slice()
  329. .sort((a, b) =>
  330. (a.instance_name || a.name).localeCompare(b.instance_name || b.name)
  331. );
  332. });
  333. }
  334. @action removeInstanceDetails(instance: Instance) {
  335. this.instancesDetails = this.instancesDetails.filter(
  336. i => i.id !== instance.id
  337. );
  338. }
  339. @action async loadInstancesDetails(opts: {
  340. endpointId: string;
  341. instances: InstanceBase[];
  342. cache?: boolean;
  343. quietError?: boolean;
  344. skipLog?: boolean;
  345. env?: any;
  346. targetProvider?: ProviderTypes | null;
  347. }): Promise<void> {
  348. const {
  349. endpointId,
  350. instances,
  351. cache,
  352. quietError,
  353. env,
  354. targetProvider,
  355. skipLog,
  356. } = opts;
  357. // Use reqId to be able to uniquely identify the request
  358. // so all but the latest request can be igonred and canceled
  359. this.reqId = !this.reqId ? 1 : this.reqId + 1;
  360. InstanceSource.cancelInstancesDetailsRequests(this.reqId - 1);
  361. instances.sort((a, b) =>
  362. (a.instance_name || a.name || a.id).localeCompare(
  363. b.instance_name || b.name || b.id
  364. )
  365. );
  366. const count = instances.length;
  367. if (count === 0) {
  368. return;
  369. }
  370. this.loadingInstancesDetails = true;
  371. this.instancesDetails = [];
  372. this.instancesDetailsCount = count;
  373. this.instancesDetailsRemaining = count;
  374. await new Promise<void>(resolve => {
  375. Promise.all(
  376. instances.map(async instanceInfo => {
  377. try {
  378. const resp = await InstanceSource.loadInstanceDetails({
  379. endpointId,
  380. instanceId: instanceInfo.instance_name || instanceInfo.id,
  381. targetProvider,
  382. reqId: this.reqId,
  383. quietError,
  384. env,
  385. cache,
  386. skipLog,
  387. });
  388. const instance = resp.instance;
  389. if (resp.reqId !== this.reqId || !instance) {
  390. return;
  391. }
  392. runInAction(() => {
  393. this.instancesDetailsRemaining -= 1;
  394. this.loadingInstancesDetails = this.instancesDetailsRemaining > 0;
  395. if (this.instancesDetails.find(i => i.id === instance.id)) {
  396. this.instancesDetails = this.instancesDetails.filter(
  397. i => i.id !== instance.id
  398. );
  399. }
  400. this.instancesDetails = [...this.instancesDetails, instance];
  401. });
  402. if (this.instancesDetailsRemaining === 0) {
  403. this.instancesDetails = this.instancesDetails
  404. .slice()
  405. .sort((a, b) =>
  406. (a.instance_name || a.name).localeCompare(
  407. b.instance_name || b.name
  408. )
  409. );
  410. resolve();
  411. }
  412. } catch (err) {
  413. runInAction(() => {
  414. this.instancesDetailsRemaining -= 1;
  415. this.loadingInstancesDetails = this.instancesDetailsRemaining > 0;
  416. });
  417. if (!err || err.reqId !== this.reqId) {
  418. return;
  419. }
  420. if (count === 0) {
  421. resolve();
  422. }
  423. }
  424. })
  425. );
  426. });
  427. }
  428. @action clearInstancesDetails() {
  429. this.instancesDetails = [];
  430. this.loadingInstancesDetails = false;
  431. this.instancesDetailsCount = 0;
  432. this.instancesDetailsRemaining = 0;
  433. }
  434. }
  435. export default new InstanceStore();