UI: Change selector dashboard breakpoint and improved detect changes updated layout data

This commit is contained in:
Vladyslav_Prykhodko 2024-08-08 17:49:11 +03:00
parent 07a45573b7
commit 38ecf3526b
13 changed files with 149 additions and 136 deletions

View File

@ -18,6 +18,7 @@ import { Injectable } from '@angular/core';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { TimeService } from '@core/services/time.service'; import { TimeService } from '@core/services/time.service';
import { import {
BreakpointInfo,
BreakpointLayoutInfo, BreakpointLayoutInfo,
Dashboard, Dashboard,
DashboardConfiguration, DashboardConfiguration,
@ -51,12 +52,15 @@ import { initModelFromDefaultTimewindow } from '@shared/models/time/time.models'
import { AlarmSearchStatus } from '@shared/models/alarm.models'; import { AlarmSearchStatus } from '@shared/models/alarm.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { BackgroundType, colorBackground, isBackgroundSettings } from '@shared/models/widget-settings.models'; import { BackgroundType, colorBackground, isBackgroundSettings } from '@shared/models/widget-settings.models';
import { MediaBreakpoints } from '@shared/models/constants';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class DashboardUtilsService { export class DashboardUtilsService {
private systemBreakpoints: BreakpointInfo[];
constructor(private utils: UtilsService, constructor(private utils: UtilsService,
private timeService: TimeService) { private timeService: TimeService) {
} }
@ -996,4 +1000,21 @@ export class DashboardUtilsService {
}; };
} }
private loadSystemBreakpoints() {
this.systemBreakpoints=[{id: 'default'}];
const dashboardMediaBreakpointIds = ['xs', 'sm', 'md', 'lg', 'xl'];
dashboardMediaBreakpointIds.forEach(breakpoint => {
const value = MediaBreakpoints[breakpoint];
const minWidth = value.match(/min-width:\s*(\d+)px/);
const maxWidth = value.match(/max-width:\s*(\d+)px/);
this.systemBreakpoints.push({id: breakpoint, minWidth: minWidth?.[1], maxWidth: maxWidth?.[1]});
});
}
getListBreakpoint(): BreakpointInfo[] {
if(!this.systemBreakpoints) {
this.loadSystemBreakpoints();
}
return this.systemBreakpoints;
}
} }

View File

@ -58,9 +58,9 @@
<mat-icon>view_compact</mat-icon> <mat-icon>view_compact</mat-icon>
{{'layout.layouts' | translate}} {{'layout.layouts' | translate}}
</button> </button>
<tb-select-dashboard-layout <tb-select-dashboard-breakpoint
[dashboardCtrl]="this"> [dashboardCtrl]="this">
</tb-select-dashboard-layout> </tb-select-dashboard-breakpoint>
</div> </div>
</ng-container> </ng-container>
<tb-states-component fxFlex.lt-md <tb-states-component fxFlex.lt-md

View File

