Manual merge with more internal changes

This commit is contained in:
Ignacio Bona 2019-09-04 21:46:02 -07:00
parent a67bf284fa
commit e2f985a298
180 changed files with 1858 additions and 2110 deletions

View File

@ -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
};
/**

View File

@ -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/';

View File

@ -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);
}
}

View File

@ -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
}
}
});

View File

@ -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));
};

View File

@ -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",

View File

@ -1,2 +1 @@
const testemConf = require('../../configs/testem-base');
module.exports = testemConf;
module.exports = require('../../configs/testem-base');

View File

@ -28,6 +28,6 @@
"../../@datahub/metadata-types/addon/**/*",
"../../@datahub/metadata-types/types/**/*",
"../../@datahub/utils/addon/**/*",
"../../@datahub/utils/types/**/*",
"../../@datahub/utils/types/**/*"
]
}

View File

@ -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>;
}

View File

@ -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>;
};
}

View File

@ -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

View File

@ -1,2 +1 @@
const testemConf = require('../../configs/testem-base');
module.exports = testemConf;
module.exports = require('../../configs/testem-base');

View File

@ -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}}

View File

@ -71,7 +71,7 @@
}
}
&__note-editor {
&__note-editor .ember-medium-editor__container {
min-height: item-spacing(6) * 4;
outline: none;
padding: item-spacing(1);

View File

@ -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",

View File

@ -1,2 +1 @@
const testemConf = require('../../configs/testem-base');
module.exports = testemConf;
module.exports = require('../../configs/testem-base');

View File

@ -4,4 +4,5 @@
margin: 0;
font-size: 24px;
font-weight: 400;
margin-right: item-spacing(2);
}

View File

@ -11,5 +11,9 @@
flex-direction: column;
justify-content: space-between;
width: 100%;
.entity-pill {
margin-right: item-spacing(2);
}
}
}

View File

@ -1,2 +1 @@
const testemConf = require('../../configs/testem-base');
module.exports = testemConf;
module.exports = require('../../configs/testem-base');

View File

@ -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
*/

View File

@ -1,2 +1 @@
const testemConf = require('../../configs/testem-base');
module.exports = testemConf;
module.exports = require('../../configs/testem-base');

View File

@ -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

View File

@ -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

View File

@ -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);
}
/**

View File

@ -1,2 +1 @@
const testemConf = require('../../configs/testem-base');
module.exports = testemConf;
module.exports = require('../../configs/testem-base');

View File

@ -1,2 +1 @@
const testemConf = require('../../configs/testem-base');
module.exports = testemConf;
module.exports = require('../../configs/testem-base');

View File

@ -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/*"]

View File

@ -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';
}

View File

@ -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 {
/**

View File

@ -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}}

View File

@ -0,0 +1 @@
export { default } from '@datahub/shared/components/entity-pill';

View File

@ -0,0 +1 @@
@import 'entity-pill/all';

View File

@ -0,0 +1 @@
@import 'entity-pill';

View File

@ -0,0 +1,9 @@
.entity-pill {
cursor: auto;
&__tier {
&--generic {
color: get-color(black);
background: get-color(slate2, 0.45);
}
}
}

View File

@ -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",

View File

@ -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');
});
});

View 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>;
}

View 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;
};
};
}

View File

@ -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();
}
}

View File

@ -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>;

View File

@ -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 }
});

View File

@ -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}

View File

@ -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

View File

@ -0,0 +1,5 @@
{{yield
(hash
trackOnAction=(action this.trackOnAction)
)
}}

View File

@ -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

View 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;
};

View File

@ -0,0 +1 @@
export { default } from '@datahub/tracking/components/track-ui-event';

View File

@ -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']
}
};
};

View File

@ -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": {

View File

@ -1,2 +1 @@
const testemConf = require('../../configs/testem-base');
module.exports = testemConf;
module.exports = require('../../configs/testem-base');

View File

@ -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"
);
});
});

View File

@ -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 {}
}

View File

@ -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));

View File

@ -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: ''

View File

@ -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'));

View File

@ -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/**/*"
]
}

View File

@ -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.

View 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;
}

View 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 };
}

View File

@ -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
}

View File

@ -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;
}
}

View 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;
}

View File

@ -0,0 +1 @@
@import 'user/all';

View File

@ -0,0 +1 @@
@import 'profile/all';

View File

@ -0,0 +1,3 @@
@import 'header';
@import 'focus-area';
@import 'content';

View File

@ -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;
}
}
}
}
}
}
}
}
}
}

View File

@ -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;
}
}

View 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);
}
}
}

View File

@ -0,0 +1 @@
export { default } from '@datahub/user/templates/user/profile';

View File

@ -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"

View File

@ -1,2 +1 @@
const testemConf = require('../../configs/testem-base');
module.exports = testemConf;
module.exports = require('../../configs/testem-base');

View File

@ -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;

View 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;

View File

@ -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;

View File

@ -0,0 +1,4 @@
@import 'datahub-utils';
@import 'nacho-core';
@import 'nacho-table';
@import 'nacho-pill';

View 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);
});
});

View File

@ -35,6 +35,6 @@
"../../@datahub/shared/addon/**/*",
"../../@datahub/shared/types/**/*",
"../../@datahub/utils/addon/**/*",
"../../@datahub/utils/types/**/*",
"../../@datahub/utils/types/**/*"
]
}

View File

@ -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 {

View File

@ -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

View 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;
}

View 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;
};

View File

@ -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,

View File

@ -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;
};

View 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
}

View File

@ -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';

View File

@ -1,2 +1 @@
const testemConf = require('../../configs/testem-base');
module.exports = testemConf;
module.exports = require('../../configs/testem-base');

View File

@ -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>;
}

View 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>>;

View File

@ -1,2 +1 @@
const testemConf = require('../../configs/testem-base');
module.exports = testemConf;
module.exports = require('../../configs/testem-base');

View File

@ -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'

View File

@ -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>;
}

View File

@ -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
*

View File

@ -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>;
}

View File

@ -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}

View File

@ -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);
}
}
}

View File

@ -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>;
}

View File

@ -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