Owner tab UX revamp - add new suggested owners component to handle system generated suggestions

This commit is contained in:
cptran777 2018-04-12 14:39:22 -07:00
parent 5b56dc51c0
commit c19ff27b5a
14 changed files with 496 additions and 56 deletions

View File

@ -0,0 +1,18 @@
import DatasetAuthorComponent from 'wherehows-web/components/dataset-author';
export default class DatasetsOwnersSuggestedOwnerCard extends DatasetAuthorComponent {
/**
* Sets the html tag binded to the element generated by this component
*/
tagName = 'div';
/**
* Sets the class names binded to the html element generated by this component
*/
classNames = ['dataset-authors-suggested__card'];
/**
* Sets the class names that are triggered by certain properties on the component evaluating truthy
*/
classNameBindings = [];
}

View File

@ -0,0 +1,43 @@
import Component from '@ember/component';
import { IOwner } from 'wherehows-web/typings/api/datasets/owners';
import { computed, get } from '@ember/object';
import ComputedProperty, { empty } from '@ember/object/computed';
export default class DatasetsOwnersSuggestedOwners extends Component {
/**
* Sets the class names binded to the html element generated by this component
*/
classNames = ['dataset-authors-suggested'];
/**
* Whether or not the component is expanded. If not, users will only see the initial header information
* whereas if expanded then users will see the list of all suggested owners
* @type {boolean}
* @default false
*/
isExpanded = false;
/**
* Computed based on the owners array, detects whether this array is empty or not
* @type {ComputedProperty<boolean>}
*/
isEmpty = empty('owners');
/**
* Passed in value from parent component, `dataset-authors`, a.k.a. systemGeneratedOwners, this list
* represents a possible list of owners provided by scanning various systems.
* @type {Array<IOwner>}
* @default []
*/
owners: Array<IOwner> = this.owners || [];
/**
* For the facepile in the suggestions window header, we do not need tos how all the faces of all the
* possible owners as this could be a large amount. Take only up to the first four to pass into the
* template for rendering
* @type {ComputedProperty<Array<IOwner>>}
*/
facepileOwners: ComputedProperty<Array<IOwner>> = computed('owners', function(this: DatasetsOwnersSuggestedOwners) {
return get(this, 'owners').slice(0, 4);
});
}

View File

