Merge pull request #993 from theseyi/handle-comp-exception

refactors exception handling in compliance presentational component t…
This commit is contained in:
Seyi Adebajo 2018-02-22 16:35:02 -08:00 committed by GitHub
commit 4f475c694f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 80 additions and 62 deletions

View File

@ -117,8 +117,6 @@ const {
complianceDataException, complianceDataException,
complianceFieldNotUnique, complianceFieldNotUnique,
missingTypes, missingTypes,
successUpdating,
failedUpdating,
helpText, helpText,
successUploading, successUploading,
invalidPolicyData, invalidPolicyData,
@ -790,43 +788,6 @@ export default class DatasetCompliance extends ObservableDecorator {
}); });
} }
/**
* Helper method to update user when an async server update to the
* security specification is handled.
* @template T
* @param {Promise<T>} request the server request
* @param {{successMessage?: string, isSaving?: boolean}} [{ successMessage = successUpdating, isSaving = false }={}]
* @prop {successMessage} optional message for successful response
* @prop {isSaving} optional flag indicating when the user intends to persist / save
* @returns {Promise<void>}
* @memberof DatasetCompliance
*/
whenRequestCompletes<T extends { status: ApiStatus }>(
this: DatasetCompliance,
request: Promise<T>,
{ successMessage = successUpdating, isSaving = false }: { successMessage?: string; isSaving?: boolean } = {}
): Promise<void> {
const { notify } = get(this, 'notifications');
return Promise.resolve(request)
.then(({ status = ApiStatus.ERROR }): void | Promise<void> => {
return status === ApiStatus.OK
? notify(NotificationEvent.success, { content: successMessage })
: Promise.reject(new Error(`Reason code for this is ${status}`));
})
.catch((err: string) => {
let message = `${failedUpdating} \n ${err}`;
if (get(this, 'isNewComplianceInfo') && !isSaving) {
return notify(NotificationEvent.info, {
content: 'This dataset does not have any previously saved fields with a identifying information.'
});
}
notify(NotificationEvent.error, { content: message });
});
}
/** /**
* Sets the default classification for the given identifier field * Sets the default classification for the given identifier field
* Using the identifierType, determine the field's default security classification based on a values * Using the identifierType, determine the field's default security classification based on a values
@ -1409,7 +1370,7 @@ export default class DatasetCompliance extends ObservableDecorator {
const onSave = get(this, 'onSave'); const onSave = get(this, 'onSave');
setSaveFlag(isSaving); setSaveFlag(isSaving);
await this.whenRequestCompletes(onSave(), { isSaving }); await onSave();
return this.updateStep(-1); return this.updateStep(-1);
} finally { } finally {
setSaveFlag(); setSaveFlag();
@ -1419,10 +1380,7 @@ export default class DatasetCompliance extends ObservableDecorator {
// Rolls back changes made to the compliance spec to current // Rolls back changes made to the compliance spec to current
// server state // server state
resetCompliance(this: DatasetCompliance) { resetCompliance(this: DatasetCompliance) {
const options = { get(this, 'onReset')();
successMessage: 'Field classification has been reset to the previously saved state.'
};
this.whenRequestCompletes(get(this, 'onReset')(), options);
} }
}; };
} }

View File

@ -1,13 +1,17 @@
import Component from '@ember/component'; import Component from '@ember/component';
import { get, set, setProperties, getProperties } from '@ember/object'; import { get, set, setProperties, getProperties } from '@ember/object';
import ComputedProperty from '@ember/object/computed';
import { inject } from '@ember/service';
import { task, TaskInstance } from 'ember-concurrency'; import { task, TaskInstance } from 'ember-concurrency';
import { action } from 'ember-decorators/object'; import { action } from 'ember-decorators/object';
import Notifications, { NotificationEvent } from 'wherehows-web/services/notifications';
import { IDatasetColumn } from 'wherehows-web/typings/api/datasets/columns'; import { IDatasetColumn } from 'wherehows-web/typings/api/datasets/columns';
import { IComplianceInfo, IComplianceSuggestion } from 'wherehows-web/typings/api/datasets/compliance'; import { IComplianceInfo, IComplianceSuggestion } from 'wherehows-web/typings/api/datasets/compliance';
import { IDatasetView } from 'wherehows-web/typings/api/datasets/dataset'; import { IDatasetView } from 'wherehows-web/typings/api/datasets/dataset';
import { IDatasetSchema } from 'wherehows-web/typings/api/datasets/schema'; import { IDatasetSchema } from 'wherehows-web/typings/api/datasets/schema';
import { IComplianceDataType } from 'wherehows-web/typings/api/list/compliance-datatypes'; import { IComplianceDataType } from 'wherehows-web/typings/api/list/compliance-datatypes';
import { import {
ApiResponseStatus,
IReadComplianceResult, IReadComplianceResult,
readDatasetComplianceByUrn, readDatasetComplianceByUrn,
readDatasetComplianceSuggestionByUrn, readDatasetComplianceSuggestionByUrn,
@ -15,7 +19,11 @@ import {
} from 'wherehows-web/utils/api'; } from 'wherehows-web/utils/api';
import { columnDataTypesAndFieldNames } from 'wherehows-web/utils/api/datasets/columns'; import { columnDataTypesAndFieldNames } from 'wherehows-web/utils/api/datasets/columns';
import { readDatasetSchemaByUrn } from 'wherehows-web/utils/api/datasets/schema'; import { readDatasetSchemaByUrn } from 'wherehows-web/utils/api/datasets/schema';
import { ApiError } from 'wherehows-web/utils/api/errors/errors';
import { readComplianceDataTypes } from 'wherehows-web/utils/api/list/compliance-datatypes'; import { readComplianceDataTypes } from 'wherehows-web/utils/api/list/compliance-datatypes';
import { compliancePolicyStrings } from 'wherehows-web/constants';
const { successUpdating, failedUpdating } = compliancePolicyStrings;
export default class DatasetComplianceContainer extends Component { export default class DatasetComplianceContainer extends Component {
/** /**
@ -42,6 +50,12 @@ export default class DatasetComplianceContainer extends Component {
*/ */
complianceSuggestion: IComplianceSuggestion | void; complianceSuggestion: IComplianceSuggestion | void;
/**
* Reference to the application notifications Service
* @type {ComputedProperty<Notifications>}
*/
notifications: ComputedProperty<Notifications> = inject();
/** /**
* Object containing the compliance information for the dataset * Object containing the compliance information for the dataset
* @type {IComplianceInfo | void} * @type {IComplianceInfo | void}
@ -140,12 +154,38 @@ export default class DatasetComplianceContainer extends Component {
* @type {Task<Promise<IDatasetSchema>, (a?: any) => TaskInstance<Promise<IDatasetSchema>>>} * @type {Task<Promise<IDatasetSchema>, (a?: any) => TaskInstance<Promise<IDatasetSchema>>>}
*/ */
getDatasetSchemaTask = task(function*(this: DatasetComplianceContainer): IterableIterator<Promise<IDatasetSchema>> { getDatasetSchemaTask = task(function*(this: DatasetComplianceContainer): IterableIterator<Promise<IDatasetSchema>> {
const { columns, schemaless } = yield readDatasetSchemaByUrn(get(this, 'urn')); try {
const schemaFieldNamesMappedToDataTypes = columnDataTypesAndFieldNames(columns); const { columns, schemaless } = yield readDatasetSchemaByUrn(get(this, 'urn'));
const schemaFieldNamesMappedToDataTypes = columnDataTypesAndFieldNames(columns);
setProperties(this, { schemaFieldNamesMappedToDataTypes, schemaless }); setProperties(this, { schemaFieldNamesMappedToDataTypes, schemaless });
} catch (e) {
// If this schema is missing, silence exception, otherwise propagate
if (!(e instanceof ApiError && e.status === ApiResponseStatus.NotFound)) {
throw e;
}
}
}); });
/**
* Handles user notifications when save succeeds or fails
* @template T the return type for the save request
* @param {Promise<T>} request async policy save request
* @returns {Promise<T>}
* @memberof DatasetComplianceContainer
*/
async notifyOnSave<T>(this: DatasetComplianceContainer, request: Promise<T>): Promise<T> {
const { notify } = get(this, 'notifications');
try {
await request;
notify(NotificationEvent.success, { content: successUpdating });
} catch (e) {
notify(NotificationEvent.error, { content: failedUpdating });
}
return request;
}
/** /**
* Persists the updates to the compliance policy on the remote host * Persists the updates to the compliance policy on the remote host
* @return {Promise<void>} * @return {Promise<void>}
@ -154,7 +194,7 @@ export default class DatasetComplianceContainer extends Component {
async savePrivacyCompliancePolicy(this: DatasetComplianceContainer): Promise<void> { async savePrivacyCompliancePolicy(this: DatasetComplianceContainer): Promise<void> {
const complianceInfo = get(this, 'complianceInfo'); const complianceInfo = get(this, 'complianceInfo');
if (complianceInfo) { if (complianceInfo) {
return saveDatasetComplianceByUrn(get(this, 'urn'), complianceInfo); return this.notifyOnSave<void>(saveDatasetComplianceByUrn(get(this, 'urn'), complianceInfo));
} }
} }

View File

@ -1,12 +1,20 @@
{{dataset-compliance {{#if getContainerDataTask.last.isError}}
datasetName=datasetName
schemaless=schemaless {{empty-state heading="An error occurred getting this dataset's compliance policy"}}
platform=platform
complianceInfo=complianceInfo {{else}}
complianceSuggestion=complianceSuggestion
isNewComplianceInfo=isNewComplianceInfo {{dataset-compliance
schemaFieldNamesMappedToDataTypes=schemaFieldNamesMappedToDataTypes datasetName=datasetName
complianceDataTypes=complianceDataTypes schemaless=schemaless
onSave=(action "savePrivacyCompliancePolicy") platform=platform
onReset=(action "resetPrivacyCompliancePolicy") complianceInfo=complianceInfo
}} complianceSuggestion=complianceSuggestion
isNewComplianceInfo=isNewComplianceInfo
schemaFieldNamesMappedToDataTypes=schemaFieldNamesMappedToDataTypes
complianceDataTypes=complianceDataTypes
onSave=(action "savePrivacyCompliancePolicy")
onReset=(action "resetPrivacyCompliancePolicy")
}}
{{/if}}

View File

@ -101,6 +101,8 @@ const readDatasetComplianceByUrn = async (urn: string): Promise<IReadComplianceR
if (e instanceof ApiError && e.status === ApiResponseStatus.NotFound) { if (e instanceof ApiError && e.status === ApiResponseStatus.NotFound) {
complianceInfo = createInitialComplianceInfo(urn); complianceInfo = createInitialComplianceInfo(urn);
isNewComplianceInfo = true; isNewComplianceInfo = true;
} else {
throw e;
} }
} }

View File

@ -1,5 +1,6 @@
import { throwIfApiError } from 'wherehows-web/utils/api/errors/errors'; import { throwIfApiError, ApiError } from 'wherehows-web/utils/api/errors/errors';
import { module, test } from 'qunit'; import { module, test } from 'qunit';
import { ApiResponseStatus } from 'wherehows-web/utils/api/shared';
module('Unit | Utility | api/errors/errors'); module('Unit | Utility | api/errors/errors');
@ -13,3 +14,12 @@ test('throwIfApiError returns a Promise / thennable', function(assert) {
'invocation returns a Promise object / thennable' 'invocation returns a Promise object / thennable'
); );
}); });
test('ApiError subclasses built-in Error and has attributes', function(assert) {
const status = ApiResponseStatus.NotFound;
const apiError = new ApiError(status);
assert.ok(apiError instanceof Error, 'is an instanceof Error');
assert.ok(apiError.timestamp instanceof Date, 'has a valid timestamp');
assert.ok(apiError.status === status, 'has a status attribute');
});