@ -83,7 +83,7 @@ import { Authority } from '@shared/models/authority.enum';
import { DialogService } from '@core/services/dialog.service'; import { DialogService } from '@core/services/dialog.service';
import { EntityService } from '@core/http/entity.service'; import { EntityService } from '@core/http/entity.service';
import { AliasController } from '@core/api/alias-controller'; import { AliasController } from '@core/api/alias-controller';
import { Observable, of, Subject, Subscription } from 'rxjs'; import { BehaviorSubject, Observable, of, Subject, Subscription } from 'rxjs';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
import { DashboardService } from '@core/http/dashboard.service'; import { DashboardService } from '@core/http/dashboard.service';
import { import {
@ -295,7 +295,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
ctrl: null, ctrl: null,
dashboardCtrl: this, dashboardCtrl: this,
displayGrid: 'onDrag&Resize', displayGrid: 'onDrag&Resize',
layoutData: null layoutData: null,
layoutDataChanged: new BehaviorSubject(null),
} }
}, },
right: { right: {
@ -310,7 +311,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
ctrl: null, ctrl: null,
dashboardCtrl: this, dashboardCtrl: this,
displayGrid: 'onDrag&Resize', displayGrid: 'onDrag&Resize',
layoutData: null layoutData: null,
layoutDataChanged: new BehaviorSubject(null),
} }
} }
}; };
@ -612,6 +614,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
if (this.dashboardResize$) { if (this.dashboardResize$) {
this.dashboardResize$.disconnect(); this.dashboardResize$.disconnect();
} }
this.layouts.main.layoutCtx.layoutDataChanged.unsubscribe();
this.layouts.right.layoutCtx.layoutDataChanged.unsubscribe();
} }
public runChangeDetection() { public runChangeDetection() {
@ -1140,6 +1144,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
private updateLayout(layout: DashboardPageLayout, layoutInfo: DashboardLayoutInfo) { private updateLayout(layout: DashboardPageLayout, layoutInfo: DashboardLayoutInfo) {
layout.layoutCtx.layoutData = layoutInfo; layout.layoutCtx.layoutData = layoutInfo;
layout.layoutCtx.layoutDataChanged.next();
layout.layoutCtx.ctrl?.updatedCurrentBreakpoint(null, layout.show); layout.layoutCtx.ctrl?.updatedCurrentBreakpoint(null, layout.show);
this.updateLayoutSizes(); this.updateLayoutSizes();
} }

View File

@ -26,7 +26,7 @@ import { Timewindow } from '@shared/models/time/time.models';
import { IAliasController, IStateController } from '@core/api/widget-api.models'; import { IAliasController, IStateController } from '@core/api/widget-api.models';
import { ILayoutController } from './layout/layout.models'; import { ILayoutController } from './layout/layout.models';
import { DashboardContextMenuItem, WidgetContextMenuItem } from '@home/models/dashboard-component.models'; import { DashboardContextMenuItem, WidgetContextMenuItem } from '@home/models/dashboard-component.models';
import { Observable } from 'rxjs'; import { BehaviorSubject, Observable } from 'rxjs';
import { displayGrids } from 'angular-gridster2/lib/gridsterConfig.interface'; import { displayGrids } from 'angular-gridster2/lib/gridsterConfig.interface';
export declare type DashboardPageScope = 'tenant' | 'customer'; export declare type DashboardPageScope = 'tenant' | 'customer';
@ -74,6 +74,7 @@ export interface IDashboardController {
export interface DashboardPageLayoutContext { export interface DashboardPageLayoutContext {
id: DashboardLayoutId; id: DashboardLayoutId;
layoutData: DashboardLayoutInfo; layoutData: DashboardLayoutInfo;
layoutDataChanged: BehaviorSubject<void>;
breakpoint: string; breakpoint: string;
widgets: LayoutWidgetsArray; widgets: LayoutWidgetsArray;
widgetLayouts: WidgetLayouts; widgetLayouts: WidgetLayouts;

View File

@ -343,21 +343,4 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
return this.layoutCtx.layoutData.default; return this.layoutCtx.layoutData.default;
} }
createBreakpointConfig(breakpoint: string) {
const currentDashboard = this.dashboardCtx.getDashboard();
const dashboardConfiguration = currentDashboard.configuration;
const states = dashboardConfiguration.states;
const state = states[this.dashboardCtx.state];
const layout = state.layouts[this.layoutCtx.id];
if (!layout.breakpoints) {
layout.breakpoints = {};
}
layout.breakpoints[breakpoint] = {
gridSettings: deepClone(layout.gridSettings),
widgets: deepClone(layout.widgets),
};
this.layoutCtx.layoutData =
this.dashboardUtils.getStateLayoutsData(currentDashboard, this.dashboardCtx.state)[this.layoutCtx.id];
}
} }

View File

@ -22,7 +22,6 @@ export interface ILayoutController {
pasteWidget($event: MouseEvent); pasteWidget($event: MouseEvent);
pasteWidgetReference($event: MouseEvent); pasteWidgetReference($event: MouseEvent);
updatedCurrentBreakpoint(breakpoint?: string, showLayout?: boolean); updatedCurrentBreakpoint(breakpoint?: string, showLayout?: boolean);
createBreakpointConfig(breakpoint?: string);
} }
export enum LayoutWidthType { export enum LayoutWidthType {

View File

@ -33,6 +33,7 @@ import { DialogComponent } from '@app/shared/components/dialog.component';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { import {
BreakpointInfo,
DashboardLayout, DashboardLayout,
DashboardLayoutId, DashboardLayoutId,
DashboardStateLayouts, DashboardStateLayouts,
@ -53,7 +54,6 @@ import {
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { MatTooltip } from '@angular/material/tooltip'; import { MatTooltip } from '@angular/material/tooltip';
import { TbTableDatasource } from '@shared/components/table/table-datasource.abstract'; import { TbTableDatasource } from '@shared/components/table/table-datasource.abstract';
import { MediaBreakpoints } from '@shared/models/constants';
export interface ManageDashboardLayoutsDialogData { export interface ManageDashboardLayoutsDialogData {
layouts: DashboardStateLayouts; layouts: DashboardStateLayouts;
@ -101,16 +101,10 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
private submitted = false; private submitted = false;
private layoutBreakpoint = { breakpoints: BreakpointInfo[];
default: 'Default', breakpointsData: {[breakpointId in string]: BreakpointInfo} = {};
xs: 'xs',
sm: 'sm',
md: 'md',
lg: 'lg',
xl: 'xl'
};
allowBreakpointIds = Object.keys(this.layoutBreakpoint); allowBreakpointIds = [];
selectedBreakpointIds = ['default']; selectedBreakpointIds = ['default'];
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
@ -128,6 +122,11 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
this.layouts = this.data.layouts; this.layouts = this.data.layouts;
this.dataSource = new DashboardLayoutDatasource(); this.dataSource = new DashboardLayoutDatasource();
this.breakpoints = this.dashboardUtils.getListBreakpoint();
this.breakpoints.forEach((breakpoint) => {
this.breakpointsData[breakpoint.id] = breakpoint;
});
let layoutType = LayoutType.default; let layoutType = LayoutType.default;
if (isDefined(this.layouts.right)) { if (isDefined(this.layouts.right)) {
layoutType = LayoutType.divider; layoutType = LayoutType.divider;
@ -217,7 +216,7 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
} }
} }
this.allowBreakpointIds = Object.keys(this.layoutBreakpoint) this.allowBreakpointIds = Object.keys(this.breakpointsData)
.filter((item) => !this.selectedBreakpointIds.includes(item)); .filter((item) => !this.selectedBreakpointIds.includes(item));
this.dataSource.loadData(this.layoutBreakpoints); this.dataSource.loadData(this.layoutBreakpoints);
@ -259,15 +258,6 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
this.layoutsFormGroup.get('sliderFixed').setValue(value, {emitEvent: false}); this.layoutsFormGroup.get('sliderFixed').setValue(value, {emitEvent: false});
} }
)); ));
this.subscriptions.push(
this.layoutsFormGroup.get('layoutType').valueChanges
.subscribe(
() => {
this.dataSource = new DashboardLayoutDatasource();
this.dataSource.loadData(this.layoutBreakpoints);
}
));
} }
ngOnDestroy(): void { ngOnDestroy(): void {
@ -491,11 +481,13 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
this.dataSource.loadData(this.layoutBreakpoints); this.dataSource.loadData(this.layoutBreakpoints);
this.addBreakpointMode = false; this.addBreakpointMode = false;
this.layoutsFormGroup.markAsDirty();
} }
private addLayoutConfiguration(breakpointId: string) { private addLayoutConfiguration(breakpointId: string) {
const layout = breakpointId === 'default' ? this.layouts.main : this.layouts.main.breakpoints[breakpointId]; const layout = breakpointId === 'default' ? this.layouts.main : this.layouts.main.breakpoints[breakpointId];
const size = breakpointId === 'default' ? '' : this.parseCssQuery(MediaBreakpoints[breakpointId]); const size = breakpointId === 'default' ? '' : this.createDescriptionSize(breakpointId);
this.layoutBreakpoints.push({ this.layoutBreakpoints.push({
icon: 'mdi:monitor', icon: 'mdi:monitor',
name: breakpointId, name: breakpointId,
@ -505,9 +497,11 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
}); });
} }
private parseCssQuery(query: string): string { private createDescriptionSize(breakpointId: string): string {
const value = Array.from(query.matchAll(/\([^:]+:\s+([^()]+)\)/g)); const currentData = this.breakpointsData[breakpointId];
return `min-width: ${value[0][1]} and max-width: ${value[1][1]}`; const minStr = isDefined(currentData.minWidth) ? `min-width: ${currentData.minWidth}px` : '';
const maxStr = isDefined(currentData.maxWidth) ? `min-width: ${currentData.maxWidth}px` : '';
return minStr && maxStr ? `${minStr} and ${maxStr}` : `${minStr}${maxStr}`;
} }
} }
@ -515,4 +509,11 @@ export class DashboardLayoutDatasource extends TbTableDatasource<DashboardLayout
constructor() { constructor() {
super(); super();
} }
connect() {
if (this.dataSubject.isStopped) {
this.dataSubject.isStopped = false;
}
return this.dataSubject.asObservable();
}
} }

