META-9929, META-9868, resolves related ticket issues and misc tasks

This commit is contained in:
Seyi Adebajo 2019-09-18 10:07:04 -07:00
parent 162d52a421
commit f66f81dc4c
41 changed files with 261 additions and 182 deletions

View File

@ -1,10 +1,20 @@
import { getJSON, cacheApi } from '@datahub/utils/api/fetcher'; import { getJSON, cacheApi } from '@datahub/utils/api/fetcher';
import { ApiVersion, getApiRoot } from '@datahub/utils/api/shared'; import { ApiVersion, getApiRoot } from '@datahub/utils/api/shared';
import buildUrl from '@datahub/utils/api/build-url'; import buildUrl from '@datahub/utils/api/build-url';
import { IBrowseParams, IBrowseResponse } from '@datahub/data-models/types/entity/browse'; import { IBrowseParams, IBrowseResponse, IBrowsePathParams } from '@datahub/data-models/types/entity/browse';
/**
* URL for browse
* @param version api version
*/
export const browseUrlRoot = (version: ApiVersion): string => `${getApiRoot(version)}/browse`; export const browseUrlRoot = (version: ApiVersion): string => `${getApiRoot(version)}/browse`;
/**
* URL for browse paths
* @param version api version
*/
export const browsePathUrlRoot = (version: ApiVersion): string => `${getApiRoot(version)}/browsePaths`;
/** /**
* Will return the string url for the browse api * Will return the string url for the browse api
* @param params GET paramaters required for this API call, see IBrowseParams type * @param params GET paramaters required for this API call, see IBrowseParams type
@ -14,6 +24,14 @@ const browseUrl = <T>(params: IBrowseParams<T>): string => {
return buildUrl(`${urlRoot}`, params); return buildUrl(`${urlRoot}`, params);
}; };
/**
* Build GET request for browse
* @param params entity type and URN
*/
const browsePathUrl = (params: IBrowsePathParams): string => {
const urlRoot = browsePathUrlRoot(ApiVersion.v2);
return buildUrl(`${urlRoot}`, params);
};
/** /**
* Will fetch browse information for a specific path and entity. * Will fetch browse information for a specific path and entity.
* Will return a paginated return of elements and a fixed set of groups/folders. * Will return a paginated return of elements and a fixed set of groups/folders.
@ -25,3 +43,13 @@ export const readBrowse = cacheApi(
return getJSON<IBrowseResponse>({ url }); return getJSON<IBrowseResponse>({ url });
} }
); );
/**
* Read the path to reach to the specified entity
*/
export const readBrowsePath = cacheApi(
(params: IBrowsePathParams): Promise<Array<string>> => {
const url = browsePathUrl(params);
return getJSON<Array<string>>({ url });
}
);

View File

@ -5,7 +5,7 @@ import { PersonEntity } from '@datahub/data-models/entity/person/person-entity';
* Defines the interface for the DataModelEntity enum below. * Defines the interface for the DataModelEntity enum below.
* This allows each entry in the enum to be indexable * This allows each entry in the enum to be indexable
*/ */
interface IDataModelEntity { export interface IDataModelEntity {
[DatasetEntity.displayName]: typeof DatasetEntity; [DatasetEntity.displayName]: typeof DatasetEntity;
[PersonEntity.displayName]: typeof PersonEntity; [PersonEntity.displayName]: typeof PersonEntity;
} }

View File

