diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000000..357bd934d9 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,8 @@ +language: node_js +node_js: + - "6.9" +before_script: + - npm install -g --silent @angular/cli@1.1.3 + - npm install -g --silent yarn +script: + - cd AzureFunctions.AngularClient && yarn install && ng build -prod && cd ../server && yarn install && tsc \ No newline at end of file diff --git a/AzureFunctions.AngularClient/.angular-cli.json b/AzureFunctions.AngularClient/.angular-cli.json index 49e3c99955..ce9e1b5cde 100644 --- a/AzureFunctions.AngularClient/.angular-cli.json +++ b/AzureFunctions.AngularClient/.angular-cli.json @@ -9,7 +9,7 @@ "outDir": "../server/src/public/ng-full", "assets": [ "assets", - "images", + "image", "../node_modules/swagger-editor", "sass/main.scss" ], diff --git a/AzureFunctions.AngularClient/AzureFunctions.AngularClient.csproj b/AzureFunctions.AngularClient/AzureFunctions.AngularClient.csproj index 43bfe3a090..2162c775a5 100644 --- a/AzureFunctions.AngularClient/AzureFunctions.AngularClient.csproj +++ b/AzureFunctions.AngularClient/AzureFunctions.AngularClient.csproj @@ -190,6 +190,9 @@ + + + @@ -230,96 +233,96 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -508,6 +511,8 @@ + + @@ -605,17 +610,16 @@ - - - - + + + diff --git a/AzureFunctions.AngularClient/package.json b/AzureFunctions.AngularClient/package.json index afff033b40..905cc8e2df 100644 --- a/AzureFunctions.AngularClient/package.json +++ b/AzureFunctions.AngularClient/package.json @@ -32,7 +32,7 @@ "jsonschema": "^1.1.1", "marked": "^0.3.6", "moment": "^2.17.0", - "monaco-editor": "^0.7.0", + "monaco-editor": "^0.10.0", "ng2-cookies": "^1.0.3", "ng2-file-upload": "~1.2.1", "ng2-popover": "0.0.14", diff --git a/AzureFunctions.AngularClient/src/app/aad-registration/aad-registration.component.scss b/AzureFunctions.AngularClient/src/app/aad-registration/aad-registration.component.scss index 05f0367db1..7f2ebcdd66 100644 --- a/AzureFunctions.AngularClient/src/app/aad-registration/aad-registration.component.scss +++ b/AzureFunctions.AngularClient/src/app/aad-registration/aad-registration.component.scss @@ -1,4 +1,4 @@ -@import '../../sass/main'; +@import '../../sass/common/variables'; div { line-height: 22px; 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 b40c60e1a8..621768ef62 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 @@ -2,7 +2,7 @@
- {{apiProxyEdit.name}}  +

{{apiProxyEdit.name}} 

{{ 'apiProxy_delete' | translate }} @@ -107,8 +107,8 @@
- - + +
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 3f2db0f899..b98643ba9b 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 @@ -1,6 +1,6 @@
-
{{ 'apiProxy_new' | translate }}
+

{{ 'apiProxy_new' | translate }}

@@ -96,7 +96,7 @@ (valueChanges)="rrOverriedValueChanges($event)">
- +
diff --git a/AzureFunctions.AngularClient/src/app/api/api-new/api-new.component.scss b/AzureFunctions.AngularClient/src/app/api/api-new/api-new.component.scss index 357c491e0a..eacbd2abe2 100644 --- a/AzureFunctions.AngularClient/src/app/api/api-new/api-new.component.scss +++ b/AzureFunctions.AngularClient/src/app/api/api-new/api-new.component.scss @@ -1,4 +1,4 @@ -@import '../../../sass/main'; +@import '../../../sass/common/variables'; form { padding:10px 0px 0px 0px; @@ -18,7 +18,6 @@ form > div { .newproxy-container{ padding: 20px; - background-color: $body-bg-color; } .header { 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 f1cfa469f9..c8f67a08e6 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 @@ -77,7 +77,8 @@
+ [functionAppInput]="functionApp" + fileName="body.json">
diff --git a/AzureFunctions.AngularClient/src/app/app.component.html b/AzureFunctions.AngularClient/src/app/app.component.html index 25f635c0bb..39244dc2d4 100644 --- a/AzureFunctions.AngularClient/src/app/app.component.html +++ b/AzureFunctions.AngularClient/src/app/app.component.html @@ -1,5 +1,7 @@ - - - +
+ + + - + +
diff --git a/AzureFunctions.AngularClient/src/app/app.component.scss b/AzureFunctions.AngularClient/src/app/app.component.scss new file mode 100644 index 0000000000..d8accf9218 --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/app.component.scss @@ -0,0 +1,11 @@ +@import '../sass/common/variables'; + + +#app-root{ + height: 100%; + width: 100%; + + &[theme=dark]{ + background-color: $body-bg-color-dark; + } +} \ No newline at end of file diff --git a/AzureFunctions.AngularClient/src/app/app.component.ts b/AzureFunctions.AngularClient/src/app/app.component.ts index 7446af57c2..90e920de9b 100644 --- a/AzureFunctions.AngularClient/src/app/app.component.ts +++ b/AzureFunctions.AngularClient/src/app/app.component.ts @@ -10,8 +10,10 @@ import { Router, ActivatedRoute } from '@angular/router'; @Component({ selector: 'app-root', templateUrl: './app.component.html', + styleUrls: ['./app.component.scss'] }) export class AppComponent implements OnInit, AfterViewInit { + theme: string; @ViewChild(BusyStateComponent) busyStateComponent: BusyStateComponent; @@ -26,30 +28,33 @@ export class AppComponent implements OnInit, AfterViewInit { route: ActivatedRoute, configService: ConfigService ) { + const devGuide = Url.getParameterByName(null, 'appsvc.devguide'); // TODO: for now we don't honor any deep links. We'll need to make a bunch of updates to our // tree logic in order to get it working properly if (_globalStateService.showTryView) { - - this._router.navigate(['/try'], { queryParams: Url.getQueryStringObj()}); - - } else if (!this._userService.inIFrame - && window.location.protocol !== 'http:' - && !this._userService.inTab - && !configService.isStandalone()) { - - this._router.navigate(['/landing'], { queryParams: Url.getQueryStringObj()}); - - } else { - - this._router.navigate(['/resources/apps'], { queryParams: Url.getQueryStringObj()}); - + this._router.navigate(['/try'], { queryParams: Url.getQueryStringObj() }); + } else if (devGuide) { + this._router.navigate(['/devguide'], { queryParams: Url.getQueryStringObj() }); + } else if ( + !this._userService.inIFrame && + window.location.protocol !== 'http:' && + !this._userService.inTab && + !configService.isStandalone() && + !this._userService.deeplinkAllowed + ) { + this._router.navigate(['/landing'], { queryParams: Url.getQueryStringObj() }); + } else if (!this._userService.deeplinkAllowed) { + this._router.navigate(['/resources/apps'], { queryParams: Url.getQueryStringObj() }); } - } - ngOnInit() { + this._userService.getStartupInfo().subscribe(info => { + this.theme = info.theme; + }); } + ngOnInit() {} + ngAfterViewInit() { this._globalStateService.GlobalBusyStateComponent = this.busyStateComponent; } diff --git a/AzureFunctions.AngularClient/src/app/app.module.ts b/AzureFunctions.AngularClient/src/app/app.module.ts index d3a276612b..72ea171057 100644 --- a/AzureFunctions.AngularClient/src/app/app.module.ts +++ b/AzureFunctions.AngularClient/src/app/app.module.ts @@ -21,40 +21,46 @@ import 'rxjs/add/operator/mergeMap'; import 'rxjs/add/observable/of'; import 'rxjs/add/operator/retry'; import 'rxjs/add/operator/switchMap'; -import 'rxjs/add/operator/takeuntil'; +import 'rxjs/add/operator/takeUntil'; import 'rxjs/add/observable/timer'; import 'rxjs/add/observable/throw'; import 'rxjs/add/observable/zip'; const routes = RouterModule.forRoot([ + // "/resources" will load the main component which has the tree view for all resources + { path: 'resources', loadChildren: 'app/main/main.module#MainModule' }, - // "/resources" will load the main component which has the tree view for all resources - { path: 'resources', loadChildren: 'app/main/main.module#MainModule' }, + // "/landing" will load the getting started page for functions.azure.com + { path: 'landing', loadChildren: 'app/getting-started/getting-started.module#GettingStartedModule' }, - // "/landing" will load the getting started page for functions.azure.com - { path: 'landing', loadChildren: 'app/getting-started/getting-started.module#GettingStartedModule' }, + // "/try" will load the try functions start page for https://functions.azure.com?trial=true + { path: 'try', loadChildren: 'app/try-landing/try-landing.module#TryLandingModule' }, + + // "/feature" will load a window to show a specific feature(i.e. app settings) with nothing else, defined by query string + { path: 'feature', loadChildren: 'app/ibiza-feature/ibiza-feature.module#IbizaFeatureModule' }, + + // /devguide + { path: 'devguide', loadChildren: 'app/dev-guide/dev-guide.module#DevGuideModule' } - // "/try" will load the try functions start page for https://functions.azure.com?trial=true - { path: 'try', loadChildren: 'app/try-landing/try-landing.module#TryLandingModule' } ]); @NgModule(AppModule.moduleDefinition) export class AppModule { - static moduleDefinition = { - declarations: [ - AppComponent, - ErrorListComponent, - DisabledDashboardComponent, - ], - imports: [ - SharedModule.forRoot(), - ReactiveFormsModule, - BrowserModule, - HttpModule, - TranslateModule.forRoot(), - PopoverModule, - routes - ], - bootstrap: [AppComponent] - }; + static moduleDefinition = { + declarations: [ + AppComponent, + ErrorListComponent, + DisabledDashboardComponent + ], + imports: [ + SharedModule.forRoot(), + ReactiveFormsModule, + BrowserModule, + HttpModule, + TranslateModule.forRoot(), + PopoverModule, + routes + ], + bootstrap: [AppComponent] + }; } 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 12ada48eab..d1ad0a2382 100644 --- a/AzureFunctions.AngularClient/src/app/apps-list/apps-list.component.html +++ b/AzureFunctions.AngularClient/src/app/apps-list/apps-list.component.html @@ -1,6 +1,6 @@ -
- -{{ 'functionApps' | translate }} +
+ +

{{ 'functionApps' | translate }}

@@ -63,12 +63,12 @@
- +

{{'emptyBrowse_title' | translate}}

{{'emptyBrowse' | translate}} {{'emptyBrowse_learnMore' | translate}} - +
\ No newline at end of file diff --git a/AzureFunctions.AngularClient/src/app/apps-list/apps-list.component.scss b/AzureFunctions.AngularClient/src/app/apps-list/apps-list.component.scss index b48a3bb2c5..1fcb9940e0 100644 --- a/AzureFunctions.AngularClient/src/app/apps-list/apps-list.component.scss +++ b/AzureFunctions.AngularClient/src/app/apps-list/apps-list.component.scss @@ -2,6 +2,10 @@ width: 100% } +h2{ + display: inline-block; +} + .bars{ width: 100%; display: inline-flex; diff --git a/AzureFunctions.AngularClient/src/app/binding-input/binding-input.component.css b/AzureFunctions.AngularClient/src/app/binding-input/binding-input.component.css index c233aa141d..49e9f6ed88 100644 --- a/AzureFunctions.AngularClient/src/app/binding-input/binding-input.component.css +++ b/AzureFunctions.AngularClient/src/app/binding-input/binding-input.component.css @@ -3,7 +3,6 @@ } .panel-heading, .panel-body{ - background-color: #FFF; border: none; padding-left: 0px; padding-right: 0px; @@ -36,8 +35,6 @@ } input, select{ - background: #fff; - border: 1px solid #dedede; position: relative; vertical-align: middle; height: 29px; @@ -46,10 +43,6 @@ input, select{ padding: 0 5px; } -input:disabled{ - background-color: #F3F3F3; -} - input[type=checkbox]{ height: 20px; width: 20px; @@ -71,11 +64,7 @@ i.select { button.go { margin-left: 100px; } -.button-go-disabled { - cursor: not-allowed; - background-color: lightgray; - color: white; -} + .storage-creds-texbox { float: left; margin-right: 0px; diff --git a/AzureFunctions.AngularClient/src/app/binding-input/binding-input.component.html b/AzureFunctions.AngularClient/src/app/binding-input/binding-input.component.html index ffae5c8179..73e8cfbeb5 100644 --- a/AzureFunctions.AngularClient/src/app/binding-input/binding-input.component.html +++ b/AzureFunctions.AngularClient/src/app/binding-input/binding-input.component.html @@ -16,6 +16,12 @@ (selectItem)="finishDialogPicker($event)"> + + + -
@@ -91,7 +97,7 @@ {{ 'bindingInput_new' | translate }} - {{ 'bindingInput_new' | translate }}

diff --git a/AzureFunctions.AngularClient/src/app/binding-input/binding-input.component.ts b/AzureFunctions.AngularClient/src/app/binding-input/binding-input.component.ts index bac061794d..aa53f78197 100644 --- a/AzureFunctions.AngularClient/src/app/binding-input/binding-input.component.ts +++ b/AzureFunctions.AngularClient/src/app/binding-input/binding-input.component.ts @@ -83,6 +83,9 @@ export class BindingInputComponent { case ResourceType.ServiceBus: this.pickerName = 'ServiceBus'; break; + case ResourceType.NotificationHub: + this.pickerName = 'NotificationHub'; + break; case ResourceType.AppSetting: this.pickerName = 'AppSetting'; break; @@ -113,7 +116,10 @@ export class BindingInputComponent { const picker = this.input; picker.inProcess = true; - if (this.pickerName !== 'EventHub' && this.pickerName !== 'ServiceBus' && this.pickerName !== 'AppSetting') { + if (this.pickerName !== 'EventHub' && + this.pickerName !== 'ServiceBus' && + this.pickerName !== 'AppSetting' && + this.pickerName !== 'NotificationHub') { this._globalStateService.setBusyState(this._translateService.instant(PortalResources.resourceSelect)); diff --git a/AzureFunctions.AngularClient/src/app/binding/binding.component.html b/AzureFunctions.AngularClient/src/app/binding/binding.component.html index 91e28c3b5b..528b981f73 100644 --- a/AzureFunctions.AngularClient/src/app/binding/binding.component.html +++ b/AzureFunctions.AngularClient/src/app/binding/binding.component.html @@ -75,7 +75,7 @@

{{ 'binding_createNewFunction' | translate }} - +
diff --git a/AzureFunctions.AngularClient/src/app/binding/binding.component.scss b/AzureFunctions.AngularClient/src/app/binding/binding.component.scss index 8929f24904..0ef9c42ffa 100644 --- a/AzureFunctions.AngularClient/src/app/binding/binding.component.scss +++ b/AzureFunctions.AngularClient/src/app/binding/binding.component.scss @@ -1,9 +1,13 @@ -@import '../../sass/main'; +@import '../../sass/common/variables'; .panel{ border: none; } +.panel, .panel-heading{ + background-color: inherit; +} + .binding-title{ margin-right: 10px; font-size: 18px; @@ -12,7 +16,6 @@ } .panel-heading, .panel-body{ - background-color: #FFF; border: none; padding-left: 0px; padding-right: 0px; @@ -49,7 +52,6 @@ } input, select{ - background: #fff; position: relative; vertical-align: middle; height: 29px; @@ -58,10 +60,6 @@ input, select{ padding: 0 5px; } -input:disabled{ - background-color: #F3F3F3; -} - input[type=checkbox]{ height: 20px; width: 20px; @@ -83,12 +81,7 @@ i.select { button.go { margin-left: 100px; } -.button-go-disabled { - cursor: not-allowed; - background-color: $background-color-disabled; - opacity: 0.4; - color: black; -} + .storage-creds-texbox { float: left; margin-right: 0px; diff --git a/AzureFunctions.AngularClient/src/app/binding/binding.component.ts b/AzureFunctions.AngularClient/src/app/binding/binding.component.ts index 356ef77a66..cb48e1d2f2 100644 --- a/AzureFunctions.AngularClient/src/app/binding/binding.component.ts +++ b/AzureFunctions.AngularClient/src/app/binding/binding.component.ts @@ -755,8 +755,8 @@ export class BindingComponent { break; case ResourceType.EventHub: case ResourceType.ServiceBus: + case ResourceType.NotificationHub: for (const key in this._appSettings) { - const value = this._appSettings[key].toLowerCase(); if (value.indexOf('sb://') > -1 && value.indexOf('sharedaccesskeyname') > -1) { result.push(key); diff --git a/AzureFunctions.AngularClient/src/app/busy-state/busy-state-scope-manager.ts b/AzureFunctions.AngularClient/src/app/busy-state/busy-state-scope-manager.ts index d337f69495..a85f8f2b68 100644 --- a/AzureFunctions.AngularClient/src/app/busy-state/busy-state-scope-manager.ts +++ b/AzureFunctions.AngularClient/src/app/busy-state/busy-state-scope-manager.ts @@ -1,31 +1,32 @@ -import { BusyStateComponent } from './busy-state.component'; -import { Subscription as RxSubscription } from 'rxjs/Subscription'; +import { BroadcastEvent } from 'app/shared/models/broadcast-event'; +import { BusyStateEvent } from './../shared/models/broadcast-event'; +import { BroadcastService } from 'app/shared/services/broadcast.service'; +import { Guid } from './../shared/Utilities/Guid'; +import { BusyStateName } from './busy-state.component'; export class BusyStateScopeManager { - private _busyState: BusyStateComponent; private _busyStateKey: string | undefined; - private _busyStateSubscription: RxSubscription; - constructor(busyState: BusyStateComponent) { - this._busyState = busyState; - this._busyStateSubscription = this._busyState.clear.subscribe(() => this._busyStateKey = null); + constructor( + private _broadcastService: BroadcastService, + private _name: BusyStateName) { + this._busyStateKey = Guid.newGuid(); } public setBusy() { - this._busyStateKey = this._busyState.setScopedBusyState(this._busyStateKey); + this._broadcastService.broadcastEvent(BroadcastEvent.UpdateBusyState, { + busyComponentName: this._name, + action: 'setBusyState', + busyStateKey: this._busyStateKey + }); } public clearBusy() { - this._busyState.clearBusyState(this._busyStateKey); - this._busyStateKey = null; - } - - public dispose() { - this.clearBusy(); - if (this._busyStateSubscription) { - this._busyStateSubscription.unsubscribe(); - this._busyStateSubscription = null; - } + this._broadcastService.broadcastEvent(BroadcastEvent.UpdateBusyState, { + busyComponentName: this._name, + action: 'clearBusyState', + busyStateKey: this._busyStateKey + }); } } diff --git a/AzureFunctions.AngularClient/src/app/busy-state/busy-state.component.html b/AzureFunctions.AngularClient/src/app/busy-state/busy-state.component.html index 1f3089da82..d5afaf22cf 100644 --- a/AzureFunctions.AngularClient/src/app/busy-state/busy-state.component.html +++ b/AzureFunctions.AngularClient/src/app/busy-state/busy-state.component.html @@ -1,10 +1,4 @@ - -
+
diff --git a/AzureFunctions.AngularClient/src/app/busy-state/busy-state.component.scss b/AzureFunctions.AngularClient/src/app/busy-state/busy-state.component.scss index a87e573d2f..9fd5565f94 100644 --- a/AzureFunctions.AngularClient/src/app/busy-state/busy-state.component.scss +++ b/AzureFunctions.AngularClient/src/app/busy-state/busy-state.component.scss @@ -1,4 +1,4 @@ -@import '../../sass/main'; +@import '../../sass/common/variables'; .container { height: 100%; @@ -9,21 +9,25 @@ } .try-functions-busy { + @extend .container; background-color: #337ab7; color: white; font-family: "Segoe UI Light", "Segoe UI", "Segoe", Tahoma, Helvetica, Arial, sans-serif; } .global { + @extend .container; position: fixed; } .busy-dashboard{ + @extend .container; width: calc(100% - #{$sidenav-width}); margin-left: $sidenav-width; } .busy-site-tabs{ + @extend .container; width: calc(100% - #{$sidenav-width}); height: calc(100% - #{$top-bar-height} - 1px); } @@ -56,8 +60,7 @@ left: 0; width: 100%; height: 100%; - background-color: #dcdfe2; - background-color: rgba(255, 255, 255, 0.30); + background-color: rgba($body-bg-color, 0.30); z-index: 196; } .fxs-progress-dots { @@ -92,5 +95,15 @@ .fxs-progress-dots > div:nth-child(4) { padding-top: 20px; width: 100%; - background: white; + background-color: rgba($body-bg-color, 0.30); + } + +:host-context(#app-root[theme=dark]){ + .fxs-progress{ + background-color: rgba($body-bg-color-dark, 0.7); + } + + .fxs-progress-dots > div:nth-child(4) { + background-color: rgba($body-bg-color-dark, 0.7); + } } \ No newline at end of file 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..b5c1e8eec6 100644 --- a/AzureFunctions.AngularClient/src/app/busy-state/busy-state.component.ts +++ b/AzureFunctions.AngularClient/src/app/busy-state/busy-state.component.ts @@ -1,59 +1,104 @@ -import { Component, Input, Output, OnInit } from '@angular/core'; +import { LogCategories } from 'app/shared/models/constants'; +import { LogService } from './../shared/services/log.service'; +import { BusyStateEvent } from './../shared/models/broadcast-event'; +import { BroadcastEvent } from 'app/shared/models/broadcast-event'; +import { BroadcastService } from './../shared/services/broadcast.service'; +import { Component, Input, Output, OnInit, OnDestroy } from '@angular/core'; import { Subject } from 'rxjs/Subject'; import { Guid } from './../shared/Utilities/Guid'; -import { BusyStateScopeManager } from './busy-state-scope-manager'; + +export type BusyStateName = + 'global' + | 'dashboard' + | 'site-tabs' + | 'try-functions' + | 'function-keys'; @Component({ selector: 'busy-state', templateUrl: './busy-state.component.html', styleUrls: ['./busy-state.component.scss'] }) -export class BusyStateComponent implements OnInit { +export class BusyStateComponent implements OnInit, OnDestroy { public busy = false; - @Input() name: string; - isGlobal = false; + @Input() name: BusyStateName; @Input() message: string; @Output() clear = new Subject(); + @Input() cssClass: string; + + private _ngUnsubscribe = new Subject(); - private busyStateMap: { [key: string]: boolean } = {}; - private reservedKey = '-'; + private _busyStateMap: { [key: string]: boolean } = {}; + private _busyStateDebounceMap: { [key: string]: Subject } = {}; + private _reservedKey = '-'; + + constructor( + private _broadcastService: BroadcastService, + private _logService: LogService) { + } ngOnInit() { - this.isGlobal = this.name === 'global'; + + this._broadcastService.getEvents(BroadcastEvent.UpdateBusyState) + .takeUntil(this._ngUnsubscribe) + .filter(event => event.busyComponentName === this.name) + .subscribe(event => { + + if (event.action === 'setBusyState') { + this._logService.verbose(LogCategories.busyState, `[${this.name}] Called set with key '${event.busyStateKey}'`) + } else if (event.action === 'clearBusyState') { + this._logService.verbose(LogCategories.busyState, `[${this.name}] Called clear with key '${event.busyStateKey}'`) + } else { + this._logService.verbose(LogCategories.busyState, `[${this.name}] Called clearOverall with key '${event.busyStateKey}'`) + } + + this._debounceEventsBasedOnKey(event); + }); + } + + ngOnDestroy() { + this._ngUnsubscribe.next(); } setBusyState() { - this.setScopedBusyState(this.reservedKey); + this.setScopedBusyState(this._reservedKey); } setScopedBusyState(key: string): string { key = key || Guid.newGuid(); - this.busyStateMap[key] = true; + this._busyStateMap[key] = true; this.busy = true; + + this._logService.debug(LogCategories.busyState, `[${this.name}][set] - Final state for key '${key}' is 'busy'`); return key; } clearBusyState(key?: string) { - key = key || this.reservedKey; - if (this.busyStateMap[key]) { - delete this.busyStateMap[key]; + key = key || this._reservedKey; + if (this._busyStateMap[key]) { + delete this._busyStateMap[key]; } - this.busy = !this.isEmptyMap(this.busyStateMap); + this.busy = !this.isEmptyMap(this._busyStateMap); + this._logService.debug( + LogCategories.busyState, + `[${this.name}][clear] - Final state for key '${key}' is '${this.busy ? 'busy' : 'not busy'}'`); } clearOverallBusyState() { - this.busyStateMap = {}; + this._busyStateMap = {}; this.clear.next(1); this.busy = false; + + this._logService.debug(LogCategories.busyState, `[${this.name}][clearOverall]`); } getBusyState(): boolean { - return this.getScopedBusyState(this.reservedKey); + return this.getScopedBusyState(this._reservedKey); } getScopedBusyState(key: string): boolean { - return !!key && !!this.busyStateMap[key]; + return !!key && !!this._busyStateMap[key]; } get isBusy(): boolean { @@ -70,8 +115,31 @@ export class BusyStateComponent implements OnInit { return true; } - getScopeManager(): BusyStateScopeManager { - return new BusyStateScopeManager(this); - } + // Debounce events based on key + private _debounceEventsBasedOnKey(event: BusyStateEvent) { + if (!this._busyStateDebounceMap[event.busyStateKey]) { + const keySubject = new Subject(); + this._busyStateDebounceMap[event.busyStateKey] = keySubject; + + keySubject + .takeUntil(this._ngUnsubscribe) + .debounceTime(50) + .subscribe(e => { + if (e.action === 'setBusyState') { + this.setScopedBusyState(e.busyStateKey); + } else if (e.action === 'clearBusyState') { + delete this._busyStateDebounceMap[e.busyStateKey]; + this.clearBusyState(e.busyStateKey); + } else { + this.clearOverallBusyState(); + this._busyStateDebounceMap = {}; + } + }); + + keySubject.next(event); + } else { + this._busyStateDebounceMap[event.busyStateKey].next(event); + } + } } diff --git a/AzureFunctions.AngularClient/src/app/controls/click-to-edit/click-to-edit.component.html b/AzureFunctions.AngularClient/src/app/controls/click-to-edit/click-to-edit.component.html index 8c3863b5ca..e0acd66ed0 100644 --- a/AzureFunctions.AngularClient/src/app/controls/click-to-edit/click-to-edit.component.html +++ b/AzureFunctions.AngularClient/src/app/controls/click-to-edit/click-to-edit.component.html @@ -1,13 +1,13 @@ -
-
+
+
{{control.value || '​'}}
-
+
{{ 'hiddenValueClickToShow' | translate }}
-
+
diff --git a/AzureFunctions.AngularClient/src/app/controls/click-to-edit/click-to-edit.component.scss b/AzureFunctions.AngularClient/src/app/controls/click-to-edit/click-to-edit.component.scss index cdf6ea3afd..602f57dcd5 100644 --- a/AzureFunctions.AngularClient/src/app/controls/click-to-edit/click-to-edit.component.scss +++ b/AzureFunctions.AngularClient/src/app/controls/click-to-edit/click-to-edit.component.scss @@ -1,4 +1,4 @@ -@import '../../../sass/main'; +@import '../../../sass/common/variables'; .click-to-edit-wrapper{ .hidden{ @@ -9,7 +9,7 @@ color: $disabled-color; } - .text{ + .read-only-text{ overflow: hidden; text-overflow: ellipsis; padding: 3px; diff --git a/AzureFunctions.AngularClient/src/app/controls/click-to-edit/click-to-edit.component.ts b/AzureFunctions.AngularClient/src/app/controls/click-to-edit/click-to-edit.component.ts index 2c86b74b5a..1b5333ef9e 100644 --- a/AzureFunctions.AngularClient/src/app/controls/click-to-edit/click-to-edit.component.ts +++ b/AzureFunctions.AngularClient/src/app/controls/click-to-edit/click-to-edit.component.ts @@ -1,7 +1,7 @@ import { DropDownComponent } from './../../drop-down/drop-down.component'; import { TextboxComponent } from './../textbox/textbox.component'; import { FormControl, FormGroup } from '@angular/forms'; -import { Component, OnInit, Input, OnDestroy, ContentChild } from '@angular/core'; +import { Component, ElementRef, OnInit, Input, AfterViewInit, OnDestroy, ContentChild, ViewChild } from '@angular/core'; import { Subject } from 'rxjs/Subject'; import { Subscription } from 'rxjs/Subscription'; @@ -16,6 +16,8 @@ export class CustomFormGroup extends FormGroup { // Overrides the ClickToEdit default behavior to start in edit mode for new items public _msStartInEditMode: boolean; + + public _msExistenceState: 'original' | 'new' | 'deleted' = 'original'; } export class CustomFormControl extends FormControl { @@ -28,7 +30,7 @@ export class CustomFormControl extends FormControl { templateUrl: './click-to-edit.component.html', styleUrls: ['./click-to-edit.component.scss'], }) -export class ClickToEditComponent implements OnInit, OnDestroy { +export class ClickToEditComponent implements OnInit, AfterViewInit, OnDestroy { public showTextbox = false; @Input() group: FormGroup; @@ -36,34 +38,52 @@ export class ClickToEditComponent implements OnInit, OnDestroy { @Input() placeholder: string; @Input() hiddenText: boolean; + // This allows for a given control to affect the state of other controls in the group while not actually being "click-to-edit-able" itself. + // (i.e. The control's own editable/non-editable state is not affected by the extended fields in the CustomFormGroup its associated with.) + @Input() alwaysShow: boolean; + + @ViewChild('target') target: ElementRef; + @ContentChild(TextboxComponent) textbox: TextboxComponent; @ContentChild(DropDownComponent) dropdown: DropDownComponent; public control: CustomFormControl; private _sub: Subscription; + private _targetFocusState: 'focused' | 'blurring' | 'blurred'; + private _focusFunc = (e: FocusEvent) => { this._targetFocusListener(e); }; + private _blurFunc = (e: FocusEvent) => { this._targetBlurListener(e); }; + constructor() { } ngOnInit() { - this.control = this.group.controls[this.name]; + this._targetFocusState = 'blurred'; - const group = this.group; + this.control = this.group.controls[this.name] as CustomFormControl; + + const group = this.group as CustomFormGroup; if (!group._msShowTextbox) { group._msShowTextbox = new Subject(); } this._sub = group._msShowTextbox.subscribe(showTextbox => { - this.showTextbox = showTextbox; + this.showTextbox = showTextbox || this.alwaysShow || (group._msStartInEditMode && group.pristine); + if (this.showTextbox && (this.group as CustomFormGroup)._msFocusedControl === this.name) { + setTimeout(() => { + this._focusChild(); + }); + } }); - if ((group)._msStartInEditMode) { + if (group._msStartInEditMode || this.alwaysShow) { this.showTextbox = true; } + } - if (this.textbox) { - this.textbox.blur.subscribe(() => this.onBlur()); - } else if (this.dropdown) { - this.dropdown.blur.subscribe(() => this.onBlur()); + ngAfterViewInit() { + if (this.target && this.target.nativeElement) { + this.target.nativeElement.addEventListener('focus', this._focusFunc, true); + this.target.nativeElement.addEventListener('blur', this._blurFunc, true); } } @@ -72,23 +92,43 @@ export class ClickToEditComponent implements OnInit, OnDestroy { this._sub.unsubscribe(); this._sub = null; } + if (this.target && this.target.nativeElement) { + this.target.nativeElement.removeEventListener('focus', this._focusFunc, true); + this.target.nativeElement.removeEventListener('blur', this._blurFunc, true); + } + } + + private _focusChild() { + if (this.textbox) { + this.textbox.focus(); + } else if (this.dropdown) { + this.dropdown.focus(); + } else { + return; + } + + this._targetFocusState = 'focused'; } - onClick() { + onMouseDown(event: MouseEvent) { if (!this.showTextbox) { - if (this.textbox) { - this.textbox.focus(); - } else if (this.dropdown) { - this.dropdown.focus(); - } + event.preventDefault(); + this._updateShowTextbox(true); } + } + private _onTargetFocus() { this._updateShowTextbox(true); } - onBlur() { - this.control._msRunValidation = true; - this.control.updateValueAndValidity(); + private _onTargetBlur() { + if (!this.group.pristine) { + for (let name in this.group.controls) { + const control = this.group.controls[name] as CustomFormControl; + control._msRunValidation = true; + control.updateValueAndValidity(); + } + } if (this.group.valid) { @@ -100,12 +140,12 @@ export class ClickToEditComponent implements OnInit, OnDestroy { // blur will remove the textbox and the click will never happen/ setTimeout(() => { this._updateShowTextbox(false); - }, 100); + }, 0); } } protected _updateShowTextbox(show: boolean) { - const group = this.group; + const group = this.group as CustomFormGroup; if (show) { group._msFocusedControl = this.name; @@ -117,4 +157,22 @@ export class ClickToEditComponent implements OnInit, OnDestroy { group._msShowTextbox.next(show); } } + + private _targetBlurListener(event: FocusEvent) { + this._targetFocusState = 'blurring'; + setTimeout(() => { + if (this._targetFocusState !== 'focused') { + this._targetFocusState = 'blurred'; + this._onTargetBlur(); + } + }); + } + + private _targetFocusListener(event: FocusEvent) { + if (this._targetFocusState === 'blurred') { + this._onTargetFocus(); + } + this._targetFocusState = 'focused'; + } + } diff --git a/AzureFunctions.AngularClient/src/app/controls/command-bar/command/command.component.html b/AzureFunctions.AngularClient/src/app/controls/command-bar/command/command.component.html index 5a1814036f..d0b6aab6b4 100644 --- a/AzureFunctions.AngularClient/src/app/controls/command-bar/command/command.component.html +++ b/AzureFunctions.AngularClient/src/app/controls/command-bar/command/command.component.html @@ -1,3 +1,4 @@ - - {{ displayText }} + + + {{displayText}} \ No newline at end of file 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 0036a69a11..6f5801d44e 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 @@ -18,14 +18,14 @@ - +
- + {{addButtonLabel}}
diff --git a/AzureFunctions.AngularClient/src/app/controls/pair-list/pair-list.component.scss b/AzureFunctions.AngularClient/src/app/controls/pair-list/pair-list.component.scss index 85493afa2f..8acf0b44d4 100644 --- a/AzureFunctions.AngularClient/src/app/controls/pair-list/pair-list.component.scss +++ b/AzureFunctions.AngularClient/src/app/controls/pair-list/pair-list.component.scss @@ -8,17 +8,6 @@ flex-basis: 170px; } -.name-fixed { - align-self: baseline; - margin-right: 14px; - overflow: auto; - border: 1px solid #dedede; - line-height: 25px; - flex-basis: 170px; - padding-left: 5px; - background-color: #f5f5f5; -} - .value { flex-grow:1; } @@ -28,8 +17,6 @@ } input, select{ - background: #fff; - border: 1px solid #dedede; position: relative; vertical-align: middle; height: 29px; diff --git a/AzureFunctions.AngularClient/src/app/controls/slide-toggle/slide-toggle.component.scss b/AzureFunctions.AngularClient/src/app/controls/slide-toggle/slide-toggle.component.scss index 6a6318f72c..bc476846bd 100644 --- a/AzureFunctions.AngularClient/src/app/controls/slide-toggle/slide-toggle.component.scss +++ b/AzureFunctions.AngularClient/src/app/controls/slide-toggle/slide-toggle.component.scss @@ -1,4 +1,4 @@ -@import '../../../sass/main'; +@import '../../../sass/common/variables'; /*Overwrite bootstrap*/ a.toggle-container { diff --git a/AzureFunctions.AngularClient/src/app/tab/tab.component.html b/AzureFunctions.AngularClient/src/app/controls/tabs/tab/tab.component.html similarity index 100% rename from AzureFunctions.AngularClient/src/app/tab/tab.component.html rename to AzureFunctions.AngularClient/src/app/controls/tabs/tab/tab.component.html diff --git a/AzureFunctions.AngularClient/src/app/tab/tab.component.spec.ts b/AzureFunctions.AngularClient/src/app/controls/tabs/tab/tab.component.spec.ts similarity index 100% rename from AzureFunctions.AngularClient/src/app/tab/tab.component.spec.ts rename to AzureFunctions.AngularClient/src/app/controls/tabs/tab/tab.component.spec.ts diff --git a/AzureFunctions.AngularClient/src/app/controls/tabs/tab/tab.component.ts b/AzureFunctions.AngularClient/src/app/controls/tabs/tab/tab.component.ts new file mode 100644 index 0000000000..c7bf7451ee --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/controls/tabs/tab/tab.component.ts @@ -0,0 +1,11 @@ +import { Input, Component } from '@angular/core'; + +@Component({ + selector: 'tab', + templateUrl: './tab.component.html' +}) +export class TabComponent { + @Input() title: string; + @Input() id: string; + @Input() active = false; +} \ No newline at end of file diff --git a/AzureFunctions.AngularClient/src/app/controls/tabs/tabs.component.html b/AzureFunctions.AngularClient/src/app/controls/tabs/tabs.component.html new file mode 100644 index 0000000000..4716d173c1 --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/controls/tabs/tabs.component.html @@ -0,0 +1,17 @@ + \ No newline at end of file diff --git a/AzureFunctions.AngularClient/src/app/controls/tabs/tabs.component.scss b/AzureFunctions.AngularClient/src/app/controls/tabs/tabs.component.scss new file mode 100644 index 0000000000..ab42b8f3ce --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/controls/tabs/tabs.component.scss @@ -0,0 +1,40 @@ +@import '../../../sass/common/variables'; + +ul.tabs-row{ + box-sizing: border-box; + list-style-type: none; + padding-left: 0px; + margin: 0px; + border-bottom: $border; +} + +li.tab-cell{ + box-sizing: border-box; + display: inline-block; + border-top-width: 0px; + text-align: center; + padding: 8px 25px; + min-width: 150px; + outline: none; + margin-left: 0px; + position: relative; + + .bottom{ + position: absolute; + bottom: -1px; + background-color: $body-bg-color; + height: 1px; + width: 100%; + left: 0px; + } +} + +#app-root[theme=dark]{ + .tab-label-selected{ + color: $default-text-color-dark; + } + + li.tab-label{ + color: $primary-color-dark; + } +} \ No newline at end of file diff --git a/AzureFunctions.AngularClient/src/app/controls/tabs/tabs.component.ts b/AzureFunctions.AngularClient/src/app/controls/tabs/tabs.component.ts new file mode 100644 index 0000000000..830d25e382 --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/controls/tabs/tabs.component.ts @@ -0,0 +1,44 @@ +import { ContentChildren, QueryList, AfterContentInit, Component, ViewEncapsulation } from '@angular/core'; +import { TabComponent } from 'app/controls/tabs/tab/tab.component'; + +@Component({ + selector: 'tabs', + templateUrl: './tabs.component.html', + styleUrls: ['./tabs.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class TabsComponent implements AfterContentInit { + + @ContentChildren(TabComponent) tabs: QueryList; + + constructor() { + } + + ngAfterContentInit() { + const activeTabs = this.tabs.filter((tab) => tab.active); + + if (activeTabs.length === 0) { + this.selectTabHelper(this.tabs.first); + } + } + + selectTabId(tabId: string) { + const tabs = this.tabs.toArray(); + const tab = tabs.find(t => t.id === tabId); + if (tab) { + this.selectTab(tab); + } + } + + selectTab(tab: TabComponent) { + this.selectTabHelper(tab); + } + + selectTabHelper(tab: TabComponent) { + + this.tabs.toArray().forEach(t => t.active = false); + if (tab) { + tab.active = true; + } + } +} \ No newline at end of file diff --git a/AzureFunctions.AngularClient/src/app/controls/textbox/textbox.component.html b/AzureFunctions.AngularClient/src/app/controls/textbox/textbox.component.html index e079f92620..bc7a976b1c 100644 --- a/AzureFunctions.AngularClient/src/app/controls/textbox/textbox.component.html +++ b/AzureFunctions.AngularClient/src/app/controls/textbox/textbox.component.html @@ -10,7 +10,7 @@ type="text" [formControl]="control" [placeholder]="placeholder" - (blur)="onBlur($event)" /> + [class.dirty]="highlightDirty && control?.dirty" />
(); + @Input() placeholder = ''; + @Input() highlightDirty: boolean; @ViewChild('textboxInput') textboxInput: any; @@ -24,15 +22,9 @@ export class TextboxComponent implements OnInit { ngOnInit() { } - onBlur(event: any) { - this.blur.next(event); - } - focus() { if (this.textboxInput) { - setTimeout(() => { - this.textboxInput.nativeElement.focus(); - }) + this.textboxInput.nativeElement.focus(); } } } diff --git a/AzureFunctions.AngularClient/src/app/copy-pre/copy-pre.component.html b/AzureFunctions.AngularClient/src/app/copy-pre/copy-pre.component.html index f2b03377ac..3edd2f653f 100644 --- a/AzureFunctions.AngularClient/src/app/copy-pre/copy-pre.component.html +++ b/AzureFunctions.AngularClient/src/app/copy-pre/copy-pre.component.html @@ -6,7 +6,7 @@
-  {{ 'copypre_copy' | translate }} +  {{ 'copypre_copy' | translate }}
diff --git a/AzureFunctions.AngularClient/src/app/copy-pre/copy-pre.component.scss b/AzureFunctions.AngularClient/src/app/copy-pre/copy-pre.component.scss index 98310e1195..4616f95ec0 100644 --- a/AzureFunctions.AngularClient/src/app/copy-pre/copy-pre.component.scss +++ b/AzureFunctions.AngularClient/src/app/copy-pre/copy-pre.component.scss @@ -1,4 +1,4 @@ -@import '../../sass/main'; +@import '../../sass/common/variables'; .wrapper { display: flex; diff --git a/AzureFunctions.AngularClient/src/app/dev-guide/button-example/button-example.component.html b/AzureFunctions.AngularClient/src/app/dev-guide/button-example/button-example.component.html new file mode 100644 index 0000000000..86409ab409 --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/dev-guide/button-example/button-example.component.html @@ -0,0 +1,27 @@ +
+

Buttons

+
+ + + + + + + + + +
+
+
+
+    <!-- Standard button -->
+    <button class="custom-button">custom-button</button>
+
+    <!-- Inverted button -->
+    <button class="custom-button-invert">custom-button-invert</button>
+
+    <!-- Standard disabled button -->
+    <button class="custom-button" disabled>custom-button disabled</button>
+        
+
+
\ No newline at end of file diff --git a/AzureFunctions.AngularClient/src/app/dev-guide/button-example/button-example.component.scss b/AzureFunctions.AngularClient/src/app/dev-guide/button-example/button-example.component.scss new file mode 100644 index 0000000000..d3ebd1df66 --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/dev-guide/button-example/button-example.component.scss @@ -0,0 +1,5 @@ + +.custom-button, .custom-button-invert{ + margin-left: 0px; + margin-right: 5px; +} \ No newline at end of file diff --git a/AzureFunctions.AngularClient/src/app/dev-guide/button-example/button-example.component.ts b/AzureFunctions.AngularClient/src/app/dev-guide/button-example/button-example.component.ts new file mode 100644 index 0000000000..f42880101b --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/dev-guide/button-example/button-example.component.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'button-example', + styleUrls: ['./button-example.component.scss'], + templateUrl: './button-example.component.html' +}) +export class ButtonExampleComponent { +} diff --git a/AzureFunctions.AngularClient/src/app/dev-guide/color-example/color-example.component.html b/AzureFunctions.AngularClient/src/app/dev-guide/color-example/color-example.component.html new file mode 100644 index 0000000000..8292a6871f --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/dev-guide/color-example/color-example.component.html @@ -0,0 +1,64 @@ +
+

Colors

+ Here's some general rules to help you deal with colors properly: + +

+
    +
  1. DON'T HARD-CODE COLORS! - You should always reference colors defined in the _variables.scss. + If you need a different shade of a color, you can use mixins like "lighten()" or "darken()" instead of defining a new one. +
  2. +
  3. + Try to avoid defining a new color rule, but if you must, make sure you redefine it for every theme - + More colors means more work! +
  4. +
  5. + Prefer to make color styles at a global level that can be reused - Defining general purpose styles + that can be reused globally makes it easier to add themeing later because you only need to make the change in one place. For + example, instead of writing a component-level class called ".my-component-link" to handle link color and hover behavior, you can just + reference a globally defined "link" class in your HTML, or better yet, switch to an anchor tag which already handles theme rules in a single place. +
  6. +
+

+ +
+

Theming for Global Styles

+ Add this to your global-level style to handle different themes: + +
+
+    // Normal style
+    .myclass{{ '{' }}
+        color: $default-text-color;
+    {{ '}' }}
+
+    // Dark theme version
+    #app-root[theme=dark]{{ '{' }}
+        .myclass{{ '{' }}
+            color: $default-text-color-dark;
+        {{ '}' }}
+    {{ '}' }}
+        
+
+ + +
+

Theming for Component Styles

+ Add this to your component-level styles to handle different themes. Since components are protected + by view encapsulation, you need to use a ":host-context" selector to select external elements with CSS + +
+
+    // Normal style
+    .myclass{{ '{' }}
+        color: $default-text-color;
+    {{ '}' }}
+
+    // Dark theme version
+    :host-context(#app-root[theme=dark]){{ '{' }}
+        .myclass{{ '{' }}
+            color: $default-text-color-dark;
+        {{ '}' }}
+    {{ '}' }}
+        
+
+
diff --git a/AzureFunctions.AngularClient/src/app/dev-guide/color-example/color-example.component.scss b/AzureFunctions.AngularClient/src/app/dev-guide/color-example/color-example.component.scss new file mode 100644 index 0000000000..34b10cb93b --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/dev-guide/color-example/color-example.component.scss @@ -0,0 +1,12 @@ +ol{ + margin-top: 15px; + + li{ + margin-top: 10px; + line-height: 20px; + } +} + +article{ + margin-top: 30px; +} \ No newline at end of file diff --git a/AzureFunctions.AngularClient/src/app/dev-guide/color-example/color-example.component.ts b/AzureFunctions.AngularClient/src/app/dev-guide/color-example/color-example.component.ts new file mode 100644 index 0000000000..f2d60626b5 --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/dev-guide/color-example/color-example.component.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'color-example', + styleUrls: ['./color-example.component.scss'], + templateUrl: './color-example.component.html' +}) +export class ColorExampleComponent { +} diff --git a/AzureFunctions.AngularClient/src/app/dev-guide/dev-guide.component.html b/AzureFunctions.AngularClient/src/app/dev-guide/dev-guide.component.html new file mode 100644 index 0000000000..78315953c7 --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/dev-guide/dev-guide.component.html @@ -0,0 +1,18 @@ +
+ + + + + + + + + + + + + + + + +
\ No newline at end of file diff --git a/AzureFunctions.AngularClient/src/app/dev-guide/dev-guide.component.scss b/AzureFunctions.AngularClient/src/app/dev-guide/dev-guide.component.scss new file mode 100644 index 0000000000..d975d07691 --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/dev-guide/dev-guide.component.scss @@ -0,0 +1,28 @@ +@import '../../sass/common/variables'; + +#dev-guide{ + padding: 0px 40px; + max-width: 1200px; + + label{ + display: block; + } +} + +.example{ + margin-top: 30px; + margin-bottom: 60px; + + .header{ + border: $border; + padding: 10px; + } +} + +figure > label{ + margin-top: 10px; +} + +.tab-label, .tab-label-selected{ + font-size: 17px; +} \ No newline at end of file diff --git a/AzureFunctions.AngularClient/src/app/dev-guide/dev-guide.component.ts b/AzureFunctions.AngularClient/src/app/dev-guide/dev-guide.component.ts new file mode 100644 index 0000000000..bc4689cd28 --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/dev-guide/dev-guide.component.ts @@ -0,0 +1,10 @@ +import { Component, ViewEncapsulation } from '@angular/core'; + +@Component({ + selector: 'dev-guide', + templateUrl: './dev-guide.component.html', + styleUrls: ['./dev-guide.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class DevGuideComponent{ +} diff --git a/AzureFunctions.AngularClient/src/app/dev-guide/dev-guide.module.ts b/AzureFunctions.AngularClient/src/app/dev-guide/dev-guide.module.ts new file mode 100644 index 0000000000..4631d1718c --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/dev-guide/dev-guide.module.ts @@ -0,0 +1,46 @@ +import { EditableTblExampleComponent } from './editable-tbl-example/editable-tbl-example.component'; +import { TblExampleComponent } from './tbl-example/tbl-example.component'; +import { RadioSelectorExampleComponent } from './radio-selector-example/radio-selector-example.component'; +import { TextboxExampleComponent } from './textbox-example/textbox-example.component'; +import { TabComponent } from './../controls/tabs/tab/tab.component'; +import { TabsComponent } from './../controls/tabs/tabs.component'; +import { SvgExampleComponent } from './svg-example/svg-example.component'; +import { ColorExampleComponent } from './color-example/color-example.component'; +import { ButtonExampleComponent } from './button-example/button-example.component'; +import { ListExampleComponent } from './list-example/list-example.component'; +import { TypographyExampleComponent } from './typography-example/typography-example.component'; +import { DevGuideComponent } from './dev-guide.component'; +import { RouterModule } from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; +import { SharedModule } from './../shared/shared.module'; +import { NgModule, ModuleWithProviders } from '@angular/core'; + +const routing: ModuleWithProviders = RouterModule.forChild([ + { + path: '', component: DevGuideComponent + } +]); + +@NgModule({ + imports: [ + TranslateModule.forChild(), + SharedModule, + routing + ], + declarations: [ + TabsComponent, + TabComponent, + DevGuideComponent, + TypographyExampleComponent, + ListExampleComponent, + ButtonExampleComponent, + ColorExampleComponent, + SvgExampleComponent, + TextboxExampleComponent, + RadioSelectorExampleComponent, + TblExampleComponent, + EditableTblExampleComponent + ], + providers: [] +}) +export class DevGuideModule { } diff --git a/AzureFunctions.AngularClient/src/app/dev-guide/editable-tbl-example/editable-tbl-example.component.html b/AzureFunctions.AngularClient/src/app/dev-guide/editable-tbl-example/editable-tbl-example.component.html new file mode 100644 index 0000000000..9ff14817b0 --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/dev-guide/editable-tbl-example/editable-tbl-example.component.html @@ -0,0 +1,73 @@ +
+

Click-to-Edit Table Example

+
+ + + + + + + + + + + + + + + + + +
+
+ +
+
+    <tbl [items]="groupArray.controls" #table="tbl" name="Editable table">
+        <tr><th colspan="2"></th></tr>
+        <tr *ngFor="let group of table.items">
+            <td>
+                <click-to-edit [group]="group" name="name">
+                    <textbox [control]="group.controls['name']"></textbox>
+                </click-to-edit>
+            </td>
+            <td>
+                <click-to-edit [group]="group" name="value">
+                    <textbox [control]="group.controls['value']"></textbox>
+                </click-to-edit>
+                </td>
+            </tr>
+    </tbl>
+        
+ + +
+
+    export class EditableTblExampleComponent {{ '{' }}
+        @ViewChild('table') table: TblComponent;
+    
+        public groupArray: FormArray;
+    
+        constructor(private _fb: FormBuilder, private _translateService: TranslateService) {{ '{' }}
+            this.groupArray = this._fb.array([]);
+            const requiredValidator = new RequiredValidator(this._translateService);
+    
+            this.groupArray.push(this._fb.group({{ '{' }}
+                name: ['a', requiredValidator.validate.bind(requiredValidator)],
+                value: ['1', requiredValidator.validate.bind(requiredValidator)]
+            {{ '}' }}));
+    
+            this.groupArray.push(this._fb.group({{ '{' }}
+                name: ['b', requiredValidator.validate.bind(requiredValidator)],
+                value: ['2', requiredValidator.validate.bind(requiredValidator)]
+            {{ '}' }}));
+    
+            this.groupArray.push(this._fb.group({{ '{' }}
+                name: ['c', requiredValidator.validate.bind(requiredValidator)],
+                value: ['3', requiredValidator.validate.bind(requiredValidator)]
+            {{ '}' }}));
+        {{ '}' }}
+    {{ '}' }}
+        
+
+
\ No newline at end of file diff --git a/AzureFunctions.AngularClient/src/app/dev-guide/editable-tbl-example/editable-tbl-example.component.scss b/AzureFunctions.AngularClient/src/app/dev-guide/editable-tbl-example/editable-tbl-example.component.scss new file mode 100644 index 0000000000..363c58f121 --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/dev-guide/editable-tbl-example/editable-tbl-example.component.scss @@ -0,0 +1,9 @@ +tr{ + >th{ + height: 5px; + } + + >td{ + width: 200px; + } +} \ No newline at end of file diff --git a/AzureFunctions.AngularClient/src/app/dev-guide/editable-tbl-example/editable-tbl-example.component.ts b/AzureFunctions.AngularClient/src/app/dev-guide/editable-tbl-example/editable-tbl-example.component.ts new file mode 100644 index 0000000000..643ef1b7ff --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/dev-guide/editable-tbl-example/editable-tbl-example.component.ts @@ -0,0 +1,36 @@ +import { TranslateService } from '@ngx-translate/core'; +import { FormArray, FormBuilder } from '@angular/forms'; +import { TblComponent } from './../../controls/tbl/tbl.component'; +import { Component, ViewChild } from '@angular/core'; +import { RequiredValidator } from 'app/shared/validators/requiredValidator'; + +@Component({ + selector: 'editable-tbl-example', + styleUrls: ['./editable-tbl-example.component.scss'], + templateUrl: './editable-tbl-example.component.html' +}) +export class EditableTblExampleComponent { + @ViewChild('table') table: TblComponent; + + public groupArray: FormArray; + + constructor(private _fb: FormBuilder, private _translateService: TranslateService) { + this.groupArray = this._fb.array([]); + const requiredValidator = new RequiredValidator(this._translateService); + + this.groupArray.push(this._fb.group({ + name: ['a', requiredValidator.validate.bind(requiredValidator)], + value: ['1', requiredValidator.validate.bind(requiredValidator)] + })); + + this.groupArray.push(this._fb.group({ + name: ['b', requiredValidator.validate.bind(requiredValidator)], + value: ['2', requiredValidator.validate.bind(requiredValidator)] + })); + + this.groupArray.push(this._fb.group({ + name: ['c', requiredValidator.validate.bind(requiredValidator)], + value: ['3', requiredValidator.validate.bind(requiredValidator)] + })); + } +} diff --git a/AzureFunctions.AngularClient/src/app/dev-guide/list-example/list-example.component.html b/AzureFunctions.AngularClient/src/app/dev-guide/list-example/list-example.component.html new file mode 100644 index 0000000000..a919b90587 --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/dev-guide/list-example/list-example.component.html @@ -0,0 +1,26 @@ +
+

Lists

+

+ Adding a "list-item" or "list-item selected" class will give you common hover and selection coloring. We could probably + make the base styles for this more complex (like add padding, borders, etc...), but we'd have to + update a lot of code that's currently using this class first. +

+
+ +
+
Item 1
+
Item 2
+
Item 3
+
Item 4 selected
+
+
+
+
+
+    <div class="list-item">Item 1</div>
+    <div class="list-item">Item 2</div>
+    <div class="list-item">Item 3</div>
+    <div class="list-item selected">Item 4 selected</div>
+    
+
+
\ No newline at end of file diff --git a/AzureFunctions.AngularClient/src/app/dev-guide/list-example/list-example.component.scss b/AzureFunctions.AngularClient/src/app/dev-guide/list-example/list-example.component.scss new file mode 100644 index 0000000000..269e20a18c --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/dev-guide/list-example/list-example.component.scss @@ -0,0 +1,11 @@ + +@import '../../../sass/common/variables'; + +.list-item{ + width: 400px; +} + +.header > div{ + border: $border; + width: 402px; +} \ No newline at end of file diff --git a/AzureFunctions.AngularClient/src/app/dev-guide/list-example/list-example.component.ts b/AzureFunctions.AngularClient/src/app/dev-guide/list-example/list-example.component.ts new file mode 100644 index 0000000000..1965ccda52 --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/dev-guide/list-example/list-example.component.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'list-example', + styleUrls: ['./list-example.component.scss'], + templateUrl: './list-example.component.html' +}) +export class ListExampleComponent { +} diff --git a/AzureFunctions.AngularClient/src/app/dev-guide/radio-selector-example/radio-selector-example.component.html b/AzureFunctions.AngularClient/src/app/dev-guide/radio-selector-example/radio-selector-example.component.html new file mode 100644 index 0000000000..8f272886e2 --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/dev-guide/radio-selector-example/radio-selector-example.component.html @@ -0,0 +1,34 @@ +
+

Radio Selector

+
+ + +
+
+ +
+
+    <radio-selector [control]="control" [options]="options" [highlightDirty]="true"></radio-selector>
+        
+ + +
+
+    export class RadioSelectorExampleComponent {{ '{' }}
+        control: FormControl;
+        options: SelectOption<'off' | 'on'>[] = [{{ '{' }}
+            displayLabel: 'Off',
+            value: 'off'
+        {{ '}' }},
+        {{ '{' }}
+            displayLabel: 'On',
+            value: 'on'
+        {{ '}' }}];
+    
+        constructor(fb: FormBuilder) {{ '{' }}
+            this.control = fb.control('off', null);
+        {{ '}' }}
+    {{ '}' }}
+        
+
+
\ No newline at end of file diff --git a/AzureFunctions.AngularClient/src/app/site/site-config/general-settings/general-settings.component.spec.ts b/AzureFunctions.AngularClient/src/app/dev-guide/radio-selector-example/radio-selector-example.component.scss similarity index 100% rename from AzureFunctions.AngularClient/src/app/site/site-config/general-settings/general-settings.component.spec.ts rename to AzureFunctions.AngularClient/src/app/dev-guide/radio-selector-example/radio-selector-example.component.scss diff --git a/AzureFunctions.AngularClient/src/app/dev-guide/radio-selector-example/radio-selector-example.component.ts b/AzureFunctions.AngularClient/src/app/dev-guide/radio-selector-example/radio-selector-example.component.ts new file mode 100644 index 0000000000..e7b858ac9c --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/dev-guide/radio-selector-example/radio-selector-example.component.ts @@ -0,0 +1,24 @@ +import { SelectOption } from './../../shared/models/select-option'; +import { FormControl, FormBuilder } from '@angular/forms'; +import { Component } from '@angular/core'; + +@Component({ + selector: 'radio-selector-example', + styleUrls: ['./radio-selector-example.component.scss'], + templateUrl: './radio-selector-example.component.html' +}) +export class RadioSelectorExampleComponent { + control: FormControl; + options: SelectOption<'off' | 'on'>[] = [{ + displayLabel: 'Off', + value: 'off' + }, + { + displayLabel: 'On', + value: 'on' + }]; + + constructor(fb: FormBuilder) { + this.control = fb.control('off', null); + } +} diff --git a/AzureFunctions.AngularClient/src/app/dev-guide/svg-example/svg-example.component.html b/AzureFunctions.AngularClient/src/app/dev-guide/svg-example/svg-example.component.html new file mode 100644 index 0000000000..d33287cfe8 --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/dev-guide/svg-example/svg-example.component.html @@ -0,0 +1,32 @@ +
+

SVG's (Notice that icon colors update with theme changes)

+
+

EXAMPLE OF MONOCHROMATIC ICONS

+ + + + + + + + + + +

EXAMPLE OF POLYCHROMATIC ICON

+ + +
+
+
+
+    <label>Small</label>
+    <span load-image="image/delete.svg" class="icon-small"></span>
+
+    <label>Medium</label>
+    <span load-image="image/delete.svg" class="icon-medium"></span>
+
+    <Label>Large</Label>
+    <span load-image="image/delete.svg" class="icon-large"></span>
+        
+
+
\ No newline at end of file diff --git a/AzureFunctions.AngularClient/src/app/dev-guide/svg-example/svg-example.component.scss b/AzureFunctions.AngularClient/src/app/dev-guide/svg-example/svg-example.component.scss new file mode 100644 index 0000000000..f57b6f2da6 --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/dev-guide/svg-example/svg-example.component.scss @@ -0,0 +1,3 @@ +#poly-icon{ + margin-top: 20px; +} \ No newline at end of file diff --git a/AzureFunctions.AngularClient/src/app/dev-guide/svg-example/svg-example.component.ts b/AzureFunctions.AngularClient/src/app/dev-guide/svg-example/svg-example.component.ts new file mode 100644 index 0000000000..63825a25ea --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/dev-guide/svg-example/svg-example.component.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'svg-example', + styleUrls: ['./svg-example.component.scss'], + templateUrl: './svg-example.component.html' +}) +export class SvgExampleComponent { +} diff --git a/AzureFunctions.AngularClient/src/app/dev-guide/tbl-example/tbl-example.component.html b/AzureFunctions.AngularClient/src/app/dev-guide/tbl-example/tbl-example.component.html new file mode 100644 index 0000000000..2d23b46eb9 --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/dev-guide/tbl-example/tbl-example.component.html @@ -0,0 +1,58 @@ +
+

Simple Sortable Table Example

+
+ + + Sortable header + Non-sortable header + + + + {{item.name}} + {{item.value}} + + +
+
+ +
+
+    <tbl [items]="items" #table="tbl" name="Simple table">
+        <tr>
+            <th><tbl-th name="name">Sortable header</tbl-th></th>
+            <th>Non-sortable header</th>
+        </tr>
+        
+        <tr *ngFor="let item of table.items">
+            <td>{{ '{' }}{{ '{' }}item.name{{ '}' }}{{ '}' }}</td>
+            <td>{{ '{' }}{{ '{' }}item.value{{ '}' }}{{ '}' }}</td>
+        </tr>
+    </tbl>
+        
+ + +
+
+    export class TblExampleComponent {{ '{' }}
+        @ViewChild('table') table: TblComponent;
+    
+        items: {{ '{' }} name: string, value: string }[];
+    
+        constructor() {{ '{' }}
+            this.items = [{{ '{' }}
+                name: 'a',
+                value: '1'
+            {{ '}' }},
+            {{ '{' }}
+                name: 'b',
+                value: '2'
+            {{ '}' }},
+            {{ '{' }}
+                name: 'c',
+                value: '3'
+            {{ '}' }}];
+        {{ '}' }}
+    {{ '}' }}
+        
+
+
\ No newline at end of file diff --git a/AzureFunctions.AngularClient/src/app/dev-guide/tbl-example/tbl-example.component.scss b/AzureFunctions.AngularClient/src/app/dev-guide/tbl-example/tbl-example.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/AzureFunctions.AngularClient/src/app/dev-guide/tbl-example/tbl-example.component.ts b/AzureFunctions.AngularClient/src/app/dev-guide/tbl-example/tbl-example.component.ts new file mode 100644 index 0000000000..ebb35aed24 --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/dev-guide/tbl-example/tbl-example.component.ts @@ -0,0 +1,28 @@ +import { TblComponent } from './../../controls/tbl/tbl.component'; +import { Component, ViewChild } from '@angular/core'; + +@Component({ + selector: 'tbl-example', + styleUrls: ['./tbl-example.component.scss'], + templateUrl: './tbl-example.component.html' +}) +export class TblExampleComponent { + @ViewChild('table') table: TblComponent; + + items: { name: string, value: string }[]; + + constructor() { + this.items = [{ + name: 'a', + value: '1' + }, + { + name: 'b', + value: '2' + }, + { + name: 'c', + value: '3' + }]; + } +} diff --git a/AzureFunctions.AngularClient/src/app/dev-guide/textbox-example/textbox-example.component.html b/AzureFunctions.AngularClient/src/app/dev-guide/textbox-example/textbox-example.component.html new file mode 100644 index 0000000000..45b8b1dfff --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/dev-guide/textbox-example/textbox-example.component.html @@ -0,0 +1,29 @@ +
+

Textbox

+
+ +
+ +
+
+
+ +
+
+    <textbox placeholder="Enter some text and then and delete it" [control]="control"></textbox>
+        
+ + +
+
+    export class TextboxExampleComponent {{ '{' }}
+        control: FormControl;
+    
+        constructor(fb: FormBuilder, translateService: TranslateService){{ '{' }}
+            const required = new RequiredValidator(translateService);
+            this.control = fb.control('', required.validate.bind(required));
+        {{ '}' }}
+    {{ '}' }}
+        
+
+
\ No newline at end of file diff --git a/AzureFunctions.AngularClient/src/app/dev-guide/textbox-example/textbox-example.component.scss b/AzureFunctions.AngularClient/src/app/dev-guide/textbox-example/textbox-example.component.scss new file mode 100644 index 0000000000..1bbf8f406c --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/dev-guide/textbox-example/textbox-example.component.scss @@ -0,0 +1,3 @@ +.textbox-container{ + max-width: 500px; +} \ No newline at end of file diff --git a/AzureFunctions.AngularClient/src/app/dev-guide/textbox-example/textbox-example.component.ts b/AzureFunctions.AngularClient/src/app/dev-guide/textbox-example/textbox-example.component.ts new file mode 100644 index 0000000000..fe540027ca --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/dev-guide/textbox-example/textbox-example.component.ts @@ -0,0 +1,18 @@ +import { TranslateService } from '@ngx-translate/core'; +import { FormControl, FormBuilder } from '@angular/forms'; +import { Component } from '@angular/core'; +import { RequiredValidator } from 'app/shared/validators/requiredValidator'; + +@Component({ + selector: 'textbox-example', + styleUrls: ['./textbox-example.component.scss'], + templateUrl: './textbox-example.component.html' +}) +export class TextboxExampleComponent { + control: FormControl; + + constructor(fb: FormBuilder, translateService: TranslateService){ + const required = new RequiredValidator(translateService); + this.control = fb.control('', required.validate.bind(required)); + } +} diff --git a/AzureFunctions.AngularClient/src/app/dev-guide/typography-example/typography-example.component.html b/AzureFunctions.AngularClient/src/app/dev-guide/typography-example/typography-example.component.html new file mode 100644 index 0000000000..1600fda67c --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/dev-guide/typography-example/typography-example.component.html @@ -0,0 +1,25 @@ +
+

Typography

+
+ +

Heading 1

+

Heading 2

+

Heading 3

+

Heading 4

+ + Link + Regular text +
+
+
+
+    <h1>Heading 1</h1>
+    <h2>Heading 2</h2>
+    <h3>Heading 3</h3>
+    <h4>Heading 4</h4>
+    <label>Label</label>
+    <a>Link</a>
+    Regular text
+    
+
+
\ No newline at end of file diff --git a/AzureFunctions.AngularClient/src/app/dev-guide/typography-example/typography-example.component.scss b/AzureFunctions.AngularClient/src/app/dev-guide/typography-example/typography-example.component.scss new file mode 100644 index 0000000000..782d9c0724 --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/dev-guide/typography-example/typography-example.component.scss @@ -0,0 +1,9 @@ +#typography-example{ + h1,h2,h3,h4,label,a{ + margin-bottom: 10px; + } + + a{ + display: block; + } +} diff --git a/AzureFunctions.AngularClient/src/app/dev-guide/typography-example/typography-example.component.ts b/AzureFunctions.AngularClient/src/app/dev-guide/typography-example/typography-example.component.ts new file mode 100644 index 0000000000..60cb6eb395 --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/dev-guide/typography-example/typography-example.component.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'typography-example', + styleUrls: ['./typography-example.component.scss'], + templateUrl: './typography-example.component.html' +}) +export class TypographyExampleComponent { +} diff --git a/AzureFunctions.AngularClient/src/app/disabled-dashboard/disabled-dashboard.component.scss b/AzureFunctions.AngularClient/src/app/disabled-dashboard/disabled-dashboard.component.scss index c28275a26d..6e7a2fbdc7 100644 --- a/AzureFunctions.AngularClient/src/app/disabled-dashboard/disabled-dashboard.component.scss +++ b/AzureFunctions.AngularClient/src/app/disabled-dashboard/disabled-dashboard.component.scss @@ -1,4 +1,4 @@ -@import '../../sass/main'; +@import '../../sass/common/variables'; .disabled-container{ position: absolute; diff --git a/AzureFunctions.AngularClient/src/app/download-function-app-content/download-function-app-content.component.html b/AzureFunctions.AngularClient/src/app/download-function-app-content/download-function-app-content.component.html index 485b1683bb..1710993e11 100644 --- a/AzureFunctions.AngularClient/src/app/download-function-app-content/download-function-app-content.component.html +++ b/AzureFunctions.AngularClient/src/app/download-function-app-content/download-function-app-content.component.html @@ -2,8 +2,8 @@

{{ 'gettingStarted_newFunctionApp' | translate }} [placeholder]="Resources.enterName | translate">
\ No newline at end of file diff --git a/AzureFunctions.AngularClient/src/app/site/create-app/create-app.component.scss b/AzureFunctions.AngularClient/src/app/site/create-app/create-app.component.scss index afbf039e52..efd2a32ab7 100644 --- a/AzureFunctions.AngularClient/src/app/site/create-app/create-app.component.scss +++ b/AzureFunctions.AngularClient/src/app/site/create-app/create-app.component.scss @@ -10,11 +10,11 @@ margin-right: 10px; } - .text-main2-heading{ + h1{ display: inline-block; } - .text-subheading{ + h4{ margin-top: 5px; img{ @@ -39,5 +39,4 @@ margin-left: 0px; margin-top: 15px; } - } \ No newline at end of file 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 531bed132e..2f3220986e 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 @@ -22,7 +22,7 @@ aria-labelledby="dailyUsageQuotaLabel dailyUsageQuotaValue"> + + + + + + + \ No newline at end of file diff --git a/AzureFunctions.AngularClient/src/app/site/site-config/default-documents/default-documents.component.ts b/AzureFunctions.AngularClient/src/app/site/site-config/default-documents/default-documents.component.ts new file mode 100644 index 0000000000..147e419b45 --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/site/site-config/default-documents/default-documents.component.ts @@ -0,0 +1,349 @@ +import { BroadcastService } from './../../../shared/services/broadcast.service'; +import { Component, Input, OnChanges, OnDestroy, SimpleChanges } from '@angular/core'; +import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Observable } from 'rxjs/Observable'; +import { Subject } from 'rxjs/Subject'; +import { Subscription as RxSubscription } from 'rxjs/Subscription'; +import { TranslateService } from '@ngx-translate/core'; + +import { SiteConfig } from './../../../shared/models/arm/site-config' +import { SaveOrValidationResult } from './../site-config.component'; +import { LogCategories } from 'app/shared/models/constants'; +import { LogService } from './../../../shared/services/log.service'; +import { PortalResources } from './../../../shared/models/portal-resources'; +import { BusyStateScopeManager } from './../../../busy-state/busy-state-scope-manager'; +import { CustomFormControl, CustomFormGroup } from './../../../controls/click-to-edit/click-to-edit.component'; +import { ArmObj } from './../../../shared/models/arm/arm-obj'; +import { CacheService } from './../../../shared/services/cache.service'; +import { AuthzService } from './../../../shared/services/authz.service'; +import { UniqueValidator } from 'app/shared/validators/uniqueValidator'; +import { RequiredValidator } from 'app/shared/validators/requiredValidator'; + +@Component({ + selector: 'default-documents', + templateUrl: './default-documents.component.html', + styleUrls: ['./../site-config.component.scss'] +}) +export class DefaultDocumentsComponent implements OnChanges, OnDestroy { + public Resources = PortalResources; + public groupArray: FormArray; + + private _resourceIdStream: Subject; + private _resourceIdSubscription: RxSubscription; + public hasWritePermissions: boolean; + public permissionsMessage: string; + public showPermissionsMessage: boolean; + public showReadOnlySettingsMessage: string; + + private _busyManager: BusyStateScopeManager; + + private _saveError: string; + + private _requiredValidator: RequiredValidator; + private _uniqueDocumentValidator: UniqueValidator; + + private _webConfigArm: ArmObj; + + public loadingFailureMessage: string; + public loadingMessage: string; + + public newItem: CustomFormGroup; + public originalItemsDeleted: number; + + @Input() mainForm: FormGroup; + + @Input() resourceId: string; + + constructor( + private _cacheService: CacheService, + private _fb: FormBuilder, + private _translateService: TranslateService, + private _logService: LogService, + private _authZService: AuthzService, + broadcastService: BroadcastService + ) { + this._busyManager = new BusyStateScopeManager(broadcastService, 'site-tabs'); + + this._resetPermissionsAndLoadingState(); + + this.newItem = null; + this.originalItemsDeleted = 0; + + this._resourceIdStream = new Subject(); + this._resourceIdSubscription = this._resourceIdStream + .distinctUntilChanged() + .switchMap(() => { + this._busyManager.setBusy(); + this._saveError = null; + this._webConfigArm = null; + this.groupArray = null; + this.newItem = null; + this.originalItemsDeleted = 0; + this._resetPermissionsAndLoadingState(); + return Observable.zip( + this._authZService.hasPermission(this.resourceId, [AuthzService.writeScope]), + this._authZService.hasReadOnlyLock(this.resourceId), + (wp, rl) => ({ writePermission: wp, readOnlyLock: rl }) + ) + }) + .mergeMap(p => { + this._setPermissions(p.writePermission, p.readOnlyLock); + return Observable.zip( + Observable.of(this.hasWritePermissions), + this._cacheService.postArm(`${this.resourceId}/config/web`, true), + (h, w) => ({ hasWritePermissions: h, webConfigResponse: w }) + ) + }) + .do(null, error => { + this._logService.error(LogCategories.defaultDocuments, '/default-documents', error); + this._setupForm(null); + this.loadingFailureMessage = this._translateService.instant(PortalResources.configLoadFailure); + this.loadingMessage = null; + this.showPermissionsMessage = true; + this._busyManager.clearBusy(); + }) + .retry() + .subscribe(r => { + this._webConfigArm = r.webConfigResponse.json(); + this._setupForm(this._webConfigArm); + this.loadingMessage = null; + this.showPermissionsMessage = true; + this._busyManager.clearBusy(); + }); + } + + ngOnChanges(changes: SimpleChanges) { + if (changes['resourceId']) { + this._resourceIdStream.next(this.resourceId); + } + if (changes['mainForm'] && !changes['resourceId']) { + this._setupForm(this._webConfigArm); + } + } + + ngOnDestroy(): void { + if (this._resourceIdSubscription) { + this._resourceIdSubscription.unsubscribe(); + this._resourceIdSubscription = null; + } + this._busyManager.clearBusy(); + } + + private _resetPermissionsAndLoadingState() { + this.hasWritePermissions = true; + this.permissionsMessage = ''; + this.showPermissionsMessage = false; + this.showReadOnlySettingsMessage = this._translateService.instant(PortalResources.configViewReadOnlySettings); + this.loadingFailureMessage = ''; + this.loadingMessage = this._translateService.instant(PortalResources.loading); + } + + private _setPermissions(writePermission: boolean, readOnlyLock: boolean) { + if (!writePermission) { + this.permissionsMessage = this._translateService.instant(PortalResources.configRequiresWritePermissionOnApp); + } else if (readOnlyLock) { + this.permissionsMessage = this._translateService.instant(PortalResources.configDisabledReadOnlyLockOnApp); + } else { + this.permissionsMessage = ""; + } + + this.hasWritePermissions = writePermission && !readOnlyLock; + } + + + private _setupForm(webConfigArm: ArmObj) { + if (!!webConfigArm) { + if (!this._saveError || !this.groupArray) { + this.newItem = null; + this.originalItemsDeleted = 0; + this.groupArray = this._fb.array([]); + + this._requiredValidator = new RequiredValidator(this._translateService); + this._uniqueDocumentValidator = new UniqueValidator( + "name", + this.groupArray, + this._translateService.instant(PortalResources.validation_duplicateError)); + + if (webConfigArm.properties.defaultDocuments) { + webConfigArm.properties.defaultDocuments.forEach(document => { + let group = this._fb.group({ + name: [ + document, + Validators.compose([ + this._requiredValidator.validate.bind(this._requiredValidator), + this._uniqueDocumentValidator.validate.bind(this._uniqueDocumentValidator)])] + }) as CustomFormGroup; + + group._msExistenceState = 'original'; + this.groupArray.push(group); + }) + } + } + + if (this.mainForm.contains("defaultDocs")) { + this.mainForm.setControl("defaultDocs", this.groupArray); + } + else { + this.mainForm.addControl("defaultDocs", this.groupArray); + } + } + else { + this.newItem = null; + this.originalItemsDeleted = 0; + this.groupArray = null; + if (this.mainForm.contains("defaultDocs")) { + this.mainForm.removeControl("defaultDocs"); + } + } + + this._saveError = null; + } + + validate(): SaveOrValidationResult { + let groups = this.groupArray.controls; + + // Purge any added entries that were never modified + for (let i = groups.length - 1; i >= 0; i--) { + let group = groups[i] as CustomFormGroup; + if (group._msStartInEditMode && group.pristine) { + groups.splice(i, 1); + if (group === this.newItem) { + this.newItem = null; + } + } + } + + groups.forEach(group => { + let controls = (group).controls; + for (let controlName in controls) { + let control = controls[controlName]; + control._msRunValidation = true; + control.updateValueAndValidity(); + } + }); + + return { + success: this.groupArray.valid, + error: this.groupArray.valid ? null : this._validationFailureMessage() + }; + } + + save(): Observable { + let defaultDocGroups = this.groupArray.controls; + + if (this.mainForm.contains("defaultDocs") && this.mainForm.controls["defaultDocs"].valid) { + let webConfigArm: ArmObj = JSON.parse(JSON.stringify(this._webConfigArm)); + webConfigArm.properties = {}; + + webConfigArm.properties.defaultDocuments = []; + defaultDocGroups.forEach(group => { + if ((group as CustomFormGroup)._msExistenceState !== 'deleted') { + webConfigArm.properties.defaultDocuments.push((group as FormGroup).controls["name"].value); + } + }) + + return this._cacheService.patchArm(`${this.resourceId}/config/web`, null, webConfigArm) + .map(webConfigResponse => { + this._webConfigArm = webConfigResponse.json(); + return { + success: true, + error: null + }; + }) + .catch(error => { + this._saveError = error._body; + return Observable.of({ + success: false, + error: error._body + }); + }); + } + else { + let failureMessage = this._validationFailureMessage(); + this._saveError = failureMessage; + return Observable.of({ + success: false, + error: failureMessage + }); + } + } + + private _validationFailureMessage(): string { + const configGroupName = this._translateService.instant(PortalResources.feature_defaultDocumentsName); + return this._translateService.instant(PortalResources.configUpdateFailureInvalidInput, { configGroupName: configGroupName }); + } + + deleteItem(group: FormGroup) { + let groups = this.groupArray; + let index = groups.controls.indexOf(group); + if (index >= 0) { + if ((group as CustomFormGroup)._msExistenceState === 'original') { + this._deleteOriginalItem(groups, group); + } + else { + this._deleteAddedItem(groups, group, index); + } + } + } + + private _deleteOriginalItem(groups: FormArray, group: FormGroup) { + // Keep the deleted group around with its state set to dirty. + // This keeps the overall state of this.groupArray and this.mainForm dirty. + group.markAsDirty(); + + // Set the group._msExistenceState to 'deleted' so we know to ignore it when validating and saving. + (group as CustomFormGroup)._msExistenceState = 'deleted'; + + // Force the deleted group to have a valid state by clear all validators on the controls and then running validation. + for (let key in group.controls) { + const control = group.controls[key]; + control.clearAsyncValidators(); + control.clearValidators(); + control.updateValueAndValidity(); + } + + this.originalItemsDeleted++; + + groups.updateValueAndValidity(); + } + + private _deleteAddedItem(groups: FormArray, group: FormGroup, index: number) { + // Remove group from groups + groups.removeAt(index); + if (group === this.newItem) { + this.newItem = null; + } + + // If group was dirty, then groups is also dirty. + // If all the remaining controls in groups are pristine, mark groups as pristine. + if (!group.pristine) { + let pristine = true; + for (let control of groups.controls) { + pristine = pristine && control.pristine; + } + + if (pristine) { + groups.markAsPristine(); + } + } + + groups.updateValueAndValidity(); + } + + addItem() { + let groups = this.groupArray; + + this.newItem = this._fb.group({ + name: [ + null, + Validators.compose([ + this._requiredValidator.validate.bind(this._requiredValidator), + this._uniqueDocumentValidator.validate.bind(this._uniqueDocumentValidator)])], + value: [null] + }) as CustomFormGroup; + + this.newItem._msExistenceState = 'new'; + this.newItem._msStartInEditMode = true; + groups.push(this.newItem); + } +} diff --git a/AzureFunctions.AngularClient/src/app/site/site-config/general-settings/general-settings.component.html b/AzureFunctions.AngularClient/src/app/site/site-config/general-settings/general-settings.component.html index b7d6744865..1dfc0b9fb4 100644 --- a/AzureFunctions.AngularClient/src/app/site/site-config/general-settings/general-settings.component.html +++ b/AzureFunctions.AngularClient/src/app/site/site-config/general-settings/general-settings.component.html @@ -1,31 +1,31 @@

{{ 'feature_generalSettingsName' | translate }}

- ({{permissionsMessage}}) + ({{ permissionsMessage }})
-
-
{{ 'netFrameWorkVersionLabel' | translate }} - +
+
+
-
{{ 'phpVersionLabel' | translate }} +
+
{{ 'feature_generalSettingsName' | translate }}
-
{{ 'pythonVersionLabel' | translate }} +
+
{{ 'feature_generalSettingsName' | translate }}
-
{{ 'javaVersionLabel' | translate }} +
+
{{ 'feature_generalSettingsName' | translate }}
-
{{ 'javaMinorVersionLabel' | translate }} +
+
{{ 'feature_generalSettingsName' | translate }}
-
{{ 'javaWebContainerLabel' | translate }} +
+
{{ 'feature_generalSettingsName' | translate }}
-
{{ 'use32BitWorkerProcessLabel' | translate }} +
+
{{ 'feature_generalSettingsName' | translate }}
-
{{ 'webSocketsEnabledLabel' | translate }} +
+
{{ 'feature_generalSettingsName' | translate }}
-
{{ 'alwaysOnLabel' | translate }} +
+
{{ 'feature_generalSettingsName' | translate }}
-
{{ 'managedPipelineModeLabel' | translate }} -
+
{{ 'feature_generalSettingsName' | translate }}
-
-
{{ 'clientAffinityEnabledLabel' | translate }} -
+
+
{{ 'feature_generalSettingsName' | translate }}
-
-
- {{loadingFailureMessage}} + +
{{ 'autoSwapNotSupportedFromProd' | translate }}
+ +
+ +
+ + +
+
+ +
+ +
+ + +
+
+
+ +
+
+ {{ loadingFailureMessage }}
+ + +

{{ 'feature_debuggingSettingsName' | translate }}

+ ({{ permissionsMessage }}) -

{{ 'feature_debuggingSettingsName' | translate }}

- ({{permissionsMessage}}) +
-
+
+ +
+ + +
+
-
-
{{ 'remoteDebuggingEnabledLabel' | translate }} +
+ +
+ + +
-
- - + +
+
+ {{ loadingFailureMessage }} +
+
+ + + +

{{ 'feature_linuxRuntimeName' | translate }}

+ ({{ permissionsMessage }}) -
-
{{ 'remoteDebuggingVersionLabel' | translate }} +
+ +
+ +
+ + +
-
- - + +
+ +
+ + +
-
-
-
- {{loadingFailureMessage}} +
+
+ {{ loadingFailureMessage }} +
-
-
+
+
-

{{permissionsMessage}}

- +

{{ permissionsMessage }}

+
diff --git a/AzureFunctions.AngularClient/src/app/site/site-config/general-settings/general-settings.component.ts b/AzureFunctions.AngularClient/src/app/site/site-config/general-settings/general-settings.component.ts index 2acb29ef77..92c5cb14a8 100644 --- a/AzureFunctions.AngularClient/src/app/site/site-config/general-settings/general-settings.component.ts +++ b/AzureFunctions.AngularClient/src/app/site/site-config/general-settings/general-settings.component.ts @@ -1,24 +1,26 @@ -import { SiteTabComponent } from './../../site-dashboard/site-tab/site-tab.component'; +import { BroadcastService } from './../../../shared/services/broadcast.service'; import { Component, Input, OnChanges, OnDestroy, SimpleChanges } from '@angular/core'; import { FormBuilder, FormGroup } from '@angular/forms'; import { Observable } from 'rxjs/Observable'; import { Subject } from 'rxjs/Subject'; import { Subscription as RxSubscription } from 'rxjs/Subscription'; import { TranslateService } from '@ngx-translate/core'; -import { SaveResult } from '../site-config.component'; +import { SaveOrValidationResult } from '../site-config.component'; import { Site } from 'app/shared/models/arm/site'; import { SiteConfig } from 'app/shared/models/arm/site-config'; -import { AvailableStackNames, AvailableStack, MajorVersion } from 'app/shared/models/arm/stacks'; -import { DropDownElement } from './../../../shared/models/drop-down-element'; +import { AvailableStackNames, AvailableStack, Framework, MajorVersion, LinuxConstants } from 'app/shared/models/arm/stacks'; +import { DropDownElement, DropDownGroupElement } from './../../../shared/models/drop-down-element'; import { SelectOption } from './../../../shared/models/select-option'; -import { AiService } from './../../../shared/services/ai.service'; +import { LogCategories } from 'app/shared/models/constants'; +import { LogService } from './../../../shared/services/log.service'; import { PortalResources } from './../../../shared/models/portal-resources'; -import { BusyStateComponent } from './../../../busy-state/busy-state.component'; import { BusyStateScopeManager } from './../../../busy-state/busy-state-scope-manager'; +import { CustomFormControl } from './../../../controls/click-to-edit/click-to-edit.component'; import { ArmObj, ArmArrayResult } from './../../../shared/models/arm/arm-obj'; import { CacheService } from './../../../shared/services/cache.service'; import { AuthzService } from './../../../shared/services/authz.service'; +import { SiteDescriptor } from 'app/shared/resourceDescriptors'; import { JavaWebContainerProperties } from './models/java-webcontainer-properties'; @@ -38,14 +40,14 @@ export class GeneralSettingsComponent implements OnChanges, OnDestroy { public showPermissionsMessage: boolean; public showReadOnlySettingsMessage: string; - private _busyState: BusyStateComponent; - private _busyStateScopeManager: BusyStateScopeManager; + private _busyManager: BusyStateScopeManager; private _saveError: string; private _webConfigArm: ArmObj; private _siteConfigArm: ArmObj; public loadingFailureMessage: string; + public loadingMessage: string; private _sku: string; private _kind: string; @@ -67,6 +69,7 @@ export class GeneralSettingsComponent implements OnChanges, OnDestroy { private _selectedJavaVersion: string; + public netFrameworkSupported = false; public phpSupported = false; public pythonSupported = false; public javaSupported = false; @@ -74,24 +77,39 @@ export class GeneralSettingsComponent implements OnChanges, OnDestroy { public webSocketsSupported = false; public alwaysOnSupported = false; public classicPipelineModeSupported = false; + public remoteDebuggingSupported = false; public clientAffinitySupported = false; + public autoSwapSupported = false; + public autoSwapEnabledOptions: SelectOption[]; + public autoSwapSlotNameOptions: DropDownElement[]; + + public linuxRuntimeSupported = false; + public linuxFxVersionOptions: DropDownGroupElement[]; + private _linuxFxVersionOptionsClean: DropDownGroupElement[]; + + @Input() mainForm: FormGroup; @Input() resourceId: string; + private _slotsConfigArmPath: string; + private _slotsConfigArm: ArmArrayResult; + public isProductionSlot: boolean; + private _ignoreChildEvents = true; constructor( private _cacheService: CacheService, private _fb: FormBuilder, private _translateService: TranslateService, - private _aiService: AiService, + private _logService: LogService, private _authZService: AuthzService, - siteTabComponent: SiteTabComponent + broadcastService: BroadcastService ) { - this._busyState = siteTabComponent.busyState; - this._busyStateScopeManager = this._busyState.getScopeManager(); + this._busyManager = new BusyStateScopeManager(broadcastService, 'site-tabs'); + + this._resetSlotsInfo(); this._resetPermissionsAndLoadingState(); @@ -101,13 +119,14 @@ export class GeneralSettingsComponent implements OnChanges, OnDestroy { this._resourceIdSubscription = this._resourceIdStream .distinctUntilChanged() .switchMap(() => { - this._busyStateScopeManager.setBusy(); + this._busyManager.setBusy(); this._saveError = null; this._siteConfigArm = null; this._webConfigArm = null; this.group = null; this.versionOptionsMap = null; this._ignoreChildEvents = true; + this._resetSlotsInfo(); this._resetSupportedControls(); this._resetPermissionsAndLoadingState(); return Observable.zip( @@ -121,28 +140,43 @@ export class GeneralSettingsComponent implements OnChanges, OnDestroy { return Observable.zip( Observable.of(this.hasWritePermissions), this._cacheService.getArm(`${this.resourceId}`, true), + this._cacheService.getArm(this._slotsConfigArmPath, true), this._cacheService.getArm(`${this.resourceId}/config/web`, true), this._cacheService.getArm(`/providers/Microsoft.Web/availablestacks`), - (h, c, w, s) => ({ hasWritePermissions: h, siteConfigResponse: c, webConfigResponse: w, availableStacksResponse: s }) + (h, c, t, w, s) => ({ + hasWritePermissions: h, + siteConfigResponse: c, + slotsConfigResponse: t, + webConfigResponse: w, + availableStacksResponse: s + }) ); }) .do(null, error => { - this._aiService.trackEvent('/errors/general-settings', error); - this._setupForm(this._webConfigArm, this._siteConfigArm); - this.loadingFailureMessage = this._translateService.instant(PortalResources.loading); - this._busyStateScopeManager.clearBusy(); + this._logService.error(LogCategories.generalSettings, '/general-settings', error); + this._setupForm(null, null, null); + this.loadingFailureMessage = this._translateService.instant(PortalResources.configLoadFailure); + this.loadingMessage = null; + this.showPermissionsMessage = true; + this._busyManager.clearBusy(); }) .retry() .subscribe(r => { this._siteConfigArm = r.siteConfigResponse.json(); this._webConfigArm = r.webConfigResponse.json(); + this._slotsConfigArm = r.slotsConfigResponse.json(); const availableStacksArm = r.availableStacksResponse.json(); if (!this._versionOptionsMapClean) { this._parseAvailableStacks(availableStacksArm); } - this._processSkuAndKind(this._siteConfigArm); - this._setupForm(this._webConfigArm, this._siteConfigArm); - this._busyStateScopeManager.clearBusy(); + if (!this._linuxFxVersionOptionsClean) { + this._parseLinuxBuiltInStacks(LinuxConstants.builtInStacks); + } + this._processSupportedControls(this._siteConfigArm, this._webConfigArm, this._slotsConfigArm); + this._setupForm(this._webConfigArm, this._siteConfigArm, this._slotsConfigArm); + this.loadingMessage = null; + this.showPermissionsMessage = true; + this._busyManager.clearBusy(); }); } @@ -151,15 +185,28 @@ export class GeneralSettingsComponent implements OnChanges, OnDestroy { this._resourceIdStream.next(this.resourceId); } if (changes['mainForm'] && !changes['resourceId']) { - this._setupForm(this._webConfigArm, this._siteConfigArm); + this._setupForm(this._webConfigArm, this._siteConfigArm, this._slotsConfigArm); } } ngOnDestroy(): void { if (this._resourceIdSubscription) { - this._resourceIdSubscription.unsubscribe(); this._resourceIdSubscription = null; + this._resourceIdSubscription.unsubscribe(); + this._resourceIdSubscription = null; + } + this._busyManager.clearBusy(); + } + + private _resetSlotsInfo() { + this._slotsConfigArmPath = null; + this._slotsConfigArm = null; + this.isProductionSlot = true; + + if (this.resourceId) { + const siteDescriptor = new SiteDescriptor(this.resourceId); + this._slotsConfigArmPath = `${siteDescriptor.getSiteOnlyResourceId()}/slots`; + this.isProductionSlot = !siteDescriptor.slot; } - this._busyStateScopeManager.dispose(); } private _resetPermissionsAndLoadingState() { @@ -168,6 +215,7 @@ export class GeneralSettingsComponent implements OnChanges, OnDestroy { this.showPermissionsMessage = false; this.showReadOnlySettingsMessage = this._translateService.instant(PortalResources.configViewReadOnlySettings); this.loadingFailureMessage = ''; + this.loadingMessage = this._translateService.instant(PortalResources.loading); } private _setPermissions(writePermission: boolean, readOnlyLock: boolean) { @@ -180,10 +228,10 @@ export class GeneralSettingsComponent implements OnChanges, OnDestroy { } this.hasWritePermissions = writePermission && !readOnlyLock; - this.showPermissionsMessage = true; } private _resetSupportedControls() { + this.netFrameworkSupported = false; this.phpSupported = false; this.pythonSupported = false; this.javaSupported = false; @@ -191,11 +239,15 @@ export class GeneralSettingsComponent implements OnChanges, OnDestroy { this.webSocketsSupported = false; this.alwaysOnSupported = false; this.classicPipelineModeSupported = false; + this.remoteDebuggingSupported = false; this.clientAffinitySupported = false; + this.autoSwapSupported = false; + this.linuxRuntimeSupported = false; } - private _processSkuAndKind(siteConfigArm: ArmObj) { + private _processSupportedControls(siteConfigArm: ArmObj, webConfigArm: ArmObj, slotsConfigArm: ArmArrayResult) { if (!!siteConfigArm) { + let netFrameworkSupported = true; let phpSupported = true; let pythonSupported = true; let javaSupported = true; @@ -203,11 +255,35 @@ export class GeneralSettingsComponent implements OnChanges, OnDestroy { let webSocketsSupported = true; let alwaysOnSupported = true; let classicPipelineModeSupported = true; + let remoteDebuggingSupported = true; let clientAffinitySupported = true; + let autoSwapSupported = false; + let linuxRuntimeSupported = false; this._sku = siteConfigArm.properties.sku; this._kind = siteConfigArm.kind; + if (slotsConfigArm && slotsConfigArm.value && slotsConfigArm.value.length > 0) { + autoSwapSupported = true; + } + + if (this._kind.indexOf('linux') >= 0) { + netFrameworkSupported = false; + phpSupported = false; + pythonSupported = false; + javaSupported = false; + platform64BitSupported = false; + webSocketsSupported = false; + classicPipelineModeSupported = false; + remoteDebuggingSupported = false; + + if ((webConfigArm.properties.linuxFxVersion || '').indexOf(LinuxConstants.dockerPrefix) === -1) { + linuxRuntimeSupported = true; + } + + autoSwapSupported = false; + } + if (this._kind === 'functionapp') { phpSupported = false; pythonSupported = false; @@ -225,6 +301,7 @@ export class GeneralSettingsComponent implements OnChanges, OnDestroy { alwaysOnSupported = false; } + this.netFrameworkSupported = netFrameworkSupported; this.phpSupported = phpSupported; this.pythonSupported = pythonSupported; this.javaSupported = javaSupported; @@ -232,18 +309,23 @@ export class GeneralSettingsComponent implements OnChanges, OnDestroy { this.webSocketsSupported = webSocketsSupported; this.alwaysOnSupported = alwaysOnSupported; this.classicPipelineModeSupported = classicPipelineModeSupported; + this.remoteDebuggingSupported = remoteDebuggingSupported; this.clientAffinitySupported = clientAffinitySupported; + this.autoSwapSupported = autoSwapSupported; + this.linuxRuntimeSupported = linuxRuntimeSupported; } } - private _setupForm(webConfigArm: ArmObj, siteConfigArm: ArmObj) { - if (!!webConfigArm && !!siteConfigArm) { + private _setupForm(webConfigArm: ArmObj, siteConfigArm: ArmObj, slotsConfigArm: ArmArrayResult) { + if (!!webConfigArm && !!siteConfigArm && !!slotsConfigArm) { this._ignoreChildEvents = true; if (!this._saveError || !this.group) { const group = this._fb.group({}); const versionOptionsMap: { [key: string]: DropDownElement[] } = {}; + const linuxFxVersionOptions: DropDownGroupElement[] = []; + const autoSwapSlotNameOptions: DropDownElement[] = []; this._setupNetFramworkVersion(group, versionOptionsMap, webConfigArm.properties.netFrameworkVersion); this._setupPhpVersion(group, versionOptionsMap, webConfigArm.properties.phpVersion); @@ -251,8 +333,14 @@ export class GeneralSettingsComponent implements OnChanges, OnDestroy { this._setupJava(group, versionOptionsMap, webConfigArm.properties.javaVersion, webConfigArm.properties.javaContainer, webConfigArm.properties.javaContainerVersion); this._setupGeneralSettings(group, webConfigArm, siteConfigArm); + this._setupAutoSwapSettings(group, autoSwapSlotNameOptions, webConfigArm, siteConfigArm, slotsConfigArm); + + this._setupLinux(group, linuxFxVersionOptions, webConfigArm.properties.linuxFxVersion, webConfigArm.properties.appCommandLine); + this.group = group; this.versionOptionsMap = versionOptionsMap; + this.linuxFxVersionOptions = linuxFxVersionOptions; + this.autoSwapSlotNameOptions = autoSwapSlotNameOptions; } @@ -295,7 +383,7 @@ export class GeneralSettingsComponent implements OnChanges, OnDestroy { } private _setEnabledStackControls() { - this._setControlsEnabledState(['netFrameWorkVersion'], !this._selectedJavaVersion); + this._setControlsEnabledState(['netFrameworkVersion'], !this._selectedJavaVersion); if (this.phpSupported) { this._setControlsEnabledState(['phpVersion'], !this._selectedJavaVersion); } @@ -341,6 +429,10 @@ export class GeneralSettingsComponent implements OnChanges, OnDestroy { { displayLabel: '2013', value: 'VS2013' }, { displayLabel: '2015', value: 'VS2015' }, { displayLabel: '2017', value: 'VS2017' }]; + + this.autoSwapEnabledOptions = + [{ displayLabel: offString, value: false }, + { displayLabel: onString, value: true }]; } private _setupGeneralSettings(group: FormGroup, webConfigArm: ArmObj, siteConfigArm: ArmObj) { @@ -359,9 +451,11 @@ export class GeneralSettingsComponent implements OnChanges, OnDestroy { if (this.clientAffinitySupported) { group.addControl('clientAffinityEnabled', this._fb.control(siteConfigArm.properties.clientAffinityEnabled)); } - group.addControl('remoteDebuggingEnabled', this._fb.control(webConfigArm.properties.remoteDebuggingEnabled)); - group.addControl('remoteDebuggingVersion', this._fb.control(webConfigArm.properties.remoteDebuggingVersion)); - setTimeout(() => { this._setControlsEnabledState(['remoteDebuggingVersion'], webConfigArm.properties.remoteDebuggingEnabled); }, 0); + if (this.remoteDebuggingSupported) { + group.addControl('remoteDebuggingEnabled', this._fb.control(webConfigArm.properties.remoteDebuggingEnabled)); + group.addControl('remoteDebuggingVersion', this._fb.control(webConfigArm.properties.remoteDebuggingVersion)); + setTimeout(() => { this._setControlsEnabledState(['remoteDebuggingVersion'], webConfigArm.properties.remoteDebuggingEnabled); }, 0); + } } public updateRemoteDebuggingVersionOptions(enabled: boolean) { @@ -370,32 +464,78 @@ export class GeneralSettingsComponent implements OnChanges, OnDestroy { } } + private _setupAutoSwapSettings( + group: FormGroup, + autoSwapSlotNameOptions: DropDownElement[], + webConfigArm: ArmObj, + siteConfigArm: ArmObj, + slotsConfigArm: ArmArrayResult + ) { + if (this.autoSwapSupported) { + if (this.isProductionSlot) { + group.addControl('autoSwapEnabled', this._fb.control(false)); + group.addControl('autoSwapSlotName', this._fb.control(null)); + setTimeout(() => { this._setControlsEnabledState(['autoSwapEnabled', 'autoSwapSlotName'], false); }, 0); + } + else { + const slotNames: string[] = ['production']; + slotsConfigArm.value + .map(s => s.name) + .filter(r => r !== siteConfigArm.name) + .forEach(n => slotNames.push(n.split("/").slice(-1)[0])) + + slotNames.forEach(name => { + autoSwapSlotNameOptions.push({ + displayLabel: name, + value: name, + default: name === webConfigArm.properties.autoSwapSlotName + }); + }) + + group.addControl('autoSwapEnabled', this._fb.control(!!webConfigArm.properties.autoSwapSlotName)); + group.addControl('autoSwapSlotName', this._fb.control(webConfigArm.properties.autoSwapSlotName)); + setTimeout(() => { this._setControlsEnabledState(['autoSwapSlotName'], !!webConfigArm.properties.autoSwapSlotName); }, 0); + } + } + } + + public updateAutoSwapSlotNameOptions(enabled: boolean) { + if (!this._ignoreChildEvents) { + this._setControlsEnabledState(['autoSwapSlotName'], enabled); + setTimeout(() => { + this.group.controls['autoSwapSlotName'].markAsDirty(); + }, 0); + } + } + private _setVersionOptions(name: string, options: DropDownElement[]) { this.versionOptionsMap = this.versionOptionsMap || {}; this.versionOptionsMap[name] = options; } private _setupNetFramworkVersion(group: FormGroup, versionOptionsMap: { [key: string]: DropDownElement[] }, netFrameworkVersion: string) { - let defaultValue = ''; + if (this.netFrameworkSupported) { + let defaultValue = ''; - const netFrameworkVersionOptions: DropDownElement[] = []; - const netFrameworkVersionOptionsClean = this._versionOptionsMapClean[AvailableStackNames.NetStack]; + const netFrameworkVersionOptions: DropDownElement[] = []; + const netFrameworkVersionOptionsClean = this._versionOptionsMapClean[AvailableStackNames.NetStack]; - netFrameworkVersionOptionsClean.forEach(element => { - const match = element.value === netFrameworkVersion || (!element.value && !netFrameworkVersion); - defaultValue = match ? element.value : defaultValue; + netFrameworkVersionOptionsClean.forEach(element => { + const match = element.value === netFrameworkVersion || (!element.value && !netFrameworkVersion); + defaultValue = match ? element.value : defaultValue; - netFrameworkVersionOptions.push({ - displayLabel: element.displayLabel, - value: element.value, - default: match + netFrameworkVersionOptions.push({ + displayLabel: element.displayLabel, + value: element.value, + default: match + }); }); - }); - const netFrameWorkVersionControl = this._fb.control(defaultValue); - group.addControl('netFrameWorkVersion', netFrameWorkVersionControl); + const netFrameworkVersionControl = this._fb.control(defaultValue); + group.addControl('netFrameworkVersion', netFrameworkVersionControl); - versionOptionsMap["netFrameWorkVersion"] = netFrameworkVersionOptions; + versionOptionsMap["netFrameworkVersion"] = netFrameworkVersionOptions; + } } private _setupPhpVersion(group: FormGroup, versionOptionsMap: { [key: string]: DropDownElement[] }, phpVersion: string) { @@ -777,15 +917,92 @@ export class GeneralSettingsComponent implements OnChanges, OnDestroy { this._versionOptionsMapClean[AvailableStackNames.JavaContainer] = javaWebContainerOptions; } - validate() { + private _parseLinuxBuiltInStacks(builtInStacks: Framework[]) { + const linuxFxVersionOptions: DropDownGroupElement[] = []; + + LinuxConstants.builtInStacks.forEach(framework => { + + const dropDownGroupElement: DropDownGroupElement = { + displayLabel: framework.display, + dropDownElements: [] + }; + + framework.majorVersions.forEach(majorVersion => { + + majorVersion.minorVersions.forEach(minorVersion => { + + dropDownGroupElement.dropDownElements.push({ + displayLabel: framework.display + ' ' + minorVersion.displayVersion, + value: framework.name + '|' + minorVersion.displayVersion, + default: false + }); + + }); + + }); + + linuxFxVersionOptions.push(dropDownGroupElement); + }); + + this._linuxFxVersionOptionsClean = linuxFxVersionOptions; + } + + private _setupLinux(group: FormGroup, linuxFxVersionOptions: DropDownGroupElement[], linuxFxVersion: string, appCommandLine: string) { + if (this.linuxRuntimeSupported) { + let defaultFxVersionValue = ''; + + this._linuxFxVersionOptionsClean.forEach(group => { + + const dropDownGroupElement: DropDownGroupElement = { + displayLabel: group.displayLabel, + dropDownElements: [] + }; + + group.dropDownElements.forEach(element => { + + const match = element.value === linuxFxVersion || (!element.value && !linuxFxVersion); + defaultFxVersionValue = match ? element.value : defaultFxVersionValue; + + dropDownGroupElement.dropDownElements.push({ + displayLabel: element.displayLabel, + value: element.value, + default: match + }); + + }); + + linuxFxVersionOptions.push(dropDownGroupElement); + }); + + const linuxFxVersionControl = this._fb.control(defaultFxVersionValue); + group.addControl('linuxFxVersion', linuxFxVersionControl); + + const appCommandLineControl = this._fb.control(appCommandLine); + group.addControl('appCommandLine', appCommandLineControl); + } + } + + validate(): SaveOrValidationResult { + let controls = this.group.controls; + for (let controlName in controls) { + let control = controls[controlName]; + control._msRunValidation = true; + control.updateValueAndValidity(); + } + + return { + success: this.group.valid, + error: this.group.valid ? null : this._validationFailureMessage() + }; } - save(): Observable { + save(): Observable { const generalSettingsControls = this.group.controls; - if (this.mainForm.valid) { + if (this.mainForm.contains("generalSettings") && this.mainForm.controls["generalSettings"].valid) { // level: site const siteConfigArm: ArmObj = JSON.parse(JSON.stringify(this._siteConfigArm)); + if (this.clientAffinitySupported) { const clientAffinityEnabled = (generalSettingsControls['clientAffinityEnabled'].value); siteConfigArm.properties.clientAffinityEnabled = clientAffinityEnabled; @@ -797,7 +1014,8 @@ export class GeneralSettingsComponent implements OnChanges, OnDestroy { } // level: site/config/web - const webConfigArm: ArmObj = JSON.parse(JSON.stringify(this._webConfigArm)); + const webConfigArm: ArmObj = JSON.parse(JSON.stringify(this._webConfigArm)); + webConfigArm.properties = {}; // -- non-stack settings -- if (this.platform64BitSupported) { @@ -812,11 +1030,19 @@ export class GeneralSettingsComponent implements OnChanges, OnDestroy { if (this.classicPipelineModeSupported) { webConfigArm.properties.managedPipelineMode = (generalSettingsControls['managedPipelineMode'].value); } - webConfigArm.properties.remoteDebuggingEnabled = (generalSettingsControls['remoteDebuggingEnabled'].value); - webConfigArm.properties.remoteDebuggingVersion = (generalSettingsControls['remoteDebuggingVersion'].value); + if (this.remoteDebuggingSupported) { + webConfigArm.properties.remoteDebuggingEnabled = (generalSettingsControls['remoteDebuggingEnabled'].value); + webConfigArm.properties.remoteDebuggingVersion = (generalSettingsControls['remoteDebuggingVersion'].value); + } + if (this.autoSwapSupported) { + const autoSwapEnabled = (generalSettingsControls['autoSwapEnabled'].value); + webConfigArm.properties.autoSwapSlotName = autoSwapEnabled ? (generalSettingsControls['autoSwapSlotName'].value) : ''; + } // -- stacks settings -- - webConfigArm.properties.netFrameworkVersion = (generalSettingsControls['netFrameWorkVersion'].value); + if (this.netFrameworkSupported) { + webConfigArm.properties.netFrameworkVersion = (generalSettingsControls['netFrameworkVersion'].value); + } if (this.phpSupported) { webConfigArm.properties.phpVersion = (generalSettingsControls['phpVersion'].value); } @@ -829,10 +1055,14 @@ export class GeneralSettingsComponent implements OnChanges, OnDestroy { webConfigArm.properties.javaContainer = !webConfigArm.properties.javaVersion ? '' : (javaWebContainerProperties.container || ''); webConfigArm.properties.javaContainerVersion = !webConfigArm.properties.javaVersion ? '' : (javaWebContainerProperties.containerMinorVersion || javaWebContainerProperties.containerMajorVersion || ''); } + if (this.linuxRuntimeSupported) { + webConfigArm.properties.linuxFxVersion = (generalSettingsControls['linuxFxVersion'].value); + webConfigArm.properties.appCommandLine = (generalSettingsControls['appCommandLine'].value); + } return Observable.zip( this._cacheService.putArm(`${this.resourceId}`, null, siteConfigArm), - this._cacheService.putArm(`${this.resourceId}/config/web`, null, webConfigArm), + this._cacheService.patchArm(`${this.resourceId}/config/web`, null, webConfigArm), (c, w) => ({ siteConfigResponse: c, webConfigResponse: w }) ) .map(r => { @@ -851,12 +1081,17 @@ export class GeneralSettingsComponent implements OnChanges, OnDestroy { }); }); } else { - const configGroupName = this._translateService.instant(PortalResources.feature_generalSettingsName); - const failureMessage = this._translateService.instant(PortalResources.configUpdateFailureInvalidInput, { configGroupName: configGroupName }); + let failureMessage = this._validationFailureMessage(); + this._saveError = failureMessage; return Observable.of({ success: false, error: failureMessage }); } } + + private _validationFailureMessage(): string { + const configGroupName = this._translateService.instant(PortalResources.feature_generalSettingsName); + return this._translateService.instant(PortalResources.configUpdateFailureInvalidInput, { configGroupName: configGroupName }); + } } diff --git a/AzureFunctions.AngularClient/src/app/site/site-config/handler-mappings/handler-mappings.component.html b/AzureFunctions.AngularClient/src/app/site/site-config/handler-mappings/handler-mappings.component.html new file mode 100644 index 0000000000..e87aeb3669 --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/site/site-config/handler-mappings/handler-mappings.component.html @@ -0,0 +1,99 @@ +
+ +

{{ 'feature_handlerMappingsName' | translate }}

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ (loadingFailureMessage || loadingMessage) }} + + + + + {{ showPermissionsMessage ? permissionsMessage : '' }} + + + + + {{ ('emptyHandlerMappings' | translate) }} + + + + + + + + + + + + + + + + + + +
+
+ {{ loadingFailureMessage }} +
+
+ +
+
+

{{ permissionsMessage }}

+ +
+
+ + +
+ +
diff --git a/AzureFunctions.AngularClient/src/app/site/site-config/handler-mappings/handler-mappings.component.ts b/AzureFunctions.AngularClient/src/app/site/site-config/handler-mappings/handler-mappings.component.ts new file mode 100644 index 0000000000..7b234a1aa3 --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/site/site-config/handler-mappings/handler-mappings.component.ts @@ -0,0 +1,343 @@ +import { BroadcastService } from './../../../shared/services/broadcast.service'; +import { Component, Input, OnChanges, OnDestroy, SimpleChanges } from '@angular/core'; +import { FormArray, FormBuilder, FormGroup } from '@angular/forms'; +import { Observable } from 'rxjs/Observable'; +import { Subject } from 'rxjs/Subject'; +import { Subscription as RxSubscription } from 'rxjs/Subscription'; +import { TranslateService } from '@ngx-translate/core'; + +import { SiteConfig } from './../../../shared/models/arm/site-config' +import { SaveOrValidationResult } from './../site-config.component'; +import { LogCategories } from 'app/shared/models/constants'; +import { LogService } from './../../../shared/services/log.service'; +import { PortalResources } from './../../../shared/models/portal-resources'; +import { BusyStateScopeManager } from './../../../busy-state/busy-state-scope-manager'; +import { CustomFormControl, CustomFormGroup } from './../../../controls/click-to-edit/click-to-edit.component'; +import { ArmObj } from './../../../shared/models/arm/arm-obj'; +import { CacheService } from './../../../shared/services/cache.service'; +import { AuthzService } from './../../../shared/services/authz.service'; +import { RequiredValidator } from 'app/shared/validators/requiredValidator'; + +@Component({ + selector: 'handler-mappings', + templateUrl: './handler-mappings.component.html', + styleUrls: ['./../site-config.component.scss'] +}) +export class HandlerMappingsComponent implements OnChanges, OnDestroy { + public Resources = PortalResources; + public groupArray: FormArray; + + private _resourceIdStream: Subject; + private _resourceIdSubscription: RxSubscription; + public hasWritePermissions: boolean; + public permissionsMessage: string; + public showPermissionsMessage: boolean; + public showReadOnlySettingsMessage: string; + + private _busyManager: BusyStateScopeManager; + + private _saveError: string; + + private _requiredValidator: RequiredValidator; + + private _webConfigArm: ArmObj; + + public loadingFailureMessage: string; + public loadingMessage: string; + + public newItem: CustomFormGroup; + public originalItemsDeleted: number; + + @Input() mainForm: FormGroup; + + @Input() resourceId: string; + + constructor( + private _cacheService: CacheService, + private _fb: FormBuilder, + private _translateService: TranslateService, + private _logService: LogService, + private _authZService: AuthzService, + broadcastService: BroadcastService + ) { + this._busyManager = new BusyStateScopeManager(broadcastService, 'site-tabs'); + + this._resetPermissionsAndLoadingState(); + + this.newItem = null; + this.originalItemsDeleted = 0; + + this._resourceIdStream = new Subject(); + this._resourceIdSubscription = this._resourceIdStream + .distinctUntilChanged() + .switchMap(() => { + this._busyManager.setBusy(); + this._saveError = null; + this._webConfigArm = null; + this.groupArray = null; + this.newItem = null; + this.originalItemsDeleted = 0; + this._resetPermissionsAndLoadingState(); + return Observable.zip( + this._authZService.hasPermission(this.resourceId, [AuthzService.writeScope]), + this._authZService.hasReadOnlyLock(this.resourceId), + (wp, rl) => ({ writePermission: wp, readOnlyLock: rl }) + ) + }) + .mergeMap(p => { + this._setPermissions(p.writePermission, p.readOnlyLock); + return Observable.zip( + Observable.of(this.hasWritePermissions), + this._cacheService.postArm(`${this.resourceId}/config/web`, true), + (h, w) => ({ hasWritePermissions: h, webConfigResponse: w }) + ) + }) + .do(null, error => { + this._logService.error(LogCategories.handlerMappings, '/handler-mappings', error); + this._setupForm(null); + this.loadingFailureMessage = this._translateService.instant(PortalResources.configLoadFailure); + this.loadingMessage = null; + this.showPermissionsMessage = true; + this._busyManager.clearBusy(); + }) + .retry() + .subscribe(r => { + this._webConfigArm = r.webConfigResponse.json(); + this._setupForm(this._webConfigArm); + this.loadingMessage = null; + this.showPermissionsMessage = true; + this._busyManager.clearBusy(); + }); + } + + ngOnChanges(changes: SimpleChanges) { + if (changes['resourceId']) { + this._resourceIdStream.next(this.resourceId); + } + if (changes['mainForm'] && !changes['resourceId']) { + this._setupForm(this._webConfigArm); + } + } + + ngOnDestroy(): void { + if (this._resourceIdSubscription) { + this._resourceIdSubscription.unsubscribe(); + this._resourceIdSubscription = null; + } + this._busyManager.clearBusy(); + } + + private _resetPermissionsAndLoadingState() { + this.hasWritePermissions = true; + this.permissionsMessage = ''; + this.showPermissionsMessage = false; + this.showReadOnlySettingsMessage = this._translateService.instant(PortalResources.configViewReadOnlySettings); + this.loadingFailureMessage = ''; + this.loadingMessage = this._translateService.instant(PortalResources.loading); + } + + private _setPermissions(writePermission: boolean, readOnlyLock: boolean) { + if (!writePermission) { + this.permissionsMessage = this._translateService.instant(PortalResources.configRequiresWritePermissionOnApp); + } else if (readOnlyLock) { + this.permissionsMessage = this._translateService.instant(PortalResources.configDisabledReadOnlyLockOnApp); + } else { + this.permissionsMessage = ""; + } + + this.hasWritePermissions = writePermission && !readOnlyLock; + } + + + private _setupForm(webConfigArm: ArmObj) { + if (!!webConfigArm) { + if (!this._saveError || !this.groupArray) { + this.newItem = null; + this.originalItemsDeleted = 0; + this.groupArray = this._fb.array([]); + + this._requiredValidator = new RequiredValidator(this._translateService); + + if (webConfigArm.properties.handlerMappings) { + webConfigArm.properties.handlerMappings.forEach(mapping => { + let group = this._fb.group({ + extension: [mapping.extension, this._requiredValidator.validate.bind(this._requiredValidator)], + scriptProcessor: [mapping.scriptProcessor, this._requiredValidator.validate.bind(this._requiredValidator)], + arguments: [mapping.arguments] + }) as CustomFormGroup; + + group._msExistenceState = 'original'; + this.groupArray.push(group); + }) + } + } + + if (this.mainForm.contains("handlerMappings")) { + this.mainForm.setControl("handlerMappings", this.groupArray); + } + else { + this.mainForm.addControl("handlerMappings", this.groupArray); + } + } + else { + this.newItem = null; + this.originalItemsDeleted = 0; + this.groupArray = null; + if (this.mainForm.contains("handlerMappings")) { + this.mainForm.removeControl("handlerMappings"); + } + } + + this._saveError = null; + } + + validate(): SaveOrValidationResult { + let groups = this.groupArray.controls; + + // Purge any added entries that were never modified + for (let i = groups.length - 1; i >= 0; i--) { + let group = groups[i] as CustomFormGroup; + if (group._msStartInEditMode && group.pristine) { + groups.splice(i, 1); + if (group === this.newItem) { + this.newItem = null; + } + } + } + + groups.forEach(group => { + let controls = (group).controls; + for (let controlName in controls) { + let control = controls[controlName]; + control._msRunValidation = true; + control.updateValueAndValidity(); + } + }); + + return { + success: this.groupArray.valid, + error: this.groupArray.valid ? null : this._validationFailureMessage() + }; + } + + save(): Observable { + let handlerMappingGroups = this.groupArray.controls; + + if (this.mainForm.contains("handlerMappings") && this.mainForm.controls["handlerMappings"].valid) { + let webConfigArm: ArmObj = JSON.parse(JSON.stringify(this._webConfigArm)); + webConfigArm.properties = {}; + + webConfigArm.properties.handlerMappings = [] + handlerMappingGroups.forEach(group => { + if ((group as CustomFormGroup)._msExistenceState !== 'deleted') { + const formGroup: FormGroup = group as FormGroup; + webConfigArm.properties.handlerMappings.push({ + extension: formGroup.controls["extension"].value, + scriptProcessor: formGroup.controls["scriptProcessor"].value, + arguments: formGroup.controls["arguments"].value, + }); + } + }) + + return this._cacheService.patchArm(`${this.resourceId}/config/web`, null, webConfigArm) + .map(webConfigResponse => { + this._webConfigArm = webConfigResponse.json(); + return { + success: true, + error: null + }; + }) + .catch(error => { + this._saveError = error._body; + return Observable.of({ + success: false, + error: error._body + }); + }); + } + else { + let failureMessage = this._validationFailureMessage(); + this._saveError = failureMessage; + return Observable.of({ + success: false, + error: failureMessage + }); + } + } + + private _validationFailureMessage(): string { + const configGroupName = this._translateService.instant(PortalResources.feature_handlerMappingsName); + return this._translateService.instant(PortalResources.configUpdateFailureInvalidInput, { configGroupName: configGroupName }); + } + + deleteItem(group: FormGroup) { + let groups = this.groupArray; + let index = groups.controls.indexOf(group); + if (index >= 0) { + if ((group as CustomFormGroup)._msExistenceState === 'original') { + this._deleteOriginalItem(groups, group); + } + else { + this._deleteAddedItem(groups, group, index); + } + } + } + + private _deleteOriginalItem(groups: FormArray, group: FormGroup) { + // Keep the deleted group around with its state set to dirty. + // This keeps the overall state of this.groupArray and this.mainForm dirty. + group.markAsDirty(); + + // Set the group._msExistenceState to 'deleted' so we know to ignore it when validating and saving. + (group as CustomFormGroup)._msExistenceState = 'deleted'; + + // Force the deleted group to have a valid state by clear all validators on the controls and then running validation. + for (let key in group.controls) { + const control = group.controls[key]; + control.clearAsyncValidators(); + control.clearValidators(); + control.updateValueAndValidity(); + } + + this.originalItemsDeleted++; + + groups.updateValueAndValidity(); + } + + private _deleteAddedItem(groups: FormArray, group: FormGroup, index: number) { + // Remove group from groups + groups.removeAt(index); + if (group === this.newItem) { + this.newItem = null; + } + + // If group was dirty, then groups is also dirty. + // If all the remaining controls in groups are pristine, mark groups as pristine. + if (!group.pristine) { + let pristine = true; + for (let control of groups.controls) { + pristine = pristine && control.pristine; + } + + if (pristine) { + groups.markAsPristine(); + } + } + + groups.updateValueAndValidity(); + } + + addItem() { + let groups = this.groupArray; + + this.newItem = this._fb.group({ + extension: [null, this._requiredValidator.validate.bind(this._requiredValidator)], + scriptProcessor: [null, this._requiredValidator.validate.bind(this._requiredValidator)], + arguments: [null] + }) as CustomFormGroup; + + this.newItem._msExistenceState = 'new'; + this.newItem._msStartInEditMode = true; + groups.push(this.newItem); + } +} diff --git a/AzureFunctions.AngularClient/src/app/site/site-config/site-config.component.html b/AzureFunctions.AngularClient/src/app/site/site-config/site-config.component.html index 1432f84020..9e3f88b09b 100644 --- a/AzureFunctions.AngularClient/src/app/site/site-config/site-config.component.html +++ b/AzureFunctions.AngularClient/src/app/site/site-config/site-config.component.html @@ -1,18 +1,20 @@ + +
@@ -21,4 +23,10 @@ + + + + + +
\ No newline at end of file diff --git a/AzureFunctions.AngularClient/src/app/site/site-config/site-config.component.scss b/AzureFunctions.AngularClient/src/app/site/site-config/site-config.component.scss index f7d0d0fd70..f63687e7a6 100644 --- a/AzureFunctions.AngularClient/src/app/site/site-config/site-config.component.scss +++ b/AzureFunctions.AngularClient/src/app/site/site-config/site-config.component.scss @@ -1,4 +1,4 @@ -@import '../../../sass/main'; +@import '../../../sass/common/variables'; #site-config-wrapper{ padding: 5px 20px; @@ -23,20 +23,46 @@ td{ } } -.nameCol{ - width: 30%; +.padded-col { + padding-left: 2px; + padding-right: 2px; } -.valueCol{ - width: 65%; +.one-third-col{ + width: 33%; } -.typeCol{ +.two-thirds-Col{ + width: 66%; +} + +.one-half-col{ + width: 50%; +} + +.single-col{ + width: 100%; +} + +.type-col, .is-app-col, .slot-setting-col{ width: 200px; } -.actionCol{ +.is-app-col, .slot-setting-col{ + text-align: center; +} + +.action-col{ width: 25px; + text-align: center; +} + +input[type="checkbox"]{ + vertical-align: text-bottom; +} + +.message-row{ + text-align: center; } .add-setting{ @@ -46,20 +72,29 @@ td{ } h3{ - @extend .text-level2-heading; font-weight: bold; margin-left: 0px; margin-bottom: 10px; margin-top: 35px; padding-bottom: 10px; display: inline-block; - //border-bottom: 1px solid #dedede; &.first-config-heading{ margin-top: 10px; } } +.auto-swap-warning{ + border-left-color: $warning-color; + border-left-style: solid; + border-left-width: 2px; + padding-left: 8px; + padding-top: 4px; + padding-bottom: 4px; + margin-top: 15px; + margin-bottom: 15px; +} + .shield{ position: absolute; left: 0; @@ -80,7 +115,6 @@ h3{ width: 100%; border: 1px solid black; background: rgba(0, 0, 0, 0.60); - color: white; text-align: center; } } @@ -102,10 +136,8 @@ h3{ margin-bottom: auto; margin-left: 15px; margin-right: 25px; - //width: 100%; border: 1px solid black; background: rgba(0, 0, 0, 0.60); - color: white; text-align: center; } @@ -118,25 +150,15 @@ h3{ .setting-label{ box-sizing: border-box; - color: rgb(71, 71, 71); display: inline-block; - font-family: az_ea_font, wf_segoe-ui_normal, "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif; - font-size: 12px; - font-weight: normal; - line-height: normal; min-height: 20px; width: 16%; } .setting-control-container{ - color: rgb(37, 37, 37); display: inline-block; - font-family: az_ea_font, wf_segoe-ui_normal, "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif; - font-size: 12px; - font-weight: normal; - line-height: normal; vertical-align: middle; - width: 255px; + width: 265px; } } } diff --git a/AzureFunctions.AngularClient/src/app/site/site-config/site-config.component.ts b/AzureFunctions.AngularClient/src/app/site/site-config/site-config.component.ts index 6214c76bfb..5705da77da 100644 --- a/AzureFunctions.AngularClient/src/app/site/site-config/site-config.component.ts +++ b/AzureFunctions.AngularClient/src/app/site/site-config/site-config.component.ts @@ -1,4 +1,6 @@ -import { SiteTabComponent } from './../site-dashboard/site-tab/site-tab.component'; +import { CacheService } from 'app/shared/services/cache.service'; +import { Site } from './../../shared/models/arm/site'; +import { ArmObj, ArmObjMap } from './../../shared/models/arm/arm-obj'; import { Component, Input, OnDestroy, ViewChild } from '@angular/core'; import { FormBuilder, FormGroup } from '@angular/forms'; import { Observable } from 'rxjs/Observable'; @@ -7,19 +9,22 @@ import { Subscription as RxSubscription } from 'rxjs/Subscription'; import { TranslateService } from '@ngx-translate/core'; import { PortalResources } from './../../shared/models/portal-resources'; -import { BusyStateComponent } from './../../busy-state/busy-state.component'; import { BusyStateScopeManager } from './../../busy-state/busy-state-scope-manager'; import { TreeViewInfo, SiteData } from './../../tree-view/models/tree-view-info'; import { GeneralSettingsComponent } from './general-settings/general-settings.component'; import { AppSettingsComponent } from './app-settings/app-settings.component'; import { ConnectionStringsComponent } from './connection-strings/connection-strings.component'; +import { DefaultDocumentsComponent } from './default-documents/default-documents.component'; +import { HandlerMappingsComponent } from './handler-mappings/handler-mappings.component'; +import { VirtualDirectoriesComponent } from './virtual-directories/virtual-directories.component'; import { PortalService } from './../../shared/services/portal.service'; import { AuthzService } from './../../shared/services/authz.service'; import { SiteTabIds } from './../../shared/models/constants'; import { BroadcastService } from './../../shared/services/broadcast.service'; -import { AiService } from './../../shared/services/ai.service'; +import { LogCategories } from 'app/shared/models/constants'; +import { LogService } from './../../shared/services/log.service'; -export interface SaveResult { +export interface SaveOrValidationResult { success: boolean; error?: string; } @@ -38,10 +43,10 @@ export class SiteConfigComponent implements OnDestroy { public mainForm: FormGroup; private _valueSubscription: RxSubscription; - private resourceId: string; + public resourceId: string; + public resourceType: string; - private _busyState: BusyStateComponent; - private _busyStateScopeManager: BusyStateScopeManager; + private _busyManager: BusyStateScopeManager; @Input() set viewInfoInput(viewInfo: TreeViewInfo) { this.viewInfoStream.next(viewInfo); @@ -50,36 +55,51 @@ export class SiteConfigComponent implements OnDestroy { @ViewChild(GeneralSettingsComponent) generalSettings: GeneralSettingsComponent; @ViewChild(AppSettingsComponent) appSettings: AppSettingsComponent; @ViewChild(ConnectionStringsComponent) connectionStrings: ConnectionStringsComponent; + @ViewChild(DefaultDocumentsComponent) defaultDocuments: DefaultDocumentsComponent; + @ViewChild(HandlerMappingsComponent) handlerMappings: HandlerMappingsComponent; + @ViewChild(VirtualDirectoriesComponent) virtualDirectories: VirtualDirectoriesComponent; + + private _site: ArmObj; constructor( private _fb: FormBuilder, private _translateService: TranslateService, private _portalService: PortalService, - private _aiService: AiService, + private _logService: LogService, private _broadcastService: BroadcastService, private _authZService: AuthzService, - siteTabsComponent: SiteTabComponent + private _cacheService: CacheService ) { - this._busyState = siteTabsComponent.busyState; - this._busyStateScopeManager = this._busyState.getScopeManager(); + this._busyManager = new BusyStateScopeManager(_broadcastService, 'site-tabs'); this.viewInfoStream = new Subject>(); this._viewInfoSubscription = this.viewInfoStream .distinctUntilChanged() .switchMap(viewInfo => { - this._busyStateScopeManager.setBusy(); + this._busyManager.setBusy(); return Observable.zip( Observable.of(viewInfo.resourceId), this._authZService.hasPermission(viewInfo.resourceId, [AuthzService.writeScope]), this._authZService.hasReadOnlyLock(viewInfo.resourceId), (r, wp, rl) => ({ resourceId: r, writePermission: wp, readOnlyLock: rl }) - ) + ); + }) + .switchMap(res => { + if (res.writePermission && !res.readOnlyLock) { + return this._cacheService.getArm(res.resourceId) + .map(site => { + this._site = >site.json(); + return res; + }); + } else { + return Observable.of(res); + } }) .do(null, error => { this.resourceId = null; this._setupForm(); - this._aiService.trackEvent('/errors/site-config', error); - this._busyStateScopeManager.clearBusy(); + this._logService.error(LogCategories.siteConfig, '/site-config', error); + this._busyManager.clearBusy(); }) .retry() .subscribe(r => { @@ -88,7 +108,37 @@ export class SiteConfigComponent implements OnDestroy { this.hasWritePermissions = r.writePermission && !r.readOnlyLock; this.resourceId = r.resourceId; this._setupForm(); - this._busyStateScopeManager.clearBusy(); + this._busyManager.clearBusy(); + }); + } + + scaleUp() { + const inputs = { + aspResourceId: this._site.properties.serverFarmId, + aseResourceId: this._site.properties.hostingEnvironmentProfile + && this._site.properties.hostingEnvironmentProfile.id + }; + + const openScaleUpBlade = this._portalService.openCollectorBladeWithInputs( + '', + inputs, + 'site-manage', + (value => { + console.log('return from scale'); + }), + 'WebsiteSpecPickerV3'); + + openScaleUpBlade + .first() + .subscribe(r => { + if(r){ + console.log('final call back succeeded!'); + } else{ + console.log('final call back was cancelled'); + } + }, + e => { + console.log('final call back failed!'); }); } @@ -108,6 +158,9 @@ export class SiteConfigComponent implements OnDestroy { if (this.mainForm.dirty) { this._broadcastService.setDirtyState(SiteTabIds.applicationSettings); } + else { + this._broadcastService.clearDirtyState(SiteTabIds.applicationSettings); + } }); } @@ -120,7 +173,7 @@ export class SiteConfigComponent implements OnDestroy { this._valueSubscription.unsubscribe(); this._valueSubscription = null; } - this._busyStateScopeManager.dispose(); + this._busyManager.clearBusy(); this._broadcastService.clearDirtyState(SiteTabIds.applicationSettings); } @@ -128,42 +181,105 @@ export class SiteConfigComponent implements OnDestroy { this.generalSettings.validate(); this.appSettings.validate(); this.connectionStrings.validate(); + this.defaultDocuments.validate(); + this.handlerMappings.validate(); + this.virtualDirectories.validate(); - this._busyStateScopeManager.setBusy(); - let notificationId = null; - this._portalService.startNotification( - this._translateService.instant(PortalResources.configUpdating), - this._translateService.instant(PortalResources.configUpdating)) - .first() - .switchMap(s => { - notificationId = s.id; - return Observable.zip( - this.generalSettings.save(), - this.appSettings.save(), - this.connectionStrings.save(), - (g, a, c) => ({ generalSettingsResult: g, appSettingsResult: a, connectionStringsResult: c }) - ); - }) - .subscribe(r => { - this._busyStateScopeManager.clearBusy(); - - const saveResults: SaveResult[] = [r.generalSettingsResult, r.appSettingsResult, r.connectionStringsResult]; - const saveFailures: string[] = saveResults.filter(r => !r.success).map(r => r.error); - const saveSuccess: boolean = saveFailures.length === 0; - const saveNotification = saveSuccess ? - this._translateService.instant(PortalResources.configUpdateSuccess) : - this._translateService.instant(PortalResources.configUpdateFailure) + JSON.stringify(saveFailures); - - // Even if the save failed, we still need to regenerate mainForm since each child component is saves independently, maintaining its own save state. - // Here we regenerate mainForm (and mark it as dirty on failure), which triggers _setupForm() to run on the child components. In _setupForm(), the child components - // with a successful save state regenerate their form before adding it to mainForm, while those with an unsuccessful save state just add their existing form to mainForm. - this._setupForm(!saveSuccess); - if (!saveSuccess) { - this.mainForm.markAsDirty(); - } + if (this.mainForm.valid) { - this._portalService.stopNotification(notificationId, saveSuccess, saveNotification); - }); + this._busyManager.setBusy(); + let notificationId = null; + let saveAttempted = false; + + this._portalService.startNotification( + this._translateService.instant(PortalResources.configUpdating), + this._translateService.instant(PortalResources.configUpdating)) + .first() + .switchMap(s => { + notificationId = s.id; + + // This is a temporary workaround for merging the slotConfigNames config from AppSettingsModule and ConnectionStringsModule. + // Adding a proper solution (for all config APIs) is tracked here: https://github.com/Azure/azure-functions-ux/issues/1856 + const asConfig: ArmObjMap = this.appSettings.getConfigForSave(); + const csConfig: ArmObjMap = this.connectionStrings.getConfigForSave(); + + const errors = [asConfig.error, csConfig.error].filter(e => !!e); + if (errors.length > 0) { + return Observable.throw(errors); + } + else { + const slotConfigNamesArm: ArmObj = + JSON.parse(JSON.stringify(asConfig["slotConfigNames"])); + slotConfigNamesArm.properties.connectionStringNames = + JSON.parse(JSON.stringify(csConfig["slotConfigNames"].properties.connectionStringNames)); + + return Observable.zip( + this._cacheService.putArm(slotConfigNamesArm.id, null, slotConfigNamesArm), + Observable.of(asConfig["appSettings"]), + Observable.of(csConfig["connectionStrings"]), + (s, a, c) => ({ slotConfigNamesResult: s, appSettingsArm: a, connectionStringsArm: c }) + ); + } + }) + .mergeMap(r => { + saveAttempted = true; + return Observable.zip( + this.generalSettings.save(), + this.appSettings.save(r.appSettingsArm, r.slotConfigNamesResult), + this.connectionStrings.save(r.connectionStringsArm, r.slotConfigNamesResult), + this.defaultDocuments.save(), + this.handlerMappings.save(), + this.virtualDirectories.save(), + (g, a, c, d, h, v) => ({ + generalSettingsResult: g, + appSettingsResult: a, + connectionStringsResult: c, + defaultDocumentsResult: d, + handlerMappingsResult: h, + virtualDirectoriesResult: v + }) + ); + }) + .do(null, error => { + this._logService.error(LogCategories.siteConfig, '/site-config', error); + this._busyManager.clearBusy(); + if (saveAttempted) { + this._setupForm(true /*retain dirty state*/); + this.mainForm.markAsDirty(); + } + this._portalService.stopNotification( + notificationId, + false, + this._translateService.instant(PortalResources.configUpdateFailure) + JSON.stringify(error)); + }) + .subscribe(r => { + this._busyManager.clearBusy(); + + const saveResults: SaveOrValidationResult[] = [ + r.generalSettingsResult, + r.appSettingsResult, + r.connectionStringsResult, + r.defaultDocumentsResult, + r.handlerMappingsResult, + r.virtualDirectoriesResult + ]; + const saveFailures: string[] = saveResults.filter(r => !r.success).map(r => r.error); + const saveSuccess: boolean = saveFailures.length === 0; + const saveNotification = saveSuccess ? + this._translateService.instant(PortalResources.configUpdateSuccess) : + this._translateService.instant(PortalResources.configUpdateFailure) + JSON.stringify(saveFailures); + + // Even if the save failed, we still need to regenerate mainForm since each child component is saves independently, maintaining its own save state. + // Here we regenerate mainForm (and mark it as dirty on failure), which triggers _setupForm() to run on the child components. In _setupForm(), the child components + // with a successful save state regenerate their form before adding it to mainForm, while those with an unsuccessful save state just add their existing form to mainForm. + this._setupForm(!saveSuccess); + if (!saveSuccess) { + this.mainForm.markAsDirty(); + } + + this._portalService.stopNotification(notificationId, saveSuccess, saveNotification); + }); + } } discard() { diff --git a/AzureFunctions.AngularClient/src/app/site/site-config/site-config.module.ts b/AzureFunctions.AngularClient/src/app/site/site-config/site-config.module.ts new file mode 100644 index 0000000000..7c7585cd28 --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/site/site-config/site-config.module.ts @@ -0,0 +1,35 @@ +import { NgModule } from '@angular/core'; +import { SiteConfigComponent } from 'app/site/site-config/site-config.component'; +import { SiteConfigStandaloneComponent } from 'app/site/site-config-standalone/site-config-standalone.component'; +import { GeneralSettingsComponent } from 'app/site/site-config/general-settings/general-settings.component'; +import { AppSettingsComponent } from 'app/site/site-config/app-settings/app-settings.component'; +import { ConnectionStringsComponent } from 'app/site/site-config/connection-strings/connection-strings.component'; +import { AppSettingsShellComponent } from 'app/ibiza-feature/app-settings-shell/app-settings-shell.component'; +import { DefaultDocumentsComponent } from 'app/site/site-config/default-documents/default-documents.component'; +import { HandlerMappingsComponent } from 'app/site/site-config/handler-mappings/handler-mappings.component'; +import { VirtualDirectoriesComponent } from 'app/site/site-config/virtual-directories/virtual-directories.component'; +import { TranslateModule } from '@ngx-translate/core'; +import { SharedModule } from 'app/shared/shared.module'; +import { SharedFunctionsModule } from 'app/shared/shared-functions.module'; + +@NgModule({ + imports: [ + TranslateModule.forChild(), SharedModule, SharedFunctionsModule + ], + declarations: [ + SiteConfigComponent, + SiteConfigStandaloneComponent, + GeneralSettingsComponent, + AppSettingsComponent, + ConnectionStringsComponent, + AppSettingsShellComponent, + DefaultDocumentsComponent, + HandlerMappingsComponent, + VirtualDirectoriesComponent + ], + exports: [ + SiteConfigComponent, + SiteConfigStandaloneComponent + ] +}) +export class SiteConfigModule { } diff --git a/AzureFunctions.AngularClient/src/app/site/site-config/virtual-directories/virtual-directories.component.html b/AzureFunctions.AngularClient/src/app/site/site-config/virtual-directories/virtual-directories.component.html new file mode 100644 index 0000000000..71b4a9afef --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/site/site-config/virtual-directories/virtual-directories.component.html @@ -0,0 +1,102 @@ +
+ +

{{ 'feature_virtualDirectoriesName' | translate }}

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ (loadingFailureMessage || loadingMessage) }} + + + + + {{ showPermissionsMessage ? permissionsMessage : '' }} + + + + + {{ ('emptyVirtualDirectories' | translate) }} + + + + + + + + + + + + + + + + + + +
+
+ {{ loadingFailureMessage }} +
+
+ +
+
+

{{ permissionsMessage }}

+ +
+
+ + +
+ +
diff --git a/AzureFunctions.AngularClient/src/app/site/site-config/virtual-directories/virtual-directories.component.ts b/AzureFunctions.AngularClient/src/app/site/site-config/virtual-directories/virtual-directories.component.ts new file mode 100644 index 0000000000..b0c01b9177 --- /dev/null +++ b/AzureFunctions.AngularClient/src/app/site/site-config/virtual-directories/virtual-directories.component.ts @@ -0,0 +1,428 @@ +import { BroadcastService } from './../../../shared/services/broadcast.service'; +import { Component, Input, OnChanges, OnDestroy, SimpleChanges } from '@angular/core'; +import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Observable } from 'rxjs/Observable'; +import { Subject } from 'rxjs/Subject'; +import { Subscription as RxSubscription } from 'rxjs/Subscription'; +import { TranslateService } from '@ngx-translate/core'; + +import { VirtualApplication, VirtualDirectory } from './../../../shared/models/arm/virtual-application'; +import { SiteConfig } from './../../../shared/models/arm/site-config' +import { SaveOrValidationResult } from './../site-config.component'; +import { LogCategories } from 'app/shared/models/constants'; +import { LogService } from './../../../shared/services/log.service'; +import { PortalResources } from './../../../shared/models/portal-resources'; +import { BusyStateScopeManager } from './../../../busy-state/busy-state-scope-manager'; +import { CustomFormControl, CustomFormGroup } from './../../../controls/click-to-edit/click-to-edit.component'; +import { ArmObj } from './../../../shared/models/arm/arm-obj'; +import { CacheService } from './../../../shared/services/cache.service'; +import { AuthzService } from './../../../shared/services/authz.service'; +import { UniqueValidator } from 'app/shared/validators/uniqueValidator'; +import { RequiredValidator } from 'app/shared/validators/requiredValidator'; + +@Component({ + selector: 'virtual-directories', + templateUrl: './virtual-directories.component.html', + styleUrls: ['./../site-config.component.scss'] +}) +export class VirtualDirectoriesComponent implements OnChanges, OnDestroy { + public Resources = PortalResources; + public groupArray: FormArray; + + private _resourceIdStream: Subject; + private _resourceIdSubscription: RxSubscription; + public hasWritePermissions: boolean; + public permissionsMessage: string; + public showPermissionsMessage: boolean; + public showReadOnlySettingsMessage: string; + + private _busyManager: BusyStateScopeManager; + + private _saveError: string; + + private _requiredValidator: RequiredValidator; + private _uniqueValidator: UniqueValidator; + + private _webConfigArm: ArmObj; + + public loadingFailureMessage: string; + public loadingMessage: string; + + public newItem: CustomFormGroup; + public originalItemsDeleted: number; + + @Input() mainForm: FormGroup; + + @Input() resourceId: string; + + constructor( + private _cacheService: CacheService, + private _fb: FormBuilder, + private _translateService: TranslateService, + private _logService: LogService, + private _authZService: AuthzService, + broadcastService: BroadcastService + ) { + this._busyManager = new BusyStateScopeManager(broadcastService, 'site-tabs'); + + this._resetPermissionsAndLoadingState(); + + this.newItem = null; + this.originalItemsDeleted = 0; + + this._resourceIdStream = new Subject(); + this._resourceIdSubscription = this._resourceIdStream + .distinctUntilChanged() + .switchMap(() => { + this._busyManager.setBusy(); + this._saveError = null; + this._webConfigArm = null; + this.groupArray = null; + this.newItem = null; + this.originalItemsDeleted = 0; + this._resetPermissionsAndLoadingState(); + return Observable.zip( + this._authZService.hasPermission(this.resourceId, [AuthzService.writeScope]), + this._authZService.hasReadOnlyLock(this.resourceId), + (wp, rl) => ({ writePermission: wp, readOnlyLock: rl }) + ) + }) + .mergeMap(p => { + this._setPermissions(p.writePermission, p.readOnlyLock); + return Observable.zip( + Observable.of(this.hasWritePermissions), + this._cacheService.postArm(`${this.resourceId}/config/web`, true), + (h, w) => ({ hasWritePermissions: h, webConfigResponse: w }) + ) + }) + .do(null, error => { + this._logService.error(LogCategories.virtualDirectories, '/virtual-directories', error); + this._setupForm(null); + this.loadingFailureMessage = this._translateService.instant(PortalResources.configLoadFailure); + this.loadingMessage = null; + this.showPermissionsMessage = true; + this._busyManager.clearBusy(); + }) + .retry() + .subscribe(r => { + this._webConfigArm = r.webConfigResponse.json(); + this._setupForm(this._webConfigArm); + this.loadingMessage = null; + this.showPermissionsMessage = true; + this._busyManager.clearBusy(); + }); + } + + ngOnChanges(changes: SimpleChanges) { + if (changes['resourceId']) { + this._resourceIdStream.next(this.resourceId); + } + if (changes['mainForm'] && !changes['resourceId']) { + this._setupForm(this._webConfigArm); + } + } + + ngOnDestroy(): void { + if (this._resourceIdSubscription) { + this._resourceIdSubscription.unsubscribe(); + this._resourceIdSubscription = null; + } + this._busyManager.clearBusy(); + } + + private _resetPermissionsAndLoadingState() { + this.hasWritePermissions = true; + this.permissionsMessage = ''; + this.showPermissionsMessage = false; + this.showReadOnlySettingsMessage = this._translateService.instant(PortalResources.configViewReadOnlySettings); + this.loadingFailureMessage = ''; + this.loadingMessage = this._translateService.instant(PortalResources.loading); + } + + private _setPermissions(writePermission: boolean, readOnlyLock: boolean) { + if (!writePermission) { + this.permissionsMessage = this._translateService.instant(PortalResources.configRequiresWritePermissionOnApp); + } else if (readOnlyLock) { + this.permissionsMessage = this._translateService.instant(PortalResources.configDisabledReadOnlyLockOnApp); + } else { + this.permissionsMessage = ""; + } + + this.hasWritePermissions = writePermission && !readOnlyLock; + } + + private _setupForm(webConfigArm: ArmObj) { + if (!!webConfigArm) { + if (!this._saveError || !this.groupArray) { + this.newItem = null; + this.originalItemsDeleted = 0; + this.groupArray = this._fb.array([]); + + this._requiredValidator = new RequiredValidator(this._translateService); + this._uniqueValidator = new UniqueValidator( + "virtualPath", + this.groupArray, + this._translateService.instant(PortalResources.validation_duplicateError), + this._getNormalizedVirtualPath); + + if (webConfigArm.properties.virtualApplications) { + webConfigArm.properties.virtualApplications.forEach(virtualApplication => { + this._addVDirToGroup( + virtualApplication.virtualPath, + virtualApplication.physicalPath, + true + ); + if (virtualApplication.virtualDirectories) { + virtualApplication.virtualDirectories.forEach(virtualDirectory => { + this._addVDirToGroup( + this._combinePaths(virtualApplication.virtualPath, virtualDirectory.virtualPath), + virtualDirectory.physicalPath, + false + ); + }) + } + }) + } + } + + if (this.mainForm.contains("virtualDirectories")) { + this.mainForm.setControl("virtualDirectories", this.groupArray); + } + else { + this.mainForm.addControl("virtualDirectories", this.groupArray); + } + } + else { + this.newItem = null; + this.originalItemsDeleted = 0; + this.groupArray = null; + if (this.mainForm.contains("virtualDirectories")) { + this.mainForm.removeControl("virtualDirectories"); + } + } + + this._saveError = null; + } + + private _getNormalizedVirtualPath(virtualPath: string): string { + if (virtualPath && virtualPath !== '/') { + if (virtualPath.endsWith('/')) { + virtualPath = virtualPath.slice(0, -1); + } + + if (!virtualPath.startsWith('/')) { + virtualPath = '/' + virtualPath; + } + } + + return virtualPath; + } + + private _addVDirToGroup(virtualPath: string, physicalPath: string, isApplication: boolean) { + let group = this._fb.group({ + virtualPath: [ + virtualPath, + Validators.compose([ + this._requiredValidator.validate.bind(this._requiredValidator), + this._uniqueValidator.validate.bind(this._uniqueValidator)])], + physicalPath: [ + physicalPath, + this._requiredValidator.validate.bind(this._requiredValidator)], + isApplication: [isApplication] + }) as CustomFormGroup; + + group._msExistenceState = 'original'; + this.groupArray.push(group); + } + + private _combinePaths(basePath: string, subPath: string): string { + const basePathAdjusted = (basePath && basePath.endsWith('/')) ? basePath : basePath + '/'; + const subPathAdjusted = (subPath && subPath.startsWith('/')) ? subPath.substring(1) : subPath; + return basePathAdjusted + subPathAdjusted; + } + + validate(): SaveOrValidationResult { + let groups = this.groupArray.controls; + + // Purge any added entries that were never modified + for (let i = groups.length - 1; i >= 0; i--) { + let group = groups[i] as CustomFormGroup; + if (group._msStartInEditMode && group.pristine) { + groups.splice(i, 1); + if (group === this.newItem) { + this.newItem = null; + } + } + } + + groups.forEach(group => { + let controls = (group).controls; + for (let controlName in controls) { + let control = controls[controlName]; + control._msRunValidation = true; + control.updateValueAndValidity(); + } + }); + + return { + success: this.groupArray.valid, + error: this.groupArray.valid ? null : this._validationFailureMessage() + }; + } + + save(): Observable { + let virtualDirGroups = this.groupArray.controls; + + if (this.mainForm.contains("virtualDirectories") && this.mainForm.controls["virtualDirectories"].valid) { + let webConfigArm: ArmObj = JSON.parse(JSON.stringify(this._webConfigArm)); + webConfigArm.properties = {}; + + const virtualApplications: VirtualApplication[] = []; + const virtualDirectories: VirtualDirectory[] = []; + + virtualDirGroups.forEach(group => { + if ((group as CustomFormGroup)._msExistenceState !== 'deleted') { + const formGroup = (group as FormGroup); + if (formGroup.controls["isApplication"].value) { + virtualApplications.push({ + virtualPath: this._getNormalizedVirtualPath(formGroup.controls["virtualPath"].value), + physicalPath: formGroup.controls["physicalPath"].value, + virtualDirectories: [] + }); + } else { + virtualDirectories.push({ + virtualPath: this._getNormalizedVirtualPath(formGroup.controls["virtualPath"].value), + physicalPath: formGroup.controls["physicalPath"].value + }); + } + } + }) + + //TODO: Prevent savinng config with no applictions defined + //if (virtualApplications.length === 0) { //DO SOMETHING - MAYBE HANDLE IN FRORM VALIDATION } + virtualApplications.sort((a, b) => { return b.virtualPath.length - a.virtualPath.length; }); + + virtualDirectories.forEach(virtualDirectory => { + let appFound = false; + const dirPathLen = virtualDirectory.virtualPath.length; + for (let i = 0; i < virtualApplications.length && !appFound; i++) { + const appPathLen = virtualApplications[i].virtualPath.length; + if (appPathLen < dirPathLen && virtualDirectory.virtualPath.startsWith(virtualApplications[i].virtualPath)) { + appFound = true; + virtualDirectory.virtualPath = virtualDirectory.virtualPath.substring(appPathLen); + virtualApplications[i].virtualDirectories.push(virtualDirectory); + } + } + //TODO: Prevent saving config with "orphan" virtual directory + //if (!parentFound) { // DO SOMETHING } + }) + + webConfigArm.properties.virtualApplications = virtualApplications; + return this._cacheService.patchArm(`${this.resourceId}/config/web`, null, webConfigArm) + .map(webConfigResponse => { + this._webConfigArm = webConfigResponse.json(); + return { + success: true, + error: null + }; + }) + .catch(error => { + this._saveError = error._body; + return Observable.of({ + success: false, + error: error._body + }); + }); + } + else { + let failureMessage = this._validationFailureMessage(); + this._saveError = failureMessage; + return Observable.of({ + success: false, + error: failureMessage + }); + } + } + + private _validationFailureMessage(): string { + const configGroupName = this._translateService.instant(PortalResources.feature_virtualDirectoriesName); + return this._translateService.instant(PortalResources.configUpdateFailureInvalidInput, { configGroupName: configGroupName }); + } + + deleteItem(group: FormGroup) { + let groups = this.groupArray; + let index = groups.controls.indexOf(group); + if (index >= 0) { + if ((group as CustomFormGroup)._msExistenceState === 'original') { + this._deleteOriginalItem(groups, group); + } + else { + this._deleteAddedItem(groups, group, index); + } + } + } + + private _deleteOriginalItem(groups: FormArray, group: FormGroup) { + // Keep the deleted group around with its state set to dirty. + // This keeps the overall state of this.groupArray and this.mainForm dirty. + group.markAsDirty(); + + // Set the group._msExistenceState to 'deleted' so we know to ignore it when validating and saving. + (group as CustomFormGroup)._msExistenceState = 'deleted'; + + // Force the deleted group to have a valid state by clear all validators on the controls and then running validation. + for (let key in group.controls) { + const control = group.controls[key]; + control.clearAsyncValidators(); + control.clearValidators(); + control.updateValueAndValidity(); + } + + this.originalItemsDeleted++; + + groups.updateValueAndValidity(); + } + + private _deleteAddedItem(groups: FormArray, group: FormGroup, index: number) { + // Remove group from groups + groups.removeAt(index); + if (group === this.newItem) { + this.newItem = null; + } + + // If group was dirty, then groups is also dirty. + // If all the remaining controls in groups are pristine, mark groups as pristine. + if (!group.pristine) { + let pristine = true; + for (let control of groups.controls) { + pristine = pristine && control.pristine; + } + + if (pristine) { + groups.markAsPristine(); + } + } + + groups.updateValueAndValidity(); + } + + addItem() { + let groups = this.groupArray; + + this.newItem = this._fb.group({ + virtualPath: [ + null, + Validators.compose([ + this._requiredValidator.validate.bind(this._requiredValidator), + this._uniqueValidator.validate.bind(this._uniqueValidator)])], + physicalPath: [ + null, + this._requiredValidator.validate.bind(this._requiredValidator)], + isApplication: [false] + }) as CustomFormGroup; + + this.newItem._msExistenceState = 'new'; + this.newItem._msStartInEditMode = true; + groups.push(this.newItem); + } +} 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 0499e3bc70..85687a2490 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 @@ -4,7 +4,7 @@ [cs-input]="{site: site}" id="site-dashboard-pin" class="link" - src="images/pin.svg" + src="image/pin.svg" (click)="pinPart()" />
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 daafe8862d..b83c91cb22 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,9 +1,8 @@ -@import '../../../sass/main'; +@import '../../../sass/common/variables'; #site-dashboard-container{ - // padding-top: 5px; background-color: $chrome-color; - min-width: 900px; + min-width: 1000px; } #site-dashboard-pin{ @@ -31,23 +30,23 @@ } .site-tab-label{ - @extend .text-subheading; border-bottom: 4px solid $default-text-color; cursor: pointer; padding: 8px 25px; width: 200px; margin-top: 5px; - text-align: center; + text-align: center; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - position: relative; - img{ - height: 16px; - width: 16px; + .icon-small{ + vertical-align: middle; + margin-top: -4px; + margin-right: 5px; + } .close-button{ @@ -58,6 +57,10 @@ .tab-diry{ font-style: italic; } + + h4{ + display: inline; + } } .inactive-label{ @@ -72,7 +75,7 @@ .closeable{ border: 1px #d8e1e6 solid; border-bottom: none; - padding: 8px 16px 12px 5px; + padding: 8px 16px 8px 5px; border-top-left-radius: 6px; border-top-right-radius: 6px; margin-left: 10px; @@ -100,4 +103,27 @@ #site-tab-content{ background-color: $body-bg-color; +} + +:host-context(#app-root[theme=dark]){ + #site-tab-content, #site-dashboard-container, .closeable, .bottom{ + background-color: $chrome-color-dark; + } + + .site-tab-label{ + border-bottom: 4px solid $default-text-color-dark; + } + + .inactive-label{ + border-bottom: 4px solid $chrome-color-dark; + } + + .closeable{ + border-bottom: 4px solid lighten($chrome-color-dark, 10%); + background-color: lighten($chrome-color-dark, 10%); + + &.inactive-label{ + background-color: $chrome-color-dark; + } + } } \ No newline at end of file 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 065a73e2ed..2a9181dd8e 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 @@ -9,7 +9,7 @@ import { FunctionRuntimeComponent } from './../function-runtime/function-runtime import { BroadcastEvent } from 'app/shared/models/broadcast-event'; import { BroadcastService } from './../../shared/services/broadcast.service'; import { SiteManageComponent } from './../site-manage/site-manage.component'; -import { TabInfo } from './../../controls/tabs/tab/tab-info'; +import { TabInfo } from './site-tab/tab-info'; import { SiteSummaryComponent } from './../site-summary/site-summary.component'; import { SiteData } from './../../tree-view/models/tree-view-info'; import { Component, OnDestroy, ElementRef, ViewChild, OnInit } from '@angular/core'; @@ -285,13 +285,13 @@ export class SiteDashboardComponent implements OnDestroy, OnInit { case SiteTabIds.functionRuntime: info.title = this._translateService.instant(PortalResources.tab_functionSettings); - info.iconUrl = 'images/Functions.svg'; + info.iconUrl = 'image/Functions.svg'; info.componentFactory = FunctionRuntimeComponent; break; case SiteTabIds.apiDefinition: info.title = this._translateService.instant(PortalResources.tab_api_definition); - info.iconUrl = 'images/api-definition.svg'; + info.iconUrl = 'image/api-definition.svg'; info.componentFactory = SwaggerDefinitionComponent; break; @@ -303,7 +303,7 @@ export class SiteDashboardComponent implements OnDestroy, OnInit { case SiteTabIds.applicationSettings: info.title = this._translateService.instant(PortalResources.tab_applicationSettings); - info.iconUrl = 'images/application-settings.svg'; + info.iconUrl = 'image/application-settings.svg'; info.componentFactory = SiteConfigComponent; info.closeable = true; break; diff --git a/AzureFunctions.AngularClient/src/app/site/site-dashboard/site-tab/site-tab.component.ts b/AzureFunctions.AngularClient/src/app/site/site-dashboard/site-tab/site-tab.component.ts index 78a7daf9a4..5477ad7d10 100644 --- a/AzureFunctions.AngularClient/src/app/site/site-dashboard/site-tab/site-tab.component.ts +++ b/AzureFunctions.AngularClient/src/app/site/site-dashboard/site-tab/site-tab.component.ts @@ -6,7 +6,7 @@ import { Component, OnChanges, Input, Type, ViewChild, ComponentFactoryResolver, selector: 'site-tab', template: `
- +
diff --git a/AzureFunctions.AngularClient/src/app/controls/tabs/tab/tab-info.ts b/AzureFunctions.AngularClient/src/app/site/site-dashboard/site-tab/tab-info.ts similarity index 100% rename from AzureFunctions.AngularClient/src/app/controls/tabs/tab/tab-info.ts rename to AzureFunctions.AngularClient/src/app/site/site-dashboard/site-tab/tab-info.ts diff --git a/AzureFunctions.AngularClient/src/app/site/site-enabled-features/site-enabled-features.component.html b/AzureFunctions.AngularClient/src/app/site/site-enabled-features/site-enabled-features.component.html index 2c31cfeb59..3ad292c6ce 100644 --- a/AzureFunctions.AngularClient/src/app/site/site-enabled-features/site-enabled-features.component.html +++ b/AzureFunctions.AngularClient/src/app/site/site-enabled-features/site-enabled-features.component.html @@ -3,9 +3,12 @@
- diff --git a/AzureFunctions.AngularClient/src/app/site/site-enabled-features/site-enabled-features.component.scss b/AzureFunctions.AngularClient/src/app/site/site-enabled-features/site-enabled-features.component.scss index 6d8049299f..c06d354c58 100644 --- a/AzureFunctions.AngularClient/src/app/site/site-enabled-features/site-enabled-features.component.scss +++ b/AzureFunctions.AngularClient/src/app/site/site-enabled-features/site-enabled-features.component.scss @@ -1,16 +1,11 @@ -@import '../../../sass/main'; +@import '../../../sass/common/variables'; div.site-enabled-feature{ padding : 6px 0px; line-height: 22px; - img{ - height : 16px; - width: 16px; + .icon-small{ margin: 0px 5px; - } - - &:hover{ - background-color: $chrome-color; + vertical-align: middle; } } \ No newline at end of file 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 05fcfed82f..2b8f7a010c 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 @@ -196,7 +196,7 @@ export class SiteEnabledFeaturesComponent { return { title: this._translateService.instant(PortalResources.tab_functionSettings), feature: feature, - iconUrl: 'images/Functions.svg', + iconUrl: 'image/Functions.svg', featureId: SiteTabIds.functionRuntime }; @@ -204,7 +204,7 @@ export class SiteEnabledFeaturesComponent { return { title: this._translateService.instant(PortalResources.feature_applicationSettingsName), feature: feature, - iconUrl: 'images/application-settings.svg', + iconUrl: 'image/application-settings.svg', featureId: SiteTabIds.applicationSettings }; @@ -212,7 +212,7 @@ export class SiteEnabledFeaturesComponent { return { title: this._translateService.instant(PortalResources.featureEnabled_appInsights), feature: feature, - iconUrl: 'images/appInsights.svg', + iconUrl: 'image/appInsights.svg', bladeInfo: { detailBlade: 'AspNetOverview', detailBladeInputs: { @@ -226,7 +226,7 @@ export class SiteEnabledFeaturesComponent { return { title: this._translateService.instant(PortalResources.featureEnabled_cors).format(args), feature: feature, - iconUrl: 'images/cors.svg', + iconUrl: 'image/cors.svg', bladeInfo: { detailBlade: 'ApiCors', detailBladeInputs: { @@ -239,7 +239,7 @@ export class SiteEnabledFeaturesComponent { return { title: this._translateService.instant(PortalResources.featureEnabled_deploymentSource).format(args), feature: feature, - iconUrl: 'images/deployment-source.svg', + iconUrl: 'image/deployment-source.svg', bladeInfo: { detailBlade: 'ContinuousDeploymentListBlade', detailBladeInputs: { @@ -253,7 +253,7 @@ export class SiteEnabledFeaturesComponent { return { title: this._translateService.instant(PortalResources.authentication), feature: feature, - iconUrl: 'images/authentication.svg', + iconUrl: 'image/authentication.svg', bladeInfo: { detailBlade: 'AppAuth', detailBladeInputs: { @@ -266,7 +266,7 @@ export class SiteEnabledFeaturesComponent { return { title: this._translateService.instant(PortalResources.feature_customDomainsName), feature: feature, - iconUrl: 'images/custom-domains.svg', + iconUrl: 'image/custom-domains.svg', bladeInfo: { detailBlade: 'CustomDomainsAndSSL', detailBladeInputs: { @@ -280,7 +280,7 @@ export class SiteEnabledFeaturesComponent { return { title: this._translateService.instant(PortalResources.featureEnabled_sslCert), feature: feature, - iconUrl: 'images/ssl.svg', + iconUrl: 'image/ssl.svg', bladeInfo: { detailBlade: 'CertificatesBlade', detailBladeInputs: { @@ -293,7 +293,7 @@ export class SiteEnabledFeaturesComponent { return { title: this._translateService.instant(PortalResources.feature_apiDefinitionName), feature: feature, - iconUrl: 'images/api-definition.svg', + iconUrl: 'image/api-definition.svg', featureId: SiteTabIds.apiDefinition }; @@ -301,7 +301,7 @@ export class SiteEnabledFeaturesComponent { return { title: this._translateService.instant(PortalResources.featureEnabled_webjobs).format(args), feature: feature, - iconUrl: 'images/webjobs.svg', + iconUrl: 'image/webjobs.svg', bladeInfo: { detailBlade: 'webjobsNewBlade', @@ -315,7 +315,7 @@ export class SiteEnabledFeaturesComponent { return { title: this._translateService.instant(PortalResources.featureEnabled_extensions).format(args), feature: feature, - iconUrl: 'images/extensions.svg', + iconUrl: 'image/extensions.svg', bladeInfo: { detailBlade: 'SiteExtensionsListBlade', 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 2b64aa17c6..e6236e2590 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 @@ -5,7 +5,7 @@ [(ngModel)]="searchTerm" placeholder="{{ 'searchFeatures' | translate }}" /> - +
diff --git a/AzureFunctions.AngularClient/src/app/site/site-manage/site-manage.component.scss b/AzureFunctions.AngularClient/src/app/site/site-manage/site-manage.component.scss index 5ef7ef0936..b00956dc6d 100644 --- a/AzureFunctions.AngularClient/src/app/site/site-manage/site-manage.component.scss +++ b/AzureFunctions.AngularClient/src/app/site/site-manage/site-manage.component.scss @@ -1,4 +1,5 @@ -@import '../../../sass/main'; +@import '../../../sass/common/variables'; +@import '../../../sass/common/mixins'; #site-manage-container{ padding: 5px 20px; @@ -18,7 +19,7 @@ #site-manage-search-icon{ position: relative; - top: -25px; + top: -22px; left: 19px; width: 14px; height: 14px; diff --git a/AzureFunctions.AngularClient/src/app/site/site-manage/site-manage.component.ts b/AzureFunctions.AngularClient/src/app/site/site-manage/site-manage.component.ts index cff0df2d90..bad875348d 100644 --- a/AzureFunctions.AngularClient/src/app/site/site-manage/site-manage.component.ts +++ b/AzureFunctions.AngularClient/src/app/site/site-manage/site-manage.component.ts @@ -1,4 +1,4 @@ -import { SiteService } from 'app/shared/services/slots.service'; +import { BusyStateScopeManager } from './../../busy-state/busy-state-scope-manager'; import { ScenarioService } from './../../shared/services/scenario/scenario.service'; import { BroadcastService } from './../../shared/services/broadcast.service'; import { Subscription as RxSubscription } from 'rxjs/Subscription'; @@ -12,11 +12,10 @@ import 'rxjs/add/operator/switchMap'; import 'rxjs/add/observable/zip'; import { TranslateService } from '@ngx-translate/core'; import { PortalResources } from './../../shared/models/portal-resources'; -import { GlobalStateService } from './../../shared/services/global-state.service'; import { CacheService } from './../../shared/services/cache.service'; import { TreeViewInfo, SiteData } from './../../tree-view/models/tree-view-info'; import { AiService } from './../../shared/services/ai.service'; -import { DisableableBladeFeature, DisableableFeature, DisableInfo, TabFeature, FeatureItem, BladeFeature } from './../../feature-group/feature-item'; +import { DisableInfo, TabFeature, FeatureItem, BladeFeature, DisableableBladeFeature, DisableableFeature } from './../../feature-group/feature-item'; import { FeatureGroup } from './../../feature-group/feature-group'; import { AuthzService } from '../../shared/services/authz.service'; import { PortalService } from '../../shared/services/portal.service'; @@ -46,11 +45,10 @@ export class SiteManageComponent implements OnDestroy { private _hasSiteWritePermissionStream = new Subject(); private _hasPlanReadPermissionStream = new Subject(); - private _dynamicDisableInfo: DisableInfo; - private _slotDisableInfo: DisableInfo; - private _selectedFeatureSubscription: RxSubscription; + private _busyManager: BusyStateScopeManager; + @Input() set viewInfoInput(viewInfo: TreeViewInfo) { this._viewInfoStream.next(viewInfo); } @@ -60,36 +58,26 @@ export class SiteManageComponent implements OnDestroy { private _portalService: PortalService, private _aiService: AiService, private _cacheService: CacheService, - private _globalStateService: GlobalStateService, private _translateService: TranslateService, private _broadcastService: BroadcastService, private _scenarioService: ScenarioService) { + this._busyManager = new BusyStateScopeManager(_broadcastService, 'site-tabs'); + this._viewInfoStream .switchMap(viewInfo => { + this._busyManager.setBusy(); this.viewInfo = viewInfo; - this._globalStateService.setBusyState(); return this._cacheService.getArm(viewInfo.resourceId); }) .switchMap(r => { - this._globalStateService.clearBusyState(); - + this._busyManager.clearBusy(); this._aiService.stopTrace('/timings/site/tab/features/revealed', this.viewInfo.data.siteTabRevealedTraceKey); const site: ArmObj = r.json(); + this._portalService.closeBlades(); this._descriptor = new SiteDescriptor(site.id); - - this._dynamicDisableInfo = { - enabled: site.properties.sku !== 'Dynamic', - disableMessage: this._translateService.instant(PortalResources.featureNotSupportedConsumption) - }; - - this._slotDisableInfo = { - enabled: !SiteService.isSlot(site.id), - disableMessage: this._translateService.instant(PortalResources.featureNotSupportedForSlots) - }; - this._disposeGroups(); this._initCol1Groups(site); @@ -131,6 +119,7 @@ export class SiteManageComponent implements OnDestroy { } ngOnDestroy() { + this._busyManager.clearBusy(); this._portalService.closeBlades(); this._disposeGroups(); if (this._selectedFeatureSubscription) { @@ -174,7 +163,7 @@ export class SiteManageComponent implements OnDestroy { ' ' + this._translateService.instant(PortalResources.options) + ' github bitbucket dropbox onedrive vsts vso', this._translateService.instant(PortalResources.feature_deploymentSourceInfo), - 'images/deployment-source.svg', + 'image/deployment-source.svg', { detailBlade: 'ContinuousDeploymentListBlade', detailBladeInputs: { @@ -189,7 +178,7 @@ export class SiteManageComponent implements OnDestroy { this._translateService.instant(PortalResources.feature_deploymentCredsName), this._translateService.instant(PortalResources.feature_deploymentCredsName), this._translateService.instant(PortalResources.feature_deploymentCredsInfo), - 'images/deployment-credentials.svg', + 'image/deployment-credentials.svg', { detailBlade: 'FtpCredentials', detailBladeInputs: { @@ -200,24 +189,30 @@ export class SiteManageComponent implements OnDestroy { ]; const developmentToolFeatures = [ - new DisableableBladeFeature( - this._translateService.instant(PortalResources.feature_consoleName), - this._translateService.instant(PortalResources.feature_consoleName) + - ' ' + this._translateService.instant(PortalResources.debug), - this._translateService.instant(PortalResources.feature_consoleInfo), - 'images/console.svg', - { - detailBlade: 'ConsoleBlade', - detailBladeInputs: { - resourceUri: site.id - } - }, - this._portalService, - this._hasSiteWritePermissionStream), + this._scenarioService.checkScenario(ScenarioIds.addConsole, { site: site }).status !== 'disabled' + ? new DisableableBladeFeature( + this._translateService.instant(PortalResources.feature_consoleName), + this._translateService.instant(PortalResources.feature_consoleName) + + ' ' + this._translateService.instant(PortalResources.debug), + this._translateService.instant(PortalResources.feature_consoleInfo), + 'image/console.svg', + { + detailBlade: 'ConsoleBlade', + detailBladeInputs: { + resourceUri: site.id + } + }, + this._portalService, + this._hasSiteWritePermissionStream) + : null, + + this._scenarioService.checkScenario(ScenarioIds.addSsh, { site: site }).status === 'enabled' + ? new OpenSshFeature(site, this._hasSiteWritePermissionStream, this._translateService) + : null, new OpenKuduFeature(site, this._hasSiteWritePermissionStream, this._translateService), - new OpenEditorFeature(site, this._hasSiteWritePermissionStream, this._translateService), + new OpenEditorFeature(site, this._hasSiteWritePermissionStream, this._translateService, this._scenarioService), this._scenarioService.checkScenario(ScenarioIds.addResourceExplorer, { site: site }).status !== 'disabled' ? new OpenResourceExplorer(site, this._translateService) @@ -227,7 +222,7 @@ export class SiteManageComponent implements OnDestroy { this._translateService.instant(PortalResources.feature_extensionsName), this._translateService.instant(PortalResources.feature_extensionsName), this._translateService.instant(PortalResources.feature_extensionsInfo), - 'images/extensions.svg', + 'image/extensions.svg', { detailBlade: 'SiteExtensionsListBlade', detailBladeInputs: { @@ -236,7 +231,7 @@ export class SiteManageComponent implements OnDestroy { }, this._portalService, this._hasSiteWritePermissionStream, - this._dynamicDisableInfo), + this._scenarioService.checkScenario(ScenarioIds.enableExtensions, { site: site })), ]; const generalFeatures: FeatureItem[] = [ @@ -244,7 +239,7 @@ export class SiteManageComponent implements OnDestroy { this._translateService.instant(PortalResources.tab_functionSettings), this._translateService.instant(PortalResources.tab_functionSettings), this._translateService.instant(PortalResources.feature_functionSettingsInfo), - 'images/functions.svg', + 'image/functions.svg', SiteTabIds.functionRuntime, this._broadcastService), @@ -252,7 +247,7 @@ export class SiteManageComponent implements OnDestroy { this._translateService.instant(PortalResources.tab_applicationSettings), this._translateService.instant(PortalResources.tab_applicationSettings), this._translateService.instant(PortalResources.feature_applicationSettingsInfo), - 'images/application-settings.svg', + 'image/application-settings.svg', SiteTabIds.applicationSettings, this._broadcastService), @@ -260,7 +255,7 @@ export class SiteManageComponent implements OnDestroy { this._translateService.instant(PortalResources.feature_propertiesName), this._translateService.instant(PortalResources.feature_propertiesName), this._translateService.instant(PortalResources.feature_propertiesInfo), - 'images/properties.svg', + 'image/properties.svg', { detailBlade: 'PropertySheetBlade', detailBladeInputs: { @@ -273,7 +268,7 @@ export class SiteManageComponent implements OnDestroy { this._translateService.instant(PortalResources.feature_backupsName), this._translateService.instant(PortalResources.feature_backupsName), this._translateService.instant(PortalResources.feature_backupsInfo), - 'images/backups.svg', + 'image/backups.svg', { detailBlade: 'BackupSummaryBlade', detailBladeInputs: { @@ -282,7 +277,7 @@ export class SiteManageComponent implements OnDestroy { }, this._portalService, this._hasSiteWritePermissionStream, - this._dynamicDisableInfo), + this._scenarioService.checkScenario(ScenarioIds.enableBackups, { site: site })), new BladeFeature( this._translateService.instant(PortalResources.feature_allSettingsName), @@ -290,7 +285,7 @@ export class SiteManageComponent implements OnDestroy { ' ' + this._translateService.instant(PortalResources.supportRequest) + ' ' + this._translateService.instant(PortalResources.scale), this._translateService.instant(PortalResources.feature_allSettingsInfo), - 'images/webapp.svg', + 'image/webapp.svg', { detailBlade: 'AppsOverviewBlade', detailBladeInputs: { @@ -318,7 +313,7 @@ export class SiteManageComponent implements OnDestroy { ' ' + this._translateService.instant(PortalResources.hybridConnections) + ' vnet', this._translateService.instant(PortalResources.feature_networkingInfo), - 'images/networking.svg', + 'image/networking.svg', { detailBlade: 'NetworkSummaryBlade', detailBladeInputs: { @@ -327,13 +322,13 @@ export class SiteManageComponent implements OnDestroy { }, this._portalService, this._hasSiteWritePermissionStream, - this._dynamicDisableInfo), + this._scenarioService.checkScenario(ScenarioIds.enableNetworking, { site: site })), new DisableableBladeFeature( 'SSL', 'ssl', this._translateService.instant(PortalResources.feature_sslInfo), - 'images/ssl.svg', + 'image/ssl.svg', { detailBlade: 'CertificatesBlade', detailBladeInputs: { resourceUri: site.id } @@ -345,7 +340,7 @@ export class SiteManageComponent implements OnDestroy { this._translateService.instant(PortalResources.feature_customDomainsName), this._translateService.instant(PortalResources.feature_customDomainsName), this._translateService.instant(PortalResources.feature_customDomainsInfo), - 'images/custom-domains.svg', + 'image/custom-domains.svg', { detailBlade: 'CustomDomainsAndSSL', detailBladeInputs: { @@ -362,41 +357,45 @@ export class SiteManageComponent implements OnDestroy { ' ' + this._translateService.instant(PortalResources.authorization) + ' aad google facebook microsoft', this._translateService.instant(PortalResources.feature_authInfo), - 'images/authentication.svg', + 'image/authentication.svg', { detailBlade: 'AppAuth', detailBladeInputs: { resourceUri: site.id } }, this._portalService, - this._hasSiteWritePermissionStream), + this._hasSiteWritePermissionStream, + this._scenarioService.checkScenario(ScenarioIds.enableAuth, { site: site })), - new DisableableBladeFeature( - this._translateService.instant(PortalResources.feature_msiName), - this._translateService.instant(PortalResources.feature_msiName) + - this._translateService.instant(PortalResources.authentication) + - 'MSI', - this._translateService.instant(PortalResources.feature_msiInfo), - 'images/toolbox.svg', - { - detailBlade: 'MSIBlade', - detailBladeInputs: { resourceUri: site.id } - }, - this._portalService, - null, - this._slotDisableInfo), + this._scenarioService.checkScenario(ScenarioIds.addMsi, { site: site }).status !== 'disabled' + ? new DisableableBladeFeature( + this._translateService.instant(PortalResources.feature_msiName), + this._translateService.instant(PortalResources.feature_msiName) + + this._translateService.instant(PortalResources.authentication) + + 'MSI', + this._translateService.instant(PortalResources.feature_msiInfo), + 'image/toolbox.svg', + { + detailBlade: 'MSIBlade', + detailBladeInputs: { resourceUri: site.id } + }, + this._portalService, + null, + this._scenarioService.checkScenario(ScenarioIds.enableMsi, { site: site })) + : null, this._scenarioService.checkScenario(ScenarioIds.addPushNotifications, { site: site }).status !== 'disabled' ? new DisableableBladeFeature( this._translateService.instant(PortalResources.feature_pushNotificationsName), this._translateService.instant(PortalResources.feature_pushNotificationsName), this._translateService.instant(PortalResources.feature_pushNotificationsInfo), - 'images/push.svg', + 'image/push.svg', { detailBlade: 'PushRegistrationBlade', detailBladeInputs: { resourceUri: this._descriptor.resourceId } }, this._portalService, - this._hasSiteWritePermissionStream) + this._hasSiteWritePermissionStream, + this._scenarioService.checkScenario(ScenarioIds.enablePushNotifications, { site: site })) : null, ]; @@ -405,7 +404,7 @@ export class SiteManageComponent implements OnDestroy { this._translateService.instant(PortalResources.feature_diagnosticLogsName), this._translateService.instant(PortalResources.feature_diagnosticLogsName), this._translateService.instant(PortalResources.feature_diagnosticLogsInfo), - 'images/diagnostic-logs.svg', + 'image/diagnostic-logs.svg', { detailBlade: 'WebsiteLogsBlade', detailBladeInputs: { WebsiteId: this._descriptor.getWebsiteId() } @@ -416,38 +415,27 @@ export class SiteManageComponent implements OnDestroy { this._translateService.instant(PortalResources.feature_logStreamingName), this._translateService.instant(PortalResources.feature_logStreamingName), this._translateService.instant(PortalResources.feature_logStreamingInfo), - 'images/log-stream.svg', + 'image/log-stream.svg', { detailBlade: 'LogStreamBlade', detailBladeInputs: { resourceUri: site.id } }, this._portalService, - this._hasSiteWritePermissionStream), + this._hasSiteWritePermissionStream, + this._scenarioService.checkScenario(ScenarioIds.enableLogStream, { site: site })), new DisableableBladeFeature( this._translateService.instant(PortalResources.feature_processExplorerName), this._translateService.instant(PortalResources.feature_processExplorerName), this._translateService.instant(PortalResources.feature_processExplorerInfo), - 'images/process-explorer.svg', + 'image/process-explorer.svg', { detailBlade: 'ProcExpNewBlade', detailBladeInputs: { resourceUri: site.id } }, this._portalService, - this._hasSiteWritePermissionStream), - - this._scenarioService.checkScenario(ScenarioIds.addTinfoil, { site: site }).status !== 'disabled' - ? new BladeFeature( - this._translateService.instant(PortalResources.feature_securityScanningName), - this._translateService.instant(PortalResources.feature_securityScanningName) + ' tinfoil', - this._translateService.instant(PortalResources.feature_securityScanningInfo), - 'images/tinfoil-flat-21px.png', - { - detailBlade: 'TinfoilSecurityBlade', - detailBladeInputs: { WebsiteId: this._descriptor.getWebsiteId() } - }, - this._portalService) - : null, + this._hasSiteWritePermissionStream, + this._scenarioService.checkScenario(ScenarioIds.enableProcessExplorer, { site: site })) ]; this.groups2 = [ @@ -465,7 +453,7 @@ export class SiteManageComponent implements OnDestroy { this._translateService.instant(PortalResources.feature_apiDefinitionName), this._translateService.instant(PortalResources.feature_apiDefinitionName) + ' swagger', this._translateService.instant(PortalResources.feature_apiDefinitionInfo), - 'images/api-definition.svg', + 'image/api-definition.svg', SiteTabIds.apiDefinition, this._broadcastService ), @@ -474,7 +462,7 @@ export class SiteManageComponent implements OnDestroy { 'CORS', 'cors api', this._translateService.instant(PortalResources.feature_corsInfo), - 'images/cors.svg', + 'image/cors.svg', { detailBlade: 'ApiCors', detailBladeInputs: { resourceUri: site.id } @@ -488,7 +476,7 @@ export class SiteManageComponent implements OnDestroy { this._translateService.instant(PortalResources.appServicePlan) + ' ' + this._translateService.instant(PortalResources.scale), this._translateService.instant(PortalResources.feature_appServicePlanInfo), - 'images/app-service-plan.svg', + 'image/app-service-plan.svg', { detailBlade: 'WebHostingPlanBlade', detailBladeInputs: { id: site.properties.serverFarmId } @@ -496,12 +484,12 @@ export class SiteManageComponent implements OnDestroy { this._portalService, this._hasPlanReadPermissionStream), - this._scenarioService.checkScenario(ScenarioIds.showSiteQuotas, { site: site }).status !== 'disabled' + this._scenarioService.checkScenario(ScenarioIds.addSiteQuotas, { site: site }).status !== 'disabled' ? new DisableableBladeFeature( this._translateService.instant(PortalResources.feature_quotasName), this._translateService.instant(PortalResources.feature_quotasName), this._translateService.instant(PortalResources.feature_quotasInfo), - 'images/quotas.svg', + 'image/quotas.svg', { detailBlade: 'QuotasBlade', detailBladeInputs: { @@ -512,12 +500,12 @@ export class SiteManageComponent implements OnDestroy { this._hasPlanReadPermissionStream) : null, - this._scenarioService.checkScenario(ScenarioIds.showSiteFileStorage, { site: site }).status !== 'disabled' + this._scenarioService.checkScenario(ScenarioIds.addSiteFileStorage, { site: site }).status !== 'disabled' ? new DisableableBladeFeature( this._translateService.instant(PortalResources.feature_quotasName), this._translateService.instant(PortalResources.feature_quotasName), this._translateService.instant(PortalResources.feature_quotasInfo), - 'images/quotas.svg', + 'image/quotas.svg', { detailBlade: 'FileSystemStorage', detailBladeInputs: { @@ -536,7 +524,7 @@ export class SiteManageComponent implements OnDestroy { ' ' + this._translateService.instant(PortalResources.feature_activityLogName) + ' ' + this._translateService.instant(PortalResources.events), this._translateService.instant(PortalResources.feature_activityLogInfo), - 'images/activity-log.svg', + 'image/activity-log.svg', { detailBlade: 'EventsBrowseBlade', detailBladeInputs: { @@ -553,7 +541,7 @@ export class SiteManageComponent implements OnDestroy { this._translateService.instant(PortalResources.feature_accessControlName), this._translateService.instant(PortalResources.feature_accessControlName) + ' rbac', this._translateService.instant(PortalResources.feature_accessControlInfo), - 'images/access-control.svg', + 'image/access-control.svg', { detailBlade: 'UserAssignmentsV2Blade', detailBladeInputs: { @@ -568,7 +556,7 @@ export class SiteManageComponent implements OnDestroy { this._translateService.instant(PortalResources.feature_tagsName), this._translateService.instant(PortalResources.feature_tagsName), this._translateService.instant(PortalResources.feature_tagsInfo), - 'images/tags.svg', + 'image/tags.svg', { detailBlade: 'ResourceTagsListBlade', detailBladeInputs: { @@ -583,7 +571,7 @@ export class SiteManageComponent implements OnDestroy { this._translateService.instant(PortalResources.feature_locksName), this._translateService.instant(PortalResources.feature_locksName), this._translateService.instant(PortalResources.feature_locksInfo), - 'images/locks.svg', + 'image/locks.svg', { detailBlade: 'LocksBlade', detailBladeInputs: { @@ -601,7 +589,7 @@ export class SiteManageComponent implements OnDestroy { ' ' + this._translateService.instant(PortalResources.template) + ' arm', this._translateService.instant(PortalResources.feature_automationScriptInfo), - 'images/automation-script.svg', + 'image/automation-script.svg', { detailBlade: 'TemplateViewerBlade', detailBladeInputs: { @@ -630,6 +618,27 @@ export class SiteManageComponent implements OnDestroy { } } +export class OpenSshFeature extends DisableableFeature { + constructor( + private _site: ArmObj, + disableInfoStream: Subject, + _translateService: TranslateService) { + + super( + _translateService.instant(PortalResources.feature_sshName), + _translateService.instant(PortalResources.feature_sshName) + + _translateService.instant(PortalResources.feature_consoleName), + _translateService.instant(PortalResources.feature_sshInfo), + 'image/console.svg', + disableInfoStream); + } + + click() { + const scmHostName = this._site.properties.hostNameSslStates.find(h => h.hostType === 1).name; + window.open(`https://${scmHostName}/webssh/host`); + } +} + export class OpenKuduFeature extends DisableableFeature { constructor( private _site: ArmObj, @@ -640,7 +649,7 @@ export class OpenKuduFeature extends DisableableFeature { _translateService.instant(PortalResources.feature_advancedToolsName), _translateService.instant(PortalResources.feature_advancedToolsName) + ' kudu', _translateService.instant(PortalResources.feature_advancedToolsInfo), - 'images/advanced-tools.svg', + 'image/advanced-tools.svg', disableInfoStream); } @@ -651,14 +660,18 @@ export class OpenKuduFeature extends DisableableFeature { } export class OpenEditorFeature extends DisableableFeature { - constructor(private _site: ArmObj, disabledInfoStream: Subject, _translateService: TranslateService) { + constructor( + private _site: ArmObj, + disabledInfoStream: Subject, + _translateService: TranslateService, + scenarioService: ScenarioService) { super( _translateService.instant(PortalResources.feature_appServiceEditorName), _translateService.instant(PortalResources.feature_appServiceEditorName), _translateService.instant(PortalResources.feature_appServiceEditorInfo), - 'images/appsvc-editor.svg', - disabledInfoStream); + 'image/appsvc-editor.svg', + disabledInfoStream, scenarioService.checkScenario(ScenarioIds.enableAppServiceEditor, { site: _site })); } click() { @@ -673,7 +686,7 @@ export class OpenResourceExplorer extends FeatureItem { _translateService.instant(PortalResources.feature_resourceExplorerName), _translateService.instant(PortalResources.feature_resourceExplorerName), _translateService.instant(PortalResources.feature_resourceExplorerInfo), - 'images/resource-explorer.svg'); + 'image/resource-explorer.svg'); } click() { diff --git a/AzureFunctions.AngularClient/src/app/site/site-summary/site-summary.component.html b/AzureFunctions.AngularClient/src/app/site/site-summary/site-summary.component.html index 94056c3b45..57816253c4 100644 --- a/AzureFunctions.AngularClient/src/app/site/site-summary/site-summary.component.html +++ b/AzureFunctions.AngularClient/src/app/site/site-summary/site-summary.component.html @@ -1,21 +1,38 @@ + [iconUrl]="site?.properties.state === 'Running' ? 'image/stop.svg' : 'image/start.svg'" + (click)="toggleState()" + [disabled]="!hasWriteAccess"> - + - + - - - - + @@ -131,7 +148,7 @@
-

{{ 'enabledFeatures_header' | translate }}

+

{{ 'enabledFeatures_header' | translate }}

-
+
\ No newline at end of file diff --git a/AzureFunctions.AngularClient/src/app/site/site-summary/site-summary.component.scss b/AzureFunctions.AngularClient/src/app/site/site-summary/site-summary.component.scss index 7e478b2d80..d19b1b9cea 100644 --- a/AzureFunctions.AngularClient/src/app/site/site-summary/site-summary.component.scss +++ b/AzureFunctions.AngularClient/src/app/site/site-summary/site-summary.component.scss @@ -1,4 +1,4 @@ -@import '../../../sass/main'; +@import '../../../sass/common/variables'; .site-faded-text{ font-style: italic; @@ -8,13 +8,6 @@ .site-summary-actions{ padding-left: 12px; - img{ - height: 15px; - width: 15px; - vertical-align: top; - margin-right: 5px; - } - .disabled-action{ opacity: 0.4; filter: alpha(opacity=40); /* msie */ @@ -70,6 +63,13 @@ #site-summary-body{ padding: 10px 35px; + + h2{ + margin-top: 20px; + margin-bottom: 10px; + font-size: 24px; + font-weight: 400; + } } #site-summary-features{ diff --git a/AzureFunctions.AngularClient/src/app/site/site-summary/site-summary.component.ts b/AzureFunctions.AngularClient/src/app/site/site-summary/site-summary.component.ts index 1c439f6f5f..a52248e606 100644 --- a/AzureFunctions.AngularClient/src/app/site/site-summary/site-summary.component.ts +++ b/AzureFunctions.AngularClient/src/app/site/site-summary/site-summary.component.ts @@ -1,8 +1,8 @@ +import { BroadcastService } from './../../shared/services/broadcast.service'; +import { BusyStateScopeManager } from './../../busy-state/busy-state-scope-manager'; import { ScenarioService } from './../../shared/services/scenario/scenario.service'; -import { SiteTabComponent } from './../site-dashboard/site-tab/site-tab.component'; -import { BusyStateComponent } from './../../busy-state/busy-state.component'; import { UserService } from './../../shared/services/user.service'; -import { Component, OnDestroy } from '@angular/core'; +import { Component, OnDestroy, Input } from '@angular/core'; import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; import { Observable } from 'rxjs/Observable'; import { Subject } from 'rxjs/Subject'; @@ -15,7 +15,6 @@ import 'rxjs/add/operator/switchMap'; import 'rxjs/add/observable/of'; import 'rxjs/add/observable/zip'; import { TranslateService } from '@ngx-translate/core'; - import { ConfigService } from './../../shared/services/config.service'; import { FunctionApp } from './../../shared/function-app'; import { PortalResources } from './../../shared/models/portal-resources'; @@ -46,8 +45,7 @@ interface DataModel { @Component({ selector: 'site-summary', templateUrl: './site-summary.component.html', - styleUrls: ['./site-summary.component.scss'], - inputs: ['viewInfoInput'] + styleUrls: ['./site-summary.component.scss'] }) export class SiteSummaryComponent implements OnDestroy { @@ -78,7 +76,8 @@ export class SiteSummaryComponent implements OnDestroy { private _subs: Subscription[]; private _blobUrl: string; private _isSlot: boolean; - private _busyState: BusyStateComponent; + + private _busyManager: BusyStateScopeManager; constructor( private _cacheService: CacheService, @@ -92,11 +91,12 @@ export class SiteSummaryComponent implements OnDestroy { private _configService: ConfigService, private _slotService: SiteService, userService: UserService, - siteTabComponent: SiteTabComponent, - scenarioService: ScenarioService) { + scenarioService: ScenarioService, + broadcastService: BroadcastService) { this.isStandalone = _configService.isStandalone(); - this._busyState = siteTabComponent.busyState; + + this._busyManager = new BusyStateScopeManager(broadcastService, 'site-tabs'); userService.getStartupInfo() .first() @@ -112,7 +112,7 @@ export class SiteSummaryComponent implements OnDestroy { timerId: 'TreeViewLoad', timerAction: 'stop' }); - this._busyState.setBusyState(); + // this._busyState.setBusyState(); return this._cacheService.getArm(viewInfo.resourceId); }) @@ -137,7 +137,7 @@ export class SiteSummaryComponent implements OnDestroy { this.location = site.location; this.state = site.properties.state; - this.stateIcon = this.state === 'Running' ? 'images/success.svg' : 'images/stopped.svg'; + this.stateIcon = this.state === 'Running' ? 'image/success.svg' : 'image/stopped.svg'; this.availabilityState = null; @@ -150,7 +150,7 @@ export class SiteSummaryComponent implements OnDestroy { this.plan = `${serverFarm} (${site.properties.sku.replace('Dynamic', 'Consumption')})`; this._isSlot = SiteService.isSlot(site.id); - this._busyState.clearBusyState(); + this._busyManager.clearBusy(); this._aiService.stopTrace('/timings/site/tab/overview/revealed', this._viewInfo.data.siteTabRevealedTraceKey); this.hideAvailability = scenarioService.checkScenario(ScenarioIds.showSiteAvailability, {site: site}).status === 'disabled'; @@ -214,7 +214,7 @@ export class SiteSummaryComponent implements OnDestroy { return Observable.of(res); }) .do(null, e => { - this._busyState.clearBusyState(); + this._busyManager.clearBusy(); if (!this._globalStateService.showTryView) { this._aiService.trackException(e, 'site-summary'); @@ -239,7 +239,7 @@ export class SiteSummaryComponent implements OnDestroy { return this._globalStateService.showTryView; } - set viewInfoInput(viewInfo: TreeViewInfo) { + @Input() set viewInfoInput(viewInfo: TreeViewInfo) { if (!viewInfo) { return; } @@ -327,7 +327,7 @@ export class SiteSummaryComponent implements OnDestroy { if (confirmResult) { let notificationId = null; - this._busyState.setBusyState(); + this._busyManager.setBusy(); this._portalService.startNotification( this.ts.instant(PortalResources.siteSummary_resetProfileNotifyTitle), this.ts.instant(PortalResources.siteSummary_resetProfileNotifyTitle)) @@ -337,14 +337,14 @@ export class SiteSummaryComponent implements OnDestroy { return this._armService.post(`${this.site.id}/newpassword`, null); }) .subscribe(() => { - this._busyState.clearBusyState(); + this._busyManager.clearBusy(); this._portalService.stopNotification( notificationId, true, this.ts.instant(PortalResources.siteSummary_resetProfileNotifySuccess)); }, e => { - this._busyState.clearBusyState(); + this._busyManager.clearBusy(); this._portalService.stopNotification( notificationId, false, @@ -365,7 +365,7 @@ export class SiteSummaryComponent implements OnDestroy { const confirmResult = confirm(this.ts.instant(PortalResources.siteSummary_restartConfirmation).format(this.site.name)); if (confirmResult) { - this._busyState.setBusyState(); + this._busyManager.setBusy(); this._portalService.startNotification( this.ts.instant(PortalResources.siteSummary_restartNotifyTitle).format(site.name), @@ -376,14 +376,14 @@ export class SiteSummaryComponent implements OnDestroy { return this._armService.post(`${site.id}/restart`, null); }) .subscribe(() => { - this._busyState.clearBusyState(); + this._busyManager.clearBusy(); this._portalService.stopNotification( notificationId, true, this.ts.instant(PortalResources.siteSummary_restartNotifySuccess).format(site.name)); }, e => { - this._busyState.clearBusyState(); + this._busyManager.clearBusy(); this._portalService.stopNotification( notificationId, false, @@ -440,15 +440,15 @@ export class SiteSummaryComponent implements OnDestroy { this.availabilityMesg = this.ts.instant(PortalResources.notApplicable); break; case AvailabilityStates.unavailable: - this.availabilityIcon = 'images/error.svg'; + this.availabilityIcon = 'image/error.svg'; this.availabilityMesg = this.ts.instant(PortalResources.notAvailable); break; case AvailabilityStates.available: - this.availabilityIcon = 'images/success.svg'; + this.availabilityIcon = 'image/success.svg'; this.availabilityMesg = this.ts.instant(PortalResources.available); break; case AvailabilityStates.userinitiated: - this.availabilityIcon = 'images/info.svg'; + this.availabilityIcon = 'image/info.svg'; this.availabilityMesg = this.ts.instant(PortalResources.notAvailable); break; @@ -466,7 +466,7 @@ export class SiteSummaryComponent implements OnDestroy { ? this.ts.instant(PortalResources.siteSummary_stopNotifyTitle).format(site.name) : this.ts.instant(PortalResources.siteSummary_startNotifyTitle).format(site.name); - this._busyState.setBusyState(); + this._busyManager.setBusy(); this._portalService.startNotification(notifyTitle, notifyTitle) .first() @@ -501,7 +501,7 @@ export class SiteSummaryComponent implements OnDestroy { ? this.ts.instant(PortalResources.siteSummary_stopNotifyFail).format(site.name) : this.ts.instant(PortalResources.siteSummary_startNotifyFail).format(site.name); - this._busyState.clearBusyState(); + this._busyManager.clearBusy(); this._portalService.stopNotification( notificationId, false, diff --git a/AzureFunctions.AngularClient/src/app/site/site.module.ts b/AzureFunctions.AngularClient/src/app/site/site.module.ts index ddcbd70ad0..cc5da6f031 100644 --- a/AzureFunctions.AngularClient/src/app/site/site.module.ts +++ b/AzureFunctions.AngularClient/src/app/site/site.module.ts @@ -1,11 +1,7 @@ +import { SiteTabComponent } from 'app/site/site-dashboard/site-tab/site-tab.component'; import { SharedFunctionsModule } from './../shared/shared-functions.module'; -import { ConnectionStringsComponent } from './site-config/connection-strings/connection-strings.component'; -import { AppSettingsComponent } from './site-config/app-settings/app-settings.component'; -import { GeneralSettingsComponent } from './site-config/general-settings/general-settings.component'; import { FeatureGroupComponent } from './../feature-group/feature-group.component'; import { DownloadFunctionAppContentComponent } from './../download-function-app-content/download-function-app-content.component'; -import { SiteTabComponent } from './site-dashboard/site-tab/site-tab.component'; -import { SiteConfigStandaloneComponent } from './site-config-standalone/site-config-standalone.component'; import { SiteConfigComponent } from './site-config/site-config.component'; import { SwaggerDefinitionComponent } from './swagger-definition/swagger-definition.component'; import { FunctionRuntimeComponent } from './function-runtime/function-runtime.component'; @@ -19,6 +15,7 @@ import { RouterModule } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; import { NgModule, ModuleWithProviders } from '@angular/core'; import { HostEditorComponent } from './../host-editor/host-editor.component'; +import { SiteConfigModule } from 'app/site/site-config/site-config.module'; const routing: ModuleWithProviders = RouterModule.forChild([ { path: '', component: SiteDashboardComponent } @@ -36,6 +33,7 @@ const routing: ModuleWithProviders = RouterModule.forChild([ TranslateModule.forChild(), SharedModule, SharedFunctionsModule, + SiteConfigModule, routing ], declarations: [ @@ -46,15 +44,10 @@ const routing: ModuleWithProviders = RouterModule.forChild([ FunctionRuntimeComponent, SwaggerDefinitionComponent, SwaggerFrameDirective, - SiteConfigComponent, - SiteConfigStandaloneComponent, - SiteTabComponent, DownloadFunctionAppContentComponent, SiteEnabledFeaturesComponent, - GeneralSettingsComponent, - AppSettingsComponent, - ConnectionStringsComponent, - HostEditorComponent + HostEditorComponent, + SiteTabComponent ], providers: [] }) diff --git a/AzureFunctions.AngularClient/src/app/site/swagger-definition/swagger-definition.component.html b/AzureFunctions.AngularClient/src/app/site/swagger-definition/swagger-definition.component.html index c045ba51a5..a2461e2506 100644 --- a/AzureFunctions.AngularClient/src/app/site/swagger-definition/swagger-definition.component.html +++ b/AzureFunctions.AngularClient/src/app/site/swagger-definition/swagger-definition.component.html @@ -73,7 +73,7 @@
-
@@ -104,7 +104,7 @@
- + diff --git a/AzureFunctions.AngularClient/src/app/site/swagger-definition/swagger-definition.component.scss b/AzureFunctions.AngularClient/src/app/site/swagger-definition/swagger-definition.component.scss index 4cbc073953..e0f2936b12 100644 --- a/AzureFunctions.AngularClient/src/app/site/swagger-definition/swagger-definition.component.scss +++ b/AzureFunctions.AngularClient/src/app/site/swagger-definition/swagger-definition.component.scss @@ -1,13 +1,13 @@ -@import '../../../sass/main'; +@import '../../../sass/common/variables'; pre { white-space: normal; } .medium { - width: 240px; + max-width: 240px; padding: 5px; - min-width: 200px; + min-width: 140px; } ul { @@ -74,7 +74,6 @@ ul { } .wrapper { - min-width: 1030px; display: flex; flex-direction: column; } @@ -158,7 +157,7 @@ ul { } .topbar { - min-width: 742px; + min-width: 700px; display: flex; flex-flow: row wrap; justify-content: flex-start; diff --git a/AzureFunctions.AngularClient/src/app/slot-new/slot-new.component.html b/AzureFunctions.AngularClient/src/app/slot-new/slot-new.component.html index 2884fc3ac8..799ed1ada2 100644 --- a/AzureFunctions.AngularClient/src/app/slot-new/slot-new.component.html +++ b/AzureFunctions.AngularClient/src/app/slot-new/slot-new.component.html @@ -1,7 +1,7 @@
-

{{ 'slotNew_heading' | translate }}

+

{{ 'slotNew_heading' | translate }}

-
{{ 'slotNew_desc' | translate}}
+

{{ 'slotNew_desc' | translate}}

-
\ No newline at end of file diff --git a/AzureFunctions.AngularClient/src/app/slot-new/slot-new.component.scss b/AzureFunctions.AngularClient/src/app/slot-new/slot-new.component.scss index cdffc7bf60..75d2408679 100644 --- a/AzureFunctions.AngularClient/src/app/slot-new/slot-new.component.scss +++ b/AzureFunctions.AngularClient/src/app/slot-new/slot-new.component.scss @@ -1,4 +1,4 @@ -@import '../../sass/main'; +@import '../../sass/common/variables'; .slot-form { width: 740px; @@ -6,11 +6,10 @@ .newSlot-container{ padding: 10px 20px; - background-color: $body-bg-color; } -.text-subheading{ - margin-top: 5px; +h4{ + margin: 5px 0 10px 0; } .slot-form label{ @@ -28,10 +27,6 @@ button{ margin-top: 15px; } -.text-main2-heading{ +h1{ display: inline-block; } - -.text-subheading{ - margin: 5px 0 10px 0; -} diff --git a/AzureFunctions.AngularClient/src/app/slots-list/slots-list.component.html b/AzureFunctions.AngularClient/src/app/slots-list/slots-list.component.html index 7f9095f280..a96214fc38 100644 --- a/AzureFunctions.AngularClient/src/app/slots-list/slots-list.component.html +++ b/AzureFunctions.AngularClient/src/app/slots-list/slots-list.component.html @@ -1,6 +1,6 @@ -
- - {{ 'slotsList_title' | translate }} +
+ +

{{ 'slotsList_title' | translate }}

diff --git a/AzureFunctions.AngularClient/src/app/slots-list/slots-list.component.scss b/AzureFunctions.AngularClient/src/app/slots-list/slots-list.component.scss index e69de29bb2..3f596485e9 100644 --- a/AzureFunctions.AngularClient/src/app/slots-list/slots-list.component.scss +++ b/AzureFunctions.AngularClient/src/app/slots-list/slots-list.component.scss @@ -0,0 +1,3 @@ +h2{ + display: inline; +} \ No newline at end of file diff --git a/AzureFunctions.AngularClient/src/app/tab/tab.component.ts b/AzureFunctions.AngularClient/src/app/tab/tab.component.ts deleted file mode 100644 index fa5f0e78d5..0000000000 --- a/AzureFunctions.AngularClient/src/app/tab/tab.component.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Input } from '@angular/core'; - -// @Component({ -// selector: 'tab', -// templateUrl: './tab.component.html' -// }) -export class TabComponent { - @Input() title: string; - @Input() id: string; - @Input() active = false; - @Input() closeable = false; - @Input() iconUrl: string; -} \ No newline at end of file diff --git a/AzureFunctions.AngularClient/src/app/table-function-monitor/table-function-monitor.component.scss b/AzureFunctions.AngularClient/src/app/table-function-monitor/table-function-monitor.component.scss index 3c0ffdf008..ee0790aaf5 100644 --- a/AzureFunctions.AngularClient/src/app/table-function-monitor/table-function-monitor.component.scss +++ b/AzureFunctions.AngularClient/src/app/table-function-monitor/table-function-monitor.component.scss @@ -1,4 +1,4 @@ -@import '../../sass/main'; +@import '../../sass/common/variables'; .color-alt-table > tbody > tr:nth-child(odd) { background-color: #f2f2f2; @@ -6,6 +6,7 @@ .color-alt-table > tbody > tr:nth-child(even) { background-color: #ccc; } + table{ width: 100%; } @@ -85,8 +86,9 @@ th, td { #function-monitor-output > div > div { font-weight: bold; } + textarea{ - background-color: #f5f5f5; + // background-color: #f5f5f5; height: 200px; margin-top: 5px; width: 100%; @@ -101,3 +103,12 @@ textarea{ .success{ color: green; } + +:host-context(#app-root[theme=dark]){ + .color-alt-table > tbody > tr:nth-child(odd) { + background-color: lighten($body-bg-color-dark, 10%); + } + .color-alt-table > tbody > tr:nth-child(even) { + background-color: lighten($body-bg-color-dark, 20%); + } +} \ No newline at end of file diff --git a/AzureFunctions.AngularClient/src/app/tabs/tabs.component.html b/AzureFunctions.AngularClient/src/app/tabs/tabs.component.html deleted file mode 100644 index 45f6b344ac..0000000000 --- a/AzureFunctions.AngularClient/src/app/tabs/tabs.component.html +++ /dev/null @@ -1,25 +0,0 @@ - diff --git a/AzureFunctions.AngularClient/src/app/tabs/tabs.component.spec.ts b/AzureFunctions.AngularClient/src/app/tabs/tabs.component.spec.ts deleted file mode 100644 index 76057156af..0000000000 --- a/AzureFunctions.AngularClient/src/app/tabs/tabs.component.spec.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { AppModule } from './../app.module'; -/* tslint:disable:no-unused-variable */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { DebugElement } from '@angular/core'; - -import { TabsComponent } from './tabs.component'; - -describe('TabsComponent', () => { - let component: TabsComponent; - let fixture: ComponentFixture; - - // beforeEach(async(() => { - // TestBed.configureTestingModule(AppModule.moduleDefinition) - // .compileComponents(); - // })); - - // beforeEach(() => { - // fixture = TestBed.createComponent(TabsComponent); - // component = fixture.componentInstance; - // fixture.detectChanges(); - // }); - - // it('should create', () => { - // expect(component).toBeTruthy(); - // }); -}); diff --git a/AzureFunctions.AngularClient/src/app/tabs/tabs.component.ts b/AzureFunctions.AngularClient/src/app/tabs/tabs.component.ts deleted file mode 100644 index e93dd95d0d..0000000000 --- a/AzureFunctions.AngularClient/src/app/tabs/tabs.component.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { BusyStateComponent } from './../busy-state/busy-state.component'; -import { AiService } from './../shared/services/ai.service'; -import { ContentChildren, QueryList, AfterContentInit, Output, EventEmitter, ViewChild } from '@angular/core'; -import { TabComponent } from '../tab/tab.component'; - -// @Component({ -// selector: 'tabs', -// templateUrl: './tabs.component.html' -// }) -export class TabsComponent implements AfterContentInit { - - @ViewChild(BusyStateComponent) busyState: BusyStateComponent; - @ContentChildren(TabComponent) tabs: QueryList; - @Output() tabSelected = new EventEmitter(); - @Output() tabClosed = new EventEmitter(); - - constructor(private _aiService: AiService) { - } - - ngAfterContentInit() { - const activeTabs = this.tabs.filter((tab) => tab.active); - - if (activeTabs.length === 0) { - this.selectTabHelper(this.tabs.first); - } - } - - selectTabId(tabId: string) { - const tabs = this.tabs.toArray(); - const tab = tabs.find(t => t.id === tabId); - if (tab) { - this.selectTab(tab); - } - } - - selectTab(tab: TabComponent) { - this._aiService.trackEvent('/sites/open-tab', { name: tab.id }); - this.selectTabHelper(tab); - } - - closeTab(tab: TabComponent) { - this.tabClosed.emit(tab); - } - - selectTabHelper(tab: TabComponent) { - - this.tabs.toArray().forEach(t => t.active = false); - if (tab) { - tab.active = true; - this.tabSelected.emit(tab); - } - } -} \ No newline at end of file diff --git a/AzureFunctions.AngularClient/src/app/template-picker/template-picker.component.scss b/AzureFunctions.AngularClient/src/app/template-picker/template-picker.component.scss index 5b61d6023a..f6e7b8da5c 100644 --- a/AzureFunctions.AngularClient/src/app/template-picker/template-picker.component.scss +++ b/AzureFunctions.AngularClient/src/app/template-picker/template-picker.component.scss @@ -1,4 +1,4 @@ -@import '../../sass/main'; +@import '../../sass/common/variables'; drop-down{ display: inline-block; @@ -132,4 +132,10 @@ drop-down{ .custom-button, .custom-button-invert{ margin-left: 0px; -} \ No newline at end of file +} + +:host-context(#app-root[theme=dark]){ + .tp-template-func-title{ + background-color: lighten($body-bg-color-dark, 10%); + } +} diff --git a/AzureFunctions.AngularClient/src/app/top-bar/top-bar.component.scss b/AzureFunctions.AngularClient/src/app/top-bar/top-bar.component.scss index f73835e3d4..313072b707 100644 --- a/AzureFunctions.AngularClient/src/app/top-bar/top-bar.component.scss +++ b/AzureFunctions.AngularClient/src/app/top-bar/top-bar.component.scss @@ -1,4 +1,4 @@ -@import '../../sass/main'; +@import '../../sass/common/variables'; /* top-bar styles */ .logo { diff --git a/AzureFunctions.AngularClient/src/app/top-warning/top-warning.component.html b/AzureFunctions.AngularClient/src/app/top-warning/top-warning.component.html index f0ac3ccb68..ffe88b3ee4 100644 --- a/AzureFunctions.AngularClient/src/app/top-warning/top-warning.component.html +++ b/AzureFunctions.AngularClient/src/app/top-warning/top-warning.component.html @@ -1,6 +1,6 @@ -
+
-