Change preview logic and fix permissions in all pages. Add preview bar when user is on a preview mode

This commit is contained in:
Konstantinos Triantafyllou 2022-12-20 17:40:46 +02:00
parent 8ec038b5b8
commit 670dfe6e0a
10 changed files with 147 additions and 80 deletions

View File

@ -6,17 +6,25 @@
[class.sidebar_mini]="!open && (hasSidebar || hasAdminMenu || hasInternalSidebar)">
<div id="modal-container"></div>
<navbar *ngIf="hasHeader" portal="monitor_dashboard" [header]="menuHeader" [userMenuItems]=userMenuItems [menuItems]="menuItems" [user]="user"></navbar>
<div>
<div *ngIf="!isHidden">
<dashboard-sidebar *ngIf="stakeholder && isFrontPage && hasSidebar && !hasInternalSidebar" queryParamsHandling="merge" [items]="sideBarItems"
[activeItem]="activeTopic?activeTopic.alias:null" [activeSubItem]="activeCategory?activeCategory.alias:null"></dashboard-sidebar>
<dashboard-sidebar *ngIf="hasAdminMenu && !hasInternalSidebar" [items]="adminMenuItems" [specialMenuItem]="specialSideBarMenuItem"></dashboard-sidebar>
<main>
<router-outlet></router-outlet>
</main>
<bottom id="bottom" *ngIf="isFrontPage" [centered]="true" [properties]="properties" [showMenuItems]="true"></bottom>
<role-verification *ngIf="stakeholder" [id]="stakeholder.alias" [name]="stakeholder.name" [type]="stakeholder.type" [userInfoLinkPrefix]="(stakeholder?stakeholder.alias:'')"></role-verification>
<notification-sidebar *ngIf="user && notificationGroupsInitialized" [entities]="entities"
[user]="user" [availableGroups]="notificationGroups" service="monitor"></notification-sidebar>
</div>
<div *ngIf="isHidden" class="private-data uk-light">
<h3 class="uk-position-center">
You are not authorized to see the content of this dashboard.
</h3>
</div>
<div *ngIf="view" class="preview uk-text-small uk-flex uk-flex-middle">
<span>You are currently in a <span class="uk-text-bold">"Preview"</span> mode. <span class="uk-visible@m"><a (click)="removeView()">The current view</a> of this dashboard may differ.</span></span>
</div>
<bottom id="bottom" *ngIf="isFrontPage" [centered]="true" [properties]="properties" [showMenuItems]="true"></bottom>
<role-verification *ngIf="stakeholder" [id]="stakeholder.alias" [name]="stakeholder.name" [type]="stakeholder.type" [userInfoLinkPrefix]="(stakeholder?stakeholder.alias:'')"></role-verification>
<notification-sidebar *ngIf="user && notificationGroupsInitialized" [entities]="entities"
[user]="user" [availableGroups]="notificationGroups" service="monitor"></notification-sidebar>
</div>
</div>

View File

