import WithEntityLists from '@datahub/lists/components/with-entity-lists'; // @ts-ignore: Ignore import of compiled template import template from '../templates/components/entity-list-container'; import { layout, tagName } from '@ember-decorators/component'; import { DataModelEntity, DataModelName, DataModelEntityInstance } from '@datahub/data-models/constants/entity'; import { computed, action } from '@ember/object'; import { findEntityInList } from '@datahub/lists/utils'; import { alias, map, gt } from '@ember/object/computed'; import { capitalize } from 'lodash'; import { IEntityLinkAttrs } from '@datahub/data-models/types/entity/shared'; import { singularize } from 'ember-inflector'; import { Snapshot } from '@datahub/metadata-types/types/metadata/snapshot'; import { IStoredEntityAttrs } from '@datahub/lists/types/list'; import { set } from '@ember/object'; import { setProperties } from '@ember/object'; import Notifications from '@datahub/utils/services/notifications'; import { inject as service } from '@ember/service'; import { NotificationEvent } from '@datahub/utils/constants/notifications'; import { task } from 'ember-concurrency'; import { ETask } from '@datahub/utils/types/concurrency'; /** * Defines the interface for the output of the EntityList mapping predicate function * Outputs an entity with its generated link attributes */ interface IEntityWithLink { // References the entity for which the link attributes should be generated entity: DataModelEntityInstance; // Generated link attributes for the aforementioned entity linkAttr?: IEntityLinkAttrs; } /** * Container component for a list of Entities * Provides actions for the entire list and data transformation * @export * @class EntityListContainer * @extends {WithEntityLists} */ @layout(template) @tagName('') export default class EntityListContainer extends WithEntityLists { /** * DataHub host application notifications service */ @service('notifications') notificationsService?: Notifications; /** * The type of DataModel entity being rendered */ entityType?: DataModelName; /** * Retains a subset of the currently selected DataModelEntity types from the items in the EntityList * This list is a non durable list because operations rely on object references for referential integrity * If persistence is required for this list, then referencing items between other lists will requires * more durable reference, for example using entity urns */ selectedEntities: Array = []; /** * A reference to the Entity class * Required for the computed property dependent computed property entityListTrampoline * @readonly */ @computed('entityType') get entity(): DataModelEntity | null { const { entityType } = this; return entityType ? DataModelEntity[entityType] : null; } /** * Singularized entity name * @readonly */ @computed('entity') get name(): string { const { entity } = this; return entity ? singularize(entity.displayName) : ''; } /** * Name of the list header * @readonly */ @computed('name') get listName(): string { const { name } = this; return name ? `${capitalize(name)} list` : ''; } /** * Number of items mapped from the source list */ @alias('entityListWithLinkAttrs.length') listCount?: number; /** * Flag indicating that more than one entity from the list is currently selected */ @gt('selectedEntities.length', 1) hasMultipleSelected!: boolean; /** * Flag indicating that all the items in the list have been selected * Weak comparison on list length equality, suffices for the moment and * cheaper than a full value equality or deep equality check which may come at cost for longer lists * @readonly */ @computed('listCount', 'selectedEntities.length') get selectedAll(): boolean { const { selectedEntities: { length }, listCount } = this; // Exclude zero length list return Boolean(length) && listCount === length; } /** * References the associated entity list * @readonly */ @alias('entityListTrampoline.list') list!: ReadonlyArray; /** * Hydrated list of entities populated on initialization from the urns serialized in persistent storage */ instances: Array = []; /** * For each entity in the entity list, outputs a decoration with IEntityLinkAttrs instance * This allows linking the list entity to the entity page */ @map('instances', function(this: EntityListContainer, instance: DataModelEntityInstance): IEntityWithLink { // The entity class (statics) associated with this container const { entity: entityType } = this; const entityWithLink: IEntityWithLink = { entity: instance }; if (entityType && instance) { const linkAttr = instance.entityLink; return linkAttr ? { ...entityWithLink, linkAttr } : entityWithLink; } return entityWithLink; }) entityListWithLinkAttrs!: Array; /** * Toggles a selected state when an entity in the list is selected or deselected in the ui * Toggle action is determined based on presence of entity in selection subset: selectedEntities * i.e. if absent added, otherwise removed */ @action onSelectEntity(selectedEntity: DataModelEntityInstance): void { const { selectedEntities } = this; const isInSelection = findEntityInList(selectedEntities)(selectedEntity) as DataModelEntityInstance; isInSelection ? selectedEntities.removeObject(isInSelection) : selectedEntities.addObject(selectedEntity); } /** * Handles the change event for the list (group) checkbox element * Toggling to checked will add all items in the list, otherwise clear the list */ @action onSelectEntityList(): void { const { selectedEntities, instances } = this; this.selectedAll ? // Remove all items from selected entities list selectedEntities.setObjects([]) : // Add all items to list // If there are items in the list , then replace the selected entities list with all items instances.length && selectedEntities.setObjects([...instances]); } /** * Handler removes all the selected items from the entity list and the selected entities list */ @action removeAllSelected(): void { const { selectedEntities, listsManager, notificationsService, entity } = this; const selectedCount = selectedEntities.length; const notificationMessage = entity ? `${selectedCount} ${entity.displayName} removed from list successfully` : ''; // Remove entities from lists manager service listsManager && listsManager.removeFromList(selectedEntities); // Also remove from component selected list selectedEntities.setObjects([]); // On removal, refresh instances this.hydrateEntitiesTask.perform(); notificationsService && notificationsService.notify({ type: NotificationEvent.success, content: notificationMessage }); } /** * Reads the list of urn stored in persistent storage and then batch queries the remote for snapshots and entities for hydration * then populates a local attribute with the received list of entities */ @(task(function*( this: EntityListContainer ): IterableIterator> | Promise>> { const { entity } = this; // Extract urns from the serialization list const urns = this.list.map(({ urn }): string => urn); if (entity && urns.length) { // Hydrate entity instances with Snapshot and IBaseEntity attributes const snapshots: Array = yield entity.readSnapshots(urns); // IBaseEntity property (entity) hydration happens in an async iteration because attributes batch GET endpoint for entities is N/A currently const instances: Array = yield Promise.all( snapshots.map( async (snapshot): Promise => { const listEntity = new entity(snapshot.urn); setProperties(listEntity, { snapshot, entity: await listEntity.readEntity }); return listEntity; } ) ); return set(this, 'instances', instances); } return set(this, 'instances', []); }).restartable()) hydrateEntitiesTask!: ETask; /** * On initialization, hydrate the entities list with data serialized in persistent storage */ init(): void { super.init(); this.hydrateEntitiesTask.perform(); } }