ProviderStore.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411
  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 {
  15. observable, action, computed, runInAction,
  16. } from 'mobx'
  17. import ProviderSource from '../sources/ProviderSource'
  18. import apiCaller from '../utils/ApiCaller'
  19. import configLoader from '../utils/Config'
  20. import { providerTypes } from '../constants'
  21. import { OptionsSchemaPlugin } from '../plugins/endpoint'
  22. import type { OptionValues } from '../@types/Endpoint'
  23. import type { Field } from '../@types/Field'
  24. import type { Providers, ProviderTypes } from '../@types/Providers'
  25. import regionStore from './RegionStore'
  26. export const getFieldChangeOptions = (config: {
  27. providerName: string | null,
  28. schema: Field[],
  29. data: any,
  30. field: Field | null,
  31. type: 'source' | 'destination',
  32. }) => {
  33. const {
  34. providerName, schema, data, field, type,
  35. } = config
  36. const providerWithEnvOptions = configLoader.config.extraOptionsApiCalls
  37. .find(p => p.name === providerName && p.types.find(t => t === type))
  38. if (!providerName || !providerWithEnvOptions) {
  39. return null
  40. }
  41. const requiredFields = providerWithEnvOptions.requiredFields
  42. const requiredValues = providerWithEnvOptions.requiredValues
  43. const relistFields = providerWithEnvOptions.relistFields
  44. const findFieldInSchema = (name: string) => schema.find(f => f.name === name)
  45. const filterValidField = (fn: string) => {
  46. const schemaField = findFieldInSchema(fn)
  47. if (data) {
  48. // This is for 'list_all_networks' field, which requires options calls after each value change
  49. // @TODO: refactor to use `relistFields` option
  50. if (schemaField && schemaField.type === 'boolean') {
  51. return true
  52. }
  53. if (data[fn] === null) {
  54. return false
  55. }
  56. const defaultValue = data[fn] === undefined && schemaField && schemaField.default
  57. const requiredValue = requiredValues?.find(f => f.field === fn)
  58. if (defaultValue != null) {
  59. if (requiredValue) {
  60. return Boolean(requiredValue.values.find(v => v === defaultValue))
  61. }
  62. return true
  63. }
  64. if (requiredValue) {
  65. return Boolean(requiredValue.values.find(v => v === data[fn]))
  66. }
  67. return data[fn]
  68. }
  69. return false
  70. }
  71. const requiredValidFields = requiredFields.filter(filterValidField)
  72. const relistValidFields = relistFields?.filter(filterValidField)
  73. const relistField = relistFields?.find(fn => fn === field?.name)
  74. const isCurrentFieldValid = field ? (
  75. requiredValidFields.find(fn => fn === field.name)
  76. || relistField
  77. ) : true
  78. if (requiredValidFields.length !== requiredFields.length || !isCurrentFieldValid) {
  79. return null
  80. }
  81. const envData: any = {}
  82. const setEnvDataValue = (fn: string) => {
  83. envData[fn] = data ? data[fn] : null
  84. if (envData[fn] == null) {
  85. const schemaField = findFieldInSchema(fn)
  86. if (schemaField && schemaField.default) {
  87. envData[fn] = schemaField.default
  88. }
  89. }
  90. }
  91. requiredValidFields.forEach(fn => {
  92. setEnvDataValue(fn)
  93. })
  94. relistValidFields?.forEach(fn => {
  95. setEnvDataValue(fn)
  96. })
  97. return envData
  98. }
  99. class ProviderStore {
  100. @observable connectionInfoSchema: Field[] = []
  101. @observable connectionSchemaLoading: boolean = false
  102. @observable providers: Providers | null = null
  103. @observable providersLoading: boolean = false
  104. @observable destinationSchema: Field[] = []
  105. @observable destinationSchemaLoading: boolean = false
  106. @observable destinationOptions: OptionValues[] = []
  107. // Set to true while loading the options call for the first set of options
  108. @observable destinationOptionsPrimaryLoading: boolean = false
  109. // Set to true while loading the options call with a set of values in the 'env' parameter
  110. @observable destinationOptionsSecondaryLoading: boolean = false
  111. @observable sourceOptions: OptionValues[] = []
  112. // Set to true while loading the options call for the first set of options
  113. @observable sourceOptionsPrimaryLoading: boolean = false
  114. // Set to true while loading the options call with a set of values in the 'env' parameter
  115. @observable sourceOptionsSecondaryLoading: boolean = false
  116. @observable sourceSchema: Field[] = []
  117. @observable sourceSchemaLoading: boolean = false
  118. lastDestinationSchemaType: 'replica' | 'migration' = 'replica'
  119. @computed
  120. get providerNames(): ProviderTypes[] {
  121. const sortPriority = configLoader.config.providerSortPriority
  122. const array: any[] = Object.keys(this.providers || {}).sort((a, b) => {
  123. if (sortPriority[a] && sortPriority[b]) {
  124. return (sortPriority[a] - sortPriority[b]) || a.localeCompare(b)
  125. }
  126. if (sortPriority[a]) {
  127. return -1
  128. }
  129. if (sortPriority[b]) {
  130. return 1
  131. }
  132. return a.localeCompare(b)
  133. })
  134. return array
  135. }
  136. private async setRegions(regionsField: Field | undefined) {
  137. if (!regionsField) {
  138. return
  139. }
  140. await regionStore.getRegions()
  141. regionsField.enum = [...regionStore.regions]
  142. }
  143. @action async getConnectionInfoSchema(providerName: ProviderTypes): Promise<void> {
  144. this.connectionSchemaLoading = true
  145. try {
  146. const fields: Field[] = await ProviderSource.getConnectionInfoSchema(providerName)
  147. await this.setRegions(fields.find(f => f.name === 'mapped_regions'))
  148. runInAction(() => { this.connectionInfoSchema = fields })
  149. } finally {
  150. runInAction(() => { this.connectionSchemaLoading = false })
  151. }
  152. }
  153. @action clearConnectionInfoSchema() {
  154. this.connectionInfoSchema = []
  155. }
  156. @action async loadProviders(): Promise<void> {
  157. if (this.providers || this.providersLoading) {
  158. return
  159. }
  160. this.providersLoading = true
  161. try {
  162. const providers: Providers = await ProviderSource.loadProviders()
  163. runInAction(() => { this.providers = providers })
  164. } finally {
  165. runInAction(() => { this.providersLoading = false })
  166. }
  167. }
  168. loadOptionsSchemaLastReqId: string = ''
  169. loadOptionsSchemaLastDirection: 'source' | 'destination' | '' = ''
  170. @action async loadOptionsSchema(options: {
  171. providerName: ProviderTypes,
  172. optionsType: 'source' | 'destination',
  173. useCache?: boolean,
  174. quietError?: boolean,
  175. }): Promise<Field[]> {
  176. const {
  177. providerName, optionsType, useCache, quietError,
  178. } = options
  179. if (optionsType === 'source') {
  180. this.sourceSchemaLoading = true
  181. } else {
  182. this.destinationSchemaLoading = true
  183. }
  184. const reqId = providerName
  185. this.loadOptionsSchemaLastReqId = reqId
  186. this.loadOptionsSchemaLastDirection = optionsType
  187. const isValid = () => {
  188. const isSameRequest = this.loadOptionsSchemaLastReqId === reqId
  189. const isSameDirection = this.loadOptionsSchemaLastDirection === optionsType
  190. if (!isSameDirection) {
  191. return true
  192. }
  193. return isSameRequest
  194. }
  195. try {
  196. const fields: Field[] = await ProviderSource.loadOptionsSchema(providerName, optionsType, useCache, quietError)
  197. this.loadOptionsSchemaSuccess(fields, optionsType, isValid())
  198. return fields
  199. } finally {
  200. this.loadOptionsSchemaDone(optionsType, isValid())
  201. }
  202. }
  203. @action loadOptionsSchemaSuccess(
  204. fields: Field[],
  205. optionsType: 'source' | 'destination',
  206. isValid: boolean,
  207. ) {
  208. if (!isValid) {
  209. return
  210. }
  211. if (optionsType === 'source') {
  212. this.sourceSchema = fields
  213. } else {
  214. this.destinationSchema = fields
  215. }
  216. }
  217. @action loadOptionsSchemaDone(optionsType: 'source' | 'destination', isValid: boolean) {
  218. if (!isValid) {
  219. return
  220. }
  221. if (optionsType === 'source') {
  222. this.sourceSchemaLoading = false
  223. } else {
  224. this.destinationSchemaLoading = false
  225. }
  226. }
  227. getOptionsValuesLastReqId: string = ''
  228. getOptionsValuesLastDirection: 'source' | 'destination' | '' = ''
  229. async getOptionsValues(config: {
  230. optionsType: 'source' | 'destination',
  231. endpointId: string,
  232. providerName: ProviderTypes,
  233. envData?: { [prop: string]: any } | null,
  234. useCache?: boolean,
  235. quietError?: boolean,
  236. allowMultiple?: boolean,
  237. }): Promise<OptionValues[]> {
  238. const {
  239. providerName, optionsType, endpointId, envData, useCache, quietError, allowMultiple,
  240. } = config
  241. const providerType = optionsType === 'source' ? providerTypes.SOURCE_OPTIONS : providerTypes.DESTINATION_OPTIONS
  242. await this.loadProviders()
  243. if (!this.providers) {
  244. return []
  245. }
  246. const providerWithExtraOptions = this.providers[providerName]
  247. .types.find(t => t === providerType)
  248. if (!providerWithExtraOptions) {
  249. return []
  250. }
  251. let canceled = false
  252. if (!allowMultiple) {
  253. apiCaller.cancelRequests(endpointId)
  254. }
  255. this.getOptionsValuesStart(optionsType, !envData)
  256. const reqId = `${endpointId}-${providerType}`
  257. this.getOptionsValuesLastReqId = reqId
  258. this.getOptionsValuesLastDirection = optionsType
  259. const isValid = () => {
  260. const isSameRequest = this.getOptionsValuesLastReqId === reqId
  261. const isSameDirection = this.getOptionsValuesLastDirection === optionsType
  262. if (!isSameDirection) {
  263. return true
  264. }
  265. return isSameRequest
  266. }
  267. try {
  268. const options = await ProviderSource
  269. .getOptionsValues(optionsType, endpointId, envData, useCache, quietError)
  270. this.getOptionsValuesSuccess(
  271. optionsType,
  272. providerName,
  273. options,
  274. isValid(),
  275. )
  276. return options
  277. } catch (err) {
  278. console.error(err)
  279. canceled = err ? err.canceled : false
  280. if (canceled) {
  281. return optionsType === 'source' ? [...this.sourceOptions] : [...this.destinationOptions]
  282. }
  283. throw err
  284. } finally {
  285. if (!canceled) {
  286. this.getOptionsValuesDone(
  287. optionsType,
  288. !envData,
  289. isValid(),
  290. )
  291. }
  292. }
  293. }
  294. @action getOptionsValuesStart(optionsType: 'source' | 'destination', isPrimary: boolean) {
  295. if (optionsType === 'source') {
  296. if (isPrimary) {
  297. this.sourceOptions = []
  298. this.sourceOptionsPrimaryLoading = true
  299. this.sourceOptionsSecondaryLoading = false
  300. } else {
  301. this.sourceOptionsPrimaryLoading = false
  302. this.sourceOptionsSecondaryLoading = true
  303. }
  304. } else if (isPrimary) {
  305. this.destinationOptions = []
  306. this.destinationOptionsPrimaryLoading = true
  307. this.destinationOptionsSecondaryLoading = false
  308. } else {
  309. this.destinationOptionsPrimaryLoading = false
  310. this.destinationOptionsSecondaryLoading = true
  311. }
  312. }
  313. @action getOptionsValuesDone(
  314. optionsType: 'source' | 'destination',
  315. isPrimary: boolean,
  316. isValid: boolean,
  317. ) {
  318. if (!isValid) {
  319. return
  320. }
  321. if (optionsType === 'source') {
  322. if (isPrimary) {
  323. this.sourceOptionsPrimaryLoading = false
  324. } else {
  325. this.sourceOptionsSecondaryLoading = false
  326. }
  327. } else if (isPrimary) {
  328. this.destinationOptionsPrimaryLoading = false
  329. } else {
  330. this.destinationOptionsSecondaryLoading = false
  331. }
  332. }
  333. @action getOptionsValuesSuccess(
  334. optionsType: 'source' | 'destination',
  335. provider: ProviderTypes,
  336. options: OptionValues[],
  337. isValid: boolean,
  338. ) {
  339. if (!isValid) {
  340. return
  341. }
  342. const schema = optionsType === 'source' ? this.sourceSchema : this.destinationSchema
  343. schema.forEach(field => {
  344. const parser = OptionsSchemaPlugin.for(provider)
  345. parser.fillFieldValues(field, options)
  346. })
  347. if (optionsType === 'source') {
  348. this.sourceSchema = [...schema]
  349. this.sourceOptions = options
  350. } else {
  351. this.destinationSchema = [...schema]
  352. this.destinationOptions = options
  353. }
  354. }
  355. }
  356. export default new ProviderStore()