/* Copyright (C) 2017 Cloudbase Solutions SRL This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ import { observable, action, computed, runInAction, } from 'mobx' import ProviderSource from '../sources/ProviderSource' import apiCaller from '../utils/ApiCaller' import configLoader from '../utils/Config' import { providerTypes } from '../constants' import { OptionsSchemaPlugin } from '../plugins/endpoint' import type { OptionValues } from '../@types/Endpoint' import type { Field } from '../@types/Field' import type { Providers, ProviderTypes } from '../@types/Providers' import regionStore from './RegionStore' export const getFieldChangeOptions = (config: { providerName: string | null, schema: Field[], data: any, field: Field | null, type: 'source' | 'destination', }) => { const { providerName, schema, data, field, type, } = config const providerWithEnvOptions = configLoader.config.extraOptionsApiCalls .find(p => p.name === providerName && p.types.find(t => t === type)) if (!providerName || !providerWithEnvOptions) { return null } const requiredFields = providerWithEnvOptions.requiredFields const requiredValues = providerWithEnvOptions.requiredValues const relistFields = providerWithEnvOptions.relistFields const findFieldInSchema = (name: string) => schema.find(f => f.name === name) const filterValidField = (fn: string) => { const schemaField = findFieldInSchema(fn) if (data) { // This is for 'list_all_networks' field, which requires options calls after each value change // @TODO: refactor to use `relistFields` option if (schemaField && schemaField.type === 'boolean') { return true } if (data[fn] === null) { return false } const defaultValue = data[fn] === undefined && schemaField && schemaField.default const requiredValue = requiredValues?.find(f => f.field === fn) if (defaultValue != null) { if (requiredValue) { return Boolean(requiredValue.values.find(v => v === defaultValue)) } return true } if (requiredValue) { return Boolean(requiredValue.values.find(v => v === data[fn])) } return data[fn] } return false } const requiredValidFields = requiredFields.filter(filterValidField) const relistValidFields = relistFields?.filter(filterValidField) const relistField = relistFields?.find(fn => fn === field?.name) const isCurrentFieldValid = field ? ( requiredValidFields.find(fn => fn === field.name) || relistField ) : true if (requiredValidFields.length !== requiredFields.length || !isCurrentFieldValid) { return null } const envData: any = {} const setEnvDataValue = (fn: string) => { envData[fn] = data ? data[fn] : null if (envData[fn] == null) { const schemaField = findFieldInSchema(fn) if (schemaField && schemaField.default) { envData[fn] = schemaField.default } } } requiredValidFields.forEach(fn => { setEnvDataValue(fn) }) relistValidFields?.forEach(fn => { setEnvDataValue(fn) }) return envData } class ProviderStore { @observable connectionInfoSchema: Field[] = [] @observable connectionSchemaLoading: boolean = false @observable providers: Providers | null = null @observable providersLoading: boolean = false @observable destinationSchema: Field[] = [] @observable destinationSchemaLoading: boolean = false @observable destinationOptions: OptionValues[] = [] // Set to true while loading the options call for the first set of options @observable destinationOptionsPrimaryLoading: boolean = false // Set to true while loading the options call with a set of values in the 'env' parameter @observable destinationOptionsSecondaryLoading: boolean = false @observable sourceOptions: OptionValues[] = [] // Set to true while loading the options call for the first set of options @observable sourceOptionsPrimaryLoading: boolean = false // Set to true while loading the options call with a set of values in the 'env' parameter @observable sourceOptionsSecondaryLoading: boolean = false @observable sourceSchema: Field[] = [] @observable sourceSchemaLoading: boolean = false lastDestinationSchemaType: 'replica' | 'migration' = 'replica' @computed get providerNames(): ProviderTypes[] { const sortPriority = configLoader.config.providerSortPriority const array: any[] = Object.keys(this.providers || {}).sort((a, b) => { if (sortPriority[a] && sortPriority[b]) { return (sortPriority[a] - sortPriority[b]) || a.localeCompare(b) } if (sortPriority[a]) { return -1 } if (sortPriority[b]) { return 1 } return a.localeCompare(b) }) return array } private async setRegions(regionsField: Field | undefined) { if (!regionsField) { return } await regionStore.getRegions() regionsField.enum = [...regionStore.regions] } @action async getConnectionInfoSchema(providerName: ProviderTypes): Promise { this.connectionSchemaLoading = true try { const fields: Field[] = await ProviderSource.getConnectionInfoSchema(providerName) await this.setRegions(fields.find(f => f.name === 'mapped_regions')) runInAction(() => { this.connectionInfoSchema = fields }) } finally { runInAction(() => { this.connectionSchemaLoading = false }) } } @action clearConnectionInfoSchema() { this.connectionInfoSchema = [] } @action async loadProviders(): Promise { if (this.providers || this.providersLoading) { return } this.providersLoading = true try { const providers: Providers = await ProviderSource.loadProviders() runInAction(() => { this.providers = providers }) } finally { runInAction(() => { this.providersLoading = false }) } } loadOptionsSchemaLastReqId: string = '' loadOptionsSchemaLastDirection: 'source' | 'destination' | '' = '' @action async loadOptionsSchema(options: { providerName: ProviderTypes, optionsType: 'source' | 'destination', useCache?: boolean, quietError?: boolean, }): Promise { const { providerName, optionsType, useCache, quietError, } = options if (optionsType === 'source') { this.sourceSchemaLoading = true } else { this.destinationSchemaLoading = true } const reqId = providerName this.loadOptionsSchemaLastReqId = reqId this.loadOptionsSchemaLastDirection = optionsType const isValid = () => { const isSameRequest = this.loadOptionsSchemaLastReqId === reqId const isSameDirection = this.loadOptionsSchemaLastDirection === optionsType if (!isSameDirection) { return true } return isSameRequest } try { const fields: Field[] = await ProviderSource.loadOptionsSchema(providerName, optionsType, useCache, quietError) this.loadOptionsSchemaSuccess(fields, optionsType, isValid()) return fields } finally { this.loadOptionsSchemaDone(optionsType, isValid()) } } @action loadOptionsSchemaSuccess( fields: Field[], optionsType: 'source' | 'destination', isValid: boolean, ) { if (!isValid) { return } if (optionsType === 'source') { this.sourceSchema = fields } else { this.destinationSchema = fields } } @action loadOptionsSchemaDone(optionsType: 'source' | 'destination', isValid: boolean) { if (!isValid) { return } if (optionsType === 'source') { this.sourceSchemaLoading = false } else { this.destinationSchemaLoading = false } } getOptionsValuesLastReqId: string = '' getOptionsValuesLastDirection: 'source' | 'destination' | '' = '' async getOptionsValues(config: { optionsType: 'source' | 'destination', endpointId: string, providerName: ProviderTypes, envData?: { [prop: string]: any } | null, useCache?: boolean, quietError?: boolean, allowMultiple?: boolean, }): Promise { const { providerName, optionsType, endpointId, envData, useCache, quietError, allowMultiple, } = config const providerType = optionsType === 'source' ? providerTypes.SOURCE_OPTIONS : providerTypes.DESTINATION_OPTIONS await this.loadProviders() if (!this.providers) { return [] } const providerWithExtraOptions = this.providers[providerName] .types.find(t => t === providerType) if (!providerWithExtraOptions) { return [] } let canceled = false if (!allowMultiple) { apiCaller.cancelRequests(endpointId) } this.getOptionsValuesStart(optionsType, !envData) const reqId = `${endpointId}-${providerType}` this.getOptionsValuesLastReqId = reqId this.getOptionsValuesLastDirection = optionsType const isValid = () => { const isSameRequest = this.getOptionsValuesLastReqId === reqId const isSameDirection = this.getOptionsValuesLastDirection === optionsType if (!isSameDirection) { return true } return isSameRequest } try { const options = await ProviderSource .getOptionsValues(optionsType, endpointId, envData, useCache, quietError) this.getOptionsValuesSuccess( optionsType, providerName, options, isValid(), ) return options } catch (err) { console.error(err) canceled = err ? err.canceled : false if (canceled) { return optionsType === 'source' ? [...this.sourceOptions] : [...this.destinationOptions] } throw err } finally { if (!canceled) { this.getOptionsValuesDone( optionsType, !envData, isValid(), ) } } } @action getOptionsValuesStart(optionsType: 'source' | 'destination', isPrimary: boolean) { if (optionsType === 'source') { if (isPrimary) { this.sourceOptions = [] this.sourceOptionsPrimaryLoading = true this.sourceOptionsSecondaryLoading = false } else { this.sourceOptionsPrimaryLoading = false this.sourceOptionsSecondaryLoading = true } } else if (isPrimary) { this.destinationOptions = [] this.destinationOptionsPrimaryLoading = true this.destinationOptionsSecondaryLoading = false } else { this.destinationOptionsPrimaryLoading = false this.destinationOptionsSecondaryLoading = true } } @action getOptionsValuesDone( optionsType: 'source' | 'destination', isPrimary: boolean, isValid: boolean, ) { if (!isValid) { return } if (optionsType === 'source') { if (isPrimary) { this.sourceOptionsPrimaryLoading = false } else { this.sourceOptionsSecondaryLoading = false } } else if (isPrimary) { this.destinationOptionsPrimaryLoading = false } else { this.destinationOptionsSecondaryLoading = false } } @action getOptionsValuesSuccess( optionsType: 'source' | 'destination', provider: ProviderTypes, options: OptionValues[], isValid: boolean, ) { if (!isValid) { return } const schema = optionsType === 'source' ? this.sourceSchema : this.destinationSchema schema.forEach(field => { const parser = OptionsSchemaPlugin.for(provider) parser.fillFieldValues(field, options) }) if (optionsType === 'source') { this.sourceSchema = [...schema] this.sourceOptions = options } else { this.destinationSchema = [...schema] this.destinationOptions = options } } } export default new ProviderStore()