import { Component , Injector , Input , OnDestroy } from '@angular/core' ; import { FormBuilder , FormGroup } from '@angular/forms' ; import { TranslateService } from '@ngx-translate/core' ; import { Observable } from 'rxjs/Observable' ; import { AiService } from '../../../shared/services/ai.service' ; import { PortalService } from '../../../shared/services/portal.service' ; import { InfoBoxType } from './../../../controls/info-box/info-box.component' ; import { FeatureComponent } from './../../../shared/components/feature-component' ; import { FunctionAppContext } from './../../../shared/function-app-context' ; import { ApplicationSettings } from './../../../shared/models/arm/application-settings' ; import { ArmObj , ExtendedLocation , ResourceId } from './../../../shared/models/arm/arm-obj' ; import { CreateSlotRequest , Site } from './../../../shared/models/arm/site' ; import { Constants , FunctionAppVersion , LogCategories , ScenarioIds , SiteTabIds , SlotOperationState , } from './../../../shared/models/constants' ; import { DropDownElement } from './../../../shared/models/drop-down-element' ; import { errorIds } from './../../../shared/models/error-ids' ; import { HttpResult } from './../../../shared/models/http-result' ; import { BroadcastMessageId } from './../../../shared/models/portal' ; import { PortalResources } from './../../../shared/models/portal-resources' ; import { SlotNewInfo } from './../../../shared/models/slot-events' ; import { ArmSiteDescriptor } from './../../../shared/resourceDescriptors' ; import { AuthzService } from './../../../shared/services/authz.service' ; import { FunctionAppService } from './../../../shared/services/function-app.service' ; import { LogService } from './../../../shared/services/log.service' ; import { ScenarioService } from './../../../shared/services/scenario/scenario.service' ; import { SiteService } from './../../../shared/services/site.service' ; import { ArmUtil } from './../../../shared/Utilities/arm-utils' ; import { RequiredValidator } from './../../../shared/validators/requiredValidator' ; import { SlotNameValidator } from './../../../shared/validators/slotNameValidator' ; import { CloneSrcValidator } from './cloneSrcValidator' ; @ Component ( { selector : 'add-slot' , templateUrl : './add-slot.component.html' , styleUrls : [ './../common.scss' , './add-slot.component.scss' ] , } ) export class AddSlotComponent extends FeatureComponent < ResourceId > implements OnDestroy { @ Input ( ) set resourceIdInput ( resourceId ) { this . setInput ( resourceId ) ; } @ Input ( ) showHeader = false ; public unsavedChangesWarning : string ; public operationInProgressWarning : string ; public addForm : FormGroup ; public hasCreateAccess = false ; public slotsQuotaMessage : string ; public isLoading = true ; public loadingFailed = false ; public loadingSiteFailureMessage = '' ; public loadingSlotsFailureMessage = '' ; public loadingAppSettingsFailureMessage = '' ; public slotPrefix = '' ; public slotSuffix = '' ; public cloneSrcIdDropDownOptions : DropDownElement < string > [ ] ; public isCreating = false ; public executeButtonDisabled = false ; public slotOptInNeeded = false ; public slotOptInEnabled = false ; public isFunctionApp = false ; public progressMessage : string ; public progressMessageClass : InfoBoxType = 'info' ; private _siteId : string ; private _slotsArm : ArmObj < Site > [ ] ; private _functionAppContext : FunctionAppContext ; private _isKubeApp = false ; private _customLocationId = '' ; constructor ( private _fb , private _siteService , private _translateService , private _aiService , private _logService , private _portalService , private _authZService , private _scenarioService , private _functionAppService , private _injector ) { super ( 'AddSlotComponent' , _injector , SiteTabIds . deploymentSlotsCreate ) ; this . featureName = 'addslot' ; this . isParentComponent = true ; this . unsavedChangesWarning = this . _translateService . instant ( PortalResources . unsavedChangesWarning ) ; this . operationInProgressWarning = this . _translateService . instant ( PortalResources . slotCreateOperationInProgressWarning ) ; const nameCtrl = this . _fb . control ( { value : null , disabled : true } ) ; const cloneSrcIdCtrl = this . _fb . control ( { value : null , disabled : true } ) ; const cloneSrcConfigCtrl = this . _fb . control ( { value : null , disabled : true } ) ; this . addForm = this . _fb . group ( { name : nameCtrl , cloneSrcId : cloneSrcIdCtrl , cloneSrcConfig : cloneSrcConfigCtrl , } ) ; } protected setup ( inputEvents ) { return inputEvents . distinctUntilChanged ( ) . switchMap ( resourceId => { this . hasCreateAccess = false ; this . slotsQuotaMessage = null ; this . isLoading = true ; this . loadingFailed = false ; this . loadingSiteFailureMessage = '' ; this . loadingSlotsFailureMessage = '' ; this . loadingAppSettingsFailureMessage = '' ; this . cloneSrcIdDropDownOptions = null ; this . isCreating = false ; this . executeButtonDisabled = false ; this . slotOptInNeeded = false ; this . slotOptInEnabled = false ; this . isFunctionApp = false ; this . _isKubeApp = false ; this . _customLocationId = '' ; this . progressMessage = null ; this . progressMessageClass = 'info' ; this . _slotsArm = null ; const siteDescriptor = new ArmSiteDescriptor ( resourceId ) ; this . _siteId = siteDescriptor . getSiteOnlyResourceId ( ) ; this . _functionAppContext = null ; return Observable . zip ( this . _siteService . getSite ( this . _siteId ) , this . _siteService . getSlots ( this . _siteId ) , this . _siteService . getAppSettings ( this . _siteId ) , this . _authZService . hasPermission ( this . _siteId , [ AuthzService . writeScope ] ) , this . _authZService . hasReadOnlyLock ( this . _siteId ) ) ; } ) . mergeMap ( r => { const [ siteResult , slotsResult , appSettingsResult , hasWritePermission , hasReadOnlyLock ] = r ; this . hasCreateAccess = hasWritePermission && ! hasReadOnlyLock ; if ( ! siteResult . isSuccessful ) { this . _logService . error ( LogCategories . addSlot , '/add-slot' , siteResult . error . result ) ; this . loadingFailed = true ; this . loadingSiteFailureMessage = this . _translateService . instant ( PortalResources . error_unableToLoadSite , { errorMessage : ( siteResult . error && siteResult . error . message ) || '' , } ) ; } else { const site = siteResult . result ; this . isFunctionApp = ArmUtil . isFunctionApp ( site ) ; this . _isKubeApp = ArmUtil . isKubeApp ( site ) ; this . _customLocationId = ! ! site . extendedLocation && site . extendedLocation . name ; } if ( ! slotsResult . isSuccessful ) { this . _logService . error ( LogCategories . addSlot , '/add-slot' , slotsResult . error . result ) ; this . loadingFailed = true ; this . loadingSlotsFailureMessage = this . _translateService . instant ( PortalResources . error_unableToLoadSlotsList , { errorMessage : ( slotsResult . error && slotsResult . error . message ) || '' , } ) ; } if ( ! this . loadingFailed ) { this . _slotsArm = slotsResult . result . value ; this . _slotsArm . unshift ( siteResult . result ) ; if ( this . isFunctionApp ) { this . _setFunctionAppContext ( siteResult , appSettingsResult ) ; if ( this . _slotsArm . length > 1 ) { this . slotOptInNeeded = false ; } else if ( appSettingsResult . isSuccessful ) { this . slotOptInNeeded = ! this . _functionAppService . isSlotsSupported ( appSettingsResult . result ) ; } else { this . slotOptInNeeded = false ; this . loadingFailed = true ; this . loadingAppSettingsFailureMessage = this . _translateService . instant ( PortalResources . error_unableToLoadConfig , { errorMessage : ( appSettingsResult . error && appSettingsResult . error . message ) || '' , } ) ; } this . executeButtonDisabled = this . slotOptInNeeded && ! this . slotOptInEnabled ; } } if ( ! this . loadingFailed ) { return this . _scenarioService . checkScenarioAsync ( ScenarioIds . getSiteSlotLimits , { site : siteResult . result } ) ; } else { return Observable . of ( null ) ; } } ) . do ( r => { if ( r && r . status === 'enabled' ) { const slotsQuota = r . data ; if ( this . _slotsArm && this . _slotsArm . length >= slotsQuota ) { this . slotsQuotaMessage = this . _translateService . instant ( PortalResources . slotNew_quotaReached , { quota : slotsQuota } ) ; } } this . _setupForm ( ) ; this . isLoading = false ; } ) ; } private _setFunctionAppContext ( siteResult , appSettingsResult ) { this . _functionAppContext = ArmUtil . mapArmSiteToContext ( siteResult . result , this . _injector ) ; if ( appSettingsResult . isSuccessful ) { this . _functionAppContext . urlTemplates . runtimeVersion = appSettingsResult . result . properties [ Constants . runtimeVersionAppSettingName ] || FunctionAppVersion . v3 ; } } private _setupForm ( ) { const nameCtrl = this . addForm . get ( 'name' ) ; const cloneSrcIdCtrl = this . addForm . get ( 'cloneSrcId' ) ; const cloneSrcConfigCtrl = this . addForm . get ( 'cloneSrcConfig' ) ; if ( ! this . hasCreateAccess || ! ! this . slotsQuotaMessage || this . loadingFailed || ( this . slotOptInNeeded && ! this . slotOptInEnabled ) ) { nameCtrl . clearValidators ( ) ; nameCtrl . clearAsyncValidators ( ) ; nameCtrl . setValue ( null ) ; nameCtrl . disable ( ) ; cloneSrcIdCtrl . clearValidators ( ) ; cloneSrcIdCtrl . clearAsyncValidators ( ) ; cloneSrcIdCtrl . setValue ( null ) ; cloneSrcIdCtrl . disable ( ) ; cloneSrcConfigCtrl . clearValidators ( ) ; cloneSrcConfigCtrl . clearAsyncValidators ( ) ; cloneSrcConfigCtrl . setValue ( null ) ; cloneSrcConfigCtrl . disable ( ) ; } else { const requiredValidator = new RequiredValidator ( this . _translateService ) ; const slotNameValidator = new SlotNameValidator ( this . _injector , this . _siteId ) ; nameCtrl . enable ( ) ; nameCtrl . setValue ( null ) ; nameCtrl . setValidators ( requiredValidator . validate . bind ( requiredValidator ) ) ; nameCtrl . setAsyncValidators ( slotNameValidator . validate . bind ( slotNameValidator ) ) ; const splitHostname = this . _slotsArm [ 0 ] . properties . defaultHostName . split ( /\.(.+)/ ) ; this . slotPrefix = splitHostname [ 0 ] ; this . slotSuffix = splitHostname [ 1 ] ; const options = [ { displayLabel : this . _translateService . instant ( PortalResources . slotNew_dontCloneConfig ) , value : '-' , } , ] ; this . _slotsArm . forEach ( s => { options . push ( { displayLabel : s . name ? s . name . replace ( '/' , '-' ) : '-' , value : s . id , } ) ; } ) ; this . cloneSrcIdDropDownOptions = options ; const cloneSrcValidator = new CloneSrcValidator ( this . _siteService , this . _translateService , this . addForm ) ; cloneSrcIdCtrl . enable ( ) ; cloneSrcIdCtrl . setValue ( '-' ) ; cloneSrcIdCtrl . setAsyncValidators ( cloneSrcValidator . validate . bind ( cloneSrcValidator ) ) ; cloneSrcConfigCtrl . enable ( ) ; cloneSrcConfigCtrl . setValue ( null ) ; } } createSlot ( ) { this . addForm . markAsPristine ( ) ; const newSlotName = this . addForm . controls [ 'name' ] . value ; const newSlotConfig = this . addForm . controls [ 'cloneSrcConfig' ] . value ; const siteId = this . _slotsArm [ 0 ] . id ; const location = this . _slotsArm [ 0 ] . location ; const serverFarmId = this . _slotsArm [ 0 ] . properties . serverFarmId ; const kind = this . _slotsArm [ 0 ] . kind ; const slotNewInfo = { resourceId : `${ siteId }/slots/${ newSlotName }` , state : SlotOperationState . started , } ; this . addForm . controls [ 'name' ] . disable ( ) ; this . addForm . controls [ 'cloneSrcId' ] . disable ( ) ; this . addForm . controls [ 'cloneSrcConfig' ] . disable ( ) ; this . _portalService . broadcastMessage ( BroadcastMessageId . slotNew , siteId , slotNewInfo ) ; this . setBusy ( ) ; this . progressMessage = this . _translateService . instant ( PortalResources . slotNew_startCreateNotifyTitle ) . format ( newSlotName ) ; this . progressMessageClass = 'spinner' ; this . isCreating = true ; this . executeButtonDisabled = true ; const sourceSlot = this . _slotsArm . find ( slot => { return slot . id === this . addForm . controls [ 'cloneSrcId' ] . value ; } ) ; let newSlot ; let extendedLocation ; if ( ! ! this . _customLocationId ) { extendedLocation = { name : this . _customLocationId , type : 'CustomLocation' , } ; } else { extendedLocation = undefined ; } if ( this . isFunctionApp ) { newSlot = { location : location , kind : kind , properties : { serverFarmId : serverFarmId , } , } ; } else { newSlot = ! ! sourceSlot ? { location : location , kind : kind , extendedLocation : extendedLocation , properties : { serverFarmId : serverFarmId , siteConfig : sourceSlot . name . includes ( '/' ) ? newSlotConfig : undefined , httpsOnly : sourceSlot . properties . httpsOnly , clientCertEnabled : sourceSlot . properties . clientCertEnabled , clientCertMode : sourceSlot . properties . clientCertMode , clientCertExclusionPaths : sourceSlot . properties . clientCertExclusionPaths , } , } : { location : location , kind : kind , extendedLocation : extendedLocation , properties : { serverFarmId : serverFarmId , siteConfig : { } , } , } ; } this . _enableSlotOptIn ( siteId ) . switchMap ( s => { if ( ! s || s . isSuccessful ) { return this . _siteService . createSlot ( siteId , newSlotName , newSlot ) ; } else { return Observable . of ( s ) ; } } ) . subscribe ( r => { if ( r . isSuccessful ) { this . progressMessage = this . _translateService . instant ( PortalResources . slotNew_startCreateSuccessNotifyTitle ) . format ( newSlotName ) ; this . progressMessageClass = 'success' ; } else { const errorMessage = r . error && r . error . message ; this . progressMessage = errorMessage ? this . _translateService . instant ( PortalResources . slotNew_startCreateFailureNotifyTitleExt ) . format ( newSlotName , errorMessage ) : this . _translateService . instant ( PortalResources . slotNew_startCreateFailureNotifyTitle ) . format ( newSlotName ) ; this . progressMessageClass = 'error' ; this . _aiService . trackEvent ( errorIds . failedToCreateSlot , { error : r . error . errorId , id : siteId } ) ; this . _logService . error ( LogCategories . addSlot , '/add-slot' , r . error ) ; } slotNewInfo . success = r . isSuccessful ; slotNewInfo . state = SlotOperationState . completed ; this . _portalService . broadcastMessage ( BroadcastMessageId . slotNew , siteId , slotNewInfo ) ; this . isCreating = false ; this . clearBusy ( ) ; } ) ; } private _enableSlotOptIn ( siteId ) { if ( ! this . isFunctionApp || ! this . slotOptInNeeded ) { return Observable . of ( null ) ; } else { const newOrUpdatedSettings = { } ; newOrUpdatedSettings [ Constants . secretStorageSettingsName ] = Constants . secretStorageSettingsValueBlob ; return this . _siteService . addOrUpdateAppSettings ( siteId , newOrUpdatedSettings ) . do ( r => { if ( r . isSuccessful && this . _functionAppContext ) { this . _functionAppService . fireSyncTrigger ( this . _functionAppContext ) ; } } ) ; } } closePanel ( ) { this . _portalService . closeSelf ( ) ; } toggleOptInState ( ) { this . slotOptInEnabled = ! this . slotOptInEnabled ; this . executeButtonDisabled = this . slotOptInNeeded && ! this . slotOptInEnabled ; this . _setupForm ( ) ; } }