mirror of
https://github.com/datahub-project/datahub.git
synced 2026-01-06 14:57:12 +00:00
Manual merge with more internal changes
This commit is contained in:
parent
a67bf284fa
commit
e2f985a298
@ -1,4 +1,5 @@
|
||||
import { DatasetEntity } from '@datahub/data-models/entity/dataset/dataset-entity';
|
||||
import { PersonEntity } from '@datahub/data-models/entity/person/person-entity';
|
||||
|
||||
/**
|
||||
* Defines the interface for the DataModelEntity enum below.
|
||||
@ -6,6 +7,7 @@ import { DatasetEntity } from '@datahub/data-models/entity/dataset/dataset-entit
|
||||
*/
|
||||
interface IDataModelEntity {
|
||||
[DatasetEntity.displayName]: typeof DatasetEntity;
|
||||
[PersonEntity.displayName]: typeof PersonEntity;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -13,7 +15,8 @@ interface IDataModelEntity {
|
||||
* Serves as the primary resource map of all DataModelEntity available classes
|
||||
*/
|
||||
export const DataModelEntity: IDataModelEntity = {
|
||||
[DatasetEntity.displayName]: DatasetEntity
|
||||
[DatasetEntity.displayName]: DatasetEntity,
|
||||
[PersonEntity.displayName]: PersonEntity
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@ -2,5 +2,6 @@
|
||||
* Constant base for the user profile link. We append the username to this to reach their
|
||||
* profile
|
||||
* @type {string}
|
||||
* @deprecated - should be moved to internal version
|
||||
*/
|
||||
export const profileLinkBase = 'https://cinco.linkedin.biz/people/';
|
||||
|
||||
@ -1,33 +1,163 @@
|
||||
import getActorFromUrn from '@datahub/data-models/utils/get-actor-from-urn';
|
||||
import { computed } from '@ember/object';
|
||||
import { profileLinkBase } from '@datahub/data-models/constants/entity/person/links';
|
||||
import { NotImplementedError } from '@datahub/data-models/constants/entity/shared';
|
||||
import {
|
||||
getRenderProps,
|
||||
IPersonEntitySpecificConfigs,
|
||||
getPersonEntitySpecificRenderProps
|
||||
} from '@datahub/data-models/entity/person/render-props';
|
||||
import { DatasetEntity } from '@datahub/data-models/entity/dataset/dataset-entity';
|
||||
import { BaseEntity, statics, IBaseEntityStatics } from '@datahub/data-models/entity/base-entity';
|
||||
import { IBaseEntity } from '@datahub/metadata-types/types/entity';
|
||||
import { IEntityRenderProps } from '@datahub/data-models/types/entity/rendering/entity-render-props';
|
||||
import { DataModelEntity } from '@datahub/data-models/constants/entity';
|
||||
|
||||
// TODO: [META-9699] Temporarily using IBaseEntity until we have a proposed API structure for
|
||||
// IPersonEntity
|
||||
@statics<IBaseEntityStatics<IBaseEntity>>()
|
||||
export class PersonEntity extends BaseEntity<IBaseEntity> {
|
||||
/**
|
||||
* The human friendly alias for Dataset entities
|
||||
*/
|
||||
static displayName: 'people' = 'people';
|
||||
|
||||
export class PersonEntity {
|
||||
/**
|
||||
* Static util function that can extract a username from the urn for a person entity using whatever
|
||||
* custom logic is necessary to accomplish this
|
||||
* @param urn - person entity identifier
|
||||
* @deprecated
|
||||
* Should be removed as part of open source. Definition will be on LiPersonEntity
|
||||
* TODO: [META-9698] Migrate to using LiPersonEntity
|
||||
*/
|
||||
static usernameFromUrn(urn: string): string {
|
||||
return getActorFromUrn(urn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Static util function that can reverse the extraction of a username from urn for a person
|
||||
* entity and return to a urn (assuming the two are different)
|
||||
* IMPLEMENTATION NEEDED - This is only an open source interface definition
|
||||
* @param {string} username - the username to be converted
|
||||
* @static
|
||||
*/
|
||||
static urnFromUsername: (username: string) => string;
|
||||
|
||||
/**
|
||||
* Static util function that can provide a profile page link for a particular username
|
||||
* @param username - username for the person entity. Can be different from urn
|
||||
* @deprecated
|
||||
* Should be removed as part of open source. Definition will be on LiPersonEntity
|
||||
* TODO: [META-9698] Migrate to using LiPersonEntity
|
||||
*/
|
||||
static profileLinkFromUsername(username: string): string {
|
||||
return `${profileLinkBase}${username}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifier for the person entity
|
||||
* Class properties common across instances
|
||||
* Dictates how visual ui components should be rendered
|
||||
* Implemented as a getter to ensure that reads are idempotent
|
||||
* @readonly
|
||||
* @static
|
||||
*/
|
||||
urn: string;
|
||||
static get renderProps(): IEntityRenderProps {
|
||||
return getRenderProps();
|
||||
}
|
||||
|
||||
static ownershipEntities: Array<{ entity: DataModelEntity; getter: keyof PersonEntity }> = [
|
||||
{ entity: DatasetEntity, getter: 'readDatasetOwnership' }
|
||||
];
|
||||
|
||||
/**
|
||||
* Properties for render props that are only applicable to the person entity. Dictates how UI
|
||||
* components should be rendered for this entity
|
||||
*/
|
||||
static get personEntityRenderProps(): IPersonEntitySpecificConfigs {
|
||||
return getPersonEntitySpecificRenderProps();
|
||||
}
|
||||
|
||||
/**
|
||||
* Combined render properties for the generic entity render props + all person entity specific
|
||||
* render properties
|
||||
*/
|
||||
static get allRenderProps(): IEntityRenderProps & IPersonEntitySpecificConfigs {
|
||||
return { ...getRenderProps(), ...getPersonEntitySpecificRenderProps() };
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows access to the static display name of the entity from an instance
|
||||
*/
|
||||
get displayName(): 'people' {
|
||||
return PersonEntity.displayName;
|
||||
}
|
||||
|
||||
/**
|
||||
* The person's human readable name
|
||||
*/
|
||||
name!: string;
|
||||
|
||||
/**
|
||||
* The person's title at the company
|
||||
*/
|
||||
title!: string;
|
||||
|
||||
/**
|
||||
* Url link to the person's profile picture
|
||||
*/
|
||||
profilePictureUrl!: string;
|
||||
|
||||
/**
|
||||
* identifier for the person that this person reports to
|
||||
*/
|
||||
reportsToUrn?: string;
|
||||
|
||||
/**
|
||||
* Actual reference to related entity for this person
|
||||
*/
|
||||
reportsTo?: PersonEntity;
|
||||
|
||||
/**
|
||||
* User's email address
|
||||
*/
|
||||
email!: string;
|
||||
|
||||
/**
|
||||
* A list of skills that this particular person entity has declared to own.
|
||||
*/
|
||||
skills: Array<string> = [];
|
||||
|
||||
/**
|
||||
* A link to the user's linkedin profile
|
||||
*/
|
||||
linkedinProfile?: string;
|
||||
|
||||
/**
|
||||
* A link to the user through slack
|
||||
*/
|
||||
slackLink?: string;
|
||||
|
||||
/**
|
||||
* List of datasets owned by this particular user entity
|
||||
*/
|
||||
datasetOwnership?: Array<DatasetEntity>;
|
||||
|
||||
/**
|
||||
* User-provided focus area, describing themselves and what they do
|
||||
*/
|
||||
focusArea: string = '';
|
||||
|
||||
/**
|
||||
* Tags that in aggregate denote which team and organization to which the user belongs
|
||||
*/
|
||||
teamTags: Array<string> = [];
|
||||
|
||||
/**
|
||||
* Computes the username for easy access from the urn
|
||||
* @type {string}
|
||||
* @deprecated
|
||||
* Should be removed in favor of adding this to internal version of the class
|
||||
* TODO: [META-9698] Migrate to using LiPersonEntity
|
||||
*/
|
||||
@computed('urn')
|
||||
get username(): string {
|
||||
@ -43,7 +173,17 @@ export class PersonEntity {
|
||||
return PersonEntity.profileLinkFromUsername(this.username);
|
||||
}
|
||||
|
||||
constructor(urn: string) {
|
||||
this.urn = urn;
|
||||
/**
|
||||
* Retrieves the basic entity information for the person
|
||||
*/
|
||||
get readEntity(): Promise<IBaseEntity> {
|
||||
throw new Error(NotImplementedError);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the datasets for which this person entity has ownership.
|
||||
*/
|
||||
readDatasetOwnership(): Promise<Array<DatasetEntity>> {
|
||||
throw new Error(NotImplementedError);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,59 @@
|
||||
import { IEntityRenderProps } from '@datahub/data-models/types/entity/rendering/entity-render-props';
|
||||
import { Tab } from '@datahub/data-models/constants/entity/shared/tabs';
|
||||
import { getTabPropertiesFor } from '@datahub/data-models/entity/utils';
|
||||
|
||||
/**
|
||||
* Specific render properties only to the person entity
|
||||
*/
|
||||
export interface IPersonEntitySpecificConfigs {
|
||||
userProfilePage: {
|
||||
headerProperties: {
|
||||
showExternalProfileLink?: boolean;
|
||||
externalProfileLinkText?: string;
|
||||
isConnectedToLinkedin?: boolean;
|
||||
isConnectedToSlack?: boolean;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Class properties common across instances
|
||||
* Dictates how visual ui components should be rendered
|
||||
* Implemented as a getter to ensure that reads are idempotent
|
||||
*/
|
||||
export const getRenderProps = (): IEntityRenderProps => {
|
||||
const tabIds = [Tab.Metadata];
|
||||
|
||||
return {
|
||||
entityPage: {
|
||||
tabIds,
|
||||
tabProperties: getTabPropertiesFor(tabIds),
|
||||
defaultTab: Tab.Metadata,
|
||||
attributePlaceholder: '–'
|
||||
},
|
||||
// Placeholder information
|
||||
search: {
|
||||
attributes: [],
|
||||
placeholder: '',
|
||||
apiName: ''
|
||||
},
|
||||
// Placeholder information
|
||||
browse: {
|
||||
showCount: false,
|
||||
showHierarchySearch: false,
|
||||
entityRoute: 'user.profile'
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Properties for render props that are only applicable to the person entity. Dictates how UI
|
||||
* components should be rendered for this entity
|
||||
*/
|
||||
export const getPersonEntitySpecificRenderProps = (): IPersonEntitySpecificConfigs => ({
|
||||
userProfilePage: {
|
||||
headerProperties: {
|
||||
showExternalProfileLink: false
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,39 @@
|
||||
import { resolveDynamicRouteName } from '@datahub/utils/routes/routing';
|
||||
import { DatasetEntity } from '@datahub/data-models/entity/dataset/dataset-entity';
|
||||
import { MaybeRouteInfoWithAttributes } from '@datahub/utils/types/vendor/routerjs';
|
||||
import { listOfEntitiesMap } from '@datahub/data-models/entity/utils/entities';
|
||||
import Transition from '@ember/routing/-private/transition';
|
||||
import { DataModelEntity } from '@datahub/data-models/constants/entity';
|
||||
|
||||
/**
|
||||
* Indexes the route names we care about to functions that resolve the placeholder value
|
||||
* defaults to the route.name, if a resolved value cannot be determined
|
||||
* @type Record<string, ((r: RouteInfoWithOrWithoutAttributes) => string) | undefined>
|
||||
*/
|
||||
export const mapOfRouteNamesToResolver: Record<string, ((r: MaybeRouteInfoWithAttributes) => string) | void> = {
|
||||
'browse.entity': (route: MaybeRouteInfoWithAttributes): string =>
|
||||
route.attributes ? `browse.${route.attributes.entity}` : route.name,
|
||||
'browse.entity.index': (route: MaybeRouteInfoWithAttributes): string =>
|
||||
route.attributes ? `browse.${route.attributes.entity}` : route.name,
|
||||
'datasets.dataset.tab': (route: MaybeRouteInfoWithAttributes): string =>
|
||||
route.attributes ? `${DatasetEntity.displayName}.${route.attributes.currentTab}` : route.name
|
||||
};
|
||||
|
||||
/**
|
||||
* Guard checks that a route name is an entity route by testing if the routeName begins with the entity name
|
||||
* @param {string} routeName the name of the route to check against
|
||||
* @returns {boolean}
|
||||
*/
|
||||
const routeNameIsEntityRoute = (routeName: string): boolean =>
|
||||
listOfEntitiesMap((e): DataModelEntity['displayName'] => e.displayName).some(
|
||||
(entityName: DataModelEntity['displayName']): boolean => routeName.startsWith(entityName)
|
||||
);
|
||||
|
||||
/**
|
||||
* Check if the route info instance has a name that is considered an entity route
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const isRouteEntityPageRoute = (routeBeingTransitionedTo: Transition['to' | 'from']): boolean => {
|
||||
const routeName = resolveDynamicRouteName(mapOfRouteNamesToResolver, routeBeingTransitionedTo);
|
||||
return Boolean(routeName && routeNameIsEntityRoute(routeName));
|
||||
};
|
||||
@ -24,6 +24,7 @@
|
||||
"postpublish": "ember ts:clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@datahub/metadata-types": "0.0.0",
|
||||
"@datahub/utils": "0.0.0",
|
||||
"ember-cli-babel": "^7.8.0",
|
||||
"ember-cli-typescript": "^2.0.2",
|
||||
@ -32,7 +33,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.4.0",
|
||||
"@datahub/metadata-types": "0.0.0",
|
||||
"@ember/optional-features": "^0.7.0",
|
||||
"@types/ember": "^3.1.0",
|
||||
"@types/ember-qunit": "^3.4.6",
|
||||
|
||||
@ -1,2 +1 @@
|
||||
const testemConf = require('../../configs/testem-base');
|
||||
module.exports = testemConf;
|
||||
module.exports = require('../../configs/testem-base');
|
||||
|
||||
@ -28,6 +28,6 @@
|
||||
"../../@datahub/metadata-types/addon/**/*",
|
||||
"../../@datahub/metadata-types/types/**/*",
|
||||
"../../@datahub/utils/addon/**/*",
|
||||
"../../@datahub/utils/types/**/*",
|
||||
"../../@datahub/utils/types/**/*"
|
||||
]
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { ITabProperties, Tab } from '@datahub/data-models/constants/entity/shared/tabs';
|
||||
import { Task } from 'ember-concurrency';
|
||||
import { ETaskPromise } from '@datahub/utils/types/concurrency';
|
||||
|
||||
/**
|
||||
* Defines expected container properties and methods for an Entity Container Component
|
||||
@ -17,5 +17,5 @@ export interface IEntityContainer<T> {
|
||||
// Tabs that are available for the entity
|
||||
tabs: Array<ITabProperties>;
|
||||
// concurrency task to materialize the related underlying IEntity
|
||||
reifyEntityTask: Task<Promise<T>, () => Promise<T>>;
|
||||
reifyEntityTask: ETaskPromise<T>;
|
||||
}
|
||||
|
||||
@ -14,8 +14,6 @@ export interface ISearchEntityRenderProps {
|
||||
showInFacets: boolean;
|
||||
// Flag indicating that this search entity attribute should be shown as a search result tag or not
|
||||
showAsTag?: boolean;
|
||||
// Component that can serve different purposes when default rendering options are not enough
|
||||
component?: string;
|
||||
// An alternative string representation of the fieldName attribute that's more human-friendly e.g. Data Origin
|
||||
displayName: string;
|
||||
// A description of the search attribute
|
||||
@ -38,4 +36,10 @@ export interface ISearchEntityRenderProps {
|
||||
iconName?: string;
|
||||
// Optional text that is shown over hovering of the element, to provide more meaning and context
|
||||
hoverText?: string;
|
||||
// Component that can serve different purposes when default rendering options are not enough
|
||||
// attributes is an optional object used to provide additional rendering information about the component
|
||||
component?: {
|
||||
name: string;
|
||||
attrs?: Record<string, unknown>;
|
||||
};
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ import { IBaseEntity } from '@datahub/metadata-types/types/entity';
|
||||
/**
|
||||
* String literal of available entity routes
|
||||
*/
|
||||
export type EntityRoute = 'browse.entity' | 'features.feature' | 'datasets.dataset' | 'metrics.metric';
|
||||
export type EntityRoute = string;
|
||||
|
||||
/**
|
||||
* Properties that enable a dynamic link to the entity or category
|
||||
|
||||
@ -1,2 +1 @@
|
||||
const testemConf = require('../../configs/testem-base');
|
||||
module.exports = testemConf;
|
||||
module.exports = require('../../configs/testem-base');
|
||||
|
||||
@ -20,11 +20,11 @@
|
||||
|
||||
{{#if isDeprecatedAlias}}
|
||||
|
||||
{{medium-editor
|
||||
deprecationNoteAlias
|
||||
options=editorOptions
|
||||
<MediumEditor
|
||||
class="entity-deprecation__note-editor"
|
||||
onChange=(action (mut deprecationNoteAlias))}}
|
||||
@value={{deprecationNoteAlias}}
|
||||
@options={{editorOptions}}
|
||||
@onInput={{action (mut deprecationNoteAlias)}} />
|
||||
|
||||
<h4 class="dataset-deprecation__header">When should this {{entityName}} be decommissioned?</h4>
|
||||
|
||||
@ -40,9 +40,9 @@
|
||||
|
||||
{{#dropdown.content class="entity-deprecation__cal-dropdown"}}
|
||||
<PowerCalendar
|
||||
class="entity-deprecation__decommission-calendar"
|
||||
@selected={{selectedDate}}
|
||||
@center={{centeredDate}}
|
||||
@class="entity-deprecation__decommission-calendar"
|
||||
@onSelect={{action "onDecommissionDateChange" value="date"}}
|
||||
@onCenterChange={{action (mut centeredDate) value="date"}} as |calendar|>
|
||||
|
||||
@ -107,4 +107,4 @@
|
||||
|
||||
</form>
|
||||
|
||||
{{yield}}
|
||||
{{yield}}
|
||||
|
||||
@ -71,7 +71,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__note-editor {
|
||||
&__note-editor .ember-medium-editor__container {
|
||||
min-height: item-spacing(6) * 4;
|
||||
outline: none;
|
||||
padding: item-spacing(1);
|
||||
|
||||
@ -27,7 +27,7 @@
|
||||
"ember-cli-moment-shim": "^3.7.1",
|
||||
"ember-cli-string-helpers": "^2.0.0",
|
||||
"ember-cli-typescript": "^2.0.2",
|
||||
"ember-medium-editor-fix": "^0.0.2",
|
||||
"ember-medium-editor-fix": "^0.0.3",
|
||||
"ember-moment": "^7.8.1",
|
||||
"ember-power-calendar": "^0.14.0",
|
||||
"ember-power-calendar-moment": "^0.1.7",
|
||||
|
||||
@ -1,2 +1 @@
|
||||
const testemConf = require('../../configs/testem-base');
|
||||
module.exports = testemConf;
|
||||
module.exports = require('../../configs/testem-base');
|
||||
|
||||
@ -4,4 +4,5 @@
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 400;
|
||||
margin-right: item-spacing(2);
|
||||
}
|
||||
|
||||
@ -11,5 +11,9 @@
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
|
||||
.entity-pill {
|
||||
margin-right: item-spacing(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,2 +1 @@
|
||||
const testemConf = require('../../configs/testem-base');
|
||||
module.exports = testemConf;
|
||||
module.exports = require('../../configs/testem-base');
|
||||
|
||||
@ -8,7 +8,8 @@ import { DataModelEntityInstance } from '@datahub/data-models/entity/entity-fact
|
||||
import { isEqual } from 'lodash';
|
||||
import { InstitutionalMemory, InstitutionalMemories } from '@datahub/data-models/models/aspects/institutional-memory';
|
||||
import { run, schedule } from '@ember/runloop';
|
||||
import { task, Task } from 'ember-concurrency';
|
||||
import { task } from 'ember-concurrency';
|
||||
import { ETaskPromise } from '@datahub/utils/types/concurrency';
|
||||
|
||||
@layout(template)
|
||||
@containerDataSource('getContainerDataTask', ['entity'])
|
||||
@ -52,7 +53,7 @@ export default class InstitutionalMemoryContainersTab extends Component {
|
||||
});
|
||||
});
|
||||
}).drop())
|
||||
getContainerDataTask!: Task<Promise<InstitutionalMemories>, () => Promise<InstitutionalMemories>>;
|
||||
getContainerDataTask!: ETaskPromise<InstitutionalMemories>;
|
||||
/**
|
||||
* This task is used to actually save user changes to the entity's institutional memory list
|
||||
*/
|
||||
@ -67,7 +68,7 @@ export default class InstitutionalMemoryContainersTab extends Component {
|
||||
|
||||
yield this.getContainerDataTask.perform();
|
||||
}).drop())
|
||||
writeContainerDataTask!: Task<Promise<InstitutionalMemories | void>, () => Promise<InstitutionalMemories | void>>;
|
||||
writeContainerDataTask!: ETaskPromise<InstitutionalMemories | void>;
|
||||
/**
|
||||
* Triggers the task to save the institutional memory state
|
||||
*/
|
||||
|
||||
@ -1,2 +1 @@
|
||||
const testemConf = require('../../configs/testem-base');
|
||||
module.exports = testemConf;
|
||||
module.exports = require('../../configs/testem-base');
|
||||
|
||||
@ -17,7 +17,8 @@ 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, Task, TaskInstance } from 'ember-concurrency';
|
||||
import { task } from 'ember-concurrency';
|
||||
import { ETask } from '@datahub/utils/types/concurrency';
|
||||
|
||||
/**
|
||||
* Defines the interface for the output of the EntityList mapping predicate function
|
||||
@ -239,7 +240,7 @@ export default class EntityListContainer extends WithEntityLists {
|
||||
|
||||
return set(this, 'instances', []);
|
||||
}).restartable())
|
||||
hydrateEntitiesTask!: Task<void, () => TaskInstance<void>>;
|
||||
hydrateEntitiesTask!: ETask<void>;
|
||||
|
||||
/**
|
||||
* On initialization, hydrate the entities list with data serialized in persistent storage
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { DataModelEntity } from '@datahub/data-models/constants/entity';
|
||||
import { DatasetEntity } from '@datahub/data-models/entity/dataset/dataset-entity';
|
||||
import { PersonEntity } from '@datahub/data-models/entity/person/person-entity';
|
||||
|
||||
// Alias for a DataModelEntity type in the list of supportedListEntities
|
||||
export type SupportedListEntity = Exclude<DataModelEntity, typeof DatasetEntity>;
|
||||
@ -8,7 +9,7 @@ export type SupportedListEntity = Exclude<DataModelEntity, typeof DatasetEntity>
|
||||
* Lists entities that have Entity List support
|
||||
* note: DatasetEntity is excluded from type pending mid-tier support for urn attribute, support for uri would be throw away
|
||||
*/
|
||||
export const supportedListEntities: Array<SupportedListEntity> = [];
|
||||
export const supportedListEntities: Array<SupportedListEntity> = [PersonEntity];
|
||||
|
||||
/**
|
||||
* Enumerates the cta text for toggling an Entity off or onto a list for action triggers where List toggle actions are called
|
||||
|
||||
@ -7,6 +7,7 @@ import { computed } from '@ember/object';
|
||||
import { supportedListEntities, SupportedListEntity } from '@datahub/lists/constants/entity/shared';
|
||||
import { noop } from '@datahub/utils/function/noop';
|
||||
import { IStoredEntityAttrs } from '@datahub/lists/types/list';
|
||||
import { PersonEntity } from '@datahub/data-models/entity/person/person-entity';
|
||||
|
||||
// Map of List Entity displayName to list of instances
|
||||
type ManagedListEntities = Record<SupportedListEntity['displayName'], ReadonlyArray<IStoredEntityAttrs>>;
|
||||
@ -40,19 +41,19 @@ export default class EntityListsManager extends Service {
|
||||
@computed('entityStorageProxy.[]')
|
||||
get entities(): ManagedListEntities {
|
||||
// Initialize with empty lists, these will be overridden in the reduction over supportedListEntities
|
||||
// const entityMap = {
|
||||
// };
|
||||
// const entityList = this.entityStorageProxy;
|
||||
const entityMap = {
|
||||
[PersonEntity.displayName]: []
|
||||
};
|
||||
const entityList = this.entityStorageProxy;
|
||||
|
||||
// return this.supportedListEntities.reduce((entityMap, EntityType: SupportedListEntity): ManagedListEntities => {
|
||||
// // entityList is a single list of all supported entity instances
|
||||
// // Filter out entities that match the EntityType
|
||||
// // Create a new instance to hydrate with the saved snapshot and baseEntity
|
||||
// const storedEntities = entityList.filter((storedEntity): boolean => storedEntity.type === EntityType.displayName);
|
||||
return this.supportedListEntities.reduce((entityMap, EntityType: SupportedListEntity): ManagedListEntities => {
|
||||
// entityList is a single list of all supported entity instances
|
||||
// Filter out entities that match the EntityType
|
||||
// Create a new instance to hydrate with the saved snapshot and baseEntity
|
||||
const storedEntities = entityList.filter((storedEntity): boolean => storedEntity.type === EntityType.displayName);
|
||||
|
||||
// return { ...entityMap, [EntityType.displayName]: Object.freeze(storedEntities) };
|
||||
// }, entityMap);
|
||||
return {};
|
||||
return { ...entityMap, [EntityType.displayName]: Object.freeze(storedEntities) };
|
||||
}, entityMap);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -1,2 +1 @@
|
||||
const testemConf = require('../../configs/testem-base');
|
||||
module.exports = testemConf;
|
||||
module.exports = require('../../configs/testem-base');
|
||||
|
||||
@ -1,2 +1 @@
|
||||
const testemConf = require('../../configs/testem-base');
|
||||
module.exports = testemConf;
|
||||
module.exports = require('../../configs/testem-base');
|
||||
|
||||
@ -6,10 +6,10 @@
|
||||
"paths": {
|
||||
"dummy/tests/*": ["tests/*"],
|
||||
"dummy/*": ["tests/dummy/app/*", "app/*"],
|
||||
"@datahub/metadata-types/test-support": ["addon-test-support"],
|
||||
"@datahub/metadata-types/test-support/*": ["addon-test-support/*"],
|
||||
"@datahub/metadata-types": ["addon"],
|
||||
"@datahub/metadata-types/*": ["addon/*"],
|
||||
"@datahub/metadata-types/test-support": ["addon-test-support"],
|
||||
"@datahub/metadata-types/test-support/*": ["addon-test-support/*"],
|
||||
"@datahub/utils": ["../../@datahub/utils/addon"],
|
||||
"@datahub/utils/*": ["../../@datahub/utils/addon/*"],
|
||||
"*": ["types/*"]
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
import Component from '@ember/component';
|
||||
// @ts-ignore: Ignore import of compiled template
|
||||
import template from '../templates/components/entity-pill';
|
||||
import { layout } from '@ember-decorators/component';
|
||||
|
||||
@layout(template)
|
||||
export default class EntityPill extends Component {
|
||||
baseClass: string = 'entity-pill';
|
||||
}
|
||||
@ -13,8 +13,7 @@ let _hasUserBeenTracked = false;
|
||||
|
||||
/**
|
||||
* The current user service can be injected into our various datahub addons to give reference
|
||||
* whenever necessary to the current logged in user based on the ember simple auth authenticated
|
||||
* service
|
||||
* whenever necessary to the current logged in user
|
||||
*/
|
||||
export default class CurrentUser extends Service {
|
||||
/**
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
{{#if @value}}
|
||||
<span
|
||||
class="{{this.baseClass}} nacho-pill nacho-pill--small
|
||||
{{if @field.component.attrs.styleModifier (concat this.baseClass @field.component.attrs.styleModifier)}}
|
||||
{{if @styleModifier (concat this.baseClass @styleModifier)}}"
|
||||
>
|
||||
{{if @field.component.attrs.titleize (titleize @value) @value}}
|
||||
</span>
|
||||
{{/if}}
|
||||
@ -0,0 +1 @@
|
||||
export { default } from '@datahub/shared/components/entity-pill';
|
||||
@ -0,0 +1 @@
|
||||
@import 'entity-pill/all';
|
||||
@ -0,0 +1 @@
|
||||
@import 'entity-pill';
|
||||
@ -0,0 +1,9 @@
|
||||
.entity-pill {
|
||||
cursor: auto;
|
||||
&__tier {
|
||||
&--generic {
|
||||
color: get-color(black);
|
||||
background: get-color(slate2, 0.45);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -26,7 +26,8 @@
|
||||
"ember-cli-htmlbars": "^3.0.0",
|
||||
"ember-cli-typescript": "^2.0.2",
|
||||
"ember-moment": "^7.8.1",
|
||||
"ember-simple-auth": "^1.8.2"
|
||||
"ember-simple-auth": "^1.8.2",
|
||||
"ember-cli-string-helpers": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.4.0",
|
||||
|
||||
@ -0,0 +1,52 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { render, findAll } from '@ember/test-helpers';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
|
||||
module('Integration | Component | entity-pill', function(hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test('it renders correctly when titleize and modifiers are present', async function(assert) {
|
||||
const mockField = {
|
||||
component: {
|
||||
attrs: {
|
||||
styleModifier: 'class1',
|
||||
titleize: true
|
||||
}
|
||||
}
|
||||
};
|
||||
this.set('value', 'testValue');
|
||||
this.set('field', mockField);
|
||||
await render(hbs`{{entity-pill value=value field=field }}`);
|
||||
|
||||
assert.ok(this.element, 'Initial render is without errors');
|
||||
assert.equal(findAll('.nacho-pill').length, 1, 'Renders a pill with the right class1');
|
||||
assert.equal(findAll('.nacho-pill--small').length, 1, 'Renders a pill with the right class2');
|
||||
assert.equal(findAll('.entity-pillclass1').length, 1, 'Renders a pill with the right class3');
|
||||
assert.equal(this.element.textContent!.trim(), 'Testvalue');
|
||||
});
|
||||
|
||||
test('it renders correctly when titleize and modifiers are absent', async function(assert) {
|
||||
this.set('value', 'testValue');
|
||||
this.set('field', {});
|
||||
await render(hbs`{{entity-pill value=value field=field }}`);
|
||||
|
||||
assert.ok(this.element, 'Initial render is without errors');
|
||||
assert.equal(findAll('.nacho-pill').length, 1, 'Renders a pill with the right class');
|
||||
assert.equal(findAll('.nacho-pill--small').length, 1, 'Renders a pill with the right class');
|
||||
assert.equal(findAll('.class1').length, 0, 'Renders a pill with the right class');
|
||||
assert.equal(this.element.textContent!.trim(), 'testValue');
|
||||
});
|
||||
|
||||
test('it renders correctly when styleModifier is passed directly', async function(assert) {
|
||||
this.set('value', 'testValue');
|
||||
this.set('styleModifier', '--advanced-style');
|
||||
await render(hbs`{{entity-pill value=value styleModifier=styleModifier }}`);
|
||||
|
||||
assert.ok(this.element, 'Initial render is without errors');
|
||||
assert.equal(findAll('.nacho-pill').length, 1, 'Renders a pill with the right class1');
|
||||
assert.equal(findAll('.nacho-pill--small').length, 1, 'Renders a pill with the right class2');
|
||||
assert.equal(findAll('.entity-pill--advanced-style').length, 1, 'Renders a pill with the right class3');
|
||||
assert.equal(this.element.textContent!.trim(), 'testValue');
|
||||
});
|
||||
});
|
||||
55
datahub-web/@datahub/shared/types/configurator/configurator.d.ts
vendored
Normal file
55
datahub-web/@datahub/shared/types/configurator/configurator.d.ts
vendored
Normal file
@ -0,0 +1,55 @@
|
||||
import { ITrackingConfig } from '@datahub/shared/types/configurator/tracking';
|
||||
import Service from '@ember/service';
|
||||
import { ApiStatus } from '@datahub/utils/addon/api/shared';
|
||||
|
||||
/**
|
||||
* Describes the interface for the configuration endpoint response object.
|
||||
* These values help to determine how the app behaves at runtime for example feature flags and feature configuration properties
|
||||
* @interface IAppConfig
|
||||
*/
|
||||
export interface IAppConfig {
|
||||
// Attributes for analytics tracking features within the application
|
||||
tracking: ITrackingConfig;
|
||||
showLineageGraph: boolean;
|
||||
useNewBrowseDataset: boolean;
|
||||
isInternal: boolean;
|
||||
userEntityProps: {
|
||||
aviUrlPrimary: string;
|
||||
aviUrlFallback: string;
|
||||
};
|
||||
showChangeManagement: boolean;
|
||||
changeManagementLink: string;
|
||||
wikiLinks: Record<string, string>;
|
||||
isStagingBanner: boolean;
|
||||
isLiveDataWarning: boolean;
|
||||
shouldShowDatasetLineage: boolean;
|
||||
showInstitutionalMemory: boolean;
|
||||
showPeople: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes the interface for the json response when a GET request is made to the
|
||||
* configurator endpoint
|
||||
* @interface IConfiguratorGetResponse
|
||||
*/
|
||||
export interface IConfiguratorGetResponse {
|
||||
status: ApiStatus;
|
||||
config: IAppConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Conditional type alias for getConfig return type, if T is assignable to a key of
|
||||
* IAppConfig, then return the property value, otherwise returns the IAppConfig object
|
||||
*/
|
||||
export type IAppConfigOrProperty<T> = T extends keyof IAppConfig
|
||||
? IAppConfig[T]
|
||||
: T extends undefined
|
||||
? IAppConfig
|
||||
: never;
|
||||
|
||||
export interface IConfigurator extends Service {
|
||||
getConfig<K extends keyof IAppConfig | undefined>(
|
||||
key?: K,
|
||||
options?: { useDefault?: boolean; default?: IAppConfigOrProperty<K> }
|
||||
): IAppConfigOrProperty<K>;
|
||||
}
|
||||
19
datahub-web/@datahub/shared/types/configurator/tracking.d.ts
vendored
Normal file
19
datahub-web/@datahub/shared/types/configurator/tracking.d.ts
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Describes the interface for subset of the tracking endpoint response object.
|
||||
* These values help to determine how tracking behaves in the application
|
||||
* @interface ITrackingConfig
|
||||
*/
|
||||
export interface ITrackingConfig {
|
||||
// Flag indicating that tracking should be enabled or not
|
||||
isEnabled: boolean;
|
||||
// Map of available trackers and configuration options per tracker
|
||||
trackers: {
|
||||
// Properties for Piwik analytics service tracking
|
||||
piwik: {
|
||||
// Website identifier for piwik tracking, used in setSideId configuration
|
||||
piwikSiteId: number;
|
||||
// Specifies the URL where the Matomo / Piwik tracking code is located
|
||||
piwikUrl: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,79 @@
|
||||
import Component from '@ember/component';
|
||||
// @ts-ignore: Ignore import of compiled template
|
||||
import template from '../templates/components/track-ui-event';
|
||||
import { layout, tagName } from '@ember-decorators/component';
|
||||
import { inject as service } from '@ember/service';
|
||||
import UnifiedTracking from '@datahub/tracking/services/unified-tracking';
|
||||
import { TrackingEventCategory } from '@datahub/tracking/constants/event-tracking';
|
||||
import { IBaseTrackingEvent } from '@datahub/tracking/types/event-tracking';
|
||||
import { action } from '@ember/object';
|
||||
import { assert } from '@ember/debug';
|
||||
import { noop } from 'lodash';
|
||||
|
||||
/**
|
||||
* Tag-less component / fragment to track user interactions or actions from an hbs template. Removes the need to concern
|
||||
* component logic with analytics operations, which can evolve independently
|
||||
* @export
|
||||
* @class TrackUiEvent
|
||||
* @extends {Component}
|
||||
*/
|
||||
@layout(template)
|
||||
@tagName('')
|
||||
export default class TrackUiEvent extends Component {
|
||||
/**
|
||||
* Reference to the UnifiedTracking tracking service for analytics tracking within host application
|
||||
*/
|
||||
@service('unified-tracking')
|
||||
tracking!: UnifiedTracking;
|
||||
|
||||
/**
|
||||
* Reference to the enum of tracking event categories
|
||||
*/
|
||||
category?: TrackingEventCategory;
|
||||
|
||||
/**
|
||||
* The action that was performed for the event category
|
||||
*/
|
||||
action?: IBaseTrackingEvent['action'];
|
||||
|
||||
/**
|
||||
* The name of the tracking event
|
||||
*/
|
||||
name?: IBaseTrackingEvent['name'];
|
||||
|
||||
/**
|
||||
* An optional value for the specific event being tracked
|
||||
*/
|
||||
value?: IBaseTrackingEvent['value'];
|
||||
|
||||
init(): void {
|
||||
super.init();
|
||||
|
||||
assert('Expected a category to be provided on initialization of TrackUiEvent', Boolean(this.category));
|
||||
assert('Expected an action to be provided on initialization of TrackUiEvent', Boolean(this.action));
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the UnifiedTracking service with options for the specific event being tracked
|
||||
* @private
|
||||
*/
|
||||
private trackEvent(): void {
|
||||
const { category, action, name, value }: Partial<IBaseTrackingEvent> = this;
|
||||
|
||||
if (category && action) {
|
||||
const resolvedOptions = Object.assign({}, { category, action }, !!name && { name }, !!value && { value });
|
||||
this.tracking.trackEvent(resolvedOptions);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracks an action triggered on a nested component
|
||||
* @param {(...args: Array<unknown>) => unknown} uiAction the action handler intended to handle the actual user interaction
|
||||
* @param {...Array<unknown>} actionArgs arguments intended to be supplied to the actual action handler
|
||||
*/
|
||||
@action
|
||||
trackOnAction(uiAction: (...args: Array<unknown>) => unknown = noop, ...actionArgs: Array<unknown>): void {
|
||||
typeof uiAction === 'function' && uiAction(...actionArgs);
|
||||
this.trackEvent();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,80 @@
|
||||
import { IBaseTrackingEvent, TrackingEvents } from '@datahub/tracking/types/event-tracking';
|
||||
import {
|
||||
insertTrackingEventsCategoryFor,
|
||||
TrackingEventCategory
|
||||
} from '@datahub/tracking/constants/event-tracking/index';
|
||||
|
||||
/**
|
||||
* TODO: META-8050 transition deprecated event references
|
||||
* Enumerates the available compliance metadata events
|
||||
* @deprecated Replaced with complianceTrackingEvents
|
||||
* @link complianceTrackingEvents
|
||||
*/
|
||||
export enum ComplianceEvent {
|
||||
Cancel = 'CancelEditComplianceMetadata',
|
||||
Next = 'NextComplianceMetadataStep',
|
||||
ManualApply = 'AdvancedEditComplianceMetadataStep',
|
||||
Previous = 'PreviousComplianceMetadataStep',
|
||||
Edit = 'BeginEditComplianceMetadata',
|
||||
Download = 'DownloadComplianceMetadata',
|
||||
Upload = 'UploadComplianceMetadata',
|
||||
SetUnspecifiedAsNone = 'SetUnspecifiedFieldsAsNone',
|
||||
FieldIdentifier = 'ComplianceMetadataFieldIdentifierSelected',
|
||||
FieldFormat = 'ComplianceMetadataFieldFormatSelected',
|
||||
Save = 'SaveComplianceMetadata'
|
||||
}
|
||||
|
||||
/**
|
||||
* Initial map if event names to partial base tracking event with actions
|
||||
* @type {Record<string, Partial<IBaseTrackingEvent>>}
|
||||
*/
|
||||
const complianceTrackingEvent: Record<string, Partial<IBaseTrackingEvent>> = {
|
||||
CancelEvent: {
|
||||
action: 'CancelEditComplianceMetadataEvent'
|
||||
},
|
||||
NextStepEvent: {
|
||||
action: 'NextComplianceMetadataStepEvent'
|
||||
},
|
||||
ManualApplyEvent: {
|
||||
action: 'AdvancedEditComplianceMetadataStepEvent'
|
||||
},
|
||||
PreviousStepEvent: {
|
||||
action: 'PreviousComplianceMetadataStepEvent'
|
||||
},
|
||||
EditEvent: {
|
||||
action: 'BeginEditComplianceMetadataEvent'
|
||||
},
|
||||
DownloadEvent: {
|
||||
action: 'DownloadComplianceMetadataEvent'
|
||||
},
|
||||
UploadEvent: {
|
||||
action: 'UploadComplianceMetadataEvent'
|
||||
},
|
||||
SetUnspecifiedAsNoneEvent: {
|
||||
action: 'SetUnspecifiedFieldsAsNoneEvent'
|
||||
},
|
||||
FieldIdentifierEvent: {
|
||||
action: 'ComplianceMetadataFieldIdentifierSelectedEvent'
|
||||
},
|
||||
FieldFormatEvent: {
|
||||
action: 'ComplianceMetadataFieldFormatSelectedEvent'
|
||||
},
|
||||
SaveEvent: {
|
||||
action: 'SaveComplianceMetadataEvent'
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The accumulator object to build attributes for a tracking event
|
||||
* @type {Partial<Record<string, Partial<IBaseTrackingEvent>>>}
|
||||
*/
|
||||
const complianceTrackingEventsAccumulator: Partial<typeof complianceTrackingEvent> = {};
|
||||
|
||||
/**
|
||||
* Compliance tracking events with required base tracking event attributes
|
||||
* @type {TrackingEvents<keyof typeof complianceTrackingEvent, IBaseTrackingEvent>}
|
||||
*/
|
||||
export const complianceTrackingEvents = Object.entries(complianceTrackingEvent).reduce(
|
||||
insertTrackingEventsCategoryFor(TrackingEventCategory.DatasetCompliance),
|
||||
complianceTrackingEventsAccumulator
|
||||
) as TrackingEvents<keyof typeof complianceTrackingEvent>;
|
||||
@ -1,4 +1,4 @@
|
||||
import { TrackingEvents, IBaseTrackingEvent } from 'wherehows-web/typings/app/analytics/event-tracking';
|
||||
import { TrackingEvents, IBaseTrackingEvent } from '@datahub/tracking/types/event-tracking';
|
||||
|
||||
/**
|
||||
* String values for categories that can be tracked with the application
|
||||
@ -21,12 +21,23 @@ export enum TrackingGoal {
|
||||
SatClick = 1
|
||||
}
|
||||
|
||||
// Convenience alias for insertTrackingEventsCategoryFor return type
|
||||
type InsertTrackingEventsCategoryForReturn = Record<string, IBaseTrackingEvent | Partial<IBaseTrackingEvent>>;
|
||||
|
||||
/**
|
||||
* Augments a tracking event partial with category information
|
||||
* @param {TrackingEvents} events the events mapping
|
||||
* @param {[string, Partial<IBaseTrackingEvent>]} [eventName, trackingEvent]
|
||||
*/
|
||||
export const insertTrackingEventsCategoryFor = (category: TrackingEventCategory) => (
|
||||
export const insertTrackingEventsCategoryFor = (
|
||||
category: TrackingEventCategory
|
||||
): ((
|
||||
events: TrackingEvents,
|
||||
eventNameAndEvent: [string, Partial<IBaseTrackingEvent>]
|
||||
) => InsertTrackingEventsCategoryForReturn) => (
|
||||
events: TrackingEvents,
|
||||
[eventName, trackingEvent]: [string, Partial<IBaseTrackingEvent>]
|
||||
) => ({ ...events, [eventName]: { ...trackingEvent, category } });
|
||||
): InsertTrackingEventsCategoryForReturn => ({
|
||||
...events,
|
||||
[eventName]: { ...trackingEvent, category }
|
||||
});
|
||||
@ -1,5 +1,5 @@
|
||||
import { IBaseTrackingEvent } from 'wherehows-web/typings/app/analytics/event-tracking';
|
||||
import { TrackingEventCategory } from 'wherehows-web/constants/analytics/event-tracking';
|
||||
import { IBaseTrackingEvent } from '@datahub/tracking/types/event-tracking';
|
||||
import { TrackingEventCategory } from '@datahub/tracking/constants/event-tracking';
|
||||
/**
|
||||
* Tag string literal union for search tracking event keys
|
||||
* @alias {string}
|
||||
@ -1,11 +1,145 @@
|
||||
import Service from '@ember/service';
|
||||
import { inject as service } from '@ember/service';
|
||||
import Metrics from 'ember-metrics';
|
||||
import CurrentUser from '@datahub/shared/services/current-user';
|
||||
import { ITrackingConfig } from '@datahub/shared/types/configurator/tracking';
|
||||
import { ITrackSiteSearchParams } from '@datahub/tracking/types/search';
|
||||
import { getPiwikActivityQueue } from '@datahub/tracking/utils/piwik';
|
||||
import { scheduleOnce } from '@ember/runloop';
|
||||
import RouterService from '@ember/routing/router-service';
|
||||
import Transition from '@ember/routing/-private/transition';
|
||||
import { resolveDynamicRouteName } from '@datahub/utils/routes/routing';
|
||||
import { mapOfRouteNamesToResolver } from '@datahub/data-models/utils/entity-route-name-resolver';
|
||||
import { searchRouteName } from '@datahub/tracking/constants/site-search-tracking';
|
||||
import RouteInfo from '@ember/routing/-private/route-info';
|
||||
import { IBaseTrackingEvent, IBaseTrackingGoal } from '@datahub/tracking/types/event-tracking';
|
||||
|
||||
/**
|
||||
* Defines the base and full api for the analytics / tracking module in Data Hub
|
||||
* @export
|
||||
* @class UnifiedTracking
|
||||
*/
|
||||
export default class UnifiedTracking extends Service {}
|
||||
export default class UnifiedTracking extends Service {
|
||||
/**
|
||||
* References the Ember Metrics addon service, which serves as a proxy to analytics services for
|
||||
* metrics collection within the application
|
||||
*/
|
||||
@service
|
||||
metrics!: Metrics;
|
||||
|
||||
/**
|
||||
* Injected reference to the shared CurrentUser service, user here to inform the analytics service of the currently logged in
|
||||
* user
|
||||
*/
|
||||
@service
|
||||
currentUser!: CurrentUser;
|
||||
|
||||
/**
|
||||
* Injects a reference to the router service, used to handle application routing concerns such as event handler binding
|
||||
*/
|
||||
@service
|
||||
router!: RouterService;
|
||||
|
||||
init(): void {
|
||||
super.init();
|
||||
|
||||
// On init ensure that page view transitions are captured by the metrics services
|
||||
this.trackPageViewOnRouteChange();
|
||||
}
|
||||
|
||||
/**
|
||||
* If tracking is enabled, activates the adapters for the applicable analytics services
|
||||
* @param {ITrackingConfig} tracking a configuration object with properties for enabling tracking or specifying behavior
|
||||
*/
|
||||
setupTrackers(tracking: ITrackingConfig): void {
|
||||
if (tracking.isEnabled) {
|
||||
const metrics = this.metrics;
|
||||
const { trackers } = tracking;
|
||||
const { piwikSiteId, piwikUrl } = trackers.piwik;
|
||||
|
||||
metrics.activateAdapters([
|
||||
{
|
||||
name: 'Piwik',
|
||||
environments: ['all'],
|
||||
config: {
|
||||
piwikUrl,
|
||||
siteId: piwikSiteId
|
||||
}
|
||||
}
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies and sets the currently logged in user to be tracked on the activated analytics services
|
||||
* @param {ITrackingConfig} tracking a configuration object with properties for enabling tracking or specifying behavior
|
||||
*/
|
||||
setCurrentUser(tracking: ITrackingConfig): void {
|
||||
const { currentUser, metrics } = this;
|
||||
|
||||
// Check if tracking is enabled prior to tracking the current user
|
||||
// Passes an anonymous function to track the currently logged in user using the `current-user` service CurrentUser
|
||||
tracking.isEnabled && currentUser.trackCurrentUser((userId: string): void => metrics.identify({ userId }));
|
||||
}
|
||||
|
||||
/**
|
||||
* This tracks the search event when a user successfully requests a search query
|
||||
* @param {ITrackSiteSearchParams} { keyword, entity, searchCount } parameters for the search operation performed by the user
|
||||
*/
|
||||
trackSiteSearch({ keyword, entity, searchCount }: ITrackSiteSearchParams): void {
|
||||
getPiwikActivityQueue().push(['trackSiteSearch', keyword, entity, searchCount]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracks application events that are not site search events or page view. These are typically custom events that occur as
|
||||
* a user interacts with the app
|
||||
*/
|
||||
trackEvent(event: IBaseTrackingEvent): void {
|
||||
const { category, action, name, value } = event;
|
||||
const resolvedOptions = Object.assign({}, { category, action }, !!name && { name }, !!value && { value });
|
||||
|
||||
this.metrics.trackEvent(resolvedOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Track when a goal is met by adding the goal identifier to the activity queue
|
||||
* @param {IBaseTrackingGoal} goal the goal to be tracked
|
||||
*/
|
||||
trackGoal(goal: IBaseTrackingGoal): void {
|
||||
getPiwikActivityQueue().push(['trackGoal', goal.name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracks impressions for all rendered DOM content
|
||||
* This is scheduled in the afterRender queue to ensure that tracking services can accurately identify content blocks
|
||||
* that have been tagged with data-track-content data attributes. This methodology is currently specific to Piwik tracking
|
||||
*/
|
||||
trackContentImpressions(): void {
|
||||
void scheduleOnce('afterRender', null, (): number => getPiwikActivityQueue().push(['trackAllContentImpressions']));
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds the handler to track page views on route change
|
||||
*/
|
||||
trackPageViewOnRouteChange(): void {
|
||||
// Bind to the routeDidChange event to track global successful route transitions and track page view on the metrics service
|
||||
this.router.on('routeDidChange', ({ to }: Transition): void => {
|
||||
const { router, metrics } = this;
|
||||
const page = router.currentURL;
|
||||
// fallback to page value if a resolution cannot be determined, e.g when to / from is null
|
||||
const title = resolveDynamicRouteName(mapOfRouteNamesToResolver, to) || page;
|
||||
const isSearchRoute =
|
||||
title.includes(searchRouteName) || (to && to.find(({ name }: RouteInfo): boolean => name === searchRouteName));
|
||||
|
||||
if (!isSearchRoute) {
|
||||
// Track a page view event only on page's / route's that are not search
|
||||
metrics.trackPage({ page, title });
|
||||
}
|
||||
|
||||
getPiwikActivityQueue().push(['enableHeartBeatTimer']);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@ember/service' {
|
||||
// eslint-disable-next-line @typescript-eslint/interface-name-prefix
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
{{yield
|
||||
(hash
|
||||
trackOnAction=(action this.trackOnAction)
|
||||
)
|
||||
}}
|
||||
@ -1,8 +1,7 @@
|
||||
import RouterService from '@ember/routing/router-service';
|
||||
import Transition, { RouteInfo } from 'wherehows-web/typings/modules/routerjs';
|
||||
import { Route } from '@ember/routing';
|
||||
import { Time } from '@datahub/metadata-types/types/common/time';
|
||||
import { action } from '@ember/object';
|
||||
import Transition from '@ember/routing/-private/transition';
|
||||
|
||||
const ROUTE_EVENT_NAME = 'routeDidChange';
|
||||
|
||||
@ -52,7 +51,7 @@ export default class DwellTime {
|
||||
* @type {(dwellTime: Time) => boolean}
|
||||
* @instance
|
||||
*/
|
||||
didDwell?: (dwellTime: Time, transition: Transition<Route>) => boolean;
|
||||
didDwell?: (dwellTime: Time, transition: Transition) => boolean;
|
||||
|
||||
/**
|
||||
* Retains a reference to the last seen transition object
|
||||
@ -60,7 +59,7 @@ export default class DwellTime {
|
||||
* @type {Transition<Route>}
|
||||
* @instance
|
||||
*/
|
||||
private lastTransition?: Transition<Route>;
|
||||
private lastTransition?: Transition;
|
||||
|
||||
/**
|
||||
*Creates an instance of DwellTime.
|
||||
@ -78,7 +77,6 @@ export default class DwellTime {
|
||||
readonly route: RouterService & {
|
||||
off?: (ev: string, cb: Function) => unknown;
|
||||
on?: (ev: string, cb: Function) => unknown;
|
||||
currentRoute: RouteInfo;
|
||||
},
|
||||
didDwell?: DwellTime['didDwell']
|
||||
) {
|
||||
@ -91,7 +89,7 @@ export default class DwellTime {
|
||||
// According to ember docs, the RouteService extends a Service, therefore
|
||||
// it has a willDestroy hook that we can wrap to autoclean DwellTime
|
||||
const willDestroy = route.willDestroy;
|
||||
route.willDestroy = () => {
|
||||
route.willDestroy = (): void => {
|
||||
this.onDestroy();
|
||||
willDestroy.call(route);
|
||||
};
|
||||
@ -123,7 +121,7 @@ export default class DwellTime {
|
||||
* @returns {void}
|
||||
* @instance
|
||||
*/
|
||||
private onRouteChange = (transition: Transition<Route>): void => {
|
||||
private onRouteChange = (transition: Transition): void => {
|
||||
this.lastTransition = transition;
|
||||
|
||||
if (transition.to) {
|
||||
@ -137,7 +135,7 @@ export default class DwellTime {
|
||||
* @returns {Time}
|
||||
* @memberof DwellTime
|
||||
*/
|
||||
record(transition: Transition<Route>): Time {
|
||||
record(transition: Transition): Time {
|
||||
const { startTime } = this;
|
||||
|
||||
// Check if dwell time is already being measured, indicated by a non-zero start time
|
||||
20
datahub-web/@datahub/tracking/addon/utils/piwik.ts
Normal file
20
datahub-web/@datahub/tracking/addon/utils/piwik.ts
Normal file
@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Fallback array for Piwik activity queue "Window._paq"
|
||||
*/
|
||||
const _piwikActivityQueueFallbackQueue: Array<Array<string>> = [];
|
||||
|
||||
/**
|
||||
* Returns a reference to the globally available Piwik queue if available
|
||||
* If not a mutable list is returned as a fallback.
|
||||
* Ensure this is called after Piwik.js is loaded to receive a reference to the global paq
|
||||
* @returns {Window['_paq']}
|
||||
*/
|
||||
export const getPiwikActivityQueue = (resetFallbackQueue?: boolean): Window['_paq'] => {
|
||||
const { _paq } = window;
|
||||
|
||||
if (resetFallbackQueue) {
|
||||
_piwikActivityQueueFallbackQueue.length = 0;
|
||||
}
|
||||
|
||||
return _paq || _piwikActivityQueueFallbackQueue;
|
||||
};
|
||||
@ -0,0 +1 @@
|
||||
export { default } from '@datahub/tracking/components/track-ui-event';
|
||||
@ -1,5 +1,13 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = function(/* environment, appConfig */) {
|
||||
return {};
|
||||
return {
|
||||
// Since ember-metrics automatically removes all unused adapters, which
|
||||
// will happen because we are using lazy initialization for API keys
|
||||
// and not specifying adapter props at build time, the ffg forces the
|
||||
// inclusion of the adapter's we currently support.
|
||||
'ember-metrics': {
|
||||
includeAdapters: ['piwik']
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@ -21,6 +21,8 @@
|
||||
"postpublish": "ember ts:clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@datahub/data-models": "0.0.0",
|
||||
"@datahub/shared": "0.0.0",
|
||||
"@datahub/utils": "0.0.0",
|
||||
"ember-cli-babel": "^7.8.0",
|
||||
"ember-cli-htmlbars": "^3.0.0",
|
||||
@ -36,6 +38,7 @@
|
||||
"@types/ember__test-helpers": "^0.7.8",
|
||||
"@types/qunit": "^2.5.4",
|
||||
"@types/rsvp": "^4.0.2",
|
||||
"@types/sinon": "^7.0.13",
|
||||
"broccoli-asset-rev": "^3.0.0",
|
||||
"ember-cli": "~3.11.0",
|
||||
"ember-cli-dependency-checker": "^3.1.0",
|
||||
@ -48,13 +51,16 @@
|
||||
"ember-load-initializers": "^2.0.0",
|
||||
"ember-lodash": "^4.19.4",
|
||||
"ember-maybe-import-regenerator": "^0.1.6",
|
||||
"ember-metrics": "^0.13.0",
|
||||
"ember-qunit": "^4.4.0",
|
||||
"ember-resolver": "^5.0.1",
|
||||
"ember-sinon": "^4.0.0",
|
||||
"ember-sinon-qunit": "^3.4.0",
|
||||
"ember-source": "~3.11.1",
|
||||
"ember-source-channel-url": "^1.1.0",
|
||||
"ember-try": "^1.0.0",
|
||||
"loader.js": "^4.7.0",
|
||||
"qunit-dom": "^0.8.4",
|
||||
"qunit-dom": "^0.8.5",
|
||||
"typescript": "^3.5.3"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@ -1,2 +1 @@
|
||||
const testemConf = require('../../configs/testem-base');
|
||||
module.exports = testemConf;
|
||||
module.exports = require('../../configs/testem-base');
|
||||
|
||||
@ -0,0 +1,92 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { render, click } from '@ember/test-helpers';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import { assertThrownException } from '@datahub/utils/test-helpers/test-exception';
|
||||
import { TrackingEventCategory } from '@datahub/tracking/constants/event-tracking';
|
||||
import { getRenderedComponent } from '@datahub/utils/test-helpers/register-component';
|
||||
import TrackUiEvent from '@datahub/tracking/components/track-ui-event';
|
||||
import sinonTest from 'ember-sinon-qunit/test-support/test';
|
||||
import UnifiedTracking from '@datahub/tracking/services/unified-tracking';
|
||||
|
||||
const category = TrackingEventCategory.Entity;
|
||||
const action = 'testAction';
|
||||
|
||||
module('Integration | Component | track-ui-event', function(hooks): void {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test('instantiation errors are raised as expected', async function(assert): Promise<void> {
|
||||
const category = TrackingEventCategory.Entity;
|
||||
|
||||
await assertThrownException(
|
||||
assert,
|
||||
async (): Promise<void> => {
|
||||
await render(hbs`
|
||||
<TrackUiEvent>
|
||||
<p>Nested Template</p>
|
||||
</TrackUiEvent>
|
||||
`);
|
||||
},
|
||||
(err: Error) => err.message.includes('Expected a category to be provided on initialization of TrackUiEvent')
|
||||
);
|
||||
|
||||
this.set('category', category);
|
||||
|
||||
await assertThrownException(
|
||||
assert,
|
||||
async (): Promise<void> => {
|
||||
await render(hbs`
|
||||
<TrackUiEvent @category={{this.category}}>
|
||||
<p>Nested Template</p>
|
||||
</TrackUiEvent>
|
||||
`);
|
||||
},
|
||||
(err: Error) => err.message.includes('Expected an action to be provided on initialization of TrackUiEvent')
|
||||
);
|
||||
});
|
||||
|
||||
test('component renders nested template', async function(assert): Promise<void> {
|
||||
this.setProperties({
|
||||
action,
|
||||
category
|
||||
});
|
||||
|
||||
await render(hbs`
|
||||
<TrackUiEvent @category={{this.category}} @action={{this.action}}>
|
||||
<p>Nested Template</p>
|
||||
</TrackUiEvent>
|
||||
`);
|
||||
|
||||
assert.dom('p').hasText('Nested Template');
|
||||
});
|
||||
|
||||
sinonTest('', async function(this: SinonTestContext, assert): Promise<void> {
|
||||
const service: UnifiedTracking = this.owner.lookup('service:unified-tracking');
|
||||
const stubbedTrackEvent = this.stub(service, 'trackEvent');
|
||||
|
||||
this.setProperties({
|
||||
action,
|
||||
category
|
||||
});
|
||||
|
||||
const component = await getRenderedComponent({
|
||||
ComponentToRender: TrackUiEvent,
|
||||
testContext: this,
|
||||
template: hbs`
|
||||
<TrackUiEvent @category={{this.category}} @action={{this.action}} as |track|>
|
||||
<button onclick={{action track.trackOnAction}} />
|
||||
</TrackUiEvent>
|
||||
`
|
||||
});
|
||||
|
||||
await click('button');
|
||||
|
||||
assert.ok(stubbedTrackEvent.called, "Expected the UnifiedTracking service's trackEvent to have been called");
|
||||
|
||||
assert.equal(
|
||||
component.tracking,
|
||||
service,
|
||||
"Expected the component's tracking property to be equal to the UnifiedTracking service"
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,12 @@
|
||||
import Service from '@ember/service';
|
||||
|
||||
export default class MetricsServiceStub extends Service {
|
||||
alias(): void {}
|
||||
identify(): void {}
|
||||
trackEvent(): void {}
|
||||
trackPage(): void {}
|
||||
activateAdapters<T>(adapters: Array<T>): Array<T> {
|
||||
return adapters;
|
||||
}
|
||||
invoke(_methodName: string): void {}
|
||||
}
|
||||
@ -2,6 +2,7 @@ import Application from 'dummy/app';
|
||||
import config from '../config/environment';
|
||||
import { setApplication } from '@ember/test-helpers';
|
||||
import { start } from 'ember-qunit';
|
||||
import 'qunit-dom';
|
||||
|
||||
setApplication(Application.create(config.APP));
|
||||
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import MetricsServiceStub from 'wherehows-web/tests/stubs/services/metrics';
|
||||
import { TestContext } from 'ember-test-helpers';
|
||||
import { IBaseTrackingEvent } from 'wherehows-web/typings/app/analytics/event-tracking';
|
||||
import { TrackingEventCategory, TrackingGoal } from 'wherehows-web/constants/analytics/event-tracking';
|
||||
import { getPiwikActivityQueue } from 'wherehows-web/utils/analytics/piwik';
|
||||
import { IBaseTrackingEvent } from '@datahub/tracking/types/event-tracking';
|
||||
import { TrackingEventCategory, TrackingGoal } from '@datahub/tracking/constants/event-tracking';
|
||||
import { getPiwikActivityQueue } from '@datahub/tracking/utils/piwik';
|
||||
import UnifiedTracking from '@datahub/tracking/services/unified-tracking';
|
||||
import Metrics from 'ember-metrics';
|
||||
import MetricsServiceStub from 'dummy/tests/stubs/services/metrics';
|
||||
|
||||
module('Unit | Service | tracking', function(hooks) {
|
||||
setupTest(hooks);
|
||||
@ -14,13 +16,13 @@ module('Unit | Service | tracking', function(hooks) {
|
||||
});
|
||||
|
||||
test('service exists and is registered', function(assert) {
|
||||
assert.ok(this.owner.lookup('service:tracking'));
|
||||
assert.ok(this.owner.lookup('service:unified-tracking'));
|
||||
});
|
||||
|
||||
test('service methods proxy to metrics service and display expected behavior', function(assert) {
|
||||
assert.expect(2);
|
||||
const trackingService = this.owner.lookup('service:tracking');
|
||||
const metricsService = this.owner.lookup('service:metrics');
|
||||
const trackingService: UnifiedTracking = this.owner.lookup('service:unified-tracking');
|
||||
const metricsService: Metrics = this.owner.lookup('service:metrics');
|
||||
const event: IBaseTrackingEvent = {
|
||||
category: TrackingEventCategory.Entity,
|
||||
action: ''
|
||||
@ -1,19 +1,19 @@
|
||||
import DwellTime from 'wherehows-web/utils/analytics/search/dwell-time';
|
||||
import DwellTime from '@datahub/tracking/utils/dwell-time';
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import { triggerEvent } from '@ember/test-helpers';
|
||||
import { searchRouteName } from 'wherehows-web/constants/analytics/site-search-tracking';
|
||||
import { searchRouteName } from '@datahub/tracking/constants/site-search-tracking';
|
||||
|
||||
module('Unit | Utility | analytics/search/dwell-time', function(hooks) {
|
||||
module('Unit | Utility | analytics/search/dwell-time', function(hooks): void {
|
||||
setupTest(hooks);
|
||||
|
||||
test('instantiation succeeds', function(assert) {
|
||||
test('instantiation succeeds', function(assert): void {
|
||||
const routerService = this.owner.lookup('service:router');
|
||||
let dwellTime = new DwellTime(searchRouteName, routerService);
|
||||
assert.ok(dwellTime);
|
||||
});
|
||||
|
||||
test('dwellTime properties', function(assert) {
|
||||
test('dwellTime properties', function(assert): void {
|
||||
const routerService = this.owner.lookup('service:router');
|
||||
let dwellTime = new DwellTime(searchRouteName, routerService);
|
||||
|
||||
@ -30,7 +30,7 @@ module('Unit | Utility | analytics/search/dwell-time', function(hooks) {
|
||||
assert.equal(dwellTime.startTime, 0, 'expected startTime to be reset by resetDwellTimeTracking');
|
||||
});
|
||||
|
||||
test('dwellTime cleanup', function(assert) {
|
||||
test('dwellTime cleanup', function(assert): void {
|
||||
assert.expect(1);
|
||||
|
||||
const dwellTime = new DwellTime(searchRouteName, this.owner.lookup('service:router'));
|
||||
@ -12,6 +12,12 @@
|
||||
"@datahub/tracking/test-support/*": ["addon-test-support/*"],
|
||||
"@datahub/utils": ["../../@datahub/utils/addon"],
|
||||
"@datahub/utils/*": ["../../@datahub/utils/addon/*"],
|
||||
"@datahub/shared": ["../../@datahub/shared/addon"],
|
||||
"@datahub/shared/*": ["../../@datahub/shared/addon/*"],
|
||||
"@datahub/data-models": ["../../@datahub/data-models/addon"],
|
||||
"@datahub/data-models/*": ["../../@datahub/data-models/addon/*"],
|
||||
"@datahub/metadata-types": ["../../@datahub/metadata-types/addon"],
|
||||
"@datahub/metadata-types/*": ["../../@datahub/metadata-types/addon/*"],
|
||||
"*": ["types/*"]
|
||||
}
|
||||
},
|
||||
@ -23,6 +29,12 @@
|
||||
"test-support/**/*",
|
||||
"addon-test-support/**/*",
|
||||
"../../@datahub/utils/addon/**/*",
|
||||
"../../@datahub/utils/types/**/*"
|
||||
"../../@datahub/utils/types/**/*",
|
||||
"../../@datahub/shared/addon/**/*",
|
||||
"../../@datahub/shared/types/**/*",
|
||||
"../../@datahub/data-models/addon/**/*",
|
||||
"../../@datahub/data-models/types/**/*",
|
||||
"../../@datahub/metadata-types/addon/**/*",
|
||||
"../../@datahub/metadata-types/types/**/*"
|
||||
]
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { TrackingEventCategory, TrackingGoal } from 'wherehows-web/constants/analytics/event-tracking';
|
||||
import { TrackingEventCategory, TrackingGoal } from '@datahub/tracking/constants/event-tracking';
|
||||
|
||||
/**
|
||||
* Describes the interface for a tracking event.
|
||||
13
datahub-web/@datahub/tracking/types/search/index.ts
Normal file
13
datahub-web/@datahub/tracking/types/search/index.ts
Normal file
@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Describes the function parameters for the tracking service method to track site search event
|
||||
* @export
|
||||
* @interface ITrackSiteSearchParams
|
||||
*/
|
||||
export interface ITrackSiteSearchParams {
|
||||
// Search keyword the user searched for
|
||||
keyword: string;
|
||||
// Search category for search results. If not needed, set to false
|
||||
entity: string | false;
|
||||
// Number of results on the search results page. Zero indicates a 'No Results Search'. Set to false if not known
|
||||
searchCount: number | false;
|
||||
}
|
||||
8
datahub-web/@datahub/tracking/types/vendor/piwik.d.ts
vendored
Normal file
8
datahub-web/@datahub/tracking/types/vendor/piwik.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Merges global type defs for global modules on the window namespace.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/interface-name-prefix
|
||||
interface Window {
|
||||
// global array for piwik tracking
|
||||
_paq: Array<Array<string>> & { push: (items: Array<string | boolean | number>) => number };
|
||||
}
|
||||
@ -1,9 +1,4 @@
|
||||
{
|
||||
/**
|
||||
Ember CLI sends analytics information by default. The data is completely
|
||||
anonymous, but there are times when you might want to disable this behavior.
|
||||
|
||||
Setting `disableAnalytics` to true will prevent any data from being sent.
|
||||
*/
|
||||
"disableAnalytics": false
|
||||
"disableAnalytics": false,
|
||||
"port": 9011
|
||||
}
|
||||
|
||||
@ -0,0 +1,22 @@
|
||||
import { DatasetEntity } from '@datahub/data-models/entity/dataset/dataset-entity';
|
||||
|
||||
// TODO: [META-9851] Temporary home for this function for integrated dev/demo, should be moved to test/dummy folder
|
||||
/**
|
||||
* Models a DatasetEntity-like class that we can mess with for mock data purposes in the dummy app.
|
||||
* This way, we don't need a full implementation for a mock entity in order to view it in our dummy
|
||||
* environment
|
||||
*/
|
||||
export class FakeDatasetEntity extends DatasetEntity {
|
||||
savedName: string = '';
|
||||
|
||||
/**
|
||||
* Creates a blank name field for the object that we can fill in for mock data
|
||||
*/
|
||||
get name(): string {
|
||||
return this.savedName;
|
||||
}
|
||||
|
||||
set name(name: string) {
|
||||
this.savedName = name;
|
||||
}
|
||||
}
|
||||
31
datahub-web/@datahub/user/addon/mocks/person-entity.ts
Normal file
31
datahub-web/@datahub/user/addon/mocks/person-entity.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { PersonEntity } from '@datahub/data-models/entity/person/person-entity';
|
||||
|
||||
// TODO: [META-9851] Temporary home for this function for integrated dev/demo, should be moved to test/dummy folder eventually
|
||||
/**
|
||||
* Allows us for demo purposes or testing purposes to provide mock data directly on a class
|
||||
* instance for a person entity.
|
||||
* @param {PersonEntity} entity - the entity instance we want to decorate
|
||||
* @param {typeof PersonEntity} personEntity - the class object itself. Optional. Passed in if we
|
||||
* want to utilize the implementation of the class not defined in the open sourced version
|
||||
*/
|
||||
export function populateMockPersonEntity(
|
||||
entity: PersonEntity,
|
||||
personEntity: typeof PersonEntity = PersonEntity
|
||||
): PersonEntity {
|
||||
entity.name = 'Ash Ketchum';
|
||||
entity.title = 'Pokemon master in training';
|
||||
entity.profilePictureUrl = 'https://i.imgur.com/vjLcuFJ.jpg';
|
||||
entity.email = 'ashketchumfrompallet@gmail.com';
|
||||
entity.linkedinProfile = 'https://www.linkedin.com/in/ash-ketchum-b212502a/';
|
||||
entity.slackLink = 'aketchum';
|
||||
entity.skills = ['training', 'catching', 'battling'];
|
||||
const managerEntity = new personEntity(personEntity.urnFromUsername('pikachu'));
|
||||
managerEntity.name = 'Pikachu';
|
||||
managerEntity.profilePictureUrl = 'https://pokemonletsgo.pokemon.com/assets/img/common/char-pikachu.png';
|
||||
|
||||
entity.reportsTo = managerEntity;
|
||||
entity.teamTags = ['Kanta', 'Nintendo', 'Game Freak', 'Bike Thief'];
|
||||
entity.focusArea = 'Trying to catch all these Pokemon in the world. Also being the very best like no one ever was.';
|
||||
|
||||
return entity;
|
||||
}
|
||||
1
datahub-web/@datahub/user/app/styles/user-entity.scss
Normal file
1
datahub-web/@datahub/user/app/styles/user-entity.scss
Normal file
@ -0,0 +1 @@
|
||||
@import 'user/all';
|
||||
1
datahub-web/@datahub/user/app/styles/user/_all.scss
Normal file
1
datahub-web/@datahub/user/app/styles/user/_all.scss
Normal file
@ -0,0 +1 @@
|
||||
@import 'profile/all';
|
||||
@ -0,0 +1,3 @@
|
||||
@import 'header';
|
||||
@import 'focus-area';
|
||||
@import 'content';
|
||||
@ -0,0 +1,58 @@
|
||||
.user-profile-content {
|
||||
$application-navbar-static-height: item-spacing(8) !default;
|
||||
$banner-alerts-height: 52px !default;
|
||||
|
||||
margin-top: item-spacing(6);
|
||||
|
||||
.user-profile-tabs {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
width: $max-container-width;
|
||||
margin: 0 auto;
|
||||
|
||||
&__tabslist {
|
||||
&.ivy-tabs-tablist {
|
||||
height: fit-content;
|
||||
|
||||
&[role='tablist'] {
|
||||
box-sizing: border-box;
|
||||
border-top: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
width: 264px;
|
||||
position: sticky;
|
||||
top: $application-navbar-static-height;
|
||||
background-color: get-color(white);
|
||||
margin: item-spacing(2 0);
|
||||
|
||||
.user-profile-tabs__tab {
|
||||
&.ivy-tabs-tab {
|
||||
&[role='tab'] {
|
||||
$set-padding: item-spacing(4);
|
||||
|
||||
margin-left: item-spacing(0);
|
||||
padding: item-spacing(3) $set-padding;
|
||||
width: 100%;
|
||||
|
||||
&:first-child {
|
||||
padding-left: $set-padding;
|
||||
}
|
||||
|
||||
&[aria-selected='true'] {
|
||||
color: get-color(black, 0.6);
|
||||
background-color: get-color(slate3, 0.25);
|
||||
|
||||
&:before {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
.user-focus-area {
|
||||
width: 50%;
|
||||
margin-bottom: item-spacing(5);
|
||||
|
||||
&__title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
margin-left: item-spacing(2);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__content {
|
||||
width: 450px;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
&__skills {
|
||||
margin-bottom: item-spacing(2);
|
||||
}
|
||||
|
||||
.nacho-pill {
|
||||
margin-right: item-spacing(2);
|
||||
line-height: 16px;
|
||||
}
|
||||
}
|
||||
136
datahub-web/@datahub/user/app/styles/user/profile/_header.scss
Normal file
136
datahub-web/@datahub/user/app/styles/user/profile/_header.scss
Normal file
@ -0,0 +1,136 @@
|
||||
.user-entity-header {
|
||||
display: flex;
|
||||
padding: item-spacing(6 0 4);
|
||||
|
||||
&__container {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
&__img-section {
|
||||
width: 128px;
|
||||
box-sizing: border-box;
|
||||
padding-right: item-spacing(5);
|
||||
}
|
||||
|
||||
&__detail-section {
|
||||
padding-left: item-spacing(5);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__profile-img {
|
||||
$profile-img-dimensions: 104px;
|
||||
height: $profile-img-dimensions;
|
||||
width: $profile-img-dimensions;
|
||||
}
|
||||
|
||||
&__external-link {
|
||||
border: 1px solid get-color(blue5);
|
||||
color: get-color(blue5);
|
||||
padding: item-spacing(2);
|
||||
line-height: 20px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
|
||||
&:hover {
|
||||
border: 1px solid get-color(black);
|
||||
}
|
||||
}
|
||||
|
||||
&__edit-profile {
|
||||
border: 1px solid get-color(blue5);
|
||||
color: get-color(blue5);
|
||||
padding: item-spacing(2);
|
||||
line-height: 20px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
|
||||
&:hover {
|
||||
border: 1px solid get-color(black);
|
||||
}
|
||||
}
|
||||
|
||||
&__name {
|
||||
&-container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
margin: item-spacing(2 0 0);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
&__job-title {
|
||||
margin: item-spacing(2 0 5);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__user-details {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&__org {
|
||||
width: 50%;
|
||||
margin-bottom: item-spacing(5);
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&__manager {
|
||||
width: 50%;
|
||||
|
||||
&-name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&-img {
|
||||
margin-right: item-spacing(2);
|
||||
}
|
||||
}
|
||||
|
||||
&__team {
|
||||
width: 50%;
|
||||
|
||||
&-name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
.nacho-pill {
|
||||
padding: item-spacing(0 2);
|
||||
margin: item-spacing(0 2 2 0);
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
&__connections {
|
||||
display: flex;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
&__connection {
|
||||
font-size: 12px;
|
||||
margin-right: item-spacing(4);
|
||||
cursor: pointer;
|
||||
|
||||
&-icon:hover,
|
||||
&:hover {
|
||||
color: get-color(blue6);
|
||||
}
|
||||
|
||||
&-link {
|
||||
color: get-color(black);
|
||||
}
|
||||
|
||||
&-icon {
|
||||
color: get-color(blue5);
|
||||
margin-right: item-spacing(1);
|
||||
transform: scale(1.25);
|
||||
}
|
||||
}
|
||||
}
|
||||
1
datahub-web/@datahub/user/app/templates/user/profile.js
Normal file
1
datahub-web/@datahub/user/app/templates/user/profile.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from '@datahub/user/templates/user/profile';
|
||||
@ -22,18 +22,28 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@datahub/data-models": "0.0.0",
|
||||
"@datahub/metadata-types": "0.0.0",
|
||||
"@datahub/shared": "0.0.0",
|
||||
"@datahub/utils": "0.0.0",
|
||||
"@fortawesome/ember-fontawesome": "^0.1.13",
|
||||
"@fortawesome/free-brands-svg-icons": "^5.7.2",
|
||||
"@fortawesome/free-regular-svg-icons": "^5.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.7.2",
|
||||
"@nacho-ui/avatars": "^0.0.21",
|
||||
"@nacho-ui/core": "^0.0.21",
|
||||
"@nacho-ui/dropdown": "^0.0.21",
|
||||
"@nacho-ui/pill": "^0.0.21",
|
||||
"@nacho-ui/table": "^0.0.21",
|
||||
"dynamic-link": "^0.2.3",
|
||||
"ember-cli-babel": "^7.8.0",
|
||||
"ember-cli-htmlbars": "^3.0.0",
|
||||
"ember-cli-moment-shim": "^3.7.1",
|
||||
"ember-cli-sass": "^10.0.0",
|
||||
"ember-cli-typescript": "^2.0.2",
|
||||
"ember-composable-helpers": "^2.1.0",
|
||||
"ember-concurrency": "^1.0.0",
|
||||
"ember-modal-dialog": "^3.0.0-beta.0",
|
||||
"ember-moment": "^7.8.1",
|
||||
"ember-simple-auth": "^1.8.2",
|
||||
"ember-truth-helpers": "^2.1.0",
|
||||
"ivy-tabs": "^3.3.0"
|
||||
|
||||
@ -1,2 +1 @@
|
||||
const testemConf = require('../../configs/testem-base');
|
||||
module.exports = testemConf;
|
||||
module.exports = require('../../configs/testem-base');
|
||||
|
||||
@ -0,0 +1,8 @@
|
||||
import { FakeDatasetEntity as mockEntity } from '@datahub/user/mocks/models/dataset-entity';
|
||||
|
||||
/**
|
||||
* Temporarily importing the mock function from the addon/ folder. However, that's only there for
|
||||
* demo purposes. After implementing real data in the integrated application, the function should
|
||||
* be moved here where it will only be used for testing purposes
|
||||
*/
|
||||
export const FakeDatasetEntity = mockEntity;
|
||||
27
datahub-web/@datahub/user/tests/dummy/app/router.js
Normal file
27
datahub-web/@datahub/user/tests/dummy/app/router.js
Normal file
@ -0,0 +1,27 @@
|
||||
// NOTE: Named as .js to solve issue: no such file or directory, ...tests/dummy/app/router.js'
|
||||
// when running ember g route <NAME> --dummy command
|
||||
import EmberRouter from '@ember/routing/router';
|
||||
import config from './config/environment';
|
||||
|
||||
const Router = EmberRouter.extend({
|
||||
location: config.locationType,
|
||||
rootURL: config.rootURL
|
||||
});
|
||||
|
||||
Router.map(function() {
|
||||
this.route('user', function() {
|
||||
this.route('ump-flow');
|
||||
this.route(
|
||||
'entity',
|
||||
{
|
||||
path: '/entity/:entity'
|
||||
},
|
||||
function() {
|
||||
this.route('own');
|
||||
}
|
||||
);
|
||||
this.route('profile', { path: '/:user_id' });
|
||||
});
|
||||
});
|
||||
|
||||
export default Router;
|
||||
@ -1,11 +0,0 @@
|
||||
import EmberRouter from '@ember/routing/router';
|
||||
import config from './config/environment';
|
||||
|
||||
const Router = EmberRouter.extend({
|
||||
location: config.locationType,
|
||||
rootURL: config.rootURL
|
||||
});
|
||||
|
||||
Router.map(function() {});
|
||||
|
||||
export default Router;
|
||||
@ -0,0 +1,4 @@
|
||||
@import 'datahub-utils';
|
||||
@import 'nacho-core';
|
||||
@import 'nacho-table';
|
||||
@import 'nacho-pill';
|
||||
11
datahub-web/@datahub/user/tests/unit/routes/user-test.ts
Normal file
11
datahub-web/@datahub/user/tests/unit/routes/user-test.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Route | user', function(hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it exists', function(assert) {
|
||||
let route = this.owner.lookup('route:user');
|
||||
assert.ok(route);
|
||||
});
|
||||
});
|
||||
@ -35,6 +35,6 @@
|
||||
"../../@datahub/shared/addon/**/*",
|
||||
"../../@datahub/shared/types/**/*",
|
||||
"../../@datahub/utils/addon/**/*",
|
||||
"../../@datahub/utils/types/**/*",
|
||||
"../../@datahub/utils/types/**/*"
|
||||
]
|
||||
}
|
||||
|
||||
@ -130,7 +130,7 @@ export const returnDefaultIfNotFound = async <T>(request: Promise<T>, defaultVal
|
||||
* @param arg
|
||||
*/
|
||||
const argToString = (arg: unknown): string => {
|
||||
// @ts-ignore
|
||||
// @ts-ignore https://github.com/typed-ember/ember-cli-typescript/issues/799
|
||||
if (typeOf(arg) === 'object') {
|
||||
return JSON.stringify(arg);
|
||||
} else {
|
||||
|
||||
@ -5,12 +5,13 @@ import Component from '@ember/component';
|
||||
// @ts-ignore: Ignore import of compiled template
|
||||
import template from '../templates/components/nacho-hover-dropdown';
|
||||
import { set } from '@ember/object';
|
||||
import { TaskInstance, timeout, task, Task } from 'ember-concurrency';
|
||||
import { TaskInstance, timeout, task } from 'ember-concurrency';
|
||||
import { action } from '@ember/object';
|
||||
import { classNames, layout } from '@ember-decorators/component';
|
||||
import { noop } from 'lodash';
|
||||
import { assert } from '@ember/debug';
|
||||
import { INachoDropdownOption } from '@nacho-ui/dropdown/types/nacho-dropdown';
|
||||
import { ETask } from '@datahub/utils/types/concurrency';
|
||||
|
||||
/**
|
||||
* Params to show dropdown
|
||||
@ -55,7 +56,7 @@ export default class NachoHoverDropdown<T> extends Component {
|
||||
/**
|
||||
* References the most recent TaskInstance to hide the drop-down options
|
||||
*/
|
||||
mostRecentHideTask?: TaskInstance<Promise<void>>;
|
||||
mostRecentHideTask?: TaskInstance<Promise<void> | void>;
|
||||
|
||||
/**
|
||||
* Task triggers the rendering of the list of drop-down options
|
||||
@ -64,7 +65,7 @@ export default class NachoHoverDropdown<T> extends Component {
|
||||
set(this, 'isExpanded', true);
|
||||
dd.actions.open();
|
||||
})
|
||||
showDropDownTask!: Task<void, (dd: IShowDropdownParams) => void>;
|
||||
showDropDownTask!: ETask<void, IShowDropdownParams>;
|
||||
|
||||
/**
|
||||
* Task triggers the occluding of the list of drop-down options
|
||||
@ -74,7 +75,7 @@ export default class NachoHoverDropdown<T> extends Component {
|
||||
yield timeout(200);
|
||||
dd.actions.close();
|
||||
})
|
||||
hideDropDownTask!: Task<void, (dd: IHideDropdownParams) => TaskInstance<Promise<void>>>;
|
||||
hideDropDownTask!: ETask<Promise<void>, IHideDropdownParams>;
|
||||
|
||||
/**
|
||||
* Action handler to prevent bubbling DOM event action
|
||||
|
||||
12
datahub-web/@datahub/utils/addon/helpers/wait-time.ts
Normal file
12
datahub-web/@datahub/utils/addon/helpers/wait-time.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { later } from '@ember/runloop';
|
||||
|
||||
/**
|
||||
* Helper to convert a timeout into a promise which is more convinient for some places
|
||||
* @param ms milliseconds that you need to wait
|
||||
*/
|
||||
export function waitTime(ms: number): Promise<void> {
|
||||
const promise: Promise<void> = new Promise((resolve): void => {
|
||||
later((): void => resolve(), ms);
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
25
datahub-web/@datahub/utils/addon/routes/routing.ts
Normal file
25
datahub-web/@datahub/utils/addon/routes/routing.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { MaybeRouteInfoWithAttributes } from '@datahub/utils/types/vendor/routerjs';
|
||||
|
||||
/**
|
||||
* Takes a RouteInfo to resolver function mapping and a RouteInfo instance, resolves a route name
|
||||
* Some routes are shared between different but related features. This is done via placeholders.
|
||||
* This provides a way to resolve the meaningful value of the placeholder segments.
|
||||
* @param {(Record<string, ((r: MaybeRouteInfoWithAttributes) => string) | void>)} routeResolverMap a map of route names to
|
||||
* functions that parse the route name to a common string for example, datasets.dataset.tab route => datasets.schema,
|
||||
* where schema is the current tab in this instance
|
||||
* @param {MaybeRouteInfoWithAttributes | null} routeBeingTransitionedTo route info object for the route being navigated to
|
||||
* @returns {string | null}
|
||||
*/
|
||||
export const resolveDynamicRouteName = (
|
||||
routeResolverMap: Record<string, ((r: MaybeRouteInfoWithAttributes) => string) | void>,
|
||||
routeBeingTransitionedTo?: MaybeRouteInfoWithAttributes | null
|
||||
): string | null => {
|
||||
if (routeBeingTransitionedTo) {
|
||||
const routeName = routeBeingTransitionedTo.name;
|
||||
const resolveRouteName = routeResolverMap[routeName];
|
||||
|
||||
return typeof resolveRouteName === 'function' ? resolveRouteName(routeBeingTransitionedTo) : routeName;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
@ -2,7 +2,7 @@ import Service from '@ember/service';
|
||||
import { INotification, IToast, IConfirmOptions } from '@datahub/utils/types/notifications/service';
|
||||
import { NotificationEvent, NotificationType } from '@datahub/utils/constants/notifications';
|
||||
import { setProperties, set } from '@ember/object';
|
||||
import { timeout, task, Task } from 'ember-concurrency';
|
||||
import { timeout, task } from 'ember-concurrency';
|
||||
import { action, computed } from '@ember/object';
|
||||
import {
|
||||
notificationDialogActionFactory,
|
||||
@ -10,6 +10,7 @@ import {
|
||||
isAConfirmationModal
|
||||
} from '@datahub/utils/lib/notifications';
|
||||
import { noop } from '@datahub/utils/function/noop';
|
||||
import { ETaskPromise } from '@datahub/utils/types/concurrency';
|
||||
|
||||
/**
|
||||
* Defines the Notifications Service which handles the co-ordination and manages rendering of notification components in the
|
||||
@ -74,7 +75,7 @@ export default class Notifications extends Service {
|
||||
}
|
||||
}
|
||||
}).restartable())
|
||||
setCurrentNotificationTask!: Task<Promise<void>, (notification: INotification) => Promise<void>>;
|
||||
setCurrentNotificationTask!: ETaskPromise<void, INotification>;
|
||||
|
||||
/**
|
||||
* Takes a notification instance and sets a reference to the current notification on the service,
|
||||
|
||||
@ -0,0 +1,33 @@
|
||||
import Ember from 'ember';
|
||||
|
||||
const originalExceptionHandler = Ember.onerror;
|
||||
|
||||
/**
|
||||
* Helps test that an expected exception is raised within a QUnit test case
|
||||
* The error should be expected under certain conditions while the application is running
|
||||
* @param {Assert} assert reference to the QUnit Assert object for the currently running test
|
||||
* @param {() => void} raiseException the function to invoke when an exception is expected to be raised
|
||||
* @param {(error: Error) => boolean} isValidException a guard to check that the exception thrown matches the expected value, e.g. message equality, referential equality
|
||||
*/
|
||||
export const assertThrownException = async (
|
||||
assert: Assert,
|
||||
raiseException: () => void,
|
||||
isValidException: (error: Error) => boolean
|
||||
): Promise<void> => {
|
||||
let isExceptionThrown = false;
|
||||
|
||||
Ember.onerror = (error: Error): void => {
|
||||
isExceptionThrown = true;
|
||||
if (!isValidException(error)) {
|
||||
typeof originalExceptionHandler === 'function' && originalExceptionHandler(error);
|
||||
}
|
||||
};
|
||||
|
||||
await raiseException();
|
||||
|
||||
// Assert that an exception has to be thrown when this function is invoked otherwise, a non throw is an exception
|
||||
assert.ok(isExceptionThrown, 'Expected an exception to be thrown');
|
||||
|
||||
// Restore onerror to state before test assertion
|
||||
Ember.onerror = originalExceptionHandler;
|
||||
};
|
||||
13
datahub-web/@datahub/utils/addon/types/vendor/routerjs.d.ts
vendored
Normal file
13
datahub-web/@datahub/utils/addon/types/vendor/routerjs.d.ts
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
import RouteInfo from '@ember/routing/-private/route-info';
|
||||
|
||||
/**
|
||||
* Extends the RouteInfo interface with the attribute attributes which contains
|
||||
* parameters from the current transition route
|
||||
* @export
|
||||
* @interface MaybeRouteInfoWithAttributes
|
||||
* @extends {RouteInfo}
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/interface-name-prefix
|
||||
export interface MaybeRouteInfoWithAttributes extends RouteInfo {
|
||||
attributes?: any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
}
|
||||
@ -4,5 +4,5 @@ import { typeOf } from '@ember/utils';
|
||||
* Checks if a type is an object
|
||||
* @param {any} candidate the entity to check
|
||||
*/
|
||||
// @ts-ignore
|
||||
// @ts-ignore https://github.com/typed-ember/ember-cli-typescript/issues/799
|
||||
export const isObject = (candidate: unknown): candidate is object => typeOf(candidate) === 'object';
|
||||
|
||||
@ -1,2 +1 @@
|
||||
const testemConf = require('../../configs/testem-base');
|
||||
module.exports = testemConf;
|
||||
module.exports = require('../../configs/testem-base');
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { containerDataSource } from '@datahub/utils/api/data-source';
|
||||
import { Task, task } from 'ember-concurrency';
|
||||
import { task } from 'ember-concurrency';
|
||||
import { ETaskPromise } from 'concurrency';
|
||||
|
||||
class PretendComponent {
|
||||
didInsertElement(): void {}
|
||||
@ -17,5 +18,5 @@ export default class TestForDecoratorsDataSourceComponent extends PretendCompone
|
||||
@task(function*(this: TestForDecoratorsDataSourceComponent): IterableIterator<Promise<void>> {
|
||||
this.assert && this.assert.ok(true, this.message);
|
||||
})
|
||||
getContainerDataTask!: Task<Promise<void>, () => Promise<void>>;
|
||||
getContainerDataTask!: ETaskPromise<void>;
|
||||
}
|
||||
|
||||
23
datahub-web/@datahub/utils/types/concurrency.d.ts
vendored
Normal file
23
datahub-web/@datahub/utils/types/concurrency.d.ts
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
import { Task, TaskInstance } from 'ember-concurrency';
|
||||
|
||||
/**
|
||||
* Helper types to reduce the amount of code to define a Task
|
||||
*/
|
||||
type ETask<T, T1 = undefined, T2 = undefined, T3 = undefined> = T1 extends undefined
|
||||
? Task<TaskInstance<T>, () => TaskInstance<T>>
|
||||
: T2 extends undefined
|
||||
? Task<TaskInstance<T>, (a: T1) => TaskInstance<T>>
|
||||
: T3 extends undefined
|
||||
? Task<TaskInstance<T>, (a: T1, b: T2) => TaskInstance<T>>
|
||||
: Task<TaskInstance<T>, (a: T1, b: T2, c: T3) => TaskInstance<T>>;
|
||||
|
||||
/**
|
||||
* Same as ETask but instead of returning a TaskInstance, returns a Promise which is a common practice
|
||||
*/
|
||||
type ETaskPromise<T = void, T1 = undefined, T2 = undefined, T3 = undefined> = T1 extends undefined
|
||||
? Task<Promise<T>, () => Promise<T>>
|
||||
: T2 extends undefined
|
||||
? Task<Promise<T>, (a: T1) => Promise<T>>
|
||||
: T3 extends undefined
|
||||
? Task<Promise<T>, (a: T1, b: T2) => Promise<T>>
|
||||
: Task<Promise<T>, (a: T1, b: T2, c: T3) => Promise<T>>;
|
||||
@ -1,2 +1 @@
|
||||
const testemConf = require('../../configs/testem-base');
|
||||
module.exports = testemConf;
|
||||
module.exports = require('../../configs/testem-base');
|
||||
|
||||
@ -10,9 +10,6 @@ module.exports = {
|
||||
// --no-sandbox is needed when running Chrome inside a container
|
||||
process.env.CI ? '--no-sandbox' : null,
|
||||
'--headless',
|
||||
/*'--disable-gpu',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-software-rasterizer',*/
|
||||
'--mute-audio',
|
||||
'--remote-debugging-port=0',
|
||||
'--window-size=1440,900'
|
||||
|
||||
@ -3,8 +3,9 @@ import { IAvatar } from 'wherehows-web/typings/app/avatars';
|
||||
import { alias } from '@ember/object/computed';
|
||||
import { set } from '@ember/object';
|
||||
import { classNames, tagName, attribute } from '@ember-decorators/component';
|
||||
import { Task, task } from 'ember-concurrency';
|
||||
import { task } from 'ember-concurrency';
|
||||
import { run } from '@ember/runloop';
|
||||
import { ETaskPromise } from '@datahub/utils/types/concurrency';
|
||||
|
||||
@tagName('img')
|
||||
@classNames('avatar')
|
||||
@ -44,11 +45,10 @@ export default class AvatarImage extends Component {
|
||||
|
||||
/**
|
||||
* Task to set the fallback image for an avatar
|
||||
* @type Task<void, (a?: {}) => TaskInstance<void>>
|
||||
* @memberof AvatarImage
|
||||
*/
|
||||
@task(function*(this: AvatarImage): IterableIterator<void> {
|
||||
set(this, 'src', this.avatar.imageUrlFallback);
|
||||
})
|
||||
onImageFallback!: Task<void, () => void>;
|
||||
onImageFallback!: ETaskPromise<void>;
|
||||
}
|
||||
|
||||
@ -8,9 +8,10 @@ import { computed } from '@ember/object';
|
||||
import { alias, or } from '@ember/object/computed';
|
||||
import BrowseEntity from 'wherehows-web/routes/browse/entity';
|
||||
import { DatasetEntity } from '@datahub/data-models/entity/dataset/dataset-entity';
|
||||
import { Task, task } from 'ember-concurrency';
|
||||
import Configurator from 'wherehows-web/services/configurator';
|
||||
import { task } from 'ember-concurrency';
|
||||
import { getConfig } from 'wherehows-web/services/configurator';
|
||||
import { BaseEntity } from '@datahub/data-models/entity/base-entity';
|
||||
import { ETaskPromise } from '@datahub/utils/types/concurrency';
|
||||
|
||||
/**
|
||||
* Defines the container component to fetch nodes for a given entity constrained by category, or prefix, etc
|
||||
@ -34,7 +35,7 @@ export default class EntityCategoriesContainer extends Component {
|
||||
/**
|
||||
* Flag to say whether we are using the new api for datasets
|
||||
*/
|
||||
useNewBrowseDataset: boolean = Configurator.getConfig('useNewBrowseDataset');
|
||||
useNewBrowseDataset: boolean = getConfig('useNewBrowseDataset');
|
||||
|
||||
/**
|
||||
* References the current DataModelEntity class if applicable
|
||||
@ -110,7 +111,7 @@ export default class EntityCategoriesContainer extends Component {
|
||||
});
|
||||
}
|
||||
}).restartable())
|
||||
getEntityCategoriesNodesTask!: Task<Promise<IBrowsePath>, () => Promise<IBrowsePath>>;
|
||||
getEntityCategoriesNodesTask!: ETaskPromise<IBrowsePath>;
|
||||
/**
|
||||
* Closure action for big-list onFinished for entity list
|
||||
*
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import Component from '@ember/component';
|
||||
import { set } from '@ember/object';
|
||||
import { Task, task } from 'ember-concurrency';
|
||||
import { task } from 'ember-concurrency';
|
||||
import { containerDataSource } from '@datahub/utils/api/data-source';
|
||||
import { tagName } from '@ember-decorators/component';
|
||||
import { DataModelEntity, DataModelName } from '@datahub/data-models/constants/entity';
|
||||
import { ETaskPromise } from '@datahub/utils/types/concurrency';
|
||||
|
||||
// TODO META-8863 remove once dataset is migrated
|
||||
@tagName('')
|
||||
@ -22,12 +23,11 @@ export default class EntityCategoryContainer extends Component {
|
||||
|
||||
/**
|
||||
* Task to request the data platform's count
|
||||
* @type {(Task<Promise<number>, (a?: any) => TaskInstance<Promise<number>>>)}
|
||||
*/
|
||||
@task(function*(this: EntityCategoryContainer): IterableIterator<Promise<number>> {
|
||||
const { entity, category } = this;
|
||||
const modelEntity: DataModelEntity = DataModelEntity[entity];
|
||||
set(this, 'count', yield modelEntity.readCategoriesCount(category));
|
||||
})
|
||||
getEntityCountTask!: Task<Promise<number>, () => Promise<number>>;
|
||||
getEntityCountTask!: ETaskPromise<number>;
|
||||
}
|
||||
|
||||
@ -19,12 +19,13 @@ import {
|
||||
import { OwnerSource, OwnerType } from 'wherehows-web/utils/api/datasets/owners';
|
||||
import Notifications from '@datahub/utils/services/notifications';
|
||||
import { noop } from 'wherehows-web/utils/helpers/functions';
|
||||
import { IAppConfig } from 'wherehows-web/typings/api/configurator/configurator';
|
||||
import { IAppConfig } from '@datahub/shared/types/configurator/configurator';
|
||||
import { makeAvatar } from 'wherehows-web/constants/avatars/avatars';
|
||||
import { OwnerWithAvatarRecord } from 'wherehows-web/typings/app/datasets/owners';
|
||||
import { NotificationEvent } from '@datahub/utils/constants/notifications';
|
||||
import { PersonEntity } from '@datahub/data-models/entity/person/person-entity';
|
||||
import { Task, task } from 'ember-concurrency';
|
||||
import { task } from 'ember-concurrency';
|
||||
import { ETaskPromise } from '@datahub/utils/types/concurrency';
|
||||
|
||||
type Comparator = -1 | 0 | 1;
|
||||
|
||||
@ -217,7 +218,7 @@ export default class DatasetAuthors extends Component {
|
||||
get commonOwners(): Array<IOwner> {
|
||||
const { confirmedOwners, systemGeneratedOwners } = this;
|
||||
|
||||
return confirmedOwners.reduce((common, owner) => {
|
||||
return confirmedOwners.reduce((common, owner): Array<IOwner> => {
|
||||
const { userName } = owner;
|
||||
return systemGeneratedOwners.findBy('userName', userName) ? [...common, owner] : common;
|
||||
}, []);
|
||||
@ -248,13 +249,12 @@ export default class DatasetAuthors extends Component {
|
||||
systemGeneratedOwnersWithAvatars: Array<OwnerWithAvatarRecord>;
|
||||
/**
|
||||
* Invokes the external action as a dropping task
|
||||
* @type {Task<Promise<Array<IOwner>>, void>}
|
||||
* @memberof DatasetAuthors
|
||||
*/
|
||||
@(task(function*(this: DatasetAuthors): IterableIterator<Promise<Array<IOwner>>> {
|
||||
yield this.save(this.owners);
|
||||
}).drop())
|
||||
saveOwners!: Task<Promise<Array<IOwner>>, () => Promise<Array<IOwner>>>;
|
||||
saveOwners!: ETaskPromise<Array<IOwner>>;
|
||||
/**
|
||||
* Adds the component owner record to the list of owners with default props
|
||||
* @returns {Array<IOwner> | void}
|
||||
|
||||
@ -1,152 +0,0 @@
|
||||
import Component from '@ember/component';
|
||||
import { set } from '@ember/object';
|
||||
import { baseCommentEditorOptions } from 'wherehows-web/constants';
|
||||
import { action, computed } from '@ember/object';
|
||||
import { IDatasetView } from 'wherehows-web/typings/api/datasets/dataset';
|
||||
import { classNames } from '@ember-decorators/component';
|
||||
import { reads } from '@ember/object/computed';
|
||||
|
||||
@classNames('dataset-deprecation-toggle')
|
||||
export default class DatasetDeprecation extends Component {
|
||||
/**
|
||||
* Currently selected date
|
||||
* @type {Date}
|
||||
* @memberof DatasetAclAccess
|
||||
*/
|
||||
selectedDate: Date = new Date();
|
||||
|
||||
/**
|
||||
* Date around which the calendar is centered
|
||||
* @type {Date}
|
||||
* @memberof DatasetAclAccess
|
||||
*/
|
||||
centeredDate: Date = this.selectedDate;
|
||||
|
||||
/**
|
||||
* Date the dataset should be decommissioned
|
||||
* @type {IDatasetView.decommissionTime}
|
||||
* @memberof DatasetAclAccess
|
||||
*/
|
||||
decommissionTime: IDatasetView['decommissionTime'];
|
||||
|
||||
/**
|
||||
* The earliest date a user can select as a decommission date
|
||||
* @type {Date}
|
||||
* @memberof DatasetAclAccess
|
||||
*/
|
||||
minSelectableDecommissionDate: Date = new Date(Date.now() + 24 * 60 * 60 * 1000);
|
||||
|
||||
/**
|
||||
* Flag indicating that the dataset is deprecated or otherwise
|
||||
* @type {(null | boolean)}
|
||||
* @memberof DatasetDeprecation
|
||||
*/
|
||||
deprecated: null | boolean;
|
||||
|
||||
/**
|
||||
* Working reference to the dataset's deprecated flag
|
||||
* @memberof DatasetDeprecation
|
||||
* @type {ComputedProperty<DatasetDeprecation.deprecated>}
|
||||
*/
|
||||
@reads('deprecated')
|
||||
deprecatedAlias: boolean;
|
||||
|
||||
/**
|
||||
* Note accompanying the deprecation flag change
|
||||
* @type {string}
|
||||
* @memberof DatasetDeprecation
|
||||
*/
|
||||
deprecationNote: string;
|
||||
|
||||
/**
|
||||
* Working reference to the dataset's deprecationNote
|
||||
* @memberof DatasetDeprecation
|
||||
* @type {ComputedProperty<DatasetDeprecation.deprecationNote>}
|
||||
*/
|
||||
@reads('deprecationNote')
|
||||
deprecationNoteAlias: string;
|
||||
|
||||
/**
|
||||
* Before a user can update the deprecation status to deprecated, they must acknowledge that even if the
|
||||
* dataset is deprecated they must still keep it compliant.
|
||||
* @memberof DatasetDeprecation
|
||||
* @type {boolean}
|
||||
*/
|
||||
isDeprecationNoticeAcknowledged: boolean = false;
|
||||
|
||||
/**
|
||||
* Checks the working / aliased copies of the deprecation properties diverge from the
|
||||
* saved versions i.e. deprecationNoteAlias and deprecationAlias
|
||||
* @type {ComputedProperty<boolean>}
|
||||
* @memberof DatasetDeprecation
|
||||
*/
|
||||
@computed('deprecatedAlias', 'deprecated', 'deprecationNote', 'deprecationNoteAlias')
|
||||
get isDirty(): boolean {
|
||||
const { deprecatedAlias, deprecated, deprecationNote, deprecationNoteAlias } = this;
|
||||
|
||||
return deprecatedAlias !== deprecated || deprecationNoteAlias !== deprecationNote;
|
||||
}
|
||||
|
||||
/**
|
||||
* The external action to be completed when a save is initiated
|
||||
* @type {(isDeprecated: boolean, updateDeprecationNode: string, decommissionTime: Date | null) => Promise<void>}
|
||||
* @memberof DatasetDeprecation
|
||||
*/
|
||||
onUpdateDeprecation: (
|
||||
isDeprecated: boolean,
|
||||
updateDeprecationNode: string,
|
||||
decommissionTime: Date | null
|
||||
) => Promise<void> | void;
|
||||
|
||||
editorOptions = {
|
||||
...baseCommentEditorOptions,
|
||||
placeholder: {
|
||||
text: "You may provide a note about this dataset's deprecation status"
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggles the boolean value of deprecatedAlias
|
||||
*/
|
||||
@action
|
||||
toggleDeprecatedStatus(this: DatasetDeprecation) {
|
||||
this.toggleProperty('deprecatedAlias');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles updates to the decommissionTime attribute
|
||||
* @param {Date} decommissionTime date dataset should be decommissioned
|
||||
*/
|
||||
@action
|
||||
onDecommissionDateChange(this: DatasetDeprecation, decommissionTime: Date) {
|
||||
set(this, 'decommissionTime', new Date(decommissionTime).getTime());
|
||||
}
|
||||
|
||||
/**
|
||||
* When a user clicks the checkbox to acknowledge or cancel acknowledgement of the notice for
|
||||
* deprecating a dataset
|
||||
*/
|
||||
@action
|
||||
onAcknowledgeDeprecationNotice(this: DatasetDeprecation) {
|
||||
this.toggleProperty('isDeprecationNoticeAcknowledged');
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the save action with the updated values for
|
||||
* deprecated decommissionTime, and deprecationNote
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
@action
|
||||
async onSave(this: DatasetDeprecation) {
|
||||
const { deprecatedAlias, deprecationNoteAlias, decommissionTime } = this;
|
||||
const { onUpdateDeprecation } = this;
|
||||
|
||||
if (onUpdateDeprecation) {
|
||||
const noteValue = deprecatedAlias ? deprecationNoteAlias : '';
|
||||
const time = decommissionTime ? new Date(decommissionTime) : null;
|
||||
|
||||
await onUpdateDeprecation(!!deprecatedAlias, noteValue || '', time);
|
||||
set(this, 'deprecationNoteAlias', noteValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,11 +1,12 @@
|
||||
import Component from '@ember/component';
|
||||
import { get } from '@ember/object';
|
||||
import { Task, task } from 'ember-concurrency';
|
||||
import { task } from 'ember-concurrency';
|
||||
import { isLiUrn } from '@datahub/data-models/entity/dataset/utils/urn';
|
||||
import { DatasetOrigins } from 'wherehows-web/typings/api/datasets/origins';
|
||||
import { readDatasetOriginsByUrn } from 'wherehows-web/utils/api/datasets/origins';
|
||||
import { isArray } from '@ember/array';
|
||||
import { containerDataSource } from '@datahub/utils/api/data-source';
|
||||
import { ETaskPromise } from '@datahub/utils/types/concurrency';
|
||||
|
||||
@containerDataSource('getFabricsTask', ['urn'])
|
||||
export default class DatasetFabricsContainer extends Component {
|
||||
@ -26,7 +27,6 @@ export default class DatasetFabricsContainer extends Component {
|
||||
/**
|
||||
* Reads the fabrics available for the dataset with this urn and sets the value of
|
||||
* the related list of available Fabrics
|
||||
* @type {Task<Promise<DatasetOrigins>, (a?: any) => TaskInstance<Promise<DatasetOrigins>>>}
|
||||
*/
|
||||
@task(function*(this: DatasetFabricsContainer): IterableIterator<Promise<DatasetOrigins>> {
|
||||
if (isLiUrn(this.urn)) {
|
||||
@ -37,5 +37,5 @@ export default class DatasetFabricsContainer extends Component {
|
||||
}
|
||||
}
|
||||
})
|
||||
getFabricsTask!: Task<Promise<DatasetOrigins>, () => Promise<DatasetOrigins>>;
|
||||
getFabricsTask!: ETaskPromise<DatasetOrigins>;
|
||||
}
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import Component from '@ember/component';
|
||||
import { set } from '@ember/object';
|
||||
import { Task, task } from 'ember-concurrency';
|
||||
import { task } from 'ember-concurrency';
|
||||
import { readDownstreamDatasetsByUrn } from 'wherehows-web/utils/api/datasets/lineage';
|
||||
import { LineageList } from 'wherehows-web/typings/api/datasets/relationships';
|
||||
import { containerDataSource } from '@datahub/utils/api/data-source';
|
||||
import { ETaskPromise } from '@datahub/utils/types/concurrency';
|
||||
|
||||
@containerDataSource('getDatasetDownstreamsTask', ['urn'])
|
||||
export default class DatasetLineageDownstreamsContainer extends Component {
|
||||
@ -35,5 +36,5 @@ export default class DatasetLineageDownstreamsContainer extends Component {
|
||||
set(this, 'downstreams', downstreams);
|
||||
}
|
||||
})
|
||||
getDatasetDownstreamsTask!: Task<Promise<LineageList>, () => Promise<LineageList>>;
|
||||
getDatasetDownstreamsTask!: ETaskPromise<LineageList>;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user