Merge pull request #850 from theseyi/ownership-rewrite

- adds owner constants: default values, confirm and update owner functions
 - opts in to Ember array prototypal extensions
 - implements ownership distinction for suggested owners and confirmed owners. updates type definitions for owners
 - defers to more robust isEqual function on lodash
 - corrects the typings for ownerType vs ownerIdType. changes updateOwnerType action name to changeOwnerType on dataset-author component. adds fixtures for owner and user types. uses correct handler selectionDidChange for ember selector component
 - adds services stub for current user and exercises dataset-authors component
 - refactors interface name format. remove unused computed property
 - replaces manual DOM event triggers with ember native dom helpers
This commit is contained in:
Seyi Adebajo 2017-11-10 16:18:09 -08:00 committed by GitHub
commit 0aa6f794bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 1048 additions and 193 deletions

View File

@ -0,0 +1,144 @@
import Component from '@ember/component';
import ComputedProperty, { equal } from '@ember/object/computed';
import { getProperties, computed } from '@ember/object';
import { assert } from '@ember/debug';
import { IOwner } from 'wherehows-web/typings/api/datasets/owners';
import { OwnerSource, OwnerType } from 'wherehows-web/utils/api/datasets/owners';
/**
* This component renders a single owner record and also provides functionality for interacting with the component
* in the ui or performing operations on a single owner record
* @export
* @class DatasetAuthor
* @extends {Component}
*/
export default class DatasetAuthor extends Component {
tagName = 'tr';
classNames = ['dataset-author-record'];
classNameBindings = ['isConfirmedSuggestedOwner:dataset-author-record--disabled'];
/**
* The owner record being rendered
* @type {IOwner}
* @memberof DatasetAuthor
*/
owner: IOwner;
/**
* List of suggested owners that have been confirmed by a user
* @type {Array<IOwner>}
* @memberof DatasetAuthor
*/
commonOwners: Array<IOwner>;
/**
* External action to handle owner removal from the confirmed list
* @param {IOwner} owner the owner to be removed
* @memberof DatasetAuthor
*/
removeOwner: (owner: IOwner) => IOwner | void;
/**
* External action to handle owner addition to the confirmed list
* @param {IOwner} owner the suggested owner to be confirmed
* @return {Array<IOwner> | void} the list of owners or void if unsuccessful
* @memberof DatasetAuthor
*/
confirmSuggestedOwner: (owner: IOwner) => Array<IOwner> | void;
/**
* External action to handle owner property updates, currently on the confirmed list
* @param {IOwner} owner the owner to update
* @param {OwnerType} type the type of the owner
* @memberof DatasetAuthor
*/
updateOwnerType: (owner: IOwner, type: OwnerType) => void;
/**
* A list of available owner types retrieved from the api
* @type {Array<string>}
* @memberof DatasetAuthor
*/
ownerTypes: Array<string>;
/**
* Compares the source attribute on an owner, if it matches the OwnerSource.Ui type
* @type {ComputedProperty<boolean>}
* @memberof DatasetAuthor
*/
isOwnerMutable: ComputedProperty<boolean> = equal('owner.source', OwnerSource.Ui);
/**
* Determines if the owner record is a system suggested owner and if this record is confirmed by a user
* @type {ComputedProperty<boolean>}
* @memberof DatasetAuthor
*/
isConfirmedSuggestedOwner: ComputedProperty<boolean> = computed('commonOwners', function(this: DatasetAuthor) {
const { commonOwners, isOwnerMutable, owner: { userName } } = getProperties(this, [
'commonOwners',
'isOwnerMutable',
'owner'
]);
if (!isOwnerMutable) {
return commonOwners.findBy('userName', userName);
}
return false;
});
constructor() {
super(...arguments);
const typeOfRemoveOwner = typeof this.removeOwner;
const typeOfConfirmSuggestedOwner = typeof this.confirmSuggestedOwner;
// Checks that the expected external actions are provided
assert(
`Expected action removeOwner to be an function (Ember action), got ${typeOfRemoveOwner}`,
typeOfRemoveOwner === 'function'
);
assert(
`Expected action confirmOwner to be an function (Ember action), got ${typeOfConfirmSuggestedOwner}`,
typeOfConfirmSuggestedOwner === 'function'
);
}
actions = {
/**
* Invokes the external action removeOwner to remove an owner from the confirmed list
* @return {boolean | void | IOwner}
*/
removeOwner: () => {
const { owner, isOwnerMutable, removeOwner } = getProperties(this, ['owner', 'isOwnerMutable', 'removeOwner']);
return isOwnerMutable && removeOwner(owner);
},
/**
* Invokes the external action for confirming the suggested owner
* @return {Array<IOwner> | void}
*/
confirmOwner: () => {
const { owner, confirmSuggestedOwner } = getProperties(this, ['owner', 'confirmSuggestedOwner']);
return confirmSuggestedOwner(owner);
},
/**
* Updates the type attribute on the owner record
* @param {OwnerType} type value to update the type attribute with
* @return {void}
*/
changeOwnerType: (type: OwnerType) => {
const { owner, isOwnerMutable, updateOwnerType } = getProperties(this, [
'owner',
'isOwnerMutable',
'updateOwnerType'
]);
return isOwnerMutable && updateOwnerType(owner, type);
}
};
}

View File

