Merge remote-tracking branch 'upstream/develop/3.5' into feature/notification-system

This commit is contained in:
Vladyslav_Prykhodko 2023-03-24 13:42:44 +02:00
commit eea6e5a5e6
16 changed files with 586 additions and 445 deletions

View File

@ -422,3 +422,190 @@ END
$$; $$;
-- ALARM FUNCTIONS END -- ALARM FUNCTIONS END
-- TTL DROP PARTITIONS FUNCTIONS UPDATE START
DROP PROCEDURE IF EXISTS drop_partitions_by_max_ttl(character varying, bigint, bigint);
DROP FUNCTION IF EXISTS get_partition_by_max_ttl_date;
CREATE OR REPLACE FUNCTION get_partition_by_system_ttl_date(IN partition_type varchar, IN date timestamp, OUT partition varchar) AS
$$
BEGIN
CASE
WHEN partition_type = 'DAYS' THEN
partition := 'ts_kv_' || to_char(date, 'yyyy') || '_' || to_char(date, 'MM') || '_' || to_char(date, 'dd');
WHEN partition_type = 'MONTHS' THEN
partition := 'ts_kv_' || to_char(date, 'yyyy') || '_' || to_char(date, 'MM');
WHEN partition_type = 'YEARS' THEN
partition := 'ts_kv_' || to_char(date, 'yyyy');
ELSE
partition := NULL;
END CASE;
IF partition IS NOT NULL THEN
IF NOT EXISTS(SELECT
FROM pg_tables
WHERE schemaname = 'public'
AND tablename = partition) THEN
partition := NULL;
RAISE NOTICE 'Failed to found partition by ttl';
END IF;
END IF;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE PROCEDURE drop_partitions_by_system_ttl(IN partition_type varchar, IN system_ttl bigint, INOUT deleted bigint)
LANGUAGE plpgsql AS
$$
DECLARE
date timestamp;
partition_by_max_ttl_date varchar;
partition_by_max_ttl_month varchar;
partition_by_max_ttl_day varchar;
partition_by_max_ttl_year varchar;
partition varchar;
partition_year integer;
partition_month integer;
partition_day integer;
BEGIN
if system_ttl IS NOT NULL AND system_ttl > 0 THEN
date := to_timestamp(EXTRACT(EPOCH FROM current_timestamp) - system_ttl);
partition_by_max_ttl_date := get_partition_by_system_ttl_date(partition_type, date);
RAISE NOTICE 'Date by max ttl: %', date;
RAISE NOTICE 'Partition by max ttl: %', partition_by_max_ttl_date;
IF partition_by_max_ttl_date IS NOT NULL THEN
CASE
WHEN partition_type = 'DAYS' THEN
partition_by_max_ttl_year := SPLIT_PART(partition_by_max_ttl_date, '_', 3);
partition_by_max_ttl_month := SPLIT_PART(partition_by_max_ttl_date, '_', 4);
partition_by_max_ttl_day := SPLIT_PART(partition_by_max_ttl_date, '_', 5);
WHEN partition_type = 'MONTHS' THEN
partition_by_max_ttl_year := SPLIT_PART(partition_by_max_ttl_date, '_', 3);
partition_by_max_ttl_month := SPLIT_PART(partition_by_max_ttl_date, '_', 4);
ELSE
partition_by_max_ttl_year := SPLIT_PART(partition_by_max_ttl_date, '_', 3);
END CASE;
IF partition_by_max_ttl_year IS NULL THEN
RAISE NOTICE 'Failed to remove partitions by max ttl date due to partition_by_max_ttl_year is null!';
ELSE
IF partition_type = 'YEARS' THEN
FOR partition IN SELECT tablename
FROM pg_tables
WHERE schemaname = 'public'
AND tablename like 'ts_kv_' || '%'
AND tablename != 'ts_kv_latest'
AND tablename != 'ts_kv_dictionary'
AND tablename != 'ts_kv_indefinite'
AND tablename != partition_by_max_ttl_date
LOOP
partition_year := SPLIT_PART(partition, '_', 3)::integer;
IF partition_year < partition_by_max_ttl_year::integer THEN
RAISE NOTICE 'Partition to delete by max ttl: %', partition;
EXECUTE format('DROP TABLE IF EXISTS %I', partition);
deleted := deleted + 1;
END IF;
END LOOP;
ELSE
IF partition_type = 'MONTHS' THEN
IF partition_by_max_ttl_month IS NULL THEN
RAISE NOTICE 'Failed to remove months partitions by max ttl date due to partition_by_max_ttl_month is null!';
ELSE
FOR partition IN SELECT tablename
FROM pg_tables
WHERE schemaname = 'public'
AND tablename like 'ts_kv_' || '%'
AND tablename != 'ts_kv_latest'
AND tablename != 'ts_kv_dictionary'
AND tablename != 'ts_kv_indefinite'
AND tablename != partition_by_max_ttl_date
LOOP
partition_year := SPLIT_PART(partition, '_', 3)::integer;
IF partition_year > partition_by_max_ttl_year::integer THEN
RAISE NOTICE 'Skip iteration! Partition: % is valid!', partition;
CONTINUE;
ELSE
IF partition_year < partition_by_max_ttl_year::integer THEN
RAISE NOTICE 'Partition to delete by max ttl: %', partition;
EXECUTE format('DROP TABLE IF EXISTS %I', partition);
deleted := deleted + 1;
ELSE
partition_month := SPLIT_PART(partition, '_', 4)::integer;
IF partition_year = partition_by_max_ttl_year::integer THEN
IF partition_month >= partition_by_max_ttl_month::integer THEN
RAISE NOTICE 'Skip iteration! Partition: % is valid!', partition;
CONTINUE;
ELSE
RAISE NOTICE 'Partition to delete by max ttl: %', partition;
EXECUTE format('DROP TABLE IF EXISTS %I', partition);
deleted := deleted + 1;
END IF;
END IF;
END IF;
END IF;
END LOOP;
END IF;
ELSE
IF partition_type = 'DAYS' THEN
IF partition_by_max_ttl_month IS NULL THEN
RAISE NOTICE 'Failed to remove days partitions by max ttl date due to partition_by_max_ttl_month is null!';
ELSE
IF partition_by_max_ttl_day IS NULL THEN
RAISE NOTICE 'Failed to remove days partitions by max ttl date due to partition_by_max_ttl_day is null!';
ELSE
FOR partition IN SELECT tablename
FROM pg_tables
WHERE schemaname = 'public'
AND tablename like 'ts_kv_' || '%'
AND tablename != 'ts_kv_latest'
AND tablename != 'ts_kv_dictionary'
AND tablename != 'ts_kv_indefinite'
AND tablename != partition_by_max_ttl_date
LOOP
partition_year := SPLIT_PART(partition, '_', 3)::integer;
IF partition_year > partition_by_max_ttl_year::integer THEN
RAISE NOTICE 'Skip iteration! Partition: % is valid!', partition;
CONTINUE;
ELSE
IF partition_year < partition_by_max_ttl_year::integer THEN
RAISE NOTICE 'Partition to delete by max ttl: %', partition;
EXECUTE format('DROP TABLE IF EXISTS %I', partition);
deleted := deleted + 1;
ELSE
partition_month := SPLIT_PART(partition, '_', 4)::integer;
IF partition_month > partition_by_max_ttl_month::integer THEN
RAISE NOTICE 'Skip iteration! Partition: % is valid!', partition;
CONTINUE;
ELSE
IF partition_month < partition_by_max_ttl_month::integer THEN
RAISE NOTICE 'Partition to delete by max ttl: %', partition;
EXECUTE format('DROP TABLE IF EXISTS %I', partition);
deleted := deleted + 1;
ELSE
partition_day := SPLIT_PART(partition, '_', 5)::integer;
IF partition_day >= partition_by_max_ttl_day::integer THEN
RAISE NOTICE 'Skip iteration! Partition: % is valid!', partition;
CONTINUE;
ELSE
IF partition_day < partition_by_max_ttl_day::integer THEN
RAISE NOTICE 'Partition to delete by max ttl: %', partition;
EXECUTE format('DROP TABLE IF EXISTS %I', partition);
deleted := deleted + 1;
END IF;
END IF;
END IF;
END IF;
END IF;
END IF;
END LOOP;
END IF;
END IF;
END IF;
END IF;
END IF;
END IF;
END IF;
END IF;
END
$$;
-- TTL DROP PARTITIONS FUNCTIONS UPDATE END

