diff --git a/wherehows-web/app/components/dataset-author.js b/wherehows-web/app/components/dataset-author.js index 6383287a24..bbeca721a9 100644 --- a/wherehows-web/app/components/dataset-author.js +++ b/wherehows-web/app/components/dataset-author.js @@ -174,6 +174,20 @@ export default Component.extend({ updatedOwner, ...updatingOwners.slice(ownerPosition + 1) ]; + // The list of ldap userNames currently in the list + const userNames = updatedOwners.mapBy('userName'); + // Checks that the userNames are not already in the list of current owners + const hasDuplicates = new Set(userNames).size !== userNames.length; + + if (hasDuplicates) { + set( + this, + 'errorMessage', + 'Uh oh! Looks like there are duplicates in the list of owners, please remove them first.' + ); + + return; + } // Full reset of the `owners` list with the new list sourceOwners.setObjects(updatedOwners); @@ -213,6 +227,7 @@ export default Component.extend({ * @return {Promise.} */ async editUserName(currentOwner, userName) { + set(this, 'errorMessage', ''); if (userName) { // getUser returns a promise, treat as such const getUser = get(this, 'ldapUsers.getPartyEntityWithUserName'); diff --git a/wherehows-web/app/routes/datasets/dataset.js b/wherehows-web/app/routes/datasets/dataset.js index 8d23ac352a..c7b7813c6a 100644 --- a/wherehows-web/app/routes/datasets/dataset.js +++ b/wherehows-web/app/routes/datasets/dataset.js @@ -3,9 +3,8 @@ import { makeUrnBreadcrumbs } from 'wherehows-web/utils/entities'; import { datasetComplianceFor, datasetComplianceSuggestionsFor } from 'wherehows-web/utils/api/datasets/compliance'; import { getDatasetOwners, - getPartyEntities, - isRequiredMinOwnersNotConfirmed, - getPartyEntitiesMap + getUserEntities, + isRequiredMinOwnersNotConfirmed } from 'wherehows-web/utils/api/datasets/owners'; const { Route, get, set, setProperties, isPresent, inject: { service }, $: { getJSON } } = Ember; @@ -395,15 +394,15 @@ export default Route.extend({ // Retrieve the current owners of the dataset and store on the controller (async id => { - const [owners, userEntities] = await Promise.all([getDatasetOwners(id), getPartyEntities()]); + const [owners, { userEntitiesSource, userEntitiesMaps }] = await Promise.all([ + getDatasetOwners(id), + getUserEntities() + ]); setProperties(controller, { requiredMinNotConfirmed: isRequiredMinOwnersNotConfirmed(owners), owners, - userEntitiesMaps: getPartyEntitiesMap(userEntities), - get userEntitiesSource() { - // TODO: memoize - return Object.keys(this.userEntitiesMaps); - } + userEntitiesMaps, + userEntitiesSource }); })(id); }, diff --git a/wherehows-web/app/services/user-lookup.js b/wherehows-web/app/services/user-lookup.js deleted file mode 100644 index 86ff5ce41b..0000000000 --- a/wherehows-web/app/services/user-lookup.js +++ /dev/null @@ -1,74 +0,0 @@ -import Ember from 'ember'; - -const { isEmpty, Service, $: { getJSON } } = Ember; -const partyEntitiesUrl = '/api/v1/party/entities'; - -const cache = { - // Cache containing results from the last request to partyEntities api - partyEntities: null -}; - -/** - * Async request for partyEntities. Caches the value from the initial request - * @return {Promise.} - */ -const getLDAPUsers = () => - new Promise(resolve => { - const cachedResults = cache.partyEntities; - - // Resolve with cachedResults if this has been previously requested - if (!isEmpty(cachedResults)) { - return resolve(cachedResults); - } - - // Cast $.getJSON to native Promise - Promise.resolve(getJSON(partyEntitiesUrl)) - .then(({ status, userEntities = [] }) => { - if (status === 'ok' && userEntities.length) { - /** - * @type {Object} userEntitiesMaps hash of userEntities: label -> displayName - */ - const userEntitiesMaps = userEntities.reduce( - (map, { label, displayName }) => ((map[label] = displayName), map), - {} - ); - - return { - userEntities, - userEntitiesMaps, - userEntitiesSource: Object.keys(userEntitiesMaps) - }; - } - }) - .then(results => (cache.partyEntities = results)) - .then(resolve); - }); - -/** - * Takes a userNameQuery query and find userNames that match by starting with - * the pattern - * @param {String} userNameQuery pattern to search for - * @param {Function} syncResults callback - * @param {Function} asyncResults callback - */ -const ldapResolver = (userNameQuery, syncResults, asyncResults) => { - const regex = new RegExp(`^${userNameQuery}.*`, 'i'); - - getLDAPUsers() - .then(({ userEntitiesSource = {} }) => userEntitiesSource.filter(entity => regex.test(entity))) - .then(asyncResults); -}; - -/** - * For a given userName, find the userEntity object that contains the userName - * @param {String} userName the unique userName - * @return {Promise.} resolves with the userEntity or null otherwise - */ -const getPartyEntityWithUserName = userName => - getLDAPUsers().then(({ userEntities }) => userEntities.find(({ label }) => label === userName) || null); - -export default Service.extend({ - getPartyEntityWithUserName, - userNamesResolver: ldapResolver, - fetchUserNames: getLDAPUsers -}); diff --git a/wherehows-web/app/services/user-lookup.ts b/wherehows-web/app/services/user-lookup.ts new file mode 100644 index 0000000000..c137a26247 --- /dev/null +++ b/wherehows-web/app/services/user-lookup.ts @@ -0,0 +1,35 @@ +import Ember from 'ember'; +import { getUserEntities } from 'wherehows-web/utils/api/datasets/owners'; +import { IPartyEntity, IPartyProps } from 'wherehows-web/typings/api/datasets/party-entities'; + +const { Service } = Ember; + +/** + * Takes a userNameQuery query and find userNames that match by starting with + * the pattern + * @param {string} userNameQuery pattern to search for + * @param {Function} _syncResults callback + * @param {Function} asyncResults callback + * @return {Promise} + */ +const ldapResolver = async (userNameQuery: string, _syncResults: Function, asyncResults: Function): Promise => { + const ldapRegex = new RegExp(`^${userNameQuery}.*`, 'i'); + const { userEntitiesSource = [] }: IPartyProps = await getUserEntities(); + asyncResults(userEntitiesSource.filter((entity: string) => ldapRegex.test(entity))); +}; + +/** + * For a given userName, find the userEntity object that contains the userName + * @param {string} userName the unique userName + * @return {Promise} resolves with the userEntity or null otherwise + */ +const getPartyEntityWithUserName = (userName: string): Promise => + getUserEntities().then( + ({ userEntities }: IPartyProps) => userEntities.find(({ label }: { label: string }) => label === userName) || null + ); + +export default Service.extend({ + getPartyEntityWithUserName, + userNamesResolver: ldapResolver, + fetchUserNames: getUserEntities +}); diff --git a/wherehows-web/app/typings/api/datasets/party-entities.d.ts b/wherehows-web/app/typings/api/datasets/party-entities.d.ts index b157fada7d..29db48e72a 100644 --- a/wherehows-web/app/typings/api/datasets/party-entities.d.ts +++ b/wherehows-web/app/typings/api/datasets/party-entities.d.ts @@ -16,3 +16,19 @@ export interface IPartyEntityResponse { status: ApiStatus; userEntities?: Array; } + +/** + * Describes a userEntityMap interface + */ +export interface userEntityMap { + [label: string]: string; +} + +/** + * Describes the props resolved by the getUserEntities function + */ +export interface IPartyProps { + userEntities: Array; + userEntitiesMaps: userEntityMap; + userEntitiesSource: Array; +} diff --git a/wherehows-web/app/utils/api/datasets/owners.ts b/wherehows-web/app/utils/api/datasets/owners.ts index 1e820813ba..41e4ab8e34 100644 --- a/wherehows-web/app/utils/api/datasets/owners.ts +++ b/wherehows-web/app/utils/api/datasets/owners.ts @@ -1,7 +1,12 @@ import Ember from 'ember'; import { ApiRoot, ApiStatus } from 'wherehows-web/utils/api/shared'; import { datasetUrlById } from 'wherehows-web/utils/api/datasets/shared'; -import { IPartyEntity, IPartyEntityResponse } from 'wherehows-web/typings/api/datasets/party-entities'; +import { + IPartyEntity, + IPartyEntityResponse, + IPartyProps, + userEntityMap +} from 'wherehows-web/typings/api/datasets/party-entities'; import { IOwner, IOwnerResponse } from 'wherehows-web/typings/api/datasets/owners'; /** @@ -48,12 +53,64 @@ export const getPartyEntities = async (): Promise> => { return status === ApiStatus.OK ? userEntities : Promise.reject(status); }; +/** + * IIFE prepares the environment scope and returns a closure function that ensures that + * there is ever only one inflight request for userEntities. + * Resolves all subsequent calls with the result for the initial invocation. + * userEntitiesSource property is also lazy evaluated and cached for app lifetime. + * @type {() => Promise} + */ +export const getUserEntities: () => Promise = (() => { + /** + * Memoized reference to the resolved value of a previous invocation to curried function in getUserEntities + * @type {{result: IPartyProps | null}} + */ + const cache: { result: IPartyProps | null; userEntitiesSource: Array } = { + result: null, + userEntitiesSource: [] + }; + let inflightRequest: Promise>; + + /** + * Invokes the requestor for party entities, and adds perf optimizations listed above + * @return {Promise} + */ + return async (): Promise => { + // If a previous request has already resolved, return the cached value + if (cache.result) { + return cache.result; + } + // If we don't already have a previous api request for party entities, + // assign a new one to free variable + if (!inflightRequest) { + inflightRequest = getPartyEntities(); + } + + const userEntities: Array = await inflightRequest; + + return (cache.result = { + userEntities, + userEntitiesMaps: getPartyEntitiesMap(userEntities), + // userEntitiesSource is not usually needed immediately + // hence using a getter for lazy evaluation + get userEntitiesSource() { + const userEntitiesSource = cache.userEntitiesSource; + if (userEntitiesSource.length) { + return userEntitiesSource; + } + + return (cache.userEntitiesSource = Object.keys(this.userEntitiesMaps)); + } + }); + }; +})(); + /** * Transforms a list of party entities into a map of entity label to displayName value * @param {Array} partyEntities * @return {Object} */ -export const getPartyEntitiesMap = (partyEntities: Array): { [label: string]: string } => +export const getPartyEntitiesMap = (partyEntities: Array): userEntityMap => partyEntities.reduce( (map: { [label: string]: string }, { label, displayName }: IPartyEntity) => ((map[label] = displayName), map), {}