@ -1,7 +0,0 @@
/**
* 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

@ -13,7 +13,7 @@ import {
IEntityLinkAttrsWithCount IEntityLinkAttrsWithCount
} from '@datahub/data-models/types/entity/shared'; } from '@datahub/data-models/types/entity/shared';
import { NotImplementedError } from '@datahub/data-models/constants/entity/shared/index'; import { NotImplementedError } from '@datahub/data-models/constants/entity/shared/index';
import { readBrowse } from '@datahub/data-models/api/browse'; import { readBrowse, readBrowsePath } from '@datahub/data-models/api/browse';
import { getFacetDefaultValueForEntity } from '@datahub/data-models/entity/utils/facets'; import { getFacetDefaultValueForEntity } from '@datahub/data-models/entity/utils/facets';
import { InstitutionalMemory } from '@datahub/data-models/models/aspects/institutional-memory'; import { InstitutionalMemory } from '@datahub/data-models/models/aspects/institutional-memory';
@ -197,6 +197,31 @@ export abstract class BaseEntity<T extends IBaseEntity> {
throw new Error(NotImplementedError); throw new Error(NotImplementedError);
} }
/**
* Workaround to get the current static instance
* This makes sense if you want to get a static property only
* implemented in a subclass, therefore, the same static
* class is needed
*/
get staticInstance(): IBaseEntityStatics<T> {
return (this.constructor as unknown) as IBaseEntityStatics<T>;
}
/**
* Will read the current path for an entity
*/
get readPath(): Promise<Array<string>> {
const entityName = this.staticInstance.renderProps.search.apiName;
return readBrowsePath({
type: entityName,
urn: this.urn
}).then(
(paths): Array<string> => {
return paths && paths.length > 0 ? paths[0].split('/').filter(Boolean) : [];
}
);
}
/** /**
* Asynchronously resolves with an instance of T * Asynchronously resolves with an instance of T
* @readonly * @readonly
@ -219,15 +244,6 @@ export abstract class BaseEntity<T extends IBaseEntity> {
throw new Error(NotImplementedError); throw new Error(NotImplementedError);
} }
/**
*
* @readonly
* @type {Array<string>}
* @memberof BaseEntity
*/
get hierarchySegments(): Array<string> {
throw new Error(NotImplementedError);
}
/** /**
* Class properties common across instances * Class properties common across instances
* Dictates how visual ui components should be rendered * Dictates how visual ui components should be rendered

View File

@ -10,9 +10,6 @@ import { oneWay } from '@ember/object/computed';
import { DatasetPlatform } from '@datahub/metadata-types/constants/entity/dataset/platform'; import { DatasetPlatform } from '@datahub/metadata-types/constants/entity/dataset/platform';
import { decodeUrn } from '@datahub/utils/validators/urn'; import { decodeUrn } from '@datahub/utils/validators/urn';
import { readCategories } from '@datahub/data-models/entity/dataset/read-categories'; import { readCategories } from '@datahub/data-models/entity/dataset/read-categories';
import { computed } from '@ember/object';
import { IDataPlatform } from '@datahub/metadata-types/types/entity/dataset/platform';
import { readDataPlatforms } from '@datahub/data-models/api/dataset/platforms';
import { getPrefix } from '@datahub/data-models/entity/dataset/utils/segments'; import { getPrefix } from '@datahub/data-models/entity/dataset/utils/segments';
import { readDatasetsCount } from '@datahub/data-models/api/dataset/count'; import { readDatasetsCount } from '@datahub/data-models/api/dataset/count';
import { setProperties } from '@ember/object'; import { setProperties } from '@ember/object';
@ -71,6 +68,17 @@ export class DatasetEntity extends BaseEntity<IDatasetApiView> {
return decodeUrn(this.urn); return decodeUrn(this.urn);
} }
/**
* Creates a link for this specific entity instance, useful for generating a dynamic link from a
* single particular dataset entity
*/
get linkForEntity(): IEntityLinkAttrs {
return DatasetEntity.getLinkForEntity({
entityUrn: this.urn,
displayName: this.name
});
}
/** /**
* Reads the snapshots for a list of Dataset urns * Reads the snapshots for a list of Dataset urns
* @static * @static
@ -109,15 +117,10 @@ export class DatasetEntity extends BaseEntity<IDatasetApiView> {
/** /**
* Reference to the data entity's native name, should not be something that is editable but gives us a * Reference to the data entity's native name, should not be something that is editable but gives us a
* more human readable form for the dataset vs the urn * more human readable form for the dataset vs the urn
* @type {string}
*/ */
@oneWay('entity.nativeName') get name(): string {
name!: string; return this.entity ? this.entity.nativeName : '';
}
/**
* Reference to the constructed data platform once it is fetched from api
*/
currentDataPlatform?: IDataPlatform;
/** /**
* Retrieves the value of the Dataset entity identified by this.urn * Retrieves the value of the Dataset entity identified by this.urn
@ -204,26 +207,6 @@ export class DatasetEntity extends BaseEntity<IDatasetApiView> {
)); ));
} }
/**
* Produces an array of strings depicting the hierarchy of a Feature Entity
* @readonly
* @type {Array<string>}
* @memberof FeatureEntity
*/
@computed('entity')
get hierarchySegments(): Array<string> {
const { entity } = this;
const { currentDataPlatform } = this;
const separator = (currentDataPlatform && currentDataPlatform.datasetNameDelimiter) || '.';
if (entity) {
const { platform, nativeName } = entity;
return [platform, ...nativeName.split(separator).filter(Boolean)];
}
return [];
}
/** /**
* Interim implementation to read categories for datasets * Interim implementation to read categories for datasets
* TODO META-8863 * TODO META-8863
@ -287,14 +270,10 @@ export class DatasetEntity extends BaseEntity<IDatasetApiView> {
export const createDatasetEntity = async (urn: string, fetchedEntity?: IDatasetApiView): Promise<DatasetEntity> => { export const createDatasetEntity = async (urn: string, fetchedEntity?: IDatasetApiView): Promise<DatasetEntity> => {
const dataset = new DatasetEntity(urn); const dataset = new DatasetEntity(urn);
const entity = fetchedEntity || (await dataset.readEntity); const entity = fetchedEntity || (await dataset.readEntity);
// TODO: [META-8652] Find way to separate this from the createDatasetEntity as the concern of creating a dataset
// entity should not be related to the concern of reading data platforms
const dataPlatforms: Array<IDataPlatform> = await readDataPlatforms();
const currentDataPlatform = dataPlatforms.find((platform): boolean => platform.name === entity.platform);
setProperties(dataset, { setProperties(dataset, {
entity, entity
currentDataPlatform
}); });
return dataset; return dataset;
}; };

View File

@ -6,7 +6,6 @@ import { getTabPropertiesFor } from '@datahub/data-models/entity/utils';
/** /**
* Class properties common across instances * Class properties common across instances
* Dictates how visual ui components should be rendered * Dictates how visual ui components should be rendered
* Implemented as a getter to ensure that reads are idempotent
* @readonly * @readonly
* @static * @static
* @type {IEntityRenderProps} * @type {IEntityRenderProps}

View File

@ -1,6 +1,5 @@
import getActorFromUrn from '@datahub/data-models/utils/get-actor-from-urn'; import getActorFromUrn from '@datahub/data-models/utils/get-actor-from-urn';
import { computed } from '@ember/object'; 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 { NotImplementedError } from '@datahub/data-models/constants/entity/shared';
import { import {
getRenderProps, getRenderProps,
@ -51,7 +50,7 @@ export class PersonEntity extends BaseEntity<IBaseEntity> {
* TODO: [META-9698] Migrate to using LiPersonEntity * TODO: [META-9698] Migrate to using LiPersonEntity
*/ */
static profileLinkFromUsername(username: string): string { static profileLinkFromUsername(username: string): string {
return `${profileLinkBase}${username}`; return `${username}`;
} }
/** /**

View File

@ -26,3 +26,11 @@ export interface IBrowseResponse {
groups: Array<{ name: string; count: number }>; groups: Array<{ name: string; count: number }>;
}; };
} }
/**
* Path for bowse paths
*/
export interface IBrowsePathParams {
type: string;
urn: string;
}