View File

@ -139,6 +139,9 @@ public class AlarmController extends BaseController {
checkNotNull(alarm.getOriginator()); checkNotNull(alarm.getOriginator());
checkEntity(alarm.getId(), alarm, Resource.ALARM); checkEntity(alarm.getId(), alarm, Resource.ALARM);
checkEntityId(alarm.getOriginator(), Operation.READ); checkEntityId(alarm.getOriginator(), Operation.READ);
if (alarm.getAssigneeId() != null) {
checkUserId(alarm.getAssigneeId(), Operation.READ);
}
return tbAlarmService.save(alarm, getCurrentUser()); return tbAlarmService.save(alarm, getCurrentUser());
} }

View File

@ -76,7 +76,7 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb
UserId newAssignee = alarm.getAssigneeId(); UserId newAssignee = alarm.getAssigneeId();
UserId curAssignee = resultAlarm.getAssigneeId(); UserId curAssignee = resultAlarm.getAssigneeId();
if (newAssignee != null && !newAssignee.equals(curAssignee)) { if (newAssignee != null && !newAssignee.equals(curAssignee)) {
resultAlarm = assign(alarm, newAssignee, alarm.getAssignTs(), user); resultAlarm = assign(resultAlarm, newAssignee, alarm.getAssignTs(), user);
} else if (newAssignee == null && curAssignee != null) { } else if (newAssignee == null && curAssignee != null) {
resultAlarm = unassign(alarm, alarm.getAssignTs(), user); resultAlarm = unassign(alarm, alarm.getAssignTs(), user);
} }

View File

