diff --git a/AzureFunctions.AngularClient/src/app/api/api-details/api-details.component.html b/AzureFunctions.AngularClient/src/app/api/api-details/api-details.component.html index 27818e893e..d78af39f89 100644 --- a/AzureFunctions.AngularClient/src/app/api/api-details/api-details.component.html +++ b/AzureFunctions.AngularClient/src/app/api/api-details/api-details.component.html @@ -97,7 +97,7 @@ + placeholder="{{ 'optional' | translate }}" [ngClass]="{'input-error':!complexForm.controls['backendUri'].valid && complexForm.controls['backendUri'].touched}"> diff --git a/AzureFunctions.AngularClient/src/app/api/api-new/api-new.component.html b/AzureFunctions.AngularClient/src/app/api/api-new/api-new.component.html index b7b19dc271..433fd162bb 100644 --- a/AzureFunctions.AngularClient/src/app/api/api-new/api-new.component.html +++ b/AzureFunctions.AngularClient/src/app/api/api-new/api-new.component.html @@ -90,7 +90,7 @@ + placeholder="{{ 'optional' | translate }}" [ngClass]="{'input-error':!complexForm.controls['backendUri'].valid && complexForm.controls['backendUri'].touched}"> diff --git a/AzureFunctions.AngularClient/src/app/api/request-respose-override/request-respose-override.component.html b/AzureFunctions.AngularClient/src/app/api/request-respose-override/request-respose-override.component.html index f89c0f3479..f1cfa469f9 100644 --- a/AzureFunctions.AngularClient/src/app/api/request-respose-override/request-respose-override.component.html +++ b/AzureFunctions.AngularClient/src/app/api/request-respose-override/request-respose-override.component.html @@ -1,74 +1,87 @@

-
{{ 'rrOverride_request' | translate }}
-
-
{{ 'httpRun_httpMethod' | translate }}
+ + +  {{ 'rrOverride_request' | translate }} + - -
+
+
+
{{ 'httpRun_httpMethod' | translate }}
-
- + +
- -
- -
- +
+ - + +
+ +
+ + + +

-
{{ 'rrOverride_response' | translate }}
-
-
+ + +  {{ 'rrOverride_response' | translate }} + + +
+
- +
+ +
+
- -
-
- +
+ +
+
-
-
- -
- + +
+ - -
+
+
-
- -
+
+ +
+
+
diff --git a/AzureFunctions.AngularClient/src/app/api/request-respose-override/request-respose-override.component.scss b/AzureFunctions.AngularClient/src/app/api/request-respose-override/request-respose-override.component.scss index f8a11fdac4..70fbfd0a28 100644 --- a/AzureFunctions.AngularClient/src/app/api/request-respose-override/request-respose-override.component.scss +++ b/AzureFunctions.AngularClient/src/app/api/request-respose-override/request-respose-override.component.scss @@ -14,4 +14,13 @@ div:nth-child(2) { padding-left:65px; } +} + +.non-visible { + visibility: hidden; + height: 0px; +} + +.shown-container { + padding-top: 15px; } \ No newline at end of file diff --git a/AzureFunctions.AngularClient/src/app/api/request-respose-override/request-respose-override.component.ts b/AzureFunctions.AngularClient/src/app/api/request-respose-override/request-respose-override.component.ts index 76676ea935..dd68c13ed9 100644 --- a/AzureFunctions.AngularClient/src/app/api/request-respose-override/request-respose-override.component.ts +++ b/AzureFunctions.AngularClient/src/app/api/request-respose-override/request-respose-override.component.ts @@ -37,6 +37,8 @@ export class RequestResposeOverrideComponent { model: RequestResponseOverrriedModel; @Input() functionApp: FunctionApp; @Output() valueChanges = new Subject(); + showResponse = false; + showRequest = false private _requestHeadersValid: boolean; private _requestParamsValid: boolean; private _responseHeadersValid: boolean; @@ -71,7 +73,11 @@ export class RequestResposeOverrideComponent { this.model.statusReason = value.responseOverrides[prop]; } if (prop.toLocaleLowerCase() === "response.body") { - this.model.body = value.responseOverrides[prop]; + if (typeof value.responseOverrides[prop] === 'string') { + this.model.body = value.responseOverrides[prop]; + } else { + this.model.body = JSON.stringify(value.responseOverrides[prop]); + } } } } @@ -134,6 +140,14 @@ export class RequestResposeOverrideComponent { this.changeValue(); } + showResponseOverride() { + this.showResponse = !this.showResponse; + } + + showRequestOverride() { + this.showRequest = !this.showRequest; + } + get valid(): boolean { return this._requestHeadersValid && this._requestParamsValid && this._responseHeadersValid; } diff --git a/AzureFunctions.AngularClient/src/app/app.module.ts b/AzureFunctions.AngularClient/src/app/app.module.ts index a27d799b69..14999d048d 100644 --- a/AzureFunctions.AngularClient/src/app/app.module.ts +++ b/AzureFunctions.AngularClient/src/app/app.module.ts @@ -123,6 +123,7 @@ import { GeneralSettingsComponent } from './site/site-config/general-settings/ge import { AppSettingsComponent } from './site/site-config/app-settings/app-settings.component'; import { ConnectionStringsComponent } from './site/site-config/connection-strings/connection-strings.component'; import { BindingEventGridComponent } from './binding-event-grid/binding-event-grid.component'; +import { TopWarningComponent } from './top-warning/top-warning.component'; export function ArmServiceFactory( http: Http, @@ -244,7 +245,8 @@ export class AppModule { ConnectionStringsComponent, PairListComponent, RequestResposeOverrideComponent, - BindingEventGridComponent + BindingEventGridComponent, + TopWarningComponent ], imports: [ FormsModule, diff --git a/AzureFunctions.AngularClient/src/app/apps-list/apps-list.component.html b/AzureFunctions.AngularClient/src/app/apps-list/apps-list.component.html index b832b0dd10..12ada48eab 100644 --- a/AzureFunctions.AngularClient/src/app/apps-list/apps-list.component.html +++ b/AzureFunctions.AngularClient/src/app/apps-list/apps-list.component.html @@ -56,12 +56,13 @@ - - {{'functionMonitor_loading' | translate}} - + + {{'functionMonitor_loading' | translate}} + -
+ +

{{'emptyBrowse_title' | translate}}

{{'emptyBrowse' | translate}} diff --git a/AzureFunctions.AngularClient/src/app/apps-list/apps-list.component.ts b/AzureFunctions.AngularClient/src/app/apps-list/apps-list.component.ts index 88f87772aa..91890f7525 100644 --- a/AzureFunctions.AngularClient/src/app/apps-list/apps-list.component.ts +++ b/AzureFunctions.AngularClient/src/app/apps-list/apps-list.component.ts @@ -32,7 +32,7 @@ export class AppsListComponent implements OnInit, OnDestroy { public appsNode: AppsNode; public Resources = PortalResources; - public isLoading = true; + public initialized = false; public allLocations = this.translateService.instant(PortalResources.allLocations); public numberLocations = this.translateService.instant(PortalResources.locationCount); @@ -64,11 +64,12 @@ export class AppsListComponent implements OnInit, OnDestroy { .distinctUntilChanged() .switchMap(viewInfo => { this.appsNode = (viewInfo.node); - this.isLoading = true; + this.initialized = false; return (viewInfo.node).childrenStream; }) .subscribe(children => { this.apps = children; + this.initialized = true; this.tableItems = this.apps.map(app => ({ title: app.title, subscription: app.subscription, @@ -88,7 +89,6 @@ export class AppsListComponent implements OnInit, OnDestroy { displayLabel: resourceGroup, value: resourceGroup })); - this.isLoading = false; }); } diff --git a/AzureFunctions.AngularClient/src/app/busy-state/busy-state.component.ts b/AzureFunctions.AngularClient/src/app/busy-state/busy-state.component.ts index 4648217b0d..233a068709 100644 --- a/AzureFunctions.AngularClient/src/app/busy-state/busy-state.component.ts +++ b/AzureFunctions.AngularClient/src/app/busy-state/busy-state.component.ts @@ -18,6 +18,7 @@ export class BusyStateComponent implements OnInit { private busyStateMap: { [key: string]: boolean } = {}; private reservedKey = '-'; + private timeouts: { [key: string]: number } = {}; ngOnInit() { this.isGlobal = this.name === 'global'; @@ -29,8 +30,10 @@ export class BusyStateComponent implements OnInit { setScopedBusyState(key: string): string { key = key || Guid.newGuid(); - this.busyStateMap[key] = true; - this.busy = true; + this.timeouts[key] = window.setTimeout(() => { + this.busyStateMap[key] = true; + this.busy = true; + }, 100); // 100 msec debounce return key; } @@ -39,11 +42,20 @@ export class BusyStateComponent implements OnInit { if (this.busyStateMap[key]) { delete this.busyStateMap[key]; } + if (this.timeouts[key]) { + clearTimeout(this.timeouts[key]); + delete this.timeouts[key] + } this.busy = !this.isEmptyMap(this.busyStateMap); } clearOverallBusyState() { this.busyStateMap = {}; + const keys = Object.keys(this.timeouts); + for (let i = 0; i < keys.length; i++) { + clearTimeout(this.timeouts[keys[i]]); + delete this.timeouts[keys[i]]; + } this.clear.next(1); this.busy = false; } diff --git a/AzureFunctions.AngularClient/src/app/controls/pair-list/pair-list.component.html b/AzureFunctions.AngularClient/src/app/controls/pair-list/pair-list.component.html index dfa6e32020..0036a69a11 100644 --- a/AzureFunctions.AngularClient/src/app/controls/pair-list/pair-list.component.html +++ b/AzureFunctions.AngularClient/src/app/controls/pair-list/pair-list.component.html @@ -13,7 +13,7 @@ - + diff --git a/AzureFunctions.AngularClient/src/app/function-dev/function-dev.component.ts b/AzureFunctions.AngularClient/src/app/function-dev/function-dev.component.ts index 703222dcf2..116bce9058 100644 --- a/AzureFunctions.AngularClient/src/app/function-dev/function-dev.component.ts +++ b/AzureFunctions.AngularClient/src/app/function-dev/function-dev.component.ts @@ -1,7 +1,7 @@ import { FileUtilities } from './../shared/Utilities/file'; import { EditModeHelper } from './../shared/Utilities/edit-mode.helper'; import { ConfigService } from './../shared/services/config.service'; -import { Component, QueryList, OnChanges, Input, SimpleChange, ViewChild, ViewChildren, OnDestroy, ElementRef } from '@angular/core'; +import { Component, QueryList, OnChanges, Input, SimpleChange, ViewChild, ViewChildren, OnDestroy, ElementRef, ChangeDetectorRef } from '@angular/core'; import { Observable } from 'rxjs/Observable'; import { Subject } from 'rxjs/Subject'; import { Subscription } from 'rxjs/Subscription'; @@ -106,7 +106,8 @@ export class FunctionDevComponent implements OnChanges, OnDestroy { private _globalStateService: GlobalStateService, private _translateService: TranslateService, private _aiService: AiService, - configService: ConfigService) { + configService: ConfigService, + cd: ChangeDetectorRef) { this.functionInvokeUrl = this._translateService.instant(PortalResources.functionDev_loading); this.isStandalone = configService.isStandalone(); @@ -218,6 +219,11 @@ export class FunctionDevComponent implements OnChanges, OnDestroy { this.setFunctionInvokeUrl(); } + // This subscribe method changes a lot of UI elements. Normally that's fine if the data leading + // to the subscribe isn't ready and need to be fetched from the server. + // if the data is cached on the client, this causes few rapid changes in the DOM and we need to inform the change detector of these changes. + // Otherwise we'll get ExpressionChangedAfterItHasBeenCheckedError + cd.detectChanges(); }); diff --git a/AzureFunctions.AngularClient/src/app/function-keys/function-keys.component.html b/AzureFunctions.AngularClient/src/app/function-keys/function-keys.component.html index ced4258c2b..f8b1b0d6a7 100644 --- a/AzureFunctions.AngularClient/src/app/function-keys/function-keys.component.html +++ b/AzureFunctions.AngularClient/src/app/function-keys/function-keys.component.html @@ -1,4 +1,4 @@ -
+
{{'functionKeys_title' | translate}}
{{'adminKeys_title' | translate}}
@@ -13,7 +13,7 @@ {{key.name}} - {{key.value}} + {{key.value}} {{'functionKeys_clickToHide' | translate}} {{'functionKeys_clickToShow' | translate}}
diff --git a/AzureFunctions.AngularClient/src/app/function-keys/function-keys.component.ts b/AzureFunctions.AngularClient/src/app/function-keys/function-keys.component.ts index a4c00686fb..565a158e73 100644 --- a/AzureFunctions.AngularClient/src/app/function-keys/function-keys.component.ts +++ b/AzureFunctions.AngularClient/src/app/function-keys/function-keys.component.ts @@ -1,3 +1,5 @@ +import { CacheService } from 'app/shared/services/cache.service'; +import { reachableInternalLoadBalancerApp } from 'app/shared/Utilities/internal-load-balancer'; import { Component, Input, OnChanges, OnDestroy, ViewChild, OnInit } from '@angular/core'; import { Observable } from 'rxjs/Observable'; import { Subject } from 'rxjs/Subject'; @@ -37,18 +39,20 @@ export class FunctionKeysComponent implements OnChanges, OnDestroy, OnInit { @ViewChild(BusyStateComponent) busyState: BusyStateComponent; private functionStream: Subject; private functionAppStream: Subject; - private newKeyName: string; - private newKeyValue: string; - private validKey: boolean; + public newKeyName: string; + public newKeyValue: string; + public validKey: boolean; public keys: Array; public addingNew: boolean; + public disabled = false; constructor( private _broadcastService: BroadcastService, private _translateService: TranslateService, private _utilities: UtilitiesService, - private _aiService: AiService) { + private _aiService: AiService, + private _cacheService: CacheService) { this.validKey = false; this.keys = []; @@ -58,24 +62,33 @@ export class FunctionKeysComponent implements OnChanges, OnDestroy, OnInit { this.functionAppStream .merge(this.functionStream) .debounceTime(100) - .switchMap((r: any) => { - + .switchMap(r => { const functionApp = r && (r).functionApp; + return reachableInternalLoadBalancerApp(functionApp || this.functionApp, this._cacheService).map(a => [r, a]); + }) + .switchMap((result: [FunctionApp | FunctionInfo, boolean]) => { + + const functionApp = result[0] && (result[0]).functionApp; let fi: FunctionInfo; if (functionApp) { this.functionApp = functionApp; - fi = r; + fi = result[0] as FunctionInfo; } this.setBusyState(); this.resetState(); - - return fi - ? this.functionApp.getFunctionKeys(fi).catch(() => Observable.of({ keys: [], links: [] })) - : this.functionApp.getFunctionHostKeys().catch(() => Observable.of({ keys: [], links: [] })); - + if (result[1]) { + this.disabled = false; + return fi + ? this.functionApp.getFunctionKeys(fi).catch(() => Observable.of({ keys: [], links: [] })) + : this.functionApp.getFunctionHostKeys().catch(() => Observable.of({ keys: [], links: [] })); + } else { + this.disabled = true; + return Observable.throw(this.disabled); + } }) .do(null, e => { + this.clearBusyState(); this._aiService.trackException(e, "/errors/function-keys"); console.error(e); }) diff --git a/AzureFunctions.AngularClient/src/app/getting-started/getting-started.component.ts b/AzureFunctions.AngularClient/src/app/getting-started/getting-started.component.ts index 4d6042c075..ee1737100c 100644 --- a/AzureFunctions.AngularClient/src/app/getting-started/getting-started.component.ts +++ b/AzureFunctions.AngularClient/src/app/getting-started/getting-started.component.ts @@ -1,3 +1,4 @@ +import { ConfigService } from './../shared/services/config.service'; import { Component, Output, EventEmitter, OnInit } from '@angular/core'; import { Response, Http } from '@angular/http'; import { Observable } from 'rxjs/Observable'; @@ -57,7 +58,8 @@ export class GettingStartedComponent implements OnInit { private _globalStateService: GlobalStateService, private _translateService: TranslateService, private _aiService: AiService, - private _http: Http + private _http: Http, + private _configService: ConfigService ) { this.isValidContainerName = true; // http://stackoverflow.com/a/8084248/3234163 @@ -377,7 +379,7 @@ export class GettingStartedComponent implements OnInit { appSettings: [ { name: 'AzureWebJobsStorage', value: connectionString }, { name: 'AzureWebJobsDashboard', value: connectionString }, - { name: Constants.runtimeVersionAppSettingName, value: Constants.runtimeVersion }, + { name: Constants.runtimeVersionAppSettingName, value: this._configService.FunctionsVersionInfo.runtimeDefault }, { name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING', value: connectionString }, { name: 'WEBSITE_CONTENTSHARE', value: name.toLocaleLowerCase() }, { name: `${storageAccount.name}_STORAGE`, value: connectionString }, diff --git a/AzureFunctions.AngularClient/src/app/main/main.component.html b/AzureFunctions.AngularClient/src/app/main/main.component.html index c23ad00f30..be5da7a6b0 100644 --- a/AzureFunctions.AngularClient/src/app/main/main.component.html +++ b/AzureFunctions.AngularClient/src/app/main/main.component.html @@ -5,6 +5,8 @@ + +
diff --git a/AzureFunctions.AngularClient/src/app/pop-over/pop-over.component.html b/AzureFunctions.AngularClient/src/app/pop-over/pop-over.component.html index 13d7165c2f..e6b62f7c16 100644 --- a/AzureFunctions.AngularClient/src/app/pop-over/pop-over.component.html +++ b/AzureFunctions.AngularClient/src/app/pop-over/pop-over.component.html @@ -1,7 +1,7 @@ (p.name.toLowerCase() === 'code')); if (codeIndex > -1) { - this._code = params[codeIndex]; + this.model.code = params[codeIndex]; params.splice(codeIndex, 1); } @@ -131,10 +130,6 @@ export class RunHttpComponent { this._paramsValid = form.valid; this.valid = this._paramsValid && this._headersValid; this.model.queryStringParams = form.value.items; - if (this._code) { - this.model.queryStringParams.push(this._code); - } - this.validChange.emit(this.valid); } diff --git a/AzureFunctions.AngularClient/src/app/shared/Utilities/internal-load-balancer.ts b/AzureFunctions.AngularClient/src/app/shared/Utilities/internal-load-balancer.ts new file mode 100644 index 0000000000..9305e4cf72 --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/shared/Utilities/internal-load-balancer.ts @@ -0,0 +1,25 @@ +import { FunctionApp } from './../function-app'; +import { Observable } from 'rxjs/Observable'; +import { HostingEnvironment } from './../models/arm/hosting-environment'; +import { ArmObj } from './../models/arm/arm-obj'; +import { CacheService } from 'app/shared/services/cache.service'; + + +export function reachableInternalLoadBalancerApp(functionApp: FunctionApp, http: CacheService): Observable { + if (functionApp && functionApp.site && + functionApp.site.properties.hostingEnvironmentProfile && + functionApp.site.properties.hostingEnvironmentProfile.id) { + return http.getArm(functionApp.site.properties.hostingEnvironmentProfile.id, false, '2016-09-01') + .mergeMap(r => { + const ase: ArmObj = r.json(); + if (ase.properties.internalLoadBalancingMode && + ase.properties.internalLoadBalancingMode !== 'None') { + return functionApp.pingScmSite(); + } else { + return Observable.of(true); + } + }); + } else { + return Observable.of(true); + } +} \ No newline at end of file diff --git a/AzureFunctions.AngularClient/src/app/shared/function-app.ts b/AzureFunctions.AngularClient/src/app/shared/function-app.ts index 1c8dba5416..0b321b7a0e 100644 --- a/AzureFunctions.AngularClient/src/app/shared/function-app.ts +++ b/AzureFunctions.AngularClient/src/app/shared/function-app.ts @@ -54,6 +54,7 @@ import { FunctionAppEditMode } from './models/function-app-edit-mode'; import { HostStatus } from './models/host-status'; import * as jsonschema from 'jsonschema'; +import { reachableInternalLoadBalancerApp } from '../shared/Utilities/internal-load-balancer'; export class FunctionApp { private masterKey: string; @@ -145,17 +146,7 @@ export class FunctionApp { this._http = new NoCorsHttpService(_ngHttp, _broadcastService, _aiService, _translateService, () => this.getPortalHeaders()); - if (!Constants.runtimeVersion) { - this.getLatestRuntime().subscribe((runtime: any) => { - Constants.runtimeVersion = runtime; - }); - } - if (!Constants.routingExtensionVersion) { - this._getLatestRoutingExtensionVersion().subscribe((routingVersion: any) => { - Constants.routingExtensionVersion = routingVersion; - }); - } if (!_globalStateService.showTryView) { this._userService.getStartupInfo() @@ -245,13 +236,6 @@ export class FunctionApp { } } - private _getLatestRoutingExtensionVersion() { - return this._cacheService.get(Constants.serviceHost + 'api/latestrouting', false, this.getPortalHeaders()) - .map(r => { - return r.json(); - }) - .retryWhen(this.retryAntares); - } getFunctions() { let fcs: FunctionInfo[]; @@ -588,22 +572,25 @@ export class FunctionApp { }); } - let firstDone = false; + let queryString = ''; + if (model.code) { + queryString = `?${model.code.name}=${model.code.value}`; + } model.queryStringParams.forEach(p => { const findResult = processedParams.find((pr) => { return pr === p.name; }); if (!findResult) { - if (!firstDone) { - url += '?'; - firstDone = true; + if (!queryString) { + queryString += '?'; } else { - url += '&'; + queryString += '&'; } - url += p.name + '=' + p.value; + queryString += p.name + '=' + p.value; } }); + url = url + queryString; const inputBinding = (functionInfo.config && functionInfo.config.bindings ? functionInfo.config.bindings.find(e => e.type === 'httpTrigger') : null); @@ -796,7 +783,9 @@ export class FunctionApp { } getHostSecretsFromScm() { - return this.getAuthSettings() + return reachableInternalLoadBalancerApp(this, this._cacheService) + .filter(i => i) + .mergeMap(() => this.getAuthSettings()) .mergeMap(authSettings => { return authSettings.clientCertEnabled ? Observable.of() @@ -1201,13 +1190,7 @@ export class FunctionApp { @ClearCache('clearAllCachedData') clearAllCachedData() { } - getLatestRuntime() { - return this._http.get(Constants.serviceHost + 'api/latestruntime', { headers: this.getPortalHeaders() }) - .map(r => { - return r.json(); - }) - .retryWhen(this.retryAntares); - } + getFunctionKeys(functionInfo: FunctionInfo, handleUnauthorized?: boolean): Observable { handleUnauthorized = typeof handleUnauthorized !== 'undefined' ? handleUnauthorized : true; @@ -1501,10 +1484,10 @@ export class FunctionApp { /** * This method just pings the root of the SCM site. It doesn't care about the response in anyway or use it. */ - pingScmSite() { + pingScmSite(): Observable { return this._http.get(this._scmUrl, { headers: this.getScmSiteHeaders() }) - .map(_ => null) - .catch(() => Observable.of(null)); + .map(_ => true) + .catch(() => Observable.of(false)); } private getExtensionVersion() { diff --git a/AzureFunctions.AngularClient/src/app/shared/models/arm/hosting-environment.ts b/AzureFunctions.AngularClient/src/app/shared/models/arm/hosting-environment.ts new file mode 100644 index 0000000000..fa8d3e1f0e --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/shared/models/arm/hosting-environment.ts @@ -0,0 +1,13 @@ +// App Service Environment + +export interface HostingEnvironmentProfile { + id: string; + name: string; + type: string; +} + +export interface HostingEnvironment { + name: string; + internalLoadBalancingMode: 'Web' | 'None' | 'Publishing' | 'Web, Publishing' | null; + vnetName: string; +} \ No newline at end of file diff --git a/AzureFunctions.AngularClient/src/app/shared/models/arm/site.ts b/AzureFunctions.AngularClient/src/app/shared/models/arm/site.ts index 0f181d28f7..d9252dd901 100644 --- a/AzureFunctions.AngularClient/src/app/shared/models/arm/site.ts +++ b/AzureFunctions.AngularClient/src/app/shared/models/arm/site.ts @@ -1,3 +1,5 @@ +import { HostingEnvironmentProfile } from './hosting-environment'; + export interface Site { state: string; hostNames: string[]; @@ -15,4 +17,5 @@ export interface Site { siteDisabledReason?: number; clientCertEnabled?: boolean; clientAffinityEnabled?: boolean; -} + hostingEnvironmentProfile?: HostingEnvironmentProfile; +} \ No newline at end of file diff --git a/AzureFunctions.AngularClient/src/app/shared/models/constants.ts b/AzureFunctions.AngularClient/src/app/shared/models/constants.ts index 01e00f5631..c1874754c4 100644 --- a/AzureFunctions.AngularClient/src/app/shared/models/constants.ts +++ b/AzureFunctions.AngularClient/src/app/shared/models/constants.ts @@ -1,4 +1,5 @@ -export class HttpMethods { + +export class HttpMethods { public GET = "get"; public POST = "post"; public DELETE = "delete"; @@ -17,8 +18,6 @@ export class Constants { ? `https://${window.location.hostname}:${window.location.port}/` : `https://${window.location.hostname}/`; - public static runtimeVersion: string; - public static routingExtensionVersion: string; public static nodeVersion = '6.5.0'; public static latest = 'latest'; public static disabled = 'disabled'; @@ -86,7 +85,8 @@ export class AvailabilityStates { export class NotificationIds { public static alwaysOn = 'alwaysOn'; public static newRuntimeVersion = 'newRuntimeVersion'; - public static slotsHostId = "slotsBlobStorage" + public static slotsHostId = 'slotsBlobStorage'; + public static runtimeV2 = 'runtimeV2'; } export class Validations { @@ -187,9 +187,10 @@ export class KeyCodes { public static readonly arrowUp = 38; public static readonly arrowRight = 39; public static readonly arrowDown = 40; + public static readonly delete = 46; } export class DomEvents { public static readonly keydown = 'keydown'; public static readonly click = 'click'; -} +} \ No newline at end of file diff --git a/AzureFunctions.AngularClient/src/app/shared/models/http-run.ts b/AzureFunctions.AngularClient/src/app/shared/models/http-run.ts index 202ea8cdfa..8ddebc0872 100644 --- a/AzureFunctions.AngularClient/src/app/shared/models/http-run.ts +++ b/AzureFunctions.AngularClient/src/app/shared/models/http-run.ts @@ -4,6 +4,7 @@ queryStringParams: Param[] = []; headers: Param[] = []; body: string; + code: Param; constructor() { } diff --git a/AzureFunctions.AngularClient/src/app/shared/models/portal-resources.ts b/AzureFunctions.AngularClient/src/app/shared/models/portal-resources.ts index 095ffc2eb6..a528613eb3 100644 --- a/AzureFunctions.AngularClient/src/app/shared/models/portal-resources.ts +++ b/AzureFunctions.AngularClient/src/app/shared/models/portal-resources.ts @@ -693,5 +693,9 @@ export class PortalResources public static rrOverride_message: string = "rrOverride_message"; public static rrOverride_request: string = "rrOverride_request"; public static rrOverride_response: string = "rrOverride_response"; + public static optional: string = "optional"; + public static topBar_runtimeV2: string = "topBar_runtimeV2"; + public static functionKeys_clickToHide: string = "functionKeys_clickToHide"; + public static expandCollapse: string = "expandCollapse"; } diff --git a/AzureFunctions.AngularClient/src/app/shared/services/config.service.ts b/AzureFunctions.AngularClient/src/app/shared/services/config.service.ts index d5a6e09680..3f93b0f390 100644 --- a/AzureFunctions.AngularClient/src/app/shared/services/config.service.ts +++ b/AzureFunctions.AngularClient/src/app/shared/services/config.service.ts @@ -1,4 +1,5 @@ -import { Injectable } from '@angular/core'; +import { FunctionsVersionInfo } from '../../../../../common/models/functions-version-info'; +import { Injectable } from '@angular/core'; import 'rxjs/add/operator/map'; import 'rxjs/add/operator/toPromise'; @@ -6,6 +7,10 @@ import 'rxjs/add/operator/toPromise'; export class ConfigService { private runtimeType = window.appsvc.env.runtimeType; + get FunctionsVersionInfo(): FunctionsVersionInfo { + return window.appsvc.functionsVersionInfo; + } + isOnPrem(): boolean { return this.runtimeType === 'OnPrem'; } diff --git a/AzureFunctions.AngularClient/src/app/shared/services/functions.service.ts b/AzureFunctions.AngularClient/src/app/shared/services/functions.service.ts index b5a9948554..679b5cd1cd 100644 --- a/AzureFunctions.AngularClient/src/app/shared/services/functions.service.ts +++ b/AzureFunctions.AngularClient/src/app/shared/services/functions.service.ts @@ -37,12 +37,6 @@ export class FunctionsService { private _userService: UserService, private _globalStateService: GlobalStateService) { - if (!Constants.runtimeVersion) { - this.getLatestRuntime().subscribe((runtime: any) => { - Constants.runtimeVersion = runtime; - }); - } - if (!_globalStateService.showTryView) { this._userService.getStartupInfo().subscribe(info => { this.token = info.token }); } @@ -124,14 +118,6 @@ export class FunctionsService { } } - getLatestRuntime() { - return this._http.get(Constants.serviceHost + 'api/latestruntime', { headers: this.getPortalHeaders() }) - .map(r => { - return r.json(); - }) - .retryWhen(this.retryAntares); - } - // to talk to Functions Portal private getPortalHeaders(contentType?: string): Headers { contentType = contentType || 'application/json'; diff --git a/AzureFunctions.AngularClient/src/app/shared/services/global-state.service.ts b/AzureFunctions.AngularClient/src/app/shared/services/global-state.service.ts index c37062de84..49aadab325 100644 --- a/AzureFunctions.AngularClient/src/app/shared/services/global-state.service.ts +++ b/AzureFunctions.AngularClient/src/app/shared/services/global-state.service.ts @@ -6,7 +6,6 @@ import { ReplaySubject } from 'rxjs/ReplaySubject'; import { TopBarNotification } from './../../top-bar/top-bar-models'; import { FunctionContainer } from '../models/function-container'; import { UserService } from './user.service'; -import { Constants } from '../models/constants'; import { BusyStateComponent } from '../../busy-state/busy-state.component'; import { FunctionsService } from './functions.service'; @@ -49,15 +48,6 @@ export class GlobalStateService { return ''; } - // The methods below should not be in the globalstate service - get RoutingExtensionVersion(): string { - return this._appSettings[Constants.routingExtensionVersionAppSettingName]; - } - - get IsRoutingEnabled() { - return this.RoutingExtensionVersion && this.RoutingExtensionVersion.toLowerCase() !== Constants.disabled; - } - set GlobalBusyStateComponent(busyStateComponent: BusyStateComponent) { this._globalBusyStateComponent = busyStateComponent; setTimeout(() => { diff --git a/AzureFunctions.AngularClient/src/app/side-nav/side-nav.component.ts b/AzureFunctions.AngularClient/src/app/side-nav/side-nav.component.ts index 986e654b68..e4c7a63119 100644 --- a/AzureFunctions.AngularClient/src/app/side-nav/side-nav.component.ts +++ b/AzureFunctions.AngularClient/src/app/side-nav/side-nav.component.ts @@ -64,7 +64,7 @@ export class SideNavComponent implements AfterViewInit { private _savedSubsKey = '/subscriptions/selectedIds'; private _subscriptionsStream = new ReplaySubject(1); - private _searchTermStream = new Subject(); + private _searchTermStream = new ReplaySubject(1); private _initialized = false; diff --git a/AzureFunctions.AngularClient/src/app/site/function-runtime/function-runtime.component.html b/AzureFunctions.AngularClient/src/app/site/function-runtime/function-runtime.component.html index 78f85c17dd..804d9b6d02 100644 --- a/AzureFunctions.AngularClient/src/app/site/function-runtime/function-runtime.component.html +++ b/AzureFunctions.AngularClient/src/app/site/function-runtime/function-runtime.component.html @@ -48,6 +48,9 @@

+
diff --git a/AzureFunctions.AngularClient/src/app/site/function-runtime/function-runtime.component.ts b/AzureFunctions.AngularClient/src/app/site/function-runtime/function-runtime.component.ts index c46ec8b5de..aa569da819 100644 --- a/AzureFunctions.AngularClient/src/app/site/function-runtime/function-runtime.component.ts +++ b/AzureFunctions.AngularClient/src/app/site/function-runtime/function-runtime.component.ts @@ -1,3 +1,4 @@ +import { ConfigService } from './../../shared/services/config.service'; import { SiteTabComponent } from './../site-dashboard/site-tab/site-tab.component'; import { BusyStateComponent } from './../../busy-state/busy-state.component'; import { EditModeHelper } from './../../shared/Utilities/edit-mode.helper'; @@ -32,6 +33,7 @@ import { FunctionApp } from './../../shared/function-app'; import { FunctionAppEditMode } from '../../shared/models/function-app-edit-mode'; import { SlotsService } from '../../shared/services/slots.service'; import { HostStatus } from './../../shared/models/host-status'; +import { FunctionsVersionInfoHelper } from '../../../../../common/models/functions-version-info'; @Component({ selector: 'function-runtime', @@ -57,6 +59,8 @@ export class FunctionRuntimeComponent implements OnDestroy { public routingExtensionVersion: string; public latestRoutingExtensionVersion: string; public apiProxiesEnabled: boolean; + public functionRutimeOptions: SelectOption[]; + public functionRuntimeValueStream: Subject; private proxySettingValueStream: Subject; private functionEditModeValueStream: Subject; public showTryView: boolean; @@ -84,6 +88,7 @@ export class FunctionRuntimeComponent implements OnDestroy { private _aiService: AiService, private _translateService: TranslateService, private _slotsService: SlotsService, + private _configService: ConfigService, siteTabsComponent: SiteTabComponent ) { @@ -143,24 +148,22 @@ export class FunctionRuntimeComponent implements OnDestroy { this.showDailyMemoryWarning = (!this.site.properties.enabled && this.site.properties.siteDisabledReason === 1); this.memorySize = this.site.properties.containerSize; - this.latestExtensionVersion = Constants.runtimeVersion; this.extensionVersion = appSettings.properties[Constants.runtimeVersionAppSettingName]; if (!this.extensionVersion) { this.extensionVersion = Constants.latest; } - this.needUpdateExtensionVersion = - Constants.runtimeVersion !== this.extensionVersion && Constants.latest !== this.extensionVersion.toLowerCase(); + this.setNeedUpdateExtensionVersion(); this.routingExtensionVersion = appSettings.properties[Constants.routingExtensionVersionAppSettingName]; if (!this.routingExtensionVersion) { this.routingExtensionVersion = Constants.disabled; } - this.latestRoutingExtensionVersion = Constants.routingExtensionVersion; + this.latestRoutingExtensionVersion = this._configService.FunctionsVersionInfo.proxyDefault; this.apiProxiesEnabled = ((this.routingExtensionVersion) && (this.routingExtensionVersion !== Constants.disabled)); this.needUpdateRoutingExtensionVersion - = Constants.routingExtensionVersion !== this.routingExtensionVersion && Constants.latest !== this.routingExtensionVersion.toLowerCase(); + = this._configService.FunctionsVersionInfo.proxyDefault !== this.routingExtensionVersion && Constants.latest !== this.routingExtensionVersion.toLowerCase(); if (EditModeHelper.isReadOnly(r.editMode)) { this.functionAppEditMode = false; @@ -206,27 +209,36 @@ export class FunctionRuntimeComponent implements OnDestroy { value: true }]; - this.proxySettingValueStream = new Subject(); - this.proxySettingValueStream - .subscribe((value: boolean) => { - if (this.apiProxiesEnabled !== value) { - this._busyState.setBusyState(); - const appSettingValue: string = value ? Constants.routingExtensionVersion : Constants.disabled; + this.functionRutimeOptions = [ + { + displayLabel: '~1', + value: '~1' + }, { + displayLabel: '~2 (beta)', + value: '~2' + }]; - this._cacheService.postArm(`${this.site.id}/config/appsettings/list`, true) - .mergeMap(r => { - return this._updateProxiesVersion(r.json(), appSettingValue); - }) - .subscribe(() => { - this.functionApp.fireSyncTrigger(); - this.apiProxiesEnabled = value; - this.needUpdateRoutingExtensionVersion = false; - this.routingExtensionVersion = Constants.routingExtensionVersion; - this._busyState.clearBusyState(); - this._cacheService.clearArmIdCachePrefix(this.site.id); - }); - } - }); + this.proxySettingValueStream = new Subject(); + this.proxySettingValueStream + .subscribe((value: boolean) => { + if (this.apiProxiesEnabled !== value) { + this._busyState.setBusyState(); + const appSettingValue: string = value ? this._configService.FunctionsVersionInfo.proxyDefault : Constants.disabled; + + this._cacheService.postArm(`${this.site.id}/config/appsettings/list`, true) + .mergeMap(r => { + return this._updateProxiesVersion(r.json(), appSettingValue); + }) + .subscribe(() => { + this.functionApp.fireSyncTrigger(); + this.apiProxiesEnabled = value; + this.needUpdateRoutingExtensionVersion = false; + this.routingExtensionVersion = this._configService.FunctionsVersionInfo.proxyDefault; + this._busyState.clearBusyState(); + this._cacheService.clearArmIdCachePrefix(this.site.id); + }); + } + }); this.functionEditModeValueStream = new Subject(); this.functionEditModeValueStream @@ -285,6 +297,11 @@ export class FunctionRuntimeComponent implements OnDestroy { this._cacheService.clearArmIdCachePrefix(this.site.id); }); }); + + this.functionRuntimeValueStream = new Subject(); + this.functionRuntimeValueStream.subscribe((value: string) => { + this.updateVersion(value); + }); } @Input('viewInfoInput') set viewInfoInput(viewInfo: TreeViewInfo) { @@ -316,15 +333,26 @@ export class FunctionRuntimeComponent implements OnDestroy { return navigator.userAgent.toLocaleLowerCase().indexOf('trident') !== -1; } - updateVersion() { + updateVersion(version?: string) { + if (version === this.extensionVersion) { + return; + } + if (!version) { + version = this.getLatestVersion(this.extensionVersion); + }; this._aiService.trackEvent('/actions/app_settings/update_version'); this._busyState.setBusyState(); this._cacheService.postArm(`${this.site.id}/config/appsettings/list`, true) .mergeMap(r => { - return this._updateContainerVersion(r.json()); + return this._updateContainerVersion(r.json(), version); }) - .subscribe(() => { - this.needUpdateExtensionVersion = false; + .mergeMap(r => { + return this.functionApp.getFunctionHostStatus(); + }) + .subscribe((hostStatus: HostStatus) => { + this.exactExtensionVersion = hostStatus ? hostStatus.version : ''; + this.extensionVersion = version; + this.setNeedUpdateExtensionVersion(); this._busyState.clearBusyState(); this._cacheService.clearArmIdCachePrefix(this.site.id); this._appNode.clearNotification(NotificationIds.newRuntimeVersion); @@ -337,7 +365,7 @@ export class FunctionRuntimeComponent implements OnDestroy { this._cacheService.postArm(`${this.site.id}/config/appsettings/list`, true) .mergeMap(r => { - return this._updateProxiesVersion(r.json(), Constants.routingExtensionVersion); + return this._updateProxiesVersion(r.json(), this._configService.FunctionsVersionInfo.proxyDefault); }) .subscribe(() => { this.needUpdateRoutingExtensionVersion = false; @@ -381,11 +409,11 @@ export class FunctionRuntimeComponent implements OnDestroy { this._broadcastService.broadcast(BroadcastEvent.OpenTab, SiteTabIds.applicationSettings); } - private _updateContainerVersion(appSettings: ArmObj) { + private _updateContainerVersion(appSettings: ArmObj, version: string) { if (appSettings.properties[Constants.azureJobsExtensionVersion]) { delete appSettings[Constants.azureJobsExtensionVersion]; } - appSettings.properties[Constants.runtimeVersionAppSettingName] = Constants.runtimeVersion; + appSettings.properties[Constants.runtimeVersionAppSettingName] = version; appSettings.properties[Constants.nodeVersionAppSettingName] = Constants.nodeVersion; return this._cacheService.putArm(appSettings.id, this._armService.websiteApiVersion, appSettings); @@ -400,7 +428,7 @@ export class FunctionRuntimeComponent implements OnDestroy { if (appSettings[Constants.routingExtensionVersionAppSettingName]) { delete appSettings.properties[Constants.routingExtensionVersionAppSettingName]; } - appSettings.properties[Constants.routingExtensionVersionAppSettingName] = value ? value : Constants.routingExtensionVersion; + appSettings.properties[Constants.routingExtensionVersionAppSettingName] = value ? value : this._configService.FunctionsVersionInfo.proxyDefault; return this._cacheService.putArm(appSettings.id, this._armService.websiteApiVersion, appSettings); } @@ -430,4 +458,26 @@ export class FunctionRuntimeComponent implements OnDestroy { public get GlobalDisabled() { return this._globalStateService.GlobalDisabled; } + + private setNeedUpdateExtensionVersion() { + this.needUpdateExtensionVersion = FunctionsVersionInfoHelper.needToUpdateRuntime(this._configService.FunctionsVersionInfo, this.extensionVersion); + this.latestExtensionVersion = this.getLatestVersion(this.extensionVersion); + } + + private getLatestVersion(version: string): string { + const match = this._configService.FunctionsVersionInfo.runtimeStable.find(v => { + return this.extensionVersion.toLowerCase() === v; + }); + if (match) { + return match; + } else { + if (version.startsWith('1.')) { + return '~1'; + } else if (version.startsWith('2.')) { + return '~2'; + } else { + return this._configService.FunctionsVersionInfo.runtimeDefault; + } + } + } } diff --git a/AzureFunctions.AngularClient/src/app/site/site-dashboard/site-dashboard.component.html b/AzureFunctions.AngularClient/src/app/site/site-dashboard/site-dashboard.component.html index d5b851911d..0499e3bc70 100644 --- a/AzureFunctions.AngularClient/src/app/site/site-dashboard/site-dashboard.component.html +++ b/AzureFunctions.AngularClient/src/app/site/site-dashboard/site-dashboard.component.html @@ -7,12 +7,17 @@ src="images/pin.svg" (click)="pinPart()" /> -