View File

@ -107,4 +107,4 @@
</form> </form>
{{yield}} {{yield}}

View File

@ -0,0 +1,47 @@
import Component from '@ember/component';
// @ts-ignore: Ignore import of compiled template
import template from '../templates/components/wait-promise-container';
import { containerDataSource } from '@datahub/utils/api/data-source';
import { ETaskPromise } from '@datahub/utils/types/concurrency';
import { task } from 'ember-concurrency';
import { set } from '@ember/object';
import { layout } from '@ember-decorators/component';
/**
* WaitPromiseContainer will show spinner while the promise passed is being resolved
*
* usage:
*
* const promise = Promise.resolve(3);
*
* <WaitPromiseContainer @promise={{promise}} as |resolved|>
* {{resolved}}
* </WaitPromiseContainer>
*
* should show '3' as the promise resolved '3'
*/
@layout(template)
@containerDataSource('getContainerDataTask', ['promise'])
export default class WaitPromiseContainer<T> extends Component {
/**
* Input promise that we want to wait
*/
promise?: Promise<T>;
/**
* Value for the resolved promise that will be yielded
*/
resolved?: T;
/**
* Will fetch and transform api data into status table input format
*/
@task(function*(this: WaitPromiseContainer<T>): IterableIterator<Promise<T>> {
const { promise } = this;
if (promise) {
const resolved: T = yield promise;
set(this, 'resolved', resolved);
}
})
getContainerDataTask!: ETaskPromise<T>;
}

View File

@ -0,0 +1,3 @@
<ConcurrencyTaskStateHandler @task={{getContainerDataTask}}>
{{yield this.resolved}}
</ConcurrencyTaskStateHandler>

View File

@ -0,0 +1 @@
export { default } from '@datahub/shared/components/wait-promise-container';

View File

@ -47,6 +47,7 @@
"ember-cli-sri": "^2.1.1", "ember-cli-sri": "^2.1.1",
"ember-cli-typescript-blueprints": "^2.0.0", "ember-cli-typescript-blueprints": "^2.0.0",
"ember-cli-uglify": "^2.1.0", "ember-cli-uglify": "^2.1.0",
"ember-concurrency": "^1.0.0",
"ember-export-application-global": "^2.0.0", "ember-export-application-global": "^2.0.0",
"ember-load-initializers": "^2.0.0", "ember-load-initializers": "^2.0.0",
"ember-maybe-import-regenerator": "^0.1.6", "ember-maybe-import-regenerator": "^0.1.6",

View File

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

View File

@ -0,0 +1,31 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render, settled } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
module('Integration | Component | wait-promise-container', function(hooks): void {
setupRenderingTest(hooks);
test('it renders', async function(assert): Promise<void> {
let testResolve: (value?: unknown) => void = (): void => {};
this.set(
'promise',
new Promise((resolve): void => {
testResolve = resolve;
})
);
await render(hbs`
<WaitPromiseContainer @promise={{promise}} as |resolved|>
{{resolved}}
</WaitPromiseContainer>
`);
assert.equal(this.element.textContent && this.element.textContent.trim(), '');
testResolve('test');
// we need to wait to the rerender to happen as setting a property will trigger an ember loop
await settled();
assert.equal(this.element.textContent && this.element.textContent.trim(), 'test');
});
});

View File