@ -0,0 +1,250 @@
import Component from '@ember/component';
import { inject } from '@ember/service';
import ComputedProperty, { or, lt, filter } from '@ember/object/computed';
import { set, get, computed, getProperties } from '@ember/object';
import { assert } from '@ember/debug';
import { isEqual } from 'lodash';
import UserLookup from 'wherehows-web/services/user-lookup';
import CurrentUser from 'wherehows-web/services/current-user';
import { IOwner } from 'wherehows-web/typings/api/datasets/owners';
import {
defaultOwnerProps,
defaultOwnerUserName,
minRequiredConfirmedOwners,
ownerAlreadyExists,
userNameEditableClass,
confirmOwner,
updateOwner
} from 'wherehows-web/constants/datasets/owner';
import { OwnerSource, OwnerIdType, OwnerType } from 'wherehows-web/utils/api/datasets/owners';
import { ApiStatus } from 'wherehows-web/utils/api';
/**
* Defines properties for the component that renders a list of owners and provides functionality for
* interacting with the list items or the list as whole
* @export
* @class DatasetAuthors
* @extends {Component}
*/
export default class DatasetAuthors extends Component {
/**
* Invokes an external save action to persist the list of owners
* @return {Promise<{ status: ApiStatus }>}
* @memberof DatasetAuthors
*/
save: (owners: Array<IOwner>) => Promise<{ status: ApiStatus }>;
/**
* The list of owners
* @type {Array<IOwner>}
* @memberof DatasetAuthors
*/
owners: Array<IOwner>;
/**
* Current user service
* @type {ComputedProperty<CurrentUser>}
* @memberof DatasetAuthors
*/
currentUser: ComputedProperty<CurrentUser> = inject();
/**
* User look up service
* @type {ComputedProperty<UserLookup>}
* @memberof DatasetAuthors
*/
userLookup: ComputedProperty<UserLookup> = inject();
/**
* Reference to the userNamesResolver function to asynchronously match userNames
* @type {UserLookup['userNamesResolver']}
* @memberof DatasetAuthors
*/
userNamesResolver: UserLookup['userNamesResolver'];
/**
* A list of valid owner type strings returned from the remote api endpoint
* @type {Array<string>}
* @memberof DatasetAuthors
*/
ownerTypes: Array<string>;
/**
* Computed flag indicating that a set of negative flags is true
* e.g. if the userName is invalid or the required minimum users are not confirmed
* @type {ComputedProperty<boolean>}
* @memberof DatasetAuthors
*/
ownershipIsInvalid: ComputedProperty<boolean> = or('userNameInvalid', 'requiredMinNotConfirmed');
/**
* Checks that the list of owners does not contain a default user name
* @type {ComputedProperty<boolean>}
* @memberof DatasetAuthors
*/
userNameInvalid: ComputedProperty<boolean> = computed('owners.[]', function(this: DatasetAuthors) {
const owners = get(this, 'owners') || [];
return owners.filter(({ userName }) => userName === defaultOwnerUserName).length > 0;
});
/**
* Flag that resolves in the affirmative if the number of confirmed owner is less the minimum required
* @type {ComputedProperty<boolean>}
* @memberof DatasetAuthors
*/
requiredMinNotConfirmed: ComputedProperty<boolean> = lt('confirmedOwners.length', minRequiredConfirmedOwners);
/**
* Lists the owners that have be confirmed view the client ui
* @type {ComputedProperty<Array<IOwner>>}
* @memberof DatasetAuthors
*/
confirmedOwners: ComputedProperty<Array<IOwner>> = filter('owners', function({ source }: IOwner) {
return source === OwnerSource.Ui;
});
/**
* Intersection of confirmed owners and suggested owners
* @type {ComputedProperty<Array<IOwner>>}
* @memberof DatasetAuthors
*/
commonOwners: ComputedProperty<Array<IOwner>> = computed(
'confirmedOwners.@each.userName',
'systemGeneratedOwners.@each.userName',
function(this: DatasetAuthors) {
const { confirmedOwners = [], systemGeneratedOwners = [] } = getProperties(this, [
'confirmedOwners',
'systemGeneratedOwners'
]);
return confirmedOwners.reduce((common, owner) => {
const { userName } = owner;
return systemGeneratedOwners.findBy('userName', userName) ? [...common, owner] : common;
}, []);
}
);
/**
* Lists owners that have been gleaned from dataset metadata
* @type {ComputedProperty<Array<IOwner>>}
* @memberof DatasetAuthors
*/
systemGeneratedOwners: ComputedProperty<Array<IOwner>> = filter('owners', function({ source }: IOwner) {
return source !== OwnerSource.Ui;
});
constructor() {
super(...arguments);
const typeOfSaveAction = typeof this.save;
// on instantiation, sets a reference to the userNamesResolver async function
set(this, 'userNamesResolver', get(get(this, 'userLookup'), 'userNamesResolver'));
assert(
`Expected action save to be an function (Ember action), got ${typeOfSaveAction}`,
typeOfSaveAction === 'function'
);
}
actions = {
/**
* Prepares component for updates to the userName attribute
* @param {IOwner} _ unused
* @param {HTMLElement} { currentTarget }
*/
willEditUserName(_: IOwner, { currentTarget }: Event) {
const { classList } = <HTMLElement>(currentTarget || {});
if (classList instanceof HTMLElement) {
classList.add(userNameEditableClass);
}
},
/**
* Updates the owner instance userName property
* @param {IOwner} currentOwner an instance of an IOwner type to be updates
* @param {string} [userName] optional userName to update to
*/
editUserName: async (currentOwner: IOwner, userName?: string) => {
if (userName) {
const { getPartyEntityWithUserName } = get(this, 'userLookup');
const partyEntity = await getPartyEntityWithUserName(userName);
if (partyEntity) {
const { label, displayName, category } = partyEntity;
const isGroup = category === 'group';
const updatedOwnerProps: IOwner = {
...currentOwner,
isGroup,
source: OwnerSource.Ui,
userName: label,
name: displayName,
idType: isGroup ? OwnerIdType.Group : OwnerIdType.User
};
updateOwner(get(this, 'owners'), currentOwner, updatedOwnerProps);
}
}
},
/**
* Adds the component owner record to the list of owners with default props
* @returns {Array<IOwner> | void}
*/
addOwner: () => {
const owners = get(this, 'owners') || [];
const newOwner: IOwner = { ...defaultOwnerProps };
if (!ownerAlreadyExists(owners, { userName: newOwner.userName })) {
const { userName } = get(get(this, 'currentUser'), 'currentUser');
let updatedOwners = [newOwner, ...owners];
confirmOwner(get(this, 'owners'), newOwner, userName);
return owners.setObjects(updatedOwners);
}
},
/**
* Updates the type attribute for a given owner in the owner list
* @param {IOwner} owner owner to be updates
* @param {OwnerType} type new value to be set on the type attribute
*/
updateOwnerType: (owner: IOwner, type: OwnerType) => {
const owners = get(this, 'owners') || [];
return updateOwner(owners, owner, 'type', type);
},
/**
* Adds the owner instance to the list of owners with the source set to ui
* @param {IOwner} owner the owner to add to the list of owner with the source set to OwnerSource.Ui
* @return {Array<IOwner> | void}
*/
confirmSuggestedOwner: (owner: IOwner) => {
const owners = get(this, 'owners') || [];
const suggestedOwner = { ...owner, source: OwnerSource.Ui };
const hasSuggested = owners.find(owner => isEqual(owner, suggestedOwner));
if (!hasSuggested) {
return owners.setObjects([...owners, suggestedOwner]);
}
},
/**
* removes an owner instance from the list of owners
* @param {IOwner} owner the owner to be removed
*/
removeOwner: (owner: IOwner) => {
const owners = get(this, 'owners') || [];
return owners.removeObject(owner);
},
/**
* Persists the owners list by invoking the external action
*/
saveOwners: () => {
const { save } = this;
save(get(this, 'owners'));
}
};
}