@ -66,6 +66,13 @@ export default class DatasetController extends Controller {
*/ */
jitAclAccessWhitelist: Array<DatasetPlatform>; jitAclAccessWhitelist: Array<DatasetPlatform>;
/**
* Flag indicating whether or not to show the ownership revamp tab information
* @type {string}
* @memberof DatasetController
*/
showOwnership: string;
/** /**
* Flag indicating the dataset policy is derived from an upstream source * Flag indicating the dataset policy is derived from an upstream source
* @type {boolean} * @type {boolean}

View File

@ -92,18 +92,20 @@ export default class DatasetRoute extends Route {
async setupController(this: DatasetRoute, controller: DatasetController, model: IDatasetView) { async setupController(this: DatasetRoute, controller: DatasetController, model: IDatasetView) {
set(controller, 'model', model); set(controller, 'model', model);
const configuratorService = get(this, 'configurator');
// TODO: refactor getConfig with conditional types after TS2.8 upgrade to reduce verbosity of annotations below // TODO: refactor getConfig with conditional types after TS2.8 upgrade to reduce verbosity of annotations below
const isInternal = <IAppConfig['isInternal']>get(this, 'configurator').getConfig<IAppConfig['isInternal']>( const isInternal = <IAppConfig['isInternal']>configuratorService.getConfig<IAppConfig['isInternal']>('isInternal');
'isInternal' const jitAclAccessWhitelist: IAppConfig['jitAclAccessWhitelist'] = <IAppConfig['jitAclAccessWhitelist']>configuratorService.getConfig<
IAppConfig['jitAclAccessWhitelist']
>('JitAclAccessWhitelist');
const showOwnership = <IAppConfig['showOwnership']>configuratorService.getConfig<IAppConfig['showOwnership']>(
'showOwnership'
); );
const jitAclAccessWhitelist: IAppConfig['jitAclAccessWhitelist'] = <IAppConfig['jitAclAccessWhitelist']>get(
this,
'configurator'
).getConfig<IAppConfig['jitAclAccessWhitelist']>('JitAclAccessWhitelist');
setProperties(controller, { setProperties(controller, {
isInternal: !!isInternal, isInternal: !!isInternal,
jitAclAccessWhitelist: jitAclAccessWhitelist || [] jitAclAccessWhitelist: jitAclAccessWhitelist || [],
showOwnership
}); });
} }

View File

@ -1,2 +1,3 @@
@import 'owner-table'; @import 'owner-table';
@import 'dataset-author'; @import 'dataset-author';
@import 'suggested-owners';

View File

@ -0,0 +1,145 @@
.dataset-authors-suggested {
background-color: rgba(225, 233, 238, 0.75);
width: 100%;
margin-bottom: 28px;
padding: 16px 24px 0;
overflow: hidden;
cursor: default;
.user-avatar {
height: 36px;
width: 36px;
}
&__header {
margin: 0;
font-weight: 400;
}
&__info {
height: 54px;
margin-bottom: 16px;
&__facepile,
&__description,
&__trigger {
float: left;
height: 100%;
padding-top: 6px;
}
&__facepile {
width: 128px;
display: flex;
flex-wrap: wrap;
&__avatar {
box-sizing: border-box;
background-clip: content-box;
border: 2px solid white;
width: 36px;
height: 36px;
clear: right;
border-radius: 50%;
margin-right: -12px;
&:last-child {
margin-right: 0;
}
}
}
&__description {
width: 560px;
word-wrap: wrap;
padding-left: 16px;
}
&__trigger {
float: right;
cursor: pointer;
&__action {
color: $link-color;
font-weight: 500;
}
}
}
&__card {
width: 31%;
box-sizing: border-box;
height: 108px;
background-color: white;
margin: 0 1% 16px;
float: left;
box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.16), 0 0 0 1px rgba(0, 0, 0, 0.08);
&__owner-info,
&__source-info {
width: 100%;
box-sizing: border-box;
padding: 6px 16px;
}
&__owner-info {
border-bottom: 1px solid rgba(0, 0, 0, 0.16);
height: 70px;
&__profile,
&__add {
width: 50%;
box-sizing: border-box;
float: left;
}
&__add {
padding-top: 12px;
padding-left: 12px;
text-align: center;
&--disabled,
.fa-check-circle-o {
font-size: 18px;
color: rgba(0, 0, 0, 0.5);
}
&--disabled {
font-weight: 600;
margin-left: 8px;
}
}
&__profile {
&__pic {
float: left;
margin-right: 6px;
margin-top: 10px;
}
&__name {
float: left;
max-width: 108px;
&__full {
max-width: 108px;
font-size: 16px;
font-weight: 400;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 0;
}
&__username {
font-size: 14px;
}
}
}
}
&__source-info {
font-size: 14px;
}
}
}

View File

@ -84,59 +84,71 @@
{{#if systemGeneratedOwners}} {{#if systemGeneratedOwners}}
<section class="dataset-author"> {{#if (eq showOwnership "show")}}
<header> <section class="dataset-author">
<h2 class="dataset-author__header"> {{datasets/owners/suggested-owners
System Suggested Owners owners=systemGeneratedOwners
</h2>
<p class="dataset-author__byline">
These are dataset ownership records, suggested based on 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>
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 class="dataset-owner-table--wrap">Add Suggested Owner</th>
</tr>
</thead>
<tbody>
{{#each systemGeneratedOwners as |systemGeneratedOwner|}}
{{dataset-author
owner=systemGeneratedOwner
ownerTypes=ownerTypes ownerTypes=ownerTypes
commonOwners=commonOwners commonOwners=commonOwners
removeOwner=(action "removeOwner") removeOwner=(action "removeOwner")
confirmSuggestedOwner=(action "confirmSuggestedOwner") confirmSuggestedOwner=(action "confirmSuggestedOwner")
updateOwnerType=(action "updateOwnerType") updateOwnerType=(action "updateOwnerType")}}
}} </section>
{{/each}} {{else}}
</tbody>
</table> <section class="dataset-author">
</section> <header>
<h2 class="dataset-author__header">
System Suggested Owners
</h2>
<p class="dataset-author__byline">
These are dataset ownership records, suggested based on 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>
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 class="dataset-owner-table--wrap">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>
{{/if}}
{{/if}} {{/if}}
<section class="action-bar"> <section class="action-bar">

View File

@ -33,6 +33,7 @@
{{dataset-authors {{dataset-authors
owners=owners owners=owners
ownerTypes=ownerTypes ownerTypes=ownerTypes
showOwnership=showOwnership
save=(action "saveOwnerChanges") save=(action "saveOwnerChanges")
}} }}

View File

@ -0,0 +1,29 @@
<section class="dataset-authors-suggested__card__owner-info">
<div class="dataset-authors-suggested__card__owner-info__profile">
{{user-avatar
class="dataset-authors-suggested__card__owner-info__profile__pic"
userName=owner.userName}}
<div class="dataset-authors-suggested__card__owner-info__profile__name">
<h5 class="dataset-authors-suggested__card__owner-info__profile__name__full">
{{owner.name}}
</h5>
<p class="dataset-authors-suggested__card__owner-info__profile__name__username">
{{owner.userName}}
</p>
</div>
</div>
<div class="dataset-authors-suggested__card__owner-info__add">
{{#if isConfirmedSuggestedOwner}}
{{fa-icon "check-circle-o" title="Added Owner" size="2"}}
<span class="dataset-authors-suggested__card__owner-info__add--disabled">Added</span>
{{else}}
<button class="nacho-button--secondary nacho-button--medium"
{{action "confirmOwner"}}>
Add as owner
</button>
{{/if}}
</div>
</section>
<section class="dataset-authors-suggested__card__source-info">
Source: {{owner.source}}
</section>

View File

@ -0,0 +1,35 @@
<h4 class="dataset-authors-suggested__header">Suggestions</h4>
{{#if isEmpty}}
<div class="dataset-authors-suggested__info__description">
We found no suggested owner(s) for this dataset, based on scanning different systems
</div>
{{else}}
<section class="dataset-authors-suggested__info">
<div class="dataset-authors-suggested__info__facepile">
{{#each facepileOwners as |owner|}}
{{user-avatar class="dataset-authors-suggested__info__facepile__avatar" userName=owner.userName}}
{{/each}}
</div>
<div class="dataset-authors-suggested__info__description">
We found {{owners.length}} {{if (gt owners.length 1) "people" "person"}} who would be great owner(s)
for this dataset, based on scanning different systems
</div>
<div class="dataset-authors-suggested__info__trigger" {{action (mut isExpanded) (if isExpanded false true)}}>
<span class="dataset-authors-suggested__info__trigger__action">View Suggestions</span>
{{fa-icon (if isExpanded "chevron-up" "chevron-down")}}
</div>
</section>
{{/if}}
{{#if isExpanded}}
{{#each owners as |owner|}}
{{datasets/owners/suggested-owner-card
owner=owner
ownerTypes=ownerTypes
commonOwners=commonOwners
removeOwner=removeOwner
confirmSuggestedOwner=confirmSuggestedOwner
updateOwnerType=updateOwnerType}}
{{/each}}
{{/if}}

View File

@ -101,7 +101,7 @@
{{/tabs.tabpanel}} {{/tabs.tabpanel}}
{{#tabs.tabpanel tabIds.Ownership}} {{#tabs.tabpanel tabIds.Ownership}}
{{datasets/containers/dataset-ownership urn=encodedUrn}} {{datasets/containers/dataset-ownership urn=encodedUrn showOwnership=showOwnership}}
{{/tabs.tabpanel}} {{/tabs.tabpanel}}
{{#tabs.tabpanel tabIds.Compliance}} {{#tabs.tabpanel tabIds.Compliance}}

View File

@ -8,6 +8,7 @@ import { DatasetPlatform } from 'wherehows-web/constants';
interface IAppConfig { interface IAppConfig {
isInternal: boolean | void; isInternal: boolean | void;
jitAclAccessWhitelist: Array<DatasetPlatform> | void; jitAclAccessWhitelist: Array<DatasetPlatform> | void;
showOwnership: string;
[key: string]: any; [key: string]: any;
} }

View File

@ -0,0 +1,120 @@
import { moduleForComponent, test } from 'ember-qunit';
import hbs from 'htmlbars-inline-precompile';
import { triggerEvent } from 'ember-native-dom-helpers';
import owners from 'wherehows-web/mirage/fixtures/owners';
import { OwnerType } from 'wherehows-web/utils/api/datasets/owners';
import noop from 'wherehows-web/utils/noop';
moduleForComponent(
'datasets/owners/suggested-owner-card',
'Integration | Component | datasets/owners/suggested owner card',
{
integration: true
}
);
const [confirmedOwner, suggestedOwner] = owners;
const ownerTypes = Object.values(OwnerType);
const commonOwners = [confirmedOwner];
const fullNameClass = '.dataset-authors-suggested__card__owner-info__profile__name__full';
const usernameClass = '.dataset-authors-suggested__card__owner-info__profile__name__username';
const addedClass = '.dataset-authors-suggested__card__owner-info__add--disabled';
const addButtonClass = '.nacho-button--secondary.nacho-button--medium';
const sourceClass = '.dataset-authors-suggested__card__source-info';
test('it renders for base and empty cases', function(assert) {
this.setProperties({
commonOwners,
owner: {},
ownerTypes: [],
removeOwner: noop,
confirmSuggestedOwner: noop
});
this.render(hbs`{{datasets/owners/suggested-owner-card
owner=owner
ownerTypes=ownerTypes
commonOwners=commonOwners
removeOwner=removeOwner
confirmSuggestedOwner=confirmSuggestedOwner}}`);
assert.ok(this.$(), 'Renders independently without errors');
assert.ok(this.$(), 'Empty owner does not create breaking error');
});
test('it renders the correct information for suggested owner', function(assert) {
const model = suggestedOwner;
const fullNameText = model.name;
const usernameText = model.userName;
const sourceText = `Source: ${model.source}`;
this.setProperties({
ownerTypes,
commonOwners,
owner: model,
removeOwner: noop,
confirmSuggestedOwner: noop
});
this.render(hbs`{{datasets/owners/suggested-owner-card
owner=owner
ownerTypes=ownerTypes
commonOwners=commonOwners
removeOwner=removeOwner
confirmSuggestedOwner=confirmSuggestedOwner}}`);
assert.ok(this.$(), 'Still renders without errors');
assert.equal(
this.$(fullNameClass)
.text()
.trim(),
fullNameText,
'Renders the name correctly'
);
assert.equal(
this.$(usernameClass)
.text()
.trim(),
usernameText,
'Renders the username correctly'
);
assert.equal(
this.$(sourceClass)
.text()
.trim(),
sourceText,
'Renders the source correctly'
);
assert.equal(this.$(addedClass).length, 0, 'Does not consider suggested owner already added');
assert.equal(this.$(addButtonClass).length, 1, 'Renders add button for suggested class');
});
test('it functions correctly to add a suggested owner', function(assert) {
const model = suggestedOwner;
this.setProperties({
ownerTypes,
commonOwners,
owner: model,
removeOwner: noop,
confirmSuggestedOwner: owner => {
assert.equal(owner.name, model.name, 'Passes the correct information to the confirmOwner function');
}
});
this.render(hbs`{{datasets/owners/suggested-owner-card
owner=owner
ownerTypes=ownerTypes
commonOwners=commonOwners
removeOwner=removeOwner
confirmSuggestedOwner=confirmSuggestedOwner}}`);
assert.ok(this.$(), 'Still renders without errors for real function passed in');
triggerEvent(addButtonClass, 'click');
});

View File

@ -0,0 +1,26 @@
import { moduleForComponent, test } from 'ember-qunit';
import hbs from 'htmlbars-inline-precompile';
moduleForComponent('datasets/owners/suggested-owners', 'Integration | Component | datasets/owners/suggested owners', {
integration: true
});
const descriptionClass = '.dataset-authors-suggested__info__description';
const suggestedCardClass = '.dataset-authors-suggested__card';
test('it renders properly for null case and empty states', function(assert) {
const descriptionText = 'We found no suggested owner(s) for this dataset, based on scanning different systems';
this.render(hbs`{{datasets/owners/suggested-owners}}`);
assert.ok(this.$(), 'Renders without errors when passed no values');
assert.equal(this.$(suggestedCardClass).length, 0, 'Renders no cards');
assert.equal(
this.$(descriptionClass)
.text()
.trim(),
descriptionText,
'Renders the correct message when there are no owners'
);
});