View File

@ -16,10 +16,11 @@
--> -->
<mat-select <mat-select
class="select-dashboard-layout" [class.hidden]="breakpointIds.length < 2"
[(ngModel)]="selectLayout" class="select-dashboard-breakpoint"
[(ngModel)]="selectedBreakpoint"
(ngModelChange)="selectLayoutChanged()"> (ngModelChange)="selectLayoutChanged()">
<mat-option *ngFor="let layout of layouts" [value]="layout"> <mat-option *ngFor="let breakpointId of breakpointIds" [value]="breakpointId">
{{ layout }} {{ breakpointId }}
</mat-option> </mat-option>
</mat-select> </mat-select>

View File

@ -16,10 +16,14 @@
:host { :host {
pointer-events: all; pointer-events: all;
width: min-content; //for Safari width: min-content; //for Safari
.hidden {
display: none;
}
} }
:host ::ng-deep { :host ::ng-deep {
.mat-mdc-select.select-dashboard-layout { .mat-mdc-select.select-dashboard-breakpoint {
.mat-mdc-select-value { .mat-mdc-select-value {
max-width: 200px; max-width: 200px;
} }

View File

@ -0,0 +1,69 @@
///
/// Copyright © 2016-2024 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 { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { DashboardPageComponent } from '@home/components/dashboard-page/dashboard-page.component';
import { Subscription } from 'rxjs';
import { BreakpointInfo } from '@shared/models/dashboard.models';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
@Component({
selector: 'tb-select-dashboard-breakpoint',
templateUrl: './select-dashboard-breakpoint.component.html',
styleUrls: ['./select-dashboard-breakpoint.component.scss']
})
export class SelectDashboardBreakpointComponent implements OnInit, OnDestroy {
@Input()
dashboardCtrl: DashboardPageComponent;
selectedBreakpoint = 'default';
breakpointsData: {[breakpointId in string]: BreakpointInfo} = {};
breakpointIds: Array<string> = ['default'];
private layoutDataChanged$: Subscription;
constructor(private dashboardUtils: DashboardUtilsService) {
this.dashboardUtils.getListBreakpoint().forEach((breakpoint) => {
this.breakpointsData[breakpoint.id] = breakpoint;
});
}
ngOnInit() {
this.layoutDataChanged$ = this.dashboardCtrl.layouts.main.layoutCtx.layoutDataChanged.subscribe(() => {
if (this.dashboardCtrl.layouts.main.layoutCtx.layoutData) {
this.breakpointIds = Object.keys(this.dashboardCtrl.layouts.main.layoutCtx?.layoutData);
if (this.breakpointIds.indexOf(this.dashboardCtrl.layouts.main.layoutCtx.breakpoint) > -1) {
this.selectedBreakpoint = this.dashboardCtrl.layouts.main.layoutCtx.breakpoint;
} else {
this.selectedBreakpoint = 'default';
this.dashboardCtrl.layouts.main.layoutCtx.breakpoint = this.selectedBreakpoint;
}
}
});
}
ngOnDestroy() {
this.layoutDataChanged$.unsubscribe();
}
selectLayoutChanged() {
this.dashboardCtrl.layouts.main.layoutCtx.ctrl.updatedCurrentBreakpoint(this.selectedBreakpoint);
this.dashboardCtrl.updateLayoutSizes();
}
}

View File

@ -1,77 +0,0 @@
///
/// Copyright © 2016-2024 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 { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { DashboardPageComponent } from '@home/components/dashboard-page/dashboard-page.component';
import { Subscription } from 'rxjs';
@Component({
selector: 'tb-select-dashboard-layout',
templateUrl: './select-dashboard-layout.component.html',
styleUrls: ['./select-dashboard-layout.component.scss']
})
export class SelectDashboardLayoutComponent implements OnInit, OnDestroy {
@Input()
dashboardCtrl: DashboardPageComponent;
layout = {
default: 'Default',
xs: 'xs',
sm: 'sm',
md: 'md',
lg: 'lg',
xl: 'xl'
};
layouts = Object.keys(this.layout);
selectLayout = 'default';
private allowBreakpointsSize = new Set<string>();
private stateChanged$: Subscription;
constructor() { }
ngOnInit() {
this.stateChanged$ = this.dashboardCtrl.dashboardCtx.stateChanged.subscribe(() => {
if (this.dashboardCtrl.layouts.main.layoutCtx.layoutData) {
this.allowBreakpointsSize = new Set(Object.keys(this.dashboardCtrl.layouts.main.layoutCtx?.layoutData));
} else {
this.allowBreakpointsSize.add('default');
}
if (this.allowBreakpointsSize.has(this.dashboardCtrl.layouts.main.layoutCtx.breakpoint)) {
this.selectLayout = this.dashboardCtrl.layouts.main.layoutCtx.breakpoint;
} else {
this.selectLayout = 'default';
this.dashboardCtrl.layouts.main.layoutCtx.breakpoint = this.selectLayout;
}
});
}
ngOnDestroy() {
this.stateChanged$.unsubscribe();
}
selectLayoutChanged() {
if (!this.dashboardCtrl.layouts.main.layoutCtx.layoutData[this.selectLayout]) {
this.dashboardCtrl.layouts.main.layoutCtx.ctrl.createBreakpointConfig(this.selectLayout);
}
this.dashboardCtrl.layouts.main.layoutCtx.ctrl.updatedCurrentBreakpoint(this.selectLayout);
this.dashboardCtrl.updateLayoutSizes();
}
}

View File

@ -176,8 +176,8 @@ import { BasicWidgetConfigModule } from '@home/components/widget/config/basic/ba
import { DeleteTimeseriesPanelComponent } from '@home/components/attribute/delete-timeseries-panel.component'; import { DeleteTimeseriesPanelComponent } from '@home/components/attribute/delete-timeseries-panel.component';
import { MoveWidgetsDialogComponent } from '@home/components/dashboard-page/layout/move-widgets-dialog.component'; import { MoveWidgetsDialogComponent } from '@home/components/dashboard-page/layout/move-widgets-dialog.component';
import { import {
SelectDashboardLayoutComponent SelectDashboardBreakpointComponent
} from '@home/components/dashboard-page/layout/select-dashboard-layout.component'; } from '@home/components/dashboard-page/layout/select-dashboard-breakpoint.component';
@NgModule({ @NgModule({
declarations: declarations:
@ -288,7 +288,7 @@ import {
DashboardPageComponent, DashboardPageComponent,
DashboardStateComponent, DashboardStateComponent,
DashboardLayoutComponent, DashboardLayoutComponent,
SelectDashboardLayoutComponent, SelectDashboardBreakpointComponent,
EditWidgetComponent, EditWidgetComponent,
DashboardWidgetSelectComponent, DashboardWidgetSelectComponent,
AddWidgetDialogComponent, AddWidgetDialogComponent,
@ -422,7 +422,7 @@ import {
DashboardPageComponent, DashboardPageComponent,
DashboardStateComponent, DashboardStateComponent,
DashboardLayoutComponent, DashboardLayoutComponent,
SelectDashboardLayoutComponent, SelectDashboardBreakpointComponent,
EditWidgetComponent, EditWidgetComponent,
DashboardWidgetSelectComponent, DashboardWidgetSelectComponent,
AddWidgetDialogComponent, AddWidgetDialogComponent,

View File

@ -85,10 +85,10 @@ export interface GridSettings {
export interface DashboardLayout { export interface DashboardLayout {
widgets: WidgetLayouts; widgets: WidgetLayouts;
gridSettings: GridSettings; gridSettings: GridSettings;
breakpoints?: {[breakpoint: string]: Omit<DashboardLayout, 'breakpoints'>}; breakpoints?: {[breakpointId: string]: Omit<DashboardLayout, 'breakpoints'>};
} }
export declare type DashboardLayoutInfo = {[breakpoint: string]: BreakpointLayoutInfo}; export declare type DashboardLayoutInfo = {[breakpointId: string]: BreakpointLayoutInfo};
export interface BreakpointLayoutInfo { export interface BreakpointLayoutInfo {
widgetIds?: string[]; widgetIds?: string[];
@ -96,6 +96,12 @@ export interface BreakpointLayoutInfo {
gridSettings?: GridSettings; gridSettings?: GridSettings;
} }
export interface BreakpointInfo {
id: string;
maxWidth?: number;
minWidth?: number;
}
export interface LayoutDimension { export interface LayoutDimension {
type?: LayoutDimensionType; type?: LayoutDimensionType;
fixedWidth?: number; fixedWidth?: number;