View File

@ -340,13 +340,6 @@ export default Component.extend({
label: value ? formatAsCapitalizedStringWithSpaces(value) : '...'
})),
/**
* Caches the policy's modification time in milliseconds
*/
policyModificationTimeInEpoch: computed('complianceInfo', function() {
return getWithDefault(this, 'complianceInfo.modifiedTime', 0);
}),
/**
* @type {Boolean} cached boolean flag indicating that fields do contain a `kafka type`
* tracking header.

View File

@ -0,0 +1,131 @@
import { IOwner } from 'wherehows-web/typings/api/datasets/owners';
import { OwnerIdType, OwnerSource, OwnerType, OwnerUrnNamespace } from 'wherehows-web/utils/api/datasets/owners';
import { isListUnique } from 'wherehows-web/utils/array';
/**
* Initial user name for candidate owners
* @type {string}
*/
const defaultOwnerUserName = 'New Owner';
/**
* The minimum required number of owners
* @type {number}
*/
const minRequiredConfirmedOwners = 2;
/**
* Class to toggle readonly mode vs edit mode
* @type {string}
*/
const userNameEditableClass = 'dataset-author-cell--editing';
/**
* Checks that a userName already exists in the list of IOwner instances
* @param {Array<IOwner>} owners the list of owners
* @param {Pick<IOwner, 'userName'>} newOwner userName for the owner
* @returns {boolean} true if owner username in current list of owners
*/
const ownerAlreadyExists = (owners: Array<IOwner>, newOwner: Pick<IOwner, 'userName'>) => {
const newUserNameRegEx = new RegExp(`.*${newOwner.userName}.*`, 'i');
return owners.mapBy('userName').some((userName: string) => newUserNameRegEx.test(userName));
};
// overloads
function updateOwner(owners: Array<IOwner>, owner: IOwner, props: IOwner): void | Array<IOwner>;
function updateOwner<K extends keyof IOwner>(
owners: Array<IOwner>,
owner: IOwner,
props: K,
value: IOwner[K]
): void | Array<IOwner>;
/**
* Updates an IOwner instance in a list of IOwners using a known key and expected value type,
* or a replacement attributes
* @template K
* @param {Array<IOwner>} owners the list containing the owners
* @param {IOwner} owner the owner to update
* @param {(K | IOwner)} props the properties to replace the IOwner instance with, or a singe IOwner attribute
* @param {IOwner[K]} [value] optional value to update the attribute with
* @returns {(void | Array<IOwner>)} the updated list of owners if the owner list contains no duplicates
*/
function updateOwner<K extends keyof IOwner>(
owners: Array<IOwner>,
owner: IOwner,
props: K | IOwner,
value?: IOwner[K]
): void | Array<IOwner> {
// creates a local working copy of the list of owners
const updatingOwners = [...owners];
// ensure that the owner is in the list by referential equality
if (updatingOwners.includes(owner)) {
const ownerPosition = updatingOwners.indexOf(owner);
let updatedOwner: IOwner;
// if props is a string, i.e. attribute IOwner, override the previous value,
// otherwise replace with new attributes
if (typeof props === 'string') {
updatedOwner = { ...owner, [props]: value };
} else {
updatedOwner = props;
}
// retain update position
const updatedOwners: Array<IOwner> = [
...updatingOwners.slice(0, ownerPosition),
updatedOwner,
...updatingOwners.slice(ownerPosition + 1)
];
// each owner is uniquely identified by the composite key of userName and source
const userKeys = updatedOwners.map(({ userName, source }) => `${userName}:${source}`);
// ensure we have not duplicates
if (isListUnique(userKeys)) {
return owners.setObjects(updatedOwners);
}
}
}
/**
* Sets the `confirmedBy` attribute to the currently logged in user
* @param {Array<IOwner>} owners the list of owners
* @param {IOwner} owner the owner to be updated
* @param {string} confirmedBy the userName of the confirming user
* @returns {(Array<IOwner> | void)}
*/
const confirmOwner = (owners: Array<IOwner>, owner: IOwner, confirmedBy: string): Array<IOwner> | void => {
const isConfirmedBy = confirmedBy || null;
return updateOwner(owners, owner, 'confirmedBy', isConfirmedBy);
// return set(owner, 'confirmedBy', isConfirmedBy);
};
/**
* Defines the default properties for a newly created IOwner instance
*@type {IOwner}
*/
const defaultOwnerProps: IOwner = {
userName: defaultOwnerUserName,
email: null,
name: '',
isGroup: false,
namespace: OwnerUrnNamespace.groupUser,
type: OwnerType.Owner,
subType: null,
sortId: 0,
source: OwnerSource.Ui,
confirmedBy: null,
idType: OwnerIdType.User
};
export {
defaultOwnerProps,
defaultOwnerUserName,
minRequiredConfirmedOwners,
userNameEditableClass,
ownerAlreadyExists,
updateOwner,
confirmOwner
};

