+
diff --git a/AzureFunctions.AngularClient/src/app/multi-drop-down/multi-drop-down.component.scss b/AzureFunctions.AngularClient/src/app/multi-drop-down/multi-drop-down.component.scss
index 140d9a79d4..d816c13217 100644
--- a/AzureFunctions.AngularClient/src/app/multi-drop-down/multi-drop-down.component.scss
+++ b/AzureFunctions.AngularClient/src/app/multi-drop-down/multi-drop-down.component.scss
@@ -29,7 +29,8 @@
padding: 0px;
input{
- margin: 0px 5px;
+ vertical-align: middle;
+ margin: 0px 5px 2px 5px;
}
li{
diff --git a/AzureFunctions.AngularClient/src/app/multi-drop-down/multi-drop-down.component.ts b/AzureFunctions.AngularClient/src/app/multi-drop-down/multi-drop-down.component.ts
index c928cc2148..e540a7c963 100644
--- a/AzureFunctions.AngularClient/src/app/multi-drop-down/multi-drop-down.component.ts
+++ b/AzureFunctions.AngularClient/src/app/multi-drop-down/multi-drop-down.component.ts
@@ -1,6 +1,8 @@
-import { Component, OnInit, ElementRef, Input } from '@angular/core';
+import { PortalResources } from './../shared/models/portal-resources';
+import { TranslateService } from '@ngx-translate/core';
+import { KeyCodes } from './../shared/models/constants';
+import { Component, OnInit, ElementRef, Input, ViewChild } from '@angular/core';
import { ReplaySubject } from 'rxjs/ReplaySubject';
-
import { DropDownElement, MultiDropDownElement } from './../shared/models/drop-down-element';
@Component({
@@ -16,14 +18,16 @@ import { DropDownElement, MultiDropDownElement } from './../shared/models/drop-d
export class MultiDropDownComponent
implements OnInit {
@Input() displayText = "";
+ @ViewChild('itemListContainer') itemListContainer: ElementRef;
public opened = false;
public options: MultiDropDownElement[];
public selectedValues = new ReplaySubject(1);
private _selectAllOption: MultiDropDownElement;
+ private _focusedIndex = -1;
- constructor(private _eref: ElementRef) {
+ constructor(private _eref: ElementRef, private _ts : TranslateService) {
this._selectAllOption = {
- displayLabel: "Select All",
+ displayLabel: _ts.instant(PortalResources.selectAll),
value: null,
isSelected: false
};
@@ -54,7 +58,7 @@ export class MultiDropDownComponent implements OnInit {
}
click() {
- if(this.opened){
+ if (this.opened) {
this._notifyChangeSubscriptions();
}
@@ -65,11 +69,14 @@ export class MultiDropDownComponent implements OnInit {
onDocumentClick(event) {
if (this.opened && !this._eref.nativeElement.contains(event.target)) {
- this.opened = false;
this._notifyChangeSubscriptions();
}
}
+ onBlur(event) {
+ this._notifyChangeSubscriptions();
+ }
+
handleChecked(option: MultiDropDownElement) {
if (option !== this._selectAllOption) {
this._selectAllOption.isSelected = false;
@@ -80,7 +87,116 @@ export class MultiDropDownComponent implements OnInit {
}
}
+ onKeyPress(event: KeyboardEvent) {
+
+ if (event.keyCode === KeyCodes.arrowDown) {
+ this._moveFocusedItemDown();
+ }
+ else if (event.keyCode === KeyCodes.arrowUp) {
+ this._moveFocusedItemUp();
+ }
+ else if (event.keyCode === KeyCodes.enter || event.keyCode === KeyCodes.space) {
+ if (this._focusedIndex >= 0 && this._focusedIndex < this.options.length) {
+ let option = this.options[this._focusedIndex];
+ option.isSelected = !option.isSelected;
+
+ if(option === this._selectAllOption){
+ if(option.isSelected){
+ this.options.forEach(o => o.isSelected = true);
+ }
+ else{
+ this.options.forEach(o => o.isSelected = false);
+ }
+ }
+ }
+ }
+ else if (event.keyCode === KeyCodes.escape) {
+ this._notifyChangeSubscriptions();
+ }
+ else if (event.keyCode === KeyCodes.tab) {
+ this._notifyChangeSubscriptions();
+ }
+
+ if (event.keyCode !== KeyCodes.tab) {
+
+ // Prevents the entire page from scrolling on up/down key press
+ event.preventDefault();
+ }
+
+ }
+
+ private _moveFocusedItemDown() {
+ if (!this.opened) {
+ this.opened = true;
+ return;
+ }
+
+ if (this._focusedIndex < this.options.length - 1) {
+ if (this._focusedIndex > -1) {
+ this.options[this._focusedIndex].isFocused = false;
+ }
+
+ this.options[++this._focusedIndex].isFocused = true;
+ }
+
+ this._scrollIntoView();
+ }
+
+ private _moveFocusedItemUp() {
+
+ if (this._focusedIndex > 0) {
+ this.options[this._focusedIndex].isFocused = false;
+ this.options[--this._focusedIndex].isFocused = true;
+ }
+
+ this._scrollIntoView();
+ }
+
+ private _getViewContainer(): HTMLDivElement {
+ return this.itemListContainer && this.itemListContainer.nativeElement;
+ }
+
+ private _scrollIntoView() {
+ let view = this._getViewContainer();
+ if(!view){
+ return;
+ }
+
+ let firstItem = view.querySelector('li');
+ if (!firstItem) {
+ return null;
+ }
+
+ let viewBottom = view.scrollTop + view.clientHeight;
+ let itemHeight = firstItem.clientHeight;
+
+ // If view needs to scroll down
+ if ((this._focusedIndex + 1) * itemHeight > viewBottom) {
+
+ // If view is scrolled way out of view, then scroll so that selected is top
+ if (viewBottom + itemHeight < (this._focusedIndex + 1) * itemHeight) {
+ view.scrollTop = this._focusedIndex * itemHeight;
+ }
+ else {
+ // If view is incremented out of view, then scroll by a single item
+ view.scrollTop += itemHeight;
+ }
+ }
+ else if (this._focusedIndex * itemHeight <= view.scrollTop) {
+ // If view needs to scroll up
+
+ if (view.scrollTop - itemHeight > this._focusedIndex * itemHeight) {
+ view.scrollTop = this._focusedIndex * itemHeight;
+ }
+ else {
+ view.scrollTop -= itemHeight;
+ }
+ }
+ }
+
private _notifyChangeSubscriptions() {
+ this.opened = false;
+
let displayText = null;
let selectedValues: T[] = [];
@@ -98,17 +214,17 @@ export class MultiDropDownComponent implements OnInit {
if (selectedValues.length === 0) {
this.options.forEach(option => {
option.isSelected = true;
- if(option !== this._selectAllOption){
+ if (option !== this._selectAllOption) {
selectedValues.push(option.value);
}
})
}
if (this._selectAllOption.isSelected) {
- displayText = `All items selected`;
+ displayText = this._ts.instant(PortalResources.allItemsSelected);
}
else if (selectedValues.length > 1) {
- displayText = `${selectedValues.length} items selected`;
+ displayText = this._ts.instant(PortalResources.numItemsSelected).format(selectedValues.length);
}
this.displayText = displayText;
diff --git a/AzureFunctions.AngularClient/src/app/pickers/service-bus/service-bus.component.ts b/AzureFunctions.AngularClient/src/app/pickers/service-bus/service-bus.component.ts
index ab8d99e65d..4ccf07558c 100644
--- a/AzureFunctions.AngularClient/src/app/pickers/service-bus/service-bus.component.ts
+++ b/AzureFunctions.AngularClient/src/app/pickers/service-bus/service-bus.component.ts
@@ -11,6 +11,7 @@ import { Response } from '@angular/http';
import { SelectOption } from '../../shared/models/select-option';
import { TranslateService } from '@ngx-translate/core';
import { PortalResources} from '../../shared/models/portal-resources';
+import { Subscription } from 'rxjs/Subscription';
class OptionTypes {
serviceBus: string = "ServiceBus";
@@ -42,6 +43,7 @@ export class ServiceBusComponent {
private _functionApp: FunctionApp;
private _descriptor: SiteDescriptor;
+ private _subscription: Subscription;
constructor(
private _cacheService: CacheService,
@@ -88,7 +90,10 @@ export class ServiceBusComponent {
onChangeNamespace(value: string) {
this.polices = null;
this.selectedPolicy = null;
- this._cacheService.getArm(value + "/AuthorizationRules", true).subscribe(r => {
+ if (this._subscription) {
+ this._subscription.unsubscribe();
+ }
+ this._subscription = this._cacheService.getArm(value + "/AuthorizationRules", true).subscribe(r => {
this.polices = r.json();
if (this.polices.value.length > 0) {
this.selectedPolicy = this.polices.value[0].id;
diff --git a/AzureFunctions.AngularClient/src/app/radio-selector/radio-selector.component.scss b/AzureFunctions.AngularClient/src/app/radio-selector/radio-selector.component.scss
index 51daa78714..f7400e25f1 100644
--- a/AzureFunctions.AngularClient/src/app/radio-selector/radio-selector.component.scss
+++ b/AzureFunctions.AngularClient/src/app/radio-selector/radio-selector.component.scss
@@ -16,8 +16,8 @@
color: #f2f2f2;
text-align: center;
cursor: pointer;
- padding-left: 40px;
- padding-right: 40px;
+ padding-left: 20px;
+ padding-right: 20px;
}
.switch-input {
diff --git a/AzureFunctions.AngularClient/src/app/shared/function-app.ts b/AzureFunctions.AngularClient/src/app/shared/function-app.ts
index da0d229e5a..7564fe9b1f 100644
--- a/AzureFunctions.AngularClient/src/app/shared/function-app.ts
+++ b/AzureFunctions.AngularClient/src/app/shared/function-app.ts
@@ -1833,17 +1833,17 @@ export class FunctionApp {
});
}
- getSystemKey() {
- let masterKey = this.masterKey
- ? Observable.of(this.masterKey)
- : this.getHostSecretsFromScm().map(r => r.json().masterKey);
+ getSystemKey(): Observable {
+ const masterKey = this.masterKey
+ ? Observable.of(null) // you have to pass something to Observable.of() otherwise it doesn't trigger subscribers.
+ : this.getHostSecretsFromScm();
return masterKey
- .mergeMap(r => {
- let headers = this.getMainSiteHeaders();
+ .mergeMap(_ => {
+ const headers = this.getMainSiteHeaders();
return this._http.get(`${this.mainSiteUrl}/admin/host/systemkeys`, { headers: headers })
.map(r => r.json())
- .do(_ => this._broadcastService.broadcast(BroadcastEvent.ClearError, ErrorIds.unableToGetSystemKey),
+ .do(__ => this._broadcastService.broadcast(BroadcastEvent.ClearError, ErrorIds.unableToGetSystemKey),
(error: FunctionsResponse) => {
if (!error.isHandled) {
this._broadcastService.broadcast(BroadcastEvent.Error, {
diff --git a/AzureFunctions.AngularClient/src/app/shared/models/binding-manager.ts b/AzureFunctions.AngularClient/src/app/shared/models/binding-manager.ts
index f9fd87f538..63d83e5871 100644
--- a/AzureFunctions.AngularClient/src/app/shared/models/binding-manager.ts
+++ b/AzureFunctions.AngularClient/src/app/shared/models/binding-manager.ts
@@ -226,6 +226,13 @@ export class BindingManager {
this.s4() + '-' + this.s4() + this.s4() + this.s4();
}
+ public static isHttpFunction(functionInfo: FunctionInfo) {
+ var inputBinding = (functionInfo.config && functionInfo.config.bindings
+ ? functionInfo.config.bindings.find(e => e.type.toLowerCase() === 'httptrigger')
+ : null);
+ return !!inputBinding;
+ }
+
private s4() {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
diff --git a/AzureFunctions.AngularClient/src/app/shared/models/constants.ts b/AzureFunctions.AngularClient/src/app/shared/models/constants.ts
index 73ea55ac03..aaa7c37ac7 100644
--- a/AzureFunctions.AngularClient/src/app/shared/models/constants.ts
+++ b/AzureFunctions.AngularClient/src/app/shared/models/constants.ts
@@ -41,15 +41,16 @@ export class Constants {
public static ReadOnlyMode = 'readOnly'.toLocaleLowerCase();
}
+export type EnableTabFeature = 'tabs' | 'inplace' | null;
+
export class SiteTabIds{
- public static overview = "Overview";
- public static monitor = "Monitor";
- public static features = "Platform features";
- public static functionRuntime = "Settings";
- public static apiDefinition = "API Definition";
- public static troubleshoot = "Troubleshoot";
- public static deploymentSource = "Deployment Source";
- public static config = "Config";
+ public static readonly overview = "overview";
+ public static readonly monitor = "monitor";
+ public static readonly features = "platformFeatures";
+ public static readonly functionRuntime = "functionRuntimeSettings";
+ public static readonly apiDefinition = "apiDefinition";
+ public static readonly config = "config";
+ public static readonly applicationSettings = "appSettings";
}
export class Arm{
@@ -84,6 +85,10 @@ export class Links{
public static standaloneCreateLearnMore = "https://go.microsoft.com/fwlink/?linkid=848756";
}
+export class LocalStorageKeys{
+ public static readonly siteTabs = "/site/tabs"
+}
+
export class Order {
public static templateOrder: string[] =
[
@@ -118,7 +123,10 @@ export class Order {
}
export class KeyCodes{
+ public static readonly tab = 9;
public static readonly enter = 13;
+ public static readonly space = 32;
+ public static readonly escape = 27;
public static readonly arrowLeft = 37;
public static readonly arrowUp = 38;
public static readonly arrowRight = 39;
diff --git a/AzureFunctions.AngularClient/src/app/shared/models/drop-down-element.ts b/AzureFunctions.AngularClient/src/app/shared/models/drop-down-element.ts
index 56544a534e..b3637b933c 100644
--- a/AzureFunctions.AngularClient/src/app/shared/models/drop-down-element.ts
+++ b/AzureFunctions.AngularClient/src/app/shared/models/drop-down-element.ts
@@ -7,4 +7,5 @@
export interface MultiDropDownElement extends DropDownElement{
isSelected?: boolean;
+ isFocused?: boolean;
}
\ No newline at end of file
diff --git a/AzureFunctions.AngularClient/src/app/shared/models/localStorage/enabled-features.ts b/AzureFunctions.AngularClient/src/app/shared/models/localStorage/enabled-features.ts
index 3c6466f664..cd3bb2ce18 100644
--- a/AzureFunctions.AngularClient/src/app/shared/models/localStorage/enabled-features.ts
+++ b/AzureFunctions.AngularClient/src/app/shared/models/localStorage/enabled-features.ts
@@ -12,7 +12,9 @@ export enum Feature{
ApiDefinition,
WebJobs,
SiteExtensions,
- AppInsight
+ AppInsight,
+ FunctionSettings,
+ AppSettings
}
export interface EnabledFeatures extends StorageItem{
@@ -25,7 +27,7 @@ export interface EnabledFeature{
}
export interface EnabledFeatureItem extends EnabledFeature{
- componentName? : string;
+ featureId? : string;
bladeInfo? : OpenBladeInfo;
iconUrl : string;
}
diff --git a/AzureFunctions.AngularClient/src/app/shared/models/localStorage/local-storage.ts b/AzureFunctions.AngularClient/src/app/shared/models/localStorage/local-storage.ts
index 6738f4c7c7..ed68cfe870 100644
--- a/AzureFunctions.AngularClient/src/app/shared/models/localStorage/local-storage.ts
+++ b/AzureFunctions.AngularClient/src/app/shared/models/localStorage/local-storage.ts
@@ -11,4 +11,8 @@ export interface StoredSubscriptions extends StorageItem{
export interface QuickstartSettings extends StorageItem{
disabled : boolean;
-}
\ No newline at end of file
+}
+
+export interface TabSettings extends StorageItem{
+ dynamicTabId : string;
+}
\ No newline at end of file
diff --git a/AzureFunctions.AngularClient/src/app/shared/models/portal-resources.ts b/AzureFunctions.AngularClient/src/app/shared/models/portal-resources.ts
index e7c2e51335..a26e4dc46e 100644
--- a/AzureFunctions.AngularClient/src/app/shared/models/portal-resources.ts
+++ b/AzureFunctions.AngularClient/src/app/shared/models/portal-resources.ts
@@ -6,6 +6,9 @@ export class PortalResources
public static azureFunctionsRuntime: string = "azureFunctionsRuntime";
public static cancel: string = "cancel";
public static configure: string = "configure";
+ public static selectAll: string = "selectAll";
+ public static allItemsSelected: string = "allItemsSelected";
+ public static numItemsSelected: string = "numItemsSelected";
public static functionCreateErrorDetails: string = "functionCreateErrorDetails";
public static functionCreateErrorMessage: string = "functionCreateErrorMessage";
public static functionDev_functionErrorDetails: string = "functionDev_functionErrorDetails";
@@ -369,6 +372,7 @@ export class PortalResources
public static feature_backupsInfo: string = "feature_backupsInfo";
public static feature_allSettingsName: string = "feature_allSettingsName";
public static feature_allSettingsInfo: string = "feature_allSettingsInfo";
+ public static feature_functionSettingsInfo: string = "feature_functionSettingsInfo";
public static feature_generalSettings: string = "feature_generalSettings";
public static feature_codeDeployment: string = "feature_codeDeployment";
public static feature_developmentTools: string = "feature_developmentTools";
@@ -421,6 +425,7 @@ export class PortalResources
public static tab_overview: string = "tab_overview";
public static tab_features: string = "tab_features";
public static tab_settings: string = "tab_settings";
+ public static tab_functionSettings: string = "tab_functionSettings";
public static tab_configuration: string = "tab_configuration";
public static try_appDisabled: string = "try_appDisabled";
public static template: string = "template";
@@ -532,7 +537,6 @@ export class PortalResources
public static keysDialog_getFunctionUrl: string = "keysDialog_getFunctionUrl";
public static keysDialog_key: string = "keysDialog_key";
public static keysDialog_url: string = "keysDialog_url";
- public static keysDialog_urlAndAdminKey: string = "keysDialog_urlAndAdminKey";
public static downloadFunctionAppContent: string = "downloadFunctionAppContent";
public static functionKeys_renewConfirmation: string = "functionKeys_renewConfirmation";
public static emptyBrowse: string = "emptyBrowse";
@@ -581,6 +585,7 @@ export class PortalResources
public static serviceBusPicker_serviceBus: string = "serviceBusPicker_serviceBus";
public static bindingInput_appSettingNotFound: string = "bindingInput_appSettingNotFound";
public static bindingInput_show: string = "bindingInput_show";
+ public static appSettingPicker_add: string = "appSettingPicker_add";
public static download: string = "download";
public static downloadFunctionAppContent_includeAppSettings: string = "downloadFunctionAppContent_includeAppSettings";
public static downloadFunctionAppContent_includeAppSettingsHelp: string = "downloadFunctionAppContent_includeAppSettingsHelp";
diff --git a/AzureFunctions.AngularClient/src/app/shared/services/local-storage.service.ts b/AzureFunctions.AngularClient/src/app/shared/services/local-storage.service.ts
index a2e0de9178..dec41d00c5 100644
--- a/AzureFunctions.AngularClient/src/app/shared/services/local-storage.service.ts
+++ b/AzureFunctions.AngularClient/src/app/shared/services/local-storage.service.ts
@@ -1,3 +1,4 @@
+import { LocalStorageKeys } from './../models/constants';
import { PortalService } from './portal.service';
import {LogEntryLevel} from '../models/portal';
import {Injectable, EventEmitter} from '@angular/core';
@@ -6,7 +7,7 @@ import {EnabledFeature, Feature} from '../models/localStorage/enabled-features';
@Injectable()
export class LocalStorageService {
- private _apiVersion = "2017-02-01";
+ private _apiVersion = "2017-06-01";
private _apiVersionKey = "appsvc-api-version";
constructor(private _portalService : PortalService){
@@ -14,6 +15,9 @@ export class LocalStorageService {
if(!apiVersion || apiVersion !== this._apiVersion){
this._resetStorage();
}
+
+ // Ensures that saving tab state should only happen per-session
+ localStorage.removeItem(LocalStorageKeys.siteTabs);
}
getItem(key : string) : StorageItem{
@@ -38,6 +42,10 @@ export class LocalStorageService {
}
}
+ removeItem(key : string){
+ localStorage.removeItem(key);
+ }
+
private _resetStorage(){
localStorage.clear();
localStorage.setItem(this._apiVersionKey, this._apiVersion);
diff --git a/AzureFunctions.AngularClient/src/app/side-nav/side-nav.component.html b/AzureFunctions.AngularClient/src/app/side-nav/side-nav.component.html
index 0e4274db14..01dce2ce9f 100644
--- a/AzureFunctions.AngularClient/src/app/side-nav/side-nav.component.html
+++ b/AzureFunctions.AngularClient/src/app/side-nav/side-nav.component.html
@@ -24,7 +24,7 @@
id="tree-view-container"
(focus)="onFocus($event)"
(blur)="onBlur($event)"
- (keydown)="onKeyDown($event)">
+ (keydown)="onKeyPress($event)">
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 d91fe34395..7b078a5332 100644
--- a/AzureFunctions.AngularClient/src/app/side-nav/side-nav.component.ts
+++ b/AzureFunctions.AngularClient/src/app/side-nav/side-nav.component.ts
@@ -1,14 +1,14 @@
import { SearchBoxComponent } from './../search-box/search-box.component';
import { TreeNodeIterator } from './../tree-view/tree-node-iterator';
import { Component, OnInit, EventEmitter, OnDestroy, Output, Input, ViewChild, AfterViewInit } from '@angular/core';
-import {Http} from '@angular/http';
+import { Http } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
import { ReplaySubject } from 'rxjs/ReplaySubject';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/mergeMap';
import 'rxjs/add/observable/of';
-import {TranslateService} from '@ngx-translate/core';
+import { TranslateService } from '@ngx-translate/core';
import { ConfigService } from './../shared/services/config.service';
import { FunctionInfo } from './../shared/models/function-info';
import { FunctionApp } from './../shared/function-app';
@@ -22,52 +22,52 @@ import { ArmArrayResult } from './../shared/models/arm/arm-obj';
import { WebsiteId } from './../shared/models/portal';
import { StorageItem, StoredSubscriptions } from './../shared/models/localStorage/local-storage';
import { LocalStorageService } from './../shared/services/local-storage.service';
-import {TreeNode} from '../tree-view/tree-node';
-import {AppsNode} from '../tree-view/apps-node';
-import {AppNode} from '../tree-view/app-node';
-import {TreeViewComponent} from '../tree-view/tree-view.component';
-import {ArmService} from '../shared/services/arm.service';
-import {CacheService} from '../shared/services/cache.service';
-import {UserService} from '../shared/services/user.service';
-import {FunctionsService} from '../shared/services/functions.service';
-import {GlobalStateService} from '../shared/services/global-state.service';
-import {BroadcastService} from '../shared/services/broadcast.service';
-import {AiService} from '../shared/services/ai.service';
-import {DropDownComponent} from '../drop-down/drop-down.component';
-import {DropDownElement} from '../shared/models/drop-down-element';
-import {TreeViewInfo} from '../tree-view/models/tree-view-info';
-import {DashboardType} from '../tree-view/models/dashboard-type';
-import {Subscription} from '../shared/models/subscription';
-import {SlotsService} from './../shared/services/slots.service';
+import { TreeNode } from '../tree-view/tree-node';
+import { AppsNode } from '../tree-view/apps-node';
+import { AppNode } from '../tree-view/app-node';
+import { TreeViewComponent } from '../tree-view/tree-view.component';
+import { ArmService } from '../shared/services/arm.service';
+import { CacheService } from '../shared/services/cache.service';
+import { UserService } from '../shared/services/user.service';
+import { FunctionsService } from '../shared/services/functions.service';
+import { GlobalStateService } from '../shared/services/global-state.service';
+import { BroadcastService } from '../shared/services/broadcast.service';
+import { AiService } from '../shared/services/ai.service';
+import { DropDownComponent } from '../drop-down/drop-down.component';
+import { DropDownElement } from '../shared/models/drop-down-element';
+import { TreeViewInfo } from '../tree-view/models/tree-view-info';
+import { DashboardType } from '../tree-view/models/dashboard-type';
+import { Subscription } from '../shared/models/subscription';
+import { SlotsService } from './../shared/services/slots.service';
@Component({
- selector: 'side-nav',
- templateUrl: './side-nav.component.html',
- styleUrls: ['./side-nav.component.scss'],
- inputs: ['tryFunctionAppInput']
+ selector: 'side-nav',
+ templateUrl: './side-nav.component.html',
+ styleUrls: ['./side-nav.component.scss'],
+ inputs: ['tryFunctionAppInput']
})
export class SideNavComponent implements AfterViewInit {
@Output() treeViewInfoEvent: EventEmitter
;
@ViewChild('treeViewContainer') treeViewContainer;
- @ViewChild(SearchBoxComponent) searchBox : SearchBoxComponent;
+ @ViewChild(SearchBoxComponent) searchBox: SearchBoxComponent;
- public rootNode : TreeNode;
+ public rootNode: TreeNode;
public subscriptionOptions: DropDownElement[] = [];
- public selectedSubscriptions : Subscription[] = [];
+ public selectedSubscriptions: Subscription[] = [];
public subscriptionsDisplayText = "";
- public resourceId : string;
- public initialResourceId : string;
+ public resourceId: string;
+ public initialResourceId: string;
public searchTerm = "";
public hasValue = false;
- public tryFunctionApp : FunctionApp;
+ public tryFunctionApp: FunctionApp;
- public selectedNode : TreeNode;
- public selectedDashboardType : DashboardType;
+ public selectedNode: TreeNode;
+ public selectedDashboardType: DashboardType;
- private _focusedNode : TreeNode; // For keyboard navigation
- private _iterator : TreeNodeIterator;
+ private _focusedNode: TreeNode; // For keyboard navigation
+ private _iterator: TreeNodeIterator;
private _savedSubsKey = "/subscriptions/selectedIds";
private _subscriptionsStream = new ReplaySubject(1);
@@ -77,38 +77,38 @@ export class SideNavComponent implements AfterViewInit {
private _tryFunctionAppStream = new Subject();
- set tryFunctionAppInput(functionApp : FunctionApp){
- if(functionApp){
+ set tryFunctionAppInput(functionApp: FunctionApp) {
+ if (functionApp) {
this._tryFunctionAppStream.next(functionApp);
}
}
constructor(
- public configService : ConfigService,
- public armService : ArmService,
- public cacheService : CacheService,
- public functionsService : FunctionsService,
- public http : Http,
- public globalStateService : GlobalStateService,
- public broadcastService : BroadcastService,
- public translateService : TranslateService,
- public userService : UserService,
- public aiService : AiService,
- public localStorageService : LocalStorageService,
- public portalService : PortalService,
- public languageService : LanguageService,
- public authZService : AuthzService,
- public slotsService: SlotsService){
+ public configService: ConfigService,
+ public armService: ArmService,
+ public cacheService: CacheService,
+ public functionsService: FunctionsService,
+ public http: Http,
+ public globalStateService: GlobalStateService,
+ public broadcastService: BroadcastService,
+ public translateService: TranslateService,
+ public userService: UserService,
+ public aiService: AiService,
+ public localStorageService: LocalStorageService,
+ public portalService: PortalService,
+ public languageService: LanguageService,
+ public authZService: AuthzService,
+ public slotsService: SlotsService) {
this.treeViewInfoEvent = new EventEmitter();
- userService.getStartupInfo().subscribe(info =>{
+ userService.getStartupInfo().subscribe(info => {
// This is a workaround for the fact that Ibiza sends us an updated info whenever
// child blades close. If we get a new info object, then we'll rebuild the tree.
// The true fix would be to make sure that we never set the resourceId of the hosting
// blade, but that's a pretty large change and this should be sufficient for now.
- if(!this._initialized){
+ if (!this._initialized) {
this._initialized = true;
// this.resourceId = !!this.resourceId ? this.resourceId : info.resourceId;
this.initialResourceId = info.resourceId;
@@ -128,76 +128,76 @@ export class SideNavComponent implements AfterViewInit {
appsNode.parent = this.rootNode;
// Need to allow the appsNode to wire up its subscriptions
- setTimeout(() =>{
+ setTimeout(() => {
appsNode.select();
}, 10);
this._searchTermStream
- .subscribe(term =>{
- this.searchTerm = term;
- })
+ .subscribe(term => {
+ this.searchTerm = term;
+ })
// Get the streams in the top-level nodes moving
- if(this.initialResourceId){
+ if (this.initialResourceId) {
let descriptor = Descriptor.getDescriptor(this.initialResourceId);
- if(descriptor.site){
+ if (descriptor.site) {
this._searchTermStream.next(`"${descriptor.site}"`);
this.hasValue = true;
}
- else{
+ else {
this._searchTermStream.next("");
}
}
- else{
+ else {
this._searchTermStream.next("");
}
- if(this.subscriptionOptions.length === 0){
+ if (this.subscriptionOptions.length === 0) {
this._setupInitialSubscriptions(info.resourceId);
}
}
});
this._tryFunctionAppStream
- .mergeMap(tryFunctionApp => {
- this.tryFunctionApp = tryFunctionApp;
- return tryFunctionApp.getFunctions();
- })
- .subscribe(functions =>{
- this.globalStateService.clearBusyState();
+ .mergeMap(tryFunctionApp => {
+ this.tryFunctionApp = tryFunctionApp;
+ return tryFunctionApp.getFunctions();
+ })
+ .subscribe(functions => {
+ this.globalStateService.clearBusyState();
- let functionInfo : FunctionInfo = null;
- if(functions && functions.length > 0){
- this.initialResourceId = `${this.tryFunctionApp.site.id}/functions/${functions[0].name}`;
- }
- else{
- this.initialResourceId = this.tryFunctionApp.site.id;
- }
+ let functionInfo: FunctionInfo = null;
+ if (functions && functions.length > 0) {
+ this.initialResourceId = `${this.tryFunctionApp.site.id}/functions/${functions[0].name}`;
+ }
+ else {
+ this.initialResourceId = this.tryFunctionApp.site.id;
+ }
- let appNode = new AppNode(
- this,
- this.tryFunctionApp.site,
- this.rootNode,
- [],
- false);
+ let appNode = new AppNode(
+ this,
+ this.tryFunctionApp.site,
+ this.rootNode,
+ [],
+ false);
- appNode.select();
+ appNode.select();
- this.rootNode = new TreeNode(this, null, null);
- this.rootNode.children = [appNode];
- this.rootNode.isExpanded = true;
- })
+ this.rootNode = new TreeNode(this, null, null);
+ this.rootNode.children = [appNode];
+ this.rootNode.isExpanded = true;
+ })
}
- ngAfterViewInit(){
+ ngAfterViewInit() {
// Search box is not available for Try Functions
- if(this.searchBox){
+ if (this.searchBox) {
this.searchBox.focus();
}
}
- onFocus(event : FocusEvent){
- if(!this._focusedNode){
+ onFocus(event: FocusEvent) {
+ if (!this._focusedNode) {
this._focusedNode = this.rootNode.children[0];
this._iterator = new TreeNodeIterator(this._focusedNode);
}
@@ -205,64 +205,120 @@ export class SideNavComponent implements AfterViewInit {
this._focusedNode.isFocused = true;
}
- onBlur(event : FocusEvent){
- if(this._focusedNode){
+ onBlur(event: FocusEvent) {
+ if (this._focusedNode) {
// Keep the focused node around in case user navigates back to it
this._focusedNode.isFocused = false;
}
}
- onKeyDown(event : KeyboardEvent){
- if(event.keyCode === KeyCodes.arrowDown){
- this._moveDown();
+ onKeyPress(event: KeyboardEvent) {
+ if (event.keyCode === KeyCodes.arrowDown) {
+ this._moveFocusedItemDown();
}
- else if(event.keyCode === KeyCodes.arrowUp){
- this._moveUp();
+ else if (event.keyCode === KeyCodes.arrowUp) {
+ this._moveFocusedItemUp();
}
- else if(event.keyCode === KeyCodes.enter){
+ else if (event.keyCode === KeyCodes.enter) {
this._focusedNode.select();
}
- else if(event.keyCode === KeyCodes.arrowRight){
- if(this._focusedNode.showExpandIcon && !this._focusedNode.isExpanded){
+ else if (event.keyCode === KeyCodes.arrowRight) {
+ if (this._focusedNode.showExpandIcon && !this._focusedNode.isExpanded) {
this._focusedNode.toggle(event);
}
- else{
- this._moveDown();
+ else {
+ this._moveFocusedItemDown();
}
}
- else if(event.keyCode === KeyCodes.arrowLeft){
- if(this._focusedNode.showExpandIcon && this._focusedNode.isExpanded){
+ else if (event.keyCode === KeyCodes.arrowLeft) {
+ if (this._focusedNode.showExpandIcon && this._focusedNode.isExpanded) {
this._focusedNode.toggle(event);
}
- else{
- this._moveUp();
+ else {
+ this._moveFocusedItemUp();
}
}
+
+ if (event.keyCode !== KeyCodes.tab) {
+ // Prevents the entire page from scrolling on up/down key press
+ event.preventDefault();
+ }
}
- private _moveDown(){
+ private _getViewContainer(): HTMLDivElement {
+ let treeViewContainer = this.treeViewContainer && this.treeViewContainer.nativeElement;
+
+ if (!treeViewContainer) {
+ return null;
+ }
+
+ return treeViewContainer.querySelector('.top-level-children');
+ }
+
+ private _moveFocusedItemDown() {
+
let nextNode = this._iterator.next();
- if(nextNode){
+ if (nextNode) {
this._focusedNode.isFocused = false;
this._focusedNode = nextNode;
+ this._scrollIntoView();
}
this._focusedNode.isFocused = true;
}
- private _moveUp(){
+ private _moveFocusedItemUp() {
let prevNode = this._iterator.previous();
- if(prevNode){
+ if (prevNode) {
this._focusedNode.isFocused = false;
this._focusedNode = prevNode;
+ this._scrollIntoView();
}
this._focusedNode.isFocused = true;
}
- private _changeFocus(node : TreeNode){
- if(this._focusedNode){
+ private _scrollIntoView(){
+ setTimeout(() =>{
+ let view = this._getViewContainer();
+ if(!view){
+ return;
+ }
+
+ let node = view.querySelector('div.tree-node.focused');
+ if(!node){
+ return;
+ }
+
+ let viewBottom = view.scrollTop + view.clientHeight;
+
+ // If view needs to scroll down
+ if(node.offsetTop + node.clientHeight > viewBottom){
+
+ // If view is scrolled way out of view, then scroll so that selected is top
+ if(viewBottom + node.clientHeight < node.offsetTop){
+ view.scrollTop = node.offsetTop;
+ }
+ else{
+ view.scrollTop += node.clientHeight + 1; // +1 for margin
+ }
+ }
+ else if(node.offsetTop < view.scrollTop){
+ // If view needs to scroll up
+ if(view.scrollTop - node.clientHeight > node.offsetTop){
+ view.scrollTop = node.offsetTop;
+ }
+ else{
+ view.scrollTop -= node.clientHeight - 1; // -1 for margin
+ }
+ }
+
+ }, 0);
+ }
+
+ private _changeFocus(node: TreeNode) {
+ if (this._focusedNode) {
this._focusedNode.isFocused = false;
node.isFocused = true;
this._iterator = new TreeNodeIterator(node);
@@ -270,15 +326,15 @@ export class SideNavComponent implements AfterViewInit {
}
}
- updateView(newSelectedNode : TreeNode, newDashboardType : DashboardType, force? : boolean) : Observable{
- if(this.selectedNode){
+ updateView(newSelectedNode: TreeNode, newDashboardType: DashboardType, force?: boolean): Observable {
+ if (this.selectedNode) {
- if(!force && this.selectedNode === newSelectedNode && this.selectedDashboardType === newDashboardType){
+ if (!force && this.selectedNode === newSelectedNode && this.selectedDashboardType === newDashboardType) {
return Observable.of(false);
}
- else{
+ else {
- if(this.selectedNode.shouldBlockNavChange()){
+ if (this.selectedNode.shouldBlockNavChange()) {
return Observable.of(false);
}
@@ -293,10 +349,10 @@ export class SideNavComponent implements AfterViewInit {
this.resourceId = newSelectedNode.resourceId;
let viewInfo = {
- resourceId : newSelectedNode.resourceId,
- dashboardType : newDashboardType,
- node : newSelectedNode,
- data : {}
+ resourceId: newSelectedNode.resourceId,
+ dashboardType: newDashboardType,
+ node: newSelectedNode,
+ data: {}
};
this.globalStateService.setDisabledMessage(null);
@@ -308,57 +364,57 @@ export class SideNavComponent implements AfterViewInit {
return newSelectedNode.handleSelection();
}
- private _logDashboardTypeChange(oldDashboard : DashboardType, newDashboard : DashboardType){
+ private _logDashboardTypeChange(oldDashboard: DashboardType, newDashboard: DashboardType) {
let oldDashboardType = DashboardType[oldDashboard];
let newDashboardType = DashboardType[newDashboard];
this.aiService.trackEvent('/sidenav/change-dashboard', {
- source : oldDashboardType,
- dest : newDashboardType
+ source: oldDashboardType,
+ dest: newDashboardType
})
}
- private _updateTitle(node : TreeNode){
+ private _updateTitle(node: TreeNode) {
let pathNames = node.getTreePathNames();
let title = "";
let subtitle = "";
- for(let i = 0; i < pathNames.length; i++){
- if(i % 2 === 1){
+ for (let i = 0; i < pathNames.length; i++) {
+ if (i % 2 === 1) {
title += pathNames[i] + " - ";
}
}
// Remove trailing dash
- if(title.length > 3){
+ if (title.length > 3) {
title = title.substring(0, title.length - 3);
}
- if(!title){
+ if (!title) {
title = this.translateService.instant(PortalResources.functionApps);
subtitle = "";
}
- else{
+ else {
subtitle = this.translateService.instant(PortalResources.functionApps);;
}
this.portalService.updateBladeInfo(title, subtitle);
}
- clearView(resourceId : string){
+ clearView(resourceId: string) {
// We only want to clear the view if the user is currently looking at something
// under the tree path being deleted
- if(this.resourceId.startsWith(resourceId)){
+ if (this.resourceId.startsWith(resourceId)) {
this.treeViewInfoEvent.emit(null);
}
}
- search(event : any){
- if(typeof event === "string"){
+ search(event: any) {
+ if (typeof event === "string") {
this._searchTermStream.next(event);
this.hasValue = !!event;
}
- else{
+ else {
this.hasValue = !!event.target.value;
let startPos = event.target.selectionStart;
@@ -369,8 +425,8 @@ export class SideNavComponent implements AfterViewInit {
// it's still not great because if the user types really fast, the cursor still moves.
this._searchTermStream.next(event.target.value);
- if(event.target.value.length !== startPos){
- setTimeout(() =>{
+ if (event.target.value.length !== startPos) {
+ setTimeout(() => {
event.target.selectionStart = startPos;
event.target.selectionEnd = endPos;
});
@@ -378,43 +434,43 @@ export class SideNavComponent implements AfterViewInit {
}
}
- searchExact(term : string){
+ searchExact(term: string) {
this.hasValue = !!term;
this._searchTermStream.next(`"${term}"`);
}
- clearSearch(){
+ clearSearch() {
this.hasValue = false;
this._searchTermStream.next("");
}
onSubscriptionsSelect(subscriptions: Subscription[]) {
- let subIds : string[];
+ let subIds: string[];
- if(subscriptions.length === this.subscriptionOptions.length){
+ if (subscriptions.length === this.subscriptionOptions.length) {
subIds = []; // Equivalent of all subs
}
- else{
+ else {
subIds = subscriptions.map(s => s.subscriptionId);
}
- let storedSelectedSubIds : StoredSubscriptions ={
- id : this._savedSubsKey,
- subscriptions : subIds
+ let storedSelectedSubIds: StoredSubscriptions = {
+ id: this._savedSubsKey,
+ subscriptions: subIds
}
this.localStorageService.setItem(storedSelectedSubIds.id, storedSelectedSubIds);
this.selectedSubscriptions = subscriptions;
this._subscriptionsStream.next(subscriptions);
- if(subscriptions.length === this.subscriptionOptions.length){
+ if (subscriptions.length === this.subscriptionOptions.length) {
this._updateSubDisplayText(this.translateService.instant(PortalResources.sideNav_AllSubscriptions));
}
- else if(subscriptions.length > 1){
+ else if (subscriptions.length > 1) {
this._updateSubDisplayText(this.translateService.instant(PortalResources.sideNav_SubscriptionCount).format(subscriptions.length));
}
- else{
+ else {
this._updateSubDisplayText(`${subscriptions[0].displayName}`);
}
}
@@ -422,19 +478,19 @@ export class SideNavComponent implements AfterViewInit {
// The multi-dropdown component has its own default display text values,
// so we need to make sure we're always overwriting them. But if we simply
// set the value to the same value twice, no change notification will happen.
- private _updateSubDisplayText(displayText : string){
+ private _updateSubDisplayText(displayText: string) {
this.subscriptionsDisplayText = "";
- setTimeout(() =>{
+ setTimeout(() => {
this.subscriptionsDisplayText = displayText;
}, 10);
}
- private _setupInitialSubscriptions(resourceId : string){
+ private _setupInitialSubscriptions(resourceId: string) {
let savedSubs = this.localStorageService.getItem(this._savedSubsKey);
let savedSelectedSubscriptionIds = savedSubs ? savedSubs.subscriptions : [];
- let descriptor : SiteDescriptor;
+ let descriptor: SiteDescriptor;
- if(resourceId){
+ if (resourceId) {
descriptor = new SiteDescriptor(resourceId);
}
@@ -444,35 +500,35 @@ export class SideNavComponent implements AfterViewInit {
this._subscriptionsStream.next([]);
this.userService.getStartupInfo()
- .first()
- .subscribe(info => {
- let count = 0;
-
- this.subscriptionOptions =
- info.subscriptions.map(e => {
- let subSelected: boolean;
-
- if(descriptor){
- subSelected = descriptor.subscription === e.subscriptionId;
- } else {
- // Multi-dropdown defaults to all of none is selected. So setting it here
- // helps us figure out whether we need to limit the # of initial subscriptions
- subSelected =
- savedSelectedSubscriptionIds.length === 0
- || savedSelectedSubscriptionIds.findIndex(s => s === e.subscriptionId) > -1;
- }
-
- if(subSelected){
- count++;
- }
-
- return {
- displayLabel: e.displayName,
- value: e,
- isSelected : subSelected && count <= Arm.MaxSubscriptionBatchSize
- };
- })
- .sort((a, b) => a.displayLabel.localeCompare(b.displayLabel));
- });
+ .first()
+ .subscribe(info => {
+ let count = 0;
+
+ this.subscriptionOptions =
+ info.subscriptions.map(e => {
+ let subSelected: boolean;
+
+ if (descriptor) {
+ subSelected = descriptor.subscription === e.subscriptionId;
+ } else {
+ // Multi-dropdown defaults to all of none is selected. So setting it here
+ // helps us figure out whether we need to limit the # of initial subscriptions
+ subSelected =
+ savedSelectedSubscriptionIds.length === 0
+ || savedSelectedSubscriptionIds.findIndex(s => s === e.subscriptionId) > -1;
+ }
+
+ if (subSelected) {
+ count++;
+ }
+
+ return {
+ displayLabel: e.displayName,
+ value: e,
+ isSelected: subSelected && count <= Arm.MaxSubscriptionBatchSize
+ };
+ })
+ .sort((a, b) => a.displayLabel.localeCompare(b.displayLabel));
+ });
}
}
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 4adf5a75bc..cbf4f7ee0b 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,5 @@
+import { TabsComponent } from './../../tabs/tabs.component';
+import { BusyStateComponent } from './../../busy-state/busy-state.component';
import { EditModeHelper } from './../../shared/Utilities/edit-mode.helper';
import { Component, Input, EventEmitter, OnInit, OnDestroy } from '@angular/core';
import { Response } from '@angular/http';
@@ -76,6 +78,7 @@ export class FunctionRuntimeComponent implements OnDestroy {
public slotsEnabled: boolean;
private slotsValueChange: Subject;
private _numSlots: number = 0;
+ private _busyState : BusyStateComponent;
constructor(
private _armService: ArmService,
@@ -86,14 +89,17 @@ export class FunctionRuntimeComponent implements OnDestroy {
private _globalStateService: GlobalStateService,
private _aiService: AiService,
private _translateService: TranslateService,
- private _slotsService: SlotsService
+ private _slotsService: SlotsService,
+ tabsComponent : TabsComponent
) {
+ this._busyState = tabsComponent.busyState;
+
this.showTryView = this._globalStateService.showTryView;
this._viewInfoSub = this._viewInfoStream
.switchMap(viewInfo => {
this._viewInfo = viewInfo;
- this._globalStateService.setBusyState();
+ this._busyState.setBusyState();
this._appNode = (viewInfo.node);
@@ -167,7 +173,7 @@ export class FunctionRuntimeComponent implements OnDestroy {
} else {
this.functionAppEditMode = true;
}
- this._globalStateService.clearBusyState();
+ this._busyState.clearBusyState();
let traceKey = this._viewInfo.data.siteTraceKey;
this._aiService.stopTrace('/site/function-runtime-tab-ready', traceKey);
@@ -210,7 +216,7 @@ export class FunctionRuntimeComponent implements OnDestroy {
this.proxySettingValueStream = new Subject();
this.proxySettingValueStream
.subscribe((value: boolean) => {
- this._globalStateService.setBusyState();
+ this._busyState.setBusyState();
let appSettingValue: string = value ? Constants.routingExtensionVersion : Constants.disabled;
this._cacheService.postArm(`${this.site.id}/config/appsettings/list`, true)
@@ -222,7 +228,7 @@ export class FunctionRuntimeComponent implements OnDestroy {
this.apiProxiesEnabled = value;
this.needUpdateRoutingExtensionVersion = false;
this.routingExtensionVersion = Constants.routingExtensionVersion;
- this._globalStateService.clearBusyState();
+ this._busyState.clearBusyState();
this._cacheService.clearArmIdCachePrefix(this.site.id);
});
});
@@ -231,7 +237,7 @@ export class FunctionRuntimeComponent implements OnDestroy {
this.functionEditModeValueStream
.switchMap(state => {
let originalState = this.functionAppEditMode;
- this._globalStateService.setBusyState();
+ this._busyState.setBusyState();
this.functionAppEditMode = state;
let appSetting = this.functionAppEditMode ? Constants.ReadWriteMode : Constants.ReadOnlyMode;
return this._cacheService.postArm(`${this.site.id}/config/appsettings/list`, true)
@@ -244,7 +250,7 @@ export class FunctionRuntimeComponent implements OnDestroy {
})
.do(null, originalState => {
this.functionAppEditMode = originalState;
- this._globalStateService.clearBusyState();
+ this._busyState.clearBusyState();
this._broadcastService.broadcast(BroadcastEvent.Error, {
message: this._translateService.instant(PortalResources.error_unableToUpdateFunctionAppEditMode),
errorType: ErrorType.ApiError,
@@ -257,26 +263,26 @@ export class FunctionRuntimeComponent implements OnDestroy {
// getFunctionAppEditMode returns a subject and updates it on demand.
.mergeMap(_ => this.functionApp.getFunctionAppEditMode())
.subscribe(fi => {
- this._globalStateService.clearBusyState();
+ this._busyState.clearBusyState();
});
this.slotsValueChange = new Subject();
this.slotsValueChange.subscribe((value: boolean) => {
- this._globalStateService.setBusyState();
+ this._busyState.setBusyState();
let slotsSettingsValue: string = value ? Constants.slotsSecretStorageSettingsValue : Constants.disabled;
this._cacheService.postArm(`${this.site.id}/config/appsettings/list`, true)
.mergeMap(r => {
return this._slotsService.setStatusOfSlotOptIn(this.site, r.json(), slotsSettingsValue);
})
.do(null, e => {
- this._globalStateService.clearBusyState();
+ this._busyState.clearBusyState();
this._aiService.trackException(e, 'function-runtime')
})
.retry()
.subscribe(r => {
this.functionApp.fireSyncTrigger();
this.slotsEnabled = value;
- this._globalStateService.clearBusyState();
+ this._busyState.clearBusyState();
this._cacheService.clearArmIdCachePrefix(this.site.id);
});
});
@@ -302,9 +308,9 @@ export class FunctionRuntimeComponent implements OnDestroy {
}
saveMemorySize(value: string | number) {
- this._globalStateService.setBusyState();
+ this._busyState.setBusyState();
this._updateMemorySize(this.site, value)
- .subscribe(r => { this._globalStateService.clearBusyState(); Object.assign(this.site, r); this.dirty = false; });
+ .subscribe(r => { this._busyState.clearBusyState(); Object.assign(this.site, r); this.dirty = false; });
}
isIE(): boolean {
@@ -313,14 +319,14 @@ export class FunctionRuntimeComponent implements OnDestroy {
updateVersion() {
this._aiService.trackEvent('/actions/app_settings/update_version');
- this._globalStateService.setBusyState();
+ this._busyState.setBusyState();
this._cacheService.postArm(`${this.site.id}/config/appsettings/list`, true)
.mergeMap(r => {
return this._updateContainerVersion(this.site, r.json());
})
.subscribe(r => {
this.needUpdateExtensionVersion = false;
- this._globalStateService.clearBusyState();
+ this._busyState.clearBusyState();
this._cacheService.clearArmIdCachePrefix(this.site.id);
this._appNode.clearNotification(NotificationIds.newRuntimeVersion);
});
@@ -328,7 +334,7 @@ export class FunctionRuntimeComponent implements OnDestroy {
updateRoutingExtensionVersion() {
this._aiService.trackEvent('/actions/app_settings/update_routing_version');
- this._globalStateService.setBusyState();
+ this._busyState.setBusyState();
this._cacheService.postArm(`${this.site.id}/config/appsettings/list`, true)
.mergeMap(r => {
@@ -336,7 +342,7 @@ export class FunctionRuntimeComponent implements OnDestroy {
})
.subscribe(r => {
this.needUpdateRoutingExtensionVersion = false;
- this._globalStateService.clearBusyState();
+ this._busyState.clearBusyState();
this._cacheService.clearArmIdCachePrefix(this.site.id);
});
}
@@ -347,28 +353,28 @@ export class FunctionRuntimeComponent implements OnDestroy {
let dailyMemoryTimeQuota = +this.dailyMemoryTimeQuota;
if (dailyMemoryTimeQuota > 0) {
- this._globalStateService.setBusyState();
+ this._busyState.setBusyState();
this._updateDailyMemory(this.site, dailyMemoryTimeQuota).subscribe((r) => {
var site = r.json();
this.showDailyMemoryWarning = (!site.properties.enabled && site.properties.siteDisabledReason === 1);
this.showDailyMemoryInfo = true;
this.site.properties.dailyMemoryTimeQuota = dailyMemoryTimeQuota;
this.dailyMemoryTimeQuotaOriginal = this.dailyMemoryTimeQuota;
- this._globalStateService.clearBusyState();
+ this._busyState.clearBusyState();
});
}
}
}
removeQuota() {
- this._globalStateService.setBusyState();
+ this._busyState.setBusyState();
this._updateDailyMemory(this.site, 0).subscribe(() => {
this.showDailyMemoryInfo = false;
this.showDailyMemoryWarning = false;
this.dailyMemoryTimeQuota = '';
this.dailyMemoryTimeQuotaOriginal = this.dailyMemoryTimeQuota;
this.site.properties.dailyMemoryTimeQuota = 0;
- this._globalStateService.clearBusyState();
+ this._busyState.clearBusyState();
});
}
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 a9bb5fe626..d8f23d017b 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
@@ -2,23 +2,40 @@
-
+
-
+
-
+
+
-
+
+
+
+
+
+
-
+
+
diff --git a/AzureFunctions.AngularClient/src/app/site/site-dashboard/site-dashboard.component.scss b/AzureFunctions.AngularClient/src/app/site/site-dashboard/site-dashboard.component.scss
index da81d5fd7a..db97545404 100644
--- a/AzureFunctions.AngularClient/src/app/site/site-dashboard/site-dashboard.component.scss
+++ b/AzureFunctions.AngularClient/src/app/site/site-dashboard/site-dashboard.component.scss
@@ -1,5 +1,10 @@
@import '../../../sass/main';
+#site-dashboard-container{
+ padding-top: 5px;
+ background-color: $chrome-color;
+}
+
#site-dashboard-pin{
display: inline-block;
height: 14px;
diff --git a/AzureFunctions.AngularClient/src/app/site/site-dashboard/site-dashboard.component.ts b/AzureFunctions.AngularClient/src/app/site/site-dashboard/site-dashboard.component.ts
index d8eff61802..7acd46c75b 100644
--- a/AzureFunctions.AngularClient/src/app/site/site-dashboard/site-dashboard.component.ts
+++ b/AzureFunctions.AngularClient/src/app/site/site-dashboard/site-dashboard.component.ts
@@ -1,7 +1,10 @@
+import { Url } from './../../shared/Utilities/url';
+import { LocalStorageService } from './../../shared/services/local-storage.service';
import { Component, OnInit, EventEmitter, Input, ViewChild } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
+import { ReplaySubject } from 'rxjs/ReplaySubject';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/retry';
import 'rxjs/add/operator/switchMap';
@@ -12,7 +15,7 @@ import { ConfigService } from './../../shared/services/config.service';
import { PortalService } from './../../shared/services/portal.service';
import { PortalResources } from './../../shared/models/portal-resources';
import { AiService } from './../../shared/services/ai.service';
-import { SiteTabIds } from './../../shared/models/constants';
+import { SiteTabIds, LocalStorageKeys, EnableTabFeature } from './../../shared/models/constants';
import { AppNode } from './../../tree-view/app-node';
import {TabsComponent} from '../../tabs/tabs.component';
import {TabComponent} from '../../tab/tab.component';
@@ -24,6 +27,7 @@ import {Descriptor, SiteDescriptor} from '../../shared/resourceDescriptors';
import {ArmObj} from '../../shared/models/arm/arm-obj';
import { Site } from '../../shared/models/arm/site';
import { PartSize } from '../../shared/models/portal';
+import { TabSettings } from './../../shared/models/localStorage/local-storage';
@Component({
selector: 'site-dashboard',
@@ -36,13 +40,16 @@ export class SiteDashboardComponent {
@ViewChild(TabsComponent) tabs : TabsComponent;
public selectedTabId: string = SiteTabIds.overview;
+ public dynamicTabId: string | null = null;
public site : ArmObj;
public viewInfoStream : Subject;
public viewInfo : TreeViewInfo;
public TabIds = SiteTabIds;
public Resources = PortalResources;
- public activeComponent = "";
public isStandalone = false;
+ public tabsFeature: EnableTabFeature;
+ public openFeatureId = new ReplaySubject(1);
+ private _prevFeatureId : string;
private _tabsLoaded = false;
private _traceOnTabSelection = false;
@@ -53,9 +60,11 @@ export class SiteDashboardComponent {
private _aiService : AiService,
private _portalService: PortalService,
private _translateService : TranslateService,
- private _configService : ConfigService) {
+ private _configService : ConfigService,
+ private _storageService : LocalStorageService) {
this.isStandalone = _configService.isStandalone();
+ this.tabsFeature = Url.getParameterByName(window.location.href, "appsvc.feature.tabs");
this.viewInfoStream = new Subject();
this.viewInfoStream
@@ -107,14 +116,21 @@ export class SiteDashboardComponent {
// time the component is loaded.
setTimeout(() =>{
let appNode = this.viewInfo.node;
- if(appNode.openFunctionSettingsTab && this.tabs && this.tabs.tabs){
- let tabs = this.tabs.tabs.toArray();
- let functionTab = tabs.find(t => t.title === SiteTabIds.functionRuntime);
- if(functionTab){
- this.tabs.selectTab(functionTab);
- }
+ if(this.tabs && this.tabs.tabs){
+
+ let savedTabInfo = this._storageService.getItem(LocalStorageKeys.siteTabs);
+ if (appNode.openFunctionSettingsTab){
+ let tabs = this.tabs.tabs.toArray();
+ let functionTab = tabs.find(t => t.id === SiteTabIds.functionRuntime);
+ if(functionTab){
+ this.tabs.selectTab(functionTab);
+ }
- appNode.openFunctionSettingsTab = false;
+ appNode.openFunctionSettingsTab = false;
+ }
+ else if(savedTabInfo){
+ this.dynamicTabId = savedTabInfo.dynamicTabId;
+ }
}
},
100);
@@ -133,21 +149,54 @@ export class SiteDashboardComponent {
this._tabsLoaded = true;
this._traceOnTabSelection = true;
+ this._prevFeatureId = this.selectedTabId;
this.selectedTabId = selectedTab.id;
}
- onTabClosed(closedTab: TabComponent){
- // For now only support a single dynamic tab
- this.activeComponent = "";
+ closeDynamicTab(tabId : string){
+ let prevFeatureId = this.dynamicTabId === this.selectedTabId ? this._prevFeatureId : null;
+ this.dynamicTabId = null;
+ this._storageService.removeItem(LocalStorageKeys.siteTabs);
+ if(prevFeatureId){
+ this.tabs.selectTabId(this._prevFeatureId);
+ }
+
+ this._prevFeatureId = null;
}
- openTab(component : string){
- this.activeComponent = component;
+ openFeature(featureId : string){
+
+ if(this.tabsFeature === 'tabs'){
+ this._prevFeatureId = this.selectedTabId;
+
+ this.dynamicTabId = featureId;
+ let tabSettings = {
+ id : LocalStorageKeys.siteTabs,
+ dynamicTabId : this.dynamicTabId
+ };
+
+ this._storageService.setItem(LocalStorageKeys.siteTabs, tabSettings);
- setTimeout(() =>{
- let tabs = this.tabs.tabs.toArray();
- this.tabs.selectTab(tabs[tabs.length-1]);
- }, 100);
+ setTimeout(() =>{
+ this.tabs.selectTabId(featureId);
+ }, 100);
+
+ }
+ else if(this.tabsFeature === 'inplace'){
+ if(featureId){
+ this.tabs.selectTabId(SiteTabIds.features);
+ }
+ else{
+ this.tabs.selectTabId(this._prevFeatureId);
+ }
+
+ setTimeout(() =>{
+ this.openFeatureId.next(featureId);
+ }, 100)
+ }
+ else{
+ this.tabs.selectTabId(featureId);
+ }
}
pinPart(){
diff --git a/AzureFunctions.AngularClient/src/app/site/site-enabled-features/site-enabled-features.component.ts b/AzureFunctions.AngularClient/src/app/site/site-enabled-features/site-enabled-features.component.ts
index edef8cffa4..19a8e73e58 100644
--- a/AzureFunctions.AngularClient/src/app/site/site-enabled-features/site-enabled-features.component.ts
+++ b/AzureFunctions.AngularClient/src/app/site/site-enabled-features/site-enabled-features.component.ts
@@ -1,3 +1,6 @@
+import { SiteDashboardComponent } from './../site-dashboard/site-dashboard.component';
+import { SiteTabIds } from './../../shared/models/constants';
+import { Url } from './../../shared/Utilities/url';
import {Component, OnInit, EventEmitter, Input, Output} from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
@@ -36,7 +39,6 @@ interface EnabledFeatureMap{
templateUrl: './site-enabled-features.component.html',
styleUrls: ['./site-enabled-features.component.scss'],
inputs: ['siteInput'],
- outputs: ['componentName']
})
// First load list of enabled features from localStorage
@@ -46,7 +48,6 @@ export class SiteEnabledFeaturesComponent {
public featureItems : EnabledFeatureItem[] = [];
public isLoading : boolean;
- public componentName = new Subject();
private _site : ArmObj;
private _siteSubject = new Subject>();
@@ -59,7 +60,8 @@ export class SiteEnabledFeaturesComponent {
private _authZService : AuthzService,
private _aiService : AiService,
private _translateService : TranslateService,
- private _globalStateService : GlobalStateService) {
+ private _globalStateService : GlobalStateService,
+ private _siteDashboard : SiteDashboardComponent) {
this._siteSubject
.distinctUntilChanged()
@@ -93,6 +95,9 @@ export class SiteEnabledFeaturesComponent {
this.isLoading = false;
this._copyCachedFeaturesToF1(storageItem);
}
+ else{
+ this._addDefaultItems(this.featureItems);
+ }
return Observable.zip(
this._getConfigFeatures(r.site),
@@ -114,6 +119,10 @@ export class SiteEnabledFeaturesComponent {
this.isLoading = false;
let latestFeatureItems : EnabledFeatureItem[] = [];
+
+ // Need to add default items to latest otherwise they'll be removed from featureItems during merge.
+ this._addDefaultItems(latestFeatureItems);
+
results.forEach(result =>{
if(result && result.length > 0){
result.forEach(featureItem =>{
@@ -126,7 +135,7 @@ export class SiteEnabledFeaturesComponent {
this._mergeFeaturesIntoF1(this.featureItems, latestFeatureItems);
this._saveFeatures(this.featureItems);
- })
+ });
}
set siteInput(site : ArmObj){
@@ -138,8 +147,8 @@ export class SiteEnabledFeaturesComponent {
}
openFeature(feature : EnabledFeatureItem){
- if(feature.componentName){
- this.componentName.next(feature.componentName);
+ if(feature.featureId){
+ this._siteDashboard.openFeature(feature.featureId);
}
else if(feature.bladeInfo){
this._portalService.openBlade(feature.bladeInfo, "site-enabled-features");
@@ -180,9 +189,37 @@ export class SiteEnabledFeaturesComponent {
})
}
+ private _addDefaultItems(features : EnabledFeature[]){
+ let functionSettings = this._getEnabledFeatureItem(Feature.FunctionSettings);
+ let appSettings = this._getEnabledFeatureItem(Feature.AppSettings);
+ features.splice(0, 0, functionSettings, appSettings);
+ }
+
private _getEnabledFeatureItem(feature : Feature, ...args: any[]) : EnabledFeatureItem{
+ let tabsFeature = Url.getParameterByName(window.location.href, "appsvc.feature.tabs");
switch (feature) {
+ case Feature.FunctionSettings:
+ return {
+ title: this._translateService.instant(tabsFeature ? PortalResources.tab_functionSettings : PortalResources.tab_settings),
+ feature: feature,
+ iconUrl: "images/Functions.svg",
+ featureId: SiteTabIds.functionRuntime
+ }
+
+ case Feature.AppSettings:
+ return {
+ title: this._translateService.instant(PortalResources.feature_applicationSettingsName),
+ feature: feature,
+ bladeInfo: {
+ detailBlade : "WebsiteConfigSiteSettings",
+ detailBladeInputs : {
+ resourceUri : this._descriptor.resourceId,
+ }
+ },
+ iconUrl: "images/application-settings.svg"
+ }
+
case Feature.AppInsight:
return {
title: this._translateService.instant(PortalResources.featureEnabled_appInsights),
@@ -269,12 +306,7 @@ export class SiteEnabledFeaturesComponent {
title : this._translateService.instant(PortalResources.feature_apiDefinitionName),
feature : feature,
iconUrl : "images/api-definition.svg",
- bladeInfo : {
- detailBlade : "ApiDefinition",
- detailBladeInputs : {
- resourceUri : this._site.id,
- }
- }
+ featureId: SiteTabIds.apiDefinition
}
case Feature.WebJobs:
@@ -329,7 +361,8 @@ export class SiteEnabledFeaturesComponent {
this._storageService.setItem(item.id, item);
}
- private _mergeFeaturesIntoF1(featureItems1 : EnabledFeatureItem[],
+ private _mergeFeaturesIntoF1(
+ featureItems1 : EnabledFeatureItem[],
featureItems2: EnabledFeatureItem[]){
let removeFeatures : EnabledFeatureItem[] = [];
diff --git a/AzureFunctions.AngularClient/src/app/site/site-manage/site-manage.component.html b/AzureFunctions.AngularClient/src/app/site/site-manage/site-manage.component.html
index 510952beb4..607eecb212 100644
--- a/AzureFunctions.AngularClient/src/app/site/site-manage/site-manage.component.html
+++ b/AzureFunctions.AngularClient/src/app/site/site-manage/site-manage.component.html
@@ -1,4 +1,4 @@
-
-