mirror of
https://github.com/datahub-project/datahub.git
synced 2025-10-15 10:57:58 +00:00
Owner tab UX revamp - add new suggested owners component to handle system generated suggestions
This commit is contained in:
parent
5b56dc51c0
commit
c19ff27b5a
@ -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 = [];
|
||||
}
|
@ -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);
|
||||
});
|
||||
}
|
@ -66,6 +66,13 @@ export default class DatasetController extends Controller {
|
||||
*/
|
||||
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
|
||||
* @type {boolean}
|
||||
|
@ -92,18 +92,20 @@ export default class DatasetRoute extends Route {
|
||||
|
||||
async setupController(this: DatasetRoute, controller: DatasetController, model: IDatasetView) {
|
||||
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
|
||||
const isInternal = <IAppConfig['isInternal']>get(this, 'configurator').getConfig<IAppConfig['isInternal']>(
|
||||
'isInternal'
|
||||
const isInternal = <IAppConfig['isInternal']>configuratorService.getConfig<IAppConfig['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, {
|
||||
isInternal: !!isInternal,
|
||||
jitAclAccessWhitelist: jitAclAccessWhitelist || []
|
||||
jitAclAccessWhitelist: jitAclAccessWhitelist || [],
|
||||
showOwnership
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,2 +1,3 @@
|
||||
@import 'owner-table';
|
||||
@import 'dataset-author';
|
||||
@import 'suggested-owners';
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -84,59 +84,71 @@
|
||||
|
||||
{{#if systemGeneratedOwners}}
|
||||
|
||||
<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 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
|
||||
{{#if (eq showOwnership "show")}}
|
||||
<section class="dataset-author">
|
||||
{{datasets/owners/suggested-owners
|
||||
owners=systemGeneratedOwners
|
||||
ownerTypes=ownerTypes
|
||||
commonOwners=commonOwners
|
||||
removeOwner=(action "removeOwner")
|
||||
confirmSuggestedOwner=(action "confirmSuggestedOwner")
|
||||
updateOwnerType=(action "updateOwnerType")
|
||||
}}
|
||||
{{/each}}
|
||||
</tbody>
|
||||
confirmSuggestedOwner=(action "confirmSuggestedOwner")
|
||||
updateOwnerType=(action "updateOwnerType")}}
|
||||
</section>
|
||||
{{else}}
|
||||
|
||||
</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 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}}
|
||||
|
||||
<section class="action-bar">
|
||||
|
@ -33,6 +33,7 @@
|
||||
{{dataset-authors
|
||||
owners=owners
|
||||
ownerTypes=ownerTypes
|
||||
showOwnership=showOwnership
|
||||
save=(action "saveOwnerChanges")
|
||||
}}
|
||||
|
||||
|
@ -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>
|
@ -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}}
|
@ -101,7 +101,7 @@
|
||||
{{/tabs.tabpanel}}
|
||||
|
||||
{{#tabs.tabpanel tabIds.Ownership}}
|
||||
{{datasets/containers/dataset-ownership urn=encodedUrn}}
|
||||
{{datasets/containers/dataset-ownership urn=encodedUrn showOwnership=showOwnership}}
|
||||
{{/tabs.tabpanel}}
|
||||
|
||||
{{#tabs.tabpanel tabIds.Compliance}}
|
||||
|
@ -8,6 +8,7 @@ import { DatasetPlatform } from 'wherehows-web/constants';
|
||||
interface IAppConfig {
|
||||
isInternal: boolean | void;
|
||||
jitAclAccessWhitelist: Array<DatasetPlatform> | void;
|
||||
showOwnership: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
|
@ -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');
|
||||
});
|
@ -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'
|
||||
);
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user