View File

@ -10,9 +10,14 @@ import { IPartyEntity, IPartyProps } from 'wherehows-web/typings/api/datasets/pa
* @param {Function} asyncResults callback
* @return {Promise<void>}
*/
const ldapResolver = async (userNameQuery: string, _syncResults: Function, asyncResults: Function): Promise<void> => {
const ldapResolver = async (
userNameQuery: string,
_syncResults: Function,
asyncResults: (results: Array<string>) => void
): Promise<void> => {
const ldapRegex = new RegExp(`^${userNameQuery}.*`, 'i');
const { userEntitiesSource = [] }: IPartyProps = await getUserEntities();
asyncResults(userEntitiesSource.filter((entity: string) => ldapRegex.test(entity)));
};

View File

@ -1 +1,2 @@
@import "owner-table";
@import 'owner-table';
@import 'dataset-author';

View File

@ -0,0 +1,23 @@
.dataset-author {
margin-top: item-spacing(7);
&__header {
display: flex;
font-size: 20px;
font-weight: fw(normal, 4);
}
}
.dataset-author-record {
&--disabled {
color: set-color(grey, mid);
font-weight: fw(italic, 2);
pointer-events: none;
}
&#{&} &__action {
&--disabled {
background-color: set-color(grey, mid);
}
}
}

View File

@ -58,3 +58,18 @@ $user-name-width: 170px;
width: 9%;
}
}
.dataset-owner-table {
&#{&} {
margin-top: item-spacing(4);
box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.16), 0 0 0 1px rgba(0, 0, 0, 0.08);
transition: box-shadow 200ms cubic-bezier(0.4, 0, 0.2, 1);
border: 0;
}
&#{&} td,
&#{&} th {
padding: item-spacing(5 4);
font-weight: fw(normal, 4);
}
}

View File

