16 changed files with 710 additions and 16 deletions
@ -0,0 +1,106 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2023 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<div class="tb-card-content" fxLayout="column" fxLayoutGap="8px"> |
|||
<div class="tb-card-header"> |
|||
<a class="tb-title-link" routerLink="/dashboards">{{ 'widgets.recent-dashboards.title' | translate }}</a> |
|||
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
|||
<tb-toggle-header #dashboardsToggle [value]="toggleValue" name="usageToggle" (valueChange)="toggleValueChange($event)" [options]="[ |
|||
{ |
|||
name: ctx.translate.instant('widgets.recent-dashboards.last'), |
|||
value: 'last' |
|||
}, |
|||
{ |
|||
name: ctx.translate.instant('widgets.recent-dashboards.starred'), |
|||
value: 'starred' |
|||
} |
|||
]"> |
|||
</tb-toggle-header> |
|||
<a *ngIf="authUser.authority === authority.TENANT_ADMIN" fxHide.md |
|||
mat-flat-button color="primary" routerLink="/dashboards" [queryParams]="{action: 'add'}">{{ 'dashboard.add' | translate }}</a> |
|||
</div> |
|||
</div> |
|||
<ng-container *ngIf="userDashboardsInfo; else loading" [ngSwitch]="dashboardsToggle.value"> |
|||
<ng-template [ngSwitchCase]="'last'"> |
|||
<div *ngIf="hasLastVisitedDashboards(); else noLastVisitedDashboards" style="overflow-y: auto;"> |
|||
<table mat-table |
|||
[dataSource]="lastVisitedDashboardsDataSource" matSort |
|||
[matSortActive]="lastVisitedDashboardsPageLink.sortOrder?.property" |
|||
[matSortDirection]="lastVisitedDashboardsPageLink.sortDirection()" matSortDisableClear> |
|||
<ng-container matColumnDef="starred"> |
|||
<mat-header-cell class="star-cell" *matHeaderCellDef> |
|||
</mat-header-cell> |
|||
<mat-cell class="star-cell" *matCellDef="let lastVisitedDashboard"> |
|||
<mat-icon (click)="toggleDashboardStar(lastVisitedDashboard)" |
|||
class="star" [ngClass]="{'starred': lastVisitedDashboard.starred}">{{ lastVisitedDashboard.starred ? 'star' : 'star_border' }}</mat-icon> |
|||
</mat-cell> |
|||
</ng-container> |
|||
<ng-container matColumnDef="title"> |
|||
<mat-header-cell class="title" *matHeaderCellDef mat-sort-header> |
|||
{{ 'widgets.recent-dashboards.name' | translate }} |
|||
</mat-header-cell> |
|||
<mat-cell class="title" *matCellDef="let lastVisitedDashboard"> |
|||
<a routerLink="/dashboards/{{lastVisitedDashboard.id}}">{{ lastVisitedDashboard.title }}</a> |
|||
</mat-cell> |
|||
</ng-container> |
|||
<ng-container matColumnDef="lastVisited"> |
|||
<mat-header-cell class="last-visited" *matHeaderCellDef mat-sort-header> |
|||
{{ 'widgets.recent-dashboards.last-viewed' | translate }} |
|||
</mat-header-cell> |
|||
<mat-cell class="last-visited" *matCellDef="let lastVisitedDashboard"> |
|||
{{ lastVisitedDashboard.lastVisited | dateAgo:{applyAgo: true} }} |
|||
</mat-cell> |
|||
</ng-container> |
|||
<mat-header-row *matHeaderRowDef="lastVisitedDashboardsColumns; sticky: true"></mat-header-row> |
|||
<mat-row *matRowDef="let lastVisitedDashboard; columns: lastVisitedDashboardsColumns;"></mat-row> |
|||
</table> |
|||
</div> |
|||
<ng-template #noLastVisitedDashboards> |
|||
<div class="tb-no-dashboards"> |
|||
<div class="tb-no-last-visited-bg"></div> |
|||
<div class="tb-no-last-visited-dashboards-text">{{ 'widgets.recent-dashboards.no-last-viewed-dashboards' | translate }}</div> |
|||
</div> |
|||
</ng-template> |
|||
</ng-template> |
|||
<ng-template [ngSwitchCase]="'starred'"> |
|||
<div style="overflow-y: auto;"> |
|||
<div class="tb-starred-dashboard-row" *ngFor="let dashboard of userDashboardsInfo?.starred"> |
|||
<div class="tb-cell star-cell"> |
|||
<mat-icon (click)="toggleDashboardStar(dashboard)" |
|||
class="star" [ngClass]="{'starred': dashboard.starred}">{{ dashboard.starred ? 'star' : 'star_border' }}</mat-icon> |
|||
</div> |
|||
<div class="tb-cell title"> |
|||
<a routerLink="/dashboards/{{dashboard.id}}">{{ dashboard.title }}</a> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<tb-dashboard-autocomplete class="tb-star-dashboard-autocomplete" |
|||
#starDashboardAutocomplete |
|||
subscriptSizing="dynamic" |
|||
appearance="outline" |
|||
[useIdValue]="false" |
|||
label="" |
|||
placeholder="{{ 'dashboard.select-dashboard' | translate }}" |
|||
[(ngModel)]="starredDashboardValue" (ngModelChange)="onStarDashboard($event)"></tb-dashboard-autocomplete> |
|||
</ng-template> |
|||
</ng-container> |
|||
<ng-template #loading> |
|||
<div class="tb-no-dashboards"> |
|||
<mat-spinner [diameter]="50" mode="indeterminate"></mat-spinner> |
|||
</div> |
|||
</ng-template> |
|||
</div> |
|||
@ -0,0 +1,235 @@ |
|||
/** |
|||
* Copyright © 2016-2023 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
@import "../../../../../../../scss/constants"; |
|||
|
|||
:host { |
|||
.tb-card-header { |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
} |
|||
.tb-no-dashboards { |
|||
flex: 1; |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
justify-content: center; |
|||
} |
|||
.tb-no-last-visited-bg { |
|||
width: 60px; |
|||
height: 60px; |
|||
margin: 10px; |
|||
position: relative; |
|||
&:before { |
|||
content: ""; |
|||
border-radius: 8px; |
|||
position: absolute; |
|||
top: 0; |
|||
left: 0; |
|||
right: 0; |
|||
bottom: 0; |
|||
background: #305680; |
|||
-webkit-mask-image: url(/assets/home/no_data_bg.svg); |
|||
-webkit-mask-repeat: no-repeat; |
|||
mask-image: url(/assets/home/no_data_bg.svg); |
|||
mask-repeat: no-repeat; |
|||
} |
|||
} |
|||
.tb-no-last-visited-dashboards-text { |
|||
font-weight: 500; |
|||
font-size: 14px; |
|||
line-height: 20px; |
|||
letter-spacing: 0.25px; |
|||
color: rgba(0, 0, 0, 0.54); |
|||
} |
|||
|
|||
.tb-starred-dashboard-row { |
|||
height: 40px; |
|||
display: flex; |
|||
flex-direction: row; |
|||
align-items: center; |
|||
@media #{$mat-md-lg} { |
|||
height: 34px; |
|||
} |
|||
} |
|||
|
|||
.tb-cell { |
|||
box-sizing: content-box; |
|||
&:first-child { |
|||
padding: 0 12px; |
|||
} |
|||
&:nth-child(n+2):nth-last-child(n+2) { |
|||
padding: 0 28px 0 0; |
|||
} |
|||
} |
|||
|
|||
.tb-star-dashboard-autocomplete { |
|||
margin-left: 64px; |
|||
margin-right: 64px; |
|||
@media #{$mat-md-lg} { |
|||
margin-left: 48px; |
|||
margin-right: 48px; |
|||
} |
|||
} |
|||
} |
|||
|
|||
:host ::ng-deep { |
|||
.mat-mdc-cell, .mat-mdc-header-cell, .mdc-data-table__row:last-child .mdc-data-table__cell { |
|||
border-bottom: none; |
|||
} |
|||
|
|||
.mat-mdc-table { |
|||
.mdc-data-table__row, .mdc-data-table__header-row { |
|||
height: 40px; |
|||
@media #{$mat-md-lg} { |
|||
height: 34px; |
|||
} |
|||
|
|||
&:hover { |
|||
background-color: inherit; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.tb-cell:nth-child(n+2):nth-last-child(n+2) { |
|||
padding: 0 28px 0 0; |
|||
} |
|||
|
|||
.mat-mdc-row:not(.mat-row-select) .mat-mdc-cell:nth-child(n+2):nth-last-child(n+2) { |
|||
@media #{$mat-md-lg} { |
|||
padding: 0 12px 0 0; |
|||
} |
|||
} |
|||
|
|||
.mat-mdc-header-cell, .mat-mdc-cell, .tb-cell { |
|||
&.star-cell { |
|||
width: 40px; |
|||
min-width: 40px; |
|||
@media #{$mat-md-lg} { |
|||
width: 24px; |
|||
min-width: 24px; |
|||
max-width: 24px; |
|||
} |
|||
} |
|||
|
|||
&.title { |
|||
width: 100%; |
|||
} |
|||
|
|||
&.last-visited { |
|||
width: 80px; |
|||
@media #{$mat-md-lg} { |
|||
width: 50px; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.mat-mdc-header-cell { |
|||
font-weight: 400; |
|||
font-size: 12px; |
|||
line-height: 16px; |
|||
letter-spacing: 0.25px; |
|||
color: rgba(0, 0, 0, 0.38); |
|||
@media #{$mat-md-lg} { |
|||
font-size: 11px; |
|||
} |
|||
} |
|||
.mat-mdc-cell.title { |
|||
max-width: 0; |
|||
} |
|||
.mat-mdc-cell, .tb-cell { |
|||
font-weight: 400; |
|||
font-size: 14px; |
|||
line-height: 20px; |
|||
letter-spacing: 0.2px; |
|||
@media #{$mat-md-lg} { |
|||
font-size: 11px; |
|||
line-height: 16px; |
|||
} |
|||
&.title { |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
white-space: nowrap; |
|||
a { |
|||
color: rgba(0, 0, 0, 0.87); |
|||
border-bottom: none; |
|||
&:hover, &:focus { |
|||
border-bottom: none; |
|||
} |
|||
} |
|||
} |
|||
&.last-visited { |
|||
cursor: default; |
|||
color: rgba(0, 0, 0, 0.54); |
|||
} |
|||
.star { |
|||
cursor: pointer; |
|||
vertical-align: middle; |
|||
color: rgba(0, 0, 0, 0.12); |
|||
&:hover { |
|||
color: rgba(0, 0, 0, 0.18); |
|||
} |
|||
&.starred { |
|||
color: #FAE205; |
|||
&:hover { |
|||
color: #CDBD2C; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
.tb-star-dashboard-autocomplete { |
|||
.mat-mdc-form-field { |
|||
font-weight: 400; |
|||
font-size: 14px; |
|||
line-height: 20px; |
|||
letter-spacing: 0.2px; |
|||
color: rgba(0, 0, 0, 0.54); |
|||
@media #{$mat-md-lg} { |
|||
font-size: 11px; |
|||
line-height: 16px; |
|||
} |
|||
} |
|||
.mat-mdc-text-field-wrapper.mdc-text-field--outlined .mat-mdc-form-field-infix { |
|||
padding-top: 4px; |
|||
padding-bottom: 4px; |
|||
min-height: 28px; |
|||
@media #{$mat-md-lg} { |
|||
min-height: 24px; |
|||
} |
|||
} |
|||
.mat-mdc-form-field-hint-wrapper { |
|||
height: 0; |
|||
} |
|||
.mdc-text-field--outlined .mdc-notched-outline { |
|||
.mdc-notched-outline__leading, .mdc-notched-outline__trailing { |
|||
border-top-style: dashed; |
|||
border-bottom-style: dashed; |
|||
} |
|||
.mdc-notched-outline__leading { |
|||
border-left-style: dashed; |
|||
} |
|||
.mdc-notched-outline__trailing { |
|||
border-right-style: dashed; |
|||
} |
|||
} |
|||
.mat-mdc-icon-button.mat-mdc-button-base { |
|||
width: 28px; |
|||
height: 28px; |
|||
padding: 2px; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,222 @@ |
|||
///
|
|||
/// Copyright © 2016-2023 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { |
|||
AfterViewInit, |
|||
ChangeDetectorRef, |
|||
Component, |
|||
Input, |
|||
OnDestroy, |
|||
OnInit, |
|||
QueryList, ViewChild, |
|||
ViewChildren |
|||
} from '@angular/core'; |
|||
import { PageComponent } from '@shared/components/page.component'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { Authority } from '@shared/models/authority.enum'; |
|||
import { BehaviorSubject, Observable, of } from 'rxjs'; |
|||
import { getCurrentAuthUser } from '@core/auth/auth.selectors'; |
|||
import { WidgetContext } from '@home/models/widget-component.models'; |
|||
import { |
|||
AbstractUserDashboardInfo, |
|||
LastVisitedDashboardInfo, StarredDashboardInfo, |
|||
UserDashboardAction, |
|||
UserDashboardsInfo |
|||
} from '@shared/models/user-settings.models'; |
|||
import { UserSettingsService } from '@core/http/user-settings.service'; |
|||
import { CollectionViewer, DataSource } from '@angular/cdk/collections'; |
|||
import { emptyPageData, PageData } from '@shared/models/page/page-data'; |
|||
import { MAX_SAFE_PAGE_SIZE, PageLink } from '@shared/models/page/page-link'; |
|||
import { map, share } from 'rxjs/operators'; |
|||
import { Direction, SortOrder } from '@shared/models/page/sort-order'; |
|||
import { MatSort } from '@angular/material/sort'; |
|||
import { DashboardInfo } from '@shared/models/dashboard.models'; |
|||
import { DashboardAutocompleteComponent } from '@shared/components/dashboard-autocomplete.component'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-recent-dashboards-widget', |
|||
templateUrl: './recent-dashboards-widget.component.html', |
|||
styleUrls: ['./home-page-widget.scss', './recent-dashboards-widget.component.scss'] |
|||
}) |
|||
export class RecentDashboardsWidgetComponent extends PageComponent implements OnInit, AfterViewInit, OnDestroy { |
|||
|
|||
@Input() |
|||
ctx: WidgetContext; |
|||
|
|||
@ViewChildren(MatSort) lastVisitedDashboardsSort: QueryList<MatSort>; |
|||
|
|||
@ViewChild('starDashboardAutocomplete', {static: false}) |
|||
starDashboardAutocomplete: DashboardAutocompleteComponent; |
|||
|
|||
authority = Authority; |
|||
|
|||
userDashboardsInfo: UserDashboardsInfo; |
|||
authUser = getCurrentAuthUser(this.store); |
|||
|
|||
toggleValue: 'last' | 'starred' = 'last'; |
|||
|
|||
lastVisitedDashboardsColumns = ['starred', 'title', 'lastVisited']; |
|||
lastVisitedDashboardsDataSource: LastVisitedDashboardsDataSource; |
|||
lastVisitedDashboardsPageLink: PageLink; |
|||
|
|||
starredDashboardValue = null; |
|||
|
|||
dirty = false; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
private cd: ChangeDetectorRef, |
|||
private userSettingService: UserSettingsService) { |
|||
super(store); |
|||
} |
|||
|
|||
ngOnInit() { |
|||
this.reload(); |
|||
} |
|||
|
|||
reload() { |
|||
this.userDashboardsInfo = null; |
|||
this.cd.markForCheck(); |
|||
(this.authUser.authority !== Authority.SYS_ADMIN ? |
|||
this.userSettingService.getUserDashboardsInfo() : of({last: [], starred: []})).subscribe( |
|||
(userDashboardsInfo) => { |
|||
this.userDashboardsInfo = userDashboardsInfo; |
|||
for (const starredDashboard of this.userDashboardsInfo?.starred) { |
|||
starredDashboard.starred = true; |
|||
} |
|||
this.userDashboardsInfo?.starred.sort((a, b) => a.starredAt - b.starredAt); |
|||
if (this.hasLastVisitedDashboards()) { |
|||
this.initLastVisitedDashboardsDataSource(); |
|||
} |
|||
this.cd.markForCheck(); |
|||
} |
|||
); |
|||
} |
|||
|
|||
toggleValueChange(value: 'last' | 'starred') { |
|||
this.toggleValue = value; |
|||
if (this.dirty) { |
|||
this.dirty = false; |
|||
this.reload(); |
|||
} else { |
|||
if (value === 'last' && this.hasLastVisitedDashboards()) { |
|||
this.initLastVisitedDashboardsDataSource(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private initLastVisitedDashboardsDataSource() { |
|||
this.lastVisitedDashboardsDataSource = new LastVisitedDashboardsDataSource(this.userDashboardsInfo.last); |
|||
const sortOrder: SortOrder = { |
|||
property: 'lastVisited', |
|||
direction: Direction.DESC |
|||
}; |
|||
this.lastVisitedDashboardsPageLink = new PageLink(MAX_SAFE_PAGE_SIZE, 0, null, sortOrder); |
|||
this.lastVisitedDashboardsDataSource.loadData(this.lastVisitedDashboardsPageLink); |
|||
} |
|||
|
|||
ngAfterViewInit() { |
|||
this.lastVisitedDashboardsSort.changes.subscribe(() => { |
|||
if (this.lastVisitedDashboardsSort.length) { |
|||
this.lastVisitedDashboardsSort.get(0).sortChange.subscribe(() => |
|||
this.updateLastVisitedDashboardsData(this.lastVisitedDashboardsSort.get(0))); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
updateLastVisitedDashboardsData(sort: MatSort) { |
|||
this.lastVisitedDashboardsPageLink.sortOrder.property = sort.active; |
|||
this.lastVisitedDashboardsPageLink.sortOrder.direction = Direction[sort.direction.toUpperCase()]; |
|||
this.lastVisitedDashboardsDataSource.loadData(this.lastVisitedDashboardsPageLink); |
|||
} |
|||
|
|||
hasLastVisitedDashboards(): boolean { |
|||
return !!(this.userDashboardsInfo && this.userDashboardsInfo.last && this.userDashboardsInfo.last.length); |
|||
} |
|||
|
|||
toggleDashboardStar(dashboard: AbstractUserDashboardInfo): void { |
|||
const action: UserDashboardAction = dashboard.starred ? UserDashboardAction.UNSTAR : UserDashboardAction.STAR; |
|||
dashboard.starred = !dashboard.starred; |
|||
this.userSettingService.reportUserDashboardAction(dashboard.id, action, {ignoreLoading: true}).subscribe(); |
|||
this.dirty = true; |
|||
if (this.toggleValue === 'starred') { |
|||
const index = this.userDashboardsInfo.starred.findIndex((d) => d.id === dashboard.id); |
|||
if (index > -1) { |
|||
this.userDashboardsInfo.starred.splice(index, 1); |
|||
this.cd.markForCheck(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
onStarDashboard(dashboard: DashboardInfo) { |
|||
if (dashboard) { |
|||
this.starDashboardAutocomplete.clear(); |
|||
const index = this.userDashboardsInfo.starred.findIndex((d) => d.id === dashboard.id.id); |
|||
if (index === -1) { |
|||
const starredDashboard: StarredDashboardInfo = { |
|||
starredAt: Date.now(), |
|||
id: dashboard.id.id, |
|||
starred: true, |
|||
title: dashboard.title |
|||
}; |
|||
this.userDashboardsInfo.starred.push(starredDashboard); |
|||
this.userSettingService.reportUserDashboardAction(dashboard.id.id, UserDashboardAction.STAR, {ignoreLoading: true}).subscribe(); |
|||
} |
|||
this.cd.markForCheck(); |
|||
} |
|||
} |
|||
|
|||
} |
|||
|
|||
export class LastVisitedDashboardsDataSource implements DataSource<LastVisitedDashboardInfo> { |
|||
|
|||
private lastVisitedDashboardsSubject = new BehaviorSubject<LastVisitedDashboardInfo[]>([]); |
|||
private pageDataSubject = new BehaviorSubject<PageData<LastVisitedDashboardInfo>>(emptyPageData<LastVisitedDashboardInfo>()); |
|||
|
|||
public pageData$ = this.pageDataSubject.asObservable(); |
|||
|
|||
constructor(private lastVisitedDashboards: Array<LastVisitedDashboardInfo>) { |
|||
} |
|||
|
|||
connect(collectionViewer: CollectionViewer): Observable<LastVisitedDashboardInfo[] | ReadonlyArray<LastVisitedDashboardInfo>> { |
|||
return this.lastVisitedDashboardsSubject.asObservable(); |
|||
} |
|||
|
|||
disconnect(collectionViewer: CollectionViewer): void { |
|||
this.lastVisitedDashboardsSubject.complete(); |
|||
this.pageDataSubject.complete(); |
|||
} |
|||
|
|||
loadData(pageLink: PageLink): void { |
|||
const result = pageLink.filterData(this.lastVisitedDashboards); |
|||
this.lastVisitedDashboardsSubject.next(result.data); |
|||
this.pageDataSubject.next(result); |
|||
} |
|||
|
|||
isEmpty(): Observable<boolean> { |
|||
return this.lastVisitedDashboardsSubject.pipe( |
|||
map((entities) => !entities.length), |
|||
share() |
|||
); |
|||
} |
|||
|
|||
total(): Observable<number> { |
|||
return this.pageDataSubject.pipe( |
|||
map((pageData) => pageData.totalElements), |
|||
share() |
|||
); |
|||
} |
|||
} |
|||
|
After Width: | Height: | Size: 1.8 KiB |
Loading…
Reference in new issue