InstanceStore.ts 14 KB

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