@ -1,148 +1,64 @@
<div class="tab-body">
<section class="action-bar">
<div class="container action-bar__content">
<button class="nacho-button nacho-button--large-inverse action-bar__item"
title={{if ownershipIsInvalid
"Need at least two confirmed owners to make changes
and no Invalid users"
"Save"}}
disabled={{ownershipIsInvalid}}
{{action "updateOwners"}}>
Save
<td>
{{user-avatar userName=owner.userName}}
{{owner.userName}}
</td>
<td>
{{owner.name}}
</td>
<td>
{{owner.idType}}
</td>
<td>
{{owner.source}}
</td>
<td>
{{moment-calendar owner.modifiedTime sameElse="MMM Do YYYY, h:mm a"}}
</td>
<td>
{{ember-selector
class=(unless isOwnerMutable "nacho-select--hidden-state")
values=ownerTypes
selected=owner.type
disabled=(not isOwnerMutable)
selectionDidChange=(action "changeOwnerType")
}}
</td>
<td>
{{#if isOwnerMutable}}
<button
class="nacho-button nacho-button--small remove-dataset-author"
{{action "removeOwner"}}>
<i class="fa fa-trash"
aria-label="Remove Owner"></i>
</button>
{{else}}
{{#if isConfirmedSuggestedOwner}}
<button
class="nacho-button nacho-button--small dataset-author-record__action--disabled">
Added
</button>
<button class="nacho-button nacho-button--large action-bar__item"
{{action "addOwner" owners}}>
<i class="fa fa-plus" title="Add an Owner">
</i>
Add an Owner
{{else}}
<button
class="nacho-button nacho-button--small confirm-suggested-dataset-author"
{{action "confirmOwner"}}>
<i class="fa fa-plus" title="Add an Owner"></i>
</button>
</div>
</section>
<div class="alert alert-info" role="alert">
<p>
<strong>
Why can't I click save? It's greyed out
</strong>
</p>
<p>
Please note that to make any update the list of owners for this dataset,
there needs to be:
</p>
{{/if}}
<p><strong>At least two (2)</strong> confirmed owners who are both</p>
<ul>
<li>
of ID Type <code>USER</code>
</li>
<li>
of Owner Type <code>Owner</code>
</li>
</ul>
<br>
<p>
<strong>
Why can't I remove an owner?
</strong>
</p>
<p>
Only owners that are not sourced from <code>SCM</code> or <code>NUAGE</code> can be removed
from this list. To remove any such owner(s), please make the change at the
source.</p>
</div>
{{#if errorMessage}}
<div class="alert alert-danger" role="alert">{{errorMessage}}</div>
{{/if}}
</td>
{{#if actionMessage}}
<div class="alert alert-success" role="alert">{{actionMessage}}</div>
{{/if}}
<table class="nacho-table nacho-table--bordered nacho-table--stripped">
<thead>
<tr>
<th class="dataset-author-column--wide">LDAP Username</th>
<th>Full Name</th>
<th class="dataset-author-column--narrow">ID Type</th>
<th>Source</th>
<th>Last Modified</th>
<th>
Owner Type
<!--TODO: DSS-6716-->
<!-- DRY out with wrapper component that takes the link as an attribute-->
<a
target="_blank"
href="https://iwww.corp.linkedin.com/wiki/cf/display/DWH/Metadata+Acquisition#ProjectOverview-ownership">
<sup>
<span class="glyphicon glyphicon-question-sign"
title="Link to more information"></span>
</sup>
</a>
</th>
<th class="dataset-author-column--narrow">Confirm?</th>
<th>Remove</th>
</tr>
</thead>
<tbody data-attribute="owner-table">
{{#each owners as |owner|}}
<tr>
<td class="dataset-author-cell"
onclick={{action "willEditUserName"
owner}}>
<label class={{unless (contains owner.source restrictedSources) "dataset-author-cell__name-tag"}}>
{{owner.userName}}
</label>
{{aupac-typeahead
source=userNamesResolver
action=(action "editUserName" owner)
autoFocus=true
async=true
limit=10
minLength=2
placeholder="Find user by LDAP"
title="username"
class="form-control dataset-author-cell__user-name"}}
</td>
<td>{{owner.name}}</td>
<td>{{owner.idType}}</td>
<td>{{owner.source}}</td>
<td class="dataset-author-cell__owner-last-mod">
{{!-- e.g Jul 18th 2016, 11:11 am --}}
{{moment-calendar owner.modifiedTime sameElse="MMM Do YYYY, h:mm a"}}
</td>
<td>
{{ember-selector
values=ownerTypes
selected=owner.type
change=(action "updateOwnerType" owner)}}
</td>
<td>
{{input
type="checkbox"
title=(if owner.confirmedBy owner.confirmedBy "Not confirmed")
checked=(readonly owner.confirmedBy)
change=(action "confirmOwner" owner)}}
</td>
<td>
<button class="nacho-button nacho-button--small"
title={{if (contains owner.source restrictedSources)
removeFromSourceMessage
"Remove"}}
disabled={{contains owner.source restrictedSources}}
{{action "removeOwner" owner}}>
<i class="fa fa-trash"
aria-label="Remove Owner"></i>
</button>
</td>
</tr>
{{/each}}
</tbody>
</table>
</div>

View File

@ -0,0 +1,118 @@
<section class="dataset-author">
<header>
<h2 class="dataset-author__header">
Owners Confirmed
</h2>
<p class="dataset-author__byline">
These are dataset ownership records that have been manually entered / confirmed by a human
</p>
</header>
<table class="nacho-table nacho-table--bordered dataset-owner-table dataset-owner-table">
<thead>
<tr>
<th class="dataset-author-column--wide">LDAP Username</th>
<th>Full Name</th>
<th class="dataset-author-column--narrow">ID Type</th>
<th>Source</th>
<th>Last Modified</th>
<th>
Ownership Type
<!--TODO: DSS-6716-->
<!-- DRY out with wrapper component that takes the link as an attribute-->
<a
target="_blank"
href="https://iwww.corp.linkedin.com/wiki/cf/display/DWH/Metadata+Acquisition#ProjectOverview-ownership">
<sup>
<span class="glyphicon glyphicon-question-sign"
title="Link to more information"></span>
</sup>
</a>
</th>
<th>Remove Owner</th>
</tr>
</thead>
{{#each confirmedOwners as |confirmedOwner|}}
<tbody>
{{dataset-author
owner=confirmedOwner
ownerTypes=ownerTypes
removeOwner=(action "removeOwner")
confirmSuggestedOwner=(action "confirmSuggestedOwner")
updateOwnerType=(action "updateOwnerType")
}}
</tbody>
{{else}}
{{/each}}
</table>
</section>
<section class="dataset-author">
<header>
<h2 class="dataset-author__header">
System Suggested Owners
</h2>
<p class="dataset-author__byline">
These are dataset ownership records, suggested based information derived from the source metadata.
</p>
</header>
<table class="nacho-table nacho-table--bordered dataset-owner-table">
<thead>
<tr>
<th class="dataset-author-column--wide">LDAP Username</th>
<th>Full Name</th>
<th class="dataset-author-column--narrow">ID Type</th>
<th>Source</th>
<th>Last Modified</th>
<th>
Owner Type
<!--TODO: DSS-6716-->
<!-- DRY out with wrapper component that takes the link as an attribute-->
<a
target="_blank"
href="https://iwww.corp.linkedin.com/wiki/cf/display/DWH/Metadata+Acquisition#ProjectOverview-ownership">
<sup>
<span class="glyphicon glyphicon-question-sign"
title="Link to more information"></span>
</sup>
</a>
</th>
<th>Add Suggested Owner</th>
</tr>
</thead>
<tbody>
{{#each systemGeneratedOwners as |systemGeneratedOwner|}}
{{dataset-author
owner=systemGeneratedOwner
ownerTypes=ownerTypes
commonOwners=commonOwners
removeOwner=(action "removeOwner")
confirmSuggestedOwner=(action "confirmSuggestedOwner")
updateOwnerType=(action "updateOwnerType")
}}
{{/each}}
</tbody>
</table>
</section>
<section class="action-bar">
<div class="container action-bar__content">
<button
class="nacho-button nacho-button--large-inverse action-bar__item dataset-authors-save"
{{action "saveOwners"}}>
Save
</button>
</div>
</section>
{{!--disabled={{ownershipIsInvalid}}--}}

View File

@ -61,7 +61,7 @@
Last saved:
<span class="policy-last-saved__saved">
{{if isNewComplianceInfo 'Never'
(moment-from-now policyModificationTimeInEpoch)}}
(moment-from-now complianceInfo.modifiedTime)}}
</span>
</div>

View File

@ -223,12 +223,9 @@
}}
</div>
<div id="ownertab" class="tab-pane active">
{{dataset-author
{{dataset-authors
owners=owners
ownerTypes=ownerTypes
showMsg=showMsg
alertType=alertType
ownerMessage=ownerMessage
save=(action "saveOwnerChanges")
}}
</div>

View File

@ -1,32 +1,22 @@
import { ApiStatus } from 'wherehows-web/utils/api/shared';
import { OwnerType } from 'wherehows-web/utils/api/datasets/owners';
/**
* Accepted string values for the Owner type
*/
type OwnerTypeLiteral = OwnerType.User | OwnerType.Group;
/**
* Accepted string values for the namespace of a user
*/
type OwnerUrnLiteral = 'urn:li:corpuser' | 'urn:li:corpGroup';
import { OwnerIdType, OwnerSource, OwnerType, OwnerUrnNamespace } from 'wherehows-web/utils/api/datasets/owners';
/**
* Describes the interface for an Owner entity
*/
export interface IOwner {
confirmedBy: null | string;
email: string;
idType: OwnerTypeLiteral;
isActive: boolean;
email: null | string;
idType: OwnerIdType;
isActive?: boolean;
isGroup: boolean;
modifiedTime: number | Date;
modifiedTime?: number | Date;
name: string;
namespace: OwnerUrnLiteral;
namespace: OwnerUrnNamespace;
sortId: null | number;
source: string;
source: OwnerSource;
subType: null;
type: string;
type: OwnerType;
userName: string;
}

View File

@ -18,9 +18,9 @@ export interface IPartyEntityResponse {
}
/**
* Describes a userEntityMap interface
* Describes a IUserEntityMap interface
*/
export interface userEntityMap {
export interface IUserEntityMap {
[label: string]: string;
}
@ -29,6 +29,6 @@ export interface userEntityMap {
*/
export interface IPartyProps {
userEntities: Array<IPartyEntity>;
userEntitiesMaps: userEntityMap;
userEntitiesSource: Array<keyof userEntityMap>;
userEntitiesMaps: IUserEntityMap;
userEntitiesSource: Array<keyof IUserEntityMap>;
}

View File

@ -0,0 +1,6 @@
import Ember from 'ember';
// opt-in to allow types for Ember Array Prototype extensions
declare global {
interface Array<T> extends Ember.ArrayPrototypeExtensions<T> {}
}

View File

@ -5,18 +5,51 @@ import {
IPartyEntity,
IPartyEntityResponse,
IPartyProps,
userEntityMap
IUserEntityMap
} from 'wherehows-web/typings/api/datasets/party-entities';
import { IOwner, IOwnerResponse } from 'wherehows-web/typings/api/datasets/owners';
/**
* Defines a string enum for valid owner types
*/
export enum OwnerType {
export enum OwnerIdType {
User = 'USER',
Group = 'GROUP'
}
/**
* Defines the string enum for the OwnerType attribute
* @type {string}
*/
export enum OwnerType {
Owner = 'Owner',
Consumer = 'Consumer',
Delegate = 'Delegate',
Producer = 'Producer',
Stakeholder = 'Stakeholder'
}
/**
* Accepted string values for the namespace of a user
*/
export enum OwnerUrnNamespace {
corpUser = 'urn:li:corpuser',
groupUser = 'urn:li:corpGroup'
}
export enum OwnerSource {
Scm = 'SCM',
Nuage = 'NUAGE',
Sos = 'SOS',
Db = 'DB',
Audit = 'AUDIT',
Jira = 'JIRA',
RB = 'RB',
Ui = 'UI',
Fs = 'FS',
Other = 'OTHER'
}
const { $: { getJSON } } = Ember;
/**
@ -39,7 +72,7 @@ export const getDatasetOwners = async (id: number): Promise<Array<IOwner>> => {
return status === ApiStatus.OK
? owners.map(owner => ({
...owner,
modifiedTime: new Date(owner.modifiedTime)
modifiedTime: new Date(owner.modifiedTime!)
}))
: Promise.reject(status);
};
@ -65,7 +98,7 @@ export const getUserEntities: () => Promise<IPartyProps> = (() => {
* Memoized reference to the resolved value of a previous invocation to curried function in getUserEntities
* @type {{result: IPartyProps | null}}
*/
const cache: { result: IPartyProps | null; userEntitiesSource: Array<keyof userEntityMap> } = {
const cache: { result: IPartyProps | null; userEntitiesSource: Array<keyof IUserEntityMap> } = {
result: null,
userEntitiesSource: []
};
@ -108,9 +141,9 @@ export const getUserEntities: () => Promise<IPartyProps> = (() => {
/**
* Transforms a list of party entities into a map of entity label to displayName value
* @param {Array<IPartyEntity>} partyEntities
* @return {Object<string>}
* @return {IUserEntityMap}
*/
export const getPartyEntitiesMap = (partyEntities: Array<IPartyEntity>): userEntityMap =>
export const getPartyEntitiesMap = (partyEntities: Array<IPartyEntity>): IUserEntityMap =>
partyEntities.reduce(
(map: { [label: string]: string }, { label, displayName }: IPartyEntity) => ((map[label] = displayName), map),
{}
@ -122,5 +155,6 @@ export const getPartyEntitiesMap = (partyEntities: Array<IPartyEntity>): userEnt
* @return {boolean}
*/
export const isRequiredMinOwnersNotConfirmed = (owners: Array<IOwner> = []): boolean =>
owners.filter(({ confirmedBy, type, idType }) => confirmedBy && type === 'Owner' && idType === OwnerType.User)
.length < minRequiredConfirmed;
owners.filter(
({ confirmedBy, type, idType }) => confirmedBy && type === OwnerType.Owner && idType === OwnerIdType.User
).length < minRequiredConfirmed;

View File

@ -0,0 +1,33 @@
import { IOwner } from 'wherehows-web/typings/api/datasets/owners';
import { OwnerSource, OwnerIdType, OwnerUrnNamespace } from 'wherehows-web/utils/api/datasets/owners';
export default <Array<IOwner>>[
{
confirmedBy: '',
email: 'confirmed-owner@linkedin.com',
idType: OwnerIdType.User,
isActive: true,
isGroup: true,
modifiedTime: Date.now(),
name: 'confirmed owner',
userName: 'fakeconfirmedowner',
namespace: OwnerUrnNamespace.corpUser,
source: OwnerSource.Ui,
subType: null,
type: 'Owner'
},
{
confirmedBy: '',
email: 'suggested-owner@linkedin.com',
idType: OwnerIdType.User,
isActive: true,
isGroup: true,
modifiedTime: Date.now(),
name: 'suggested owner',
userName: 'fakesuggestedowner',
namespace: OwnerUrnNamespace.corpUser,
source: OwnerSource.Nuage,
subType: null,
type: 'Owner'
}
];

View File

@ -0,0 +1,15 @@
import { IUser } from 'wherehows-web/typings/api/authentication/user';
export default <Array<IUser>>[
{
departmentNum: 42,
email: 'hitchiker@linkedin.com',
id: 1337,
name: 'fake user',
userName: 'p0wn',
userSetting: {
defaultWatch: '',
detailDefaultView: ''
}
}
];

View File

@ -20,6 +20,7 @@
"devDependencies": {
"@types/ember": "^2.8.0",
"@types/ember-testing-helpers": "^0.0.3",
"@types/lodash": "^4.14.83",
"@types/rsvp": "^4.0.0",
"babel-eslint": "^8.0.1",
"babel-plugin-transform-class-properties": "^6.24.1",
@ -51,8 +52,8 @@
"ember-export-application-global": "^2.0.0",
"ember-fetch": "^3.4.3",
"ember-load-initializers": "^1.0.0",
"ember-lodash-shim": "^2.0.5",
"ember-metrics": "^0.12.1",
"ember-native-dom-helpers": "^0.5.4",
"ember-pikaday": "^2.2.1",
"ember-redux-shim": "^1.1.1",
"ember-redux-thunk-shim": "^1.1.2",

View File

@ -0,0 +1,91 @@
import { moduleForComponent, test } from 'ember-qunit';
import hbs from 'htmlbars-inline-precompile';
import { triggerEvent } from 'ember-native-dom-helpers';
import { run } from '@ember/runloop';
import noop from 'wherehows-web/utils/noop';
import owners from 'wherehows-web/mirage/fixtures/owners';
import { OwnerType } from 'wherehows-web/utils/api/datasets/owners';
const [confirmedOwner, suggestedOwner] = owners;
const commonOwners = [];
const ownerTypes = Object.values(OwnerType);
moduleForComponent('dataset-author', 'Integration | Component | dataset author', {
integration: true
});
test('it renders', function(assert) {
this.set('removeOwner', noop);
this.set('confirmSuggestedOwner', noop);
this.set('owner', confirmedOwner);
this.set('commonOwners', commonOwners);
this.render(
hbs`{{dataset-author confirmSuggestedOwner=confirmSuggestedOwner removeOwner=removeOwner owner=owner commonOwners=commonOwners}}`
);
assert.equal(document.querySelector('tr.dataset-author-record').tagName, 'TR');
});
test('triggers the removeOwner action when invoked', function(assert) {
assert.expect(2);
let removeActionCallCount = 0;
this.set('removeOwner', () => {
removeActionCallCount++;
assert.equal(removeActionCallCount, 1, 'action is called once');
});
this.set('confirmSuggestedOwner', noop);
this.set('owner', confirmedOwner);
this.set('commonOwners', commonOwners);
this.render(
hbs`{{dataset-author confirmSuggestedOwner=confirmSuggestedOwner removeOwner=removeOwner owner=owner commonOwners=commonOwners}}`
);
assert.equal(removeActionCallCount, 0, 'action is not called on render');
triggerEvent('.remove-dataset-author', 'click');
});
test('triggers the confirmSuggestedOwner action when invoked', function(assert) {
assert.expect(2);
let confirmSuggestedOwnerActionCallCount = 0;
this.set('removeOwner', noop);
this.set('confirmSuggestedOwner', () => {
confirmSuggestedOwnerActionCallCount++;
assert.equal(confirmSuggestedOwnerActionCallCount, 1, 'action is called once');
});
this.set('owner', suggestedOwner);
this.set('commonOwners', commonOwners);
this.render(
hbs`{{dataset-author confirmSuggestedOwner=confirmSuggestedOwner removeOwner=removeOwner owner=owner commonOwners=commonOwners}}`
);
assert.equal(confirmSuggestedOwnerActionCallCount, 0, 'action is not called on render');
triggerEvent('.confirm-suggested-dataset-author', 'click');
});
test('triggers the updateOwnerType action when invoked', function(assert) {
assert.expect(2);
this.set('removeOwner', noop);
this.set('confirmSuggestedOwner', noop);
this.set('updateOwnerType', (owner, type) => {
assert.ok(confirmedOwner === owner, 'updateOwnerType action is invoked correct owner reference');
assert.equal(type, confirmedOwner.type, 'updateOwnerType action is invoked with selected type');
});
this.set('owner', confirmedOwner);
this.set('commonOwners', commonOwners);
this.set('ownerTypes', ownerTypes);
this.render(
hbs`{{dataset-author confirmSuggestedOwner=confirmSuggestedOwner removeOwner=removeOwner owner=owner commonOwners=commonOwners updateOwnerType=updateOwnerType ownerTypes=ownerTypes}}`
);
triggerEvent('select', 'change');
});

View File

@ -0,0 +1,84 @@
import { moduleForComponent, test } from 'ember-qunit';
import hbs from 'htmlbars-inline-precompile';
import { run } from '@ember/runloop';
import noop from 'wherehows-web/utils/noop';
import { OwnerType, OwnerSource } from 'wherehows-web/utils/api/datasets/owners';
import owners from 'wherehows-web/mirage/fixtures/owners';
import userStub from 'wherehows-web/tests/stubs/services/current-user';
const [confirmedOwner] = owners;
const ownerTypes = Object.values(OwnerType);
moduleForComponent('dataset-authors', 'Integration | Component | dataset authors', {
integration: true,
beforeEach() {
this.register('service:current-user', userStub);
this.inject.service('current-user');
}
});
test('it renders', function(assert) {
assert.expect(1);
this.set('owners', owners);
this.set('ownerTypes', ownerTypes);
this.set('saveOwnerChanges', noop);
this.render(hbs`{{dataset-authors owners=owners ownerTypes=ownerTypes save=(action saveOwnerChanges)}}`);
assert.equal(this.$('.dataset-author').length, 2, 'expected two dataset author components to be rendered');
});
test('it should remove an owner when removeOwner is invoked', function(assert) {
assert.expect(1);
this.set('owners', [confirmedOwner]);
this.set('ownerTypes', ownerTypes);
this.set('saveOwnerChanges', noop);
this.render(hbs`{{dataset-authors owners=owners ownerTypes=ownerTypes save=(action saveOwnerChanges)}}`);
run(() => {
document.querySelector('.remove-dataset-author').click();
});
assert.equal(this.get('owners').length, 0);
});
test('it should update a suggested owner to confirmed', function(assert) {
assert.expect(3);
const initialLength = owners.length;
this.set('owners', owners);
this.set('ownerTypes', ownerTypes);
this.set('saveOwnerChanges', noop);
this.render(hbs`{{dataset-authors owners=owners ownerTypes=ownerTypes save=(action saveOwnerChanges)}}`);
assert.equal(
this.get('owners.length'),
initialLength,
`the list of owners is ${initialLength} before adding confirmed owner`
);
run(() => {
document.querySelector('.confirm-suggested-dataset-author').click();
});
assert.equal(this.get('owners.length'), initialLength + 1, 'the list of owner contains one more new owner');
assert.equal(this.get('owners.lastObject.source'), OwnerSource.Ui, 'contains a new owner with ui source');
});
test('it should invoke the external save action on save', function(assert) {
assert.expect(2);
this.set('owners', [confirmedOwner]);
this.set('ownerTypes', ownerTypes);
this.set('saveOwnerChanges', owners => {
assert.ok(owners === this.get('owners'), 'the list of owners is passed into the save action');
});
this.render(hbs`{{dataset-authors owners=owners ownerTypes=ownerTypes save=(action saveOwnerChanges)}}`);
run(() => {
document.querySelector('.dataset-authors-save').click();
});
assert.equal(this.get('owners').length, 1);
});

View File

@ -0,0 +1,11 @@
import Service from '@ember/service';
import users from 'wherehows-web/mirage/fixtures/users';
const [user] = users;
export default class extends Service {
currentUser = user;
load = () => Promise.resolve();
invalidateSession = () => {};
trackCurrentUser = () => {};
}

View File

@ -113,6 +113,10 @@
version "3.2.15"
resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.2.15.tgz#3f620a9f5a0b296866f4bc729825226d0a35fba6"
"@types/lodash@^4.14.83":
version "4.14.83"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.83.tgz#2f2154797ce8fd8d6ea91a8d304a4e44ee95920c"
"@types/rsvp@*", "@types/rsvp@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@types/rsvp/-/rsvp-4.0.0.tgz#6c59d84bb5ea8a4fd11ec3d7aa748710e0e5e373"
@ -2976,13 +2980,6 @@ ember-load-initializers@^1.0.0:
dependencies:
ember-cli-babel "^6.0.0-beta.7"
ember-lodash-shim@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/ember-lodash-shim/-/ember-lodash-shim-2.0.5.tgz#c5015c5e91e09510238885b5de9c831538a0156c"
dependencies:
ember-cli-babel "^5.1.7"
ember-cli-htmlbars "^1.1.1"
ember-lodash@4.17.2:
version "4.17.2"
resolved "https://registry.yarnpkg.com/ember-lodash/-/ember-lodash-4.17.2.tgz#0ed40ab89c2f9846765fc2504c0034000f666933"