@ -357,6 +357,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
protected Tenant savedDifferentTenant; protected Tenant savedDifferentTenant;
protected User savedDifferentTenantUser; protected User savedDifferentTenantUser;
private Customer savedDifferentCustomer; private Customer savedDifferentCustomer;
protected User differentCustomerUser;
protected void loginDifferentTenant() throws Exception { protected void loginDifferentTenant() throws Exception {
if (savedDifferentTenant != null) { if (savedDifferentTenant != null) {
@ -387,7 +388,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
createDifferentCustomer(); createDifferentCustomer();
loginTenantAdmin(); loginTenantAdmin();
User differentCustomerUser = new User(); differentCustomerUser = new User();
differentCustomerUser.setAuthority(Authority.CUSTOMER_USER); differentCustomerUser.setAuthority(Authority.CUSTOMER_USER);
differentCustomerUser.setTenantId(tenantId); differentCustomerUser.setTenantId(tenantId);
differentCustomerUser.setCustomerId(savedDifferentCustomer.getId()); differentCustomerUser.setCustomerId(savedDifferentCustomer.getId());

View File

@ -656,4 +656,38 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest {
return foundAlarm; return foundAlarm;
} }
@Test
public void testCreateAlarmWithOtherTenantsAssignee() throws Exception {
loginDifferentTenant();
loginTenantAdmin();
Alarm alarm = Alarm.builder()
.tenantId(tenantId)
.customerId(customerId)
.originator(customerDevice.getId())
.severity(AlarmSeverity.CRITICAL)
.assigneeId(savedDifferentTenantUser.getId())
.build();
doPost("/api/alarm", alarm).andExpect(status().isForbidden());
}
@Test
public void testCreateAlarmWithOtherCustomerAsAssignee() throws Exception {
loginDifferentCustomer();
loginCustomerUser();
Alarm alarm = Alarm.builder()
.tenantId(tenantId)
.customerId(customerId)
.originator(customerDevice.getId())
.severity(AlarmSeverity.CRITICAL)
.assigneeId(differentCustomerUser.getId())
.build();
doPost("/api/alarm", alarm).andExpect(status().isForbidden());
}
} }

View File

@ -99,14 +99,16 @@ public class JpaSqlTimeseriesDao extends AbstractChunkedAggregationTimeseriesDao
@Override @Override
public void cleanup(long systemTtl) { public void cleanup(long systemTtl) {
cleanupPartitions(systemTtl); if (systemTtl > 0) {
cleanupPartitions(systemTtl);
}
super.cleanup(systemTtl); super.cleanup(systemTtl);
} }
private void cleanupPartitions(long systemTtl) { private void cleanupPartitions(long systemTtl) {
log.info("Going to cleanup old timeseries data partitions using partition type: {} and ttl: {}s", partitioning, systemTtl); log.info("Going to cleanup old timeseries data partitions using partition type: {} and ttl: {}s", partitioning, systemTtl);
try (Connection connection = dataSource.getConnection(); try (Connection connection = dataSource.getConnection();
PreparedStatement stmt = connection.prepareStatement("call drop_partitions_by_max_ttl(?,?,?)")) { PreparedStatement stmt = connection.prepareStatement("call drop_partitions_by_system_ttl(?,?,?)")) {
stmt.setString(1, partitioning); stmt.setString(1, partitioning);
stmt.setLong(2, systemTtl); stmt.setLong(2, systemTtl);
stmt.setLong(3, 0); stmt.setLong(3, 0);

View File

@ -34,13 +34,10 @@ CREATE TABLE IF NOT EXISTS ts_kv_dictionary
CONSTRAINT ts_key_id_pkey PRIMARY KEY (key) CONSTRAINT ts_key_id_pkey PRIMARY KEY (key)
); );
CREATE OR REPLACE PROCEDURE drop_partitions_by_max_ttl(IN partition_type varchar, IN system_ttl bigint, INOUT deleted bigint) CREATE OR REPLACE PROCEDURE drop_partitions_by_system_ttl(IN partition_type varchar, IN system_ttl bigint, INOUT deleted bigint)
LANGUAGE plpgsql AS LANGUAGE plpgsql AS
$$ $$
DECLARE DECLARE
max_tenant_ttl bigint;
max_customer_ttl bigint;
max_ttl bigint;
date timestamp; date timestamp;
partition_by_max_ttl_date varchar; partition_by_max_ttl_date varchar;
partition_by_max_ttl_month varchar; partition_by_max_ttl_month varchar;
@ -52,20 +49,9 @@ DECLARE
partition_day integer; partition_day integer;
BEGIN BEGIN
SELECT max(attribute_kv.long_v) if system_ttl IS NOT NULL AND system_ttl > 0 THEN
FROM tenant date := to_timestamp(EXTRACT(EPOCH FROM current_timestamp) - system_ttl);
INNER JOIN attribute_kv ON tenant.id = attribute_kv.entity_id partition_by_max_ttl_date := get_partition_by_system_ttl_date(partition_type, date);
WHERE attribute_kv.attribute_key = 'TTL'
into max_tenant_ttl;
SELECT max(attribute_kv.long_v)
FROM customer
INNER JOIN attribute_kv ON customer.id = attribute_kv.entity_id
WHERE attribute_kv.attribute_key = 'TTL'
into max_customer_ttl;
max_ttl := GREATEST(system_ttl, max_customer_ttl, max_tenant_ttl);
if max_ttl IS NOT NULL AND max_ttl > 0 THEN
date := to_timestamp(EXTRACT(EPOCH FROM current_timestamp) - max_ttl);
partition_by_max_ttl_date := get_partition_by_max_ttl_date(partition_type, date);
RAISE NOTICE 'Date by max ttl: %', date; RAISE NOTICE 'Date by max ttl: %', date;
RAISE NOTICE 'Partition by max ttl: %', partition_by_max_ttl_date; RAISE NOTICE 'Partition by max ttl: %', partition_by_max_ttl_date;
IF partition_by_max_ttl_date IS NOT NULL THEN IF partition_by_max_ttl_date IS NOT NULL THEN
@ -203,7 +189,7 @@ BEGIN
END END
$$; $$;
CREATE OR REPLACE FUNCTION get_partition_by_max_ttl_date(IN partition_type varchar, IN date timestamp, OUT partition varchar) AS CREATE OR REPLACE FUNCTION get_partition_by_system_ttl_date(IN partition_type varchar, IN date timestamp, OUT partition varchar) AS
$$ $$
BEGIN BEGIN
CASE CASE

View File

@ -421,6 +421,13 @@ export class MenuService {
type: 'link', type: 'link',
path: '/customers', path: '/customers',
icon: 'supervisor_account' icon: 'supervisor_account'
},
{
id: guid(),
name: 'rulechain.rulechains',
type: 'link',
path: '/ruleChains',
icon: 'settings_ethernet'
} }
); );
if (authState.edgesSupportEnabled) { if (authState.edgesSupportEnabled) {
@ -458,13 +465,6 @@ export class MenuService {
path: '/features', path: '/features',
icon: 'construction', icon: 'construction',
pages: [ pages: [
{
id: guid(),
name: 'rulechain.rulechains',
type: 'link',
path: '/features/ruleChains',
icon: 'settings_ethernet'
},
{ {
id: guid(), id: guid(),
name: 'ota-update.ota-updates', name: 'ota-update.ota-updates',

View File

@ -17,7 +17,6 @@
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
import { Authority } from '@shared/models/authority.enum'; import { Authority } from '@shared/models/authority.enum';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { ruleChainsRoutes } from '@home/pages/rulechain/rulechain-routing.module';
import { otaUpdatesRoutes } from '@home/pages/ota-update/ota-update-routing.module'; import { otaUpdatesRoutes } from '@home/pages/ota-update/ota-update-routing.module';
import { vcRoutes } from '@home/pages/vc/vc-routing.module'; import { vcRoutes } from '@home/pages/vc/vc-routing.module';
@ -37,10 +36,9 @@ const routes: Routes = [
children: [], children: [],
data: { data: {
auth: [Authority.TENANT_ADMIN], auth: [Authority.TENANT_ADMIN],
redirectTo: '/features/ruleChains' redirectTo: '/features/otaUpdates'
} }
}, },
...ruleChainsRoutes,
...otaUpdatesRoutes, ...otaUpdatesRoutes,
...vcRoutes ...vcRoutes
] ]

View File

@ -122,7 +122,7 @@ export const importRuleChainBreadcumbLabelFunction: BreadCrumbLabelFunction<Rule
return `${translate.instant('rulechain.import')}: ${component.ruleChain.name}`; return `${translate.instant('rulechain.import')}: ${component.ruleChain.name}`;
}); });
export const ruleChainsRoutes: Routes = [ const routes: Routes = [
{ {
path: 'ruleChains', path: 'ruleChains',
data: { data: {
@ -189,24 +189,6 @@ export const ruleChainsRoutes: Routes = [
} }
]; ];
const routes: Routes = [
{
path: 'ruleChains',
pathMatch: 'full',
redirectTo: '/features/ruleChains'
},
{
path: 'ruleChains/:ruleChainId',
pathMatch: 'full',
redirectTo: '/features/ruleChains/:ruleChainId'
},
{
path: 'ruleChains/ruleChain/import',
pathMatch: 'full',
redirectTo: '/features/ruleChains/ruleChain/import'
}
];
// @dynamic // @dynamic
@NgModule({ @NgModule({
imports: [RouterModule.forChild(routes)], imports: [RouterModule.forChild(routes)],

View File

@ -15,222 +15,229 @@
limitations under the License. limitations under the License.
--> -->
<form [formGroup]="timewindowForm" (ngSubmit)="update()"> <form [formGroup]="timewindowForm" class="mat-content">
<fieldset [disabled]="(isLoading$ | async)"> <mat-tab-group [ngClass]="{'tb-headless': historyOnly}"
<div class="mat-content" fxLayout="column"> (selectedIndexChange)="timewindowForm.markAsDirty()" [(selectedIndex)]="timewindow.selectedTab">
<mat-tab-group dynamicHeight [ngClass]="{'tb-headless': historyOnly}" <mat-tab label="{{ 'timewindow.realtime' | translate }}">
(selectedIndexChange)="timewindowForm.markAsDirty()" [(selectedIndex)]="timewindow.selectedTab"> <section fxLayout="row">
<mat-tab label="{{ 'timewindow.realtime' | translate }}"> <section *ngIf="isEdit" fxLayout="column" fxLayoutAlign="start center"
<section fxLayout="row"> style="padding-top: 8px; padding-left: 16px;">
<section *ngIf="isEdit" fxLayout="column" fxLayoutAlign="start center" style="padding-top: 8px; padding-left: 16px;"> <label class="tb-small hide-label" translate>timewindow.hide</label>
<label class="tb-small hide-label" translate>timewindow.hide</label> <mat-checkbox [ngModelOptions]="{standalone: true}" [(ngModel)]="timewindow.hideInterval"
<mat-checkbox [ngModelOptions]="{standalone: true}" [(ngModel)]="timewindow.hideInterval" (ngModelChange)="onHideIntervalChanged()"></mat-checkbox>
(ngModelChange)="onHideIntervalChanged()"></mat-checkbox>
</section>
<section fxLayout="column" fxFlex [fxShow]="isEdit || !timewindow.hideInterval">
<div formGroupName="realtime" class="mat-content mat-padding" style="padding-top: 8px;">
<mat-radio-group *ngIf="!quickIntervalOnly" [fxShow]="isEdit || (!timewindow.hideLastInterval && !timewindow.hideQuickInterval)"
formControlName="realtimeType">
<mat-radio-button [value]="realtimeTypes.LAST_INTERVAL" color="primary">
<section fxLayout="row">
<section *ngIf="isEdit" fxLayout="column" fxLayoutAlign="start center" style="padding-right: 8px;">
<label class="tb-small hide-label" translate>timewindow.hide</label>
<mat-checkbox [ngModelOptions]="{standalone: true}" [(ngModel)]="timewindow.hideLastInterval"
(ngModelChange)="onHideLastIntervalChanged()"></mat-checkbox>
</section>
<section fxLayout="column">
<span translate>timewindow.last</span>
<tb-timeinterval
formControlName="timewindowMs"
predefinedName="timewindow.last"
[fxShow]="timewindowForm.get('realtime.realtimeType').value === realtimeTypes.LAST_INTERVAL"
[required]="timewindow.selectedTab === timewindowTypes.REALTIME &&
timewindowForm.get('realtime.realtimeType').value === realtimeTypes.LAST_INTERVAL"
style="padding-top: 8px;"></tb-timeinterval>
</section>
</section>
</mat-radio-button>
<mat-radio-button [value]="realtimeTypes.INTERVAL" color="primary">
<section fxLayout="row">
<section *ngIf="isEdit" fxLayout="column" fxLayoutAlign="start center" style="padding-right: 8px;">
<label class="tb-small hide-label" translate>timewindow.hide</label>
<mat-checkbox [ngModelOptions]="{standalone: true}" [(ngModel)]="timewindow.hideQuickInterval"
(ngModelChange)="onHideQuickIntervalChanged()"></mat-checkbox>
</section>
<section fxLayout="column">
<span translate>timewindow.interval</span>
<tb-quick-time-interval
formControlName="quickInterval"
onlyCurrentInterval="true"
[fxShow]="timewindowForm.get('realtime.realtimeType').value === realtimeTypes.INTERVAL"
[required]="timewindow.selectedTab === timewindowTypes.REALTIME &&
timewindowForm.get('realtime.realtimeType').value === realtimeTypes.INTERVAL"
style="padding-top: 8px"></tb-quick-time-interval>
</section>
</section>
</mat-radio-button>
</mat-radio-group>
<tb-timeinterval *ngIf="!isEdit && !timewindow.hideLastInterval && timewindow.hideQuickInterval"
formControlName="timewindowMs"
predefinedName="timewindow.last"
required
style="padding-top: 8px;"></tb-timeinterval>
<tb-quick-time-interval *ngIf="quickIntervalOnly || !isEdit && timewindow.hideLastInterval && !timewindow.hideQuickInterval"
formControlName="quickInterval"
onlyCurrentInterval="true"
required
style="padding-top: 8px"></tb-quick-time-interval>
</div>
</section>
</section>
</mat-tab>
<mat-tab label="{{ 'timewindow.history' | translate }}">
<section fxLayout="row">
<section *ngIf="isEdit" fxLayout="column" fxLayoutAlign="start center" style="padding-top: 8px; padding-left: 16px;">
<label class="tb-small hide-label" translate>timewindow.hide</label>
<mat-checkbox [ngModelOptions]="{standalone: true}" [(ngModel)]="timewindow.hideInterval"
(ngModelChange)="onHideIntervalChanged()"></mat-checkbox>
</section>
<section fxLayout="column" fxFlex [fxShow]="isEdit || !timewindow.hideInterval">
<div formGroupName="history" class="mat-content mat-padding" style="padding-top: 8px;">
<mat-radio-group formControlName="historyType">
<mat-radio-button [value]="historyTypes.LAST_INTERVAL" color="primary">
<section fxLayout="column">
<span translate>timewindow.last</span>
<tb-timeinterval
formControlName="timewindowMs"
predefinedName="timewindow.last"
class="history-time-input"
[fxShow]="timewindowForm.get('history.historyType').value === historyTypes.LAST_INTERVAL"
[required]="timewindow.selectedTab === timewindowTypes.HISTORY &&
timewindowForm.get('history.historyType').value === historyTypes.LAST_INTERVAL"
style="padding-top: 8px;"></tb-timeinterval>
</section>
</mat-radio-button>
<mat-radio-button [value]="historyTypes.FIXED" color="primary">
<section fxLayout="column">
<span translate>timewindow.time-period</span>
<tb-datetime-period
formControlName="fixedTimewindow"
class="history-time-input"
[fxShow]="timewindowForm.get('history.historyType').value === historyTypes.FIXED"
[required]="timewindow.selectedTab === timewindowTypes.HISTORY &&
timewindowForm.get('history.historyType').value === historyTypes.FIXED"
style="padding-top: 8px;"></tb-datetime-period>
</section>
</mat-radio-button>
<mat-radio-button [value]="historyTypes.INTERVAL" color="primary">
<section fxLayout="column">
<span translate>timewindow.interval</span>
<tb-quick-time-interval
formControlName="quickInterval"
class="history-time-input"
[fxShow]="timewindowForm.get('history.historyType').value === historyTypes.INTERVAL"
[required]="timewindow.selectedTab === timewindowTypes.HISTORY &&
timewindowForm.get('history.historyType').value === historyTypes.INTERVAL"
style="padding-top: 8px"></tb-quick-time-interval>
</section>
</mat-radio-button>
</mat-radio-group>
</div>
</section>
</section>
</mat-tab>
</mat-tab-group>
<div *ngIf="aggregation" formGroupName="aggregation" class="mat-content mat-padding" fxLayout="column">
<section fxLayout="row">
<section fxLayout="column" fxLayoutAlign="start center" [fxShow]="isEdit">
<label class="tb-small hide-label" translate>timewindow.hide</label>
<mat-checkbox [ngModelOptions]="{standalone: true}" [(ngModel)]="timewindow.hideAggregation"
(ngModelChange)="onHideAggregationChanged()"></mat-checkbox>
</section>
<section fxFlex fxLayout="column" [fxShow]="isEdit || !timewindow.hideAggregation">
<mat-form-field>
<mat-label translate>aggregation.function</mat-label>
<mat-select formControlName="type" style="min-width: 150px;">
<mat-option *ngFor="let aggregation of aggregations" [value]="aggregation">
{{ aggregationTypesTranslations.get(aggregationTypes[aggregation]) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</section>
</section> </section>
<section fxLayout="row" *ngIf="timewindowForm.get('aggregation.type').value === aggregationTypes.NONE"> <section fxLayout="column" fxFlex [fxShow]="isEdit || !timewindow.hideInterval">
<section fxLayout="column" fxLayoutAlign="start center" [fxShow]="isEdit"> <div formGroupName="realtime" class="mat-content mat-padding" style="padding-top: 8px;">
<label class="tb-small hide-label" translate>timewindow.hide</label> <mat-radio-group *ngIf="!quickIntervalOnly"
<mat-checkbox [ngModelOptions]="{standalone: true}" [(ngModel)]="timewindow.hideAggInterval" [fxShow]="isEdit || (!timewindow.hideLastInterval && !timewindow.hideQuickInterval)"
(ngModelChange)="onHideAggIntervalChanged()"></mat-checkbox> formControlName="realtimeType">
</section> <mat-radio-button [value]="realtimeTypes.LAST_INTERVAL" color="primary">
<section fxLayout="column" fxFlex [fxShow]="isEdit || !timewindow.hideAggInterval"> <section fxLayout="row">
<div class="limit-slider-container" fxLayout="row" fxLayoutAlign="start center" <section *ngIf="isEdit" fxLayout="column" fxLayoutAlign="start center" style="padding-right: 8px;">
fxLayout.xs="column" fxLayoutAlign.xs="stretch"> <label class="tb-small hide-label" translate>timewindow.hide</label>
<label translate>aggregation.limit</label> <mat-checkbox [ngModelOptions]="{standalone: true}" [(ngModel)]="timewindow.hideLastInterval"
<div fxLayout="row" fxLayoutAlign="start center" fxFlex> (ngModelChange)="onHideLastIntervalChanged()"></mat-checkbox>
<mat-slider fxFlex </section>
discrete <section fxLayout="column">
min="{{minDatapointsLimit()}}" <span translate>timewindow.last</span>
max="{{maxDatapointsLimit()}}"><input matSliderThumb formControlName="limit" /> <tb-timeinterval
</mat-slider> formControlName="timewindowMs"
<mat-form-field class="limit-slider-value"> predefinedName="timewindow.last"
<input matInput formControlName="limit" type="number" step="1" [fxShow]="timewindowForm.get('realtime.realtimeType').value === realtimeTypes.LAST_INTERVAL"
[value]="timewindowForm.get('aggregation.limit').value" [required]="timewindow.selectedTab === timewindowTypes.REALTIME &&
min="{{minDatapointsLimit()}}" timewindowForm.get('realtime.realtimeType').value === realtimeTypes.LAST_INTERVAL"
max="{{maxDatapointsLimit()}}"/> style="padding-top: 8px;"></tb-timeinterval>
</mat-form-field> </section>
</div> </section>
</div> </mat-radio-button>
</section> <mat-radio-button [value]="realtimeTypes.INTERVAL" color="primary">
<section fxLayout="row">
<section *ngIf="isEdit" fxLayout="column" fxLayoutAlign="start center" style="padding-right: 8px;">
<label class="tb-small hide-label" translate>timewindow.hide</label>
<mat-checkbox [ngModelOptions]="{standalone: true}" [(ngModel)]="timewindow.hideQuickInterval"
(ngModelChange)="onHideQuickIntervalChanged()"></mat-checkbox>
</section>
<section fxLayout="column">
<span translate>timewindow.interval</span>
<tb-quick-time-interval
formControlName="quickInterval"
onlyCurrentInterval="true"
[fxShow]="timewindowForm.get('realtime.realtimeType').value === realtimeTypes.INTERVAL"
[required]="timewindow.selectedTab === timewindowTypes.REALTIME &&
timewindowForm.get('realtime.realtimeType').value === realtimeTypes.INTERVAL"
style="padding-top: 8px"></tb-quick-time-interval>
</section>
</section>
</mat-radio-button>
</mat-radio-group>
<tb-timeinterval *ngIf="!isEdit && !timewindow.hideLastInterval && timewindow.hideQuickInterval"
formControlName="timewindowMs"
predefinedName="timewindow.last"
required
style="padding-top: 8px;"></tb-timeinterval>
<tb-quick-time-interval
*ngIf="quickIntervalOnly || !isEdit && timewindow.hideLastInterval && !timewindow.hideQuickInterval"
formControlName="quickInterval"
onlyCurrentInterval="true"
required
style="padding-top: 8px"></tb-quick-time-interval>
</div>
</section> </section>
</div> </section>
<div formGroupName="realtime" <ng-container *ngTemplateOutlet="additionalData">
*ngIf="aggregation && timewindowForm.get('aggregation.type').value !== aggregationTypes.NONE && </ng-container>
timewindow.selectedTab === timewindowTypes.REALTIME" class="mat-content mat-padding" fxLayout="column"> </mat-tab>
<tb-timeinterval <mat-tab label="{{ 'timewindow.history' | translate }}">
formControlName="interval" <section fxLayout="row">
[isEdit]="isEdit" <section *ngIf="isEdit" fxLayout="column" fxLayoutAlign="start center"
[(hideFlag)]="timewindow.hideAggInterval" style="padding-top: 8px; padding-left: 16px;">
(hideFlagChange)="onHideAggIntervalChanged()" <label class="tb-small hide-label" translate>timewindow.hide</label>
[min]="minRealtimeAggInterval()" [max]="maxRealtimeAggInterval()" <mat-checkbox [ngModelOptions]="{standalone: true}" [(ngModel)]="timewindow.hideInterval"
predefinedName="aggregation.group-interval"> (ngModelChange)="onHideIntervalChanged()"></mat-checkbox>
</tb-timeinterval> </section>
</div> <section fxLayout="column" fxFlex [fxShow]="isEdit || !timewindow.hideInterval">
<div formGroupName="history" <div formGroupName="history" class="mat-content mat-padding" style="padding-top: 8px;">
*ngIf="aggregation && timewindowForm.get('aggregation.type').value !== aggregationTypes.NONE && <mat-radio-group formControlName="historyType">
timewindow.selectedTab === timewindowTypes.HISTORY" class="mat-content mat-padding" fxLayout="column"> <mat-radio-button [value]="historyTypes.LAST_INTERVAL" color="primary">
<tb-timeinterval <section fxLayout="column">
formControlName="interval" <span translate>timewindow.last</span>
[isEdit]="isEdit" <tb-timeinterval
[(hideFlag)]="timewindow.hideAggInterval" formControlName="timewindowMs"
(hideFlagChange)="onHideAggIntervalChanged()" predefinedName="timewindow.last"
[min]="minHistoryAggInterval()" [max]="maxHistoryAggInterval()" class="history-time-input"
predefinedName="aggregation.group-interval"> [fxShow]="timewindowForm.get('history.historyType').value === historyTypes.LAST_INTERVAL"
</tb-timeinterval> [required]="timewindow.selectedTab === timewindowTypes.HISTORY &&
</div> timewindowForm.get('history.historyType').value === historyTypes.LAST_INTERVAL"
<div *ngIf="timezone" class="mat-content mat-padding" fxLayout="row"> style="padding-top: 8px;"></tb-timeinterval>
</section>
</mat-radio-button>
<mat-radio-button [value]="historyTypes.FIXED" color="primary">
<section fxLayout="column">
<span translate>timewindow.time-period</span>
<tb-datetime-period
formControlName="fixedTimewindow"
class="history-time-input"
[fxShow]="timewindowForm.get('history.historyType').value === historyTypes.FIXED"
[required]="timewindow.selectedTab === timewindowTypes.HISTORY &&
timewindowForm.get('history.historyType').value === historyTypes.FIXED"
style="padding-top: 8px;"></tb-datetime-period>
</section>
</mat-radio-button>
<mat-radio-button [value]="historyTypes.INTERVAL" color="primary">
<section fxLayout="column">
<span translate>timewindow.interval</span>
<tb-quick-time-interval
formControlName="quickInterval"
class="history-time-input"
[fxShow]="timewindowForm.get('history.historyType').value === historyTypes.INTERVAL"
[required]="timewindow.selectedTab === timewindowTypes.HISTORY &&
timewindowForm.get('history.historyType').value === historyTypes.INTERVAL"
style="padding-top: 8px"></tb-quick-time-interval>
</section>
</mat-radio-button>
</mat-radio-group>
</div>
</section>
</section>
<ng-container *ngTemplateOutlet="additionalData">
</ng-container>
</mat-tab>
</mat-tab-group>
<ng-template #additionalData>
<div *ngIf="aggregation" formGroupName="aggregation" class="mat-content mat-padding" fxLayout="column">
<section fxLayout="row">
<section fxLayout="column" fxLayoutAlign="start center" [fxShow]="isEdit"> <section fxLayout="column" fxLayoutAlign="start center" [fxShow]="isEdit">
<label class="tb-small hide-label" translate>timewindow.hide</label> <label class="tb-small hide-label" translate>timewindow.hide</label>
<mat-checkbox [ngModelOptions]="{standalone: true}" [(ngModel)]="timewindow.hideTimezone" <mat-checkbox [ngModelOptions]="{standalone: true}" [(ngModel)]="timewindow.hideAggregation"
(ngModelChange)="onHideTimezoneChanged()"></mat-checkbox> (ngModelChange)="onHideAggregationChanged()"></mat-checkbox>
</section> </section>
<tb-timezone-select fxFlex [fxShow]="isEdit || !timewindow.hideTimezone" <section fxFlex fxLayout="column" [fxShow]="isEdit || !timewindow.hideAggregation">
localBrowserTimezonePlaceholderOnEmpty="true" <mat-form-field>
formControlName="timezone"> <mat-label translate>aggregation.function</mat-label>
</tb-timezone-select> <mat-select formControlName="type" style="min-width: 150px;">
</div> <mat-option *ngFor="let aggregation of aggregations" [value]="aggregation">
<div fxLayout="row" class="tb-panel-actions" fxLayoutAlign="end center"> {{ aggregationTypesTranslations.get(aggregationTypes[aggregation]) | translate }}
<button type="button" </mat-option>
mat-button </mat-select>
[disabled]="(isLoading$ | async)" </mat-form-field>
(click)="cancel()"> </section>
{{ 'action.cancel' | translate }} </section>
</button> <section fxLayout="row" *ngIf="timewindowForm.get('aggregation.type').value === aggregationTypes.NONE">
<button type="submit" <section fxLayout="column" fxLayoutAlign="start center" [fxShow]="isEdit">
mat-raised-button <label class="tb-small hide-label" translate>timewindow.hide</label>
color="primary" <mat-checkbox [ngModelOptions]="{standalone: true}" [(ngModel)]="timewindow.hideAggInterval"
[disabled]="(isLoading$ | async) || timewindowForm.invalid || !timewindowForm.dirty"> (ngModelChange)="onHideAggIntervalChanged()"></mat-checkbox>
{{ 'action.update' | translate }} </section>
</button> <section fxLayout="column" fxFlex [fxShow]="isEdit || !timewindow.hideAggInterval">
</div> <div class="limit-slider-container" fxLayout="row" fxLayoutAlign="start center"
fxLayout.xs="column" fxLayoutAlign.xs="stretch">
<label translate>aggregation.limit</label>
<div fxLayout="row" fxLayoutAlign="start center" fxFlex>
<mat-slider fxFlex
discrete
min="{{minDatapointsLimit()}}"
max="{{maxDatapointsLimit()}}"><input matSliderThumb formControlName="limit"/>
</mat-slider>
<mat-form-field class="limit-slider-value">
<input matInput formControlName="limit" type="number" step="1"
[value]="timewindowForm.get('aggregation.limit').value"
min="{{minDatapointsLimit()}}"
max="{{maxDatapointsLimit()}}"/>
</mat-form-field>
</div>
</div>
</section>
</section>
</div> </div>
</fieldset> <div formGroupName="realtime"
*ngIf="aggregation && timewindowForm.get('aggregation.type').value !== aggregationTypes.NONE &&
timewindow.selectedTab === timewindowTypes.REALTIME" class="mat-content mat-padding" fxLayout="column">
<tb-timeinterval
formControlName="interval"
[isEdit]="isEdit"
[(hideFlag)]="timewindow.hideAggInterval"
(hideFlagChange)="onHideAggIntervalChanged()"
[min]="minRealtimeAggInterval()" [max]="maxRealtimeAggInterval()"
predefinedName="aggregation.group-interval">
</tb-timeinterval>
</div>
<div formGroupName="history"
*ngIf="aggregation && timewindowForm.get('aggregation.type').value !== aggregationTypes.NONE &&
timewindow.selectedTab === timewindowTypes.HISTORY" class="mat-content mat-padding" fxLayout="column">
<tb-timeinterval
formControlName="interval"
[isEdit]="isEdit"
[(hideFlag)]="timewindow.hideAggInterval"
(hideFlagChange)="onHideAggIntervalChanged()"
[min]="minHistoryAggInterval()" [max]="maxHistoryAggInterval()"
predefinedName="aggregation.group-interval">
</tb-timeinterval>
</div>
<div *ngIf="timezone" class="mat-content mat-padding" fxLayout="row">
<section fxLayout="column" fxLayoutAlign="start center" [fxShow]="isEdit">
<label class="tb-small hide-label" translate>timewindow.hide</label>
<mat-checkbox [ngModelOptions]="{standalone: true}" [(ngModel)]="timewindow.hideTimezone"
(ngModelChange)="onHideTimezoneChanged()"></mat-checkbox>
</section>
<tb-timezone-select fxFlex [fxShow]="isEdit || !timewindow.hideTimezone"
localBrowserTimezonePlaceholderOnEmpty="true"
formControlName="timezone">
</tb-timezone-select>
</div>
</ng-template>
</form> </form>
<div fxLayout="row" class="tb-panel-actions" fxLayoutAlign="end center">
<button type="button"
mat-button
[disabled]="(isLoading$ | async)"
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button type="button"
mat-raised-button
color="primary"
(click)="update()"
[disabled]="(isLoading$ | async) || timewindowForm.invalid || !timewindowForm.dirty">
{{ 'action.update' | translate }}
</button>
</div>

View File

@ -16,16 +16,14 @@
@import "../../../../scss/constants"; @import "../../../../scss/constants";
:host { :host {
width: 100%; display: flex;
height: 100%; flex-direction: column;
form, max-height: 100%;
fieldset { max-width: 100%;
height: 100%; background-color: #fff;
}
.mat-content { .mat-content {
overflow: hidden; overflow: hidden;
background-color: #fff;
} }
.mat-padding { .mat-padding {
@ -70,7 +68,6 @@
:host ::ng-deep { :host ::ng-deep {
.mat-mdc-radio-button { .mat-mdc-radio-button {
display: block; display: block;
margin-bottom: 16px;
.mdc-form-field { .mdc-form-field {
align-items: start; align-items: start;
> label { > label {
@ -78,4 +75,7 @@
} }
} }
} }
.mat-mdc-tab-group:not(.tb-headless) {
height: 100%;
}
} }

View File

@ -25,14 +25,13 @@ import {
Timewindow, Timewindow,
TimewindowType TimewindowType
} from '@shared/models/time/time.models'; } from '@shared/models/time/time.models';
import { OverlayRef } from '@angular/cdk/overlay';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { TimeService } from '@core/services/time.service'; import { TimeService } from '@core/services/time.service';
import { isDefined } from '@core/utils';
export const TIMEWINDOW_PANEL_DATA = new InjectionToken<any>('TimewindowPanelData'); import { OverlayRef } from '@angular/cdk/overlay';
export interface TimewindowPanelData { export interface TimewindowPanelData {
historyOnly: boolean; historyOnly: boolean;
@ -43,6 +42,8 @@ export interface TimewindowPanelData {
isEdit: boolean; isEdit: boolean;
} }
export const TIMEWINDOW_PANEL_DATA = new InjectionToken<any>('TimewindowPanelData');
@Component({ @Component({
selector: 'tb-timewindow-panel', selector: 'tb-timewindow-panel',
templateUrl: './timewindow-panel.component.html', templateUrl: './timewindow-panel.component.html',
@ -62,8 +63,6 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
timewindow: Timewindow; timewindow: Timewindow;
result: Timewindow;
timewindowForm: UntypedFormGroup; timewindowForm: UntypedFormGroup;
historyTypes = HistoryWindowType; historyTypes = HistoryWindowType;
@ -78,6 +77,8 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
aggregationTypesTranslations = aggregationTranslations; aggregationTypesTranslations = aggregationTranslations;
result: Timewindow;
constructor(@Inject(TIMEWINDOW_PANEL_DATA) public data: TimewindowPanelData, constructor(@Inject(TIMEWINDOW_PANEL_DATA) public data: TimewindowPanelData,
public overlayRef: OverlayRef, public overlayRef: OverlayRef,
protected store: Store<AppState>, protected store: Store<AppState>,
@ -101,77 +102,60 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
const hideAggInterval = this.timewindow.hideAggInterval || false; const hideAggInterval = this.timewindow.hideAggInterval || false;
const hideTimezone = this.timewindow.hideTimezone || false; const hideTimezone = this.timewindow.hideTimezone || false;
const realtime = this.timewindow.realtime;
const history = this.timewindow.history;
const aggregation = this.timewindow.aggregation;
this.timewindowForm = this.fb.group({ this.timewindowForm = this.fb.group({
realtime: this.fb.group( realtime: this.fb.group({
{ realtimeType: [{
realtimeType: this.fb.control({ value: isDefined(realtime?.realtimeType) ? this.timewindow.realtime.realtimeType : RealtimeWindowType.LAST_INTERVAL,
value: this.timewindow.realtime && typeof this.timewindow.realtime.realtimeType !== 'undefined' disabled: hideInterval
? this.timewindow.realtime.realtimeType : RealtimeWindowType.LAST_INTERVAL, }],
disabled: hideInterval timewindowMs: [{
}), value: isDefined(realtime?.timewindowMs) ? this.timewindow.realtime.timewindowMs : null,
timewindowMs: this.fb.control({ disabled: hideInterval || hideLastInterval
value: this.timewindow.realtime && typeof this.timewindow.realtime.timewindowMs !== 'undefined' }],
? this.timewindow.realtime.timewindowMs : null, interval: [isDefined(realtime?.interval) ? this.timewindow.realtime.interval : null],
disabled: hideInterval || hideLastInterval quickInterval: [{
}), value: isDefined(realtime?.quickInterval) ? this.timewindow.realtime.quickInterval : null,
interval: [ disabled: hideInterval || hideQuickInterval
this.timewindow.realtime && typeof this.timewindow.realtime.interval !== 'undefined' }]
? this.timewindow.realtime.interval : null }),
], history: this.fb.group({
quickInterval: this.fb.control({ historyType: [{
value: this.timewindow.realtime && typeof this.timewindow.realtime.quickInterval !== 'undefined' value: isDefined(history?.historyType) ? this.timewindow.history.historyType : HistoryWindowType.LAST_INTERVAL,
? this.timewindow.realtime.quickInterval : null, disabled: hideInterval
disabled: hideInterval || hideQuickInterval }],
}) timewindowMs: [{
} value: isDefined(history?.timewindowMs) ? this.timewindow.history.timewindowMs : null,
), disabled: hideInterval
history: this.fb.group( }],
{ interval: [ isDefined(history?.interval) ? this.timewindow.history.interval : null
historyType: this.fb.control({ ],
value: this.timewindow.history && typeof this.timewindow.history.historyType !== 'undefined' fixedTimewindow: [{
? this.timewindow.history.historyType : HistoryWindowType.LAST_INTERVAL, value: isDefined(history?.fixedTimewindow) ? this.timewindow.history.fixedTimewindow : null,
disabled: hideInterval disabled: hideInterval
}), }],
timewindowMs: this.fb.control({ quickInterval: [{
value: this.timewindow.history && typeof this.timewindow.history.timewindowMs !== 'undefined' value: isDefined(history?.quickInterval) ? this.timewindow.history.quickInterval : null,
? this.timewindow.history.timewindowMs : null, disabled: hideInterval
disabled: hideInterval }]
}), }),
interval: [ aggregation: this.fb.group({
this.timewindow.history && typeof this.timewindow.history.interval !== 'undefined' type: [{
? this.timewindow.history.interval : null value: isDefined(aggregation?.type) ? this.timewindow.aggregation.type : null,
], disabled: hideAggregation
fixedTimewindow: this.fb.control({ }],
value: this.timewindow.history && typeof this.timewindow.history.fixedTimewindow !== 'undefined' limit: [{
? this.timewindow.history.fixedTimewindow : null, value: isDefined(aggregation?.limit) ? this.checkLimit(this.timewindow.aggregation.limit) : null,
disabled: hideInterval disabled: hideAggInterval
}), }, []]
quickInterval: this.fb.control({ }),
value: this.timewindow.history && typeof this.timewindow.history.quickInterval !== 'undefined' timezone: [{
? this.timewindow.history.quickInterval : null, value: isDefined(this.timewindow.timezone) ? this.timewindow.timezone : null,
disabled: hideInterval disabled: hideTimezone
}) }]
}
),
aggregation: this.fb.group(
{
type: this.fb.control({
value: this.timewindow.aggregation && typeof this.timewindow.aggregation.type !== 'undefined'
? this.timewindow.aggregation.type : null,
disabled: hideAggregation
}),
limit: this.fb.control({
value: this.timewindow.aggregation && typeof this.timewindow.aggregation.limit !== 'undefined'
? this.checkLimit(this.timewindow.aggregation.limit) : null,
disabled: hideAggInterval
}, [])
}
),
timezone: this.fb.control({
value: this.timewindow.timezone !== 'undefined'
? this.timewindow.timezone : null,
disabled: hideTimezone
})
}); });
this.updateValidators(); this.updateValidators();
this.timewindowForm.get('aggregation.type').valueChanges.subscribe(() => { this.timewindowForm.get('aggregation.type').valueChanges.subscribe(() => {

View File

@ -15,30 +15,34 @@
limitations under the License. limitations under the License.
--> -->
<button *ngIf="asButton" cdkOverlayOrigin #timewindowPanelOrigin="cdkOverlayOrigin" [disabled]="timewindowDisabled" <button *ngIf="asButton"
[disabled]="timewindowDisabled"
type="button" type="button"
mat-raised-button color="primary" (click)="openEditMode()"> mat-raised-button color="primary"
(click)="toggleTimewindow($event)">
<mat-icon class="material-icons">query_builder</mat-icon> <mat-icon class="material-icons">query_builder</mat-icon>
<span>{{innerValue?.displayValue}}</span> <span>{{innerValue?.displayValue}}</span>
</button> </button>
<section *ngIf="!asButton" cdkOverlayOrigin #timewindowPanelOrigin="cdkOverlayOrigin" <section *ngIf="!asButton"
class="tb-timewindow" fxLayout="row" fxLayoutAlign="start center"> class="tb-timewindow"
fxLayout="row"
fxLayoutAlign="start center">
<button *ngIf="direction === 'left'" [disabled]="timewindowDisabled" mat-icon-button class="tb-mat-32" <button *ngIf="direction === 'left'" [disabled]="timewindowDisabled" mat-icon-button class="tb-mat-32"
type="button" type="button"
(click)="openEditMode()" (click)="toggleTimewindow($event)"
matTooltip="{{ 'timewindow.edit' | translate }}" matTooltip="{{ 'timewindow.edit' | translate }}"
[matTooltipPosition]="tooltipPosition"> [matTooltipPosition]="tooltipPosition">
<mat-icon class="material-icons">query_builder</mat-icon> <mat-icon class="material-icons">query_builder</mat-icon>
</button> </button>
<span [fxHide]="hideLabel()" <span [fxHide]="hideLabel()"
(click)="openEditMode()" (click)="toggleTimewindow($event)"
matTooltip="{{ 'timewindow.edit' | translate }}" matTooltip="{{ 'timewindow.edit' | translate }}"
[matTooltipPosition]="tooltipPosition"> [matTooltipPosition]="tooltipPosition">
{{innerValue?.displayValue}} <span [fxShow]="innerValue?.displayTimezoneAbbr !== ''">| <span class="timezone-abbr">{{innerValue.displayTimezoneAbbr}}</span></span> {{innerValue?.displayValue}} <span [fxShow]="innerValue?.displayTimezoneAbbr !== ''">| <span class="timezone-abbr">{{innerValue.displayTimezoneAbbr}}</span></span>
</span> </span>
<button *ngIf="direction === 'right'" [disabled]="timewindowDisabled" mat-icon-button class="tb-mat-32" <button *ngIf="direction === 'right'" [disabled]="timewindowDisabled" mat-icon-button class="tb-mat-32"
type="button" type="button"
(click)="openEditMode()" (click)="toggleTimewindow($event)"
matTooltip="{{ 'timewindow.edit' | translate }}" matTooltip="{{ 'timewindow.edit' | translate }}"
[matTooltipPosition]="tooltipPosition"> [matTooltipPosition]="tooltipPosition">
<mat-icon class="material-icons">query_builder</mat-icon> <mat-icon class="material-icons">query_builder</mat-icon>

View File

@ -17,14 +17,13 @@
import { import {
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
ElementRef,
forwardRef, forwardRef,
Inject,
Injector, Injector,
Input, Input,
OnDestroy, OnDestroy,
OnInit, OnInit,
StaticProvider, StaticProvider,
ViewChild,
ViewContainerRef ViewContainerRef
} from '@angular/core'; } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
@ -40,21 +39,16 @@ import {
Timewindow, Timewindow,
TimewindowType TimewindowType
} from '@shared/models/time/time.models'; } from '@shared/models/time/time.models';
import { DatePipe, DOCUMENT } from '@angular/common'; import { DatePipe } from '@angular/common';
import { CdkOverlayOrigin, ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; import { TIMEWINDOW_PANEL_DATA, TimewindowPanelComponent } from '@shared/components/time/timewindow-panel.component';
import {
TIMEWINDOW_PANEL_DATA,
TimewindowPanelComponent,
TimewindowPanelData
} from '@shared/components/time/timewindow-panel.component';
import { ComponentPortal } from '@angular/cdk/portal';
import { MediaBreakpoints } from '@shared/models/constants'; import { MediaBreakpoints } from '@shared/models/constants';
import { BreakpointObserver } from '@angular/cdk/layout'; import { BreakpointObserver } from '@angular/cdk/layout';
import { WINDOW } from '@core/services/window.service';
import { TimeService } from '@core/services/time.service'; import { TimeService } from '@core/services/time.service';
import { TooltipPosition } from '@angular/material/tooltip'; import { TooltipPosition } from '@angular/material/tooltip';
import { deepClone, isDefinedAndNotNull } from '@core/utils'; import { deepClone, isDefinedAndNotNull } from '@core/utils';
import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';
// @dynamic // @dynamic
@Component({ @Component({
@ -174,24 +168,21 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces
@Input() disabled: boolean; @Input() disabled: boolean;
@ViewChild('timewindowPanelOrigin') timewindowPanelOrigin: CdkOverlayOrigin;
innerValue: Timewindow; innerValue: Timewindow;
timewindowDisabled: boolean; timewindowDisabled: boolean;
private propagateChange = (_: any) => {}; private propagateChange = (_: any) => {};
constructor(private translate: TranslateService, constructor(private overlay: Overlay,
private translate: TranslateService,
private timeService: TimeService, private timeService: TimeService,
private millisecondsToTimeStringPipe: MillisecondsToTimeStringPipe, private millisecondsToTimeStringPipe: MillisecondsToTimeStringPipe,
private datePipe: DatePipe, private datePipe: DatePipe,
private overlay: Overlay,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
private nativeElement: ElementRef,
public viewContainerRef: ViewContainerRef, public viewContainerRef: ViewContainerRef,
public breakpointObserver: BreakpointObserver, public breakpointObserver: BreakpointObserver) {
@Inject(DOCUMENT) private document: Document,
@Inject(WINDOW) private window: Window) {
} }
ngOnInit(): void { ngOnInit(): void {
@ -200,82 +191,51 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces
ngOnDestroy(): void { ngOnDestroy(): void {
} }
openEditMode() { toggleTimewindow($event: Event) {
if (this.timewindowDisabled) { if ($event) {
return; $event.stopPropagation();
} }
const isGtXs = this.breakpointObserver.isMatched(MediaBreakpoints['gt-xs']);
const position = this.overlay.position();
const config = new OverlayConfig({ const config = new OverlayConfig({
panelClass: 'tb-timewindow-panel', panelClass: 'tb-timewindow-panel',
backdropClass: 'cdk-overlay-transparent-backdrop', backdropClass: 'cdk-overlay-transparent-backdrop',
hasBackdrop: isGtXs, hasBackdrop: true,
maxHeight: '80vh',
height: 'min-content'
}); });
if (isGtXs) { config.hasBackdrop = true;
config.minWidth = '417px'; const connectedPosition: ConnectedPosition = {
config.maxHeight = '550px'; originX: 'start',
const panelHeight = 375; originY: 'bottom',
const panelWidth = 417; overlayX: 'start',
const el = this.timewindowPanelOrigin.elementRef.nativeElement; overlayY: 'top'
const offset = el.getBoundingClientRect(); };
const scrollTop = this.window.pageYOffset || this.document.documentElement.scrollTop || this.document.body.scrollTop || 0; config.positionStrategy = this.overlay.position().flexibleConnectedTo(this.nativeElement)
const scrollLeft = this.window.pageXOffset || this.document.documentElement.scrollLeft || this.document.body.scrollLeft || 0; .withPositions([connectedPosition]);
const bottomY = offset.bottom - scrollTop;
const leftX = offset.left - scrollLeft;
let originX;
let originY;
let overlayX;
let overlayY;
const wHeight = this.document.documentElement.clientHeight;
const wWidth = this.document.documentElement.clientWidth;
if (bottomY + panelHeight > wHeight) {
originY = 'top';
overlayY = 'bottom';
} else {
originY = 'bottom';
overlayY = 'top';
}
if (leftX + panelWidth > wWidth) {
originX = 'end';
overlayX = 'end';
} else {
originX = 'start';
overlayX = 'start';
}
const connectedPosition: ConnectedPosition = {
originX,
originY,
overlayX,
overlayY
};
config.positionStrategy = position.flexibleConnectedTo(this.timewindowPanelOrigin.elementRef)
.withPositions([connectedPosition]);
} else {
config.minWidth = '100%';
config.minHeight = '100%';
config.positionStrategy = position.global().top('0%').left('0%')
.right('0%').bottom('0%');
}
const overlayRef = this.overlay.create(config); const overlayRef = this.overlay.create(config);
overlayRef.backdropClick().subscribe(() => { overlayRef.backdropClick().subscribe(() => {
overlayRef.dispose(); overlayRef.dispose();
}); });
const providers: StaticProvider[] = [
const injector = this._createTimewindowPanelInjector(
overlayRef,
{ {
timewindow: deepClone(this.innerValue), provide: TIMEWINDOW_PANEL_DATA,
historyOnly: this.historyOnly, useValue: {
quickIntervalOnly: this.quickIntervalOnly, timewindow: deepClone(this.innerValue),
aggregation: this.aggregation, historyOnly: this.historyOnly,
timezone: this.timezone, quickIntervalOnly: this.quickIntervalOnly,
isEdit: this.isEdit aggregation: this.aggregation,
timezone: this.timezone,
isEdit: this.isEdit
}
},
{
provide: OverlayRef,
useValue: overlayRef
} }
); ];
const injector = Injector.create({parent: this.viewContainerRef.injector, providers});
const componentRef = overlayRef.attach(new ComponentPortal(TimewindowPanelComponent, this.viewContainerRef, injector)); const componentRef = overlayRef.attach(new ComponentPortal(TimewindowPanelComponent,
this.viewContainerRef, injector));
componentRef.onDestroy(() => { componentRef.onDestroy(() => {
if (componentRef.instance.result) { if (componentRef.instance.result) {
this.innerValue = componentRef.instance.result; this.innerValue = componentRef.instance.result;
@ -284,14 +244,7 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces
this.notifyChanged(); this.notifyChanged();
} }
}); });
} this.cd.detectChanges();
private _createTimewindowPanelInjector(overlayRef: OverlayRef, data: TimewindowPanelData): Injector {
const providers: StaticProvider[] = [
{provide: TIMEWINDOW_PANEL_DATA, useValue: data},
{provide: OverlayRef, useValue: overlayRef}
];
return Injector.create({parent: this.viewContainerRef.injector, providers});
} }
private onHistoryOnlyChanged(): boolean { private onHistoryOnlyChanged(): boolean {

View File

@ -505,7 +505,7 @@ export const baseDetailsPageByEntityType = new Map<EntityType, string>([
[EntityType.DEVICE, '/entities/devices'], [EntityType.DEVICE, '/entities/devices'],
[EntityType.DEVICE_PROFILE, '/profiles/deviceProfiles'], [EntityType.DEVICE_PROFILE, '/profiles/deviceProfiles'],
[EntityType.ASSET_PROFILE, '/profiles/assetProfiles'], [EntityType.ASSET_PROFILE, '/profiles/assetProfiles'],
[EntityType.RULE_CHAIN, '/features/ruleChains'], [EntityType.RULE_CHAIN, '/ruleChains'],
[EntityType.EDGE, '/edgeManagement/instances'], [EntityType.EDGE, '/edgeManagement/instances'],
[EntityType.ENTITY_VIEW, '/entities/entityViews'], [EntityType.ENTITY_VIEW, '/entities/entityViews'],
[EntityType.TB_RESOURCE, '/resources/resources-library'], [EntityType.TB_RESOURCE, '/resources/resources-library'],