-
+
{{ 'download' | translate }}
diff --git a/AzureFunctions.AngularClient/src/app/download-function-app-content/download-function-app-content.component.ts b/AzureFunctions.AngularClient/src/app/download-function-app-content/download-function-app-content.component.ts
index d7400ab1ed..17ea5f1e9c 100644
--- a/AzureFunctions.AngularClient/src/app/download-function-app-content/download-function-app-content.component.ts
+++ b/AzureFunctions.AngularClient/src/app/download-function-app-content/download-function-app-content.component.ts
@@ -3,6 +3,7 @@ import { TranslateService } from '@ngx-translate/core';
import { SelectOption } from './../shared/models/select-option';
import { Subject } from 'rxjs/Subject';
import { Component, Input, Output } from '@angular/core';
+import { KeyCodes } from 'app/shared/models/constants';
type DownloadOption = 'siteContent' | 'vsProject';
@@ -41,4 +42,10 @@ export class DownloadFunctionAppContentComponent {
closeModal() {
this.close.next();
}
+
+ onKeyDown(event: KeyboardEvent) {
+ if (event.keyCode === KeyCodes.escape) {
+ this.closeModal();
+ }
+ }
}
diff --git a/AzureFunctions.AngularClient/src/app/drop-down/drop-down.component.html b/AzureFunctions.AngularClient/src/app/drop-down/drop-down.component.html
index ed4b50d531..704b3a396f 100644
--- a/AzureFunctions.AngularClient/src/app/drop-down/drop-down.component.html
+++ b/AzureFunctions.AngularClient/src/app/drop-down/drop-down.component.html
@@ -14,13 +14,25 @@
class="drop-down-select"
[disabled]="disabled">
-
+
-
- {{option.displayLabel}}
+
+
+ {{option.displayLabel}}
+
- {{option.displayLabel}}
+
+ {{option.displayLabel}}
+
@@ -37,13 +49,25 @@
[formControl]="control"
#selectInput>
-
+
-
- {{option.displayLabel}}
+
+
+ {{option.displayLabel}}
+
- {{option.displayLabel}}
+
+ {{option.displayLabel}}
+
diff --git a/AzureFunctions.AngularClient/src/app/drop-down/drop-down.component.ts b/AzureFunctions.AngularClient/src/app/drop-down/drop-down.component.ts
index 8d17caa5f4..67e1146d95 100644
--- a/AzureFunctions.AngularClient/src/app/drop-down/drop-down.component.ts
+++ b/AzureFunctions.AngularClient/src/app/drop-down/drop-down.component.ts
@@ -125,8 +125,11 @@ export class DropDownComponent implements OnInit, OnChanges {
} else {
this.onSelectValue(selectedOptionValue);
}
+ } else {
+ if (this.selectedElement) {
+ delete this.selectedElement;
+ }
}
-
}
@Input() set resetOnChange(_) {
diff --git a/AzureFunctions.AngularClient/src/app/file-explorer/file-explorer.component.ts b/AzureFunctions.AngularClient/src/app/file-explorer/file-explorer.component.ts
index b5ccbbe576..af55553881 100644
--- a/AzureFunctions.AngularClient/src/app/file-explorer/file-explorer.component.ts
+++ b/AzureFunctions.AngularClient/src/app/file-explorer/file-explorer.component.ts
@@ -15,7 +15,7 @@ import { FunctionAppContextComponent } from 'app/shared/components/function-app-
import { FunctionAppService } from 'app/shared/services/function-app.service';
import { Subject } from 'rxjs/Subject';
import { Subscription } from 'rxjs/Subscription';
-import { FunctionAppHttpResult } from 'app/shared/models/function-app-http-result';
+import { HttpResult } from 'app/shared/models/http-result';
@Component({
@@ -182,7 +182,7 @@ export class FileExplorerComponent extends FunctionAppContextComponent {
setTimeout(() => element.focus(), 50);
}
- addFile(content?: string): Observable> {
+ addFile(content?: string): Observable> {
if (this.newFileName && this.files.find(f => f.name.toLocaleLowerCase() === this.newFileName.toLocaleLowerCase())) {
const error = {
message: this._translateService.instant(PortalResources.fileExplorer_fileAlreadyExists, { fileName: this.newFileName })
diff --git a/AzureFunctions.AngularClient/src/app/function-monitor/function-monitor.component.html b/AzureFunctions.AngularClient/src/app/function-monitor/function-monitor.component.html
index 1fdfda4112..6e80ff4925 100644
--- a/AzureFunctions.AngularClient/src/app/function-monitor/function-monitor.component.html
+++ b/AzureFunctions.AngularClient/src/app/function-monitor/function-monitor.component.html
@@ -4,7 +4,7 @@
{{ 'monitoring_appInsightsIsNotFound' | translate}}
+
diff --git a/AzureFunctions.AngularClient/src/app/function-monitor/function-monitor.component.ts b/AzureFunctions.AngularClient/src/app/function-monitor/function-monitor.component.ts
index fe3e66caea..1b86fc4997 100644
--- a/AzureFunctions.AngularClient/src/app/function-monitor/function-monitor.component.ts
+++ b/AzureFunctions.AngularClient/src/app/function-monitor/function-monitor.component.ts
@@ -1,4 +1,5 @@
-import { LogCategories } from './../shared/models/constants';
+import { ScenarioService } from './../shared/services/scenario/scenario.service';
+import { LogCategories, ScenarioIds } from './../shared/models/constants';
import { Constants } from 'app/shared/models/constants';
import { LogService } from './../shared/services/log.service';
import { Component } from '@angular/core';
@@ -39,6 +40,7 @@ export class FunctionMonitorComponent extends BaseFunctionComponent {
public aiId: string = null;
public azureWebJobsDashboardMissed = true;
public aiNotFound = false;
+ public aiEnabled = false;
constructor(
public globalStateService: GlobalStateService,
@@ -48,7 +50,8 @@ export class FunctionMonitorComponent extends BaseFunctionComponent {
private _cacheService: CacheService,
broadcastService: BroadcastService,
private _functionAppService: FunctionAppService,
- private _logService: LogService
+ private _logService: LogService,
+ private _scenarioService: ScenarioService
) {
super('function-monitor', broadcastService, _functionAppService, () => globalStateService.setBusyState(), DashboardType.FunctionMonitorDashboard);
this.columns = [
@@ -80,9 +83,12 @@ export class FunctionMonitorComponent extends BaseFunctionComponent {
.switchMap(fi => {
this.currentFunction = fi.functionInfo.result;
this.globalStateService.setBusyState();
+
+ this.aiEnabled = this._scenarioService.checkScenario(ScenarioIds.enableAppInsights).status !== 'disabled';
+
return Observable.zip(
this._cacheService.postArm(`${this.context.site.id}/config/appsettings/list`, true),
- this._functionAppService.isAppInsightsEnabled(this.context.site.id));
+ this.aiEnabled ? this._functionAppService.isAppInsightsEnabled(this.context.site.id) : Observable.of(null));
})
.switchMap(r => {
const appSettings = r[0].json();
diff --git a/AzureFunctions.AngularClient/src/app/function/embedded/embedded-function-editor/embedded-function-editor.component.html b/AzureFunctions.AngularClient/src/app/function/embedded/embedded-function-editor/embedded-function-editor.component.html
index b003b31876..5cc3c5f30f 100644
--- a/AzureFunctions.AngularClient/src/app/function/embedded/embedded-function-editor/embedded-function-editor.component.html
+++ b/AzureFunctions.AngularClient/src/app/function/embedded/embedded-function-editor/embedded-function-editor.component.html
@@ -4,7 +4,7 @@
- {{fileName}}
+ {{displayName}}
> {
+ private _getScriptContent(resourceId: string): Observable> {
this._busyManager.setBusy();
this.resourceId = resourceId;
@@ -89,10 +92,13 @@ export class EmbeddedFunctionEditorComponent implements OnInit, AfterContentInit
const scriptHrefParts = this._functionInfo.script_href.split('/');
this.fileName = scriptHrefParts[scriptHrefParts.length - 1];
+ const event = this._functionInfo.config.bindings[0].message.toLowerCase();
+ const entity = scriptHrefParts[8].toLowerCase();
+ this.displayName = `${entity}/${event}/${this.fileName}`;
return this._cacheService.getArm(this._functionInfo.script_href, true);
})
.map(r => {
- return >{
+ return >{
isSuccessful: true,
error: null,
result: r.text()
@@ -100,7 +106,7 @@ export class EmbeddedFunctionEditorComponent implements OnInit, AfterContentInit
})
.catch(e => {
const descriptor = new CdsFunctionDescriptor(this.resourceId);
- return Observable.of(>{
+ return Observable.of(>{
isSuccessful: false,
error: {
errorId: errorIds.embeddedEditorLoadError,
@@ -163,21 +169,23 @@ export class EmbeddedFunctionEditorComponent implements OnInit, AfterContentInit
const result = confirm(this._translateService.instant(PortalResources.functionManage_areYouSure, { name: this._functionInfo.name }));
if (result) {
this._busyManager.setBusy();
- this._cacheService.deleteArm(this.resourceId)
- .subscribe(r => {
- this._busyManager.clearBusy();
- this._broadcastService.broadcastEvent(BroadcastEvent.TreeUpdate, {
- resourceId: this.resourceId,
- operation: 'remove'
- });
- }, err => {
- this._busyManager.clearBusy();
- this._broadcastService.broadcast(BroadcastEvent.Error, {
- message: this._translateService.instant(PortalResources.error_unableToDeleteFunction).format(this._functionInfo.name),
- errorId: errorIds.embeddedEditorDeleteError,
- resourceId: this.resourceId,
- });
- });
+ this._embeddedService.deleteFunction(this.resourceId)
+ .subscribe(r => {
+ if (r.isSuccessful) {
+ this._busyManager.clearBusy();
+ this._broadcastService.broadcastEvent(BroadcastEvent.TreeUpdate, {
+ resourceId: this.resourceId,
+ operation: 'remove'
+ });
+ } else {
+ this._busyManager.clearBusy();
+ this._broadcastService.broadcast(BroadcastEvent.Error, {
+ message: r.error.message,
+ errorId: r.error.errorId,
+ resourceId: this.resourceId,
+ });
+ }
+ });
}
}
}
diff --git a/AzureFunctions.AngularClient/src/app/function/embedded/embedded-function-test-tab/embedded-function-test-tab.component.ts b/AzureFunctions.AngularClient/src/app/function/embedded/embedded-function-test-tab/embedded-function-test-tab.component.ts
index 6f686e03f8..878320109b 100644
--- a/AzureFunctions.AngularClient/src/app/function/embedded/embedded-function-test-tab/embedded-function-test-tab.component.ts
+++ b/AzureFunctions.AngularClient/src/app/function/embedded/embedded-function-test-tab/embedded-function-test-tab.component.ts
@@ -4,7 +4,7 @@ import { TranslateService } from '@ngx-translate/core';
import { ErrorEvent } from 'app/shared/models/error-event';
import { errorIds } from 'app/shared/models/error-ids';
import { Observable } from 'rxjs/Observable';
-import { FunctionAppHttpResult } from './../../../shared/models/function-app-http-result';
+import { HttpResult } from './../../../shared/models/http-result';
import { BottomTabEvent } from './../../../controls/bottom-tabs/bottom-tab-event';
import { FunctionEditorEvent } from './../function-editor-event';
import { RightTabEvent } from './../../../controls/right-tabs/right-tab-event';
@@ -93,7 +93,7 @@ export class EmbeddedFunctionTestTabComponent implements OnInit, OnChanges, OnDe
private _getFunction(resourceId: string) {
return this._cacheService.getArm(resourceId, true)
.map(r => {
- return >{
+ return >{
isSuccessful: true,
error: null,
result: r.json()
@@ -101,7 +101,7 @@ export class EmbeddedFunctionTestTabComponent implements OnInit, OnChanges, OnDe
})
.catch(e => {
const descriptor = new CdsFunctionDescriptor(resourceId);
- return Observable.of(>{
+ return Observable.of(>{
isSuccessful: false,
error: {
errorId: errorIds.embeddedEditorLoadError,
diff --git a/AzureFunctions.AngularClient/src/app/function/embedded/models/entity.ts b/AzureFunctions.AngularClient/src/app/function/embedded/models/entity.ts
new file mode 100644
index 0000000000..1ae602b4e1
--- /dev/null
+++ b/AzureFunctions.AngularClient/src/app/function/embedded/models/entity.ts
@@ -0,0 +1,4 @@
+export interface Entity {
+ name: string;
+ displayName: string;
+}
diff --git a/AzureFunctions.AngularClient/src/app/function/embedded/temp-templates.ts b/AzureFunctions.AngularClient/src/app/function/embedded/temp-templates.ts
index a2e8cc5104..6aec4ccda6 100644
--- a/AzureFunctions.AngularClient/src/app/function/embedded/temp-templates.ts
+++ b/AzureFunctions.AngularClient/src/app/function/embedded/temp-templates.ts
@@ -1,13 +1,13 @@
// This is a temporary file until we move this to the template gallery
-export class Templates{
- public static readonly templatesJson = JSON.stringify([
+export class Templates {
+ public readonly templatesJson = JSON.stringify([
{
'id': 'SyncTrigger-CSharp',
'runtime': '1',
'files': {
'readme.md': '# HttpTrigger -on',
- 'run.csx': '#r \'..\\bin\\Microsoft.Xrm.Sdk.dll\'\nusing Microsoft.Xrm.Sdk;\n\npublic static Entity Run(Entity entity, TraceWriter log)\n{\n\tentity.Attributes[\'name\'] = entity.Attributes[\'name\'].ToString().ToUpper();\n\treturn entity;\n}',
+ 'run.csx': '#r \"..\\bin\\Microsoft.Xrm.Sdk.dll\"\nusing Microsoft.Xrm.Sdk;\n\npublic static Entity Run(Entity entity, TraceWriter log)\n{\n\tentity.Attributes[\"name\"] = entity.Attributes[\"name\"].ToString().ToUpper();\n\treturn entity;\n}',
'sample.dat': '{}'
},
'function': {
@@ -41,7 +41,7 @@ export class Templates{
'id': 'SyncTrigger-JavaScript',
'runtime': '1',
'files': {
- 'index.js': 'module.exports',
+ 'index.js': 'module.exports = function (context req) {\n\tcontext.log(\'Created entity!\');\n\tvar entity = context.bindings.entity;\n\tentity.Attributes.name=entity.Attributes.name.toUpperCase();\n\tcontext.done(null, entity);\n};',
'sample.dat': '{}'
},
'function': {
@@ -73,7 +73,7 @@ export class Templates{
}
]);
- public static readonly bindingsJson = JSON.stringify({
+ public readonly bindingsJson = JSON.stringify({
'bindings': [
{
'type': 'syncTrigger',
@@ -89,20 +89,16 @@ export class Templates{
'display': 'Create'
},
{
- 'value': 'Destroy',
- 'display': 'Destroy'
+ 'value': 'Delete',
+ 'display': 'Delete'
},
{
'value': 'Update',
'display': 'Update'
- },
- {
- 'value': 'Retrieve',
- 'display': 'Retrieve'
}
],
'label': 'Event',
- 'help': 'Event help'
+ 'help': 'CDS event on which the function will trigger'
}
]
diff --git a/AzureFunctions.AngularClient/src/app/function/function-new-detail/function-new-detail.component.ts b/AzureFunctions.AngularClient/src/app/function/function-new-detail/function-new-detail.component.ts
index c9977a65b5..3306ec945f 100644
--- a/AzureFunctions.AngularClient/src/app/function/function-new-detail/function-new-detail.component.ts
+++ b/AzureFunctions.AngularClient/src/app/function/function-new-detail/function-new-detail.component.ts
@@ -1,11 +1,9 @@
-import { ArmEmbeddedService } from './../../shared/services/arm-embedded.service';
import { KeyCodes, LogCategories } from './../../shared/models/constants';
import { TreeViewInfo } from 'app/tree-view/models/tree-view-info';
import { FunctionAppService } from 'app/shared/services/function-app.service';
import { FunctionAppContext } from './../../shared/function-app-context';
import { LogService } from 'app/shared/services/log.service';
import { Subject } from 'rxjs/Subject';
-import { CacheService } from './../../shared/services/cache.service';
import { AppNode } from './../../tree-view/app-node';
import { FunctionsNode } from './../../tree-view/functions-node';
import { AiService } from './../../shared/services/ai.service';
@@ -24,6 +22,10 @@ import { UIFunctionBinding } from '../../shared/models/binding';
import { PortalService } from '../../shared/services/portal.service';
import { Observable } from 'rxjs/Observable';
import { CreateCard } from 'app/function/function-new/function-new.component';
+import { EmbeddedService } from 'app/shared/services/embedded.service';
+import { BroadcastService } from 'app/shared/services/broadcast.service';
+import { ErrorEvent } from 'app/shared/models/error-event';
+import { BroadcastEvent } from 'app/shared/models/broadcast-event';
@Component({
selector: 'function-new-detail',
@@ -32,8 +34,6 @@ import { CreateCard } from 'app/function/function-new/function-new.component';
})
export class FunctionNewDetailComponent implements OnChanges {
- // TODO: ellhamai - figure out where to put this
- private _cdsEntitiesUrl = 'https://tip1.api.cds.microsoft.com/providers/Microsoft.CommonDataModel/environments/0fb7e803-94aa-4e69-9694-d3b3cea74523/namespaces/5d5374aa-0df3-421c-9656-5244ac88593c/entities?api-version=2016-11-01-alpha&$expand=namespace&headeronly=true';
private _bindingComponents: BindingComponent[] = [];
@Input() functionCard: CreateCard;
@@ -76,9 +76,10 @@ export class FunctionNewDetailComponent implements OnChanges {
private _translateService: TranslateService,
private _portalService: PortalService,
private _aiService: AiService,
- private _cacheService: CacheService,
private _functionAppService: FunctionAppService,
- private _logService: LogService) {
+ private _logService: LogService,
+ private _embeddedService: EmbeddedService,
+ private _broadcastService: BroadcastService) {
this.isEmbedded = this._portalService.isEmbeddedFunctions;
}
@@ -122,17 +123,19 @@ export class FunctionNewDetailComponent implements OnChanges {
}
getEntityOptions() {
- this._getEntities()
+ this._embeddedService.getEntities()
.subscribe(r => {
- const entities = r.value.map(e => e.name);
- this.entityOptions = [];
- entities.forEach(entity => {
- const dropDownElement: any = {
- displayLabel: entity,
- value: entity
- };
- this.entityOptions.push(dropDownElement);
- });
+ if (r.isSuccessful) {
+ const entities = (r.result.value.map(e => e.name)).sort();
+ this.entityOptions = [];
+ entities.forEach(entity => {
+ const dropDownElement: any = {
+ displayLabel: entity,
+ value: entity
+ };
+ this.entityOptions.push(dropDownElement);
+ });
+ }
});
}
@@ -311,12 +314,6 @@ export class FunctionNewDetailComponent implements OnChanges {
}
}
- private _getEntities() {
- const url = this._cdsEntitiesUrl;
- return this._cacheService.get(url)
- .map(r => r.json());
- }
-
private _createFunction() {
this._portalService.logAction('new-function', 'creating', { template: this.currentTemplate.id, name: this.functionName });
@@ -329,22 +326,30 @@ export class FunctionNewDetailComponent implements OnChanges {
});
this._globalStateService.setBusyState();
- const createContext = this._portalService.isEmbeddedFunctions ? this.entityContext : this.context;
- this._functionAppService.createFunctionV2(createContext, this.functionName, this.currentTemplate.files, this.bc.UIToFunctionConfig(this.model.config))
+ if (this._portalService.isEmbeddedFunctions) {
+ this._embeddedService.createFunction(this.entityContext, this.functionName, this.currentTemplate.files, this.bc.UIToFunctionConfig(this.model.config))
+ .subscribe(r => {
+ if (r.isSuccessful) {
+ r.result.context = this.entityContext;
+ this.functionsNode = this.appNode.children.find(node => node.title === this.functionsNode.title);
+ this.functionsNode.addChild(r.result);
+ } else {
+ this._broadcastService.broadcast(BroadcastEvent.Error, {
+ message: r.error.message,
+ errorId: r.error.errorId,
+ resourceId: r.result.context.site.id,
+ });
+ }
+ this._globalStateService.clearBusyState();
+ });
+ } else {
+ this._functionAppService.createFunctionV2(this.context, this.functionName, this.currentTemplate.files, this.bc.UIToFunctionConfig(this.model.config))
.subscribe(newFunctionInfo => {
if (newFunctionInfo.isSuccessful) {
this._portalService.logAction('new-function', 'success', { template: this.currentTemplate.id, name: this.functionName });
this._aiService.trackEvent('new-function', { template: this.currentTemplate.id, result: 'success', first: 'false' });
- if (this._portalService.isEmbeddedFunctions) {
- this._cacheService.clearCachePrefix(`${ArmEmbeddedService.url}${this.context.site.id}`);
- } else {
- this._cacheService.clearCachePrefix(this.context.urlTemplates.scmSiteUrl);
- }
-
- newFunctionInfo.result.context = createContext;
-
- // If someone refreshed the app, it would created a new set of child nodes under the app node.
+ newFunctionInfo.result.context = this.context;
this.functionsNode = this.appNode.children.find(node => node.title === this.functionsNode.title);
this.functionsNode.addChild(newFunctionInfo.result);
}
@@ -353,6 +358,7 @@ export class FunctionNewDetailComponent implements OnChanges {
() => {
this._globalStateService.clearBusyState();
});
+ }
}
onCreate() {
diff --git a/AzureFunctions.AngularClient/src/app/function/function-new/function-new.component.html b/AzureFunctions.AngularClient/src/app/function/function-new/function-new.component.html
index f2fd22dcb4..252713a768 100644
--- a/AzureFunctions.AngularClient/src/app/function/function-new/function-new.component.html
+++ b/AzureFunctions.AngularClient/src/app/function/function-new/function-new.component.html
@@ -44,6 +44,16 @@ {{ 'functionNew_chooseTemplate' | translate }}
(value)="onScenarioChanged($event)"
attr.aria-label="{{'templatePicker_scenario' | translate}}">
+
+ {{ 'experimentalLanguageSupport' | translate }}
+
+
+
diff --git a/AzureFunctions.AngularClient/src/app/function/function-new/function-new.component.scss b/AzureFunctions.AngularClient/src/app/function/function-new/function-new.component.scss
index 03bef979c0..559cb82aba 100644
--- a/AzureFunctions.AngularClient/src/app/function/function-new/function-new.component.scss
+++ b/AzureFunctions.AngularClient/src/app/function/function-new/function-new.component.scss
@@ -174,6 +174,10 @@ drop-down{
padding-right: 25px;
}
+.language-toggle {
+ padding-left: 7px;
+}
+
.sidebar-container{
height: 100%;
width: 100%;
diff --git a/AzureFunctions.AngularClient/src/app/function/function-new/function-new.component.ts b/AzureFunctions.AngularClient/src/app/function/function-new/function-new.component.ts
index 5445e024f3..af9473b106 100644
--- a/AzureFunctions.AngularClient/src/app/function/function-new/function-new.component.ts
+++ b/AzureFunctions.AngularClient/src/app/function/function-new/function-new.component.ts
@@ -36,6 +36,8 @@ export interface CreateCard extends Template {
icon: string;
barcolor: string;
focusable: boolean;
+ allLanguages?: string[];
+ supportedLanguages?: string[];
}
@Component({
@@ -63,6 +65,11 @@ export class FunctionNewComponent extends FunctionAppContextComponent implements
public createCards: CreateCard[] = [];
public createFunctionCard: CreateCard;
public createFunctionLanguage: string = null;
+ public showExperimentalLanguages = false;
+ public allLanguages: DropDownElement
[] = [];
+ public supportedLanguages: DropDownElement[] = [];
+
+ public readonly allExperimentalLanguages = ['Bash', 'Batch', 'PHP', 'PowerShell', 'Python', 'TypeScript' ];
public createCardStyles = {
'blob': { color: '#1E5890', barcolor: '#DAE6EF', icon: 'image/blob.svg' },
@@ -86,28 +93,28 @@ export class FunctionNewComponent extends FunctionAppContextComponent implements
private _orderedCategoties: CategoryOrder[] =
[{
- name: this._translateService.instant('temp_category_core'),
+ name: this._translateService.instant('temp_category_all'),
index: 0
},
+ {
+ name: this._translateService.instant('temp_category_core'),
+ index: 1
+ },
{
name: this._translateService.instant('temp_category_api'),
- index: 1,
+ index: 2
},
{
name: this._translateService.instant('temp_category_dataProcessing'),
- index: 2,
+ index: 3
},
{
name: this._translateService.instant('temp_category_samples'),
- index: 3,
+ index: 4
},
{
name: this._translateService.instant('temp_category_experimental'),
- index: 4,
- },
- {
- name: this._translateService.instant('temp_category_all'),
- index: 1000,
+ index: 5,
}];
@ViewChild('container') createCardContainer: ElementRef;
@@ -204,7 +211,7 @@ export class FunctionNewComponent extends FunctionAppContextComponent implements
});
if (index === -1) {
- const dropDownElement: any = {
+ const dropDownElement: DropDownElement = {
displayLabel: c,
value: c
};
@@ -224,6 +231,8 @@ export class FunctionNewComponent extends FunctionAppContextComponent implements
return finalTemplate.name === template.metadata.name;
});
+ const isExperimental = this._languageIsExperimental(template.metadata.language);
+
// if the card doesn't exist, create it based off the template, else add information to the preexisting card
if (templateIndex === -1) {
this.createCards.push({
@@ -232,7 +241,9 @@ export class FunctionNewComponent extends FunctionAppContextComponent implements
description: template.metadata.description,
enabledInTryMode: template.metadata.enabledInTryMode,
AADPermissions: template.metadata.AADPermissions,
- languages: [`${template.metadata.language}`],
+ languages: isExperimental ? [] : [`${template.metadata.language}`],
+ supportedLanguages: isExperimental ? [] : [`${template.metadata.language}`],
+ allLanguages: [`${template.metadata.language}`],
categories: template.metadata.category,
ids: [`${template.id}`],
icon: this.createCardStyles.hasOwnProperty(template.metadata.categoryStyle) ?
@@ -244,7 +255,11 @@ export class FunctionNewComponent extends FunctionAppContextComponent implements
focusable: false
});
} else {
- this.createCards[templateIndex].languages.push(`${template.metadata.language}`);
+ if (!isExperimental) {
+ this.createCards[templateIndex].languages.push(`${template.metadata.language}`);
+ this.createCards[templateIndex].supportedLanguages.push(`${template.metadata.language}`);
+ }
+ this.createCards[templateIndex].allLanguages.push(`${template.metadata.language}`);
this.createCards[templateIndex].categories = this.createCards[templateIndex].categories.concat(template.metadata.category);
this.createCards[templateIndex].ids.push(`${template.id}`);
}
@@ -266,10 +281,21 @@ export class FunctionNewComponent extends FunctionAppContextComponent implements
this._sortCategories();
- this.languages = this.languages.sort((a: DropDownElement, b: DropDownElement) => {
- return a.displayLabel > b.displayLabel ? 1 : -1;
+ // sort out supported languages and set those as default language options in drop-down
+ this.languages.forEach(language => {
+ const isExperimental = this._languageIsExperimental(language.value);
+ if (!isExperimental) {
+ this.supportedLanguages.push({
+ displayLabel: language.displayLabel,
+ value: language.value
+ });
+ }
});
+ this.allLanguages = this._languageSort(this.languages);
+ this.supportedLanguages = this._languageSort(this.supportedLanguages);
+ this.languages = this.supportedLanguages;
+
// order preference defined in constants.ts
this.createCards.sort((a: Template, b: Template) => {
let ia = Order.templateOrder.findIndex(item => (a.value.startsWith(item)));
@@ -293,6 +319,26 @@ export class FunctionNewComponent extends FunctionAppContextComponent implements
});
}
+ switchExperimentalLanguagesOption() {
+ this.showExperimentalLanguages = !this.showExperimentalLanguages;
+ this.createCards.forEach(card => {
+ card.languages = this.showExperimentalLanguages ? card.allLanguages : card.supportedLanguages;
+ });
+ this.languages = this.showExperimentalLanguages ? this.allLanguages : this.supportedLanguages;
+ }
+
+ private _languageIsExperimental(language: string): boolean {
+ return !!(this.allExperimentalLanguages.find((l) => {
+ return l === language;
+ }));
+ }
+
+ private _languageSort(languages: DropDownElement[]): DropDownElement[] {
+ return languages.sort((a: DropDownElement, b: DropDownElement) => {
+ return a.displayLabel > b.displayLabel ? 1 : -1;
+ });
+ }
+
onLanguageChanged(language: string) {
this.language = language;
this.categories = [];
@@ -312,7 +358,7 @@ export class FunctionNewComponent extends FunctionAppContextComponent implements
});
if (index === -1) {
- const dropDownElement: any = {
+ const dropDownElement: DropDownElement = {
displayLabel: c,
value: c
};
@@ -323,11 +369,18 @@ export class FunctionNewComponent extends FunctionAppContextComponent implements
dropDownElement.default = true;
}
- this.categories.push(dropDownElement);
+ // only display the experimental category if experimental languages are shown
+ if (c === this._translateService.instant('temp_category_experimental')) {
+ if (this.showExperimentalLanguages) {
+ this.categories.push(dropDownElement);
+ }
+ } else {
+ this.categories.push(dropDownElement);
+ }
}
}));
- const dropDownElement: any = {
+ const dropDownElement: DropDownElement = {
displayLabel: this._translateService.instant('temp_category_all'),
value: this._translateService.instant('temp_category_all')
};
diff --git a/AzureFunctions.AngularClient/src/app/functions-list/functions-list.component.html b/AzureFunctions.AngularClient/src/app/functions-list/functions-list.component.html
index d203c6be8f..c61b2e5ac8 100644
--- a/AzureFunctions.AngularClient/src/app/functions-list/functions-list.component.html
+++ b/AzureFunctions.AngularClient/src/app/functions-list/functions-list.component.html
@@ -11,7 +11,9 @@
-
{{ 'functions' | translate }}
+
{{ 'functions' | translate }}
+
{{ 'functionsPreviewTitle' | translate }}
+
{{ 'functions' | translate }}
-
+
{{ '_name' | translate }}
-
+
{{ 'entity' | translate }}
-
+
{{ 'status' | translate }}
-
+
diff --git a/AzureFunctions.AngularClient/src/app/functions-list/functions-list.component.ts b/AzureFunctions.AngularClient/src/app/functions-list/functions-list.component.ts
index feb6f7b3e8..d70e34fdaa 100644
--- a/AzureFunctions.AngularClient/src/app/functions-list/functions-list.component.ts
+++ b/AzureFunctions.AngularClient/src/app/functions-list/functions-list.component.ts
@@ -1,7 +1,6 @@
import { ArmSiteDescriptor } from './../shared/resourceDescriptors';
import { FunctionAppContext } from './../shared/function-app-context';
import { TreeUpdateEvent, BroadcastEvent } from './../shared/models/broadcast-event';
-import { CacheService } from 'app/shared/services/cache.service';
import { FunctionInfo } from 'app/shared/models/function-info';
import { CreateCard } from 'app/function/function-new/function-new.component';
import { DashboardType } from 'app/tree-view/models/dashboard-type';
@@ -19,6 +18,8 @@ import { Observable } from 'rxjs/Observable';
import { FunctionAppService } from 'app/shared/services/function-app.service';
import { Subscription } from 'rxjs/Subscription';
import { NavigableComponent } from '../shared/components/navigable-component';
+import { EmbeddedService } from 'app/shared/services/embedded.service';
+import { ErrorEvent } from 'app/shared/models/error-event';
@Component({
selector: 'functions-list',
@@ -47,10 +48,11 @@ export class FunctionsListComponent extends NavigableComponent implements OnDest
private _translateService: TranslateService,
broadcastService: BroadcastService,
private _functionAppService: FunctionAppService,
- private _cacheService: CacheService) {
+ private _embeddedService: EmbeddedService) {
+
super('functions-list', broadcastService, DashboardType.FunctionsDashboard);
- this.isEmbedded = this._portalService.isEmbeddedFunctions;
+ this.isEmbedded = this._portalService.isEmbeddedFunctions;
}
setupNavigation(): Subscription {
@@ -73,9 +75,9 @@ export class FunctionsListComponent extends NavigableComponent implements OnDest
this.runtimeVersion = tuple[1];
this.isLoading = false;
this.functions = (this._functionsNode.children);
- this.functionsInfo = this._functionsNode.children.map((child: FunctionNode) =>{
+ this.functionsInfo = this._functionsNode.children.map((child: FunctionNode) => {
return child.functionInfo;
- })
+ });
});
}
@@ -85,36 +87,36 @@ export class FunctionsListComponent extends NavigableComponent implements OnDest
templates.result.forEach((template) => {
- const templateIndex = this.createCards.findIndex(finalTemplate => {
- return finalTemplate.name === template.metadata.name;
- });
+ const templateIndex = this.createCards.findIndex(finalTemplate => {
+ return finalTemplate.name === template.metadata.name;
+ });
- // if the card doesn't exist, create it based off the template, else add information to the preexisting card
- if (templateIndex === -1) {
- this.createCards.push({
- name: `${template.metadata.name}`,
- value: template.id,
- description: template.metadata.description,
- enabledInTryMode: template.metadata.enabledInTryMode,
- AADPermissions: template.metadata.AADPermissions,
- languages: [`${template.metadata.language}`],
- categories: template.metadata.category,
- ids: [`${template.id}`],
- icon: 'image/other.svg',
- color: '#000000',
- barcolor: '#D9D9D9',
- focusable: false
- });
- } else {
- this.createCards[templateIndex].languages.push(`${template.metadata.language}`);
- this.createCards[templateIndex].categories = this.createCards[templateIndex].categories.concat(template.metadata.category);
- this.createCards[templateIndex].ids.push(`${template.id}`);
- }
+ // if the card doesn't exist, create it based off the template, else add information to the preexisting card
+ if (templateIndex === -1) {
+ this.createCards.push({
+ name: `${template.metadata.name}`,
+ value: template.id,
+ description: template.metadata.description,
+ enabledInTryMode: template.metadata.enabledInTryMode,
+ AADPermissions: template.metadata.AADPermissions,
+ languages: [`${template.metadata.language}`],
+ categories: template.metadata.category,
+ ids: [`${template.id}`],
+ icon: 'image/other.svg',
+ color: '#000000',
+ barcolor: '#D9D9D9',
+ focusable: false
+ });
+ } else {
+ this.createCards[templateIndex].languages.push(`${template.metadata.language}`);
+ this.createCards[templateIndex].categories = this.createCards[templateIndex].categories.concat(template.metadata.category);
+ this.createCards[templateIndex].ids.push(`${template.id}`);
+ }
});
// unique categories
this.createCards.forEach((template, index) => {
- const categoriesDict: {[key: string]: string; } = {};
+ const categoriesDict: { [key: string]: string; } = {};
template.categories.forEach(category => {
categoriesDict[category] = category;
});
@@ -127,7 +129,7 @@ export class FunctionsListComponent extends NavigableComponent implements OnDest
});
this.createFunctionCard = this.createCards[0];
- });
+ });
}
clickRow(item: FunctionNode) {
@@ -201,18 +203,24 @@ export class FunctionsListComponent extends NavigableComponent implements OnDest
embeddedDelete(item: FunctionNode) {
const result = confirm(this._translateService.instant(PortalResources.functionManage_areYouSure, { name: item.functionInfo.name }));
if (result) {
- this._globalStateService.setBusyState();
- this._cacheService.deleteArm(item.resourceId)
- .subscribe(r => {
- this._globalStateService.clearBusyState();
- this._broadcastService.broadcastEvent(BroadcastEvent.TreeUpdate, {
- resourceId: item.resourceId,
- operation: 'remove'
- });
- }, err => {
- this._globalStateService.clearBusyState();
- // TODO: ellhamai - handle error
- });
+ this._globalStateService.setBusyState();
+ this._embeddedService.deleteFunction(item.resourceId)
+ .subscribe(r => {
+ if (r.isSuccessful) {
+ this._globalStateService.clearBusyState();
+ this._broadcastService.broadcastEvent(BroadcastEvent.TreeUpdate, {
+ resourceId: item.resourceId,
+ operation: 'remove'
+ });
+ } else {
+ this._globalStateService.clearBusyState();
+ this._broadcastService.broadcast(BroadcastEvent.Error, {
+ message: r.error.message,
+ errorId: r.error.errorId,
+ resourceId: item.resourceId,
+ });
+ }
+ });
}
}
diff --git a/AzureFunctions.AngularClient/src/app/getting-started/getting-started.component.ts b/AzureFunctions.AngularClient/src/app/getting-started/getting-started.component.ts
index fe9de0a599..8171c20bca 100644
--- a/AzureFunctions.AngularClient/src/app/getting-started/getting-started.component.ts
+++ b/AzureFunctions.AngularClient/src/app/getting-started/getting-started.component.ts
@@ -221,13 +221,9 @@ export class GettingStartedComponent implements OnInit, OnDestroy {
const portalHostName = 'https://portal.azure.com';
let environment = '';
if (window.location.host.indexOf('staging') !== -1) {
- // Temporarily redirecting FunctionsNext to use the Canary Ibiza environment.
- environment = '?feature.fastmanifest=false&websitesextension_functionsstaged=true';
-
+ environment = '?websitesextension_ext=appsvc.env=stage';
} else if (window.location.host.indexOf('next') !== -1) {
-
- // Temporarily redirecting FunctionsNext to use the Canary Ibiza environment.
- environment = '?feature.canmodifystamps=true&BizTalkExtension=canary&WebsitesExtension=canary&feature.fastmanifest=false&websitesextension_functionsnext=true';
+ environment = '?websitesextension_ext=appsvc.env=next';
}
window.location.replace(`${portalHostName}/${currentTenant.DomainName}${environment}#resource${armId}`);
diff --git a/AzureFunctions.AngularClient/src/app/ibiza-feature/deployment-shell/deployment-shell.component.html b/AzureFunctions.AngularClient/src/app/ibiza-feature/deployment-shell/deployment-shell.component.html
new file mode 100644
index 0000000000..aa1c4711c5
--- /dev/null
+++ b/AzureFunctions.AngularClient/src/app/ibiza-feature/deployment-shell/deployment-shell.component.html
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/AzureFunctions.AngularClient/src/app/ibiza-feature/deployment-shell/deployment-shell.component.scss b/AzureFunctions.AngularClient/src/app/ibiza-feature/deployment-shell/deployment-shell.component.scss
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/AzureFunctions.AngularClient/src/app/ibiza-feature/deployment-shell/deployment-shell.component.spec.ts b/AzureFunctions.AngularClient/src/app/ibiza-feature/deployment-shell/deployment-shell.component.spec.ts
new file mode 100644
index 0000000000..52b90c8b80
--- /dev/null
+++ b/AzureFunctions.AngularClient/src/app/ibiza-feature/deployment-shell/deployment-shell.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { DeploymentShellComponent } from './deployment-shell.component';
+
+describe('DeploymentShellComponent', () => {
+ let component: DeploymentShellComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ DeploymentShellComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(DeploymentShellComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/AzureFunctions.AngularClient/src/app/ibiza-feature/deployment-shell/deployment-shell.component.ts b/AzureFunctions.AngularClient/src/app/ibiza-feature/deployment-shell/deployment-shell.component.ts
new file mode 100644
index 0000000000..f978b1441c
--- /dev/null
+++ b/AzureFunctions.AngularClient/src/app/ibiza-feature/deployment-shell/deployment-shell.component.ts
@@ -0,0 +1,33 @@
+import { Component, OnDestroy } from '@angular/core';
+import { TreeViewInfo, SiteData } from 'app/tree-view/models/tree-view-info';
+import { Subscription } from 'rxjs';
+import { ActivatedRoute } from '@angular/router';
+import { DashboardType } from 'app/tree-view/models/dashboard-type';
+
+@Component({
+ selector: 'app-deployment-shell',
+ templateUrl: './deployment-shell.component.html',
+ styleUrls: ['./deployment-shell.component.scss']
+})
+export class DeploymentShellComponent implements OnDestroy {
+ viewInfo: TreeViewInfo;
+
+ private routeParamsSubscription: Subscription;
+
+ constructor(route: ActivatedRoute) {
+ this.routeParamsSubscription = route.params.subscribe(x => {
+ this.viewInfo = {
+ resourceId:
+ `/subscriptions/${x['subscriptionId']}/resourceGroups/${x['resourceGroup']}/providers/Microsoft.Web/sites/${x['site']}` +
+ (x['slot'] ? `/slots/${x['slot']}` : ``),
+ dashboardType: DashboardType.none,
+ node: null,
+ data: null
+ };
+ });
+ }
+
+ ngOnDestroy(): void {
+ this.routeParamsSubscription.unsubscribe();
+ }
+}
diff --git a/AzureFunctions.AngularClient/src/app/ibiza-feature/deployment-shell/deployment-shell.module.ts b/AzureFunctions.AngularClient/src/app/ibiza-feature/deployment-shell/deployment-shell.module.ts
new file mode 100644
index 0000000000..c8c59f7125
--- /dev/null
+++ b/AzureFunctions.AngularClient/src/app/ibiza-feature/deployment-shell/deployment-shell.module.ts
@@ -0,0 +1,19 @@
+import { NgModule, ModuleWithProviders } from '@angular/core';
+import { DeploymentShellComponent } from './deployment-shell.component';
+import { RouterModule } from '@angular/router';
+import { SharedFunctionsModule } from 'app/shared/shared-functions.module';
+import { SharedModule } from 'app/shared/shared.module';
+import { TranslateModule } from '@ngx-translate/core';
+import { SiteConfigModule } from 'app/site/site-config/site-config.module';
+import { DeploymentCenterModule } from 'app/site/deployment-center/deployment-center.module';
+import { DeploymentCenterComponent } from 'app/site/deployment-center/deployment-center.component';
+
+const routing: ModuleWithProviders = RouterModule.forChild([{ path: '', component: DeploymentShellComponent }]);
+
+@NgModule({
+ entryComponents: [DeploymentShellComponent, DeploymentCenterComponent],
+ imports: [TranslateModule.forChild(), SharedModule, SharedFunctionsModule, SiteConfigModule, routing, DeploymentCenterModule],
+ declarations: [DeploymentShellComponent],
+ providers: []
+})
+export class DeploymentShellModule {}
diff --git a/AzureFunctions.AngularClient/src/app/ibiza-feature/ibiza-feature.module.ts b/AzureFunctions.AngularClient/src/app/ibiza-feature/ibiza-feature.module.ts
index e90aaeed20..95cb6f21b3 100644
--- a/AzureFunctions.AngularClient/src/app/ibiza-feature/ibiza-feature.module.ts
+++ b/AzureFunctions.AngularClient/src/app/ibiza-feature/ibiza-feature.module.ts
@@ -21,6 +21,14 @@ const routing: ModuleWithProviders = RouterModule.forChild([
{
path: 'subscriptions/:subscriptionId/resourcegroups/:resourceGroup/providers/microsoft.web/sites/:site/slots/:slot/settings',
loadChildren: 'app/ibiza-feature/app-settings-shell/app-settings-shell.module#AppSettingsShellModule'
+ },
+ {
+ path: 'subscriptions/:subscriptionId/resourcegroups/:resourceGroup/providers/microsoft.web/sites/:site/deployment',
+ loadChildren: 'app/ibiza-feature/deployment-shell/deployment-shell.module#DeploymentShellModule'
+ },
+ {
+ path: 'subscriptions/:subscriptionId/resourcegroups/:resourceGroup/providers/microsoft.web/sites/:site/slots/:slot/deployment',
+ loadChildren: 'app/ibiza-feature/deployment-shell/deployment-shell.module#DeploymentShellModule'
}
]
}
@@ -28,8 +36,6 @@ const routing: ModuleWithProviders = RouterModule.forChild([
@NgModule({
imports: [TranslateModule.forChild(), SharedModule, routing],
- declarations: [
- IbizaFeatureComponent
- ]
+ declarations: [IbizaFeatureComponent]
})
export class IbizaFeatureModule {}
diff --git a/AzureFunctions.AngularClient/src/app/radio-selector/radio-selector.component.ts b/AzureFunctions.AngularClient/src/app/radio-selector/radio-selector.component.ts
index d0794aa5de..037a8d9d3f 100644
--- a/AzureFunctions.AngularClient/src/app/radio-selector/radio-selector.component.ts
+++ b/AzureFunctions.AngularClient/src/app/radio-selector/radio-selector.component.ts
@@ -48,7 +48,7 @@ export class RadioSelectorComponent implements OnInit, OnChanges, AfterViewIn
}
ngAfterViewInit() {
- if (this.focusOnLoad){
+ if (this.focusOnLoad) {
this.radioGroup.nativeElement.focus();
}
}
diff --git a/AzureFunctions.AngularClient/src/app/shared/components/base-function-component.ts b/AzureFunctions.AngularClient/src/app/shared/components/base-function-component.ts
index 1402979823..77aa301c40 100644
--- a/AzureFunctions.AngularClient/src/app/shared/components/base-function-component.ts
+++ b/AzureFunctions.AngularClient/src/app/shared/components/base-function-component.ts
@@ -7,14 +7,14 @@ import { FunctionDescriptor } from 'app/shared/resourceDescriptors';
import { FunctionAppContext } from 'app/shared/function-app-context';
import { FunctionInfo } from 'app/shared/models/function-info';
import { FunctionAppService } from 'app/shared/services/function-app.service';
-import { FunctionAppHttpResult } from 'app/shared/models/function-app-http-result';
+import { HttpResult } from 'app/shared/models/http-result';
import { NavigableComponent } from './navigable-component';
type FunctionChangedEventsType = Observable & {
siteDescriptor: ArmSiteDescriptor;
functionDescriptor: FunctionDescriptor;
context: FunctionAppContext;
- functionInfo: FunctionAppHttpResult;
+ functionInfo: HttpResult;
}>;
export abstract class BaseFunctionComponent extends NavigableComponent {
diff --git a/AzureFunctions.AngularClient/src/app/shared/components/errorable-component.ts b/AzureFunctions.AngularClient/src/app/shared/components/errorable-component.ts
index 58e7b59bf3..39ecf24346 100644
--- a/AzureFunctions.AngularClient/src/app/shared/components/errorable-component.ts
+++ b/AzureFunctions.AngularClient/src/app/shared/components/errorable-component.ts
@@ -5,7 +5,7 @@ import { BroadcastEvent } from '../models/broadcast-event';
export abstract class ErrorableComponent {
- constructor(_componentName: string, protected _broadcastService: BroadcastService) { }
+ constructor(protected componentName: string, protected _broadcastService: BroadcastService) { }
showComponentError(error: ErrorEvent) {
this._broadcastService.broadcast(BroadcastEvent.Error, error);
diff --git a/AzureFunctions.AngularClient/src/app/shared/components/feature-component.ts b/AzureFunctions.AngularClient/src/app/shared/components/feature-component.ts
new file mode 100644
index 0000000000..fe45fa3920
--- /dev/null
+++ b/AzureFunctions.AngularClient/src/app/shared/components/feature-component.ts
@@ -0,0 +1,54 @@
+import { LogService } from './../services/log.service';
+import { TelemetryService } from './../services/telemetry.service';
+import { BroadcastService } from 'app/shared/services/broadcast.service';
+import { Injector } from '@angular/core/src/core';
+import { Observable } from 'rxjs/Observable';
+import { Subject } from 'rxjs/Subject';
+import { OnDestroy } from '@angular/core/src/metadata/lifecycle_hooks';
+import { ErrorableComponent } from './errorable-component';
+import { Input } from '@angular/core';
+import { LogCategories } from 'app/shared/models/constants';
+
+export abstract class FeatureComponent extends ErrorableComponent implements OnDestroy {
+ @Input() featureName: string;
+ isParentComponent = false;
+
+ private _inputEvents = new Subject();
+ private _ngUnsubscribe = new Subject();
+ private _telemetryService: TelemetryService;
+
+ constructor(componentName: string, injector: Injector) {
+ super(componentName, injector.get(BroadcastService));
+
+ this._telemetryService = injector.get(TelemetryService);
+ const logService = injector.get(LogService);
+
+ const preCheckEvents = this._inputEvents
+ .takeUntil(this._ngUnsubscribe)
+ .do(input => {
+ if (!this.featureName) {
+ throw Error('featureName is not defined');
+ }
+
+ this._telemetryService.featureLoading(this.isParentComponent, this.featureName, this.componentName);
+ });
+
+ this.setup(preCheckEvents)
+ .takeUntil(this._ngUnsubscribe)
+ .subscribe(r => {
+ this._telemetryService.featureLoadingComplete(this.featureName, this.componentName);
+ }, err => {
+ logService.error(LogCategories.featureLoading, '/load-failure', err);
+ });
+ }
+
+ protected setInput(input: T) {
+ this._inputEvents.next(input);
+ }
+
+ protected abstract setup(inputEvents: Observable): Observable;
+
+ ngOnDestroy(): void {
+ this._ngUnsubscribe.next();
+ }
+}
diff --git a/AzureFunctions.AngularClient/src/app/shared/components/function-app-context-component.ts b/AzureFunctions.AngularClient/src/app/shared/components/function-app-context-component.ts
index 7500452922..44ebd4b42e 100644
--- a/AzureFunctions.AngularClient/src/app/shared/components/function-app-context-component.ts
+++ b/AzureFunctions.AngularClient/src/app/shared/components/function-app-context-component.ts
@@ -3,7 +3,7 @@ import { OnDestroy, Input } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { TreeViewInfo } from 'app/tree-view/models/tree-view-info';
import { FunctionAppContext } from 'app/shared/function-app-context';
-import { FunctionAppHttpResult } from 'app/shared/models/function-app-http-result';
+import { HttpResult } from 'app/shared/models/http-result';
import { FunctionInfo } from 'app/shared/models/function-info';
import { ArmSiteDescriptor, ArmFunctionDescriptor, CdsEntityDescriptor } from 'app/shared/resourceDescriptors';
import { Subject } from 'rxjs/Subject';
@@ -21,7 +21,7 @@ export abstract class FunctionAppContextComponent extends ErrorableComponent imp
siteDescriptor: ArmSiteDescriptor | CdsEntityDescriptor | null;
functionDescriptor: ArmFunctionDescriptor | null;
context: FunctionAppContext | null;
- functionInfo: FunctionAppHttpResult | null;
+ functionInfo: HttpResult | null;
}>;
protected ngUnsubscribe: Observable;
@@ -70,7 +70,7 @@ export abstract class FunctionAppContextComponent extends ErrorableComponent imp
return Observable.zip(
tuple[1].functionDescriptor
? functionAppService.getFunction(tuple[0], tuple[1].functionDescriptor.name)
- : Observable.of({ isSuccessful: false, error: { errorId: '' } } as FunctionAppHttpResult),
+ : Observable.of({ isSuccessful: false, error: { errorId: '' } } as HttpResult),
Observable.of(tuple[0]),
Observable.of(tuple[1])
);
diff --git a/AzureFunctions.AngularClient/src/app/shared/conditional-http-client.ts b/AzureFunctions.AngularClient/src/app/shared/conditional-http-client.ts
index d339faf293..feea3c6102 100644
--- a/AzureFunctions.AngularClient/src/app/shared/conditional-http-client.ts
+++ b/AzureFunctions.AngularClient/src/app/shared/conditional-http-client.ts
@@ -1,15 +1,13 @@
-import { FunctionAppContext } from './function-app-context';
+import { ArmError, HttpError } from './models/http-result';
import { Preconditions as p } from './preconditions';
-import { CacheService } from 'app/shared/services/cache.service';
import { Observable } from 'rxjs/Observable';
-import { FunctionAppHttpResult } from 'app/shared/models/function-app-http-result';
-import { LogService } from './services/log.service';
-import 'rxjs/add/observable/forkJoin';
+import { HttpResult, HttpErrorResponse } from 'app/shared/models/http-result';
+import { Injector } from '@angular/core';
+import { errorIds } from 'app/shared/models/error-ids';
type AuthenticatedQuery = (t: AuthToken) => Observable;
type Query = Observable | AuthenticatedQuery;
type AuthToken = string;
-type ErrorId = string;
type Milliseconds = number;
interface ExecuteOptions {
retryCount: number;
@@ -21,32 +19,38 @@ export class ConditionalHttpClient {
private readonly preconditionsMap: p.PreconditionMap = {} as p.PreconditionMap;
private readonly conditions: p.HttpPreconditions[];
- constructor(cacheService: CacheService, logService: LogService, private getToken: (context: FunctionAppContext) => Observable, ...defaultConditions: p.HttpPreconditions[]) {
+ constructor(injector: Injector, private getToken: (resourceId: string) => Observable, ...defaultConditions: p.HttpPreconditions[]) {
this.conditions = defaultConditions;
- this.preconditionsMap['NoClientCertificate'] = new p.NoClientCertificatePrecondition(cacheService, logService);
- this.preconditionsMap['NotOverQuota'] = new p.NotOverQuotaPrecondition(cacheService, logService);
- this.preconditionsMap['NotStopped'] = new p.NotStoppedPrecondition(cacheService, logService);
- this.preconditionsMap['ReachableLoadballancer'] = new p.ReachableLoadballancerPrecondition(cacheService, logService);
- this.preconditionsMap['RuntimeAvailable'] = new p.RuntimeAvailablePrecondition(cacheService, logService, getToken);
+ this.preconditionsMap['NoClientCertificate'] = new p.NoClientCertificatePrecondition(injector);
+ this.preconditionsMap['NotOverQuota'] = new p.NotOverQuotaPrecondition(injector);
+ this.preconditionsMap['NotStopped'] = new p.NotStoppedPrecondition(injector);
+ this.preconditionsMap['ReachableLoadballancer'] = new p.ReachableLoadballancerPrecondition(injector);
+ this.preconditionsMap['RuntimeAvailable'] = new p.RuntimeAvailablePrecondition(injector, getToken);
}
- execute(context: FunctionAppContext, query: Query, executeOptions?: ExecuteOptions) {
- return this.executeWithConditions(this.conditions, context, query, executeOptions);
+ execute(input: p.PreconditionInput, query: Query, executeOptions?: ExecuteOptions) {
+ return this.executeWithConditions(this.conditions, input, query, executeOptions);
}
- executeWithConditions(preconditions: p.HttpPreconditions[], context: FunctionAppContext, query: Query, executeOptions?: ExecuteOptions): Observable> {
+ executeWithConditions(
+ preconditions: p.HttpPreconditions[],
+ input: p.PreconditionInput,
+ query: Query,
+ executeOptions?: ExecuteOptions): Observable> {
+
const errorMapper = (error: p.PreconditionResult) => Observable.of({
isSuccessful: false,
error: {
- errorId: error.errorId
+ errorId: error.errorId,
+ result: error
},
result: null
});
const observableQuery = typeof query === 'function'
- ? this.getToken(context).take(1).concatMap(t => query(t))
+ ? this.getToken(input.resourceId).take(1).concatMap(t => query(t))
: query;
const successMapper = () => observableQuery
@@ -55,20 +59,65 @@ export class ConditionalHttpClient {
error: null,
result: r
}))
- .catch((e: ErrorId) => Observable.of({
- isSuccessful: false,
- error: {
- errorId: e
- },
- result: null
- }));
-
- return preconditions.length > 0
- ? Observable.forkJoin(preconditions
- .map(i => this.preconditionsMap[i])
- .map(i => context ? i.check(context) : Observable.of({ conditionMet: true, errorId: null })))
+ .catch((e: any) => {
+
+ return Observable.of({
+ isSuccessful: false,
+ error: this._getErrorObj(e),
+ result: null
+ });
+ });
+
+ if (preconditions.length > 0) {
+ const checkPreconditions = Observable.forkJoin(
+ preconditions
+ .map(i => this.preconditionsMap[i])
+ .map(i => input ? i.check(input) : Observable.of({ conditionMet: true, errorId: null })));
+
+ return checkPreconditions
.map(preconditionResults => preconditionResults.find(r => !r.conditionMet))
- .concatMap(maybeError => maybeError ? errorMapper(maybeError) : successMapper())
- : successMapper();
+ .concatMap(failedPreconditionResult =>
+ failedPreconditionResult
+ ? errorMapper(failedPreconditionResult)
+ : successMapper());
+ } else {
+ return successMapper();
+ }
+ }
+
+ // We have no idea what kind of observable will be failing, so we make a best
+ // effort to come up with some kind of useful error.
+ private _getErrorObj(e: any) {
+ let mesg: string;
+ let errorId = '/errors/unknown-error';
+
+ if (typeof e === 'string') {
+ mesg = e;
+ } else {
+ const httpError = e as HttpErrorResponse;
+ let body: ArmError;
+ if (httpError.json) {
+ body = httpError.json();
+ if (body && body.error) {
+ mesg = body.error.message;
+
+ if (httpError.status === 401) {
+ errorId = errorIds.armErrors.noAccess;
+ } else if (httpError.status === 409 && body.error.code === 'ScopeLocked') {
+ errorId = errorIds.armErrors.scopeLocked;
+ }
+ }
+ }
+
+ if (!mesg && httpError.statusText && httpError.url) {
+ mesg = `${httpError.statusText} - ${httpError.url}`;
+ }
+ }
+
+ return {
+ errorId: errorId,
+ message: mesg,
+ result: e
+ };
}
}
diff --git a/AzureFunctions.AngularClient/src/app/shared/models/arm/arm-obj.ts b/AzureFunctions.AngularClient/src/app/shared/models/arm/arm-obj.ts
index d10d19e9a7..c7dd1fc284 100644
--- a/AzureFunctions.AngularClient/src/app/shared/models/arm/arm-obj.ts
+++ b/AzureFunctions.AngularClient/src/app/shared/models/arm/arm-obj.ts
@@ -1,3 +1,5 @@
+export type ResourceId = string;
+
export interface ArmObj {
id: string;
name: string;
@@ -9,16 +11,16 @@ export interface ArmObj {
}
export interface ArmObjMap {
- objects: { [key: string]: ArmObj },
- error?: string
+ objects: { [key: string]: ArmObj };
+ error?: string;
}
export interface ArmArrayResult {
- value : ArmObj[];
- nextLink : string;
+ value: ArmObj[];
+ nextLink: string;
}
-export interface Identity{
+export interface Identity {
principalId: string;
tenantId: string;
type: string;
diff --git a/AzureFunctions.AngularClient/src/app/shared/models/arm/connection-strings.ts b/AzureFunctions.AngularClient/src/app/shared/models/arm/connection-strings.ts
index bc0fd8d8cf..8b290e4327 100644
--- a/AzureFunctions.AngularClient/src/app/shared/models/arm/connection-strings.ts
+++ b/AzureFunctions.AngularClient/src/app/shared/models/arm/connection-strings.ts
@@ -19,4 +19,21 @@ export enum ConnectionStringType {
DocDb,
RedisCache,
PostgreSQL
+}
+
+// Only the following connection string types are actually supported: MySql, SQLServer, SQLAzure, Custom
+// The remaing types are not valid but were inadvertently exposed in the portal for several months.
+// THE INVALID TYPES ARE ONLY TO BE USED/DISPLAYED IN CASES WHERE THEY ARE PRESENT IN EXISTING CONFIGURATION.
+export namespace ConnectionStringType {
+ export function isSupported(type: ConnectionStringType) {
+ switch (type) {
+ case ConnectionStringType.MySql:
+ case ConnectionStringType.SQLServer:
+ case ConnectionStringType.SQLAzure:
+ case ConnectionStringType.Custom:
+ return true;
+ default:
+ return false;
+ }
+ }
}
\ No newline at end of file
diff --git a/AzureFunctions.AngularClient/src/app/shared/models/broadcast-event.ts b/AzureFunctions.AngularClient/src/app/shared/models/broadcast-event.ts
index 640b563978..5dc7c96f2b 100644
--- a/AzureFunctions.AngularClient/src/app/shared/models/broadcast-event.ts
+++ b/AzureFunctions.AngularClient/src/app/shared/models/broadcast-event.ts
@@ -16,6 +16,7 @@ export enum BroadcastEvent {
ClearError,
OpenTab,
DirtyStateChange,
+ ReloadDeploymentCenter,
UpdateAppsList,
FunctionEditorEvent,
RightTabsEvent,
diff --git a/AzureFunctions.AngularClient/src/app/shared/models/constants.ts b/AzureFunctions.AngularClient/src/app/shared/models/constants.ts
index 617b727be0..66428961ff 100644
--- a/AzureFunctions.AngularClient/src/app/shared/models/constants.ts
+++ b/AzureFunctions.AngularClient/src/app/shared/models/constants.ts
@@ -1,5 +1,4 @@
-
-export class HttpMethods {
+export class HttpMethods {
public static GET = 'get';
public static POST = 'post';
public static DELETE = 'delete';
@@ -11,10 +10,9 @@ export class HttpMethods {
}
export class Constants {
- public static serviceHost =
- window.location.hostname === 'localhost' || window.appsvc.env.runtimeType === 'Standalone' || window.appsvc.env.runtimeType === 'OnPrem'
- ? `https://${window.location.hostname}:${window.location.port}/`
- : `https://${window.location.hostname}/`;
+ public static serviceHost = window.location.hostname === 'localhost' || window.appsvc.env.runtimeType === 'Standalone' || window.appsvc.env.runtimeType === 'OnPrem'
+ ? `https://${window.location.hostname}:${window.location.port}/`
+ : `https://${window.location.hostname}/`;
public static nodeVersion = '6.5.0';
public static latest = 'latest';
@@ -66,6 +64,7 @@ export class SiteTabIds {
public static readonly apiDefinition = 'apiDefinition';
public static readonly config = 'config';
public static readonly applicationSettings = 'appSettings';
+ public static readonly continuousDeployment = 'continuousDeployment';
public static readonly logicApps = 'logicApps';
}
@@ -113,40 +112,39 @@ export class LocalStorageKeys {
}
export class Order {
- public static templateOrder: string[] =
- [
- 'HttpTrigger-',
- 'TimerTrigger-',
- 'QueueTrigger-',
- 'ServiceBusQueueTrigger-',
- 'ServiceBusTopicTrigger-',
- 'BlobTrigger-',
- 'EventHubTrigger-',
- 'CosmosDBTrigger-',
- 'IoTHubTrigger-',
- 'IoTHubServiceBusQueueTrigger-',
- 'IoTHubServiceBusTopicTrigger-',
- 'GenericWebHook-',
- 'GitHubCommenter-',
- 'GitHubWebHook-',
- 'HttpGET(CRUD)-',
- 'HttpPOST(CRUD)-',
- 'HttpPUT(CRUD)-',
- 'HttpTriggerWithParameters-',
- 'ScheduledMail-',
- 'SendGrid-',
- 'FaceLocator-',
- 'ImageResizer-',
- 'SasToken-',
- 'ManualTrigger-',
- 'CDS-',
- 'AppInsightsHttpAvailability-',
- 'AppInsightsRealtimePowerBI-',
- 'AppInsightsScheduledAnalytics-',
- 'AppInsightsScheduledDigest-',
- 'ExternalFileTrigger-',
- 'ExternalTable-'
- ];
+ public static templateOrder: string[] = [
+ 'HttpTrigger-',
+ 'TimerTrigger-',
+ 'QueueTrigger-',
+ 'ServiceBusQueueTrigger-',
+ 'ServiceBusTopicTrigger-',
+ 'BlobTrigger-',
+ 'EventHubTrigger-',
+ 'CosmosDBTrigger-',
+ 'IoTHubTrigger-',
+ 'IoTHubServiceBusQueueTrigger-',
+ 'IoTHubServiceBusTopicTrigger-',
+ 'GenericWebHook-',
+ 'GitHubCommenter-',
+ 'GitHubWebHook-',
+ 'HttpGET(CRUD)-',
+ 'HttpPOST(CRUD)-',
+ 'HttpPUT(CRUD)-',
+ 'HttpTriggerWithParameters-',
+ 'ScheduledMail-',
+ 'SendGrid-',
+ 'FaceLocator-',
+ 'ImageResizer-',
+ 'SasToken-',
+ 'ManualTrigger-',
+ 'CDS-',
+ 'AppInsightsHttpAvailability-',
+ 'AppInsightsRealtimePowerBI-',
+ 'AppInsightsScheduledAnalytics-',
+ 'AppInsightsScheduledDigest-',
+ 'ExternalFileTrigger-',
+ 'ExternalTable-'
+ ];
}
// NOTE: If you change any string values here, make sure you search for references to the values
@@ -164,6 +162,7 @@ export class ScenarioIds {
public static readonly addConsole = 'AddConsole';
public static readonly addSsh = 'AddSsh';
public static readonly addTopLevelAppsNode = 'AddTopLevelAppsNode';
+ public static readonly enableAppInsights = 'EnableAppInsights';
public static readonly enablePushNotifications = 'EnablePushNotifications';
public static readonly enableAuth = 'EnableAuth';
public static readonly enableMsi = 'EnableMsi';
@@ -193,6 +192,7 @@ export class ScenarioIds {
public static readonly noPaddingOnSideNav = 'NoPaddingOnSideNav';
public static readonly downloadWithAppSettings = 'DownloadWithAppSettings';
public static readonly downloadWithVsProj = 'DownloadWithVsProj';
+ public static readonly openOldWebhostingPlanBlade = 'OpenOldWebhostingPlanBlade';
}
export class ServerFarmSku {
@@ -236,6 +236,9 @@ export class LogCategories {
public static readonly swaggerDefinition = 'SwaggerDefinition';
public static readonly binding = 'Binding';
public static readonly functionNew = 'FunctionNew';
+ public static readonly cicd = 'CICD';
+ public static readonly telemetry = 'Telemetry';
+ public static readonly featureLoading = 'FeatureLoading';
}
export class KeyCodes {
@@ -326,3 +329,13 @@ export class HttpConstants {
500: 'Server Error'
};
}
+
+export class DeploymentCenterConstants {
+ public static githubUri = 'https://github.com';
+ public static githubApiUrl = 'https://api.github.com';
+ public static bitbucketApiUrl = 'https://api.bitbucket.org/2.0';
+ public static bitbucketUrl = 'https://bitbucket.org';
+ public static dropboxApiUrl = 'https://api.dropboxapi.com/2';
+ public static dropboxUri = 'https://www.dropbox.com/home/Apps/Azure';
+ public static onedriveApiUri = 'https://api.onedrive.com/v1.0/drive/special/approot:';
+}
diff --git a/AzureFunctions.AngularClient/src/app/shared/models/error-ids.ts b/AzureFunctions.AngularClient/src/app/shared/models/error-ids.ts
index dc94692204..f25483efc4 100644
--- a/AzureFunctions.AngularClient/src/app/shared/models/error-ids.ts
+++ b/AzureFunctions.AngularClient/src/app/shared/models/error-ids.ts
@@ -6,6 +6,12 @@ export namespace errorIds {
export const unreachableInternalLoadBalancer = '/errors/preconditions/unreachableInternalLoadBalancer';
export const runtimeIsNotAvailable = '/errors/preconditions/runtimeIsNotAvailable';
export const runtimeHttpNotAvailable = '/errors/preconditions/runtimeHttpNotAvailable';
+ export const noFunctionAppContext = '/errors/preconditions/noFunctionAppContext';
+ }
+
+ export namespace armErrors {
+ export const noAccess = '/errors/arm/noaccess';
+ export const scopeLocked = '/errors/arm/scopelocked';
}
export const functionNotFound = '/errors/functionNotFound';
@@ -61,5 +67,7 @@ export namespace errorIds {
export const proxyJsonNotFound = '/errors/proxyJsonNotFound';
export const embeddedEditorLoadError = '/errors/embedded/editor-load';
export const embeddedEditorSaveError = '/errors/embedded/editor-save';
- export const embeddedEditorDeleteError = '/errors/embedded/editor-delete';
+ export const embeddedDeleteError = '/errors/embedded/delete';
+ export const embeddedGetEntities = '/errors/embedded/get-entities';
+ export const embeddedCreateError = '/errors/embedded/create';
}
diff --git a/AzureFunctions.AngularClient/src/app/shared/models/function-app-http-result.ts b/AzureFunctions.AngularClient/src/app/shared/models/function-app-http-result.ts
deleted file mode 100644
index dfdfd02015..0000000000
--- a/AzureFunctions.AngularClient/src/app/shared/models/function-app-http-result.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-export interface FunctionAppHttpError {
- errorId: string;
- message?: string;
-}
-
-export interface FunctionAppHttpResult {
- isSuccessful: boolean;
- error: FunctionAppHttpError | null;
- result: T | null;
-}
diff --git a/AzureFunctions.AngularClient/src/app/shared/models/function-config.ts b/AzureFunctions.AngularClient/src/app/shared/models/function-config.ts
index e1296f5520..4f56459846 100644
--- a/AzureFunctions.AngularClient/src/app/shared/models/function-config.ts
+++ b/AzureFunctions.AngularClient/src/app/shared/models/function-config.ts
@@ -19,4 +19,5 @@ export interface FunctionBinding {
authLevel: string;
route: string;
methods: string[];
-}
\ No newline at end of file
+ message: string;
+}
diff --git a/AzureFunctions.AngularClient/src/app/shared/models/http-result.ts b/AzureFunctions.AngularClient/src/app/shared/models/http-result.ts
new file mode 100644
index 0000000000..69b479ce6c
--- /dev/null
+++ b/AzureFunctions.AngularClient/src/app/shared/models/http-result.ts
@@ -0,0 +1,30 @@
+export interface HttpError {
+ errorId: string;
+ message?: string;
+ result?: any;
+}
+
+export interface HttpResult {
+ isSuccessful: boolean;
+ error: HttpError | null;
+ result: T | null;
+}
+
+export interface ArmError {
+ error: {
+ code: 'InvalidAuthenticationTokenTenant' | 'ScopeLocked';
+ message: string;
+ }
+}
+
+export interface HttpErrorResponse {
+ message: string;
+ error: any | null;
+ ok: boolean;
+ headers: Headers;
+ status: number;
+ statusText: string;
+ url: string | null;
+ json(): T;
+ text(): string;
+}
diff --git a/AzureFunctions.AngularClient/src/app/shared/models/portal-resources.ts b/AzureFunctions.AngularClient/src/app/shared/models/portal-resources.ts
index 0f83fb61eb..e48459d99b 100644
--- a/AzureFunctions.AngularClient/src/app/shared/models/portal-resources.ts
+++ b/AzureFunctions.AngularClient/src/app/shared/models/portal-resources.ts
@@ -1,6 +1,5 @@
// This file is auto generated
-
-export class PortalResources
+ export class PortalResources
{
public static azureFunctions: string = "azureFunctions";
public static azureFunctionsRuntime: string = "azureFunctionsRuntime";
@@ -448,6 +447,7 @@ export class PortalResources
public static autoSwapUpsell: string = "autoSwapUpsell";
public static autoSwapEnabledLabel: string = "autoSwapEnabledLabel";
public static autoSwapSlotNameLabel: string = "autoSwapSlotNameLabel";
+ public static productionSlotDisplayName: string = "productionSlotDisplayName";
public static clientAffinityEnabledLabel: string = "clientAffinityEnabledLabel";
public static clientAffinityInfoText: string = "clientAffinityInfoText";
public static remoteDebuggingEnabledLabel: string = "remoteDebuggingEnabledLabel";
@@ -765,6 +765,17 @@ export class PortalResources
public static rrOverride_request: string = "rrOverride_request";
public static rrOverride_response: string = "rrOverride_response";
public static optional: string = "optional";
+ public static gitCloneUri: string = "gitCloneUri";
+ public static rollbackEnabled: string = "rollbackEnabled";
+ public static scmType: string = "scmType";
+ public static redeploy: string = "redeploy";
+ public static activity: string = "activity";
+ public static active: string = "active";
+ public static time: string = "time";
+ public static log: string = "log";
+ public static showLogs: string = "showLogs";
+ public static commitIdAuthor: string = "commitIdAuthor";
+ public static checkinMessage: string = "checkinMessage";
public static topBar_runtimeV2: string = "topBar_runtimeV2";
public static functionKeys_clickToHide: string = "functionKeys_clickToHide";
public static feature_logicAppsInfo: string = "feature_logicAppsInfo";
@@ -822,7 +833,74 @@ export class PortalResources
public static appFunctionSettings_warning_6: string = "appFunctionSettings_warning_6";
public static appFunctionSettings_warning_7: string = "appFunctionSettings_warning_7";
public static warning: string = "warning";
+ public static authorizedAs: string = "authorizedAs";
+ public static continue: string = "continue";
+ public static authorize: string = "authorize";
+ public static changeAuthorization: string = "changeAuthorization";
+ public static back: string = "back";
+ public static finish: string = "finish";
+ public static code: string = "code";
+ public static repository: string = "repository";
+ public static branch: string = "branch";
+ public static folder: string = "folder";
+ public static repoType: string = "repoType";
+ public static organization: string = "organization";
+ public static vstsAccount: string = "vstsAccount";
+ public static project: string = "project";
+ public static build: string = "build";
+ public static account: string = "account";
+ public static loadTest: string = "loadTest";
+ public static slot: string = "slot";
+ public static production: string = "production";
public static entity: string = "entity";
public static entityColon: string = "entityColon";
public static swaggerDefinition_notSupportedForBeta: string = "swaggerDefinition_notSupportedForBeta";
-}
+ public static deployedSuccessfullyTo: string = "deployedSuccessfullyTo";
+ public static deployedFailedTo: string = "deployedFailedTo";
+ public static swappedSlotSuccess: string = "swappedSlotSuccess";
+ public static swappedSlotFail: string = "swappedSlotFail";
+ public static setupCDSuccessAndTriggerBuild: string = "setupCDSuccessAndTriggerBuild";
+ public static setupCDSuccess: string = "setupCDSuccess";
+ public static setupCDFail: string = "setupCDFail";
+ public static createVSTSAccountSuccess: string = "createVSTSAccountSuccess";
+ public static createVSTSAccountFail: string = "createVSTSAccountFail";
+ public static createdNewSlotSuccess: string = "createdNewSlotSuccess";
+ public static createdNewSlotFail: string = "createdNewSlotFail";
+ public static createTestWebAppSuccess: string = "createTestWebAppSuccess";
+ public static createTestWebAppFail: string = "createTestWebAppFail";
+ public static disconnectCICDVSOSuccess: string = "disconnectCICDVSOSuccess";
+ public static appServiceStartSuccess: string = "appServiceStartSuccess";
+ public static appServiceStartFail: string = "appServiceStartFail";
+ public static appServiceStopSuccess: string = "appServiceStopSuccess";
+ public static appServiceStopFail: string = "appServiceStopFail";
+ public static appServiceRestartSuccess: string = "appServiceRestartSuccess";
+ public static appServiceRestartFail: string = "appServiceRestartFail";
+ public static vsoSync: string = "vsoSync";
+ public static buildDefinition: string = "buildDefinition";
+ public static releaseDefinition: string = "releaseDefinition";
+ public static buildTriggered: string = "buildTriggered";
+ public static webApp: string = "webApp";
+ public static vsoAccount: string = "vsoAccount";
+ public static viewInstructions: string = "viewInstructions";
+ public static buildUrl: string = "buildUrl";
+ public static releaseUrl: string = "releaseUrl";
+ public static deploymentCenter: string = "deploymentCenter";
+ public static sourceControl: string = "sourceControl";
+ public static buildProvider: string = "buildProvider";
+ public static deploy: string = "deploy";
+ public static summary: string = "summary";
+ public static experimentalLanguageSupport: string = "experimentalLanguageSupport";
+ public static sourceProvider: string = "sourceProvider";
+ public static onedriveDesc: string = "onedriveDesc";
+ public static githubDesc: string = "githubDesc";
+ public static vstsDesc: string = "vstsDesc";
+ public static externalDesc: string = "externalDesc";
+ public static bitbucketDesc: string = "bitbucketDesc";
+ public static localGitDesc: string = "localGitDesc";
+ public static dropboxDesc: string = "dropboxDesc";
+ public static temp_category_durableFunctions: string = "temp_category_durableFunctions";
+ public static DurableFunctionsActivity_description: string = "DurableFunctionsActivity_description";
+ public static DurableFunctionsOrchestrator_description: string = "DurableFunctionsOrchestrator_description";
+ public static DurableFunctionsHttpStart_description: string = "DurableFunctionsHttpStart_description";
+ public static functionsPreviewTitle: string = "functionsPreviewTitle";
+}
diff --git a/AzureFunctions.AngularClient/src/app/shared/models/portal.ts b/AzureFunctions.AngularClient/src/app/shared/models/portal.ts
index 848996bca1..cd36166675 100644
--- a/AzureFunctions.AngularClient/src/app/shared/models/portal.ts
+++ b/AzureFunctions.AngularClient/src/app/shared/models/portal.ts
@@ -24,6 +24,16 @@ export interface StartupInfo {
resourceId: string;
graphToken: string;
theme: string;
+ crmInfo?: CrmInfo;
+ armEndpoint?: string;
+}
+
+export interface CrmInfo {
+ crmTokenHeaderName: string;
+ crmInstanceHeaderName: string;
+ crmSolutionIdHeaderName: string;
+ environmentId: string;
+ namespaceId: string;
}
export interface DataMessage{
diff --git a/AzureFunctions.AngularClient/src/app/shared/models/publishing-credentials.ts b/AzureFunctions.AngularClient/src/app/shared/models/publishing-credentials.ts
index 22ab55d68d..dc28ef024d 100644
--- a/AzureFunctions.AngularClient/src/app/shared/models/publishing-credentials.ts
+++ b/AzureFunctions.AngularClient/src/app/shared/models/publishing-credentials.ts
@@ -1,12 +1,6 @@
export interface PublishingCredentials {
- id: string;
name: string;
- type: string;
- location: string;
- properties: {
- name: string;
- publishingUserName: string;
- publishingPassword: string;
- scmUri: string;
- }
-}
\ No newline at end of file
+ publishingUserName: string;
+ publishingPassword: string;
+ scmUri: string;
+}
diff --git a/AzureFunctions.AngularClient/src/app/shared/preconditions.ts b/AzureFunctions.AngularClient/src/app/shared/preconditions.ts
index e38f179a6d..c8c5210c98 100644
--- a/AzureFunctions.AngularClient/src/app/shared/preconditions.ts
+++ b/AzureFunctions.AngularClient/src/app/shared/preconditions.ts
@@ -1,4 +1,3 @@
-import { FunctionAppContext } from './function-app-context';
import { CacheService } from 'app/shared/services/cache.service';
import { Observable } from 'rxjs/Observable';
import { ArmObj } from './models/arm/arm-obj';
@@ -8,12 +7,24 @@ import { HostingEnvironment } from './models/arm/hosting-environment';
import { LogService } from './services/log.service';
import { Headers } from '@angular/http';
import { HostStatus } from './models/host-status';
+import { Injector } from '@angular/core';
+import { FunctionAppContext } from 'app/shared/function-app-context';
+import { ArmUtil } from 'app/shared/Utilities/arm-utils';
export namespace Preconditions {
export type PreconditionErrorId = string;
- export type HttpPreconditions = 'NotStopped' | 'ReachableLoadballancer' | 'NotOverQuota' | 'RuntimeAvailable' | 'NoClientCertificate';
+ export type HttpPreconditions =
+ 'NotStopped'
+ | 'ReachableLoadballancer'
+ | 'NotOverQuota'
+ | 'RuntimeAvailable'
+ | 'NoClientCertificate';
+
export type PreconditionMap = {[key in HttpPreconditions]: HttpPrecondition };
- export type DataService = CacheService;
+
+ export interface PreconditionInput {
+ resourceId: string;
+ }
export interface PreconditionResult {
conditionMet: boolean;
@@ -21,13 +32,28 @@ export namespace Preconditions {
}
export abstract class HttpPrecondition {
- constructor(protected dataService: DataService, protected logService: LogService) { }
- abstract check(context: FunctionAppContext): Observable;
+ protected cacheService: CacheService;
+ protected logService: LogService;
+
+ constructor(protected injector: Injector) {
+ this.cacheService = injector.get(CacheService);
+ this.logService = injector.get(LogService);
+ }
+
+ abstract check(input: PreconditionInput): Observable;
+
+ // Can't reference the FunctionAppService here since that service creates these preconditions
+ // and would create a stack overflow.
+ protected getFunctionAppContext(resourceId: string){
+ return this.cacheService.getArm(resourceId)
+ .map(r => ArmUtil.mapArmSiteToContext(r.json(), this.injector));
+ }
}
export class NotStoppedPrecondition extends HttpPrecondition {
- check(context: FunctionAppContext): Observable {
- return this.dataService.getArm(context.site.id)
+ check(input: PreconditionInput): Observable {
+
+ return this.cacheService.getArm(input.resourceId)
.map(r => {
const app: ArmObj = r.json();
return {
@@ -46,18 +72,19 @@ export namespace Preconditions {
}
export class ReachableLoadballancerPrecondition extends HttpPrecondition {
- check(context: FunctionAppContext): Observable {
- return this.dataService.getArm(context.site.id)
- .concatMap(r => {
- const app: ArmObj = r.json();
+
+ check(input: PreconditionInput): Observable {
+ return this.getFunctionAppContext(input.resourceId)
+ .concatMap(context => {
+ const app: ArmObj = context.site;
if (app.properties.hostingEnvironmentProfile &&
app.properties.hostingEnvironmentProfile.id) {
- return this.dataService.getArm(app.properties.hostingEnvironmentProfile.id, false, '2016-09-01')
+ return this.cacheService.getArm(app.properties.hostingEnvironmentProfile.id, false, '2016-09-01')
.concatMap(a => {
const ase: ArmObj = a.json();
if (ase.properties.internalLoadBalancingMode &&
ase.properties.internalLoadBalancingMode !== 'None') {
- return this.dataService.get(context.urlTemplates.runtimeSiteUrl)
+ return this.cacheService.get(context.urlTemplates.runtimeSiteUrl)
.map(() => true)
.catch(() => Observable.of(false));
} else {
@@ -85,7 +112,7 @@ export namespace Preconditions {
}
export class NotOverQuotaPrecondition extends HttpPrecondition {
- check(context: FunctionAppContext): Observable {
+ check(input: PreconditionInput): Observable {
return Observable.of({
conditionMet: true,
errorId: null
@@ -94,15 +121,19 @@ export namespace Preconditions {
}
export class NoClientCertificatePrecondition extends HttpPrecondition {
- check(context: FunctionAppContext): Observable {
- return this.dataService.postArm(`${context.site.id}/config/authsettings/list`)
- .map(r => {
- const auth: ArmObj = r.json();
+ check(input: PreconditionInput): Observable {
+
+ return Observable.zip(
+ this.cacheService.getArm(input.resourceId),
+ this.cacheService.postArm(`${input.resourceId}/config/authsettings/list`))
+ .map(tuple => {
+ const site: ArmObj = tuple[0].json();
+ const auth: ArmObj = tuple[1].json();
return {
easyAuthEnabled: auth.properties['enabled'] && auth.properties['unauthenticatedClientAction'] !== 1,
AADConfigured: auth.properties['clientId'] ? true : false,
AADNotConfigured: auth.properties['clientId'] ? false : true,
- clientCertEnabled: context.site.properties.clientCertEnabled
+ clientCertEnabled: site.properties.clientCertEnabled
};
})
.map(auth => {
@@ -122,16 +153,22 @@ export namespace Preconditions {
}
export class RuntimeAvailablePrecondition extends HttpPrecondition {
- constructor(dataService: DataService, logService: LogService, private getToken: (context: FunctionAppContext) => Observable) {
- super(dataService, logService);
+
+ constructor(injector: Injector, private getToken: (resourceId: string) => Observable) {
+ super(injector);
}
- check(context: FunctionAppContext): Observable {
- return this.getToken(context)
- .take(1)
+
+ check(input: PreconditionInput): Observable {
+ let context: FunctionAppContext;
+ return this.getFunctionAppContext(input.resourceId)
+ .concatMap(c => {
+ context = c;
+ return this.getToken(input.resourceId).take(1)
+ })
.concatMap(token => {
const headers = new Headers();
headers.append('Authorization', `Bearer ${token}`);
- return this.dataService.post(context.urlTemplates.runtimeStatusUrl, false, headers);
+ return this.cacheService.post(context.urlTemplates.runtimeStatusUrl, false, headers);
})
.map(r => {
const status: HostStatus = r.json();
diff --git a/AzureFunctions.AngularClient/src/app/shared/services/arm-embedded.service.ts b/AzureFunctions.AngularClient/src/app/shared/services/arm-embedded.service.ts
index 8b52f2bbf5..3b4e832f66 100644
--- a/AzureFunctions.AngularClient/src/app/shared/services/arm-embedded.service.ts
+++ b/AzureFunctions.AngularClient/src/app/shared/services/arm-embedded.service.ts
@@ -72,9 +72,9 @@ export class ArmEmbeddedService extends ArmService {
// Get a list of all functions in the environment
return super.send(method, url, body, etag, headers);
} else if (urlNoQuery.endsWith('/config/web')) {
- // TODO: filter out these requests
+ return Observable.of(null);
} else if (urlNoQuery.endsWith('slots')) {
- // TODO: filter out these requests
+ return Observable.of(null);
}
if (this._whitelistedRPPrefixUrls.find(u => urlNoQuery.startsWith(u.toLowerCase()))) {
diff --git a/AzureFunctions.AngularClient/src/app/shared/services/arm.service.ts b/AzureFunctions.AngularClient/src/app/shared/services/arm.service.ts
index adec80227f..339694e7c2 100644
--- a/AzureFunctions.AngularClient/src/app/shared/services/arm.service.ts
+++ b/AzureFunctions.AngularClient/src/app/shared/services/arm.service.ts
@@ -42,13 +42,13 @@ export class ArmService {
});
}
- getHeaders(etag?: string){
+ getHeaders(etag?: string) {
return ArmServiceHelper.getHeaders(this._token, this._sessionId, etag);
}
send(method: string, url: string, body?: any, etag?: string, headers?: Headers, invokeApi?: boolean) {
- headers = headers ? headers : ArmServiceHelper.getHeaders(this._token, this._sessionId, etag);
+ headers = headers ? headers : this.getHeaders(etag);
if (invokeApi) {
let pathAndQuery = url.slice(this.armUrl.length);
@@ -71,23 +71,28 @@ export class ArmService {
get(resourceId: string, apiVersion?: string) {
const url = `${this.armUrl}${resourceId}?api-version=${apiVersion ? apiVersion : this.websiteApiVersion}`;
- return this._http.get(url, { headers: ArmServiceHelper.getHeaders(this._token, this._sessionId) });
+ return this._http.get(url, { headers: this.getHeaders() });
}
delete(resourceId: string, apiVersion?: string) {
const url = `${this.armUrl}${resourceId}?api-version=${apiVersion ? apiVersion : this.websiteApiVersion}`;
- return this._http.delete(url, { headers: ArmServiceHelper.getHeaders(this._token, this._sessionId) });
+ return this._http.delete(url, { headers: this.getHeaders() });
}
put(resourceId: string, body: any, apiVersion?: string) {
const url = `${this.armUrl}${resourceId}?api-version=${apiVersion ? apiVersion : this.websiteApiVersion}`;
- return this._http.put(url, JSON.stringify(body), { headers: ArmServiceHelper.getHeaders(this._token, this._sessionId) });
+ return this._http.put(url, JSON.stringify(body), { headers: this.getHeaders() });
+ }
+
+ patch(resourceId: string, body: any, apiVersion?: string) {
+ const url = `${this.armUrl}${resourceId}?api-version=${apiVersion ? apiVersion : this.websiteApiVersion}`;
+ return this._http.patch(url, JSON.stringify(body), { headers: ArmServiceHelper.getHeaders(this._token, this._sessionId) });
}
post(resourceId: string, body: any, apiVersion?: string) {
const content = !!body ? JSON.stringify(body) : null;
const url = `${this.armUrl}${resourceId}?api-version=${apiVersion ? apiVersion : this.websiteApiVersion}`;
- return this._http.post(url, content, { headers: ArmServiceHelper.getHeaders(this._token, this._sessionId) });
+ return this._http.post(url, content, { headers: this.getHeaders() });
}
getArmUrl(resourceId: string, apiVersion?: string) {
diff --git a/AzureFunctions.AngularClient/src/app/shared/services/broadcast.service.ts b/AzureFunctions.AngularClient/src/app/shared/services/broadcast.service.ts
index ae10279aff..de33f2c2da 100644
--- a/AzureFunctions.AngularClient/src/app/shared/services/broadcast.service.ts
+++ b/AzureFunctions.AngularClient/src/app/shared/services/broadcast.service.ts
@@ -11,7 +11,7 @@ import { Subscription } from 'rxjs/Subscription';
import { FunctionInfo } from '../models/function-info';
import { TutorialEvent } from '../models/tutorial';
import { ErrorEvent } from '../models/error-event';
-import { FunctionAppHttpError } from '../models/function-app-http-result';
+import { HttpError } from '../models/http-result';
interface EventInfo {
eventType: BroadcastEvent;
@@ -26,7 +26,7 @@ export class BroadcastService {
private functionUpdatedEvent: EventEmitter;
private integrateChangedEvent: EventEmitter;
private tutorialStepEvent: EventEmitter;
- private errorEvent: EventEmitter;
+ private errorEvent: EventEmitter;
private trialExpired: EventEmitter;
private resetKeySelection: EventEmitter;
private clearErrorEvent: EventEmitter;
@@ -56,6 +56,7 @@ export class BroadcastService {
this._streamMap[BroadcastEvent.DirtyStateChange] = new ReplaySubject(1);
this._streamMap[BroadcastEvent.UpdateAppsList] = new ReplaySubject(1);
this._streamMap[BroadcastEvent.IntegrateChanged] = new Subject();
+ this._streamMap[BroadcastEvent.ReloadDeploymentCenter] = new Subject();
this._streamMap[BroadcastEvent.FunctionEditorEvent] = new Subject();
this._streamMap[BroadcastEvent.RightTabsEvent] = new Subject();
this._streamMap[BroadcastEvent.BottomTabsEvent] = new Subject();
diff --git a/AzureFunctions.AngularClient/src/app/shared/services/embedded.service.ts b/AzureFunctions.AngularClient/src/app/shared/services/embedded.service.ts
new file mode 100644
index 0000000000..89c706b475
--- /dev/null
+++ b/AzureFunctions.AngularClient/src/app/shared/services/embedded.service.ts
@@ -0,0 +1,133 @@
+import { ArmEmbeddedService } from './arm-embedded.service';
+import { FunctionInfo } from 'app/shared/models/function-info';
+import { FunctionAppContext } from 'app/shared/function-app-context';
+import { StartupInfo } from 'app/shared/models/portal';
+import { ArmService } from 'app/shared/services/arm.service';
+import { errorIds } from 'app/shared/models/error-ids';
+import { Entity } from './../../function/embedded/models/entity';
+import { ArmArrayResult } from './../models/arm/arm-obj';
+import { HttpResult } from './../models/http-result';
+import { Observable } from 'rxjs/Observable';
+import { CacheService } from 'app/shared/services/cache.service';
+import { Injectable } from '@angular/core';
+import { UserService } from 'app/shared/services/user.service';
+import { Response } from '@angular/http';
+
+@Injectable()
+export class EmbeddedService {
+ private _cdsEntitiesUrlFormat = 'https://tip1.api.cds.microsoft.com/providers/Microsoft.CommonDataModel/environments/{0}/namespaces/{1}/entities?api-version=2016-11-01-alpha&$expand=namespace&headeronly=true';
+
+ constructor(
+ private _userService: UserService,
+ private _cacheService: CacheService,
+ private _armService: ArmService) { }
+
+ createFunction(context: FunctionAppContext, functionName: string, files: any, config: any): Observable> {
+ const filesCopy = Object.assign({}, files);
+ const sampleData = filesCopy['sample.dat'];
+ delete filesCopy['sample.dat'];
+
+ return this._userService.getStartupInfo()
+ .first()
+ .switchMap(info => {
+ const headers = this._getHeaders(info);
+ const content = JSON.stringify({ files: filesCopy, test_data: sampleData, config: config });
+ const url = context.urlTemplates.getFunctionUrl(functionName);
+ return this._cacheService.put(url, headers, content).map(r => r.json() as FunctionInfo);
+ })
+ .do(() => {
+ const smallerSiteId = context.site.id.split('/').filter(part => !!part).slice(0, 4).join('/');
+ const functionsUrl = `${ArmEmbeddedService.url}/${smallerSiteId}/functions`;
+ this._cacheService.clearCachePrefix(functionsUrl);
+ })
+ .map((r: FunctionInfo) => {
+ const result: HttpResult = {
+ isSuccessful: true,
+ error: null,
+ result: r
+ };
+ return result;
+ })
+ .catch(e => {
+ const result: HttpResult = {
+ isSuccessful: false,
+ error: {
+ errorId: errorIds.embeddedCreateError,
+ message: 'Failed to create function'
+ },
+ result: null
+ };
+
+ return Observable.of(result);
+ });
+ }
+
+ deleteFunction(resourceId: string): Observable> {
+ return this._userService.getStartupInfo()
+ .first()
+ .switchMap(info => {
+ const headers = this._getHeaders(info);
+ const url = this._armService.getArmUrl(resourceId, this._armService.websiteApiVersion);
+ return this._cacheService.delete(url, headers);
+ })
+ .map(r => {
+ const result: HttpResult = {
+ isSuccessful: true,
+ error: null,
+ result: null
+ };
+ return result;
+ })
+ .catch(e => {
+ const result: HttpResult = {
+ isSuccessful: false,
+ error: {
+ errorId: errorIds.embeddedDeleteError,
+ message: 'Failed to delete function'
+ },
+ result: null
+ };
+
+ return Observable.of(result);
+ });
+ }
+
+ getEntities(): Observable>> {
+ return this._userService
+ .getStartupInfo()
+ .first()
+ .switchMap(info => {
+ const headers = this._getHeaders(info);
+ const url = this._cdsEntitiesUrlFormat.format(info.crmInfo.environmentId, info.crmInfo.namespaceId);
+ return this._cacheService.get(url, false, headers);
+ })
+ .map((r: Response) => {
+ const result: HttpResult> = {
+ isSuccessful: true,
+ error: null,
+ result: r.json()
+ };
+ return result;
+ })
+ .catch(e => {
+ const result: HttpResult> = {
+ isSuccessful: false,
+ error: {
+ errorId: errorIds.embeddedGetEntities,
+ message: 'Failed to get entitites'
+ },
+ result: null
+ };
+
+ return Observable.of(result);
+ });
+ }
+
+ private _getHeaders(info: StartupInfo){
+ const headers = this._armService.getHeaders();
+ headers.append('x-cds-crm-user-token', info.crmInfo.crmTokenHeaderName);
+ headers.append('x-cds-crm-org', info.crmInfo.crmInstanceHeaderName);
+ headers.append('x-cds-crm-solutionid', info.crmInfo.crmSolutionIdHeaderName);
+ return headers;
+ }
+}
diff --git a/AzureFunctions.AngularClient/src/app/shared/services/function-app.service.ts b/AzureFunctions.AngularClient/src/app/shared/services/function-app.service.ts
index fd8be13521..9308aa587e 100644
--- a/AzureFunctions.AngularClient/src/app/shared/services/function-app.service.ts
+++ b/AzureFunctions.AngularClient/src/app/shared/services/function-app.service.ts
@@ -9,7 +9,7 @@ import { CacheService } from 'app/shared/services/cache.service';
import { Injectable, Injector } from '@angular/core';
import { Headers, Response, ResponseType } from '@angular/http';
import { FunctionInfo } from 'app/shared/models/function-info';
-import { FunctionAppHttpResult } from './../models/function-app-http-result';
+import { HttpResult } from './../models/http-result';
import { ArmObj } from 'app/shared/models/arm/arm-obj';
import { FunctionsVersionInfoHelper } from 'app/shared/models/functions-version-info';
import { Constants } from 'app/shared/models/constants';
@@ -36,121 +36,14 @@ import { errorIds } from 'app/shared/models/error-ids';
import { LogService } from './log.service';
import { PortalService } from 'app/shared/services/portal.service';
import { ExtensionInstallStatus } from '../models/extension-install-status';
+import { Templates } from './../../function/embedded/temp-templates';
-type Result = Observable>;
+type Result = Observable>;
@Injectable()
export class FunctionAppService {
private readonly runtime: ConditionalHttpClient;
private readonly azure: ConditionalHttpClient;
-
- private testJsonTemplates = JSON.stringify([
- {
- "id": "SyncTrigger-CSharp",
- "runtime": "1",
- "files": {
- "readme.md": "# HttpTrigger -on",
- "run.csx": "#r \"..\\bin\\Microsoft.Xrm.Sdk.dll\"\nusing Microsoft.Xrm.Sdk;\n\npublic static Entity Run(Entity entity, TraceWriter log)\n{\n\tentity.Attributes[\"name\"] = entity.Attributes[\"name\"].ToString().ToUpper();\n\treturn entity;\n}",
- "sample.dat": "{}"
- },
- "function": {
- "disabled": false,
- "bindings": [
- {
- "name": "entity",
- "message": "create",
- "type": "synctrigger",
- "direction": "in"
- }
- ]
- },
- "metadata": {
- "defaultFunctionName": "SyncTriggerCSharp",
- "description": "$SyncTrigger_description",
- "name": "Sync trigger",
- "language": "C#",
- "trigger": "SyncTrigger",
- "category": [
- "$temp_category_core"
- ],
- "categoryStyle": "http",
- "enabledInTryMode": true,
- "userPrompt": [
- "message"
- ]
- }
- },
- {
- "id": "SyncTrigger-JavaScript",
- "runtime": "1",
- "files": {
- "index.js": "module.exports",
- "sample.dat": "{}"
- },
- "function": {
- "disabled": false,
- "bindings": [
- {
- "name": "entity",
- "message": "create",
- "type": "synctrigger",
- "direction": "in"
- }
- ]
- },
- "metadata": {
- "defaultFunctionName": "SyncTriggerJS",
- "description": "$SyncTrigger_description",
- "name": "Sync trigger",
- "language": "JavaScript",
- "trigger": "SyncTrigger",
- "category": [
- "$temp_category_core"
- ],
- "categoryStyle": "http",
- "enabledInTryMode": true,
- "userPrompt": [
- "message"
- ]
- }
- }
- ]);
-
- private testJsonBindings = JSON.stringify({
- "bindings": [
- {
- "type": "syncTrigger",
- "displayName": "Sync",
- "direction": "trigger",
- "settings": [
- {
- "name": "message",
- "value": "enum",
- "enum": [
- {
- "value": "Create",
- "display": "Create"
- },
- {
- "value": "Destroy",
- "display": "Destroy"
- },
- {
- "value": "Update",
- "display": "Update"
- },
- {
- "value": "Retrieve",
- "display": "Retrieve"
- }
- ],
- "label": "Event",
- "help": "Event help"
- }
- ]
-
- }
- ]
- });
+ private readonly _embeddedTemplates: Templates;
constructor(private _cacheService: CacheService,
private _translateService: TranslateService,
@@ -158,14 +51,22 @@ export class FunctionAppService {
private _injector: Injector,
private _portalService: PortalService,
private _globalStateService: GlobalStateService,
- logService: LogService) {
+ logService: LogService,
+ injector: Injector) {
- this.runtime = new ConditionalHttpClient(_cacheService, logService, context => this.getRuntimeToken(context), 'NoClientCertificate', 'NotOverQuota', 'NotStopped', 'ReachableLoadballancer');
- this.azure = new ConditionalHttpClient(_cacheService, logService, _ => _userService.getStartupInfo().map(i => i.token), 'NotOverQuota', 'ReachableLoadballancer');
+ this.runtime = new ConditionalHttpClient(injector, resourceId => this.getRuntimeToken(resourceId), 'NoClientCertificate', 'NotOverQuota', 'NotStopped', 'ReachableLoadballancer');
+ this.azure = new ConditionalHttpClient(injector, _ => _userService.getStartupInfo().map(i => i.token), 'NotOverQuota', 'ReachableLoadballancer');
+ this._embeddedTemplates = new Templates();
}
- private getRuntimeToken(context: FunctionAppContext): Observable {
- return this._userService.getStartupInfo()
+ private getRuntimeToken(resourceId: string): Observable {
+ let context: FunctionAppContext;
+
+ return this.getAppContext(resourceId)
+ .concatMap(c => {
+ context = c;
+ return this._userService.getStartupInfo()
+ })
.concatMap(info => ArmUtil.isLinuxApp(context.site)
? this._cacheService.get(Constants.serviceHost + `api/runtimetoken${context.site.id}`, false, this.portalHeaders(info.token))
: this._cacheService.get(context.urlTemplates.scmTokenUrl, false, this.headers(info.token)))
@@ -177,7 +78,7 @@ export class FunctionAppService {
}
getFunction(context: FunctionAppContext, name: string): Result {
- return this.getClient(context).execute(context, t => Observable.zip(
+ return this.getClient(context).execute({ resourceId: context.site.id }, t => Observable.zip(
this._cacheService.get(context.urlTemplates.getFunctionUrl(name), false, this.headers(t)),
this._cacheService.postArm(`${context.site.id}/config/appsettings/list`),
(functions, appSettings) => ({ function: functions.json() as FunctionInfo, appSettings: appSettings.json() }))
@@ -193,7 +94,7 @@ export class FunctionAppService {
}
getFunctions(context: FunctionAppContext): Result {
- return this.getClient(context).execute(context, t => Observable.zip(
+ return this.getClient(context).execute({ resourceId: context.site.id }, t => Observable.zip(
this._cacheService.get(context.urlTemplates.functionsUrl, false, this.headers(t)),
this._cacheService.postArm(`${context.site.id}/config/appsettings/list`),
(functions, appSettings) => ({ functions: functions.json() as FunctionInfo[], appSettings: appSettings.json() }))
@@ -216,7 +117,7 @@ export class FunctionAppService {
getApiProxies(context: FunctionAppContext): Result {
const client = this.getClient(context);
- return client.execute(context, t => Observable.zip(
+ return client.execute({ resourceId: context.site.id }, t => Observable.zip(
this._cacheService.get(context.urlTemplates.proxiesJsonUrl, false, this.headers(t))
.catch(err => err.status === 404
? Observable.throw(errorIds.proxyJsonNotFound)
@@ -240,26 +141,26 @@ export class FunctionAppService {
const uri = context.urlTemplates.proxiesJsonUrl;
this._cacheService.clearCachePrefix(uri);
- return this.getClient(context).execute(context, t => this._cacheService.put(uri, this.jsonHeaders(t, ['If-Match', '*']), jsonString));
+ return this.getClient(context).execute({ resourceId: context.site.id }, t => this._cacheService.put(uri, this.jsonHeaders(t, ['If-Match', '*']), jsonString));
}
getFileContent(context: FunctionAppContext, file: VfsObject | string): Result {
const fileHref = typeof file === 'string' ? file : file.href;
- return this.getClient(context).execute(context, t => this._cacheService.get(fileHref, false, this.headers(t)).map(r => r.text()));
+ return this.getClient(context).execute({ resourceId: context.site.id }, t => this._cacheService.get(fileHref, false, this.headers(t)).map(r => r.text()));
}
saveFile(context: FunctionAppContext, file: VfsObject | string, updatedContent: string, functionInfo?: FunctionInfo): Result {
const fileHref = typeof file === 'string' ? file : file.href;
- return this.getClient(context).execute(context, t =>
+ return this.getClient(context).execute({ resourceId: context.site.id }, t =>
this._cacheService.put(fileHref, this.jsonHeaders(t, ['Content-Type', 'plain/text'], ['If-Match', '*']), updatedContent).map(() => file));
}
deleteFile(context: FunctionAppContext, file: VfsObject | string, functionInfo?: FunctionInfo): Result {
const fileHref = typeof file === 'string' ? file : file.href;
- return this.getClient(context).execute(context, t =>
+ return this.getClient(context).execute({ resourceId: context.site.id }, t =>
this._cacheService.delete(fileHref, this.jsonHeaders(t, ['Content-Type', 'plain/text'], ['If-Match', '*'])).map(() => file));
}
@@ -278,7 +179,7 @@ export class FunctionAppService {
getTemplates(context: FunctionAppContext): Result {
if (this._portalService.isEmbeddedFunctions) {
- const devTemplate: FunctionTemplate[] = JSON.parse(this.testJsonTemplates);
+ const devTemplate: FunctionTemplate[] = JSON.parse(this._embeddedTemplates.templatesJson);
return Observable.of({
isSuccessful: true,
result: devTemplate,
@@ -300,7 +201,7 @@ export class FunctionAppService {
} catch (e) {
console.error(e);
}
- return this.azure.executeWithConditions([], context, t =>
+ return this.azure.executeWithConditions([], { resourceId: context.site.id }, t =>
this.getExtensionVersionFromAppSettings(context)
.mergeMap(extensionVersion => {
const headers = this.portalHeaders(t);
@@ -330,13 +231,13 @@ export class FunctionAppService {
config: {}
};
- return this.getClient(context).execute(context, t =>
+ return this.getClient(context).execute({ resourceId: context.site.id }, t =>
this._cacheService.put(context.urlTemplates.getFunctionUrl(functionName), this.jsonHeaders(t), JSON.stringify(body))
.map(r => r.json()));
}
getFunctionAppAzureAppSettings(context: FunctionAppContext) {
- return this.azure.executeWithConditions([], context, t =>
+ return this.azure.executeWithConditions([], { resourceId: context.site.id }, t =>
this._cacheService.postArm(`${context.site.id}/config/appsettings/list`, true)
.map(r => r.json() as ArmObj<{ [key: string]: string }>));
}
@@ -349,8 +250,13 @@ export class FunctionAppService {
const content = JSON.stringify({ files: filesCopy, test_data: sampleData, config: config });
const url = context.urlTemplates.getFunctionUrl(functionName);
- return this.getClient(context).executeWithConditions([], context, t =>
- this._cacheService.put(url, this.jsonHeaders(t), content).map(r => r.json() as FunctionInfo));
+ return this.getClient(context).executeWithConditions([], { resourceId: context.site.id }, t => {
+ const headers = this.jsonHeaders(t);
+ return this._cacheService.put(url, headers, content).map(r => r.json() as FunctionInfo)
+ .do(() => {
+ this._cacheService.clearCachePrefix(context.urlTemplates.scmSiteUrl);
+ });
+ });
}
statusCodeToText(code: number) {
@@ -359,7 +265,7 @@ export class FunctionAppService {
}
runHttpFunction(context: FunctionAppContext, functionInfo: FunctionInfo, url: string, model: HttpRunModel): Result {
- return this.runtime.executeWithConditions([], context, token => {
+ return this.runtime.executeWithConditions([], { resourceId: context.site.id }, token => {
const content = model.body;
const regExp = /\{([^}]+)\}/g;
@@ -468,12 +374,12 @@ export class FunctionAppService {
contentType = 'plain/text';
}
- return this.runtime.executeWithConditions([], context, t =>
+ return this.runtime.executeWithConditions([], { resourceId: context.site.id }, t =>
this.runFunctionInternal(context, this._cacheService.post(url, true, this.headers(t, ['Content-Type', contentType]), _content), functionInfo));
}
deleteFunction(context: FunctionAppContext, functionInfo: FunctionInfo): Result {
- return this.getClient(context).execute(context, t =>
+ return this.getClient(context).execute({ resourceId: context.site.id }, t =>
this._cacheService.delete(functionInfo.href, this.jsonHeaders(t)));
// .concatMap(r => this.getRuntimeGeneration())
// .concatMap((runtimeVersion: string) => {
@@ -509,27 +415,27 @@ export class FunctionAppService {
}
getHostJson(context: FunctionAppContext): Result {
- return this.getClient(context).execute(context, t =>
+ return this.getClient(context).execute({ resourceId: context.site.id }, t =>
this._cacheService.get(context.urlTemplates.hostJsonUrl, false, this.headers(t)).map(r => r.json()));
}
saveFunction(context: FunctionAppContext, fi: FunctionInfo, config: any) {
this._cacheService.clearCachePrefix(context.scmUrl);
this._cacheService.clearCachePrefix(context.mainSiteUrl);
- return this.getClient(context).execute(context, t =>
+ return this.getClient(context).execute({ resourceId: context.site.id }, t =>
this._cacheService.put(fi.href, this.jsonHeaders(t), JSON.stringify({ config: config })).map(r => r.json() as FunctionInfo));
}
getHostToken(context: FunctionAppContext) {
return ArmUtil.isLinuxApp(context.site)
- ? this.azure.executeWithConditions([], context, t =>
+ ? this.azure.executeWithConditions([], { resourceId: context.site.id }, t =>
this._cacheService.get(Constants.serviceHost + `api/runtimetoken${context.site.id}`, false, this.portalHeaders(t)))
- : this.azure.execute(context, t =>
+ : this.azure.execute({ resourceId: context.site.id }, t =>
this._cacheService.get(context.urlTemplates.scmTokenUrl, false, this.headers(t)));
}
getHostKeys(context: FunctionAppContext): Result {
- return this.runtime.execute(context, t =>
+ return this.runtime.execute({ resourceId: context.site.id }, t =>
Observable.zip(
this._cacheService.get(context.urlTemplates.adminKeysUrl, false, this.headers(t)),
this._cacheService.get(context.urlTemplates.masterKeyUrl, false, this.headers(t))
@@ -549,7 +455,7 @@ export class FunctionAppService {
getBindingConfig(context: FunctionAppContext): Result {
if (this._portalService.isEmbeddedFunctions) {
- const devBindings: BindingConfig = JSON.parse(this.testJsonBindings);
+ const devBindings: BindingConfig = JSON.parse(this._embeddedTemplates.bindingsJson);
return Observable.of({
isSuccessful: true,
result: devBindings,
@@ -573,14 +479,14 @@ export class FunctionAppService {
console.error(e);
}
- return this.azure.execute(context, t => this.getExtensionVersionFromAppSettings(context)
+ return this.azure.execute({ resourceId: context.site.id }, t => this.getExtensionVersionFromAppSettings(context)
.concatMap(extensionVersion => {
if (!extensionVersion) {
extensionVersion = 'latest';
}
const headers = this.portalHeaders(t);
- if(this._globalStateService.showTryView){
+ if (this._globalStateService.showTryView) {
headers.delete('Authorization');
}
@@ -603,25 +509,25 @@ export class FunctionAppService {
this._cacheService.clearCachePrefix(context.scmUrl);
this._cacheService.clearCachePrefix(context.mainSiteUrl);
- return this.getClient(context).execute(context, t =>
+ return this.getClient(context).execute({ resourceId: context.site.id }, t =>
this._cacheService.put(fi.href, this.jsonHeaders(t), JSON.stringify(fiCopy))
.map(r => r.json() as FunctionInfo));
}
getFunctionErrors(context: FunctionAppContext, fi: FunctionInfo, handleUnauthorized?: boolean): Result {
- return this.runtime.execute(context, t =>
+ return this.runtime.execute({ resourceId: context.site.id }, t =>
this._cacheService.get(context.urlTemplates.getFunctionRuntimeErrorsUrl(fi.name), false, this.headers(t))
.map(r => (r.json().errors || []) as string[]));
}
getHostErrors(context: FunctionAppContext): Result {
- return this.runtime.execute(context, t =>
+ return this.runtime.execute({ resourceId: context.site.id }, t =>
this._cacheService.get(context.urlTemplates.runtimeStatusUrl, true, this.headers(t))
.map(r => (r.json().errors || []) as string[]));
}
getFunctionHostStatus(context: FunctionAppContext): Result {
- return this.runtime.execute(context, t =>
+ return this.runtime.execute({ resourceId: context.site.id }, t =>
this._cacheService.get(context.urlTemplates.runtimeStatusUrl, true, this.headers(t))
.map(r => r.json() as HostStatus));
}
@@ -629,7 +535,7 @@ export class FunctionAppService {
getOldLogs(context: FunctionAppContext, fi: FunctionInfo, range: number): Result {
const url = context.urlTemplates.getFunctionLogUrl(fi.name);
- return this.getClient(context).execute(context, t =>
+ return this.getClient(context).execute({ resourceId: context.site.id }, t =>
this._cacheService.get(url, false, this.headers(t))
.concatMap(r => {
let files: VfsObject[] = r.json();
@@ -654,13 +560,13 @@ export class FunctionAppService {
getVfsObjects(context: FunctionAppContext, fi: FunctionInfo | string): Result {
const href = typeof fi === 'string' ? fi : fi.script_root_path_href;
- return this.getClient(context).execute(context, t =>
+ return this.getClient(context).execute({ resourceId: context.site.id }, t =>
this._cacheService.get(href, false, this.headers(t)).map(e => e.json()));
}
getFunctionKeys(context: FunctionAppContext, functionInfo: FunctionInfo): Result {
- return this.runtime.execute(context, t =>
+ return this.runtime.execute({ resourceId: context.site.id }, t =>
this._cacheService.get(context.urlTemplates.getFunctionKeysUrl(functionInfo.name), false, this.headers(t))
.map(r => r.json() as FunctionKeys));
}
@@ -683,7 +589,7 @@ export class FunctionAppService {
})
: null;
- return this.runtime.execute(context, t => {
+ return this.runtime.execute({ resourceId: context.site.id }, t => {
const req = body
? this._cacheService.put(url, this.jsonHeaders(t), body)
: this._cacheService.post(url, true, this.jsonHeaders(t));
@@ -701,7 +607,7 @@ export class FunctionAppService {
? context.urlTemplates.getFunctionKeyUrl(functionInfo.name, key.name)
: context.urlTemplates.getAdminKeyUrl(key.name);
- return this.runtime.execute(context, t => this._cacheService.delete(url, this.jsonHeaders(t)));
+ return this.runtime.execute({ resourceId: context.site.id }, t => this._cacheService.delete(url, this.jsonHeaders(t)));
}
renewKey(context: FunctionAppContext, key: FunctionKey, functionInfo?: FunctionInfo): Result {
@@ -711,7 +617,7 @@ export class FunctionAppService {
? context.urlTemplates.getFunctionKeyUrl(functionInfo.name, key.name)
: context.urlTemplates.getAdminKeyUrl(key.name);
- return this.runtime.execute(context, t => this._cacheService.post(url, true, this.jsonHeaders(t)));
+ return this.runtime.execute({ resourceId: context.site.id }, t => this._cacheService.post(url, true, this.jsonHeaders(t)));
}
private clearKeysCache(context: FunctionAppContext, functionInfo?: FunctionInfo) {
@@ -725,12 +631,12 @@ export class FunctionAppService {
fireSyncTrigger(context: FunctionAppContext): void {
const url = context.urlTemplates.syncTriggersUrl;
- this.azure.execute(context, t => this._cacheService.post(url, true, this.jsonHeaders(t)))
+ this.azure.execute({ resourceId: context.site.id }, t => this._cacheService.post(url, true, this.jsonHeaders(t)))
.subscribe(success => console.log(success), error => console.log(error));
}
isSourceControlEnabled(context: FunctionAppContext): Result {
- return this.azure.executeWithConditions([], context, this._cacheService.getArm(`${context.site.id}/config/web`)
+ return this.azure.executeWithConditions([], { resourceId: context.site.id }, this._cacheService.getArm(`${context.site.id}/config/web`)
.map(r => {
const config: ArmObj = r.json();
return !config.properties['scmType'] || config.properties['scmType'] !== 'None';
@@ -768,7 +674,7 @@ export class FunctionAppService {
result: [],
error: null
})
- : this.azure.executeWithConditions([], typeof context !== 'string' ? context : null, this._cacheService.getArm(`${id}/slots`)
+ : this.azure.executeWithConditions([], typeof context !== 'string' ? { resourceId: context.site.id } : null, this._cacheService.getArm(`${id}/slots`)
.map(r => r.json().value as ArmObj[]));
}
@@ -814,10 +720,10 @@ export class FunctionAppService {
// | Yes | false | undefined | ReadOnlySlots |
// |______|_______________|_________________|_______________________________|
- return this.azure.executeWithConditions([], context,
+ return this.azure.executeWithConditions([], { resourceId: context.site.id },
Observable.zip(
this.isSourceControlEnabled(context),
- this.azure.executeWithConditions([], context, this._cacheService.postArm(`${context.site.id}/config/appsettings/list`, true)),
+ this.azure.executeWithConditions([], { resourceId: context.site.id }, this._cacheService.postArm(`${context.site.id}/config/appsettings/list`, true)),
this.isSlot(context)
? Observable.of({ isSuccessful: true, result: true, error: null })
: this.getSlotsList(context).map(r => r.isSuccessful ? Object.assign(r, { result: r.result.length > 0 }) : r),
@@ -853,7 +759,7 @@ export class FunctionAppService {
}
public getAuthSettings(context: FunctionAppContext): Result {
- return this.azure.executeWithConditions([], context, this._cacheService.postArm(`${context.site.id}/config/authsettings/list`)
+ return this.azure.executeWithConditions([], { resourceId: context.site.id }, this._cacheService.postArm(`${context.site.id}/config/authsettings/list`)
.map(r => {
const auth: ArmObj = r.json();
return {
@@ -869,7 +775,7 @@ export class FunctionAppService {
* This method just pings the root of the SCM site. It doesn't care about the response in anyway or use it.
*/
pingScmSite(context: FunctionAppContext): Result {
- return this.azure.execute(context, t =>
+ return this.azure.execute({ resourceId: context.site.id }, t =>
this._cacheService.get(context.urlTemplates.pingScmSiteUrl, true, this.headers(t))
.map(_ => true)
.catch(() => Observable.of(false)));
@@ -928,36 +834,36 @@ export class FunctionAppService {
getGeneratedSwaggerData(context: FunctionAppContext, key: string): Result {
const url: string = context.urlTemplates.getGeneratedSwaggerDataUrl;
- return this.runtime.execute(context, t => this._cacheService.get(`${url}?code=${key}`, false, this.headers(t)).map(r => r.json()));
+ return this.runtime.execute({ resourceId: context.site.id }, t => this._cacheService.get(`${url}?code=${key}`, false, this.headers(t)).map(r => r.json()));
}
getSwaggerDocument(context: FunctionAppContext, key: string): Result {
const url: string = context.urlTemplates.getSwaggerDocumentUrl;
- return this.runtime.execute(context, t => this._cacheService.get(`${url}?code=${key}`, false, this.headers(t)).map(r => r.json()));
+ return this.runtime.execute({ resourceId: context.site.id }, t => this._cacheService.get(`${url}?code=${key}`, false, this.headers(t)).map(r => r.json()));
}
addOrUpdateSwaggerDocument(context: FunctionAppContext, swaggerUrl: string, content: string): Result {
- return this.runtime.execute(context, this._cacheService.post(swaggerUrl, false, this.jsonHeaders(null), content).map(r => r.json()));
+ return this.runtime.execute({ resourceId: context.site.id }, this._cacheService.post(swaggerUrl, false, this.jsonHeaders(null), content).map(r => r.json()));
}
deleteSwaggerDocument(context: FunctionAppContext, swaggerUrl: string) {
- return this.runtime.execute(context, this._cacheService.delete(swaggerUrl));
+ return this.runtime.execute({ resourceId: context.site.id }, this._cacheService.delete(swaggerUrl));
}
saveHostJson(context: FunctionAppContext, jsonString: string): Result {
- return this.getClient(context).execute(context, t =>
+ return this.getClient(context).execute({ resourceId: context.site.id }, t =>
this._cacheService.put(context.urlTemplates.hostJsonUrl, this.jsonHeaders(t, ['If-Match', '*']), jsonString).map(r => r.json()));
}
createSystemKey(context: FunctionAppContext, keyName: string) {
- return this.runtime.execute(context, t => this._cacheService.post(context.urlTemplates.getSystemKeyUrl(keyName), true, this.jsonHeaders(t, ['If-Match', '*']))
+ return this.runtime.execute({ resourceId: context.site.id }, t => this._cacheService.post(context.urlTemplates.getSystemKeyUrl(keyName), true, this.jsonHeaders(t, ['If-Match', '*']))
.map(r => r.json()));
}
// Try and the list of runtime extensions install.
// If there was an error getting the list, show an error. return an empty list.
getHostExtensions(context: FunctionAppContext): Result {
- return this.runtime.execute(context, t =>
+ return this.runtime.execute({ resourceId: context.site.id }, t =>
this._cacheService.get(context.urlTemplates.runtimeHostExtensionsUrl, false, this.headers(t))
.map(r => r.json() as FunctionKeys));
}
@@ -966,20 +872,20 @@ export class FunctionAppService {
// TODO: [soninaren] returns error object when resulted in error
// TODO: [soninaren] error.id is not defined
installExtension(context: FunctionAppContext, extension: RuntimeExtension): Result {
- return this.runtime.execute(context, t =>
+ return this.runtime.execute({ resourceId: context.site.id }, t =>
this._cacheService.post(context.urlTemplates.runtimeHostExtensionsUrl, true, this.jsonHeaders(t), extension)
.map(r => r.json() as ExtensionInstallStatus));
}
getExtensionInstallStatus(context: FunctionAppContext, jobId: string): Result {
- return this.runtime.execute(context, t =>
+ return this.runtime.execute({ resourceId: context.site.id }, t =>
this._cacheService.get(context.urlTemplates.getRuntimeHostExtensionsJobStatusUrl(jobId), true, this.headers(t))
.map(r => r.json() as ExtensionInstallStatus)
);
}
getSystemKey(context: FunctionAppContext): Result {
- return this.runtime.execute(context, t =>
+ return this.runtime.execute({ resourceId: context.site.id }, t =>
this._cacheService.get(context.urlTemplates.systemKeysUrl, false, this.headers(t))
.map(r => r.json() as FunctionKeys));
}
@@ -995,7 +901,7 @@ export class FunctionAppService {
error: null
};
} else {
- return result as any as FunctionAppHttpResult;
+ return result as any as HttpResult;
}
});
}
diff --git a/AzureFunctions.AngularClient/src/app/shared/services/log.service.ts b/AzureFunctions.AngularClient/src/app/shared/services/log.service.ts
index be3eddde3d..3dfb2412da 100644
--- a/AzureFunctions.AngularClient/src/app/shared/services/log.service.ts
+++ b/AzureFunctions.AngularClient/src/app/shared/services/log.service.ts
@@ -83,18 +83,18 @@ export class LogService {
}
}
- public log(level: LogLevel, category: string, data: any, id?: string){
- if(!id && (level === LogLevel.error || level === LogLevel.warning)){
+ public log(level: LogLevel, category: string, data: any, id?: string) {
+ if (!id && (level === LogLevel.error || level === LogLevel.warning)) {
throw Error('Error and Warning log levels require an id');
}
- if(level === LogLevel.error){
+ if (level === LogLevel.error) {
this.error(category, id, data);
- } else if(level === LogLevel.warning){
+ } else if (level === LogLevel.warning) {
this.warn(category, id, data);
- } else if(level === LogLevel.debug){
+ } else if (level === LogLevel.debug) {
this.debug(category, data);
- } else{
+ } else {
this.verbose(category, data);
}
}
@@ -102,7 +102,9 @@ export class LogService {
private _shouldLog(category: string, logLevel: LogLevel) {
if (logLevel <= this._logLevel) {
- if (this._categories.length > 0 && this._categories.find(c => c.toLowerCase() === category.toLowerCase())) {
+ if (logLevel === LogLevel.error || logLevel === LogLevel.warning) {
+ return true;
+ } else if (this._categories.length > 0 && this._categories.find(c => c.toLowerCase() === category.toLowerCase())) {
return true;
} else if (this._categories.length === 0) {
return true;
@@ -114,7 +116,7 @@ export class LogService {
return false;
}
- private _getTime(){
+ private _getTime() {
const now = new Date();
return now.toISOString();
}
diff --git a/AzureFunctions.AngularClient/src/app/shared/services/portal.service.ts b/AzureFunctions.AngularClient/src/app/shared/services/portal.service.ts
index dce6a22216..36dd743672 100644
--- a/AzureFunctions.AngularClient/src/app/shared/services/portal.service.ts
+++ b/AzureFunctions.AngularClient/src/app/shared/services/portal.service.ts
@@ -1,10 +1,10 @@
-import { Jwt } from './../Utilities/jwt';
+import { ArmServiceHelper } from './arm.service-helper';
+import { Jwt } from './../Utilities/jwt';
import { Observable } from 'rxjs/Observable';
import { Url } from './../Utilities/url';
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs/Subject';
import { ReplaySubject } from 'rxjs/ReplaySubject';
-
import { PinPartInfo, GetStartupInfo, NotificationInfo, NotificationStartedInfo, DataMessage, BladeResult, DirtyStateInfo } from './../models/portal';
import { Event, Data, Verbs, Action, LogEntryLevel, Message, UpdateBladeInfo, OpenBladeInfo, StartupInfo, TimerEvent } from '../models/portal';
import { ErrorEvent } from '../models/error-event';
@@ -24,14 +24,13 @@ export class PortalService {
private embeddedSignature = 'FunctionsEmbedded';
private acceptedSignatures = [this.portalSignature, this.portalSignatureFrameBlade, this.embeddedSignature];
- private acceptedOrigins = [
- 'https://ms.portal.azure.com',
- 'https://rc.portal.azure.com',
- 'https://portal.azure.com',
- 'https://portal.microsoftazure.de',
- 'https://portal.azure.cn',
- 'https://portal.azure.us',
- 'https://powerapps.cloudapp.net'
+ private acceptedOriginsSuffix = [
+ 'portal.azure.com',
+ 'portal.microsoftazure.de',
+ 'portal.azure.cn',
+ 'portal.azure.us',
+ 'powerapps.cloudapp.net',
+ 'web.powerapps.com'
];
private startupInfo: StartupInfo | null;
@@ -181,8 +180,7 @@ export class PortalService {
};
this.postMessage(Verbs.setNotification, JSON.stringify(payload));
- }
- else {
+ } else {
setTimeout(() => {
this.notificationStartStream.next({ id: 'id' });
});
@@ -244,7 +242,7 @@ export class PortalService {
private iframeReceivedMsg(event: Event): void {
if (!event || !event.data) {
return;
- } else if (!this.acceptedOrigins.find(o => event.origin.toLowerCase() === o.toLowerCase())) {
+ } else if (!this.acceptedOriginsSuffix.find(o => event.origin.toLowerCase().endsWith(o.toLowerCase()))) {
return;
} else if (!this.acceptedSignatures.find(s => event.data.signature !== s)) {
return;
@@ -260,6 +258,10 @@ export class PortalService {
this.sessionId = this.startupInfo.sessionId;
this._aiService.setSessionId(this.sessionId);
+ // Prefer whatever Ibiza sends us if hosted in iframe. This is mainly for national clouds
+ ArmServiceHelper.armEndpoint = this.startupInfo.armEndpoint ? this.startupInfo.armEndpoint : ArmServiceHelper.armEndpoint;
+ window.appsvc.env.azureResourceManagerEndpoint = ArmServiceHelper.armEndpoint;
+
this.startupInfoObservable.next(this.startupInfo);
this.logTokenExpiration(this.startupInfo.token, '/portal-service/token-new-startupInfo');
} else if (methodName === Verbs.sendToken) {
diff --git a/AzureFunctions.AngularClient/src/app/shared/services/scenario/national-cloud.environment.ts b/AzureFunctions.AngularClient/src/app/shared/services/scenario/national-cloud.environment.ts
index 31011a8a33..b34629d3e8 100644
--- a/AzureFunctions.AngularClient/src/app/shared/services/scenario/national-cloud.environment.ts
+++ b/AzureFunctions.AngularClient/src/app/shared/services/scenario/national-cloud.environment.ts
@@ -34,6 +34,19 @@ export class NationalCloudEnvironment extends AzureEnvironment {
}
};
+ this.scenarioChecks[ScenarioIds.enableAppInsights] = {
+ id: ScenarioIds.enableAppInsights,
+ runCheck: () => {
+ return { status: 'disabled' };
+ }
+ };
+
+ this.scenarioChecks[ScenarioIds.addMsi] = {
+ id: ScenarioIds.addMsi,
+ runCheck: () => {
+ return { status: 'disabled' };
+ }
+ };
}
public isCurrentEnvironment(input?: ScenarioCheckInput): boolean {
diff --git a/AzureFunctions.AngularClient/src/app/shared/services/scenario/onprem.environment.ts b/AzureFunctions.AngularClient/src/app/shared/services/scenario/onprem.environment.ts
index 0c2099422f..16f53ea4e1 100644
--- a/AzureFunctions.AngularClient/src/app/shared/services/scenario/onprem.environment.ts
+++ b/AzureFunctions.AngularClient/src/app/shared/services/scenario/onprem.environment.ts
@@ -14,6 +14,12 @@ export class OnPremEnvironment extends Environment {
}
};
+ this.scenarioChecks[ScenarioIds.openOldWebhostingPlanBlade] = {
+ id: ScenarioIds.openOldWebhostingPlanBlade,
+ runCheck: () => {
+ return { status: 'enabled' };
+ }
+ };
}
public isCurrentEnvironment(input?: ScenarioCheckInput): boolean {
diff --git a/AzureFunctions.AngularClient/src/app/shared/services/site.service.ts b/AzureFunctions.AngularClient/src/app/shared/services/site.service.ts
new file mode 100644
index 0000000000..7a9261db9c
--- /dev/null
+++ b/AzureFunctions.AngularClient/src/app/shared/services/site.service.ts
@@ -0,0 +1,46 @@
+import { Injectable, Injector } from '@angular/core';
+import { ConditionalHttpClient } from 'app/shared/conditional-http-client';
+import { UserService } from 'app/shared/services/user.service';
+import { CacheService } from 'app/shared/services/cache.service';
+import { ArmSiteDescriptor } from 'app/shared/resourceDescriptors';
+import { Observable } from 'rxjs/Observable';
+import { HttpResult } from './../models/http-result';
+import { ArmObj } from 'app/shared/models/arm/arm-obj';
+import { Site } from 'app/shared/models/arm/site';
+import { SlotConfigNames } from 'app/shared/models/arm/slot-config-names';
+
+type Result = Observable>;
+
+@Injectable()
+export class SiteService {
+ private readonly _client: ConditionalHttpClient;
+
+ constructor(
+ userService: UserService,
+ injector: Injector,
+ private _cacheService: CacheService) {
+
+ this._client = new ConditionalHttpClient(injector, _ => userService.getStartupInfo().map(i => i.token))
+ }
+
+ getSite(resourceId: string): Result> {
+ const getSite = this._cacheService.getArm(resourceId).map(r => r.json());
+ return this._client.execute({ resourceId: resourceId}, t => getSite);
+ }
+
+ getAppSettings(resourceId: string): Result> {
+
+ const getAppSettings = this._cacheService.postArm(`${resourceId}/config/appSettings/list`, true)
+ .map(r => r.json());
+
+ return this._client.execute({ resourceId: resourceId }, t => getAppSettings);
+ }
+
+ getSlotConfigNames(resourceId: string): Result> {
+ const slotsConfigNamesId = `${new ArmSiteDescriptor(resourceId).getSiteOnlyResourceId()}/config/slotConfigNames`;
+ const getSlotConfigNames = this._cacheService.getArm(slotsConfigNamesId, true)
+ .map(r => r.json());
+
+ return this._client.execute( { resourceId: resourceId }, t => getSlotConfigNames);
+ }
+}
diff --git a/AzureFunctions.AngularClient/src/app/shared/services/telemetry.service.ts b/AzureFunctions.AngularClient/src/app/shared/services/telemetry.service.ts
new file mode 100644
index 0000000000..5225a0541a
--- /dev/null
+++ b/AzureFunctions.AngularClient/src/app/shared/services/telemetry.service.ts
@@ -0,0 +1,110 @@
+import { LogCategories } from './../models/constants';
+import { LogService } from 'app/shared/services/log.service';
+import { PortalService } from './portal.service';
+import { Observable } from 'rxjs/Observable';
+import { Injectable } from '@angular/core';
+
+interface ComponentMap {
+ [key: string]: string;
+}
+
+@Injectable()
+export class TelemetryService {
+
+ // Keeps track of which features are currently being loaded.
+ private _featureMap: { [key: string]: ComponentMap } = {};
+
+ // Keeps track to make sure that no feature gets logged without a parent
+ // being logged first. This is to help enforce that someone writing a new
+ // feature properly defines a parent component
+ private _registeredParentFeatures: { [key: string]: string } = {};
+
+ private readonly _loadingIdFormat = '/feature/{0}/loading';
+ private readonly _debouceTimeMs = 250;
+
+ constructor(
+ private _portalService: PortalService,
+ private _logService: LogService) {
+ }
+
+ public featureLoading(isParentComponent: boolean, featureName: string, componentName: string) {
+ if (!this._featureMap[featureName]) {
+
+ if (isParentComponent) {
+ this._registeredParentFeatures[featureName] = featureName;
+ this._featureMap[featureName] = {};
+
+ this._logService.verbose(LogCategories.telemetry, `Feature loading started. feature: ${featureName}`);
+
+ this._portalService.sendTimerEvent({
+ timerId: this._loadingIdFormat.format(featureName),
+ timerAction: 'start'
+ });
+ } else if (!this._registeredParentFeatures[featureName]) {
+
+ // There needs to be one parent component which represents timing for the entire feature.
+ // Otherwise if one child component is used independently of a parent component and start/stops
+ // it's timer, our load times would be completely off.
+ this._logService.error(
+ LogCategories.telemetry,
+ '/no-parent-component-defined',
+ `No parentComponent defined for feature: ${featureName}, component: ${componentName}.
+ One parent component must be defined for telemetry to work properly.`);
+ return;
+ } else {
+
+ // This can happen if a feature has already been loaded, but there's child components that get created
+ // after the initial loading
+ this._logService.verbose(
+ LogCategories.telemetry,
+ `A child component started after feature load complete. feature: ${featureName}, component: ${componentName}`
+ );
+ return;
+ }
+ }
+
+ this._logService.verbose(
+ LogCategories.telemetry,
+ `Loading feature: ${featureName}, component: ${componentName}`);
+
+ this._featureMap[featureName][componentName] = componentName;
+ }
+
+ public featureLoadingComplete(featureName: string, componentName: string) {
+ if (!this._featureMap[featureName] || !this._featureMap[featureName][componentName]) {
+ return;
+ }
+
+ delete this._featureMap[featureName][componentName];
+
+ this._logService.verbose(
+ LogCategories.telemetry,
+ `Component is done loading. feature: ${featureName}, component: ${componentName}`);
+
+ if (Object.keys(this._featureMap[featureName]).length === 0) {
+
+ this._logService.verbose(
+ LogCategories.telemetry,
+ `All components should be complete for: ${featureName}. Debouncing for ${this._debouceTimeMs}ms`);
+
+ // "Debouncing" any straggling signals. We need to do this it's possible that on completion of
+ // a parent component, new child components may be created from the result of conditional statements
+ // becoming true. In that case, the child components will start to emit feature loading events, so we
+ // need to wait for things to settle down.
+ Observable.timer(this._debouceTimeMs)
+ .subscribe(() => {
+ if (this._featureMap[featureName] && Object.keys(this._featureMap[featureName]).length === 0) {
+
+ this._logService.verbose(LogCategories.telemetry, `Completing load for feature: ${featureName}`);
+ delete this._featureMap[featureName];
+ this._portalService.sendTimerEvent({
+ timerId: this._loadingIdFormat.format(featureName),
+ timerAction: 'stop'
+ });
+ } else {
+ this._logService.verbose(LogCategories.telemetry, `New loading signals detected for feature: ${featureName}`);
+ }
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/AzureFunctions.AngularClient/src/app/shared/services/user.service.ts b/AzureFunctions.AngularClient/src/app/shared/services/user.service.ts
index 874a277e55..437ba4af64 100644
--- a/AzureFunctions.AngularClient/src/app/shared/services/user.service.ts
+++ b/AzureFunctions.AngularClient/src/app/shared/services/user.service.ts
@@ -138,7 +138,8 @@ export class UserService {
effectiveLocale: this._startupInfo.effectiveLocale,
resourceId: this._startupInfo.resourceId,
stringResources: r.resources,
- theme: this._startupInfo.theme
+ theme: this._startupInfo.theme,
+ armEndpoint: this._startupInfo.armEndpoint
};
this.updateStartupInfo(info);
diff --git a/AzureFunctions.AngularClient/src/app/shared/shared.module.ts b/AzureFunctions.AngularClient/src/app/shared/shared.module.ts
index c05df508b6..63f2725056 100644
--- a/AzureFunctions.AngularClient/src/app/shared/shared.module.ts
+++ b/AzureFunctions.AngularClient/src/app/shared/shared.module.ts
@@ -1,3 +1,4 @@
+import { TelemetryService } from './services/telemetry.service';
import { PortalService } from 'app/shared/services/portal.service';
import { Injector } from '@angular/core';
import { TabComponent } from './../controls/tabs/tab/tab.component';
@@ -47,7 +48,7 @@ import { ArmTryService } from './services/arm-try.service';
import { AiService } from './services/ai.service';
import { UserService } from './services/user.service';
import { Http } from '@angular/http';
-import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { FormsModule, ReactiveFormsModule, FormBuilder } from '@angular/forms';
import { CommonModule } from '@angular/common';
import { NgModule, ModuleWithProviders, ErrorHandler } from '@angular/core';
import { TranslateModule } from '@ngx-translate/core';
@@ -61,6 +62,8 @@ import { TableRowComponent } from './../controls/table-row/table-row.component';
import { TableRootComponent } from './../controls/table-root/table-root.component';
import { DeletedItemsFilter } from './../controls/table-root/deleted-items-filter.pipe';
import { ActivateWithKeysDirective } from './../controls/activate-with-keys/activate-with-keys.directive';
+import { EmbeddedService } from 'app/shared/services/embedded.service';
+import { SiteService } from 'app/shared/services/site.service';
export function ArmServiceFactory(
http: Http,
@@ -177,6 +180,7 @@ export class SharedModule {
PortalService,
BroadcastService,
FunctionMonitorService,
+ FormBuilder,
LogService,
{
provide: ArmService, useFactory: ArmServiceFactory, deps: [
@@ -195,6 +199,9 @@ export class SharedModule {
UtilitiesService,
BackgroundTasksService,
GlobalStateService,
+ EmbeddedService,
+ SiteService,
+ TelemetryService,
{ provide: AiService, useFactory: AiServiceFactory },
{ provide: ErrorHandler, useClass: GlobalErrorHandler }
]
diff --git a/AzureFunctions.AngularClient/src/app/side-nav/side-nav.component.ts b/AzureFunctions.AngularClient/src/app/side-nav/side-nav.component.ts
index 539e0dd214..3ea5cde59b 100644
--- a/AzureFunctions.AngularClient/src/app/side-nav/side-nav.component.ts
+++ b/AzureFunctions.AngularClient/src/app/side-nav/side-nav.component.ts
@@ -276,6 +276,7 @@ export class SideNavComponent implements AfterViewInit, OnDestroy {
}
this.selectedNode.handleDeselection(newSelectedNode);
+ this.selectedNode.showMenu = false;
}
}
@@ -284,6 +285,7 @@ export class SideNavComponent implements AfterViewInit, OnDestroy {
this.selectedNode = newSelectedNode;
this.selectedDashboardType = newDashboardType;
this.resourceId = newSelectedNode.resourceId; // TODO: should this be updated to resourceId passed in or is this fine?
+ this.selectedNode.showMenu = true;
const viewInfo = >{
resourceId: resourceId,
diff --git a/AzureFunctions.AngularClient/src/app/site/create-app/create-app.component.ts b/AzureFunctions.AngularClient/src/app/site/create-app/create-app.component.ts
index 64dd8f2597..a05a763f23 100644
--- a/AzureFunctions.AngularClient/src/app/site/create-app/create-app.component.ts
+++ b/AzureFunctions.AngularClient/src/app/site/create-app/create-app.component.ts
@@ -22,7 +22,6 @@ import { BroadcastEvent } from 'app/shared/models/broadcast-event';
import { DashboardType } from 'app/tree-view/models/dashboard-type';
import { ErrorableComponent } from 'app/shared/components/errorable-component';
-
@Component({
selector: 'create-app',
templateUrl: './create-app.component.html',
@@ -47,42 +46,38 @@ export class CreateAppComponent extends ErrorableComponent implements OnInit, On
_fb: FormBuilder,
private _aiService: AiService,
userService: UserService,
- injector: Injector) {
+ injector: Injector
+ ) {
super('create-app', broadcastService);
- userService.getStartupInfo()
- .first()
- .subscribe(info => {
- const subs = info.subscriptions;
- let defaultSubId: string;
- subs.forEach(sub => {
- if (sub.state === 'Enabled') {
- this.subscriptionOptions.push({
- displayLabel: `${sub.displayName}(${sub.subscriptionId})`,
- value: sub.subscriptionId
- });
- if (!defaultSubId) {
- defaultSubId = sub.subscriptionId;
- }
+ userService.getStartupInfo().first().subscribe(info => {
+ const subs = info.subscriptions;
+ let defaultSubId: string;
+ subs.forEach(sub => {
+ if (sub.state === 'Enabled') {
+ this.subscriptionOptions.push({
+ displayLabel: `${sub.displayName}(${sub.subscriptionId})`,
+ value: sub.subscriptionId
+ });
+ if (!defaultSubId) {
+ defaultSubId = sub.subscriptionId;
}
- });
- const sub = subs.find(s => s.state === 'Enabled');
- if (!sub) {
- return;
}
-
- const required = new RequiredValidator(this._translateService);
- const siteNameValidator = new SiteNameValidator(injector, sub.subscriptionId);
-
- this.group = _fb.group({
- name: [
- null,
- required.validate.bind(required),
- siteNameValidator.validate.bind(siteNameValidator)],
- subscription: defaultSubId,
- runtimeImage: RuntimeImage.v2
- });
});
+ const sub = subs.find(s => s.state === 'Enabled');
+ if (!sub) {
+ return;
+ }
+
+ const required = new RequiredValidator(this._translateService);
+ const siteNameValidator = new SiteNameValidator(injector, sub.subscriptionId);
+
+ this.group = _fb.group({
+ name: [null, required.validate.bind(required), siteNameValidator.validate.bind(siteNameValidator)],
+ subscription: defaultSubId,
+ runtimeImage: RuntimeImage.v2
+ });
+ });
this.runtimeImageOptions.push({
displayLabel: this._translateService.instant(PortalResources.runtimeImagev1),
@@ -101,19 +96,19 @@ export class CreateAppComponent extends ErrorableComponent implements OnInit, On
*/
this.viewInfoStream = new Subject>();
- this.viewInfoStream
- .subscribe(viewInfo => {
- this._viewInfo = viewInfo;
- });
-
+ this.viewInfoStream.subscribe(viewInfo => {
+ this._viewInfo = viewInfo;
+ });
}
- @Input() set viewInfoInput(viewInfo: TreeViewInfo) {
+ @Input()
+ set viewInfoInput(viewInfo: TreeViewInfo) {
this.viewInfoStream.next(viewInfo);
}
ngOnInit() {
- this._broadcastService.getEvents>(BroadcastEvent.TreeNavigation)
+ this._broadcastService
+ .getEvents>(BroadcastEvent.TreeNavigation)
.filter(info => {
return info.dashboardType === DashboardType.createApp;
})
@@ -136,7 +131,8 @@ export class CreateAppComponent extends ErrorableComponent implements OnInit, On
return;
}
- const id = `/subscriptions/${this.group.controls['subscription'].value}/resourceGroups/StandaloneResourceGroup/providers/Microsoft.Web/sites/${name}`;
+ const id = `/subscriptions/${this.group.controls['subscription']
+ .value}/resourceGroups/StandaloneResourceGroup/providers/Microsoft.Web/sites/${name}`;
const body = {
properties: {
@@ -152,13 +148,14 @@ export class CreateAppComponent extends ErrorableComponent implements OnInit, On
};
this._globalStateService.setBusyState();
- this._cacheService.putArm(id, null, body)
- .subscribe(r => {
+ this._cacheService.putArm(id, null, body).subscribe(
+ r => {
this._globalStateService.clearBusyState();
const siteObj: ArmObj = r.json();
const appsNode: AppsNode = this._viewInfo.node;
appsNode.addChild(siteObj);
- }, error => {
+ },
+ error => {
this._globalStateService.clearBusyState();
this.showComponentError({
@@ -169,6 +166,7 @@ export class CreateAppComponent extends ErrorableComponent implements OnInit, On
});
this._aiService.trackEvent(errorIds.failedToCreateApp, { error: error, id: id });
- });
+ }
+ );
}
}
diff --git a/AzureFunctions.AngularClient/src/app/site/deployment-center/Models/Deployment-enums.ts b/AzureFunctions.AngularClient/src/app/site/deployment-center/Models/Deployment-enums.ts
new file mode 100644
index 0000000000..ad2215a2de
--- /dev/null
+++ b/AzureFunctions.AngularClient/src/app/site/deployment-center/Models/Deployment-enums.ts
@@ -0,0 +1,31 @@
+export enum VSTSLogMessageType {
+ Deployment = 0,
+ SlotSwap = 1,
+ CDDeploymentConfiguration = 2,
+ CDSlotCreation = 3,
+ CDTestWebAppCreation = 4,
+ CDAccountCreated = 5,
+ CDDisconnect = 6,
+ StartAzureAppService = 7,
+ StopAzureAppService = 8,
+ RestartAzureAppService = 9,
+ Other = 10,
+ Sync = 11,
+ LocalGitCdConfiguration = 12
+}
+
+export enum ProviderType {
+ None = 0,
+ VSTS = 1,
+ GitHub = 2,
+ BitbucketGit = 3,
+ Tfs = 4,
+ ExternalGit = 5,
+ LocalGit = 6
+}
+
+export enum ScmType {
+ None = 0,
+ VSTSRM = 1,
+ Kudu = 2
+}
diff --git a/AzureFunctions.AngularClient/src/app/site/deployment-center/Models/deployment-data.ts b/AzureFunctions.AngularClient/src/app/site/deployment-center/Models/deployment-data.ts
new file mode 100644
index 0000000000..2e9b744b0e
--- /dev/null
+++ b/AzureFunctions.AngularClient/src/app/site/deployment-center/Models/deployment-data.ts
@@ -0,0 +1,37 @@
+import { PublishingCredentials } from '../../../shared/models/publishing-credentials';
+import { SiteConfig } from '../../../shared/models/arm/site-config';
+import { Site } from '../../../shared/models/arm/site';
+import { ArmArrayResult, ArmObj } from '../../../shared/models/arm/arm-obj';
+export class DeploymentData {
+ site: ArmObj;
+ siteConfig: ArmObj;
+ siteMetadata: ArmObj;
+ deployments: ArmArrayResult;
+ publishingCredentials: ArmObj;
+ sourceControls: ArmObj;
+ publishingUser: ArmObj<{
+ publishingUserName: string;
+ }>;
+}
+
+export interface Deployment {
+ id: string;
+ status: number;
+ status_text: string;
+ author_email: string;
+ author: string;
+ deployer: string;
+ message: string;
+ progress: string;
+ received_time: Date;
+ start_time: Date;
+ end_time: Date;
+ last_success_end_time: Date;
+ complete: boolean;
+ active: boolean;
+ is_temp: boolean;
+ is_readonly: boolean;
+ url: string;
+ log_url: string;
+ site_name: string;
+}
diff --git a/AzureFunctions.AngularClient/src/app/site/deployment-center/Models/summary-item.ts b/AzureFunctions.AngularClient/src/app/site/deployment-center/Models/summary-item.ts
new file mode 100644
index 0000000000..43168362fc
--- /dev/null
+++ b/AzureFunctions.AngularClient/src/app/site/deployment-center/Models/summary-item.ts
@@ -0,0 +1,4 @@
+export interface summaryItem {
+ name: string;
+ value: string;
+}
diff --git a/AzureFunctions.AngularClient/src/app/site/deployment-center/Models/vso-build-models.ts b/AzureFunctions.AngularClient/src/app/site/deployment-center/Models/vso-build-models.ts
new file mode 100644
index 0000000000..6015f76828
--- /dev/null
+++ b/AzureFunctions.AngularClient/src/app/site/deployment-center/Models/vso-build-models.ts
@@ -0,0 +1,90 @@
+export interface RespositoryProperties {
+ connectedServiceId: string;
+ apiUrl: string;
+ branchesUrl: string;
+ cloneUrl: string;
+ refsUrl: string;
+}
+
+export interface Repository {
+ properties: RespositoryProperties;
+ id: string;
+ type: string;
+ url: string;
+ defaultBranch: string;
+ clean: string;
+ checkoutSubmodules: boolean;
+}
+
+export interface AuthoredBy {
+ id: string;
+ displayName: string;
+ uniqueName: string;
+ url: string;
+ imageUrl: string;
+}
+
+export interface Project {
+ id: string;
+ name: string;
+ description: string;
+ url: string;
+ state: string;
+ revision: number;
+ visibility: string;
+}
+
+export interface VSOBuildDefinition {
+ repository: Repository;
+ authoredBy: AuthoredBy;
+ url: string;
+ id: number;
+ name: string;
+ path: string;
+ type: string;
+ revision: number;
+ createdDate: Date;
+ project: Project;
+}
+
+export interface UrlInfo {
+ urlIcon?: string;
+ urlText: string;
+ url: string;
+}
+
+export interface ActivityDetailsLog {
+ type: string;
+ id: string;
+ icon: string;
+ message: string;
+ date: string;
+ time: string;
+ urlInfo: UrlInfo[];
+}
+
+export interface KuduLogMessage {
+ type: string;
+ commitId?: string;
+ buildId?: number;
+ releaseId?: number;
+ buildNumber?: string;
+ releaseName?: string;
+ repoProvider?: string;
+ repoName?: string;
+ collectionUrl?: string;
+ teamProject?: string;
+ prodAppName?: string;
+ slotName?: string;
+ sourceSlot?: string;
+ targetSlot?: string;
+ message?: string;
+ VSTSRM_BuildDefinitionWebAccessUrl?: string;
+ VSTSRM_ConfiguredCDEndPoint?: string;
+ VSTSRM_BuildWebAccessUrl?: string;
+ AppUrl?: string;
+ SlotUrl?: string;
+ VSTSRM_AccountUrl?: string;
+ VSTSRM_RepoUrl?: string;
+ VSTSRM_AccountId?: string;
+}
diff --git a/AzureFunctions.AngularClient/src/app/site/deployment-center/Models/vso-repo.ts b/AzureFunctions.AngularClient/src/app/site/deployment-center/Models/vso-repo.ts
new file mode 100644
index 0000000000..9d531a838c
--- /dev/null
+++ b/AzureFunctions.AngularClient/src/app/site/deployment-center/Models/vso-repo.ts
@@ -0,0 +1,12 @@
+export interface VSORepo {
+ remoteUrl: string;
+ name: string;
+ project: { name: string };
+ id: string;
+ account: string;
+}
+
+export interface VSOAccount {
+ isAccountOwner: boolean;
+ accountName: string;
+}
diff --git a/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/deployment-center-setup.component.html b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/deployment-center-setup.component.html
new file mode 100644
index 0000000000..744d6121db
--- /dev/null
+++ b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/deployment-center-setup.component.html
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/deployment-center-setup.component.scss b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/deployment-center-setup.component.scss
new file mode 100644
index 0000000000..70f9fd4ac3
--- /dev/null
+++ b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/deployment-center-setup.component.scss
@@ -0,0 +1,10 @@
+@import '../../../../sass/main';
+
+.centered-content {
+ margin: auto;
+ text-align: center;
+}
+
+.form-button {
+ margin-top: 12px;
+}
diff --git a/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/deployment-center-setup.component.spec.ts b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/deployment-center-setup.component.spec.ts
new file mode 100644
index 0000000000..b7ddbb61a1
--- /dev/null
+++ b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/deployment-center-setup.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { DeploymentCenterSetupComponent } from './deployment-center-setup.component';
+
+describe('DeploymentCenterSetupComponent', () => {
+ let component: DeploymentCenterSetupComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ DeploymentCenterSetupComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(DeploymentCenterSetupComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should be created', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/deployment-center-setup.component.ts b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/deployment-center-setup.component.ts
new file mode 100644
index 0000000000..ba125280b9
--- /dev/null
+++ b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/deployment-center-setup.component.ts
@@ -0,0 +1,99 @@
+import { Component, Input, SimpleChanges } from '@angular/core';
+import { DeploymentCenterStateManager } from 'app/site/deployment-center/deployment-center-setup/wizard-logic/deployment-center-state-manager';
+import { FormBuilder, Validators } from '@angular/forms';
+import { sourceControlProvider } from 'app/site/deployment-center/deployment-center-setup/wizard-logic/deployment-center-setup-models';
+import { OnChanges } from '@angular/core/src/metadata/lifecycle_hooks';
+import { TranslateService } from '@ngx-translate/core';
+
+@Component({
+ selector: 'app-deployment-center-setup',
+ templateUrl: './deployment-center-setup.component.html',
+ styleUrls: ['./deployment-center-setup.component.scss'],
+ providers: [DeploymentCenterStateManager]
+})
+export class DeploymentCenterSetupComponent implements OnChanges {
+ @Input() resourceId: string;
+
+ constructor(private _wizardService: DeploymentCenterStateManager, private _fb: FormBuilder, translateService: TranslateService) {
+ this._wizardService.wizardForm = this._fb.group({
+ sourceProvider: ['', []],
+ buildProvider: ['kudu', []],
+ sourceSettings: this._fb.group({
+ repoUrl: ['', [Validators.required]],
+ branch: ['', []],
+ isManualIntegration: [false, []],
+ deploymentRollbackEnabled: [false, []],
+ isMercurial: [false, []]
+ }),
+ vstsBuildSettings: this._fb.group({
+ createNewVsoAccount: ['', []],
+ vstsAccount: ['', []],
+ vstsProject: ['', []],
+ location: ['', []],
+ applicationFramework: ['', []],
+ testEnvironment: this._fb.group({
+ enabled: ['', []],
+ AppServicePlanId: ['', []],
+ AppName: ['', []]
+ }),
+ deploymentSlot: ['', []]
+ })
+ });
+ }
+
+ get showTestStep() {
+ const buildProvider: sourceControlProvider =
+ this._wizardService &&
+ this._wizardService.wizardForm &&
+ this._wizardService.wizardForm.controls['buildProvider'] &&
+ this._wizardService.wizardForm.controls['buildProvider'].value;
+ return buildProvider === 'vsts';
+ }
+
+ get showDeployStep() {
+ const buildProvider: sourceControlProvider =
+ this._wizardService &&
+ this._wizardService.wizardForm &&
+ this._wizardService.wizardForm.controls['buildProvider'] &&
+ this._wizardService.wizardForm.controls['buildProvider'].value;
+ return buildProvider === 'vsts';
+ }
+
+ get showBuildStep() {
+ return false;
+ // const sourceControlProvider =
+ // this._wizardService &&
+ // this._wizardService.wizardForm &&
+ // this._wizardService.wizardForm.controls['sourceProvider'] &&
+ // this._wizardService.wizardForm.controls['sourceProvider'].value;
+ // return (
+ // sourceControlProvider !== 'onedrive' &&
+ // sourceControlProvider !== 'dropbox' &&
+ // sourceControlProvider !== 'bitbucket' &&
+ // sourceControlProvider !== 'ftp' &&
+ // sourceControlProvider !== 'webdeploy'
+ // );
+ }
+
+ get showConfigureStep() {
+ const sourceControlProvider: sourceControlProvider =
+ this._wizardService &&
+ this._wizardService.wizardForm &&
+ this._wizardService.wizardForm.controls['sourceProvider'] &&
+ this._wizardService.wizardForm.controls['sourceProvider'].value;
+
+ const buildProvider: sourceControlProvider =
+ this._wizardService &&
+ this._wizardService.wizardForm &&
+ this._wizardService.wizardForm.controls['buildProvider'] &&
+ this._wizardService.wizardForm.controls['buildProvider'].value;
+ const localGitKudu = sourceControlProvider === 'localgit' && buildProvider === 'kudu';
+ return sourceControlProvider !== 'ftp' && sourceControlProvider !== 'webdeploy' && !localGitKudu;
+ }
+
+ public ngOnChanges(changes: SimpleChanges): void {
+ if (changes['resourceId']) {
+ this._wizardService.resourceIdStream.next(this.resourceId);
+ }
+ }
+}
diff --git a/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-build-provider/step-build-provider.component.html b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-build-provider/step-build-provider.component.html
new file mode 100644
index 0000000000..6cfe9db1b1
--- /dev/null
+++ b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-build-provider/step-build-provider.component.html
@@ -0,0 +1,15 @@
+
+
+
+
+ {{card.description}}
+
+
+
+
\ No newline at end of file
diff --git a/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-build-provider/step-build-provider.component.scss b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-build-provider/step-build-provider.component.scss
new file mode 100644
index 0000000000..b3f437e179
--- /dev/null
+++ b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-build-provider/step-build-provider.component.scss
@@ -0,0 +1,60 @@
+:host {
+ width: 100%;
+ height: 100%;
+}
+
+.selectionContainer {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-direction: row;
+ -webkit-flex-wrap: wrap;
+ flex-wrap: wrap;
+ width: 80%;
+ margin-left: auto;
+ margin-right: auto;
+}
+.cardContainer {
+ background: #fff;
+ border-radius: 2px;
+ height: 200px;
+ margin: 1rem;
+ position: relative;
+ width: 300px;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
+ transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
+ &:hover {
+ box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
+ }
+ &:active {
+ box-shadow: none;
+ }
+}
+
+.sourceDescription {
+ text-align: left;
+ width: 100%;
+ height: 160px;
+ vertical-align: middle;
+ display: inline-block;
+}
+
+.headerBox {
+ width: 100%;
+ height: 40px;
+ img {
+ display: inline-block;
+ height: 40px;
+ width: 40px;
+ padding-right: 10px;
+ }
+}
+
+.footer {
+ position: fixed;
+ height: 50px;
+ margin-bottom: 0px;
+ bottom: 0px;
+ width: 100%;
+ box-shadow: 0px -5px 13px -1px rgba(0, 0, 0, 0.38);
+}
\ No newline at end of file
diff --git a/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-build-provider/step-build-provider.component.spec.ts b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-build-provider/step-build-provider.component.spec.ts
new file mode 100644
index 0000000000..5a00c660ad
--- /dev/null
+++ b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-build-provider/step-build-provider.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { StepBuildProviderComponent } from './step-build-provider.component';
+
+describe('StepBuildProviderComponent', () => {
+ let component: StepBuildProviderComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ StepBuildProviderComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(StepBuildProviderComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should be created', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-build-provider/step-build-provider.component.ts b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-build-provider/step-build-provider.component.ts
new file mode 100644
index 0000000000..076d340f48
--- /dev/null
+++ b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-build-provider/step-build-provider.component.ts
@@ -0,0 +1,41 @@
+import { Component } from '@angular/core';
+import { ProviderCard } from 'app/site/deployment-center/deployment-center-setup/step-source-control/step-source-control.component';
+import { DeploymentCenterStateManager } from 'app/site/deployment-center/deployment-center-setup/wizard-logic/deployment-center-state-manager';
+
+@Component({
+ selector: 'app-step-build-provider',
+ templateUrl: './step-build-provider.component.html',
+ styleUrls: ['./step-build-provider.component.scss']
+})
+export class StepBuildProviderComponent {
+ public readonly providerCards: ProviderCard[] = [
+ {
+ id: 'vsts',
+ name: 'VSTS build server',
+ icon: 'image/deployment-center/onedrive-logo.svg',
+ color: '#68227A',
+ barColor: '#CED2EA',
+ description:
+ 'Use VSTS as the build server. You can choose to leverage advanced options for a full release management workflow.',
+ authorizedStatus: 'none'
+ },
+ {
+ id: 'kudu',
+ name: 'App Service Kudu build server',
+ icon: 'image/deployment-center/onedrive-logo.svg',
+ color: '#000000',
+ barColor: '#D6D6D6',
+ description:
+ 'Use App Service as the build server. The App Service Kudu engine will automatically build your code during deployment when applicable with no additional configuration required.',
+ authorizedStatus: 'none'
+ }
+ ];
+
+ public selectedProvider: ProviderCard = null;
+
+ constructor(public wizard: DeploymentCenterStateManager) {}
+
+ chooseBuildProvider(card: ProviderCard) {
+ this.wizard.wizardForm.controls['buildProvider'].setValue(card.id, { onlySelf: true });
+ }
+}
diff --git a/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-complete/step-complete.component.html b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-complete/step-complete.component.html
new file mode 100644
index 0000000000..4df787dbb5
--- /dev/null
+++ b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-complete/step-complete.component.html
@@ -0,0 +1,19 @@
+
+
+
Summary
+
+
+
+
{{item.name}}
+
+
+ {{item.value}}
+
+
+
+
+
+
diff --git a/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-complete/step-complete.component.scss b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-complete/step-complete.component.scss
new file mode 100644
index 0000000000..086cc3e0f4
--- /dev/null
+++ b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-complete/step-complete.component.scss
@@ -0,0 +1,152 @@
+@import '../../../../../sass/common/variables';
+
+:host {
+ width: 100%;
+ height: 100%;
+}
+
+.centered-content {
+ margin: auto;
+ text-align: center;
+}
+
+.footer {
+ position: fixed;
+ height: 50px;
+ margin-bottom: 0px;
+ bottom: 0px;
+ width: 100%;
+ box-shadow: 0px -5px 13px -1px rgba(0, 0, 0, 0.38);
+}
+
+#site-config-wrapper {
+ padding: 5px 20px;
+}
+
+th {
+ height: 0px;
+}
+
+td {
+ span {
+ font-size: 16px;
+
+ &:hover {
+ color: $error-color;
+ }
+ }
+
+ span.delete:hover {
+ color: $error-color;
+ }
+}
+
+.nameCol {
+ width: 30%;
+}
+
+.valueCol {
+ width: 65%;
+}
+
+.typeCol {
+ width: 200px;
+}
+
+.actionCol {
+ width: 25px;
+}
+
+.add-setting {
+ margin-top: 10px;
+ margin-left: 5px;
+ display: inline-block;
+}
+
+h3 {
+ font-weight: bold;
+ margin-left: 0px;
+ margin-bottom: 10px;
+ margin-top: 35px;
+ padding-bottom: 10px;
+ display: inline-block;
+
+ &.first-config-heading {
+ margin-top: 10px;
+ }
+}
+
+.shield {
+ position: absolute;
+ left: 0;
+ right: 0;
+ top: 0;
+ bottom: 0;
+ z-index: 10;
+ display: flex;
+ background: rgba(255, 255, 255, 0.60);
+
+ .shield-message {
+ padding-top: 15px;
+ padding-bottom: 15px;
+ margin-left: 25px;
+ margin-right: 25px;
+ margin-top: auto;
+ margin-bottom: auto;
+ width: 100%;
+ border: 1px solid black;
+ background: rgba(0, 0, 0, 0.60);
+ color: white;
+ text-align: center;
+ }
+}
+
+.settings-group-wrapper {
+ position: relative;
+ padding: 10px;
+}
+
+.settings-wrapper {
+ margin-top: 10px;
+ margin-bottom: 10px;
+ padding-left: 10px;
+ position: relative;
+
+ .settings-load-message {
+ padding-top: 15px;
+ padding-bottom: 15px;
+ margin-top: auto;
+ margin-bottom: auto;
+ margin-left: 15px;
+ margin-right: 25px;
+ border: 1px solid black;
+ background: rgba(0, 0, 0, 0.60);
+ text-align: center;
+ }
+
+ .setting-wrapper {
+ width: 535px;
+ padding-bottom: 15px;
+
+ &.last {
+ padding-bottom: 0px;
+ }
+
+ .setting-label {
+ box-sizing: border-box;
+ display: inline-block;
+ min-height: 20px;
+ width: 32%;
+ }
+
+ .setting-control-container {
+ display: inline-block;
+ vertical-align: middle;
+ width: 66%;
+ }
+ }
+}
+
+.transparent {
+ background: transparent;
+}
diff --git a/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-complete/step-complete.component.spec.ts b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-complete/step-complete.component.spec.ts
new file mode 100644
index 0000000000..76485b0a02
--- /dev/null
+++ b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-complete/step-complete.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { StepCompleteComponent } from './step-complete.component';
+
+describe('StepCompleteComponent', () => {
+ let component: StepCompleteComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ StepCompleteComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(StepCompleteComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should be created', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-complete/step-complete.component.ts b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-complete/step-complete.component.ts
new file mode 100644
index 0000000000..070fee67e7
--- /dev/null
+++ b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-complete/step-complete.component.ts
@@ -0,0 +1,149 @@
+import { Component, OnInit } from '@angular/core';
+import { DeploymentCenterStateManager } from 'app/site/deployment-center/deployment-center-setup/wizard-logic/deployment-center-state-manager';
+import { CacheService } from 'app/shared/services/cache.service';
+import { ArmService } from 'app/shared/services/arm.service';
+import { Observable } from 'rxjs/Observable';
+import { BroadcastService } from 'app/shared/services/broadcast.service';
+import { BroadcastEvent } from 'app/shared/models/broadcast-event';
+import { BusyStateScopeManager } from 'app/busy-state/busy-state-scope-manager';
+import { Subject } from 'rxjs/Subject';
+import { ArmObj } from 'app/shared/models/arm/arm-obj';
+import { Site } from 'app/shared/models/arm/site';
+import { PublishingCredentials } from 'app/shared/models/publishing-credentials';
+import { LogService } from 'app/shared/services/log.service';
+import { LogCategories } from 'app/shared/models/constants';
+import { summaryItem } from 'app/site/deployment-center/Models/summary-item';
+import { sourceControlProvider } from 'app/site/deployment-center/deployment-center-setup/wizard-logic/deployment-center-setup-models';
+import { TranslateService } from '@ngx-translate/core';
+
+@Component({
+ selector: 'app-step-complete',
+ templateUrl: './step-complete.component.html',
+ styleUrls: ['./step-complete.component.scss', '../deployment-center-setup.component.scss']
+})
+export class StepCompleteComponent implements OnInit {
+ resourceId: string;
+ private _busyManager: BusyStateScopeManager;
+ private _ngUnsubscribe = new Subject();
+ private site: ArmObj;
+ private pubCreds: ArmObj;
+ private pubUserName: ArmObj<{
+ publishingUserName: string;
+ }>;
+ constructor(
+ public wizard: DeploymentCenterStateManager,
+ private _cacheService: CacheService,
+ private _armService: ArmService,
+ private _broadcastService: BroadcastService,
+ private _logService: LogService,
+ private _translateService: TranslateService
+ ) {
+ this._busyManager = new BusyStateScopeManager(_broadcastService, 'site-tabs');
+
+ this.wizard.resourceIdStream
+ .takeUntil(this._ngUnsubscribe)
+ .switchMap(resourceId => {
+ return Observable.zip(
+ this._cacheService.getArm(resourceId, false),
+ this._cacheService.postArm(`${resourceId}/config/publishingcredentials/list`, false),
+ this._cacheService.getArm(`/providers/Microsoft.Web/publishingUsers/web`, false),
+ (site, pubCreds, publishingUser) => ({
+ site: site.json(),
+ pubCreds: pubCreds.json(),
+ publishingUser: publishingUser.json()
+ })
+ );
+ })
+ .subscribe(r => {
+ this.site = r.site;
+ this.pubCreds = r.pubCreds;
+ this.pubUserName = r.publishingUser;
+ });
+ }
+
+ get LocalGitCloneUri() {
+ const publishingUsername = this.pubUserName && this.pubUserName.properties.publishingUserName;
+ const scmUri = this.pubCreds && this.pubCreds.properties.scmUri.split('@')[1];
+ const siteName = this.site && this.site.name;
+ return `https://${publishingUsername}@${scmUri}:443/${siteName}.git`;
+ }
+
+ Save() {
+ this._busyManager.setBusy();
+ let payload = this.wizard.wizardForm.controls.sourceSettings.value;
+ if (this.wizard.wizardForm.controls.sourceProvider.value === 'external') {
+ payload.isManualIntegration = true;
+ }
+
+ Observable.zip(
+ this._armService.put(`${this.resourceId}/sourcecontrols/web`, {
+ properties: this.wizard.wizardForm.controls.sourceSettings.value
+ }),
+ t => ({
+ sc: t.json()
+ })
+ )
+ .do(r => {
+ this._busyManager.clearBusy();
+ })
+ .subscribe(
+ r => {
+ this._busyManager.clearBusy();
+ this._broadcastService.broadcastEvent(BroadcastEvent.ReloadDeploymentCenter);
+ },
+ err => {
+ this._busyManager.clearBusy();
+ this._logService.error(LogCategories.cicd, '/save-cicd', err);
+ }
+ );
+ }
+
+ public get summaryItems(): summaryItem[] {
+ let summaryItems: summaryItem[] = [];
+ let sourceProvider: sourceControlProvider =
+ this.wizard.wizardForm.controls && this.wizard.wizardForm.controls.sourceProvider && this.wizard.wizardForm.controls.sourceProvider.value;
+
+ summaryItems.push({
+ name: this._translateService.instant('sourceProvider'),
+ value: sourceProvider
+ });
+
+ summaryItems.push({
+ name: this._translateService.instant('buildProvider'),
+ value: 'Kudu'
+ });
+
+ if (sourceProvider === 'github' || sourceProvider === 'bitbucket' || sourceProvider === 'vsts' || sourceProvider === 'external') {
+ summaryItems.push({
+ name: this._translateService.instant('repository'),
+ value: this.wizard.wizardForm.controls.sourceSettings.value.repoUrl
+ });
+ summaryItems.push({
+ name: this._translateService.instant('branch'),
+ value: this.wizard.wizardForm.controls.sourceSettings.value.branch
+ });
+ } else if (sourceProvider === 'onedrive' || sourceProvider === 'dropbox') {
+ const FolderUrl: string = this.wizard.wizardForm.controls.sourceSettings.value.repoUrl;
+ const folderUrlPieces = FolderUrl.split('/');
+ const folderName = folderUrlPieces[folderUrlPieces.length - 1];
+ summaryItems.push({
+ name: 'Folder',
+ value: folderName
+ });
+ } else if (sourceProvider === 'localgit') {
+ summaryItems.push({ name: this._translateService.instant('repository'), value: this.LocalGitCloneUri });
+ summaryItems.push({ name: this._translateService.instant('branch'), value: 'master' });
+ }
+
+ if (sourceProvider === 'external') {
+ const isMercurial = this.wizard.wizardForm.controls.sourceSettings.value.isMercurial;
+ summaryItems.push({
+ name: this._translateService.instant('repoType'),
+ value: isMercurial ? 'Mercurial' : 'Git'
+ });
+ }
+
+ return summaryItems;
+ }
+ ngOnInit() {}
+}
diff --git a/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-bitbucket/configure-bitbucket.component.html b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-bitbucket/configure-bitbucket.component.html
new file mode 100644
index 0000000000..a28d025405
--- /dev/null
+++ b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-bitbucket/configure-bitbucket.component.html
@@ -0,0 +1,31 @@
+
+
+
{{'code' | translate}}
+
+
+
{{'repository' | translate}}
+
+
+
+
+
+
+
+
+
+
+
+
{{'branch' | translate}}
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-bitbucket/configure-bitbucket.component.scss b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-bitbucket/configure-bitbucket.component.scss
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-bitbucket/configure-bitbucket.component.spec.ts b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-bitbucket/configure-bitbucket.component.spec.ts
new file mode 100644
index 0000000000..987d441019
--- /dev/null
+++ b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-bitbucket/configure-bitbucket.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ConfigureBitbucketComponent } from './configure-bitbucket.component';
+
+describe('ConfigureBitbucketComponent', () => {
+ let component: ConfigureBitbucketComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ ConfigureBitbucketComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ConfigureBitbucketComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should be created', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-bitbucket/configure-bitbucket.component.ts b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-bitbucket/configure-bitbucket.component.ts
new file mode 100644
index 0000000000..f1ce7acb72
--- /dev/null
+++ b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-bitbucket/configure-bitbucket.component.ts
@@ -0,0 +1,102 @@
+import { Component, OnDestroy } from '@angular/core';
+import { DropDownElement } from 'app/shared/models/drop-down-element';
+import { ReplaySubject } from 'rxjs/ReplaySubject';
+import { DeploymentCenterStateManager } from 'app/site/deployment-center/deployment-center-setup/wizard-logic/deployment-center-state-manager';
+import { PortalService } from 'app/shared/services/portal.service';
+import { CacheService } from 'app/shared/services/cache.service';
+import { ArmService } from 'app/shared/services/arm.service';
+import { AiService } from 'app/shared/services/ai.service';
+import { Constants, LogCategories, DeploymentCenterConstants } from 'app/shared/models/constants';
+import { LogService } from 'app/shared/services/log.service';
+import { Subject } from 'rxjs/Subject';
+
+@Component({
+ selector: 'app-configure-bitbucket',
+ templateUrl: './configure-bitbucket.component.html',
+ styleUrls: ['./configure-bitbucket.component.scss', '../step-configure.component.scss']
+})
+export class ConfigureBitbucketComponent implements OnDestroy {
+ public RepoList: DropDownElement[];
+ public BranchList: DropDownElement[];
+
+ private reposStream = new ReplaySubject();
+ private _ngUnsubscribe = new Subject();
+
+ constructor(
+ private _wizard: DeploymentCenterStateManager,
+ _portalService: PortalService,
+ private _cacheService: CacheService,
+ private _logService: LogService,
+ _armService: ArmService,
+ _aiService: AiService
+ ) {
+ this.reposStream.takeUntil(this._ngUnsubscribe).subscribe(r => {
+ this.fetchBranches(r);
+ });
+ this.fetchRepos();
+ }
+
+ fetchRepos() {
+ this.RepoList = [];
+ this._cacheService
+ .post(Constants.serviceHost + `api/bitbucket/passthrough?repo=`, true, null, {
+ url: `${DeploymentCenterConstants.bitbucketApiUrl}/repositories?role=admin`
+ })
+ .subscribe(
+ r => {
+ const newRepoList: DropDownElement[] = [];
+ r.json().values.forEach(repo => {
+ newRepoList.push({
+ displayLabel: repo.name,
+ value: repo.full_name
+ });
+ });
+
+ this.RepoList = newRepoList;
+ },
+ err => {
+ this._logService.error(LogCategories.cicd, '/fetch-bitbucket-repos', err);
+ }
+ );
+ }
+
+ fetchBranches(repo: string) {
+ if (repo) {
+ this.BranchList = [];
+ this._cacheService
+ .post(Constants.serviceHost + `api/bitbucket/passthrough?branch=${repo}`, true, null, {
+ url: `${DeploymentCenterConstants.bitbucketApiUrl}/repositories/${repo}/refs/branches`
+ })
+ .subscribe(
+ r => {
+ const newBranchList: DropDownElement[] = [];
+
+ r.json().values.forEach(branch => {
+ newBranchList.push({
+ displayLabel: branch.name,
+ value: branch.name
+ });
+ });
+
+ this.BranchList = newBranchList;
+ },
+ err => {
+ this._logService.error(LogCategories.cicd, '/fetch-bitbucket-branches', err);
+ }
+ );
+ }
+ }
+
+ RepoChanged(repo: string) {
+ this._wizard.wizardForm.controls.sourceSettings.value.repoUrl = `${DeploymentCenterConstants.bitbucketUrl}/${repo}`;
+ this.reposStream.next(repo);
+ }
+
+ BranchChanged(branch: string) {
+ this._wizard.wizardForm.controls.sourceSettings.value.repoUrl = `${DeploymentCenterConstants.bitbucketUrl}/${branch}`;
+ }
+
+ ngOnDestroy(): void {
+ this._ngUnsubscribe.next();
+ }
+}
diff --git a/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-dropbox/configure-dropbox.component.html b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-dropbox/configure-dropbox.component.html
new file mode 100644
index 0000000000..1945fd98bd
--- /dev/null
+++ b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-dropbox/configure-dropbox.component.html
@@ -0,0 +1,20 @@
+
+
+
{{'code' | translate}}
+
+
+
+
{{'folder' | translate}}
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-dropbox/configure-dropbox.component.scss b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-dropbox/configure-dropbox.component.scss
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-dropbox/configure-dropbox.component.spec.ts b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-dropbox/configure-dropbox.component.spec.ts
new file mode 100644
index 0000000000..6d76bf3cb9
--- /dev/null
+++ b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-dropbox/configure-dropbox.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ConfigureDropboxComponent } from './configure-dropbox.component';
+
+describe('ConfigureDropboxComponent', () => {
+ let component: ConfigureDropboxComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ ConfigureDropboxComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ConfigureDropboxComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should be created', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-dropbox/configure-dropbox.component.ts b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-dropbox/configure-dropbox.component.ts
new file mode 100644
index 0000000000..40a5000ca3
--- /dev/null
+++ b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-dropbox/configure-dropbox.component.ts
@@ -0,0 +1,74 @@
+import { Component } from '@angular/core';
+import { DropDownElement } from 'app/shared/models/drop-down-element';
+import { DeploymentCenterStateManager } from 'app/site/deployment-center/deployment-center-setup/wizard-logic/deployment-center-state-manager';
+import { PortalService } from 'app/shared/services/portal.service';
+import { CacheService } from 'app/shared/services/cache.service';
+import { ArmService } from 'app/shared/services/arm.service';
+import { AiService } from 'app/shared/services/ai.service';
+import { Constants, LogCategories, DeploymentCenterConstants } from 'app/shared/models/constants';
+import { LogService } from 'app/shared/services/log.service';
+
+@Component({
+ selector: 'app-configure-dropbox',
+ templateUrl: './configure-dropbox.component.html',
+ styleUrls: ['./configure-dropbox.component.scss', '../step-configure.component.scss']
+})
+export class ConfigureDropboxComponent {
+ private _resourceId: string;
+ public folderList: DropDownElement[];
+
+ constructor(
+ public wizard: DeploymentCenterStateManager,
+ _portalService: PortalService,
+ private _cacheService: CacheService,
+ _armService: ArmService,
+ _aiService: AiService,
+ private _logService: LogService
+ ) {
+ this.wizard.resourceIdStream.subscribe(r => {
+ this._resourceId = r;
+ });
+ this.fillDropboxFolders();
+ }
+
+ public fillDropboxFolders() {
+ this.folderList = [];
+ return this._cacheService
+ .post(Constants.serviceHost + 'api/dropbox/passthrough', true, null, {
+ url: `${DeploymentCenterConstants.dropboxApiUrl}/files/list_folder`,
+ arg: {
+ path: ''
+ },
+ content_type: 'application/json'
+ })
+ .subscribe(
+ r => {
+ const rawFolders = r.json();
+ let options: DropDownElement[] = [];
+ const splitRID = this._resourceId.split('/');
+ const siteName = splitRID[splitRID.length - 1];
+
+ options.push({
+ displayLabel: siteName,
+ value: `${DeploymentCenterConstants.dropboxUri}/${siteName}`
+ });
+
+ rawFolders.entries.forEach(item => {
+ if (siteName.toLowerCase() === item.name.toLowerCase() || item['.tag'] !== 'folder') {
+ } else {
+ options.push({
+ displayLabel: item.name,
+ value: `${DeploymentCenterConstants.dropboxUri}/${item.name}`
+ });
+ }
+ });
+
+ this.folderList = options;
+ this.wizard.wizardForm.controls.sourceSettings.value.repoUrl = `${DeploymentCenterConstants.dropboxUri}/${siteName}`;
+ },
+ err => {
+ this._logService.error(LogCategories.cicd, '/fetch-dropbox-folders', err);
+ }
+ );
+ }
+}
diff --git a/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-external/configure-external.component.html b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-external/configure-external.component.html
new file mode 100644
index 0000000000..a79ce3ad9b
--- /dev/null
+++ b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-external/configure-external.component.html
@@ -0,0 +1,40 @@
+
+
+
{{'code' | translate}}
+
+
+
+
{{'repository' | translate}}
+
+
+
+
+
+
+
+
+
+
+
{{'branch' | translate}}
+
+
+
+
+
+
+
+
+
+
+
{{'repoType' | translate}}
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-external/configure-external.component.scss b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-external/configure-external.component.scss
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-external/configure-external.component.spec.ts b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-external/configure-external.component.spec.ts
new file mode 100644
index 0000000000..660e0243a8
--- /dev/null
+++ b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-external/configure-external.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ConfigureExternalComponent } from './configure-external.component';
+
+describe('ConfigureExternalComponent', () => {
+ let component: ConfigureExternalComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ ConfigureExternalComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ConfigureExternalComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should be created', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-external/configure-external.component.ts b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-external/configure-external.component.ts
new file mode 100644
index 0000000000..98a2e206b2
--- /dev/null
+++ b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-external/configure-external.component.ts
@@ -0,0 +1,29 @@
+import { Component } from '@angular/core';
+import { SelectOption } from 'app/shared/models/select-option';
+import { DeploymentCenterStateManager } from 'app/site/deployment-center/deployment-center-setup/wizard-logic/deployment-center-state-manager';
+
+@Component({
+ selector: 'app-configure-external',
+ templateUrl: './configure-external.component.html',
+ styleUrls: ['./configure-external.component.scss', '../step-configure.component.scss']
+})
+export class ConfigureExternalComponent {
+ public RepoTypeOptions: SelectOption[] = [
+ {
+ displayLabel: 'Mercurial',
+ value: 'Mercurial'
+ },
+ {
+ displayLabel: 'Git',
+ value: 'Git'
+ }
+ ];
+ public repoMode = 'Git';
+ constructor(public wizard: DeploymentCenterStateManager) {}
+
+ repoTypeChanged(evt) {
+ this.repoMode = evt;
+ this.wizard.wizardForm.controls.sourceSettings.value.isMercurial = evt === 'Mercurial';
+ console.log(evt);
+ }
+}
diff --git a/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-github/configure-github.component.html b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-github/configure-github.component.html
new file mode 100644
index 0000000000..e383541447
--- /dev/null
+++ b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-github/configure-github.component.html
@@ -0,0 +1,46 @@
+
+
+
{{'code' | translate}}
+
+
+
+
{{'organization' | translate}}
+
+
+
+
+
+
+
+
+
+
+
+
{{'repository' | translate}}
+
+
+
+
+
+
+
+
+
+
+
+
{{'branch' | translate}}
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-github/configure-github.component.scss b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-github/configure-github.component.scss
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-github/configure-github.component.spec.ts b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-github/configure-github.component.spec.ts
new file mode 100644
index 0000000000..f386107502
--- /dev/null
+++ b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-github/configure-github.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ConfigureGithubComponent } from './configure-github.component';
+
+describe('ConfigureGithubComponent', () => {
+ let component: ConfigureGithubComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ ConfigureGithubComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ConfigureGithubComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should be created', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-github/configure-github.component.ts b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-github/configure-github.component.ts
new file mode 100644
index 0000000000..e7cd23575b
--- /dev/null
+++ b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-github/configure-github.component.ts
@@ -0,0 +1,183 @@
+import { Component } from '@angular/core';
+import { DropDownElement } from 'app/shared/models/drop-down-element';
+import { DeploymentCenterStateManager } from 'app/site/deployment-center/deployment-center-setup/wizard-logic/deployment-center-state-manager';
+import { PortalService } from 'app/shared/services/portal.service';
+import { CacheService } from 'app/shared/services/cache.service';
+import { ArmService } from 'app/shared/services/arm.service';
+import { Constants, LogCategories, DeploymentCenterConstants } from 'app/shared/models/constants';
+import { Observable } from 'rxjs/Observable';
+import { ReplaySubject } from 'rxjs/ReplaySubject';
+import { Guid } from 'app/shared/Utilities/Guid';
+import { LogService } from 'app/shared/services/log.service';
+import { OnDestroy } from '@angular/core/src/metadata/lifecycle_hooks';
+import { Subject } from 'rxjs/Subject';
+
+@Component({
+ selector: 'app-configure-github',
+ templateUrl: './configure-github.component.html',
+ styleUrls: ['./configure-github.component.scss', '../step-configure.component.scss']
+})
+export class ConfigureGithubComponent implements OnDestroy {
+ public OrgList: DropDownElement[];
+ public RepoList: DropDownElement[];
+ private repoUrlToNameMap: { [key: string]: string } = {};
+ public BranchList: DropDownElement[];
+
+ private reposStream = new ReplaySubject();
+ private _ngUnsubscribe = new Subject();
+ private orgStream = new ReplaySubject();
+ constructor(
+ public wizard: DeploymentCenterStateManager,
+ _portalService: PortalService,
+ private _cacheService: CacheService,
+ _armService: ArmService,
+ private _logService: LogService
+ ) {
+ this.orgStream.takeUntil(this._ngUnsubscribe).subscribe(r => {
+ this.fetchRepos(r);
+ });
+ this.reposStream.takeUntil(this._ngUnsubscribe).subscribe(r => {
+ this.fetchBranches(r);
+ });
+
+ this.fetchOrgs();
+ }
+
+ fetchOrgs() {
+ return Observable.zip(
+ this._cacheService.post(Constants.serviceHost + 'api/github/passthrough?orgs=', true, null, {
+ url: `${DeploymentCenterConstants.githubApiUrl}/user/orgs`
+ }),
+ this._cacheService.post(Constants.serviceHost + 'api/github/passthrough?user=', true, null, {
+ url: `${DeploymentCenterConstants.githubApiUrl}/user`
+ }),
+ (orgs, user) => ({
+ orgs: orgs.json(),
+ user: user.json()
+ })
+ ).subscribe(r => {
+ const newOrgsList: DropDownElement[] = [];
+ newOrgsList.push({
+ displayLabel: r.user.login,
+ value: r.user.repos_url
+ });
+
+ r.orgs.forEach(org => {
+ newOrgsList.push({
+ displayLabel: org.login,
+ value: org.url
+ });
+ });
+
+ this.OrgList = newOrgsList;
+ });
+ }
+
+ fetchRepos(org: string) {
+ if (org) {
+ let fetchListCall: Observable = null;
+ this.RepoList = [];
+ this.BranchList = [];
+
+ //This branch is to handle the differences between getting a users personal repos and getting repos for a specific org such as Azure
+ //The API handles these differently but the UX shows them the same
+ if (org.toLocaleLowerCase().indexOf('github.com/users/') > -1) {
+ fetchListCall = this._cacheService
+ .post(Constants.serviceHost + `api/github/passthrough?repo=${org}`, true, null, {
+ url: `${org}`
+ })
+ .switchMap(r => {
+ return Observable.of(r.json());
+ });
+ } else {
+ fetchListCall = this._cacheService
+ .post(Constants.serviceHost + `api/github/passthrough?repo=${org}`, true, null, {
+ url: `${org}`
+ })
+ .switchMap(r => {
+ const orgData = r.json();
+ const pageCount = (orgData.public_repos + orgData.total_private_repos) / 100 + 1;
+ const pageCalls: Observable[] = [];
+ for (let i = 1; i <= pageCount; i++) {
+ pageCalls.push(
+ this._cacheService.post(Constants.serviceHost + `api/github/passthrough?repo=${org}&t=${Guid.newTinyGuid()}`, true, null, {
+ url: `${org}/repos?per_page=100&page=${i}`
+ })
+ );
+ }
+ return Observable.forkJoin(pageCalls);
+ })
+ .switchMap(r => {
+ let ret: any[] = [];
+ r.forEach(e => {
+ ret = ret.concat(e.json());
+ });
+ return Observable.of(ret);
+ });
+ }
+ fetchListCall.subscribe(r => {
+ const newRepoList: DropDownElement[] = [];
+ this.repoUrlToNameMap = {};
+ r
+ .filter(repo => {
+ return !repo.permissions || repo.permissions.admin;
+ })
+ .forEach(repo => {
+ newRepoList.push({
+ displayLabel: repo.name,
+ value: repo.html_url
+ });
+ this.repoUrlToNameMap[repo.html_url] = repo.full_name;
+ });
+
+ this.RepoList = newRepoList;
+ }), err => {
+ this._logService.error(LogCategories.cicd, '/fetch-github-repos', err);
+ };
+ }
+ }
+
+ fetchBranches(repo: string) {
+ if (repo) {
+ this.BranchList = [];
+ this._cacheService
+ .post(Constants.serviceHost + `api/github/passthrough?branch=${repo}`, true, null, {
+ url: `${DeploymentCenterConstants.githubApiUrl}/repos/${this.repoUrlToNameMap[repo]}/branches?per_page=100`
+ })
+ .subscribe(
+ r => {
+ const newBranchList: DropDownElement[] = [];
+
+ r.json().forEach(branch => {
+ newBranchList.push({
+ displayLabel: branch.name,
+ value: branch.name
+ });
+ });
+
+ this.BranchList = newBranchList;
+ },
+ err => {
+ this._logService.error(LogCategories.cicd, '/fetch-github-branches', err);
+ }
+ );
+ }
+ }
+
+ RepoChanged(repo: string) {
+ this.wizard.wizardForm.controls.sourceSettings.value.repoUrl = `${DeploymentCenterConstants.githubUri}/${this.repoUrlToNameMap[repo]}`;
+ this.reposStream.next(repo);
+ }
+
+ OrgChanged(org: string) {
+ this.orgStream.next(org);
+ }
+
+ BranchChanged(branch: string) {
+ this.wizard.wizardForm.controls.sourceSettings.value.branch = branch;
+ }
+
+ ngOnDestroy(): void {
+ this._ngUnsubscribe.next();
+ }
+}
diff --git a/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-onedrive/configure-onedrive.component.html b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-onedrive/configure-onedrive.component.html
new file mode 100644
index 0000000000..9f4ca1d35e
--- /dev/null
+++ b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-onedrive/configure-onedrive.component.html
@@ -0,0 +1,20 @@
+
+
+
{{'code' | translate}}
+
+
+
+
{{'folder' | translate}}
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-onedrive/configure-onedrive.component.scss b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-onedrive/configure-onedrive.component.scss
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-onedrive/configure-onedrive.component.spec.ts b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-onedrive/configure-onedrive.component.spec.ts
new file mode 100644
index 0000000000..ec83036d7d
--- /dev/null
+++ b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-onedrive/configure-onedrive.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ConfigureOnedriveComponent } from './configure-onedrive.component';
+
+describe('ConfigureOnedriveComponent', () => {
+ let component: ConfigureOnedriveComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ ConfigureOnedriveComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ConfigureOnedriveComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should be created', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-onedrive/configure-onedrive.component.ts b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-onedrive/configure-onedrive.component.ts
new file mode 100644
index 0000000000..132fe0c160
--- /dev/null
+++ b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-onedrive/configure-onedrive.component.ts
@@ -0,0 +1,81 @@
+import { Component } from '@angular/core';
+import { DeploymentCenterStateManager } from 'app/site/deployment-center/deployment-center-setup/wizard-logic/deployment-center-state-manager';
+import { PortalService } from 'app/shared/services/portal.service';
+import { CacheService } from 'app/shared/services/cache.service';
+import { ArmService } from 'app/shared/services/arm.service';
+import { DropDownElement } from 'app/shared/models/drop-down-element';
+import { Constants, LogCategories, DeploymentCenterConstants } from 'app/shared/models/constants';
+import { Subject } from 'rxjs/Subject';
+import { OnDestroy } from '@angular/core/src/metadata/lifecycle_hooks';
+import { LogService } from 'app/shared/services/log.service';
+
+@Component({
+ selector: 'app-configure-onedrive',
+ templateUrl: './configure-onedrive.component.html',
+ styleUrls: ['./configure-onedrive.component.scss', '../step-configure.component.scss']
+})
+export class ConfigureOnedriveComponent implements OnDestroy {
+ private _resourceId: string;
+ public folderList: DropDownElement[];
+ private _ngUnsubscribe = new Subject();
+ private _onedriveCallSubject = new Subject();
+
+ constructor(
+ public wizard: DeploymentCenterStateManager,
+ _portalService: PortalService,
+ private _cacheService: CacheService,
+ _armService: ArmService,
+ private _logService: LogService
+ ) {
+ this.wizard.wizardForm.controls.sourceSettings.value.isManualIntegration = false;
+ this.wizard.resourceIdStream.takeUntil(this._ngUnsubscribe).subscribe(r => {
+ this._resourceId = r;
+ });
+ this._onedriveCallSubject
+ .takeUntil(this._ngUnsubscribe)
+ .switchMap(() =>
+ this._cacheService.post(Constants.serviceHost + 'api/onedrive/passthrough', true, null, {
+ url: `${DeploymentCenterConstants.onedriveApiUri}/children`
+ })
+ )
+ .subscribe(
+ r => {
+ const rawFolders = r.json();
+ let options: DropDownElement[] = [];
+ const splitRID = this._resourceId.split('/');
+ const siteName = splitRID[splitRID.length - 1];
+
+ options.push({
+ displayLabel: siteName,
+ value: `${DeploymentCenterConstants.onedriveApiUri}/${siteName}`
+ });
+
+ rawFolders.value.forEach(item => {
+ if (siteName.toLowerCase() === item.name.toLowerCase()) {
+ } else {
+ options.push({
+ displayLabel: item.name,
+ value: `${DeploymentCenterConstants.onedriveApiUri}/${item.name}`
+ });
+ }
+ });
+
+ this.folderList = options;
+ this.wizard.wizardForm.controls.sourceSettings.value.repoUrl = `${DeploymentCenterConstants.onedriveApiUri}/${siteName}`;
+ },
+ err => {
+ this._logService.error(LogCategories.cicd, '/fetch-onedrive-folders', err);
+ }
+ );
+ this.fillOnedriveFolders();
+ }
+
+ public fillOnedriveFolders() {
+ this.folderList = [];
+ this._onedriveCallSubject.next();
+ }
+
+ ngOnDestroy(): void {
+ this._ngUnsubscribe.next();
+ }
+}
diff --git a/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-vsts-build/configure-vsts-build.component.html b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-vsts-build/configure-vsts-build.component.html
new file mode 100644
index 0000000000..6d38998661
--- /dev/null
+++ b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-vsts-build/configure-vsts-build.component.html
@@ -0,0 +1,51 @@
+
\ No newline at end of file
diff --git a/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-vsts-build/configure-vsts-build.component.scss b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-vsts-build/configure-vsts-build.component.scss
new file mode 100644
index 0000000000..63ddbc5850
--- /dev/null
+++ b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-vsts-build/configure-vsts-build.component.scss
@@ -0,0 +1,11 @@
+@import '../../../../../../sass/main';
+
+.newOrExistingSelection{
+ margin-bottom: 5px;
+ font-size: 12px;
+ color: $value-text-color;
+
+ input{
+ margin: 0px 10px;
+ }
+}
\ No newline at end of file
diff --git a/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-vsts-build/configure-vsts-build.component.spec.ts b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-vsts-build/configure-vsts-build.component.spec.ts
new file mode 100644
index 0000000000..948811e003
--- /dev/null
+++ b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-vsts-build/configure-vsts-build.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ConfigureVstsBuildComponent } from './configure-vsts-build.component';
+
+describe('ConfigureVstsBuildComponent', () => {
+ let component: ConfigureVstsBuildComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ ConfigureVstsBuildComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ConfigureVstsBuildComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should be created', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-vsts-build/configure-vsts-build.component.ts b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-vsts-build/configure-vsts-build.component.ts
new file mode 100644
index 0000000000..2d113ead39
--- /dev/null
+++ b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-vsts-build/configure-vsts-build.component.ts
@@ -0,0 +1,15 @@
+import { Component, OnInit } from '@angular/core';
+
+@Component({
+ selector: 'app-configure-vsts-build',
+ templateUrl: './configure-vsts-build.component.html',
+ styleUrls: ['./configure-vsts-build.component.scss', '../step-configure.component.scss']
+})
+export class ConfigureVstsBuildComponent implements OnInit {
+
+ constructor() { }
+
+ ngOnInit() {
+ }
+
+}
diff --git a/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-vsts-source/configure-vsts-source.component.html b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-vsts-source/configure-vsts-source.component.html
new file mode 100644
index 0000000000..6c22ab42c1
--- /dev/null
+++ b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-vsts-source/configure-vsts-source.component.html
@@ -0,0 +1,132 @@
+
+
+
{{'code' | translate}}
+
+
+
+
{{'vstsAccount' | translate}}
+
+
+
+
+
+
+
+
{{'project' | translate}}
+
+
+
+
+
+
+
+
{{'repository' | translate}}
+
+
+
+
+
+
+
+
{{'branch' | translate}}
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-vsts-source/configure-vsts-source.component.scss b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-vsts-source/configure-vsts-source.component.scss
new file mode 100644
index 0000000000..63ddbc5850
--- /dev/null
+++ b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-vsts-source/configure-vsts-source.component.scss
@@ -0,0 +1,11 @@
+@import '../../../../../../sass/main';
+
+.newOrExistingSelection{
+ margin-bottom: 5px;
+ font-size: 12px;
+ color: $value-text-color;
+
+ input{
+ margin: 0px 10px;
+ }
+}
\ No newline at end of file
diff --git a/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-vsts-source/configure-vsts-source.component.spec.ts b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-vsts-source/configure-vsts-source.component.spec.ts
new file mode 100644
index 0000000000..33c8769dce
--- /dev/null
+++ b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-vsts-source/configure-vsts-source.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ConfigureVstsSourceComponent } from './configure-vsts-source.component';
+
+describe('ConfigureVstsSourceComponent', () => {
+ let component: ConfigureVstsSourceComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ ConfigureVstsSourceComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ConfigureVstsSourceComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should be created', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-vsts-source/configure-vsts-source.component.ts b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-vsts-source/configure-vsts-source.component.ts
new file mode 100644
index 0000000000..49b39aa83f
--- /dev/null
+++ b/AzureFunctions.AngularClient/src/app/site/deployment-center/deployment-center-setup/step-configure/configure-vsts-source/configure-vsts-source.component.ts
@@ -0,0 +1,239 @@
+import { Component } from '@angular/core';
+import { DropDownElement } from 'app/shared/models/drop-down-element';
+import { DeploymentCenterStateManager } from 'app/site/deployment-center/deployment-center-setup/wizard-logic/deployment-center-state-manager';
+import { CacheService } from 'app/shared/services/cache.service';
+import { Headers } from '@angular/http';
+import { UserService } from 'app/shared/services/user.service';
+import { Observable } from 'rxjs/Observable';
+import * as _ from 'lodash';
+import { Subject } from 'rxjs/Subject';
+import { VSORepo, VSOAccount } from 'app/site/deployment-center/Models/vso-repo';
+import { forkJoin } from 'rxjs/observable/forkJoin';
+import { OnDestroy } from '@angular/core/src/metadata/lifecycle_hooks';
+import { LogService } from 'app/shared/services/log.service';
+import { LogCategories } from 'app/shared/models/constants';
+export const PythonFramework = {
+ Bottle: 'Bottle',
+ Django: 'Django',
+ Flask: 'Flask'
+};
+
+export const TaskRunner = {
+ None: 'None',
+ Gulp: 'Gulp',
+ Grunt: 'Grunt'
+};
+
+export const WebAppFramework = {
+ AspNetWap: 'AspNetWap',
+ AspNetCore: 'AspNetCore',
+ Node: 'Node',
+ PHP: 'PHP',
+ Python: 'Python'
+};
+
+export class VSTSRepository {
+ name: string;
+ account: string;
+ remoteUrl: string;
+ projectName: string;
+ id: string;
+}
+
+@Component({
+ selector: 'app-configure-vsts-source',
+ templateUrl: './configure-vsts-source.component.html',
+ styleUrls: ['./configure-vsts-source.component.scss', '../step-configure.component.scss']
+})
+export class ConfigureVstsSourceComponent implements OnDestroy {
+ WebApplicationFrameworks: DropDownElement[] = [
+ {
+ displayLabel: 'ASP.NET',
+ value: WebAppFramework.AspNetWap
+ },
+ {
+ displayLabel: 'ASP.NET Core',
+ value: WebAppFramework.AspNetCore
+ },
+ {
+ displayLabel: 'Node.JS',
+ value: WebAppFramework.Node
+ },
+ {
+ displayLabel: 'PHP',
+ value: WebAppFramework.PHP
+ },
+ {
+ displayLabel: 'Python',
+ value: WebAppFramework.Python
+ }
+ ];
+ chosenBuildFramework: string;
+ public AccountList: DropDownElement[];
+ public ProjectList: DropDownElement[];
+ public RepositoryList: DropDownElement[];
+ public BranchList: DropDownElement[];
+ private token: string;
+ private _ngUnsubscribe = new Subject();
+
+ private vstsRepositories: VSORepo[];
+
+ //Subscriptions
+ private _memberIdSubscription = new Subject();
+ private _branchSubscription = new Subject();
+ constructor(private _wizard: DeploymentCenterStateManager, private _cacheService: CacheService, private _userService: UserService, private _logService: LogService) {
+ this._userService.getStartupInfo().takeUntil(this._ngUnsubscribe).subscribe(r => {
+ this.token = r.token;
+ });
+ this.setupSubscriptions();
+ this.populate();
+ this._wizard.wizardForm.controls.buildProvider.valueChanges.distinctUntilChanged().takeUntil(this._ngUnsubscribe).subscribe(r => {
+ this.populate();
+ });
+ }
+
+ private populate() {
+ this._memberIdSubscription.next();
+ }
+
+ setupSubscriptions() {
+ this._memberIdSubscription
+ .takeUntil(this._ngUnsubscribe)
+ .switchMap(() => this._cacheService.get('https://app.vssps.visualstudio.com/_apis/profile/profiles/me'))
+ .map(r => r.json())
+ .switchMap(r => this.fetchAccounts(r.id))
+ .switchMap(r => {
+ let projectCalls: Observable