Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updating site-config to do permission/lock checks after initial load #2318

Merged
merged 3 commits into from
Feb 9, 2018
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { LogService } from './../services/log.service';
import { LogService } from 'app/shared/services/log.service';
import { BusyStateName } from './../../busy-state/busy-state.component';
import { TelemetryService } from './../services/telemetry.service';
import { BroadcastService } from 'app/shared/services/broadcast.service';
import { Injector } from '@angular/core/src/core';
Expand All @@ -8,48 +9,122 @@ 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';
import { BusyStateScopeManager } from 'app/busy-state/busy-state-scope-manager';

export abstract class FeatureComponent<T> extends ErrorableComponent implements OnDestroy {
// The name of the feature this component is apart of. Telemetry will
// be collected for all features with the same name.
@Input() featureName: string;

// Specifies if this is the main parent component for the entire feature.
// There should only be one parent component per feature
isParentComponent = false;

private _inputEvents = new Subject<T>();
private _ngUnsubscribe = new Subject();
private _telemetryService: TelemetryService;
private _busyManager: BusyStateScopeManager;
private _busyClearedEarly = false;

private __logService: LogService;
private __telemetryService: TelemetryService;

constructor(
componentName: string,
injector: Injector,
busyComponentName?: BusyStateName) {

constructor(componentName: string, injector: Injector) {
super(componentName, injector.get(BroadcastService));

this._telemetryService = injector.get(TelemetryService);
const logService = injector.get(LogService);
this.__telemetryService = injector.get(TelemetryService);
this.__logService = injector.get(LogService);
if (busyComponentName) {
this._busyManager = new BusyStateScopeManager(this._broadcastService, busyComponentName);
}

const preCheckEvents = this._inputEvents
.takeUntil(this._ngUnsubscribe)
.do(input => {

if (!this.featureName) {
throw Error('featureName is not defined');
}

if (this.isParentComponent) {
this._telemetryService.featureConstructComplete(this.featureName);
this.__telemetryService.featureConstructComplete(this.featureName);
}

this._telemetryService.featureLoading(this.isParentComponent, this.featureName, this.componentName);
if (this._busyManager) {
this.__telemetryService.featureLoading(
this.isParentComponent,
this.featureName,
this.componentName);

this.__logService.verbose(
LogCategories.featureComponent,
`New input received, setting busy componentName: ${this.componentName}`);

this.setBusy();
}
});

this.setup(preCheckEvents)
.takeUntil(this._ngUnsubscribe)
.subscribe(r => {
this._telemetryService.featureLoadingComplete(this.featureName, this.componentName);
if (!this._busyClearedEarly && this._busyManager) {
this.__logService.verbose(
LogCategories.featureComponent,
`Clearing busy normally componentName: ${this.componentName}`);

this.__telemetryService.featureLoadingComplete(this.featureName, this.componentName);
this.clearBusy();
}
}, err => {
logService.error(LogCategories.featureLoading, '/load-failure', err);
this.__logService.error(LogCategories.featureComponent, '/load-failure', err);
this.clearBusy();
});
}

protected setInput(input: T) {
this._inputEvents.next(input);
}

protected setBusy() {
if (!this._busyManager) {
throw Error(`No busy manager defined, component: ${this.componentName}`);
}

this.__logService.verbose(
LogCategories.featureComponent,
`Setting busy componentName: ${this.componentName}`);

this._busyManager.setBusy();
}

// Use this to clear the busy state before the initial setup observable has
// completed loading. This is useful if the main parts of your UI are ready
// to be used, but you still want to load some stuff in the background.
protected clearBusyEarly() {
this._busyClearedEarly = true;
this.__telemetryService.featureLoadingComplete(this.featureName, this.componentName);
this.__logService.verbose(
LogCategories.featureComponent,
`Clearing busy early componentName: ${this.componentName}`);

this.clearBusy();
}

protected clearBusy() {
if (!this._busyManager) {
throw Error(`No busy manager defined, component: ${this.componentName}`);
}

this.__logService.verbose(
LogCategories.featureComponent,
`Clearing busy componentName: ${this.componentName}`);

this._busyManager.clearBusy();
}

protected abstract setup(inputEvents: Observable<T>): Observable<any>;

ngOnDestroy(): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ export class ConditionalHttpClient {
if (body && body.error) {
mesg = body.error.message;

if (response.status === 401) {
if (response.status === 401 || response.status === 403) {
errorId = errorIds.armErrors.noAccess;
} else if (response.status === 409 && body.error.code === 'ScopeLocked') {
errorId = errorIds.armErrors.scopeLocked;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ export class LogCategories {
public static readonly functionNew = 'FunctionNew';
public static readonly cicd = 'CICD';
public static readonly telemetry = 'Telemetry';
public static readonly featureLoading = 'FeatureLoading';
public static readonly featureComponent = 'FeatureComponent';
}

export class KeyCodes {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ export class AuthzService {
return this._getLocks(resourceId)
.map(locks => {
return !!locks.find(l => l.properties.level === 'ReadOnly');
})
.catch(e => {
return Observable.of(false);
});
Copy link
Contributor

@andimarc andimarc Feb 9, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean we assume there are no locks if the ARM call fails? #Resolved

Copy link
Contributor Author

@ehamai ehamai Feb 9, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. In the past we've had issues where ARM has an LSI going on for certain permission calls which would completely break our UI. Returning false would allow us to make a best effort to continue to load as much data as we can. If they actually have a lock, then the back-end would handle it properly.


In reply to: 167211993 [](ancestors = 167211993)

}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { ConnectionString } from './../models/arm/connection-strings';
import { AvailableStack } from './../models/arm/stacks';
import { SiteConfig } from './../models/arm/site-config';
import { ArmArrayResult } from './../models/arm/arm-obj';
import { Injectable, Injector } from '@angular/core';
import { ConditionalHttpClient } from 'app/shared/conditional-http-client';
import { UserService } from 'app/shared/services/user.service';
Expand Down Expand Up @@ -25,22 +29,48 @@ export class SiteService {

getSite(resourceId: string): Result<ArmObj<Site>> {
const getSite = this._cacheService.getArm(resourceId).map(r => r.json());
return this._client.execute({ resourceId: resourceId}, t => getSite);
return this._client.execute({ resourceId: resourceId }, t => getSite);
}

getAppSettings(resourceId: string): Result<ArmObj<{[key: string]: string}>> {
getSlots(resourceId: string): Result<ArmArrayResult<Site>> {
const siteDescriptor = new ArmSiteDescriptor(resourceId);
const slotsId = `${siteDescriptor.getSiteOnlyResourceId()}/slots`;
const getSlots = this._cacheService.getArm(slotsId).map(r => r.json());

const getAppSettings = this._cacheService.postArm(`${resourceId}/config/appSettings/list`, true)
return this._client.execute({ resourceId: resourceId }, t => getSlots);
}

getSiteConfig(resourceId: string, force?: boolean): Result<ArmObj<SiteConfig>> {
const getSiteConfig = this._cacheService.getArm(`${resourceId}/config/web`, force).map(r => r.json());
return this._client.execute({ resourceId: resourceId }, t => getSiteConfig);
}

getAppSettings(resourceId: string, force?: boolean): Result<ArmObj<{ [key: string]: string }>> {

const getAppSettings = this._cacheService.postArm(`${resourceId}/config/appSettings/list`, force)
.map(r => r.json());

return this._client.execute({ resourceId: resourceId }, t => getAppSettings);
}

getSlotConfigNames(resourceId: string): Result<ArmObj<SlotConfigNames>> {
getConnectionStrings(resourceId: string, force?: boolean): Result<ArmObj<ConnectionString>> {

const getConnectionStrings = this._cacheService.postArm(`${resourceId}/config/connectionstrings/list`, force)
.map(r => r.json());

return this._client.execute({ resourceId: resourceId }, t => getConnectionStrings);
}

getSlotConfigNames(resourceId: string, force?: boolean): Result<ArmObj<SlotConfigNames>> {
const slotsConfigNamesId = `${new ArmSiteDescriptor(resourceId).getSiteOnlyResourceId()}/config/slotConfigNames`;
const getSlotConfigNames = this._cacheService.getArm(slotsConfigNamesId, true)
const getSlotConfigNames = this._cacheService.getArm(slotsConfigNamesId, force)
.map(r => r.json());

return this._client.execute( { resourceId: resourceId }, t => getSlotConfigNames);
return this._client.execute({ resourceId: resourceId }, t => getSlotConfigNames);
}

getAvailableStacks(): Result<ArmArrayResult<AvailableStack>> {
const getAvailableStacks = this._cacheService.getArm('/providers/Microsoft.Web/availablestacks').map(r => r.json());
return this._client.execute({ resourceId: null }, t => getAvailableStacks);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ export class TelemetryService {
}

public featureLoading(isParentComponent: boolean, featureName: string, componentName: string) {
// If the inputs on a parent component have changed, then we should reset timers
if (isParentComponent && this._featureMap[featureName]) {
delete this._featureMap[featureName];
}

if (!this._featureMap[featureName]) {

if (isParentComponent) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export class AppSettingsComponent extends FeatureComponent<ResourceId> implement
public newItem: CustomFormGroup;
public originalItemsDeleted: number;

private _busyManager: BusyStateScopeManager;
private busyManager: BusyStateScopeManager;
private _saveError: string;
private _validatorFns: ValidatorFn[];
private _requiredValidator: RequiredValidator;
Expand All @@ -61,7 +61,7 @@ export class AppSettingsComponent extends FeatureComponent<ResourceId> implement
injector: Injector
) {
super('AppSettingsComponent', injector);
this._busyManager = new BusyStateScopeManager(this._broadcastService, 'site-tabs');
this.busyManager = new BusyStateScopeManager(this._broadcastService, 'site-tabs');

this._resetPermissionsAndLoadingState();

Expand All @@ -74,7 +74,7 @@ export class AppSettingsComponent extends FeatureComponent<ResourceId> implement
.distinctUntilChanged()
.switchMap(() => {

this._busyManager.setBusy();
this.busyManager.setBusy();
this._saveError = null;
this._appSettingsArm = null;
this._slotConfigNamesArm = null;
Expand All @@ -87,10 +87,10 @@ export class AppSettingsComponent extends FeatureComponent<ResourceId> implement

return Observable.zip(
this._siteService.getSite(this.resourceId),
this._siteService.getAppSettings(this.resourceId),
this._siteService.getSlotConfigNames(this.resourceId));
this._siteService.getAppSettings(this.resourceId, true),
this._siteService.getSlotConfigNames(this.resourceId, true));
})
.do((results: any[]) => {
.do(results => {
const siteResult = results[0];
const asResult = results[1];
const slotNamesResult = results[2];
Expand Down Expand Up @@ -120,8 +120,10 @@ export class AppSettingsComponent extends FeatureComponent<ResourceId> implement
this._setupForm(this._appSettingsArm, this._slotConfigNamesArm);
}

const failedRequest = results.find(r => !r.isSuccessful && r.error.preconditionSuccess)
if (failedRequest) {
const failedRequest = results.find(r => !r.isSuccessful);

// If there's a failed request for a reason other than write permission, then show failure
if (failedRequest && this.hasWritePermissions) {
this._logService.error(LogCategories.appSettings, '/app-settings', failedRequest.error.message);
this._setupForm(null, null);
this.loadingFailureMessage = this._translateService.instant(PortalResources.configLoadFailure);
Expand All @@ -130,7 +132,7 @@ export class AppSettingsComponent extends FeatureComponent<ResourceId> implement

this.loadingMessage = null;
this.showPermissionsMessage = true;
this._busyManager.clearBusy();
this.busyManager.clearBusy();
});
}

Expand All @@ -145,7 +147,7 @@ export class AppSettingsComponent extends FeatureComponent<ResourceId> implement

ngOnDestroy(): void {
super.ngOnDestroy();
this._busyManager.clearBusy();
this.busyManager.clearBusy();
}

private _resetPermissionsAndLoadingState() {
Expand Down
Loading