diff --git a/wherehows-web/app/components/dataset-deprecation.ts b/wherehows-web/app/components/dataset-deprecation.ts index f98be57573..04402c7620 100644 --- a/wherehows-web/app/components/dataset-deprecation.ts +++ b/wherehows-web/app/components/dataset-deprecation.ts @@ -1,21 +1,14 @@ import Component from '@ember/component'; -import { inject } from '@ember/service'; import { getProperties, computed, set } from '@ember/object'; import ComputedProperty, { oneWay } from '@ember/object/computed'; import { baseCommentEditorOptions } from 'wherehows-web/constants'; -import Notifications, { NotificationEvent } from 'wherehows-web/services/notifications'; +import { action } from 'ember-decorators/object'; export default class DatasetDeprecation extends Component { tagName = 'div'; classNames = ['dataset-deprecation-toggle']; - /** - * References the application notifications service - * @memberof DatasetDeprecation - */ - notifications = >inject(); - /** * Flag indicating that the dataset is deprecated or otherwise * @type {(null | boolean)} @@ -68,11 +61,11 @@ export default class DatasetDeprecation extends Component { ); /** - * The action to be completed when a save is initiated - * @type {Function} + * The external action to be completed when a save is initiated + * @type {(isDeprecated: boolean, updateDeprecationNode: string) => Promise} * @memberof DatasetDeprecation */ - onUpdateDeprecation: Function; + onUpdateDeprecation: (isDeprecated: boolean, updateDeprecationNode: string) => Promise | void; editorOptions = { ...baseCommentEditorOptions, @@ -81,42 +74,29 @@ export default class DatasetDeprecation extends Component { } }; - actions = { - /** - * Toggles the boolean value of deprecatedAlias - */ - toggleDeprecatedStatus(this: DatasetDeprecation) { - this.toggleProperty('deprecatedAlias'); - }, + /** + * Toggles the boolean value of deprecatedAlias + */ + @action + toggleDeprecatedStatus(this: DatasetDeprecation) { + this.toggleProperty('deprecatedAlias'); + } - /** - * Invokes the save action with the updated values for - * deprecated and deprecationNote - */ - async onSave(this: DatasetDeprecation) { - const { deprecatedAlias, deprecationNoteAlias, notifications: { notify } } = getProperties(this, [ - 'deprecatedAlias', - 'deprecationNoteAlias', - 'notifications' - ]); - const { onUpdateDeprecation } = this; + /** + * Invokes the save action with the updated values for + * deprecated and deprecationNote + * @return {Promise} + */ + @action + async onSave(this: DatasetDeprecation) { + const { deprecatedAlias, deprecationNoteAlias } = getProperties(this, ['deprecatedAlias', 'deprecationNoteAlias']); + const { onUpdateDeprecation } = this; - if (onUpdateDeprecation) { - const noteValue = deprecatedAlias ? deprecationNoteAlias : ''; + if (onUpdateDeprecation) { + const noteValue = deprecatedAlias ? deprecationNoteAlias : ''; - try { - await onUpdateDeprecation(deprecatedAlias, noteValue); - set(this, 'deprecationNoteAlias', noteValue); - - notify(NotificationEvent.success, { - content: 'Successfully updated deprecation status' - }); - } catch (e) { - notify(NotificationEvent.error, { - content: `An error occurred: ${e.message}` - }); - } - } + await onUpdateDeprecation(deprecatedAlias, noteValue); + set(this, 'deprecationNoteAlias', noteValue); } - }; + } } diff --git a/wherehows-web/app/components/datasets/containers/dataset-compliance.ts b/wherehows-web/app/components/datasets/containers/dataset-compliance.ts index 3877d7e8e1..2f073e9e79 100644 --- a/wherehows-web/app/components/datasets/containers/dataset-compliance.ts +++ b/wherehows-web/app/components/datasets/containers/dataset-compliance.ts @@ -146,20 +146,20 @@ export default class DatasetComplianceContainer extends Component { setProperties(this, { schemaFieldNamesMappedToDataTypes, schemaless }); }); - @action /** * Persists the updates to the compliance policy on the remote host * @param {IComplianceInfo} complianceInfo * @return {Promise} */ + @action savePrivacyCompliancePolicy(complianceInfo: IComplianceInfo): Promise { return saveDatasetComplianceByUrn(get(this, 'urn'), complianceInfo); } - @action /** * Resets the compliance information for the dataset with the previously persisted properties */ + @action resetPrivacyCompliancePolicy() { get(this, 'getComplianceTask').perform(); } diff --git a/wherehows-web/app/components/datasets/containers/dataset-ownership.ts b/wherehows-web/app/components/datasets/containers/dataset-ownership.ts index 6319c3711e..891d2d171b 100644 --- a/wherehows-web/app/components/datasets/containers/dataset-ownership.ts +++ b/wherehows-web/app/components/datasets/containers/dataset-ownership.ts @@ -68,12 +68,12 @@ export default class DatasetOwnershipContainer extends Component { set(this, 'ownerTypes', ownerTypes); }); - @action /** * Persists the changes to the owners list * @param {Array} updatedOwners * @return {Promise} */ + @action saveOwnerChanges(updatedOwners: Array): Promise { return updateDatasetOwnersByUrn(get(this, 'urn'), '', updatedOwners); } diff --git a/wherehows-web/app/components/datasets/containers/dataset-properties.ts b/wherehows-web/app/components/datasets/containers/dataset-properties.ts index 09f0e0f541..95bb4ffb08 100644 --- a/wherehows-web/app/components/datasets/containers/dataset-properties.ts +++ b/wherehows-web/app/components/datasets/containers/dataset-properties.ts @@ -1,7 +1,10 @@ import Component from '@ember/component'; import { get, setProperties } from '@ember/object'; +import ComputedProperty from '@ember/object/computed'; +import { inject } from '@ember/service'; import { task } from 'ember-concurrency'; import { action } from 'ember-decorators/object'; +import Notifications, { NotificationEvent } from 'wherehows-web/services/notifications'; import { IDatasetView } from 'wherehows-web/typings/api/datasets/dataset'; import { readDatasetByUrn } from 'wherehows-web/utils/api/datasets/dataset'; import { updateDatasetDeprecationByUrn } from 'wherehows-web/utils/api/datasets/properties'; @@ -31,6 +34,13 @@ export default class DatasetPropertiesContainer extends Component { */ properties: Array = []; + /** + * References the application notifications service + * @memberof DatasetPropertiesContainer + * @type {ComputedProperty} + */ + notifications = >inject(); + constructor() { super(...arguments); this.deprecationNote || (this.deprecationNote = ''); @@ -55,15 +65,33 @@ export default class DatasetPropertiesContainer extends Component { setProperties(this, { deprecated, deprecationNote }); }); - @action /** * Persists the changes to the dataset deprecation properties upstream * @param {boolean} isDeprecated * @param {string} updatedDeprecationNote * @return {Promise} */ - async updateDeprecation(isDeprecated: boolean, updatedDeprecationNote: string): Promise { - await updateDatasetDeprecationByUrn(get(this, 'urn'), isDeprecated, updatedDeprecationNote); - get(this, 'getDeprecationPropertiesTask').perform(); + @action + async updateDeprecation( + this: DatasetPropertiesContainer, + isDeprecated: boolean, + updatedDeprecationNote: string + ): Promise { + const { notify } = get(this, 'notifications'); + + try { + await updateDatasetDeprecationByUrn(get(this, 'urn'), isDeprecated, updatedDeprecationNote); + + notify(NotificationEvent.success, { + content: 'Successfully updated deprecation status' + }); + } catch (e) { + notify(NotificationEvent.error, { + content: `An error occurred: ${e.message}` + }); + } finally { + // set current state + get(this, 'getDeprecationPropertiesTask').perform(); + } } } diff --git a/wherehows-web/app/components/datasets/containers/datasets-list.js b/wherehows-web/app/components/datasets/containers/datasets-list.js deleted file mode 100644 index 00744852a2..0000000000 --- a/wherehows-web/app/components/datasets/containers/datasets-list.js +++ /dev/null @@ -1,23 +0,0 @@ -import Component from '@ember/component'; -import { connect } from 'ember-redux'; - -/** - * A Selector function that takes the Redux Store and applies - * store state to props - * @param {Object} datasets is the slice of the store containing datasets - * and related state - * @return {{datasets: (any[]|Array), isFetching: boolean}} - */ -const stateToComputed = ({ datasets }) => { - const { byPage, byId, currentPage, isFetching = false } = datasets; - // List of datasets for the current Page - const pagedDatasetIds = byPage[currentPage] || []; - - return { - // Takes the normalized list of ids and maps to dataset objects - datasets: pagedDatasetIds.map(datasetId => byId[datasetId]), - isFetching - }; -}; - -export default connect(stateToComputed)(Component.extend({})); diff --git a/wherehows-web/app/constants/errors/errors.ts b/wherehows-web/app/constants/errors/errors.ts new file mode 100644 index 0000000000..f12c793053 --- /dev/null +++ b/wherehows-web/app/constants/errors/errors.ts @@ -0,0 +1,14 @@ +import { ApiStatusNumber } from 'wherehows-web/utils/api/shared'; + +/** + * Returns a default msg for a given status + * @param {ApiStatusNumber} status + * @returns {string} + */ +const apiErrorStatusMessage = (status: ApiStatusNumber): string => + (<{ [prop: number]: string }>{ + [ApiStatusNumber.NotFound]: 'Could not find the requested resource', + [ApiStatusNumber.InternalServerError]: 'An error occurred with the server' + })[status]; + +export { apiErrorStatusMessage }; diff --git a/wherehows-web/app/utils/api/datasets/properties.ts b/wherehows-web/app/utils/api/datasets/properties.ts index 099bc3602f..fad34bc831 100644 --- a/wherehows-web/app/utils/api/datasets/properties.ts +++ b/wherehows-web/app/utils/api/datasets/properties.ts @@ -25,8 +25,6 @@ interface IPropertyItem { */ const datasetPropertiesUrlById = (id: number) => `${datasetUrlById(id)}/properties`; -const datasetDeprecationUrlById = (id: number) => `${datasetUrlById(id)}/deprecate`; - /** * Returns the url for a dataset deprecation endpoint by urn * @param {string} urn @@ -143,7 +141,7 @@ const readNonPinotProperties = async (id: number): Promise> }; /** - * Describes the inteface of object returned from the api request to get pinot properties + * Describes the interface of object returned from the api request to get pinot properties * @interface IDatasetSamplesAndColumns */ interface IDatasetSamplesAndColumns { @@ -184,26 +182,6 @@ const readPinotProperties = async (id: number) => { } }; -/** - * Updates the properties on the dataset for deprecation - * @param {number} id the id of the dataset - * @param {boolean} deprecated flag indicating deprecation - * @param {string} [deprecationNote=''] optional note accompanying deprecation change - */ -const updateDatasetDeprecation = async (id: number, deprecated: boolean, deprecationNote: string = '') => { - const { status, msg } = await putJSON<{ status: ApiStatus; msg: string }>({ - url: datasetDeprecationUrlById(id), - data: { - deprecated, - deprecationNote - } - }); - - if (status !== ApiStatus.OK) { - throw new Error(msg); - } -}; - /** * Persists the changes to a datasets deprecation properties by urn * @param {string} urn @@ -211,24 +189,13 @@ const updateDatasetDeprecation = async (id: number, deprecated: boolean, depreca * @param {string} deprecationNote * @return {Promise} */ -const updateDatasetDeprecationByUrn = ( - urn: string, - deprecated: boolean, - deprecationNote: string = '' -): Promise => { - return putJSON({ +const updateDatasetDeprecationByUrn = (urn: string, deprecated: boolean, deprecationNote: string = ''): Promise => + putJSON({ url: datasetDeprecationUrlByUrn(urn), data: { deprecated, deprecationNote } }); -}; -export { - readDatasetProperties, - readNonPinotProperties, - readPinotProperties, - updateDatasetDeprecation, - updateDatasetDeprecationByUrn -}; +export { readDatasetProperties, readNonPinotProperties, readPinotProperties, updateDatasetDeprecationByUrn }; diff --git a/wherehows-web/app/utils/api/errors/errors.ts b/wherehows-web/app/utils/api/errors/errors.ts new file mode 100644 index 0000000000..5a455c0f3c --- /dev/null +++ b/wherehows-web/app/utils/api/errors/errors.ts @@ -0,0 +1,21 @@ +import { apiErrorStatusMessage } from 'wherehows-web/constants/errors/errors'; + +/** + * Wraps a Response object, pass through json response if no api error, + * otherwise raise exception with error message + * @template T + * @param {Response} response + * @returns {Promise} + */ +const throwIfApiError = async (response: Response): Promise => { + const { status, ok } = response; + + if (!ok) { + const { msg = apiErrorStatusMessage(status) } = await response.json(); + throw new Error(msg); + } + + return response.json(); +}; + +export { throwIfApiError }; diff --git a/wherehows-web/app/utils/api/fetcher.ts b/wherehows-web/app/utils/api/fetcher.ts index b7de866741..5cf20b5979 100644 --- a/wherehows-web/app/utils/api/fetcher.ts +++ b/wherehows-web/app/utils/api/fetcher.ts @@ -1,4 +1,5 @@ import fetch from 'fetch'; +import { throwIfApiError } from 'wherehows-web/utils/api/errors/errors'; /** * Describes the attributes on the fetch configuration object @@ -10,7 +11,7 @@ interface FetchConfig { } /** - * Desribes the available options on an option bag to be passed into a fetch call + * Describes the available options on an option bag to be passed into a fetch call * @interface IFetchOptions */ interface IFetchOptions { @@ -38,11 +39,11 @@ const withBaseFetchHeaders = (headers: FetchConfig['headers']): { headers: Fetch * Sends a HTTP request and resolves with the JSON response * @template T * @param {string} url the url for the endpoint to request a response from - * @param {object} fetchConfig - * @returns {Promise} + * @param {object} fetchConfig + * @returns {Promise} */ const json = (url: string = '', fetchConfig: IFetchOptions = {}): Promise => - fetch(url, fetchConfig).then(response => response.json()); + fetch(url, fetchConfig).then(response => throwIfApiError(response)); /** * Conveniently gets a JSON response using the fetch api @@ -59,8 +60,8 @@ const getJSON = (config: FetchConfig): Promise => { /** * Initiates a POST request using the Fetch api * @template T - * @param {FetchConfig} config - * @returns {Promise} + * @param {FetchConfig} config + * @returns {Promise} */ const postJSON = (config: FetchConfig): Promise => { const requestBody = config.data ? { body: JSON.stringify(config.data) } : {}; @@ -120,22 +121,4 @@ const getHeaders = async (config: FetchConfig): Promise => { throw new Error(statusText); }; -/** - * Wraps a request Promise, pass-through response if successful, otherwise handle the error and rethrow if not api error - * @template T - * @param {Promise} fetcher the api request to wrap - * @param {T} defaultValue - * @returns {Promise} - */ -const fetchAndHandleIfApiError = async (fetcher: Promise, defaultValue: T): Promise => { - let result = typeof defaultValue === 'undefined' ? null : defaultValue; - try { - result = await fetcher; - } catch (e) { - // TODO: if error is an api error, display notification and allow default return - // otherwise throw - } - return result; -}; - -export { getJSON, postJSON, deleteJSON, putJSON, getHeaders, fetchAndHandleIfApiError }; +export { getJSON, postJSON, deleteJSON, putJSON, getHeaders }; diff --git a/wherehows-web/app/utils/api/shared.ts b/wherehows-web/app/utils/api/shared.ts index e97dd81c62..fd84c2b543 100644 --- a/wherehows-web/app/utils/api/shared.ts +++ b/wherehows-web/app/utils/api/shared.ts @@ -21,3 +21,13 @@ export enum ApiStatus { FAILED = 'failed', ERROR = 'error' } + +/** + * Enumerates the currently available Api statuses + * @type {number} + */ +export enum ApiStatusNumber { + NotFound = 404, + UnAuthorized = 401, + InternalServerError = 500 +} diff --git a/wherehows-web/tests/integration/components/dataset-deprecation-test.js b/wherehows-web/tests/integration/components/dataset-deprecation-test.js index 0a2d67d989..794732e720 100644 --- a/wherehows-web/tests/integration/components/dataset-deprecation-test.js +++ b/wherehows-web/tests/integration/components/dataset-deprecation-test.js @@ -5,13 +5,7 @@ import hbs from 'htmlbars-inline-precompile'; import { run } from '@ember/runloop'; moduleForComponent('dataset-deprecation', 'Integration | Component | dataset deprecation', { - integration: true, - - beforeEach() { - this.register('service:notifications', notificationsStub); - - this.inject.service('notifications'); - } + integration: true }); test('it renders', function(assert) { diff --git a/wherehows-web/tests/unit/utils/api/errors/errors-test.js b/wherehows-web/tests/unit/utils/api/errors/errors-test.js new file mode 100644 index 0000000000..a5a2b00bf8 --- /dev/null +++ b/wherehows-web/tests/unit/utils/api/errors/errors-test.js @@ -0,0 +1,15 @@ +import { throwIfApiError } from 'wherehows-web/utils/api/errors/errors'; +import { module, test } from 'qunit'; + +module('Unit | Utility | api/errors/errors'); + +test('throwIfApiError exists', function(assert) { + assert.ok(typeof throwIfApiError === 'function', 'throwIfApiError exists as a function'); +}); + +test('throwIfApiError returns a Promise / thennable', function(assert) { + assert.ok( + typeof throwIfApiError({ status: 200, ok: true, json: () => Promise.resolve() }).then === 'function', + 'invocation returns a Promise object / thennable' + ); +}); diff --git a/wherehows-web/tests/unit/utils/api/fetcher-test.js b/wherehows-web/tests/unit/utils/api/fetcher-test.js index 14082e0923..764f493cb6 100644 --- a/wherehows-web/tests/unit/utils/api/fetcher-test.js +++ b/wherehows-web/tests/unit/utils/api/fetcher-test.js @@ -1,11 +1,4 @@ -import { - getJSON, - postJSON, - deleteJSON, - putJSON, - getHeaders, - fetchAndHandleIfApiError -} from 'wherehows-web/utils/api/fetcher'; +import { getJSON, postJSON, deleteJSON, putJSON, getHeaders } from 'wherehows-web/utils/api/fetcher'; import { module, test } from 'qunit'; import sinon from 'sinon'; @@ -20,7 +13,7 @@ module('Unit | Utility | api/fetcher', { }); test('each http request function exists', function(assert) { - [getJSON, postJSON, deleteJSON, putJSON, getHeaders, fetchAndHandleIfApiError].forEach(httpRequest => + [getJSON, postJSON, deleteJSON, putJSON, getHeaders].forEach(httpRequest => assert.ok(typeof httpRequest === 'function', `${httpRequest} is a function`) ); });