ProviderStore.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  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. if (!this.providers) {
  122. return []
  123. }
  124. const sortPriority = configLoader.config.providerSortPriority
  125. const array = Object.keys(this.providers).sort((a, b) => {
  126. const aTyped = a as ProviderTypes
  127. const bTyped = b as ProviderTypes
  128. if (sortPriority[aTyped] && sortPriority[bTyped]) {
  129. return (sortPriority[aTyped] - sortPriority[bTyped]) || a.localeCompare(b)
  130. }
  131. if (sortPriority[aTyped]) {
  132. return -1
  133. }
  134. if (sortPriority[bTyped]) {
  135. return 1
  136. }
  137. return a.localeCompare(b)
  138. }) as ProviderTypes[]
  139. return array
  140. }
  141. private async setRegions(regionsField: Field | undefined) {
  142. if (!regionsField) {
  143. return
  144. }
  145. await regionStore.getRegions()
  146. regionsField.enum = [...regionStore.regions]
  147. }
  148. @action async getConnectionInfoSchema(providerName: ProviderTypes): Promise<void> {
  149. this.connectionSchemaLoading = true
  150. try {
  151. const fields: Field[] = await ProviderSource.getConnectionInfoSchema(providerName)
  152. await this.setRegions(fields.find(f => f.name === 'mapped_regions'))
  153. runInAction(() => { this.connectionInfoSchema = fields })
  154. } finally {
  155. runInAction(() => { this.connectionSchemaLoading = false })
  156. }
  157. }
  158. @action clearConnectionInfoSchema() {
  159. this.connectionInfoSchema = []
  160. }
  161. @action async loadProviders(): Promise<void> {
  162. if (this.providers || this.providersLoading) {
  163. return
  164. }
  165. this.providersLoading = true
  166. try {
  167. const providers: Providers = await ProviderSource.loadProviders()
  168. runInAction(() => { this.providers = providers })
  169. } finally {
  170. runInAction(() => { this.providersLoading = false })
  171. }
  172. }
  173. loadOptionsSchemaLastReqId: string = ''
  174. loadOptionsSchemaLastDirection: 'source' | 'destination' | '' = ''
  175. @action async loadOptionsSchema(options: {
  176. providerName: ProviderTypes,
  177. optionsType: 'source' | 'destination',
  178. useCache?: boolean,
  179. quietError?: boolean,
  180. }): Promise<Field[]> {
  181. const {
  182. providerName, optionsType, useCache, quietError,
  183. } = options
  184. if (optionsType === 'source') {
  185. this.sourceSchemaLoading = true
  186. } else {
  187. this.destinationSchemaLoading = true
  188. }
  189. const reqId = providerName
  190. this.loadOptionsSchemaLastReqId = reqId
  191. this.loadOptionsSchemaLastDirection = optionsType
  192. const isValid = () => {
  193. const isSameRequest = this.loadOptionsSchemaLastReqId === reqId
  194. const isSameDirection = this.loadOptionsSchemaLastDirection === optionsType
  195. if (!isSameDirection) {
  196. return true
  197. }
  198. return isSameRequest
  199. }
  200. try {
  201. const fields: Field[] = await ProviderSource.loadOptionsSchema(providerName, optionsType, useCache, quietError)
  202. this.loadOptionsSchemaSuccess(fields, optionsType, isValid())
  203. return fields
  204. } finally {
  205. this.loadOptionsSchemaDone(optionsType, isValid())
  206. }
  207. }
  208. @action loadOptionsSchemaSuccess(
  209. fields: Field[],
  210. optionsType: 'source' | 'destination',
  211. isValid: boolean,
  212. ) {
  213. if (!isValid) {
  214. return
  215. }
  216. if (optionsType === 'source') {
  217. this.sourceSchema = fields
  218. } else {
  219. this.destinationSchema = fields
  220. }
  221. }
  222. @action loadOptionsSchemaDone(optionsType: 'source' | 'destination', isValid: boolean) {
  223. if (!isValid) {
  224. return
  225. }
  226. if (optionsType === 'source') {
  227. this.sourceSchemaLoading = false
  228. } else {
  229. this.destinationSchemaLoading = false
  230. }
  231. }
  232. getOptionsValuesLastReqId: string = ''
  233. getOptionsValuesLastDirection: 'source' | 'destination' | '' = ''
  234. async getOptionsValues(config: {
  235. optionsType: 'source' | 'destination',
  236. endpointId: string,
  237. providerName: ProviderTypes,
  238. envData?: { [prop: string]: any } | null,
  239. useCache?: boolean,
  240. quietError?: boolean,
  241. allowMultiple?: boolean,
  242. }): Promise<OptionValues[]> {
  243. const {
  244. providerName, optionsType, endpointId, envData, useCache, quietError, allowMultiple,
  245. } = config
  246. const providerType = optionsType === 'source' ? providerTypes.SOURCE_OPTIONS : providerTypes.DESTINATION_OPTIONS
  247. await this.loadProviders()
  248. if (!this.providers) {
  249. return []
  250. }
  251. const providerWithExtraOptions = this.providers[providerName]
  252. .types.find(t => t === providerType)
  253. if (!providerWithExtraOptions) {
  254. return []
  255. }
  256. let canceled = false
  257. if (!allowMultiple) {
  258. apiCaller.cancelRequests(endpointId)
  259. }
  260. this.getOptionsValuesStart(optionsType, !envData)
  261. const reqId = `${endpointId}-${providerType}`
  262. this.getOptionsValuesLastReqId = reqId
  263. this.getOptionsValuesLastDirection = optionsType
  264. const isValid = () => {
  265. const isSameRequest = this.getOptionsValuesLastReqId === reqId
  266. const isSameDirection = this.getOptionsValuesLastDirection === optionsType
  267. if (!isSameDirection) {
  268. return true
  269. }
  270. return isSameRequest
  271. }
  272. try {
  273. const options = await ProviderSource
  274. .getOptionsValues(optionsType, endpointId, envData, useCache, quietError)
  275. this.getOptionsValuesSuccess(
  276. optionsType,
  277. providerName,
  278. options,
  279. isValid(),
  280. )
  281. return options
  282. } catch (err) {
  283. console.error(err)
  284. canceled = err ? err.canceled : false
  285. if (canceled) {
  286. return optionsType === 'source' ? [...this.sourceOptions] : [...this.destinationOptions]
  287. }
  288. throw err
  289. } finally {
  290. if (!canceled) {
  291. this.getOptionsValuesDone(
  292. optionsType,
  293. !envData,
  294. isValid(),
  295. )
  296. }
  297. }
  298. }
  299. @action getOptionsValuesStart(optionsType: 'source' | 'destination', isPrimary: boolean) {
  300. if (optionsType === 'source') {
  301. if (isPrimary) {
  302. this.sourceOptions = []
  303. this.sourceOptionsPrimaryLoading = true
  304. this.sourceOptionsSecondaryLoading = false
  305. } else {
  306. this.sourceOptionsPrimaryLoading = false
  307. this.sourceOptionsSecondaryLoading = true
  308. }
  309. } else if (isPrimary) {
  310. this.destinationOptions = []
  311. this.destinationOptionsPrimaryLoading = true
  312. this.destinationOptionsSecondaryLoading = false
  313. } else {
  314. this.destinationOptionsPrimaryLoading = false
  315. this.destinationOptionsSecondaryLoading = true
  316. }
  317. }
  318. @action getOptionsValuesDone(
  319. optionsType: 'source' | 'destination',
  320. isPrimary: boolean,
  321. isValid: boolean,
  322. ) {
  323. if (!isValid) {
  324. return
  325. }
  326. if (optionsType === 'source') {
  327. if (isPrimary) {
  328. this.sourceOptionsPrimaryLoading = false
  329. } else {
  330. this.sourceOptionsSecondaryLoading = false
  331. }
  332. } else if (isPrimary) {
  333. this.destinationOptionsPrimaryLoading = false
  334. } else {
  335. this.destinationOptionsSecondaryLoading = false
  336. }
  337. }
  338. @action getOptionsValuesSuccess(
  339. optionsType: 'source' | 'destination',
  340. provider: ProviderTypes,
  341. options: OptionValues[],
  342. isValid: boolean,
  343. ) {
  344. if (!isValid) {
  345. return
  346. }
  347. const schema = optionsType === 'source' ? this.sourceSchema : this.destinationSchema
  348. schema.forEach(field => {
  349. const parser = OptionsSchemaPlugin.for(provider)
  350. parser.fillFieldValues(field, options)
  351. })
  352. if (optionsType === 'source') {
  353. this.sourceSchema = [...schema]
  354. this.sourceOptions = options
  355. } else {
  356. this.destinationSchema = [...schema]
  357. this.destinationOptions = options
  358. }
  359. }
  360. }
  361. export default new ProviderStore()