@ -40,7 +40,7 @@ export class AppComponent implements OnInit, OnDestroy {
hasAdminMenu: boolean = false;
hasInternalSidebar: boolean = false;
isFrontPage: boolean = false;
isViewPublic: boolean = false;
view: Visibility;
sideBarItems: MenuItem[] = [];
specialSideBarMenuItem: MenuItem = null;
menuItems: MenuItem[] = [];
@ -55,7 +55,7 @@ export class AppComponent implements OnInit, OnDestroy {
title: "Default menu header",
logoUrl: null,
logoSmallUrl: null,
position: 'center',
position: 'left',
badge: true,
menuPosition: "center"
};
@ -127,7 +127,7 @@ export class AppComponent implements OnInit, OnDestroy {
this.cdr.detectChanges();
}));
this.route.queryParams.subscribe(params => {
this.isViewPublic = (params['view'] == 'public');
this.view = params['view'];
});
this.layoutService.setSmallScreen((this.innerWidth && this.innerWidth <= 640));
this.layoutService.setOpen(!(this.innerWidth && this.innerWidth <= 640));
@ -191,15 +191,15 @@ export class AppComponent implements OnInit, OnDestroy {
setActives(params: Params) {
if (params && params['topic']) {
this.activeTopic = this.stakeholder.topics.find(topic => topic.alias === decodeURIComponent(params['topic']) && this.isPublicOrIsMember(topic.visibility));
this.activeTopic = this.stakeholder.topics.find(topic => topic.alias === decodeURIComponent(params['topic']) && this.hasPermission(topic.visibility));
} else {
this.activeTopic = this.stakeholder.topics.find(topic => this.isPublicOrIsMember(topic.visibility));
this.activeTopic = this.stakeholder.topics.find(topic => this.hasPermission(topic.visibility));
}
if(this.activeTopic) {
if (params && params['category']) {
this.activeCategory = this.activeTopic.categories.find(category => category.alias === decodeURIComponent(params['category']) && this.isPublicOrIsMember(category.visibility));
this.activeCategory = this.activeTopic.categories.find(category => category.alias === decodeURIComponent(params['category']) && this.hasPermission(category.visibility));
} else {
this.activeCategory = this.activeTopic.categories.find(category => this.isPublicOrIsMember(category.visibility));
this.activeCategory = this.activeTopic.categories.find(category => this.hasPermission(category.visibility));
}
}
}
@ -249,15 +249,23 @@ export class AppComponent implements OnInit, OnDestroy {
this.router.navigate([this.properties.errorLink], {queryParams: {'page': this.properties.baseLink + this.router.url}});
}
public removeView() {
this.router.navigate([], {relativeTo: this.route});
}
public get open() {
return this.layoutService.open;
}
get isHidden() {
return this.stakeholder && !this.hasPermission(this.view?this.view:this.stakeholder.visibility);
}
private setSideBar() {
let items: MenuItem[] = [];
if (this.isPublicOrIsMember(this.stakeholder.visibility)) {
if (this.hasPermission(this.view?this.view:this.stakeholder.visibility)) {
this.stakeholder.topics.forEach((topic: Topic) => {
if (this.isPublicOrIsMember(topic.visibility)) {
if (this.hasPermission(topic.visibility)) {
let topicItem: MenuItem = new MenuItem(topic.alias, topic.name, "", '/' + this.stakeholder.alias + '/' + topic.alias,
null, [], [], {}, {svg: topic.icon}, null, null, (
'/' + this.stakeholder.alias + '/' + topic.alias));
@ -304,12 +312,12 @@ export class AppComponent implements OnInit, OnDestroy {
}
if (this.stakeholder) {
this.userMenuItems.push(new MenuItem("", "User information", "", "/" + this.stakeholder.alias + "/user-info", false, [], [], {}));
this.menuItems.push(
new MenuItem("dashboard", "Dashboard",
"", "/" + this.stakeholder.alias, false, [], null, {}
, null, null, null, null)
);
if (this.isPublicOrIsMember(this.stakeholder.visibility)) {
if (this.hasPermission((this.view && this.isManager(this.stakeholder))?this.view:this.stakeholder.visibility)) {
this.menuItems.push(
new MenuItem("dashboard", "Dashboard",
"", "/" + this.stakeholder.alias, false, [], null, {}
, null, null, null, null)
);
this.menuItems.push(
new MenuItem("search", "Browse Data", "", this.properties.searchLinkToResults,
false, [], null, {resultbestaccessright: '"' + encodeURIComponent("Open Access") + '"'},
@ -339,7 +347,7 @@ export class AppComponent implements OnInit, OnDestroy {
logoSmallUrl: StringUtils.getLogoUrl(this.stakeholder),
logoInfo: '<div class="uk-margin-left uk-width-medium"><div class="uk-margin-remove uk-text-background uk-text-bold uk-text-small">Monitor</div>' +
'<div class="uk-h6 uk-text-truncate uk-margin-remove">Dashboard</div></div>',
position: 'center',
position: 'left',
badge: true,
menuPosition: "center"
};
@ -352,7 +360,7 @@ export class AppComponent implements OnInit, OnDestroy {
logoSmallUrl: StringUtils.getLogoUrl(this.stakeholder),
logoInfo: '<div class="uk-margin-left uk-width-medium"><div class="uk-margin-remove uk-text-background uk-text-bold uk-text-small">Monitor Admin</div>' +
'<div class="uk-h6 uk-text-truncate uk-margin-remove">Dashboard</div></div>',
position: 'center',
position: 'left',
badge: true,
menuPosition: "center"
};
@ -403,12 +411,23 @@ export class AppComponent implements OnInit, OnDestroy {
return this.user && (Session.isPortalAdministrator(this.user) || Session.isMonitorCurator(this.user) || Session.isKindOfMonitorManager(this.user));
}
public isMember(stakeholder: Stakeholder) {
return this.user && (Session.isPortalAdministrator(this.user) || Session.isCurator(stakeholder.type, this.user)
|| Session.isManager(stakeholder.type, stakeholder.alias, this.user) || Session.isMember(stakeholder.type, stakeholder.alias, this.user));
}
public isManager(stakeholder: Stakeholder) {
return this.user && (Session.isPortalAdministrator(this.user) || Session.isCurator(stakeholder.type, this.user) || Session.isManager(stakeholder.type, stakeholder.alias, this.user));
}
public isPublicOrIsMember(visibility: Visibility): boolean {
return !(visibility == "PRIVATE" || (this.isViewPublic && visibility != "PUBLIC"));
public hasPermission(visibility: Visibility): boolean {
if(visibility === 'PUBLIC') {
return true;
} else if(visibility === 'RESTRICTED') {
return (!this.view || this.view === 'RESTRICTED') && this.isMember(this.stakeholder);
} else {
return !this.view && this.isManager(this.stakeholder);
}
}
setProperties(id, type = null) {

View File

@ -24,6 +24,7 @@ import {AdminLoginGuard} from "./openaireLibrary/login/adminLoginGuard.guard";
import {AdminDashboardGuard} from "./utils/adminDashboard.guard";
import {NotificationsSidebarModule} from "./openaireLibrary/notifications/notifications-sidebar/notifications-sidebar.module";
import {LoginGuard} from "./openaireLibrary/login/loginGuard.guard";
import {IconsModule} from "./openaireLibrary/utils/icons/icons.module";
@NgModule({
@ -37,10 +38,10 @@ import {LoginGuard} from "./openaireLibrary/login/loginGuard.guard";
NavigationBarModule,
BottomModule,
CookieLawModule,
BrowserModule.withServerTransition({ appId: 'serverApp' }),
BrowserModule.withServerTransition({appId: 'serverApp'}),
AppRoutingModule,
BrowserTransferStateModule,
SideBarModule, Schema2jsonldModule, RoleVerificationModule, LoadingModule, NotificationsSidebarModule
SideBarModule, Schema2jsonldModule, RoleVerificationModule, LoadingModule, NotificationsSidebarModule, IconsModule
],
declarations: [AppComponent, OpenaireErrorPageComponent],
exports: [AppComponent],

View File

@ -46,7 +46,7 @@
<div *ngIf="activeCategory && countSubCategoriesToShow(activeCategory) > 1" class="uk-margin-medium-top" actions>
<ul *ngIf="stakeholder && status === errorCodes.DONE && activeTopic" class="uk-tab uk-margin-remove-bottom">
<ng-template ngFor [ngForOf]="activeCategory.subCategories" let-subCategory>
<li *ngIf="isPublicOrIsMember(subCategory.visibility)"
<li *ngIf="hasPermission(subCategory.visibility)"
[class.uk-active]="subCategory.alias === activeSubCategory.alias">
<a [routerLink]="['/', stakeholder.alias, activeTopic.alias, activeCategory.alias, subCategory.alias]"
[queryParams]="queryParams">
@ -97,7 +97,7 @@
uk-height-match="target: .uk-card">
<h5 *ngIf="number.title" class="uk-width-1-1 uk-margin-bottom">{{number.title}}</h5>
<ng-template ngFor [ngForOf]="number.indicators" let-indicator let-j="index">
<div *ngIf="isPublicOrIsMember(indicator.visibility)" [ngClass]="getNumberClassBySize(indicator.width)">
<div *ngIf="hasPermission(indicator.visibility)" [ngClass]="getNumberClassBySize(indicator.width)">
<div class="uk-card uk-card-default uk-padding-small number-card uk-position-relative"
[class.semiFiltered]="indicator.indicatorPaths[0].filtersApplied < countSelectedFilters()">
<div *ngIf="!showDescriptionOverlay[j]">
@ -145,7 +145,7 @@
uk-height-match="target: .uk-card">
<h5 *ngIf="chart.title" class="uk-width-1-1 uk-margin-bottom">{{chart.title}}</h5>
<ng-template ngFor [ngForOf]="chart.indicators" let-indicator let-j="index">
<div *ngIf="isPublicOrIsMember(indicator.visibility) && chartsActiveType.get(i + '-' + j)"
<div *ngIf="hasPermission(indicator.visibility) && chartsActiveType.get(i + '-' + j)"
[ngClass]="getChartClassBySize(indicator.width)">
<div class="uk-card uk-card-default uk-position-relative"
[class.semiFiltered]="chartsActiveType.get(i + '-' + j).filtersApplied < countSelectedFilters()">
@ -226,7 +226,7 @@
<ng-container *ngTemplateOutlet="graph_and_feedback_template"></ng-container>
</div>
<ng-template #graph_and_feedback_template>
<div class="uk-margin-small-top uk-margin-small-bottom uk-text-small uk-flex hideInfo">
<div *ngIf="!view" class="uk-margin-small-top uk-margin-small-bottom uk-text-small uk-flex hideInfo">
<!-- Last Stats Date-->
<div class="uk-width-2-3@m uk-width-1-2">
<icon name="graph" customClass="text-graph"></icon>

View File

@ -23,7 +23,7 @@ import {IndicatorUtils, StakeholderUtils} from "../utils/indicator-utils";
import {LayoutService} from "../openaireLibrary/dashboard/sharedComponents/sidebar/layout.service";
import {UntypedFormBuilder, UntypedFormControl} from "@angular/forms";
import {Subscriber, Subscription} from "rxjs";
import {User} from "../openaireLibrary/login/utils/helper.class";
import {Session, User} from "../openaireLibrary/login/utils/helper.class";
import {UserManagementService} from "../openaireLibrary/services/user-management.service";
import {RangeFilter} from "../openaireLibrary/utils/rangeFilter/rangeFilterHelperClasses.class";
import {Filter, Value} from "../openaireLibrary/searchPages/searchUtils/searchHelperClasses.class";
@ -49,7 +49,7 @@ export class MonitorComponent implements OnInit, OnDestroy {
public divContents = null;
public status: number;
public loading: boolean = true;
public isViewPublic: boolean = false;
public view: Visibility;
public indicatorUtils: IndicatorUtils = new IndicatorUtils();
public stakeholderUtils: StakeholderUtils = new StakeholderUtils();
public activeTopic: Topic = null;
@ -172,36 +172,36 @@ export class MonitorComponent implements OnInit, OnDestroy {
}
this.subscriptions.push(this.route.queryParams.subscribe( queryParams => {
this.handleQueryParams(queryParams, params);
}));
this.seoService.createLinkForCanonicalURL(url, false);
this._meta.updateTag({content: url}, "property='og:url'");
this.description = "Monitor Dashboard | " + this.stakeholder.name;
this.title = "Monitor Dashboard | " + this.stakeholder.name;
this._meta.updateTag({content: this.description}, "name='description'");
this._meta.updateTag({content: this.description}, "property='og:description'");
this._meta.updateTag({content: this.title}, "property='og:title'");
this._title.setTitle(this.title);
if (this.properties.enablePiwikTrack && (typeof document !== 'undefined')) {
this.subscriptions.push(this.configurationService.communityInformationState.subscribe(portal => {
if (portal && portal.piwik) {
this.piwikSiteId = portal.piwik;
this.subscriptions.push(this._piwikService.trackView(this.properties, this.title, this.piwikSiteId).subscribe());
}
}));
}
if (this.isPublicOrIsMember(stakeholder.visibility)) {
//this.getDivContents();
// this.getPageContents();
this.status = this.errorCodes.DONE;
this.setView(params);
} else {
this.privateStakeholder = true;
// this.navigateToError();
if (subscription) {
subscription.unsubscribe();
this.seoService.createLinkForCanonicalURL(url, false);
this._meta.updateTag({content: url}, "property='og:url'");
this.description = "Monitor Dashboard | " + this.stakeholder.name;
this.title = "Monitor Dashboard | " + this.stakeholder.name;
this._meta.updateTag({content: this.description}, "name='description'");
this._meta.updateTag({content: this.description}, "property='og:description'");
this._meta.updateTag({content: this.title}, "property='og:title'");
this._title.setTitle(this.title);
if (this.properties.enablePiwikTrack && (typeof document !== 'undefined')) {
this.subscriptions.push(this.configurationService.communityInformationState.subscribe(portal => {
if (portal && portal.piwik) {
this.piwikSiteId = portal.piwik;
this.subscriptions.push(this._piwikService.trackView(this.properties, this.title, this.piwikSiteId).subscribe());
}
}));
}
}
if (this.hasPermission((this.view && this.isManager(this.stakeholder))?this.view:this.stakeholder.visibility)) {
//this.getDivContents();
// this.getPageContents();
this.status = this.errorCodes.DONE;
this.setView(params);
} else {
this.privateStakeholder = true;
// this.navigateToError();
if (subscription) {
subscription.unsubscribe();
}
}
}));
} else {
this.navigateToError();
if (subscription) {
@ -239,7 +239,7 @@ export class MonitorComponent implements OnInit, OnDestroy {
this.router.navigate([], {queryParams: {}});
}
}
this.isViewPublic = (queryParams['view'] == 'public');
this.view = queryParams['view'];
}
private initializeFilters() {
@ -346,20 +346,20 @@ export class MonitorComponent implements OnInit, OnDestroy {
private setView(params: Params) {
this.loading = false;
if (params['topic']) {
this.activeTopic = this.stakeholder.topics.find(topic => topic.alias === decodeURIComponent(params['topic']) && this.isPublicOrIsMember(topic.visibility));
this.activeTopic = this.stakeholder.topics.find(topic => topic.alias === decodeURIComponent(params['topic']) && this.hasPermission(topic.visibility));
if (this.activeTopic) {
if (params['category']) {
this.activeCategory = this.activeTopic.categories.find(category =>
(category.alias === params['category']) && this.isPublicOrIsMember(category.visibility));
(category.alias === params['category']) && this.hasPermission(category.visibility));
if (!this.activeCategory) {
this.navigateToError();
return;
}
} else {
this.activeCategory = this.activeTopic.categories.find(category => this.isPublicOrIsMember(category.visibility));
this.activeCategory = this.activeTopic.categories.find(category => this.hasPermission(category.visibility));
if (this.activeCategory) {
this.activeSubCategory = this.activeCategory.subCategories.find(subCategory =>
this.isPublicOrIsMember(subCategory.visibility));
this.hasPermission(subCategory.visibility));
if (this.activeSubCategory) {
this.setIndicators();
}
@ -369,14 +369,14 @@ export class MonitorComponent implements OnInit, OnDestroy {
if (this.activeCategory) {
if (params['subCategory']) {
this.activeSubCategory = this.activeCategory.subCategories.find(subCategory =>
(subCategory.alias === params['subCategory'] && this.isPublicOrIsMember(subCategory.visibility)));
(subCategory.alias === params['subCategory'] && this.hasPermission(subCategory.visibility)));
if (!this.activeSubCategory) {
this.navigateToError();
return;
}
} else {
this.activeSubCategory = this.activeCategory.subCategories.find(subCategory =>
this.isPublicOrIsMember(subCategory.visibility));
this.hasPermission(subCategory.visibility));
}
if (this.activeSubCategory) {
this.setIndicators();
@ -392,11 +392,11 @@ export class MonitorComponent implements OnInit, OnDestroy {
return;
}
} else {
this.activeTopic = this.stakeholder.topics.find(topic => this.isPublicOrIsMember(topic.visibility));
this.activeTopic = this.stakeholder.topics.find(topic => this.hasPermission(topic.visibility));
if (this.activeTopic) {
this.activeCategory = this.activeTopic.categories.find(category => this.isPublicOrIsMember(category.visibility));
this.activeCategory = this.activeTopic.categories.find(category => this.hasPermission(category.visibility));
if (this.activeCategory) {
this.activeSubCategory = this.activeCategory.subCategories.find(subCategory => this.isPublicOrIsMember(subCategory.visibility));
this.activeSubCategory = this.activeCategory.subCategories.find(subCategory => this.hasPermission(subCategory.visibility));
if (this.activeSubCategory) {
this.setIndicators();
}
@ -472,7 +472,7 @@ export class MonitorComponent implements OnInit, OnDestroy {
let urls: Map<string, [number, number][]> = new Map<string, [number, number][]>();
this.activeSubCategory.numbers.forEach((section, i) => {
section.indicators.forEach((number, j) => {
if (this.isPublicOrIsMember(number.visibility)) {
if (this.hasPermission(number.visibility)) {
let url = this.indicatorUtils.getFullUrlWithFilters(this.stakeholder, number.indicatorPaths[0], this.getfl0(), this.periodFilter.selectedFromValue, this.periodFilter.selectedToValue, this.getCoFunded());
const pair = JSON.stringify([number.indicatorPaths[0].source, url]);
const indexes = urls.get(pair) ? urls.get(pair) : [];
@ -543,12 +543,27 @@ export class MonitorComponent implements OnInit, OnDestroy {
});
}
public isPublicOrIsMember(visibility: Visibility): boolean {
return !(visibility == "PRIVATE" || (this.isViewPublic && visibility != "PUBLIC"));
public isMember(stakeholder: Stakeholder) {
return this.user && (Session.isPortalAdministrator(this.user) || Session.isCurator(stakeholder.type, this.user)
|| Session.isManager(stakeholder.type, stakeholder.alias, this.user) || Session.isMember(stakeholder.type, stakeholder.alias, this.user));
}
public isManager(stakeholder: Stakeholder) {
return this.user && (Session.isPortalAdministrator(this.user) || Session.isCurator(stakeholder.type, this.user) || Session.isManager(stakeholder.type, stakeholder.alias, this.user));
}
public hasPermission(visibility: Visibility): boolean {
if(visibility === 'PUBLIC') {
return true;
} else if(visibility === 'RESTRICTED') {
return (!this.view || this.view === 'RESTRICTED') && this.isMember(this.stakeholder);
} else {
return !this.view && this.isManager(this.stakeholder);
}
}
public countSubCategoriesToShow(category: Category): number {
return category.subCategories.filter(subCategory => this.isPublicOrIsMember(subCategory.visibility)).length;
return category.subCategories.filter(subCategory => this.hasPermission(subCategory.visibility)).length;
}
public countSectionsWithIndicatorsToShow(sections: Section[]):number {
@ -556,7 +571,7 @@ export class MonitorComponent implements OnInit, OnDestroy {
}
public countIndicatorsToShow(indicators: Indicator[]): number {
return indicators.filter(indicator => this.isPublicOrIsMember(indicator.visibility)).length;
return indicators.filter(indicator => this.hasPermission(indicator.visibility)).length;
}
public get feedback() {

@ -1 +1 @@
Subproject commit 06f2f586a88a1766f183356b6497c0fc66436f36
Subproject commit 10130d368a1e3c72e6785fe2291234529040bf84

View File

@ -35,7 +35,6 @@ export class MonitorPublicationComponent {
this.communityId = stakeholder.alias;
this.subscriptions.push(this.configurationService.communityInformationState.subscribe(portal => {
if (portal) {
console.debug(portal)
this.piwikSiteId = portal.piwik;
}
}));

View File

@ -310,12 +310,12 @@
<ul class="uk-nav uk-dropdown-nav">
<li><a target="_blank"
[routerLink]="'/' + stakeholder.alias + '/' + stakeholder.topics[topicIndex].alias"
[queryParams]="{view: 'public'}"
[queryParams]="{view: 'PUBLIC'}"
(click)="hide(element)">Public view</a>
</li>
<li><a target="_blank" [routerLink]="'/' + stakeholder.alias + '/' +
stakeholder.topics[topicIndex].alias"
[queryParams]="{view: 'restricted'}"
[queryParams]="{view: 'RESTRICTED'}"
(click)="hide(element)">Restricted view</a>
</li>
<!--<li class="disabled"><a class="uk-disabled uk-text-muted"

View File

@ -64,6 +64,31 @@
& #filters_icon .end {
stop-color: @monitor-dark-color;
}
.private-data {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-image: url("private-overlay.png");
background-repeat: no-repeat;
background-size: cover;
z-index: 0;
}
.preview {
position: fixed;
bottom: 40px;
left: 50%;
transform: translateX(-50%);
background: @global-inverse-color;
border: 2px solid @background-primary-background;
border-radius: @global-border-radius;
box-shadow: @global-large-box-shadow;
padding: 20px 25px;
z-index: @global-z-index;
}
}
#print_toggle {

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB