Merge branch 'develop/3.5.2' into feature/basic-widget-config
This commit is contained in:
commit
c6dd11f576
@ -300,6 +300,7 @@ import * as QueueFormComponent from '@home/components/queue/queue-form.component
|
||||
import * as AssetProfileComponent from '@home/components/profile/asset-profile.component';
|
||||
import * as AssetProfileDialogComponent from '@home/components/profile/asset-profile-dialog.component';
|
||||
import * as AssetProfileAutocompleteComponent from '@home/components/profile/asset-profile-autocomplete.component';
|
||||
import * as RuleChainSelectComponent from '@shared/components/rule-chain/rule-chain-select.component';
|
||||
|
||||
import { IModulesMap } from '@modules/common/modules-map.models';
|
||||
|
||||
@ -418,6 +419,7 @@ class ModulesMap implements IModulesMap {
|
||||
'@shared/components/time/quick-time-interval.component': QuickTimeIntervalComponent,
|
||||
'@shared/components/dashboard-select.component': DashboardSelectComponent,
|
||||
'@shared/components/dashboard-select-panel.component': DashboardSelectPanelComponent,
|
||||
'@shared/components/rule-chain/rule-chain-select.component': RuleChainSelectComponent,
|
||||
'@shared/components/time/datetime-period.component': DatetimePeriodComponent,
|
||||
'@shared/components/time/datetime.component': DatetimeComponent,
|
||||
'@shared/components/time/timezone-select.component': TimezoneSelectComponent,
|
||||
|
||||
@ -66,4 +66,29 @@
|
||||
</ng-template>
|
||||
</mat-expansion-panel>
|
||||
</fieldset>
|
||||
<mat-form-field fxFlex class="mat-block">
|
||||
<mat-label translate>widgets.table.default-column-visibility</mat-label>
|
||||
<mat-select formControlName="defaultColumnVisibility">
|
||||
<mat-option [value]="'visible'">
|
||||
{{ 'widgets.table.column-visibility-visible' | translate }}
|
||||
</mat-option>
|
||||
<mat-option [value]="'hidden'">
|
||||
{{ 'widgets.table.column-visibility-hidden' | translate }}
|
||||
</mat-option>
|
||||
<mat-option [value]="'hidden-mobile'">
|
||||
{{ 'widgets.table.column-visibility-hidden-mobile' | translate }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field fxFlex class="mat-block">
|
||||
<mat-label translate>widgets.table.column-selection-to-display</mat-label>
|
||||
<mat-select formControlName="columnSelectionToDisplay">
|
||||
<mat-option [value]="'enabled'">
|
||||
{{ 'widgets.table.column-selection-to-display-enabled' | translate }}
|
||||
</mat-option>
|
||||
<mat-option [value]="'disabled'">
|
||||
{{ 'widgets.table.column-selection-to-display-disabled' | translate }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</section>
|
||||
|
||||
@ -43,7 +43,9 @@ export class TimeseriesTableKeySettingsComponent extends WidgetSettingsComponent
|
||||
useCellStyleFunction: false,
|
||||
cellStyleFunction: '',
|
||||
useCellContentFunction: false,
|
||||
cellContentFunction: ''
|
||||
cellContentFunction: '',
|
||||
defaultColumnVisibility: 'visible',
|
||||
columnSelectionToDisplay: 'enabled'
|
||||
};
|
||||
}
|
||||
|
||||
@ -53,6 +55,8 @@ export class TimeseriesTableKeySettingsComponent extends WidgetSettingsComponent
|
||||
cellStyleFunction: [settings.cellStyleFunction, [Validators.required]],
|
||||
useCellContentFunction: [settings.useCellContentFunction, []],
|
||||
cellContentFunction: [settings.cellContentFunction, [Validators.required]],
|
||||
defaultColumnVisibility: [settings.defaultColumnVisibility, []],
|
||||
columnSelectionToDisplay: [settings.columnSelectionToDisplay, []],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -73,4 +73,29 @@
|
||||
</ng-template>
|
||||
</mat-expansion-panel>
|
||||
</fieldset>
|
||||
<mat-form-field fxFlex class="mat-block">
|
||||
<mat-label translate>widgets.table.default-column-visibility</mat-label>
|
||||
<mat-select formControlName="defaultColumnVisibility">
|
||||
<mat-option [value]="'visible'">
|
||||
{{ 'widgets.table.column-visibility-visible' | translate }}
|
||||
</mat-option>
|
||||
<mat-option [value]="'hidden'">
|
||||
{{ 'widgets.table.column-visibility-hidden' | translate }}
|
||||
</mat-option>
|
||||
<mat-option [value]="'hidden-mobile'">
|
||||
{{ 'widgets.table.column-visibility-hidden-mobile' | translate }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field fxFlex class="mat-block">
|
||||
<mat-label translate>widgets.table.column-selection-to-display</mat-label>
|
||||
<mat-select formControlName="columnSelectionToDisplay">
|
||||
<mat-option [value]="'enabled'">
|
||||
{{ 'widgets.table.column-selection-to-display-enabled' | translate }}
|
||||
</mat-option>
|
||||
<mat-option [value]="'disabled'">
|
||||
{{ 'widgets.table.column-selection-to-display-disabled' | translate }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</section>
|
||||
|
||||
@ -44,7 +44,9 @@ export class TimeseriesTableLatestKeySettingsComponent extends WidgetSettingsCom
|
||||
useCellStyleFunction: false,
|
||||
cellStyleFunction: '',
|
||||
useCellContentFunction: false,
|
||||
cellContentFunction: ''
|
||||
cellContentFunction: '',
|
||||
defaultColumnVisibility: 'visible',
|
||||
columnSelectionToDisplay: 'enabled'
|
||||
};
|
||||
}
|
||||
|
||||
@ -56,6 +58,8 @@ export class TimeseriesTableLatestKeySettingsComponent extends WidgetSettingsCom
|
||||
cellStyleFunction: [settings.cellStyleFunction, [Validators.required]],
|
||||
useCellContentFunction: [settings.useCellContentFunction, []],
|
||||
cellContentFunction: [settings.cellContentFunction, [Validators.required]],
|
||||
defaultColumnVisibility: [settings.defaultColumnVisibility, []],
|
||||
columnSelectionToDisplay: [settings.columnSelectionToDisplay, []],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -23,6 +23,9 @@
|
||||
<mat-checkbox formControlName="enableSearch">
|
||||
{{ 'widgets.table.enable-search' | translate }}
|
||||
</mat-checkbox>
|
||||
<mat-checkbox formControlName="enableSelectColumnDisplay">
|
||||
{{ 'widgets.table.enable-select-column-display' | translate }}
|
||||
</mat-checkbox>
|
||||
</section>
|
||||
<section fxLayout="column" fxFlex>
|
||||
<mat-checkbox formControlName="enableStickyHeader">
|
||||
|
||||
@ -41,6 +41,7 @@ export class TimeseriesTableWidgetSettingsComponent extends WidgetSettingsCompon
|
||||
protected defaultSettings(): WidgetSettings {
|
||||
return {
|
||||
enableSearch: true,
|
||||
enableSelectColumnDisplay: true,
|
||||
enableStickyHeader: true,
|
||||
enableStickyAction: true,
|
||||
reserveSpaceForHiddenAction: 'true',
|
||||
@ -59,6 +60,7 @@ export class TimeseriesTableWidgetSettingsComponent extends WidgetSettingsCompon
|
||||
protected onSettingsSet(settings: WidgetSettings) {
|
||||
this.timeseriesTableWidgetSettingsForm = this.fb.group({
|
||||
enableSearch: [settings.enableSearch, []],
|
||||
enableSelectColumnDisplay: [settings.enableSelectColumnDisplay, []],
|
||||
enableStickyHeader: [settings.enableStickyHeader, []],
|
||||
enableStickyAction: [settings.enableStickyAction, []],
|
||||
reserveSpaceForHiddenAction: [settings.reserveSpaceForHiddenAction, []],
|
||||
|
||||
@ -31,6 +31,7 @@ type ColumnSelectionOptions = 'enabled' | 'disabled';
|
||||
|
||||
export interface TableWidgetSettings {
|
||||
enableSearch: boolean;
|
||||
enableSelectColumnDisplay: boolean;
|
||||
enableStickyAction: boolean;
|
||||
enableStickyHeader: boolean;
|
||||
displayPagination: boolean;
|
||||
|
||||
@ -19,9 +19,12 @@ import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ElementRef,
|
||||
Injector,
|
||||
Input,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
QueryList,
|
||||
StaticProvider,
|
||||
ViewChild,
|
||||
ViewChildren,
|
||||
ViewContainerRef
|
||||
@ -64,8 +67,9 @@ import {
|
||||
CellStyleInfo,
|
||||
checkHasActions,
|
||||
constructTableCssString,
|
||||
DisplayColumn,
|
||||
getCellContentInfo,
|
||||
getCellStyleInfo,
|
||||
getCellStyleInfo, getColumnDefaultVisibility, getColumnSelectionAvailability,
|
||||
getRowStyleInfo,
|
||||
getTableCellButtonActions,
|
||||
noDataMessage,
|
||||
@ -75,12 +79,17 @@ import {
|
||||
TableWidgetDataKeySettings,
|
||||
TableWidgetSettings
|
||||
} from '@home/components/widget/lib/table-widget.models';
|
||||
import { Overlay } from '@angular/cdk/overlay';
|
||||
import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
|
||||
import { SubscriptionEntityInfo } from '@core/api/widget-api.models';
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { coerceBooleanProperty } from '@angular/cdk/coercion';
|
||||
import { ResizeObserver } from '@juggle/resize-observer';
|
||||
import { hidePageSizePixelValue } from '@shared/models/constants';
|
||||
import {
|
||||
DISPLAY_COLUMNS_PANEL_DATA,
|
||||
DisplayColumnsPanelComponent
|
||||
} from '@home/components/widget/lib/display-columns-panel.component';
|
||||
import { ComponentPortal } from '@angular/cdk/portal';
|
||||
|
||||
export interface TimeseriesTableWidgetSettings extends TableWidgetSettings {
|
||||
showTimestamp: boolean;
|
||||
@ -105,6 +114,8 @@ interface TimeseriesHeader {
|
||||
dataKey: DataKey;
|
||||
sortable: boolean;
|
||||
show: boolean;
|
||||
columnDefaultVisibility?: boolean;
|
||||
columnSelectionAvailability?: boolean;
|
||||
styleInfo: CellStyleInfo;
|
||||
contentInfo: CellContentInfo;
|
||||
order?: number;
|
||||
@ -131,7 +142,7 @@ interface TimeseriesTableSource {
|
||||
templateUrl: './timeseries-table-widget.component.html',
|
||||
styleUrls: ['./timeseries-table-widget.component.scss', './table-widget.scss']
|
||||
})
|
||||
export class TimeseriesTableWidgetComponent extends PageComponent implements OnInit, AfterViewInit {
|
||||
export class TimeseriesTableWidgetComponent extends PageComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
@Input()
|
||||
ctx: WidgetContext;
|
||||
@ -169,6 +180,8 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
|
||||
private useEntityLabel = false;
|
||||
private dateFormatFilter: string;
|
||||
|
||||
private displayedColumns: Array<DisplayColumn[]> = [];
|
||||
|
||||
private rowStylesInfo: RowStyleInfo;
|
||||
|
||||
private subscriptions: Subscription[] = [];
|
||||
@ -184,6 +197,15 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
|
||||
}
|
||||
};
|
||||
|
||||
private columnDisplayAction: WidgetAction = {
|
||||
name: 'entity.columns-to-display',
|
||||
show: true,
|
||||
icon: 'view_column',
|
||||
onAction: ($event) => {
|
||||
this.editColumnsToDisplay($event);
|
||||
}
|
||||
};
|
||||
|
||||
constructor(protected store: Store<AppState>,
|
||||
private elementRef: ElementRef,
|
||||
private overlay: Overlay,
|
||||
@ -275,11 +297,12 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
|
||||
}
|
||||
|
||||
private initialize() {
|
||||
this.ctx.widgetActions = [this.searchAction ];
|
||||
this.ctx.widgetActions = [this.searchAction, this.columnDisplayAction];
|
||||
|
||||
this.setCellButtonAction = !!this.ctx.actionsApi.getActionDescriptors('actionCellButton').length;
|
||||
|
||||
this.searchAction.show = isDefined(this.settings.enableSearch) ? this.settings.enableSearch : true;
|
||||
this.columnDisplayAction.show = isDefined(this.settings.enableSelectColumnDisplay) ? this.settings.enableSelectColumnDisplay : true;
|
||||
this.displayPagination = isDefined(this.settings.displayPagination) ? this.settings.displayPagination : true;
|
||||
this.enableStickyHeader = isDefined(this.settings.enableStickyHeader) ? this.settings.enableStickyHeader : true;
|
||||
this.enableStickyAction = isDefined(this.settings.enableStickyAction) ? this.settings.enableStickyAction : true;
|
||||
@ -365,9 +388,82 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
|
||||
this.sources.push(source);
|
||||
}
|
||||
}
|
||||
this.prepareDisplayedColumn();
|
||||
this.sources[this.sourceIndex].displayedColumns =
|
||||
this.displayedColumns[this.sourceIndex].filter(value => value.display).map(value => value.def);
|
||||
this.updateActiveEntityInfo();
|
||||
}
|
||||
|
||||
private editColumnsToDisplay($event: Event) {
|
||||
if ($event) {
|
||||
$event.stopPropagation();
|
||||
}
|
||||
const target = $event.target || $event.currentTarget;
|
||||
const config = new OverlayConfig();
|
||||
config.backdropClass = 'cdk-overlay-transparent-backdrop';
|
||||
config.hasBackdrop = true;
|
||||
const connectedPosition: ConnectedPosition = {
|
||||
originX: 'end',
|
||||
originY: 'bottom',
|
||||
overlayX: 'end',
|
||||
overlayY: 'top'
|
||||
};
|
||||
config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement)
|
||||
.withPositions([connectedPosition]);
|
||||
|
||||
const overlayRef = this.overlay.create(config);
|
||||
overlayRef.backdropClick().subscribe(() => {
|
||||
overlayRef.dispose();
|
||||
});
|
||||
const source = this.sources[this.sourceIndex];
|
||||
|
||||
this.prepareDisplayedColumn();
|
||||
|
||||
const providers: StaticProvider[] = [
|
||||
{
|
||||
provide: DISPLAY_COLUMNS_PANEL_DATA,
|
||||
useValue: {
|
||||
columns: this.displayedColumns[this.sourceIndex],
|
||||
columnsUpdated: (newColumns) => {
|
||||
source.displayedColumns = newColumns.filter(value => value.display).map(value => value.def);
|
||||
this.clearCache();
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
provide: OverlayRef,
|
||||
useValue: overlayRef
|
||||
}
|
||||
];
|
||||
|
||||
const injector = Injector.create({parent: this.viewContainerRef.injector, providers});
|
||||
overlayRef.attach(new ComponentPortal(DisplayColumnsPanelComponent,
|
||||
this.viewContainerRef, injector));
|
||||
this.ctx.detectChanges();
|
||||
}
|
||||
|
||||
private prepareDisplayedColumn() {
|
||||
if (!this.displayedColumns[this.sourceIndex]) {
|
||||
this.displayedColumns[this.sourceIndex] = this.sources[this.sourceIndex].displayedColumns.map(value => {
|
||||
let title = '';
|
||||
const header = this.sources[this.sourceIndex].header.find(column => column.index.toString() === value);
|
||||
if (value === '0') {
|
||||
title = 'Timestamp';
|
||||
} else if (value === 'actions') {
|
||||
title = 'Actions';
|
||||
} else {
|
||||
title = header.dataKey.name;
|
||||
}
|
||||
return {
|
||||
title,
|
||||
def: value,
|
||||
display: header?.columnDefaultVisibility ?? true,
|
||||
selectable: header?.columnSelectionAvailability ?? true
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private prepareHeader(datasource: Datasource): TimeseriesHeader[] {
|
||||
const dataKeys = datasource.dataKeys;
|
||||
const latestDataKeys = datasource.latestDataKeys;
|
||||
@ -377,6 +473,8 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
|
||||
const keySettings: TableWidgetDataKeySettings = dataKey.settings;
|
||||
const styleInfo = getCellStyleInfo(keySettings, 'value, rowData, ctx');
|
||||
const contentInfo = getCellContentInfo(keySettings, 'value, rowData, ctx');
|
||||
const columnDefaultVisibility = getColumnDefaultVisibility(keySettings, this.ctx);
|
||||
const columnSelectionAvailability = getColumnSelectionAvailability(keySettings);
|
||||
contentInfo.units = dataKey.units;
|
||||
contentInfo.decimals = dataKey.decimals;
|
||||
header.push({
|
||||
@ -386,6 +484,8 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
|
||||
styleInfo,
|
||||
contentInfo,
|
||||
show: true,
|
||||
columnDefaultVisibility,
|
||||
columnSelectionAvailability,
|
||||
order: index + 2
|
||||
});
|
||||
});
|
||||
@ -396,6 +496,8 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
|
||||
const keySettings: TimeseriesWidgetLatestDataKeySettings = dataKey.settings;
|
||||
const styleInfo = getCellStyleInfo(keySettings, 'value, rowData, ctx');
|
||||
const contentInfo = getCellContentInfo(keySettings, 'value, rowData, ctx');
|
||||
const columnDefaultVisibility = getColumnDefaultVisibility(keySettings, this.ctx);
|
||||
const columnSelectionAvailability = getColumnSelectionAvailability(keySettings);
|
||||
contentInfo.units = dataKey.units;
|
||||
contentInfo.decimals = dataKey.decimals;
|
||||
header.push({
|
||||
@ -405,13 +507,13 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
|
||||
styleInfo,
|
||||
contentInfo,
|
||||
show: isDefinedAndNotNull(keySettings.show) ? keySettings.show : true,
|
||||
columnDefaultVisibility,
|
||||
columnSelectionAvailability,
|
||||
order: isDefinedAndNotNull(keySettings.order) ? keySettings.order : (index + 2)
|
||||
});
|
||||
});
|
||||
}
|
||||
header = header.sort((a, b) => {
|
||||
return a.order - b.order;
|
||||
});
|
||||
header = header.sort((a, b) => a.order - b.order);
|
||||
return header;
|
||||
}
|
||||
|
||||
|
||||
@ -59,8 +59,7 @@
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<tb-template-autocomplete
|
||||
required
|
||||
allowCreate
|
||||
required allowCreate allowEdit
|
||||
formControlName="templateId"
|
||||
[notificationTypes]="ruleNotificationForm.get('triggerType').value">
|
||||
</tb-template-autocomplete>
|
||||
|
||||
@ -49,8 +49,7 @@
|
||||
</div>
|
||||
<div *ngIf="notificationRequestForm.get('useTemplate').value; else scratchTemplate">
|
||||
<tb-template-autocomplete
|
||||
required
|
||||
allowCreate
|
||||
required allowCreate allowEdit
|
||||
formControlName="templateId"
|
||||
[notificationTypes]="notificationType.GENERAL">
|
||||
</tb-template-autocomplete>
|
||||
|
||||
@ -15,55 +15,48 @@
|
||||
limitations under the License.
|
||||
|
||||
-->
|
||||
<div class="mat-content" fxFlex tb-fullscreen [fullscreen]="isFullscreen" tb-hotkeys [hotkeys]="hotKeys"
|
||||
[cheatSheet]="cheatSheetComponent"
|
||||
fxLayout="column" class="tb-rulechain">
|
||||
<div class="mat-content tb-rulechain" fxFlex tb-fullscreen [fullscreen]="isFullscreen" tb-hotkeys [hotkeys]="hotKeys"
|
||||
[cheatSheet]="cheatSheetComponent" fxLayout="column">
|
||||
<tb-hotkeys-cheatsheet #cheatSheetComponent></tb-hotkeys-cheatsheet>
|
||||
<section class="tb-rulechain-container" fxFlex fxLayout="column">
|
||||
<div class="tb-rulechain-layout" fxFlex fxLayout="row">
|
||||
<section fxLayout="row"
|
||||
class="tb-header-buttons tb-library-open" [fxShow]="!isLibraryOpen">
|
||||
<button color="primary"
|
||||
mat-mini-fab
|
||||
class="tb-btn-header tb-btn-open-library"
|
||||
(click)="isLibraryOpen = true"
|
||||
matTooltip="{{ 'rulenode.open-node-library' | translate }}"
|
||||
matTooltipPosition="above">
|
||||
<mat-icon>menu</mat-icon>
|
||||
</button>
|
||||
</section>
|
||||
<mat-drawer-container style="width: 100%; height: 100%;">
|
||||
<mat-drawer class="tb-rulechain-library mat-elevation-z4"
|
||||
disableClose="true"
|
||||
mode="side"
|
||||
[opened]="isLibraryOpen"
|
||||
#drawer
|
||||
opened
|
||||
position="start"
|
||||
fxLayout="column">
|
||||
<mat-toolbar color="primary" class="tb-dark">
|
||||
<tb-rule-chain-select
|
||||
fxFlex
|
||||
*ngIf="!isImport"
|
||||
[ruleChainType]="ruleChainType"
|
||||
[disabled]="isDirtyValue"
|
||||
[(ngModel)]="ruleChain.id.id"
|
||||
(ngModelChange)="currentRuleChainIdChanged(ruleChain.id?.id)">
|
||||
</tb-rule-chain-select>
|
||||
</mat-toolbar>
|
||||
<mat-toolbar>
|
||||
<div class="mat-toolbar-tools">
|
||||
<button mat-icon-button class="tb-small"
|
||||
<mat-form-field fxFlex class="tb-appearance-transparent">
|
||||
<button mat-icon-button matPrefix class="tb-small"
|
||||
matTooltip="{{'rulenode.search' | translate}}"
|
||||
matTooltipPosition="above">
|
||||
<mat-icon>search</mat-icon>
|
||||
</button>
|
||||
<mat-form-field fxFlex class="tb-appearance-transparent">
|
||||
<input #ruleNodeSearchInput matInput
|
||||
[(ngModel)]="ruleNodeTypeSearch"
|
||||
placeholder="{{'rulenode.search' | translate}}"/>
|
||||
</mat-form-field>
|
||||
<button mat-icon-button class="tb-small"
|
||||
<button mat-icon-button matSuffix class="tb-small"
|
||||
[fxShow]="ruleNodeTypeSearch !== ''"
|
||||
(click)="ruleNodeTypeSearch = ''; updateRuleChainLibrary()"
|
||||
matTooltip="{{'action.clear-search' | translate}}"
|
||||
matTooltipPosition="above">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button class="tb-small"
|
||||
(click)="isLibraryOpen = false"
|
||||
matTooltip="{{'action.close' | translate}}"
|
||||
matTooltipPosition="above">
|
||||
<mat-icon>chevron_left</mat-icon>
|
||||
</button>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</mat-toolbar>
|
||||
<div class="tb-rulechain-library-panel-group">
|
||||
@ -164,6 +157,14 @@
|
||||
</tb-details-panel>
|
||||
</mat-drawer>
|
||||
<mat-drawer-content class="tb-rulechain-graph-content">
|
||||
<button color="primary"
|
||||
mat-mini-fab
|
||||
class="tb-library-node-btn"
|
||||
(click)="drawer.toggle();"
|
||||
matTooltip="{{ (drawer.opened ? 'rulenode.close-node-library' : 'rulenode.open-node-library') | translate }}"
|
||||
matTooltipPosition="above">
|
||||
<mat-icon class="tb-library-node-btn-icon" [ngClass]="{'tb-library-node-btn-icon-toggled' : drawer.opened}">chevron_right</mat-icon>
|
||||
</button>
|
||||
<button #versionControlButton
|
||||
*ngIf="!isImport"
|
||||
color="primary"
|
||||
|
||||
@ -54,20 +54,43 @@
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
section.tb-header-buttons.tb-library-open {
|
||||
.tb-library-node-btn {
|
||||
width: 20px;
|
||||
height: 90px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
top: 50%;
|
||||
z-index: 1000;
|
||||
opacity: 0.5;
|
||||
border-radius: 0 15px 15px 0 !important;
|
||||
transform: translateY(-50%);
|
||||
&-icon {
|
||||
transition: transform 0.4s !important;
|
||||
&-toggled {
|
||||
transform: rotateZ(180deg);
|
||||
}
|
||||
}
|
||||
&:before {
|
||||
width: 20px;
|
||||
height: 30px;
|
||||
top: -28px;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
.mdc-fab.tb-btn-open-library {
|
||||
top: 0;
|
||||
border-radius: 0 0 0 15px;
|
||||
box-shadow: -10px 0px 0 0px #305680;
|
||||
border: none;
|
||||
}
|
||||
&:after {
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 30px;
|
||||
bottom: -28px;
|
||||
left: 0;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
margin: 4px 0 0 4px;
|
||||
line-height: 36px;
|
||||
opacity: .5;
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
border-top-left-radius: 15px;
|
||||
box-shadow: -10px 0px 0px 0px #305680;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
@ -77,8 +100,7 @@
|
||||
min-width: 250px;
|
||||
|
||||
.mat-toolbar {
|
||||
height: 48px;
|
||||
min-height: 48px;
|
||||
height: min-content;
|
||||
padding: 0;
|
||||
|
||||
.mat-toolbar-tools {
|
||||
|
||||
@ -15,7 +15,9 @@
|
||||
///
|
||||
|
||||
import {
|
||||
AfterViewChecked,
|
||||
AfterViewInit,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
@ -90,6 +92,7 @@ import { MatMiniFabButton } from '@angular/material/button';
|
||||
import { TbPopoverService } from '@shared/components/popover.service';
|
||||
import { VersionControlComponent } from '@home/components/vc/version-control.component';
|
||||
import { ComponentClusteringMode } from '@shared/models/component-descriptor.models';
|
||||
import { MatDrawer } from '@angular/material/sidenav';
|
||||
import Timeout = NodeJS.Timeout;
|
||||
|
||||
@Component({
|
||||
@ -99,7 +102,7 @@ import Timeout = NodeJS.Timeout;
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class RuleChainPageComponent extends PageComponent
|
||||
implements AfterViewInit, OnInit, OnDestroy, HasDirtyFlag, ISearchableComponent {
|
||||
implements AfterViewInit, OnInit, OnDestroy, HasDirtyFlag, ISearchableComponent, AfterViewChecked {
|
||||
|
||||
get isDirty(): boolean {
|
||||
return this.isDirtyValue || this.isImport;
|
||||
@ -121,6 +124,8 @@ export class RuleChainPageComponent extends PageComponent
|
||||
|
||||
@ViewChild('ruleChainMenuTrigger', {static: true}) ruleChainMenuTrigger: MatMenuTrigger;
|
||||
|
||||
@ViewChild('drawer') drawer: MatDrawer;
|
||||
|
||||
eventTypes = EventType;
|
||||
|
||||
debugEventTypes = DebugEventType;
|
||||
@ -159,7 +164,6 @@ export class RuleChainPageComponent extends PageComponent
|
||||
hotKeys: Hotkey[] = [];
|
||||
|
||||
enableHotKeys = true;
|
||||
isLibraryOpen = true;
|
||||
|
||||
ruleNodeSearch = '';
|
||||
ruleNodeTypeSearch = '';
|
||||
@ -266,6 +270,7 @@ export class RuleChainPageComponent extends PageComponent
|
||||
private popoverService: TbPopoverService,
|
||||
private renderer: Renderer2,
|
||||
private viewContainerRef: ViewContainerRef,
|
||||
private changeDetector: ChangeDetectorRef,
|
||||
public dialog: MatDialog,
|
||||
public dialogService: DialogService,
|
||||
public fb: UntypedFormBuilder) {
|
||||
@ -281,6 +286,10 @@ export class RuleChainPageComponent extends PageComponent
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
ngAfterViewChecked(){
|
||||
this.changeDetector.detectChanges();
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
fromEvent(this.ruleNodeSearchInputField.nativeElement, 'keyup')
|
||||
.pipe(
|
||||
@ -299,6 +308,14 @@ export class RuleChainPageComponent extends PageComponent
|
||||
this.rxSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
currentRuleChainIdChanged(ruleChainId: string) {
|
||||
if (this.ruleChainType === RuleChainType.CORE) {
|
||||
this.router.navigateByUrl(`ruleChains/${ruleChainId}`);
|
||||
} else {
|
||||
this.router.navigateByUrl(`edgeManagement/ruleChains/${ruleChainId}`);
|
||||
}
|
||||
}
|
||||
|
||||
onSearchTextUpdated(searchText: string) {
|
||||
this.ruleNodeSearch = searchText;
|
||||
this.updateRuleNodesHighlight();
|
||||
|
||||
@ -29,6 +29,14 @@
|
||||
(click)="clear()">
|
||||
<mat-icon class="material-icons">close</mat-icon>
|
||||
</button>
|
||||
<button *ngIf="selectTemplateFormGroup.get('templateName').value && !disabled && allowEdit"
|
||||
type="button"
|
||||
matSuffix mat-icon-button aria-label="Edit"
|
||||
matTooltip="{{ 'notification.edit-notification-template' | translate }}"
|
||||
matTooltipPosition="above"
|
||||
(click)="editTemplate($event)">
|
||||
<mat-icon class="material-icons">edit</mat-icon>
|
||||
</button>
|
||||
<button #createTemplateButton
|
||||
mat-button color="primary" matSuffix
|
||||
*ngIf="allowCreate && !selectTemplateFormGroup.get('templateName').value && !disabled"
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
|
||||
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { catchError, debounceTime, distinctUntilChanged, map, share, switchMap, tap } from 'rxjs/operators';
|
||||
import { catchError, debounceTime, map, share, switchMap, tap } from 'rxjs/operators';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppState } from '@core/core.state';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
@ -66,6 +66,10 @@ export class TemplateAutocompleteComponent implements ControlValueAccessor, OnIn
|
||||
@coerceBoolean()
|
||||
allowCreate = false;
|
||||
|
||||
@Input()
|
||||
@coerceBoolean()
|
||||
allowEdit = false;
|
||||
|
||||
|
||||
@Input()
|
||||
disabled: boolean;
|
||||
@ -196,18 +200,34 @@ export class TemplateAutocompleteComponent implements ControlValueAccessor, OnIn
|
||||
}, 0);
|
||||
}
|
||||
|
||||
editTemplate($event: Event) {
|
||||
if ($event) {
|
||||
$event.stopPropagation();
|
||||
}
|
||||
this.notificationService.getNotificationTemplateById(this.modelValue.id).subscribe(
|
||||
(template) => {
|
||||
this.openNotificationTemplateDialog({template});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
createTemplate($event: Event, button: MatButton) {
|
||||
if ($event) {
|
||||
$event.stopPropagation();
|
||||
}
|
||||
button._elementRef.nativeElement.blur();
|
||||
this.openNotificationTemplateDialog({
|
||||
isAdd: true,
|
||||
predefinedType: this.notificationTypes
|
||||
});
|
||||
}
|
||||
|
||||
private openNotificationTemplateDialog(dialogData?: TemplateNotificationDialogData) {
|
||||
this.dialog.open<TemplateNotificationDialogComponent, TemplateNotificationDialogData,
|
||||
NotificationTemplate>(TemplateNotificationDialogComponent, {
|
||||
disableClose: true,
|
||||
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
|
||||
data: {
|
||||
predefinedType: this.notificationTypes
|
||||
}
|
||||
data: dialogData
|
||||
}).afterClosed()
|
||||
.subscribe((res) => {
|
||||
if (res) {
|
||||
|
||||
@ -0,0 +1,27 @@
|
||||
<!--
|
||||
|
||||
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.
|
||||
|
||||
-->
|
||||
<mat-select fxFlex
|
||||
class="tb-rule-chain-select"
|
||||
[required]="required"
|
||||
[disabled]="disabled"
|
||||
[(ngModel)]="ruleChainId"
|
||||
(ngModelChange)="ruleChainIdChanged()">
|
||||
<mat-option *ngFor="let ruleChain of ruleChains$ | async" [value]="ruleChain.id.id">
|
||||
{{ruleChain.name}}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
@ -0,0 +1,26 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
:host {
|
||||
min-width: 52px;
|
||||
width: 100%;
|
||||
padding: 0 6px;
|
||||
.tb-rule-chain-select {
|
||||
display: flex;
|
||||
height: 48px;
|
||||
min-height: 100%;
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,109 @@
|
||||
///
|
||||
/// 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 { Component, forwardRef, Input, OnInit } from '@angular/core';
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
import { Observable } from 'rxjs';
|
||||
import { PageLink } from '@shared/models/page/page-link';
|
||||
import { map, share } from 'rxjs/operators';
|
||||
import { PageData } from '@shared/models/page/page-data';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppState } from '@app/core/core.state';
|
||||
import { TooltipPosition } from '@angular/material/tooltip';
|
||||
import { RuleChain, RuleChainType } from '@shared/models/rule-chain.models';
|
||||
import { RuleChainService } from '@core/http/rule-chain.service';
|
||||
import { isDefinedAndNotNull } from '@core/utils';
|
||||
import { coerceBoolean } from '@shared/decorators/coercion';
|
||||
import { Direction } from '@shared/models/page/sort-order';
|
||||
|
||||
@Component({
|
||||
selector: 'tb-rule-chain-select',
|
||||
templateUrl: './rule-chain-select.component.html',
|
||||
styleUrls: ['./rule-chain-select.component.scss'],
|
||||
providers: [{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => RuleChainSelectComponent),
|
||||
multi: true
|
||||
}]
|
||||
})
|
||||
export class RuleChainSelectComponent implements ControlValueAccessor, OnInit {
|
||||
|
||||
@Input()
|
||||
tooltipPosition: TooltipPosition = 'above';
|
||||
|
||||
@Input()
|
||||
@coerceBoolean()
|
||||
required: boolean;
|
||||
|
||||
@Input()
|
||||
@coerceBoolean()
|
||||
disabled: boolean;
|
||||
|
||||
@Input()
|
||||
ruleChainType: RuleChainType = RuleChainType.CORE;
|
||||
|
||||
ruleChains$: Observable<Array<RuleChain>>;
|
||||
|
||||
ruleChainId: string | null;
|
||||
|
||||
private propagateChange = (v: any) => { };
|
||||
|
||||
constructor(private ruleChainService: RuleChainService) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
const pageLink = new PageLink(100, 0, null, {
|
||||
property: 'name',
|
||||
direction: Direction.ASC
|
||||
});
|
||||
|
||||
this.ruleChains$ = this.getRuleChains(pageLink).pipe(
|
||||
map((pageData) => pageData.data),
|
||||
share()
|
||||
);
|
||||
}
|
||||
|
||||
registerOnChange(fn: any): void {
|
||||
this.propagateChange = fn;
|
||||
}
|
||||
|
||||
registerOnTouched(fn: any): void {
|
||||
}
|
||||
|
||||
|
||||
setDisabledState(isDisabled: boolean): void {
|
||||
this.disabled = isDisabled;
|
||||
}
|
||||
|
||||
writeValue(value: string | null): void {
|
||||
if (isDefinedAndNotNull(value)) {
|
||||
this.ruleChainId = value;
|
||||
}
|
||||
}
|
||||
|
||||
ruleChainIdChanged() {
|
||||
this.updateView();
|
||||
}
|
||||
|
||||
private updateView() {
|
||||
this.propagateChange(this.ruleChainId);
|
||||
}
|
||||
|
||||
private getRuleChains(pageLink: PageLink): Observable<PageData<RuleChain>> {
|
||||
return this.ruleChainService.getRuleChains(pageLink, this.ruleChainType, {ignoreLoading: true});
|
||||
}
|
||||
|
||||
}
|
||||
@ -191,6 +191,7 @@ import {
|
||||
import { ColorPickerComponent } from '@shared/components/color-picker/color-picker.component';
|
||||
import { ShortNumberPipe } from '@shared/pipe/short-number.pipe';
|
||||
import { ToggleHeaderComponent } from '@shared/components/toggle-header.component';
|
||||
import { RuleChainSelectComponent } from '@shared/components/rule-chain/rule-chain-select.component';
|
||||
|
||||
export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) {
|
||||
return markedOptionsService;
|
||||
@ -360,7 +361,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
|
||||
GtMdLgLayoutGapDirective,
|
||||
GtMdLgShowHideDirective,
|
||||
ColorPickerComponent,
|
||||
ToggleHeaderComponent
|
||||
ToggleHeaderComponent,
|
||||
RuleChainSelectComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
@ -586,7 +588,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
|
||||
GtMdLgLayoutGapDirective,
|
||||
GtMdLgShowHideDirective,
|
||||
ColorPickerComponent,
|
||||
ToggleHeaderComponent
|
||||
ToggleHeaderComponent,
|
||||
RuleChainSelectComponent
|
||||
]
|
||||
})
|
||||
export class SharedModule { }
|
||||
|
||||
@ -3349,6 +3349,7 @@
|
||||
"events": "Events",
|
||||
"search": "Search nodes",
|
||||
"open-node-library": "Open node library",
|
||||
"close-node-library": "Close node library",
|
||||
"add": "Add rule node",
|
||||
"name": "Name",
|
||||
"name-required": "Name is required.",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user