@ -1,6 +1,6 @@
import { ITrackingConfig } from '@datahub/shared/types/configurator/tracking'; import { ITrackingConfig } from '@datahub/shared/types/configurator/tracking';
import Service from '@ember/service'; import Service from '@ember/service';
import { ApiStatus } from '@datahub/utils/addon/api/shared'; import { ApiStatus } from '@datahub/utils/api/shared';
/** /**
* Describes the interface for the configuration endpoint response object. * Describes the interface for the configuration endpoint response object.

View File

@ -19,6 +19,7 @@ export function populateMockPersonEntity(
entity.linkedinProfile = 'https://www.linkedin.com/in/ash-ketchum-b212502a/'; entity.linkedinProfile = 'https://www.linkedin.com/in/ash-ketchum-b212502a/';
entity.slackLink = 'aketchum'; entity.slackLink = 'aketchum';
entity.skills = ['training', 'catching', 'battling']; entity.skills = ['training', 'catching', 'battling'];
const managerEntity = new personEntity(personEntity.urnFromUsername('pikachu')); const managerEntity = new personEntity(personEntity.urnFromUsername('pikachu'));
managerEntity.name = 'Pikachu'; managerEntity.name = 'Pikachu';
managerEntity.profilePictureUrl = 'https://pokemonletsgo.pokemon.com/assets/img/common/char-pikachu.png'; managerEntity.profilePictureUrl = 'https://pokemonletsgo.pokemon.com/assets/img/common/char-pikachu.png';

View File

@ -1,12 +1,12 @@
import Component from '@ember/component'; import Component from '@ember/component';
// @ts-ignore: Ignore import of compiled template // @ts-ignore: Ignore import of compiled template
import template from '../templates/components/concurrency-task-state-handler'; import template from '../templates/components/concurrency-task-state-handler';
import { layout, classNames } from '@ember-decorators/component'; import { layout, tagName } from '@ember-decorators/component';
import { action } from '@ember/object'; import { action } from '@ember/object';
import { Task, task } from 'ember-concurrency'; import { Task, task } from 'ember-concurrency';
@layout(template) @layout(template)
@classNames('concurrency-task-state-handler') @tagName('')
export default class ConcurrencyTaskStateHandler<T> extends Component { export default class ConcurrencyTaskStateHandler<T> extends Component {
/** /**
* List of arguments to be passed to the task when performed. The array is spread into the task invocation * List of arguments to be passed to the task when performed. The array is spread into the task invocation

View File

@ -30,7 +30,9 @@
{{else}} {{else}}
{{#if @task.isRunning}} {{#if @task.isRunning}}
{{pendulum-ellipsis-animation}} <div class="concurrency-task-state-handler">
{{pendulum-ellipsis-animation}}
</div>
{{else}} {{else}}
{{yield}} {{yield}}
{{/if}} {{/if}}

View File

@ -2,6 +2,6 @@ export interface IDynamicLinkNode<T, Z = string, P extends {} = {}> {
title: string; title: string;
text: string; text: string;
route: Z; route: Z;
model: T; model?: T;
queryParams?: P; queryParams?: P;
} }

View File

@ -1,5 +1,5 @@
import Component from '@ember/component'; import Component from '@ember/component';
import { IDynamicLinkNode } from 'wherehows-web/typings/app/browse/dynamic-link'; import { IDynamicLinkNode } from '@datahub/utils/types/vendor/dynamic-link';
/** /**
* Card component. It is used in the home page of the app to * Card component. It is used in the home page of the app to

View File

@ -2,7 +2,7 @@ import Component from '@ember/component';
import { tagName } from '@ember-decorators/component'; import { tagName } from '@ember-decorators/component';
import { computed } from '@ember/object'; import { computed } from '@ember/object';
import { DataModelEntity } from '@datahub/data-models/constants/entity'; import { DataModelEntity } from '@datahub/data-models/constants/entity';
import { IDynamicLinkNode } from 'wherehows-web/typings/app/browse/dynamic-link'; import { IDynamicLinkNode } from '@datahub/utils/types/vendor/dynamic-link';
/** /**
* Indicates the total number of entities within a category and on user interaction * Indicates the total number of entities within a category and on user interaction

View File

@ -296,7 +296,7 @@ export default class DatasetAuthors extends Component {
@action @action
confirmSuggestedOwner(this: DatasetAuthors, owner: IOwner): Array<IOwner> | void { confirmSuggestedOwner(this: DatasetAuthors, owner: IOwner): Array<IOwner> | void {
const suggestedOwner = { ...owner, source: OwnerSource.Ui }; const suggestedOwner = { ...owner, source: OwnerSource.Ui };
return this.actions.addOwner.call(this, suggestedOwner); return this.addOwner.call(this, suggestedOwner);
} }
/** /**

View File

@ -200,14 +200,30 @@ export default class DatasetMainContainer extends Component {
// TODO should fetch the dataset itself, but right now dataset is fetched at route level // TODO should fetch the dataset itself, but right now dataset is fetched at route level
const { dataset } = this; const { dataset } = this;
//TODO: META-8267 Container notifications decorator
if (dataset) { if (dataset) {
const entity: DatasetEntity = yield createDatasetEntity(this.dataset.uri, this.dataset as IDatasetApiView); const entity: DatasetEntity = yield createDatasetEntity(this.dataset.uri, this.dataset as IDatasetApiView);
set(this, 'entity', entity); set(this, 'entity', entity);
} }
}).restartable()) }).restartable())
reifyEntityTask!: ETaskPromise<DatasetEntity>; reifyEntityTask!: ETaskPromise<DatasetEntity>;
/**
* This will return the paths for an entity. We should be able to consume entity.readPath
* direcly but since datasets are not migrated we need to flag guard it.
*
* TODO META-8863 Interim implementation to read categories for datasets
*/
@computed('entity')
get paths(): Promise<Array<string>> {
const { entity } = this;
if (!entity) {
return Promise.resolve([]);
}
return entity.readPath;
}
/** /**
* Converts the uri on a model to a usable URN format * Converts the uri on a model to a usable URN format
* @type {ComputedProperty<string>} * @type {ComputedProperty<string>}

View File

@ -1,7 +1,7 @@
import Component from '@ember/component'; import Component from '@ember/component';
import { computed } from '@ember/object'; import { computed } from '@ember/object';
import { tagName } from '@ember-decorators/component'; import { tagName } from '@ember-decorators/component';
import { IDynamicLinkNode } from 'wherehows-web/typings/app/browse/dynamic-link'; import { IDynamicLinkNode } from '@datahub/utils/types/vendor/dynamic-link';
/** /**
* Query params with pages for routes * Query params with pages for routes

View File

@ -4,6 +4,7 @@ import SearchService from 'wherehows-web/services/search';
import { alias } from '@ember/object/computed'; import { alias } from '@ember/object/computed';
import { grammarProcessingSteps, typeaheadQueryProcessor } from 'wherehows-web/utils/parsers/autocomplete'; import { grammarProcessingSteps, typeaheadQueryProcessor } from 'wherehows-web/utils/parsers/autocomplete';
import { ISuggestionGroup } from 'wherehows-web/utils/parsers/autocomplete/types'; import { ISuggestionGroup } from 'wherehows-web/utils/parsers/autocomplete/types';
import { DatasetEntity } from '@datahub/data-models/entity/dataset/dataset-entity';
import { tagName } from '@ember-decorators/component'; import { tagName } from '@ember-decorators/component';
import { computed } from '@ember/object'; import { computed } from '@ember/object';
import { DataModelEntity, DataModelName } from '@datahub/data-models/constants/entity'; import { DataModelEntity, DataModelName } from '@datahub/data-models/constants/entity';
@ -68,7 +69,7 @@ export default class SearchBoxContainer extends Component {
* @param {Entity} [entity] * @param {Entity} [entity]
* @memberof SearchBoxContainer * @memberof SearchBoxContainer
*/ */
onSearch(text: string = '', entity: DataModelEntity['displayName'] = 'datasets'): void { onSearch(text: string = '', entity: DataModelName = DatasetEntity.displayName): void {
// entity (dropdown value) might be different than this.entity (Page that you are in) // entity (dropdown value) might be different than this.entity (Page that you are in)
const dataModelEntity = DataModelEntity[entity]; const dataModelEntity = DataModelEntity[entity];
const { attributes } = dataModelEntity.renderProps.search; const { attributes } = dataModelEntity.renderProps.search;

View File

@ -17,13 +17,18 @@ const fallback = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAA
const makeAvatar = ({ aviUrlPrimary, aviUrlFallback = fallback }: IAppConfig['userEntityProps']): AvatarCreatorFunc => ( const makeAvatar = ({ aviUrlPrimary, aviUrlFallback = fallback }: IAppConfig['userEntityProps']): AvatarCreatorFunc => (
object: Partial<IAvatar> object: Partial<IAvatar>
): IAvatar => { ): IAvatar => {
const props = pick(object, ['email', 'userName', 'name', 'imageUrl']); const props = pick(object, ['email', 'userName', 'name', 'imageUrl', 'pictureLink']);
const { userName } = props; const { userName, pictureLink } = props;
const imageFallback = aviUrlFallback || fallback; const imageUrlFallback = aviUrlFallback || fallback;
const imageUrl = pictureLink
? pictureLink
: userName && aviUrlPrimary
? aviUrlPrimary.replace('[username]', userName)
: imageUrlFallback;
return { return {
imageUrl: userName && aviUrlPrimary ? aviUrlPrimary.replace('[username]', userName) : imageFallback, imageUrlFallback,
imageUrlFallback: imageFallback, imageUrl,
...props ...props
}; };
}; };

View File

@ -5,7 +5,7 @@ import Search from 'wherehows-web/services/search';
import { set } from '@ember/object'; import { set } from '@ember/object';
import { DataModelEntity } from '@datahub/data-models/constants/entity'; import { DataModelEntity } from '@datahub/data-models/constants/entity';
import { unGuardedEntities } from 'wherehows-web/utils/entity/flag-guard'; import { unGuardedEntities } from 'wherehows-web/utils/entity/flag-guard';
import { IDynamicLinkNode } from 'wherehows-web/typings/app/browse/dynamic-link'; import { IDynamicLinkNode } from '@datahub/utils/types/vendor/dynamic-link';
import { capitalize } from '@ember/string'; import { capitalize } from '@ember/string';
import { getConfig } from 'wherehows-web/services/configurator'; import { getConfig } from 'wherehows-web/services/configurator';

View File

@ -115,6 +115,7 @@ $nacho-breadcrumbs-arrow-padding: 20px;
top: $application-navbar-static-height; top: $application-navbar-static-height;
z-index: z('nav') + z('below'); // To give precendence to tooltip and dropdown z-index: z('nav') + z('below'); // To give precendence to tooltip and dropdown
width: 100%; width: 100%;
min-height: $nacho-breadcrumbs-height;
&--with-banner-offset { &--with-banner-offset {
top: $banner-alerts-height + $application-navbar-static-height; top: $banner-alerts-height + $application-navbar-static-height;

View File

@ -2,11 +2,11 @@
<section class="browse-category-container {{if (eq model.segments.length 0) "browse-category-container--no-breadcrumbs"}}"> <section class="browse-category-container {{if (eq model.segments.length 0) "browse-category-container--no-breadcrumbs"}}">
{{!-- Dont show breadcrumbs first level since we already have the title and there is real action --}} {{!-- Dont show breadcrumbs first level since we already have the title and there is real action --}}
{{#if (gt model.segments.length 0)}} {{#if (gt model.segments.length 0)}}
{{browser/entity-breadcrumbs <Browser::EntityBreadcrumbs
class=(with-banner-offset "nacho-breadcrumbs-container") class={{with-banner-offset "nacho-breadcrumbs-container"}}
entity=(lowercase model.displayName) @entity={{lowercase model.displayName}}
segments=model.segments @segments={{model.segments}}
}} />
{{/if}} {{/if}}
<div class="container"> <div class="container">

View File

@ -69,17 +69,6 @@
</table> </table>
</section> </section>
<section class="dataset-author">
{{datasets/owners/suggested-owners
owners=systemGeneratedOwnersWithAvatars
ownerTypes=ownerTypes
commonOwners=commonOwners
removeOwner=(action "removeOwner")
confirmSuggestedOwner=(action "confirmSuggestedOwner")
updateOwnerType=(action "updateOwnerType")
}}
</section>
<section class="action-bar"> <section class="action-bar">
<div class="container action-bar__content"> <div class="container action-bar__content">
<button <button
@ -113,4 +102,4 @@
</div> </div>
{{/if}} {{/if}}
</div> </div>
</section> </section>

View File

@ -21,6 +21,7 @@
notifications=notificationsMap notifications=notificationsMap
headerComponent=headerComponent headerComponent=headerComponent
fields=fields fields=fields
paths=this.paths
tabSelectionChanged=(action tabSelectionChanged) tabSelectionChanged=(action tabSelectionChanged)
setOwnershipRuleChange=(action setOwnershipRuleChange) setOwnershipRuleChange=(action setOwnershipRuleChange)
) )

View File

@ -48,22 +48,6 @@
{{/if}} {{/if}}
</div> </div>
<div class="dataset-header-meta">
{{#datasets/containers/dataset-fabrics
class="dataset-fabric-row-container"
urn=model.uri
as |fabricsContainer|
}}
<div class="dataset-fabric-entity-container">
<strong>Fabric:</strong>
{{datasets/dataset-fabric-switcher
urn=fabricsContainer.urn
fabrics=fabricsContainer.fabrics
}}
</div>
{{/datasets/containers/dataset-fabrics}}
</div>
{{datasets/containers/dataset-owner-list {{datasets/containers/dataset-owner-list
urn=encodedUrn urn=encodedUrn
avatarEntityProps=avatarEntityProps avatarEntityProps=avatarEntityProps

View File

@ -8,11 +8,16 @@ as |container|
as |snapshot| as |snapshot|
}} }}
<div id="dataset" class="entity-header"> <div id="dataset" class="entity-header">
{{browser/entity-breadcrumbs <WaitPromiseContainer
class=(with-banner-offset "nacho-breadcrumbs-container") class={{with-banner-offset "nacho-breadcrumbs-container"}}
entity=(or (lowercase container.entity.displayName) "--") @promise={{container.paths}} as |paths|>
segments=container.entity.hierarchySegments <Browser::EntityBreadcrumbs
}} @tagName=""
@entity={{or (lowercase container.entity.displayName) "--"}}
@segments={{paths}}
/>
</WaitPromiseContainer>
<div class="container"> <div class="container">
{{component {{component
container.headerComponent container.headerComponent

View File

@ -9,6 +9,9 @@ export interface IAvatar {
imageUrl: string; imageUrl: string;
// Fallback url for avatar image on error // Fallback url for avatar image on error
imageUrlFallback: string; imageUrlFallback: string;
// Alternate url for avatar image
pictureLink?: string;
// Email address for the associated entity if available
email?: null | string; email?: null | string;
// Handle for the avatar // Handle for the avatar
userName?: string; userName?: string;

View File

@ -23,6 +23,7 @@ import { browse } from 'wherehows-web/mirage/helpers/browse';
import searchResponse from 'wherehows-web/mirage/fixtures/search-response'; import searchResponse from 'wherehows-web/mirage/fixtures/search-response';
import { getSamplePageViewResponse } from 'wherehows-web/mirage/helpers/search/pageview-response'; import { getSamplePageViewResponse } from 'wherehows-web/mirage/helpers/search/pageview-response';
import { getEntitySearchResults } from 'wherehows-web/mirage/helpers/search/entity'; import { getEntitySearchResults } from 'wherehows-web/mirage/helpers/search/entity';
import { browsePaths } from 'wherehows-web/mirage/helpers/browse-paths';
export default function(this: IMirageServer): void { export default function(this: IMirageServer): void {
this.get('/config', getConfig); this.get('/config', getConfig);
@ -37,6 +38,8 @@ export default function(this: IMirageServer): void {
this.get('/browse', browse); this.get('/browse', browse);
this.get('/browsePaths', browsePaths);
this.get('/datasets/:identifier/', getDatasetView); this.get('/datasets/:identifier/', getDatasetView);
this.get('/datasets/:identifier/owners', getDatasetOwners); this.get('/datasets/:identifier/owners', getDatasetOwners);

View File

@ -47,8 +47,9 @@ export default Factory.extend({
userEntityProps() { userEntityProps() {
return { return {
aviUrlPrimary: aviUrlPrimary:
'https://cinco.corp.linkedin.com/api/profile/[username]/picture?access_token=2rzmbzEMGlHsszQktFY-B1TxUic', 'https://raw.githubusercontent.com/linkedin/WhereHows/datahub/datahub-web/packages/data-portal/public/assets/images/default_avatar.png',
aviUrlFallback: 'https://static.licdn-ei.com/sc/h/djzv59yelk5urv2ujlazfyvrk' aviUrlFallback:
'https://raw.githubusercontent.com/linkedin/WhereHows/datahub/datahub-web/packages/data-portal/public/assets/images/default_avatar.png'
}; };
} }
}); });

View File

@ -0,0 +1,11 @@
import { getDatasetUrnParts } from '@datahub/data-models/entity/dataset/utils/urn';
import { FabricType } from '@datahub/metadata-types/constants/common/fabric-type';
export const browsePaths = (_schema: any, request: any): Array<string> => {
const {
queryParams: { urn }
} = request;
const { platform, prefix = '', fabric = FabricType.PROD } = getDatasetUrnParts(urn);
return [`${fabric}/${platform}/${prefix}`];
};

View File

@ -11,14 +11,15 @@ module('Acceptance | breadcrumbs-smoke-test', function(hooks) {
test('Breadcrumbs Smoke Test', async function(this: IMirageTestContext, assert) { test('Breadcrumbs Smoke Test', async function(this: IMirageTestContext, assert) {
const categoryLinkClass = '.browse-category__link'; const categoryLinkClass = '.browse-category__link';
const breadcrumbsClass = '.nacho-breadcrumbs__crumb'; const breadCrumbClass = '.nacho-breadcrumbs';
const breadCrumbsClass = `${breadCrumbClass}__crumb`;
defaultScenario(this.server); defaultScenario(this.server);
await appLogin(); await appLogin();
await visit('/'); await visit('/');
await visit('/browse/datasets?path=hdfs'); await visit('/browse/datasets?path=hdfs');
await waitFor(categoryLinkClass); await waitFor(categoryLinkClass);
let categoryLinks = findAll(categoryLinkClass); let categoryLinks = findAll(categoryLinkClass);
let breadcrumbs = findAll(breadcrumbsClass); let breadcrumbs = findAll(breadCrumbsClass);
assert.equal(currentURL(), '/browse/datasets?path=hdfs'); assert.equal(currentURL(), '/browse/datasets?path=hdfs');
assert.equal(categoryLinks.length, 1, 'There is 1 folder in root hdfs'); assert.equal(categoryLinks.length, 1, 'There is 1 folder in root hdfs');
@ -30,7 +31,7 @@ module('Acceptance | breadcrumbs-smoke-test', function(hooks) {
await click(`${categoryLinkClass}:first-child`); await click(`${categoryLinkClass}:first-child`);
await waitFor(categoryLinkClass); await waitFor(categoryLinkClass);
categoryLinks = findAll(categoryLinkClass); categoryLinks = findAll(categoryLinkClass);
breadcrumbs = findAll(breadcrumbsClass); breadcrumbs = findAll(breadCrumbsClass);
assert.equal(currentURL(), '/browse/datasets?path=hdfs%2Fsome'); assert.equal(currentURL(), '/browse/datasets?path=hdfs%2Fsome');
assert.equal(categoryLinks.length, 1, 'There is 1 folder in path'); assert.equal(categoryLinks.length, 1, 'There is 1 folder in path');
@ -42,7 +43,7 @@ module('Acceptance | breadcrumbs-smoke-test', function(hooks) {
await click(`${categoryLinkClass}:first-child`); await click(`${categoryLinkClass}:first-child`);
await waitFor(categoryLinkClass); await waitFor(categoryLinkClass);
categoryLinks = findAll(categoryLinkClass); categoryLinks = findAll(categoryLinkClass);
breadcrumbs = findAll(breadcrumbsClass); breadcrumbs = findAll(breadCrumbsClass);
assert.equal(currentURL(), '/browse/datasets?path=hdfs%2Fsome%2Fpath'); assert.equal(currentURL(), '/browse/datasets?path=hdfs%2Fsome%2Fpath');
assert.equal(categoryLinks.length, 1, 'There is 1 folder in path'); assert.equal(categoryLinks.length, 1, 'There is 1 folder in path');
@ -54,7 +55,7 @@ module('Acceptance | breadcrumbs-smoke-test', function(hooks) {
await click(`${categoryLinkClass}:first-child`); await click(`${categoryLinkClass}:first-child`);
await waitFor(categoryLinkClass, { count: 2 }); await waitFor(categoryLinkClass, { count: 2 });
categoryLinks = findAll(categoryLinkClass); categoryLinks = findAll(categoryLinkClass);
breadcrumbs = findAll(breadcrumbsClass); breadcrumbs = findAll(breadCrumbsClass);
assert.equal(currentURL(), '/browse/datasets?path=hdfs%2Fsome%2Fpath%2Fwith'); assert.equal(currentURL(), '/browse/datasets?path=hdfs%2Fsome%2Fpath%2Fwith');
assert.equal(categoryLinks.length, 2, 'There are 2 folder in path'); assert.equal(categoryLinks.length, 2, 'There are 2 folder in path');
@ -66,7 +67,7 @@ module('Acceptance | breadcrumbs-smoke-test', function(hooks) {
await click(`${categoryLinkClass}:first-child`); await click(`${categoryLinkClass}:first-child`);
await waitFor(categoryLinkClass, { count: 2 }); await waitFor(categoryLinkClass, { count: 2 });
categoryLinks = findAll(categoryLinkClass); categoryLinks = findAll(categoryLinkClass);
breadcrumbs = findAll(breadcrumbsClass); breadcrumbs = findAll(breadCrumbsClass);
assert.equal(currentURL(), '/browse/datasets?path=hdfs%2Fsome%2Fpath%2Fwith%2Fdirectories'); assert.equal(currentURL(), '/browse/datasets?path=hdfs%2Fsome%2Fpath%2Fwith%2Fdirectories');
assert.equal(categoryLinks.length, 2, 'There is only 2 datasets in path'); assert.equal(categoryLinks.length, 2, 'There is only 2 datasets in path');
@ -76,29 +77,13 @@ module('Acceptance | breadcrumbs-smoke-test', function(hooks) {
// path adataset1 // path adataset1
await click(`${categoryLinkClass}:first-child`); await click(`${categoryLinkClass}:first-child`);
breadcrumbs = findAll(breadcrumbsClass); breadcrumbs = findAll(breadCrumbsClass);
assert.equal( assert.equal(
currentURL(), currentURL(),
'/datasets/urn:li:dataset:(urn:li:dataPlatform:hdfs,%2Fsome%2Fpath%2Fwith%2Fdirectories%2Fadataset1,PROD)/schema' '/datasets/urn:li:dataset:(urn:li:dataPlatform:hdfs,%2Fsome%2Fpath%2Fwith%2Fdirectories%2Fadataset1,PROD)/schema'
); );
assert.equal(breadcrumbs.length, 7, 'There are 7 links in breadcrumb'); assert.equal(breadcrumbs.length, 8, 'There are 8 links in breadcrumb');
assert.equal( assert.dom(breadCrumbClass).hasText('Datasets PROD hdfs some path with directories adataset1');
getTextNoSpacesFromElements(breadcrumbs),
'Datasetshdfssomepathwithdirectoriesadataset1',
'Text match'
);
// path /some/path/with/directories
await click(`${breadcrumbsClass}:nth-child(6) a`);
await waitFor(categoryLinkClass, { count: 2 });
categoryLinks = findAll(categoryLinkClass);
breadcrumbs = findAll(breadcrumbsClass);
assert.equal(currentURL(), '/browse/datasets?page=1&path=hdfs%2Fsome%2Fpath%2Fwith%2Fdirectories');
assert.equal(categoryLinks.length, 2, 'There is only 2 datasets in path');
assert.equal(getTextNoSpacesFromElements(categoryLinks), 'adataset1adataset2', 'Text match');
assert.equal(breadcrumbs.length, 6, 'There are 6 links in breadcrumb');
assert.equal(getTextNoSpacesFromElements(breadcrumbs), 'Datasetshdfssomepathwithdirectories', 'Text match');
}); });
}); });

View File

@ -6,17 +6,17 @@ import hbs from 'htmlbars-inline-precompile';
const componentClassName = '.nacho-breadcrumbs-container'; const componentClassName = '.nacho-breadcrumbs-container';
const breadcrumbsListClassName = '.nacho-breadcrumbs'; const breadcrumbsListClassName = '.nacho-breadcrumbs';
module('Integration | Component | browser/entity-breadcrumbs', function(hooks) { module('Integration | Component | browser/entity-breadcrumbs', function(hooks): void {
setupRenderingTest(hooks); setupRenderingTest(hooks);
test('Breadcrumbs component rendering', async function(assert) { test('Breadcrumbs component rendering', async function(assert): Promise<void> {
const entity = 'Test Entity'; const entity = 'Test Entity';
let segments: Array<string> = []; let segments: Array<string> = [];
this.setProperties({ segments, entity }); this.setProperties({ segments, entity });
await render(hbs` await render(hbs`
{{browser/entity-breadcrumbs segments=segments entity=entity}} <Browser::EntityBreadcrumbs @segments={{segments}} @entity={{entity}} />
`); `);
assert.dom(componentClassName).isVisible(); assert.dom(componentClassName).isVisible();

View File

@ -4,7 +4,7 @@ import { render, findAll, click } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile'; import hbs from 'htmlbars-inline-precompile';
import { noop } from 'wherehows-web/utils/helpers/functions'; import { noop } from 'wherehows-web/utils/helpers/functions';
import { OwnerType, OwnerSource } from 'wherehows-web/utils/api/datasets/owners'; import { OwnerType } from 'wherehows-web/utils/api/datasets/owners';
import owners from 'wherehows-web/mirage/fixtures/owners'; import owners from 'wherehows-web/mirage/fixtures/owners';
import userStub from 'wherehows-web/tests/stubs/services/current-user'; import userStub from 'wherehows-web/tests/stubs/services/current-user';
import { minRequiredConfirmedOwners } from 'wherehows-web/constants/datasets/owner'; import { minRequiredConfirmedOwners } from 'wherehows-web/constants/datasets/owner';
@ -29,7 +29,7 @@ module('Integration | Component | dataset authors', function(hooks) {
this.set('saveOwnerChanges', noop); this.set('saveOwnerChanges', noop);
await render(hbs`{{dataset-authors owners=owners ownerTypes=ownerTypes save=(action saveOwnerChanges)}}`); await render(hbs`{{dataset-authors owners=owners ownerTypes=ownerTypes save=(action saveOwnerChanges)}}`);
assert.equal(findAll('.dataset-author').length, 2, 'expected two dataset author components to be rendered'); assert.equal(findAll('.dataset-author').length, 1, 'expected two dataset author components to be rendered');
}); });
test('it should remove an owner when removeOwner is invoked', async function(assert) { test('it should remove an owner when removeOwner is invoked', async function(assert) {
@ -43,39 +43,6 @@ module('Integration | Component | dataset authors', function(hooks) {
assert.equal(this.get('owners').length, 0); assert.equal(this.get('owners').length, 0);
}); });
test('it should update a suggested owner to confirmed', async function(assert) {
assert.expect(3);
const [owner, suggestedOwner] = owners;
const resolvedOwners = [owner];
const suggestedOwners = [suggestedOwner];
const initialLength = resolvedOwners.length;
let userName, confirmedOwner;
this.set('owners', resolvedOwners);
this.set('ownerTypes', ownerTypes);
this.set('saveOwnerChanges', noop);
this.set('suggestedOwners', suggestedOwners);
await render(
hbs`{{dataset-authors owners=owners suggestedOwners=suggestedOwners ownerTypes=ownerTypes save=(action saveOwnerChanges)}}`
);
assert.equal(
this.get('owners.length'),
initialLength,
`the list of owners is ${initialLength} before adding confirmed owner`
);
await click('.dataset-authors-suggested__info__trigger');
await click('.suggested-owner-card__owner-info__add button');
userName = this.get('current-user.currentUser.userName');
confirmedOwner = this.get('owners').findBy('confirmedBy', userName);
assert.equal(this.get('owners.length'), initialLength + 1, 'the list of owner contains one more new owner');
assert.equal(confirmedOwner.source, OwnerSource.Ui, 'contains a new owner with ui source');
});
test('it should disable the save button when confirmedOwners is less than required minimum', async function(assert) { test('it should disable the save button when confirmedOwners is less than required minimum', async function(assert) {
assert.expect(2); assert.expect(2);
this.set('owners', [confirmedOwner]); this.set('owners', [confirmedOwner]);