Browse Source

Merge branch 'develop/3.5.2' into feature/filter-nodes-improvements

pull/8786/head
ShvaykaD 3 years ago
parent
commit
16739b18c2
  1. 30
      application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
  2. 38
      application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java
  3. 3
      application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java
  4. 3
      application/src/main/java/org/thingsboard/server/service/entitiy/user/DefaultUserService.java
  5. 47
      application/src/test/java/org/thingsboard/server/controller/AlarmControllerTest.java
  6. 6
      ui-ngx/src/app/core/services/dialog.service.ts
  7. 2142
      ui-ngx/src/app/core/services/material-icons-codepoints.raw
  8. 25
      ui-ngx/src/app/core/services/resources.service.ts
  9. 50
      ui-ngx/src/app/core/services/script/node-script-test.service.ts
  10. 75
      ui-ngx/src/app/core/services/utils.service.ts
  11. 2
      ui-ngx/src/app/modules/common/modules-map.ts
  12. 6
      ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.scss
  13. 4
      ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts
  14. 29
      ui-ngx/src/app/modules/home/components/event/event-table-config.ts
  15. 37
      ui-ngx/src/app/modules/home/components/event/event-table.component.ts
  16. 27
      ui-ngx/src/app/modules/home/components/widget/config/basic/alarm/alarms-table-basic-config.component.html
  17. 27
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/entities-table-basic-config.component.html
  18. 22
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.html
  19. 27
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/timeseries-table-basic-config.component.html
  20. 27
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/flot-basic-config.component.html
  21. 11
      ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.html
  22. 2
      ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.html
  23. 32
      ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.ts
  24. 40
      ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts
  25. 46
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page-widget.scss
  26. 11
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.html
  27. 11
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-latest-key-settings.component.html
  28. 54
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.html
  29. 27
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.html
  30. 1
      ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts
  31. 18
      ui-ngx/src/app/modules/home/pages/api-usage/api-usage-routing.module.ts
  32. 16
      ui-ngx/src/app/modules/home/pages/api-usage/api-usage.component.ts
  33. 110
      ui-ngx/src/app/modules/home/pages/home-links/home-links-routing.module.ts
  34. 33
      ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.ts
  35. 4
      ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html
  36. 8
      ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts
  37. 9
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html
  38. 24
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts
  39. 13
      ui-ngx/src/app/shared/components/color-input.component.html
  40. 9
      ui-ngx/src/app/shared/components/color-input.component.scss
  41. 42
      ui-ngx/src/app/shared/components/color-input.component.ts
  42. 30
      ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.html
  43. 36
      ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.scss
  44. 55
      ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.ts
  45. 14
      ui-ngx/src/app/shared/components/color-picker/color-picker.component.html
  46. 39
      ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss
  47. 27
      ui-ngx/src/app/shared/components/color-picker/color-picker.component.ts
  48. 30
      ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.html
  49. 22
      ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.scss
  50. 24
      ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.ts
  51. 71
      ui-ngx/src/app/shared/components/dialog/material-icons-dialog.component.html
  52. 35
      ui-ngx/src/app/shared/components/dialog/material-icons-dialog.component.scss
  53. 56
      ui-ngx/src/app/shared/components/dialog/material-icons-dialog.component.ts
  54. 12
      ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts
  55. 12
      ui-ngx/src/app/shared/components/material-icon-select.component.html
  56. 31
      ui-ngx/src/app/shared/components/material-icon-select.component.scss
  57. 36
      ui-ngx/src/app/shared/components/material-icon-select.component.ts
  58. 65
      ui-ngx/src/app/shared/components/material-icons.component.html
  59. 61
      ui-ngx/src/app/shared/components/material-icons.component.scss
  60. 135
      ui-ngx/src/app/shared/components/material-icons.component.ts
  61. 20
      ui-ngx/src/app/shared/components/popover.component.ts
  62. 4
      ui-ngx/src/app/shared/components/popover.service.ts
  63. 1
      ui-ngx/src/app/shared/components/public-api.ts
  64. 3
      ui-ngx/src/app/shared/components/unit-input.component.html
  65. 72
      ui-ngx/src/app/shared/models/icon.models.ts
  66. 9
      ui-ngx/src/app/shared/models/rule-node.models.ts
  67. 6
      ui-ngx/src/app/shared/shared.module.ts
  68. 0
      ui-ngx/src/assets/dashboard/api_usage.json
  69. 0
      ui-ngx/src/assets/dashboard/customer_user_home_page.json
  70. 0
      ui-ngx/src/assets/dashboard/sys_admin_home_page.json
  71. 0
      ui-ngx/src/assets/dashboard/tenant_admin_home_page.json
  72. BIN
      ui-ngx/src/assets/fonts/MaterialIcons-Regular.ttf
  73. 14
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  74. 6367
      ui-ngx/src/assets/metadata/material-icons.json
  75. 140
      ui-ngx/src/form.scss

30
application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java

@ -19,9 +19,11 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.DefaultAuthenticationEventPublisher;
import org.springframework.security.config.annotation.ObjectPostProcessor;
@ -29,7 +31,6 @@ import org.springframework.security.config.annotation.authentication.builders.Au
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver;
@ -37,9 +38,11 @@ import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.header.writers.StaticHeadersWriter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.filter.ShallowEtagHeaderFilter;
import org.thingsboard.server.dao.oauth2.OAuth2Configuration;
import org.thingsboard.server.exception.ThingsboardErrorResponseHandler;
import org.thingsboard.server.queue.util.TbCoreComponent;
@ -119,6 +122,17 @@ public class ThingsboardSecurityConfiguration {
@Autowired private RateLimitProcessingFilter rateLimitProcessingFilter;
@Bean
protected FilterRegistrationBean<ShallowEtagHeaderFilter> buildEtagFilter() throws Exception {
ShallowEtagHeaderFilter etagFilter = new ShallowEtagHeaderFilter();
etagFilter.setWriteWeakETag(true);
FilterRegistrationBean<ShallowEtagHeaderFilter> filterRegistrationBean
= new FilterRegistrationBean<>( etagFilter);
filterRegistrationBean.addUrlPatterns("*.js","*.css","*.ico","/assets/*","/static/*");
filterRegistrationBean.setName("etagFilter");
return filterRegistrationBean;
}
@Bean
protected RestLoginProcessingFilter buildRestLoginProcessingFilter() throws Exception {
RestLoginProcessingFilter filter = new RestLoginProcessingFilter(FORM_BASED_LOGIN_ENTRY_POINT, successHandler, failureHandler);
@ -181,8 +195,18 @@ public class ThingsboardSecurityConfiguration {
private OAuth2AuthorizationRequestResolver oAuth2AuthorizationRequestResolver;
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring().antMatchers("/*.js","/*.css","/*.ico","/assets/**","/static/**");
@Order(0)
SecurityFilterChain resources(HttpSecurity http) throws Exception {
http
.requestMatchers((matchers) -> matchers.antMatchers("/*.js","/*.css","/*.ico","/assets/**","/static/**"))
.headers().defaultsDisabled()
.addHeaderWriter(new StaticHeadersWriter(HttpHeaders.CACHE_CONTROL, "max-age=0, public"))
.and()
.authorizeHttpRequests((authorize) -> authorize.anyRequest().permitAll())
.requestCache().disable()
.securityContext().disable()
.sessionManagement().disable();
return http.build();
}
@Bean

38
application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java

@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.alarm.AlarmComment;
import org.thingsboard.server.common.data.alarm.AlarmCommentType;
import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmQueryV2;
import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
@ -36,9 +37,13 @@ import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@Service
@AllArgsConstructor
@ -210,6 +215,39 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb
return alarmInfo;
}
@Override
public void unassignUserAlarms(TenantId tenantId, User user, long unassignTs) {
AlarmQueryV2 alarmQuery = AlarmQueryV2.builder().assigneeId(user.getId()).pageLink(new TimePageLink(Integer.MAX_VALUE)).build();
try {
List<AlarmInfo> alarms = alarmService.findAlarmsV2(tenantId, alarmQuery).get(30, TimeUnit.SECONDS).getData();
for (AlarmInfo alarm : alarms) {
AlarmApiCallResult result = alarmSubscriptionService.unassignAlarm(tenantId, alarm.getId(), getOrDefault(unassignTs));
if (!result.isSuccessful()) {
continue;
}
if (result.isModified()) {
AlarmComment alarmComment = AlarmComment.builder()
.alarmId(alarm.getId())
.type(AlarmCommentType.SYSTEM)
.comment(JacksonUtil.newObjectNode().put("text", String.format("Alarm was unassigned because user %s - was deleted",
(user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName()))
.put("userId", user.getId().toString())
.put("subtype", "ASSIGN"))
.build();
try {
alarmCommentService.saveAlarmComment(alarm, alarmComment, user);
} catch (ThingsboardException e) {
log.error("Failed to save alarm comment", e);
}
notificationEntityService.notifyCreateOrUpdateAlarm(result.getAlarm(), ActionType.ALARM_UNASSIGNED, user);
}
}
} catch (InterruptedException | ExecutionException | TimeoutException e) {
throw new RuntimeException(e);
}
}
@Override
public Boolean delete(Alarm alarm, User user) {
TenantId tenantId = alarm.getTenantId();

3
application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java

@ -19,6 +19,7 @@ import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
public interface TbAlarmService {
@ -37,5 +38,7 @@ public interface TbAlarmService {
AlarmInfo unassign(Alarm alarm, long unassignTs, User user) throws ThingsboardException;
void unassignUserAlarms(TenantId tenantId, User user, long unassignTs);
Boolean delete(Alarm alarm, User user);
}

3
application/src/main/java/org/thingsboard/server/service/entitiy/user/DefaultUserService.java

@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
import org.thingsboard.server.service.entitiy.alarm.TbAlarmService;
import org.thingsboard.server.service.security.system.SystemSecurityService;
import javax.servlet.http.HttpServletRequest;
@ -43,6 +44,7 @@ import static org.thingsboard.server.controller.UserController.ACTIVATE_URL_PATT
public class DefaultUserService extends AbstractTbEntityService implements TbUserService {
private final UserService userService;
private final TbAlarmService tbAlarmService;
private final MailService mailService;
private final SystemSecurityService systemSecurityService;
@ -80,6 +82,7 @@ public class DefaultUserService extends AbstractTbEntityService implements TbUse
UserId userId = tbUser.getId();
try {
tbAlarmService.unassignUserAlarms(tenantId, tbUser, System.currentTimeMillis());
userService.deleteUser(tenantId, userId);
notificationEntityService.notifyCreateOrUpdateOrDelete(tenantId, customerId, userId, tbUser,
user, ActionType.DELETED, true, null, customerId.toString());

47
application/src/test/java/org/thingsboard/server/controller/AlarmControllerTest.java

@ -32,6 +32,7 @@ import org.springframework.test.context.ContextConfiguration;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
@ -39,6 +40,7 @@ import org.thingsboard.server.common.data.alarm.AlarmStatus;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.alarm.AlarmDao;
import org.thingsboard.server.dao.service.DaoSqlTest;
@ -529,6 +531,51 @@ public class AlarmControllerTest extends AbstractControllerTest {
tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.ALARM_UNASSIGNED);
}
@Test
public void testUnassignAlarmOnUserRemoving() throws Exception {
loginDifferentTenant();
User user = new User();
user.setAuthority(Authority.TENANT_ADMIN);
user.setTenantId(tenantId);
user.setEmail("tenantForAssign@thingsboard.org");
User savedUser = createUser(user, "password");
Device device = createDevice("Different tenant device", "default", "differentTenantTest");
Alarm alarm = Alarm.builder()
.type(TEST_ALARM_TYPE)
.tenantId(savedDifferentTenant.getId())
.originator(device.getId())
.severity(AlarmSeverity.MAJOR)
.build();
alarm = doPost("/api/alarm", alarm, Alarm.class);
Assert.assertNotNull(alarm);
alarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class);
Assert.assertNotNull(alarm);
Mockito.reset(tbClusterService, auditLogService);
long beforeAssignmentTs = System.currentTimeMillis();
doPost("/api/alarm/" + alarm.getId() + "/assign/" + savedUser.getId().getId()).andExpect(status().isOk());
AlarmInfo foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class);
Assert.assertNotNull(foundAlarm);
Assert.assertEquals(savedUser.getId(), foundAlarm.getAssigneeId());
Assert.assertTrue(foundAlarm.getAssignTs() >= beforeAssignmentTs);
beforeAssignmentTs = System.currentTimeMillis();
Mockito.reset(tbClusterService, auditLogService);
doDelete("/api/user/" + savedUser.getId().getId()).andExpect(status().isOk());
foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class);
Assert.assertNotNull(foundAlarm);
Assert.assertNull(foundAlarm.getAssigneeId());
Assert.assertTrue(foundAlarm.getAssignTs() >= beforeAssignmentTs);
}
@Test
public void testFindAlarmsViaCustomerUser() throws Exception {
loginCustomerUser();

6
ui-ngx/src/app/core/services/dialog.service.ts

@ -103,7 +103,8 @@ export class DialogService {
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
color
}
},
autoFocus: false
}).afterClosed();
}
@ -114,7 +115,8 @@ export class DialogService {
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
icon
}
},
autoFocus: false
}).afterClosed();
}

2142
ui-ngx/src/app/core/services/material-icons-codepoints.raw

File diff suppressed because it is too large

25
ui-ngx/src/app/core/services/resources.service.ts

@ -47,6 +47,7 @@ export interface ModulesWithFactories {
})
export class ResourcesService {
private loadedJsonResources: { [url: string]: ReplaySubject<any> } = {};
private loadedResources: { [url: string]: ReplaySubject<void> } = {};
private loadedModules: { [url: string]: ReplaySubject<Type<any>[]> } = {};
private loadedModulesAndFactories: { [url: string]: ReplaySubject<ModulesWithFactories> } = {};
@ -61,6 +62,30 @@ export class ResourcesService {
this.store.pipe(select(selectIsAuthenticated)).subscribe(() => this.clearModulesCache());
}
public loadJsonResource<T>(url: string, postProcess?: (data: T) => T): Observable<T> {
if (this.loadedJsonResources[url]) {
return this.loadedJsonResources[url].asObservable();
}
const subject = new ReplaySubject<any>();
this.loadedJsonResources[url] = subject;
this.http.get<T>(url).subscribe(
{
next: (o) => {
if (postProcess) {
o = postProcess(o);
}
this.loadedJsonResources[url].next(o);
this.loadedJsonResources[url].complete();
},
error: () => {
this.loadedJsonResources[url].error(new Error(`Unable to load ${url}`));
delete this.loadedJsonResources[url];
}
}
);
return subject.asObservable();
}
public loadResource(url: string): Observable<any> {
if (this.loadedResources[url]) {
return this.loadedResources[url].asObservable();

50
ui-ngx/src/app/core/services/script/node-script-test.service.ts

@ -23,8 +23,8 @@ import {
NodeScriptTestDialogComponent,
NodeScriptTestDialogData
} from '@shared/components/dialog/node-script-test-dialog.component';
import { sortObjectKeys } from '@core/utils';
import { ScriptLanguage } from '@shared/models/rule-node.models';
import { DebugRuleNodeEventBody } from '@shared/models/event.models';
@Injectable({
providedIn: 'root'
@ -37,56 +37,52 @@ export class NodeScriptTestService {
testNodeScript(script: string, scriptType: string, functionTitle: string,
functionName: string, argNames: string[], ruleNodeId: string, helpId?: string,
scriptLang?: ScriptLanguage): Observable<string> {
if (ruleNodeId) {
scriptLang?: ScriptLanguage, debugEventBody?: DebugRuleNodeEventBody): Observable<string> {
if (ruleNodeId && !debugEventBody) {
return this.ruleChainService.getLatestRuleNodeDebugInput(ruleNodeId).pipe(
switchMap((debugIn) => {
let msg: any;
let metadata: {[key: string]: string};
let msgType: string;
if (debugIn) {
if (debugIn.data) {
try {
msg = JSON.parse(debugIn.data);
} catch (e) {}
}
if (debugIn.metadata) {
try {
metadata = JSON.parse(debugIn.metadata);
} catch (e) {}
}
msgType = debugIn.msgType;
}
return this.openTestScriptDialog(script, scriptType, functionTitle,
functionName, argNames, msg, metadata, msgType, helpId, scriptLang);
functionName, argNames, debugIn, helpId, scriptLang);
})
);
} else {
return this.openTestScriptDialog(script, scriptType, functionTitle,
functionName, argNames, null, null, null, helpId, scriptLang);
functionName, argNames, debugEventBody, helpId, scriptLang);
}
}
private openTestScriptDialog(script: string, scriptType: string,
functionTitle: string, functionName: string, argNames: string[],
msg?: any, metadata?: {[key: string]: string}, msgType?: string, helpId?: string,
private openTestScriptDialog(script: string, scriptType: string, functionTitle: string, functionName: string,
argNames: string[], eventBody: DebugRuleNodeEventBody, helpId?: string,
scriptLang?: ScriptLanguage): Observable<string> {
let msg: any;
let metadata: {[key: string]: string};
let msgType: string;
if (eventBody && eventBody.data) {
try {
msg = JSON.parse(eventBody.data);
} catch (e) {}
}
if (!msg) {
msg = {
temperature: 22.4,
humidity: 78
};
}
if (eventBody && eventBody.metadata) {
try {
metadata = JSON.parse(eventBody.metadata);
} catch (e) {}
}
if (!metadata) {
metadata = {
deviceName: 'Test Device',
deviceType: 'default',
ts: new Date().getTime() + ''
};
} else {
metadata = sortObjectKeys(metadata);
}
if (!msgType) {
if (eventBody && eventBody.msgType) {
msgType = eventBody.msgType;
} else {
msgType = 'POST_TELEMETRY_REQUEST';
}
return this.dialog.open<NodeScriptTestDialogComponent, NodeScriptTestDialogData, string>(NodeScriptTestDialogComponent,

75
ui-ngx/src/app/core/services/utils.service.ts

@ -21,32 +21,31 @@ import { Inject, Injectable, NgZone } from '@angular/core';
import { WINDOW } from '@core/services/window.service';
import { ExceptionData } from '@app/shared/models/error.models';
import {
base64toObj,
base64toString,
baseUrl,
createLabelFromDatasource,
deepClone,
deleteNullProperties,
guid, hashCode,
guid,
hashCode,
isDefined,
isDefinedAndNotNull,
isString,
isUndefined,
objToBase64,
objToBase64URI,
base64toString,
base64toObj
objToBase64URI
} from '@core/utils';
import { WindowMessage } from '@shared/models/window-message.model';
import { TranslateService } from '@ngx-translate/core';
import { customTranslationsPrefix, i18nPrefix } from '@app/shared/models/constants';
import { DataKey, Datasource, DatasourceType, KeyInfo } from '@shared/models/widget.models';
import { EntityType } from '@shared/models/entity-type.models';
import { DataKeyType } from '@app/shared/models/telemetry/telemetry.models';
import { alarmFields } from '@shared/models/alarm.models';
import { alarmFields, alarmSeverityTranslations, alarmStatusTranslations } from '@shared/models/alarm.models';
import { materialColors } from '@app/shared/models/material.models';
import { WidgetInfo } from '@home/models/widget-component.models';
import jsonSchemaDefaults from 'json-schema-defaults';
import materialIconsCodepoints from '!raw-loader!./material-icons-codepoints.raw';
import { Observable, of, ReplaySubject } from 'rxjs';
import { Observable } from 'rxjs';
import { publishReplay, refCount } from 'rxjs/operators';
import { WidgetContext } from '@app/modules/home/models/widget-component.models';
import {
@ -56,6 +55,8 @@ import {
TelemetryType
} from '@shared/models/telemetry/telemetry.models';
import { EntityId } from '@shared/models/id/entity-id';
import { DatePipe } from '@angular/common';
import { entityTypeTranslations } from '@shared/models/entity-type.models';
const i18nRegExp = new RegExp(`{${i18nPrefix}:[^{}]+}`, 'g');
@ -86,13 +87,6 @@ const defaultAlarmFields: Array<string> = [
alarmFields.status.keyName
];
const commonMaterialIcons: Array<string> = ['more_horiz', 'more_vert', 'open_in_new',
'visibility', 'play_arrow', 'arrow_back', 'arrow_downward',
'arrow_forward', 'arrow_upwards', 'close', 'refresh', 'menu', 'show_chart', 'multiline_chart', 'pie_chart', 'insert_chart', 'people',
'person', 'domain', 'devices_other', 'now_widgets', 'dashboards', 'map', 'pin_drop', 'my_location', 'extension', 'search',
'settings', 'notifications', 'notifications_active', 'info', 'info_outline', 'warning', 'list', 'file_download', 'import_export',
'share', 'add', 'edit', 'done', 'delete'];
// @dynamic
@Injectable({
providedIn: 'root'
@ -121,10 +115,9 @@ export class UtilsService {
defaultAlarmDataKeys: Array<DataKey> = [];
materialIcons: Array<string> = [];
constructor(@Inject(WINDOW) private window: Window,
private zone: NgZone,
private datePipe: DatePipe,
private translate: TranslateService) {
let frame: Element = null;
try {
@ -180,6 +173,27 @@ export class UtilsService {
return deepClone(this.defaultAlarmDataKeys);
}
public defaultAlarmFieldContent(key: DataKey | {name: string}, value: any): string {
if (isDefined(value)) {
const alarmField = alarmFields[key.name];
if (alarmField) {
if (alarmField.time) {
return value ? this.datePipe.transform(value, 'yyyy-MM-dd HH:mm:ss') : '';
} else if (alarmField === alarmFields.severity) {
return this.translate.instant(alarmSeverityTranslations.get(value));
} else if (alarmField === alarmFields.status) {
return alarmStatusTranslations.get(value) ? this.translate.instant(alarmStatusTranslations.get(value)) : value;
} else if (alarmField === alarmFields.originatorType) {
return this.translate.instant(entityTypeTranslations.get(value).type);
} else if (alarmField.value === alarmFields.assignee.value) {
return '';
}
}
return value;
}
return '';
}
public generateObjectFromJsonSchema(schema: any): any {
const obj = jsonSchemaDefaults(schema);
deleteNullProperties(obj);
@ -306,31 +320,6 @@ export class UtilsService {
return datasources;
}
public getMaterialIcons(): Observable<Array<string>> {
if (this.materialIcons.length) {
return of(this.materialIcons);
} else {
const materialIconsSubject = new ReplaySubject<Array<string>>();
this.zone.runOutsideAngular(() => {
const codepointsArray = materialIconsCodepoints
.split('\n')
.filter((codepoint) => codepoint && codepoint.length);
codepointsArray.forEach((codepoint) => {
const values = codepoint.split(' ');
if (values && values.length === 2) {
this.materialIcons.push(values[0]);
}
});
materialIconsSubject.next(this.materialIcons);
});
return materialIconsSubject.asObservable();
}
}
public getCommonMaterialIcons(): Array<string> {
return commonMaterialIcons;
}
public getMaterialColor(index: number) {
const colorIndex = index % materialColors.length;
return materialColors[colorIndex].value;
@ -411,7 +400,7 @@ export class UtilsService {
public stringToHslColor(str: string, saturationPercentage: number, lightnessPercentage: number): string {
if (str && str.length) {
let hue = hashCode(str) % 360;
const hue = hashCode(str) % 360;
return `hsl(${hue}, ${saturationPercentage}%, ${lightnessPercentage}%)`;
}
}

2
ui-ngx/src/app/modules/common/modules-map.ts

@ -181,6 +181,7 @@ import * as StringItemsListComponent from '@shared/components/string-items-list.
import * as ToggleHeaderComponent from '@shared/components/toggle-header.component';
import * as ToggleSelectComponent from '@shared/components/toggle-select.component';
import * as UnitInputComponent from '@shared/components/unit-input.component';
import * as MaterialIconsComponent from '@shared/components/material-icons.component';
import * as AddEntityDialogComponent from '@home/components/entity/add-entity-dialog.component';
import * as EntitiesTableComponent from '@home/components/entity/entities-table.component';
@ -482,6 +483,7 @@ class ModulesMap implements IModulesMap {
'@shared/components/toggle-header.component': ToggleHeaderComponent,
'@shared/components/toggle-select.component': ToggleSelectComponent,
'@shared/components/unit-input.component': UnitInputComponent,
'@shared/components/material-icons.component': MaterialIconsComponent,
'@home/components/entity/add-entity-dialog.component': AddEntityDialogComponent,
'@home/components/entity/entities-table.component': EntitiesTableComponent,

6
ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.scss

@ -45,9 +45,3 @@
margin-right: 8px;
}
}
.drop-down-icon {
&.inline {
margin-right: -12px;
}
}

4
ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts

@ -637,6 +637,10 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa
}
}
cellActionDescriptorsUpdated() {
this.cellActionDescriptors = [...this.entitiesTableConfig.cellActionDescriptors];
}
headerCellStyle(column: EntityColumn<BaseData<HasId>>) {
const index = this.entitiesTableConfig.columns.indexOf(column);
let res = this.headerCellStyleCache[index];

29
ui-ngx/src/app/modules/home/components/event/event-table-config.ts

@ -21,7 +21,7 @@ import {
EntityTableColumn,
EntityTableConfig
} from '@home/models/entity/entities-table-config.models';
import { DebugEventType, Event, EventType, FilterEventBody } from '@shared/models/event.models';
import { DebugEventType, Event, EventBody, EventType, FilterEventBody } from '@shared/models/event.models';
import { TimePageLink } from '@shared/models/page/page-link';
import { TranslateService } from '@ngx-translate/core';
import { DatePipe } from '@angular/common';
@ -41,7 +41,7 @@ import {
} from '@home/components/event/event-content-dialog.component';
import { isEqual, sortObjectKeys } from '@core/utils';
import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { ChangeDetectorRef, Injector, StaticProvider, ViewContainerRef } from '@angular/core';
import { ChangeDetectorRef, EventEmitter, Injector, StaticProvider, ViewContainerRef } from '@angular/core';
import { ComponentPortal } from '@angular/cdk/portal';
import {
EVENT_FILTER_PANEL_DATA,
@ -61,6 +61,7 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
set eventType(eventType: EventType | DebugEventType) {
if (this.eventTypeValue !== eventType) {
this.eventTypeValue = eventType;
this.updateCellAction();
this.updateColumns(true);
this.updateFilterColumns();
}
@ -84,7 +85,9 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
private debugEventTypes: Array<DebugEventType> = null,
private overlay: Overlay,
private viewContainerRef: ViewContainerRef,
private cd: ChangeDetectorRef) {
private cd: ChangeDetectorRef,
public testButtonLabel?: string,
private debugEventSelected?: EventEmitter<EventBody>) {
super();
this.loadDataOnInit = false;
this.tableTitle = '';
@ -120,6 +123,7 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
this.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC};
this.updateColumns();
this.updateCellAction();
this.updateFilterColumns();
this.headerActionDescriptors.push({
@ -349,6 +353,25 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
}
}
updateCellAction() {
this.cellActionDescriptors = [];
switch (this.eventType) {
case DebugEventType.DEBUG_RULE_NODE:
if (this.testButtonLabel) {
this.cellActionDescriptors.push({
name: this.translate.instant('rulenode.test-with-this-message', {test: this.translate.instant(this.testButtonLabel)}),
icon: 'bug_report',
isEnabled: (entity) => entity.body.type === 'IN',
onAction: ($event, entity) => {
this.debugEventSelected.next(entity.body);
}
});
}
break;
}
this.getTable()?.cellActionDescriptorsUpdated();
}
showContent($event: MouseEvent, content: string, title: string, contentType: ContentType = null, sortKeys = false): void {
if ($event) {
$event.stopPropagation();

37
ui-ngx/src/app/modules/home/components/event/event-table.component.ts

@ -17,10 +17,10 @@
import {
AfterViewInit,
ChangeDetectorRef,
Component,
Component, EventEmitter,
Input,
OnDestroy,
OnInit,
OnInit, Output,
ViewChild,
ViewContainerRef
} from '@angular/core';
@ -32,9 +32,10 @@ import { EntitiesTableComponent } from '@home/components/entity/entities-table.c
import { EventTableConfig } from './event-table-config';
import { EventService } from '@core/http/event.service';
import { DialogService } from '@core/services/dialog.service';
import { DebugEventType, EventType } from '@shared/models/event.models';
import { DebugEventType, EventBody, EventType } from '@shared/models/event.models';
import { Overlay } from '@angular/cdk/overlay';
import { Subscription } from 'rxjs';
import { isNotEmptyStr } from '@core/utils';
@Component({
selector: 'tb-event-table',
@ -59,6 +60,10 @@ export class EventTableComponent implements OnInit, AfterViewInit, OnDestroy {
dirtyValue = false;
entityIdValue: EntityId;
get active(): boolean {
return this.activeValue;
}
@Input()
set active(active: boolean) {
if (this.activeValue !== active) {
@ -83,6 +88,28 @@ export class EventTableComponent implements OnInit, AfterViewInit, OnDestroy {
}
}
private functionTestButtonLabelValue: string;
get functionTestButtonLabel(): string {
return this.functionTestButtonLabelValue;
}
@Input()
set functionTestButtonLabel(value: string) {
if (isNotEmptyStr(value)) {
this.functionTestButtonLabelValue = value;
} else {
this.functionTestButtonLabelValue = '';
}
if (this.eventTableConfig) {
this.eventTableConfig.testButtonLabel = this.functionTestButtonLabel;
this.eventTableConfig.updateCellAction();
}
}
@Output()
debugEventSelected = new EventEmitter<EventBody>();
@ViewChild(EntitiesTableComponent, {static: true}) entitiesTable: EntitiesTableComponent;
eventTableConfig: EventTableConfig;
@ -114,7 +141,9 @@ export class EventTableComponent implements OnInit, AfterViewInit, OnDestroy {
this.debugEventTypes,
this.overlay,
this.viewContainerRef,
this.cd
this.cd,
this.functionTestButtonLabel,
this.debugEventSelected
);
}

27
ui-ngx/src/app/modules/home/components/widget/config/basic/alarm/alarms-table-basic-config.component.html

@ -58,16 +58,15 @@
<input matInput formControlName="title" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-row space-between same-padding">
<div class="tb-form-row space-between">
<mat-slide-toggle class="mat-slide" formControlName="showTitleIcon">
{{ 'widget-config.card-icon' | translate }}
</mat-slide-toggle>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-material-icon-select asBoxInput
[color]="alarmsTableWidgetConfigForm.get('iconColor').value"
formControlName="titleIcon">
</tb-material-icon-select>
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput
formControlName="iconColor">
</tb-color-input>
@ -82,23 +81,17 @@
<mat-chip-option value="fullscreen">{{ 'fullscreen.fullscreen' | translate }}</mat-chip-option>
</mat-chip-listbox>
</div>
<div class="tb-form-row space-between same-padding">
<div class="tb-form-row space-between">
<div>{{ 'widget-config.text-color' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput
formControlName="color">
</tb-color-input>
</div>
<tb-color-input asBoxInput
formControlName="color">
</tb-color-input>
</div>
<div class="tb-form-row space-between same-padding">
<div class="tb-form-row space-between">
<div>{{ 'widget-config.background-color' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput
formControlName="backgroundColor">
</tb-color-input>
</div>
<tb-color-input asBoxInput
formControlName="backgroundColor">
</tb-color-input>
</div>
</div>
<tb-widget-actions-panel

27
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/entities-table-basic-config.component.html

@ -47,16 +47,15 @@
<input matInput formControlName="title" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-row space-between same-padding">
<div class="tb-form-row space-between">
<mat-slide-toggle class="mat-slide" formControlName="showTitleIcon">
{{ 'widget-config.card-icon' | translate }}
</mat-slide-toggle>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-material-icon-select asBoxInput
[color]="entitiesTableWidgetConfigForm.get('iconColor').value"
formControlName="titleIcon">
</tb-material-icon-select>
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput
formControlName="iconColor">
</tb-color-input>
@ -70,23 +69,17 @@
<mat-chip-option value="fullscreen">{{ 'fullscreen.fullscreen' | translate }}</mat-chip-option>
</mat-chip-listbox>
</div>
<div class="tb-form-row space-between same-padding">
<div class="tb-form-row space-between">
<div>{{ 'widget-config.text-color' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput
formControlName="color">
</tb-color-input>
</div>
<tb-color-input asBoxInput
formControlName="color">
</tb-color-input>
</div>
<div class="tb-form-row space-between same-padding">
<div class="tb-form-row space-between">
<div>{{ 'widget-config.background-color' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput
formControlName="backgroundColor">
</tb-color-input>
</div>
<tb-color-input asBoxInput
formControlName="backgroundColor">
</tb-color-input>
</div>
</div>
<tb-widget-actions-panel

22
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.html

@ -67,23 +67,17 @@
<mat-chip-option value="fullscreen">{{ 'fullscreen.fullscreen' | translate }}</mat-chip-option>
</mat-chip-listbox>
</div>
<div class="tb-form-row space-between same-padding">
<div class="tb-form-row space-between">
<div>{{ 'widget-config.text-color' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput
formControlName="color">
</tb-color-input>
</div>
<tb-color-input asBoxInput
formControlName="color">
</tb-color-input>
</div>
<div class="tb-form-row space-between same-padding">
<div class="tb-form-row space-between">
<div>{{ 'widget-config.background' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput
formControlName="backgroundColor">
</tb-color-input>
</div>
<tb-color-input asBoxInput
formControlName="backgroundColor">
</tb-color-input>
</div>
</div>
<tb-widget-actions-panel

27
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/timeseries-table-basic-config.component.html

@ -47,16 +47,15 @@
<input matInput formControlName="title" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-row space-between same-padding">
<div class="tb-form-row space-between">
<mat-slide-toggle class="mat-slide" formControlName="showTitleIcon">
{{ 'widget-config.card-icon' | translate }}
</mat-slide-toggle>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-material-icon-select asBoxInput
[color]="timeseriesTableWidgetConfigForm.get('iconColor').value"
formControlName="titleIcon">
</tb-material-icon-select>
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput
formControlName="iconColor">
</tb-color-input>
@ -70,23 +69,17 @@
<mat-chip-option value="fullscreen">{{ 'fullscreen.fullscreen' | translate }}</mat-chip-option>
</mat-chip-listbox>
</div>
<div class="tb-form-row space-between same-padding">
<div class="tb-form-row space-between">
<div>{{ 'widget-config.text-color' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput
formControlName="color">
</tb-color-input>
</div>
<tb-color-input asBoxInput
formControlName="color">
</tb-color-input>
</div>
<div class="tb-form-row space-between same-padding">
<div class="tb-form-row space-between">
<div>{{ 'widget-config.background-color' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput
formControlName="backgroundColor">
</tb-color-input>
</div>
<tb-color-input asBoxInput
formControlName="backgroundColor">
</tb-color-input>
</div>
</div>
<tb-widget-actions-panel

27
ui-ngx/src/app/modules/home/components/widget/config/basic/chart/flot-basic-config.component.html

@ -47,16 +47,15 @@
<input matInput formControlName="title" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-row space-between same-padding">
<div class="tb-form-row space-between">
<mat-slide-toggle class="mat-slide" formControlName="showTitleIcon">
{{ 'widget-config.card-icon' | translate }}
</mat-slide-toggle>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-material-icon-select asBoxInput
[color]="flotWidgetConfigForm.get('iconColor').value"
formControlName="titleIcon">
</tb-material-icon-select>
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput
formControlName="iconColor">
</tb-color-input>
@ -68,23 +67,17 @@
<mat-chip-option value="fullscreen">{{ 'fullscreen.fullscreen' | translate }}</mat-chip-option>
</mat-chip-listbox>
</div>
<div class="tb-form-row space-between same-padding">
<div class="tb-form-row space-between">
<div>{{ 'widget-config.text-color' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput
formControlName="color">
</tb-color-input>
</div>
<tb-color-input asBoxInput
formControlName="color">
</tb-color-input>
</div>
<div class="tb-form-row space-between same-padding">
<div class="tb-form-row space-between">
<div>{{ 'widget-config.background-color' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput
formControlName="backgroundColor">
</tb-color-input>
</div>
<tb-color-input asBoxInput
formControlName="backgroundColor">
</tb-color-input>
</div>
</div>
<div class="tb-form-panel">

11
ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.html

@ -59,14 +59,11 @@
</mat-form-field>
</div>
</ng-container>
<div class="tb-form-row space-between same-padding" *ngIf="!hideDataKeyColor">
<div class="tb-form-row space-between" *ngIf="!hideDataKeyColor">
<div>{{ 'datakey.color' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput
formControlName="color">
</tb-color-input>
</div>
<tb-color-input asBoxInput
formControlName="color">
</tb-color-input>
</div>
<ng-container *ngIf="widgetType === widgetTypes.latest && modelValue.type === dataKeyTypes.timeseries">
<mat-form-field subscriptSizing="dynamic">

2
ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.html

@ -72,7 +72,7 @@
</div>
</div>
<div *ngIf="!hideDataKeyColor" style="padding: 3px;">
<div class="tb-color-preview small box" (click)="showColorPicker(key)">
<div #keyColorButton class="tb-color-preview small box" (click)="openColorPickerPopup(key, $event, keyColorButton)">
<div class="tb-color-result" [ngStyle]="{background: key.color}"></div>
</div>
</div>

32
ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.ts

@ -27,6 +27,7 @@ import {
SimpleChanges,
SkipSelf,
ViewChild,
ViewContainerRef,
ViewEncapsulation
} from '@angular/core';
import {
@ -56,7 +57,6 @@ import { alarmFields } from '@shared/models/alarm.models';
import { UtilsService } from '@core/services/utils.service';
import { ErrorStateMatcher } from '@angular/material/core';
import { TruncatePipe } from '@shared/pipe/truncate.pipe';
import { DialogService } from '@core/services/dialog.service';
import { MatDialog } from '@angular/material/dialog';
import {
DataKeyConfigDialogComponent,
@ -69,6 +69,8 @@ import { DndDropEvent } from 'ngx-drag-drop/lib/dnd-dropzone.directive';
import { moveItemInArray } from '@angular/cdk/drag-drop';
import { coerceBoolean } from '@shared/decorators/coercion';
import { DatasourceComponent } from '@home/components/widget/config/datasource.component';
import { ColorPickerPanelComponent } from '@shared/components/color-picker/color-picker-panel.component';
import { TbPopoverService } from '@shared/components/popover.service';
@Component({
selector: 'tb-data-keys',
@ -208,10 +210,11 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
private datasourceComponent: DatasourceComponent,
public translate: TranslateService,
private utils: UtilsService,
private dialogs: DialogService,
private dialog: MatDialog,
private fb: UntypedFormBuilder,
private cd: ChangeDetectorRef,
private popoverService: TbPopoverService,
private viewContainerRef: ViewContainerRef,
private renderer: Renderer2,
public truncate: TruncatePipe) {
}
@ -471,15 +474,30 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
this.propagateChange(this.modelValue);
}
showColorPicker(key: DataKey) {
this.dialogs.colorPicker(key.color).subscribe(
(color) => {
openColorPickerPopup(key: DataKey, $event: Event, keyColorButton: HTMLDivElement) {
if ($event) {
$event.stopPropagation();
}
const trigger = keyColorButton;
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
} else {
const colorPickerPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, ColorPickerPanelComponent, 'left', true, null,
{
color: key.color
},
{},
{}, {}, true);
colorPickerPopover.tbComponentRef.instance.popover = colorPickerPopover;
colorPickerPopover.tbComponentRef.instance.colorSelected.subscribe((color) => {
colorPickerPopover.hide();
if (color && key.color !== color) {
key.color = color;
this.propagateChange(this.modelValue);
}
}
);
});
}
}
editDataKey(key: DataKey, index: number) {

40
ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts

@ -23,6 +23,7 @@ import {
Injector,
Input,
NgZone,
OnDestroy,
OnInit,
StaticProvider,
ViewChild,
@ -52,7 +53,6 @@ import { Direction } from '@shared/models/page/sort-order';
import { CollectionViewer, DataSource, SelectionModel } from '@angular/cdk/collections';
import { BehaviorSubject, forkJoin, fromEvent, merge, Observable, Subscription } from 'rxjs';
import { emptyPageData, PageData } from '@shared/models/page/page-data';
import { entityTypeTranslations } from '@shared/models/entity-type.models';
import { debounceTime, distinctUntilChanged, map, take, tap } from 'rxjs/operators';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort, SortDirection } from '@angular/material/sort';
@ -92,15 +92,7 @@ import {
DisplayColumnsPanelComponent,
DisplayColumnsPanelData
} from '@home/components/widget/lib/display-columns-panel.component';
import {
AlarmDataInfo,
alarmFields,
AlarmInfo,
alarmSeverityColors,
alarmSeverityTranslations,
AlarmStatus,
alarmStatusTranslations
} from '@shared/models/alarm.models';
import { AlarmDataInfo, alarmFields, AlarmInfo, alarmSeverityColors, AlarmStatus } from '@shared/models/alarm.models';
import { DatePipe } from '@angular/common';
import {
AlarmDetailsDialogComponent,
@ -139,7 +131,6 @@ import {
AlarmFilterConfigData
} from '@home/components/alarm/alarm-filter-config.component';
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { UserId } from '@shared/models/id/user-id';
interface AlarmsTableWidgetSettings extends TableWidgetSettings {
alarmsTitle: string;
@ -167,7 +158,7 @@ interface AlarmWidgetActionDescriptor extends TableCellButtonActionDescriptor {
templateUrl: './alarms-table-widget.component.html',
styleUrls: ['./alarms-table-widget.component.scss', './table-widget.scss']
})
export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, AfterViewInit {
export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, OnDestroy, AfterViewInit {
@Input()
ctx: WidgetContext;
@ -431,7 +422,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
keySettings.columnWidth = '120px';
}
if (alarmField && alarmField.keyName === alarmFields.assignee.keyName) {
keySettings.columnWidth = '120px'
keySettings.columnWidth = '120px';
}
}
this.stylesInfo[dataKey.def] = getCellStyleInfo(keySettings, 'value, alarm, ctx');
@ -543,14 +534,12 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
overlayRef.dispose();
});
const columns: DisplayColumn[] = this.columns.map(column => {
return {
const columns: DisplayColumn[] = this.columns.map(column => ({
title: column.title,
def: column.def,
display: this.displayedColumns.indexOf(column.def) > -1,
selectable: this.columnSelectionAvailability[column.def]
};
});
}));
const providers: StaticProvider[] = [
{
@ -1010,7 +999,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
data: {
alarmId: alarm.id.id
}
}).afterClosed()
}).afterClosed();
}
}
@ -1018,20 +1007,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
if (isDefined(value)) {
const alarmField = alarmFields[key.name];
if (alarmField) {
if (alarmField.time) {
return value ? this.datePipe.transform(value, 'yyyy-MM-dd HH:mm:ss') : '';
} else if (alarmField.value === alarmFields.severity.value) {
return this.translate.instant(alarmSeverityTranslations.get(value));
} else if (alarmField.value === alarmFields.status.value) {
return alarmStatusTranslations.get(value) ? this.translate.instant(alarmStatusTranslations.get(value)) : value;
} else if (alarmField.value === alarmFields.originatorType.value) {
return this.translate.instant(entityTypeTranslations.get(value).type);
} else if (alarmField.value === alarmFields.assignee.value) {
return '';
}
else {
return value;
}
return this.utils.defaultAlarmFieldContent(key, value);
}
const entityField = entityFields[key.name];
if (entityField) {

46
ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page-widget.scss

@ -67,50 +67,4 @@
color: inherit;
}
}
.tb-no-data-available {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.tb-no-data-bg {
margin: 10px;
position: relative;
flex: 1;
width: 100%;
max-height: 100px;
&:before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: #305680;
-webkit-mask-image: url(/assets/home/no_data_folder_bg.svg);
-webkit-mask-repeat: no-repeat;
-webkit-mask-size: contain;
-webkit-mask-position: center;
mask-image: url(/assets/home/no_data_folder_bg.svg);
mask-repeat: no-repeat;
mask-size: contain;
mask-position: center;
}
}
.tb-no-data-text {
font-weight: 500;
font-size: 14px;
line-height: 20px;
letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.54);
@media #{$mat-md-lg} {
font-size: 12px;
line-height: 16px;
}
}
}

11
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.html

@ -235,14 +235,11 @@
<input matInput formControlName="comparisonValuesLabel" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-row space-between same-padding">
<div class="tb-form-row space-between">
<div>{{ 'widgets.chart.comparison-line-color' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput
formControlName="color">
</tb-color-input>
</div>
<tb-color-input asBoxInput
formControlName="color">
</tb-color-input>
</div>
</ng-template>
</mat-expansion-panel>

11
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-latest-key-settings.component.html

@ -36,14 +36,11 @@
<span matSuffix>px</span>
</mat-form-field>
</div>
<div class="tb-form-row space-between same-padding">
<div class="tb-form-row space-between">
<div>{{ 'widgets.chart.threshold-color' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput
formControlName="thresholdColor">
</tb-color-input>
</div>
<tb-color-input asBoxInput
formControlName="thresholdColor">
</tb-color-input>
</div>
</ng-template>
</mat-expansion-panel>

54
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.html

@ -61,14 +61,13 @@
<input matInput formControlName="thresholdsLineWidth" type="number" min="0" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-row space-between same-padding">
<div class="tb-form-row space-between">
<div>{{ 'widgets.chart.default-font' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="fontSize" type="number" min="0" placeholder="{{ 'widget-config.set' | translate }}">
<span matSuffix>px</span>
</mat-form-field>
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput
formControlName="fontColor">
</tb-color-input>
@ -132,14 +131,11 @@
</mat-panel-description>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div class="tb-form-row space-between same-padding">
<div class="tb-form-row space-between">
<div>{{ 'widget-config.color' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput
formControlName="color">
</tb-color-input>
</div>
<tb-color-input asBoxInput
formControlName="color">
</tb-color-input>
</div>
<div class="tb-form-row space-between">
<div translate>widget-config.decimals-short</div>
@ -187,14 +183,11 @@
</mat-panel-description>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div class="tb-form-row space-between same-padding">
<div class="tb-form-row space-between">
<div>{{ 'widget-config.color' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput
formControlName="color">
</tb-color-input>
</div>
<tb-color-input asBoxInput
formControlName="color">
</tb-color-input>
</div>
</ng-template>
</mat-expansion-panel>
@ -213,36 +206,29 @@
{{ 'widgets.chart.horizontal-grid-lines' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row space-between same-padding">
<div class="tb-form-row space-between">
<div>{{ 'widgets.chart.grid-lines-color' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput
formControlName="tickColor">
</tb-color-input>
</div>
<tb-color-input asBoxInput
formControlName="tickColor">
</tb-color-input>
</div>
<div class="tb-form-row space-between same-padding">
<div class="tb-form-row space-between">
<div>{{ 'widgets.chart.border' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="outlineWidth" type="number" min="0" placeholder="{{ 'widget-config.set' | translate }}">
<span matSuffix>px</span>
</mat-form-field>
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput
formControlName="color">
</tb-color-input>
</div>
</div>
<div class="tb-form-row space-between same-padding">
<div class="tb-form-row space-between">
<div>{{ 'widgets.chart.background-color' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput
formControlName="backgroundColor">
</tb-color-input>
</div>
<tb-color-input asBoxInput
formControlName="backgroundColor">
</tb-color-input>
</div>
</div>
<div class="tb-form-panel tb-slide-toggle">

27
ui-ngx/src/app/modules/home/components/widget/widget-config.component.html

@ -48,11 +48,11 @@
<input matInput formControlName="titleTooltip" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-row space-between same-padding">
<div class="tb-form-row space-between">
<mat-slide-toggle class="mat-slide" formControlName="showTitleIcon">
{{ 'widget-config.display-icon' | translate }}
</mat-slide-toggle>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-material-icon-select asBoxInput
[color]="widgetSettings.get('iconColor').value"
formControlName="titleIcon">
@ -60,7 +60,6 @@
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="iconSize">
</mat-form-field>
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput
formControlName="iconColor">
</tb-color-input>
@ -84,23 +83,17 @@
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.card-style</div>
<div class="tb-form-row space-between same-padding">
<div class="tb-form-row space-between">
<div>{{ 'widget-config.text-color' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput
formControlName="color">
</tb-color-input>
</div>
<tb-color-input asBoxInput
formControlName="color">
</tb-color-input>
</div>
<div class="tb-form-row space-between same-padding">
<div class="tb-form-row space-between">
<div>{{ 'widget-config.background-color' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput
formControlName="backgroundColor">
</tb-color-input>
</div>
<tb-color-input asBoxInput
formControlName="backgroundColor">
</tb-color-input>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widget-config.padding' | translate }}</div>

1
ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts

@ -80,6 +80,7 @@ export interface IEntitiesTableComponent {
exitFilterMode(): void;
resetSortAndFilter(update?: boolean, preserveTimewindow?: boolean): void;
columnsUpdated(resetData?: boolean): void;
cellActionDescriptorsUpdated(): void;
headerCellStyle(column: EntityColumn<BaseData<HasId>>): any;
clearCellCache(col: number, row: number): void;
cellContent(entity: BaseData<HasId>, column: EntityColumn<BaseData<HasId>>, row: number): any;

18
ui-ngx/src/app/modules/home/pages/api-usage/api-usage-routing.module.ts

@ -14,10 +14,21 @@
/// limitations under the License.
///
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { inject, NgModule } from '@angular/core';
import { ActivatedRouteSnapshot, ResolveFn, RouterModule, RouterStateSnapshot, Routes } from '@angular/router';
import { Authority } from '@shared/models/authority.enum';
import { ApiUsageComponent } from '@home/pages/api-usage/api-usage.component';
import { Dashboard } from '@shared/models/dashboard.models';
import { ResourcesService } from '@core/services/resources.service';
import { Observable } from 'rxjs';
const apiUsageDashboardJson = '/assets/dashboard/api_usage.json';
export const apiUsageDashboardResolver: ResolveFn<Dashboard> = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
resourcesService = inject(ResourcesService)
): Observable<Dashboard> => resourcesService.loadJsonResource(apiUsageDashboardJson);
const routes: Routes = [
{
@ -30,6 +41,9 @@ const routes: Routes = [
label: 'api-usage.api-usage',
icon: 'insert_chart'
}
},
resolve: {
apiUsageDashboard: apiUsageDashboardResolver
}
}
];

16
ui-ngx/src/app/modules/home/pages/api-usage/api-usage.component.ts

@ -14,28 +14,24 @@
/// limitations under the License.
///
import { Component, OnInit } from '@angular/core';
import { Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { PageComponent } from '@shared/components/page.component';
import apiUsageDashboardJson from '!raw-loader!./api_usage_json.raw';
import { Dashboard } from '@shared/models/dashboard.models';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'tb-api-usage',
templateUrl: './api-usage.component.html',
styleUrls: ['./api-usage.component.scss']
})
export class ApiUsageComponent extends PageComponent implements OnInit {
export class ApiUsageComponent extends PageComponent {
apiUsageDashboard: Dashboard;
apiUsageDashboard: Dashboard = this.route.snapshot.data.apiUsageDashboard;
constructor(protected store: Store<AppState>) {
constructor(protected store: Store<AppState>,
private route: ActivatedRoute) {
super(store);
}
ngOnInit() {
this.apiUsageDashboard = JSON.parse(apiUsageDashboardJson);
}
}

110
ui-ngx/src/app/modules/home/pages/home-links/home-links-routing.module.ts

@ -14,8 +14,8 @@
/// limitations under the License.
///
import { Injectable, NgModule } from '@angular/core';
import { Resolve, RouterModule, Routes } from '@angular/router';
import { inject, NgModule } from '@angular/core';
import { ActivatedRouteSnapshot, ResolveFn, RouterModule, RouterStateSnapshot, Routes } from '@angular/router';
import { HomeLinksComponent } from './home-links.component';
import { Authority } from '@shared/models/authority.enum';
@ -25,57 +25,19 @@ import { DashboardService } from '@core/http/dashboard.service';
import { select, Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { map } from 'rxjs/operators';
import {
getCurrentAuthUser,
selectHasRepository,
selectPersistDeviceStateToTelemetry
} from '@core/auth/auth.selectors';
import sysAdminHomePageDashboardJson from '!raw-loader!./sys_admin_home_page.raw';
import tenantAdminHomePageDashboardJson from '!raw-loader!./tenant_admin_home_page.raw';
import customerUserHomePageDashboardJson from '!raw-loader!./customer_user_home_page.raw';
import { getCurrentAuthUser, selectPersistDeviceStateToTelemetry } from '@core/auth/auth.selectors';
import { EntityKeyType } from '@shared/models/query/query.models';
import { ResourcesService } from '@core/services/resources.service';
@Injectable()
export class HomeDashboardResolver implements Resolve<HomeDashboard> {
const sysAdminHomePageJson = '/assets/dashboard/sys_admin_home_page.json';
const tenantAdminHomePageJson = '/assets/dashboard/tenant_admin_home_page.json';
const customerUserHomePageJson = '/assets/dashboard/customer_user_home_page.json';
constructor(private dashboardService: DashboardService,
private store: Store<AppState>) {
}
resolve(): Observable<HomeDashboard> {
return this.dashboardService.getHomeDashboard().pipe(
mergeMap((dashboard) => {
if (!dashboard) {
let dashboard$: Observable<HomeDashboard>;
const authority = getCurrentAuthUser(this.store).authority;
switch (authority) {
case Authority.SYS_ADMIN:
dashboard$ = of(JSON.parse(sysAdminHomePageDashboardJson));
break;
case Authority.TENANT_ADMIN:
dashboard$ = this.updateDeviceActivityKeyFilterIfNeeded(JSON.parse(tenantAdminHomePageDashboardJson));
break;
case Authority.CUSTOMER_USER:
dashboard$ = this.updateDeviceActivityKeyFilterIfNeeded(JSON.parse(customerUserHomePageDashboardJson));
break;
}
if (dashboard$) {
return dashboard$.pipe(
map((homeDashboard) => {
homeDashboard.hideDashboardToolbar = true;
return homeDashboard;
})
);
}
}
return of(dashboard);
})
);
}
private updateDeviceActivityKeyFilterIfNeeded(dashboard: HomeDashboard): Observable<HomeDashboard> {
return this.store.pipe(select(selectPersistDeviceStateToTelemetry)).pipe(
map((persistToTelemetry) => {
const updateDeviceActivityKeyFilterIfNeeded = (store: Store<AppState>,
dashboard$: Observable<HomeDashboard>): Observable<HomeDashboard> =>
store.pipe(select(selectPersistDeviceStateToTelemetry)).pipe(
mergeMap((persistToTelemetry) => dashboard$.pipe(
map((dashboard) => {
if (persistToTelemetry) {
for (const filterId of Object.keys(dashboard.configuration.filters)) {
if (['Active Devices', 'Inactive Devices'].includes(dashboard.configuration.filters[filterId].filter)) {
@ -85,9 +47,44 @@ export class HomeDashboardResolver implements Resolve<HomeDashboard> {
}
return dashboard;
})
);
}
}
))
);
export const homeDashboardResolver: ResolveFn<HomeDashboard> = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
dashboardService = inject(DashboardService),
resourcesService = inject(ResourcesService),
store: Store<AppState> = inject(Store<AppState>)
): Observable<HomeDashboard> =>
dashboardService.getHomeDashboard().pipe(
mergeMap((dashboard) => {
if (!dashboard) {
let dashboard$: Observable<HomeDashboard>;
const authority = getCurrentAuthUser(store).authority;
switch (authority) {
case Authority.SYS_ADMIN:
dashboard$ = resourcesService.loadJsonResource(sysAdminHomePageJson);
break;
case Authority.TENANT_ADMIN:
dashboard$ = updateDeviceActivityKeyFilterIfNeeded(store, resourcesService.loadJsonResource(tenantAdminHomePageJson));
break;
case Authority.CUSTOMER_USER:
dashboard$ = updateDeviceActivityKeyFilterIfNeeded(store, resourcesService.loadJsonResource(customerUserHomePageJson));
break;
}
if (dashboard$) {
return dashboard$.pipe(
map((homeDashboard) => {
homeDashboard.hideDashboardToolbar = true;
return homeDashboard;
})
);
}
}
return of(dashboard);
})
);
const routes: Routes = [
{
@ -102,16 +99,13 @@ const routes: Routes = [
}
},
resolve: {
homeDashboard: HomeDashboardResolver
homeDashboard: homeDashboardResolver
}
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
providers: [
HomeDashboardResolver
]
exports: [RouterModule]
})
export class HomeLinksRoutingModule { }

33
ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.ts

@ -18,14 +18,22 @@ import {
AfterViewInit,
Component,
ComponentRef,
EventEmitter,
forwardRef,
Input,
OnDestroy,
OnInit,
Output,
ViewChild,
ViewContainerRef
} from '@angular/core';
import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import {
ControlValueAccessor,
NG_VALUE_ACCESSOR,
UntypedFormBuilder,
UntypedFormGroup,
Validators
} from '@angular/forms';
import {
IRuleNodeConfigurationComponent,
RuleNodeConfiguration,
@ -76,6 +84,12 @@ export class RuleNodeConfigComponent implements ControlValueAccessor, OnInit, On
@Input()
ruleChainType: RuleChainType;
@Output()
initRuleNode = new EventEmitter<void>();
@Output()
changeScript = new EventEmitter<void>();
nodeDefinitionValue: RuleNodeDefinition;
@Input()
@ -85,6 +99,7 @@ export class RuleNodeConfigComponent implements ControlValueAccessor, OnInit, On
if (this.nodeDefinitionValue) {
this.validateDefinedDirective();
}
setTimeout(() => this.initRuleNode.emit());
}
}
@ -98,8 +113,11 @@ export class RuleNodeConfigComponent implements ControlValueAccessor, OnInit, On
changeSubscription: Subscription;
changeScriptSubscription: Subscription;
definedConfigComponent: IRuleNodeConfigurationComponent;
private definedConfigComponentRef: ComponentRef<IRuleNodeConfigurationComponent>;
private definedConfigComponent: IRuleNodeConfigurationComponent;
private configuration: RuleNodeConfiguration;
@ -127,6 +145,14 @@ export class RuleNodeConfigComponent implements ControlValueAccessor, OnInit, On
if (this.definedConfigComponentRef) {
this.definedConfigComponentRef.destroy();
}
if (this.changeSubscription) {
this.changeSubscription.unsubscribe();
this.changeSubscription = null;
}
if (this.changeScriptSubscription) {
this.changeScriptSubscription.unsubscribe();
this.changeScriptSubscription = null;
}
}
ngAfterViewInit(): void {
@ -199,6 +225,9 @@ export class RuleNodeConfigComponent implements ControlValueAccessor, OnInit, On
this.changeSubscription = this.definedConfigComponent.configurationChanged.subscribe((configuration) => {
this.updateModel(configuration);
});
if (this.definedConfigComponent?.changeScript) {
this.changeScriptSubscription = this.definedConfigComponent.changeScript.subscribe(() => this.changeScript.emit());
}
}
}

4
ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html

@ -50,7 +50,9 @@
[ruleNodeId]="ruleNode.ruleNodeId?.id"
[ruleChainId]="ruleChainId"
[ruleChainType]="ruleChainType"
[nodeDefinition]="ruleNode.component.configurationDescriptor.nodeDefinition">
[nodeDefinition]="ruleNode.component.configurationDescriptor.nodeDefinition"
(initRuleNode)="initRuleNode.emit($event)"
(changeScript)="changeScript.emit($event)">
</tb-rule-node-config>
<div formGroupName="additionalInfo" fxLayout="column" class="description-block">
<mat-form-field class="mat-block">

8
ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { Component, Input, OnChanges, OnInit, SimpleChanges, ViewChild } from '@angular/core';
import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@ -55,6 +55,12 @@ export class RuleNodeDetailsComponent extends PageComponent implements OnInit, O
@Input()
isAdd = false;
@Output()
initRuleNode = new EventEmitter<void>();
@Output()
changeScript = new EventEmitter<void>();
ruleNodeType = RuleNodeType;
entityType = EntityType;

9
ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html

@ -111,7 +111,9 @@
[ruleChainId]="ruleChain.id?.id"
[ruleChainType]="ruleChainType"
[isEdit]="true"
[isReadOnly]="false">
[isReadOnly]="false"
(initRuleNode)="onRuleNodeInit()"
(changeScript)="switchToFirstTab()">
</tb-rule-node>
</mat-tab>
<mat-tab *ngIf="editingRuleNode.ruleNodeId" label="{{ 'rulenode.events' | translate }}" #eventsTab="matTab">
@ -119,7 +121,10 @@
[defaultEventType]="debugEventTypes.DEBUG_RULE_NODE"
[active]="eventsTab.isActive"
[tenantId]="ruleChain.tenantId.id"
[entityId]="editingRuleNode.ruleNodeId"></tb-event-table>
[entityId]="editingRuleNode.ruleNodeId"
[functionTestButtonLabel]="ruleNodeTestButtonLabel"
(debugEventSelected)="onDebugEventSelected($event)">
</tb-event-table>
</mat-tab>
<mat-tab label="{{ 'rulenode.help' | translate }}">
<div class="tb-rule-node-help">

24
ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts

@ -87,7 +87,7 @@ import { DialogComponent } from '@shared/components/dialog.component';
import { MatMenuTrigger } from '@angular/material/menu';
import { ItemBufferService, RuleNodeConnection } from '@core/services/item-buffer.service';
import { Hotkey } from 'angular2-hotkeys';
import { DebugEventType, EventType } from '@shared/models/event.models';
import { DebugEventType, DebugRuleNodeEventBody, EventType } from '@shared/models/event.models';
import { MatMiniFabButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service';
import { VersionControlComponent } from '@home/components/vc/version-control.component';
@ -153,6 +153,7 @@ export class RuleChainPageComponent extends PageComponent
editingRuleNodeAllowCustomLabels = false;
editingRuleNodeLinkLabels: {[label: string]: LinkLabel};
editingRuleNodeSourceRuleChainId: string;
ruleNodeTestButtonLabel: string;
@ViewChild('tbRuleNode') ruleNodeComponent: RuleNodeDetailsComponent;
@ViewChild('tbRuleNodeLink') ruleNodeLinkComponent: RuleNodeLinkComponent;
@ -1277,6 +1278,27 @@ export class RuleChainPageComponent extends PageComponent
this.editingRuleNodeLink = deepClone(edge);
}
onDebugEventSelected(debugEventBody: DebugRuleNodeEventBody) {
const ruleNodeConfigComponent = this.ruleNodeComponent.ruleNodeConfigComponent;
const ruleNodeConfigDefinedComponent = ruleNodeConfigComponent.definedConfigComponent;
if (ruleNodeConfigComponent.useDefinedDirective() && ruleNodeConfigDefinedComponent.hasScript && ruleNodeConfigDefinedComponent.testScript) {
ruleNodeConfigDefinedComponent.testScript(debugEventBody);
}
}
onRuleNodeInit() {
const ruleNodeConfigDefinedComponent = this.ruleNodeComponent.ruleNodeConfigComponent.definedConfigComponent;
if (this.ruleNodeComponent.ruleNodeConfigComponent.useDefinedDirective() && ruleNodeConfigDefinedComponent.hasScript) {
this.ruleNodeTestButtonLabel = ruleNodeConfigDefinedComponent.testScriptLabel;
} else {
this.ruleNodeTestButtonLabel = '';
}
}
switchToFirstTab() {
this.selectedRuleNodeTabIndex = 0;
}
saveRuleNode() {
this.ruleNodeComponent.validate();
if (this.ruleNodeComponent.ruleNodeFormGroup.valid) {

13
ui-ngx/src/app/shared/components/color-input.component.html

@ -35,7 +35,14 @@
</mat-error>
</mat-form-field>
<ng-template #boxInput>
<div class="tb-color-preview no-margin box" [ngClass]="{'disabled': disabled}" (click)="!disabled && showColorPicker($event)">
<div class="tb-color-result" [ngStyle]="!disabled ? {background: colorFormGroup.get('color').value} : {}"></div>
</div>
<button type="button"
mat-stroked-button
class="color-box"
[disabled]="disabled"
#matButton
(click)="openColorPickerPopup($event, matButton)">
<div class="tb-color-preview no-margin box" [ngClass]="{'disabled': disabled}">
<div class="tb-color-result" [ngStyle]="!disabled ? {background: colorFormGroup.get('color').value} : {}"></div>
</div>
</button>
</ng-template>

9
ui-ngx/src/app/shared/components/color-input.component.scss

@ -13,6 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import './../../../scss/mixins';
:host {
.mat-mdc-form-field {
width: 100%;
@ -29,4 +32,10 @@
margin: 0;
}
}
button.mat-mdc-button-base.color-box {
width: 40px;
min-width: 40px;
height: 40px;
padding: 7px;
}
}

42
ui-ngx/src/app/shared/components/color-input.component.ts

@ -14,15 +14,24 @@
/// limitations under the License.
///
import { ChangeDetectorRef, Component, forwardRef, Input, OnInit } from '@angular/core';
import { ChangeDetectorRef, Component, forwardRef, Input, OnInit, Renderer2, ViewContainerRef } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import {
ControlValueAccessor,
NG_VALUE_ACCESSOR,
UntypedFormBuilder,
UntypedFormGroup,
Validators
} from '@angular/forms';
import { TranslateService } from '@ngx-translate/core';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { DialogService } from '@core/services/dialog.service';
import { coerceBoolean } from '@shared/decorators/coercion';
import { TbPopoverService } from '@shared/components/popover.service';
import { ColorPickerPanelComponent } from '@shared/components/color-picker/color-picker-panel.component';
import { MatButton } from '@angular/material/button';
@Component({
selector: 'tb-color-input',
@ -100,6 +109,9 @@ export class ColorInputComponent extends PageComponent implements OnInit, Contro
constructor(protected store: Store<AppState>,
private dialogs: DialogService,
private translate: TranslateService,
private popoverService: TbPopoverService,
private renderer: Renderer2,
private viewContainerRef: ViewContainerRef,
private fb: UntypedFormBuilder,
private cd: ChangeDetectorRef) {
super(store);
@ -167,6 +179,32 @@ export class ColorInputComponent extends PageComponent implements OnInit, Contro
);
}
openColorPickerPopup($event: Event, matButton: MatButton) {
if ($event) {
$event.stopPropagation();
}
const trigger = matButton._elementRef.nativeElement;
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
} else {
const colorPickerPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, ColorPickerPanelComponent, 'left', true, null,
{
color: this.colorFormGroup.get('color').value
},
{},
{}, {}, true);
colorPickerPopover.tbComponentRef.instance.popover = colorPickerPopover;
colorPickerPopover.tbComponentRef.instance.colorSelected.subscribe((color) => {
colorPickerPopover.hide();
this.colorFormGroup.patchValue(
{color}, {emitEvent: true}
);
this.cd.markForCheck();
});
}
}
clear() {
this.colorFormGroup.get('color').patchValue(null, {emitEvent: true});
this.cd.markForCheck();

30
ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.html

@ -0,0 +1,30 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-color-picker-panel">
<div class="tb-color-picker-title" translate>color.color</div>
<tb-color-picker [formControl]="colorPickerControl"></tb-color-picker>
<div class="tb-color-picker-panel-buttons">
<button mat-raised-button
color="primary"
type="button"
(click)="selectColor()"
[disabled]="colorPickerControl.invalid || !colorPickerControl.dirty">
{{ 'action.select' | translate }}
</button>
</div>
</div>

36
ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.scss

@ -0,0 +1,36 @@
/**
* 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.
*/
.tb-color-picker-panel {
width: 328px;
display: flex;
flex-direction: column;
gap: 16px;
.tb-color-picker-title {
font-size: 16px;
font-weight: 500;
line-height: 24px;
letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.87);
}
.tb-color-picker-panel-buttons {
height: 60px;
display: flex;
flex-direction: row;
gap: 16px;
justify-content: flex-end;
align-items: flex-end;
}
}

55
ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.ts

@ -0,0 +1,55 @@
///
/// 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 { PageComponent } from '@shared/components/page.component';
import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { UntypedFormControl } from '@angular/forms';
import { TbPopoverComponent } from '@shared/components/popover.component';
@Component({
selector: 'tb-color-picker-panel',
templateUrl: './color-picker-panel.component.html',
providers: [],
styleUrls: ['./color-picker-panel.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class ColorPickerPanelComponent extends PageComponent implements OnInit {
@Input()
color: string;
@Input()
popover: TbPopoverComponent<ColorPickerPanelComponent>;
@Output()
colorSelected = new EventEmitter<string>();
colorPickerControl: UntypedFormControl;
constructor(protected store: Store<AppState>) {
super(store);
}
ngOnInit(): void {
this.colorPickerControl = new UntypedFormControl(this.color);
}
selectColor() {
this.colorSelected.emit(this.colorPickerControl.value);
}
}

14
ui-ngx/src/app/shared/components/color-picker/color-picker.component.html

@ -19,7 +19,7 @@
<div class="control-component">
<indicator-component class="indicator-component"
[colorType]="presentations[selectedPresentation]"
[colorType]="presentations[presentationControl.value]"
[color]="control.value">
</indicator-component>
<div class="hue-alpha-range">
@ -29,7 +29,12 @@
</div>
<div class="color-input-block">
<div class="color-input" [ngSwitch]="presentations[selectedPresentation]">
<mat-select class="presentation-select" [formControl]="presentationControl">
<mat-option [value]="0">HEX</mat-option>
<mat-option [value]="1">RGBA</mat-option>
<mat-option [value]="2">HSLA</mat-option>
</mat-select>
<div class="color-input" [ngSwitch]="presentations[presentationControl.value]">
<rgba-input-component *ngSwitchCase="'rgba'" label
[(color)]="control.value" [(hue)]="control.hue"></rgba-input-component>
<hsla-input-component *ngSwitchCase="'hsla'" label
@ -37,5 +42,8 @@
<hex-input-component *ngSwitchCase="'hex'" label prefix="#" [(color)]="control.value"
[(hue)]="control.hue"></hex-input-component>
</div>
<div class="type-btn" (click)="changePresentation()"></div>
</div>
<div class="color-presets-block">
<color-presets-component class="color-presets-component" columns="11" [colorPresets]="colorPresets" [(color)]="control.value" [(hue)]="control.hue"></color-presets-component>
</div>

39
ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss

@ -17,12 +17,10 @@
width: 100%;
display: flex;
flex-direction: column;
gap: 16px;
gap: 32px;
.saturation-component {
height: 100%;
min-height: 200px;
max-height: 300px;
height: 238px;
border-radius: 8px;
}
@ -55,6 +53,12 @@
.color-input-block {
display: flex;
gap: 20px;
.presentation-select {
font-size: 14px;
width: 56px;
}
.color-input {
flex: 1;
@ -63,16 +67,13 @@
color: initial;
}
}
}
.type-btn {
height: 26px;
width: 20px;
background: transparent url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAAgCAMAAAAootjDAAAAM1BMVEUAAAAzMzMzMzMzMzMzMzM0NDQzMzMzMzM0NDQzMzMzMzM0NDQzMzMzMzMyMjIrKyszMzPF8UZlAAAAEHRSTlMA1fHr4ZxxSRP45sG+sCkGH2+Z6QAAAHJJREFUKM+9kkkSgCAQA0FEVLb5/2tViqgQvNrHviSzKGCt6nDGuNass8i8NsrLiX+bZbrUtDwm7VLYE0zWUtEZ+RvUZpEvN8YhH9QmQRoC8kFpEnVHVP/DJUZVeSAem5fDKxwtms/BR+PT8gN8vwk/0wE1gQzNVYryIwAAAABJRU5ErkJggg==') no-repeat center;
background-size: 6px 12px;
&:hover {
background-color: #eee;
}
.color-presets-block {
.color-presets-component {
display: flex;
flex-direction: column;
gap: 12px;
}
}
}
@ -105,4 +106,16 @@
}
}
}
.color-presets-component {
.presets-row {
gap: 10px;
justify-content: space-between;
}
color-preset {
height: 20px;
width: 20px;
border-radius: 4px;
}
}
}

27
ui-ngx/src/app/shared/components/color-picker/color-picker.component.ts

@ -17,7 +17,7 @@
import { Component, forwardRef, OnDestroy } from '@angular/core';
import { Color, ColorPickerControl } from '@iplab/ngx-color-picker';
import { Subscription } from 'rxjs';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormControl } from '@angular/forms';
export enum ColorType {
hex = 'hex',
@ -29,6 +29,10 @@ export enum ColorType {
cmyk = 'cmyk'
}
const colorPresetsHex =
['#435B63', '#F44336', '#E89623', '#F5DD00', '#8BC34A', '#4CAF50', '#009688', '#048AD3', '#673AB7', '#9C27B0', '#E91E63',
'#A1ADB1', '#F9A19B', '#FFD190', '#FFF59D', '#C5E1A4', '#A5D7A7', '#80CBC3', '#81C4E9', '#B39CDB', '#CD93D7', '#F48FB1'];
@Component({
selector: `tb-color-picker`,
templateUrl: `./color-picker.component.html`,
@ -43,10 +47,13 @@ export enum ColorType {
})
export class ColorPickerComponent implements ControlValueAccessor, OnDestroy {
selectedPresentation = 0;
presentations = [ColorType.hex, ColorType.rgba, ColorType.hsla];
control = new ColorPickerControl();
presentationControl = new UntypedFormControl(0);
colorPresets: Color[] = colorPresetsHex.map(c => Color.from(c));
private modelValue: string;
private subscriptions: Array<Subscription> = [];
@ -65,6 +72,11 @@ export class ColorPickerComponent implements ControlValueAccessor, OnDestroy {
}
})
);
this.subscriptions.push(
this.presentationControl.valueChanges.subscribe(() => {
this.updateModel();
})
);
}
registerOnChange(fn: any): void {
@ -86,12 +98,11 @@ export class ColorPickerComponent implements ControlValueAccessor, OnDestroy {
} else if (this.control.initType === ColorType.hsl) {
this.control.initType = ColorType.hsla;
}
this.selectedPresentation = this.presentations.indexOf(this.control.initType);
this.presentationControl.patchValue(this.presentations.indexOf(this.control.initType), {emitEvent: false});
}
private updateModel() {
const color: string = this.getValueByType(this.control.value, this.presentations[this.selectedPresentation]);
const color: string = this.getValueByType(this.control.value, this.presentations[this.presentationControl.value]);
if (this.modelValue !== color) {
this.modelValue = color;
this.propagateChange(color);
@ -103,12 +114,6 @@ export class ColorPickerComponent implements ControlValueAccessor, OnDestroy {
this.subscriptions.length = 0;
}
public changePresentation(): void {
this.selectedPresentation =
this.selectedPresentation === this.presentations.length - 1 ? 0 : this.selectedPresentation + 1;
this.updateModel();
}
getValueByType(color: Color, type: ColorType): string {
switch (type) {
case ColorType.hex:

30
ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.html

@ -15,22 +15,14 @@
limitations under the License.
-->
<form [formGroup]="colorPickerFormGroup" (ngSubmit)="select()" style="width: 320px">
<div mat-dialog-content style="padding: 16px; display: flex">
<tb-color-picker formControlName="color"></tb-color-picker>
</div>
<div mat-dialog-actions fxLayout="row">
<span fxFlex></span>
<button mat-button
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-button
type="submit"
[disabled]="(isLoading$ | async) || colorPickerFormGroup.invalid || !colorPickerFormGroup.dirty">
{{ 'action.select' | translate }}
</button>
</div>
</form>
<div mat-dialog-content>
<button class="tb-close-button"
mat-icon-button
(click)="cancel()"
type="button">
<mat-icon>close</mat-icon>
</button>
<tb-color-picker-panel [color]="color"
(colorSelected)="selectColor($event)">
</tb-color-picker-panel>
</div>

22
ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.scss

@ -0,0 +1,22 @@
/**
* 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 {
.tb-close-button {
position: absolute;
top: 6px;
right: 6px;
}
}

24
ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.ts

@ -14,11 +14,10 @@
/// limitations under the License.
///
import { Component, Inject, OnInit } from '@angular/core';
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { DialogComponent } from '@shared/components/dialog.component';
@ -29,33 +28,26 @@ export interface ColorPickerDialogData {
@Component({
selector: 'tb-color-picker-dialog',
templateUrl: './color-picker-dialog.component.html',
styleUrls: []
styleUrls: ['./color-picker-dialog.component.scss']
})
export class ColorPickerDialogComponent extends DialogComponent<ColorPickerDialogComponent, string>
implements OnInit {
export class ColorPickerDialogComponent extends DialogComponent<ColorPickerDialogComponent, string> {
colorPickerFormGroup: FormGroup;
color: string;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: ColorPickerDialogData,
public dialogRef: MatDialogRef<ColorPickerDialogComponent, string>,
public fb: FormBuilder) {
public dialogRef: MatDialogRef<ColorPickerDialogComponent, string>) {
super(store, router, dialogRef);
this.color = data.color;
}
ngOnInit(): void {
this.colorPickerFormGroup = this.fb.group({
color: [this.data.color, [Validators.required]]
});
selectColor(color: string) {
this.dialogRef.close(color);
}
cancel(): void {
this.dialogRef.close(null);
}
select(): void {
const color: string = this.colorPickerFormGroup.get('color').value;
this.dialogRef.close(color);
}
}

71
ui-ngx/src/app/shared/components/dialog/material-icons-dialog.component.html

@ -15,63 +15,14 @@
limitations under the License.
-->
<form class="tb-material-icons-dialog" style="min-width: 600px;">
<mat-toolbar fxLayout="row" color="primary">
<h2>{{ 'icon.select-icon' | translate }}</h2>
<span fxFlex></span>
<section fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-slide-toggle [formControl]="showAllControl">
</mat-slide-toggle>
<label translate>icon.show-all</label>
</section>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div class="tb-absolute-fill tb-icons-load" *ngIf="loadingIcons$ | async" fxLayout="column" fxLayoutAlign="center center">
<mat-spinner color="accent" mode="indeterminate" diameter="40"></mat-spinner>
</div>
<div mat-dialog-content>
<div class="mat-content mat-padding" fxLayout="column">
<fieldset [disabled]="(isLoading$ | async)">
<ng-template ngFor let-icon [ngForOf]="icons$ | async" let-last="last">
<ng-container #iconButtons>
<button *ngIf="icon === selectedIcon"
class="tb-select-icon-button"
mat-raised-button
color="primary"
(click)="selectIcon(icon)"
matTooltip="{{ icon }}"
matTooltipPosition="above"
type="button">
<mat-icon>{{icon}}</mat-icon>
</button>
<button *ngIf="icon !== selectedIcon"
class="tb-select-icon-button"
mat-button
(click)="selectIcon(icon)"
matTooltip="{{ icon }}"
matTooltipPosition="above"
type="button">
<mat-icon>{{icon}}</mat-icon>
</button>
</ng-container>
</ng-template>
</fieldset>
</div>
</div>
<div mat-dialog-actions fxLayout="row">
<span fxFlex></span>
<button mat-button
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
</div>
</form>
<div mat-dialog-content>
<button class="tb-close-button"
mat-icon-button
(click)="cancel()"
type="button">
<mat-icon>close</mat-icon>
</button>
<tb-material-icons [selectedIcon]="selectedIcon"
(iconSelected)="selectIcon($event)">
</tb-material-icons>
</div>

35
ui-ngx/src/app/shared/components/dialog/material-icons-dialog.component.scss

@ -14,36 +14,9 @@
* limitations under the License.
*/
:host {
.tb-material-icons-dialog {
position: relative;
}
.tb-icons-load {
top: 64px;
z-index: 3;
background: rgba(255, 255, 255, .75);
}
}
:host ::ng-deep {
.tb-material-icons-dialog {
button.mat-mdc-button-base.tb-select-icon-button {
width: 56px;
min-width: 56px;
height: 56px;
padding: 16px;
margin: 10px;
border: solid 1px #ffa500;
border-radius: 0;
line-height: 0;
display: inline-block;
vertical-align: baseline;
.mat-icon {
width: 24px;
margin: 0;
height: 24px;
vertical-align: initial;
font-size: 24px;
}
}
.tb-close-button {
position: absolute;
top: 6px;
right: 6px;
}
}

56
ui-ngx/src/app/shared/components/dialog/material-icons-dialog.component.ts

@ -14,16 +14,12 @@
/// limitations under the License.
///
import { AfterViewInit, Component, Inject, OnInit, QueryList, ViewChildren } from '@angular/core';
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { DialogComponent } from '@shared/components/dialog.component';
import { UtilsService } from '@core/services/utils.service';
import { UntypedFormControl } from '@angular/forms';
import { merge, Observable, of } from 'rxjs';
import { delay, map, mapTo, mergeMap, share, startWith, tap } from 'rxjs/operators';
export interface MaterialIconsDialogData {
icon: string;
@ -35,64 +31,16 @@ export interface MaterialIconsDialogData {
providers: [],
styleUrls: ['./material-icons-dialog.component.scss']
})
export class MaterialIconsDialogComponent extends DialogComponent<MaterialIconsDialogComponent, string>
implements OnInit, AfterViewInit {
@ViewChildren('iconButtons') iconButtons: QueryList<HTMLElement>;
export class MaterialIconsDialogComponent extends DialogComponent<MaterialIconsDialogComponent, string> {
selectedIcon: string;
icons$: Observable<Array<string>>;
loadingIcons$: Observable<boolean>;
showAllControl: UntypedFormControl;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: MaterialIconsDialogData,
private utils: UtilsService,
public dialogRef: MatDialogRef<MaterialIconsDialogComponent, string>) {
super(store, router, dialogRef);
this.selectedIcon = data.icon;
this.showAllControl = new UntypedFormControl(false);
}
ngOnInit(): void {
this.icons$ = this.showAllControl.valueChanges.pipe(
map((showAll) => {
return {firstTime: false, showAll};
}),
startWith<{firstTime: boolean, showAll: boolean}>({firstTime: true, showAll: false}),
mergeMap((data) => {
if (data.showAll) {
return this.utils.getMaterialIcons().pipe(delay(100));
} else {
const res = of(this.utils.getCommonMaterialIcons());
return data.firstTime ? res : res.pipe(delay(50));
}
}),
share()
);
}
ngAfterViewInit(): void {
this.loadingIcons$ = merge(
this.showAllControl.valueChanges.pipe(
mapTo(true),
),
this.iconButtons.changes.pipe(
delay(100),
mapTo( false),
)
).pipe(
tap((loadingIcons) => {
if (loadingIcons) {
this.showAllControl.disable({emitEvent: false});
} else {
this.showAllControl.enable({emitEvent: false});
}
}),
share()
);
}
selectIcon(icon: string) {

12
ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts

@ -26,7 +26,7 @@ import {
ViewChild
} from '@angular/core';
import { MatFormFieldAppearance } from '@angular/material/form-field';
import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR } from '@angular/forms';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { merge, Observable, of, Subject } from 'rxjs';
import { catchError, debounceTime, map, share, switchMap, tap } from 'rxjs/operators';
import { Store } from '@ngrx/store';
@ -55,7 +55,7 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit
selectEntityFormGroup: UntypedFormGroup;
modelValue: string | null;
modelValue: string | EntityId | null;
entityTypeValue: EntityType | AliasEntityType;
@ -113,6 +113,10 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit
@Input()
requiredText: string;
@Input()
@coerceBoolean()
useFullEntityId: boolean;
@Input()
appearance: MatFormFieldAppearance = 'fill';
@ -171,7 +175,7 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit
if (typeof value === 'string' || !value) {
modelValue = null;
} else {
modelValue = value.id.id;
modelValue = this.useFullEntityId ? value.id : value.id.id;
}
this.updateView(modelValue, value);
if (value === null) {
@ -307,7 +311,7 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit
} catch (e) {
this.propagateChange(null);
}
this.modelValue = entity !== null ? entity.id.id : null;
this.modelValue = entity !== null ? (this.useFullEntityId ? entity.id : entity.id.id) : null;
this.selectEntityFormGroup.get('entity').patchValue(entity !== null ? entity : '', {emitEvent: false});
this.entityChanged.emit(entity);
} else {

12
ui-ngx/src/app/shared/components/material-icon-select.component.html

@ -29,7 +29,13 @@
</mat-form-field>
</div>
<ng-template #boxInput>
<mat-icon class="icon-box" [ngStyle]="color && !disabled ? { color: color } : {}"
[ngClass]="{'disabled': disabled}"
(click)="openIconDialog()">{{materialIconFormGroup.get('icon').value}}</mat-icon>
<button type="button"
mat-stroked-button
class="icon-box"
[ngStyle]="color && !disabled ? { color: color } : {}"
[disabled]="disabled"
#matButton
(click)="openIconPopup($event, matButton)">
<mat-icon>{{materialIconFormGroup.get('icon').value}}</mat-icon>
</button>
</ng-template>

31
ui-ngx/src/app/shared/components/material-icon-select.component.scss

@ -22,20 +22,23 @@
border: solid 1px rgba(0, 0, 0, .27);
box-sizing: initial;
}
&.icon-box {
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 4px;
cursor: pointer;
box-sizing: border-box;
padding: 8px;
height: 40px;
width: 40px;
font-size: 22px;
vertical-align: middle;
&.disabled {
cursor: initial;
color: rgba(0, 0, 0, 0.38);
}
}
}
:host ::ng-deep {
button.mat-mdc-button-base.icon-box {
width: 40px;
min-width: 40px;
height: 40px;
padding: 7px;
&:not(:disabled) {
color: rgba(0, 0, 0, 0.87);
}
> .mat-icon {
width: 24px;
height: 24px;
font-size: 24px;
margin: 0;
}
}
}

36
ui-ngx/src/app/shared/components/material-icon-select.component.ts

@ -14,15 +14,18 @@
/// limitations under the License.
///
import { ChangeDetectorRef, Component, forwardRef, Input, OnInit } from '@angular/core';
import { ChangeDetectorRef, Component, forwardRef, Input, OnInit, Renderer2, ViewContainerRef } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR } from '@angular/forms';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { DialogService } from '@core/services/dialog.service';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { TranslateService } from '@ngx-translate/core';
import { coerceBoolean } from '@shared/decorators/coercion';
import { TbPopoverService } from '@shared/components/popover.service';
import { MaterialIconsComponent } from '@shared/components/material-icons.component';
import { MatButton } from '@angular/material/button';
@Component({
selector: 'tb-material-icon-select',
@ -81,6 +84,9 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit
constructor(protected store: Store<AppState>,
private dialogs: DialogService,
private translate: TranslateService,
private popoverService: TbPopoverService,
private renderer: Renderer2,
private viewContainerRef: ViewContainerRef,
private fb: UntypedFormBuilder,
private cd: ChangeDetectorRef) {
super(store);
@ -142,6 +148,32 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit
}
}
openIconPopup($event: Event, matButton: MatButton) {
if ($event) {
$event.stopPropagation();
}
const trigger = matButton._elementRef.nativeElement;
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
} else {
const materialIconsPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, MaterialIconsComponent, 'left', true, null,
{
selectedIcon: this.materialIconFormGroup.get('icon').value
},
{},
{}, {}, true);
materialIconsPopover.tbComponentRef.instance.popover = materialIconsPopover;
materialIconsPopover.tbComponentRef.instance.iconSelected.subscribe((icon) => {
materialIconsPopover.hide();
this.materialIconFormGroup.patchValue(
{icon}, {emitEvent: true}
);
this.cd.markForCheck();
});
}
}
clear() {
this.materialIconFormGroup.get('icon').patchValue(null, {emitEvent: true});
this.cd.markForCheck();

65
ui-ngx/src/app/shared/components/material-icons.component.html

@ -0,0 +1,65 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-material-icons-panel">
<div class="tb-material-icons-title" translate>icon.icons</div>
<mat-form-field class="tb-material-icons-search tb-inline-field" appearance="outline" subscriptSizing="dynamic">
<mat-icon matPrefix>search</mat-icon>
<input matInput [formControl]="searchIconControl" placeholder="{{ 'icon.search-icon' | translate }}"/>
<button *ngIf="searchIconControl.value"
type="button"
matSuffix mat-icon-button aria-label="Clear"
(click)="clearSearch()">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-form-field>
<cdk-virtual-scroll-viewport [fxShow]="!notFound" #iconsPanel
[itemSize]="iconsRowHeight" class="tb-material-icons-viewport" [ngStyle]="{width: iconsPanelWidth, height: iconsPanelHeight}">
<div *cdkVirtualFor="let iconRow of iconRows$ | async" class="tb-material-icons-row">
<ng-container *ngFor="let icon of iconRow">
<button *ngIf="icon.name === selectedIcon"
class="tb-select-icon-button"
mat-raised-button
color="primary"
(click)="selectIcon(icon)"
matTooltip="{{ icon.displayName }}"
matTooltipPosition="above"
type="button">
<mat-icon>{{icon.name}}</mat-icon>
</button>
<button *ngIf="icon.name !== selectedIcon"
class="tb-select-icon-button"
mat-button
(click)="selectIcon(icon)"
matTooltip="{{ icon.displayName }}"
matTooltipPosition="above"
type="button">
<mat-icon>{{icon.name}}</mat-icon>
</button>
</ng-container>
</div>
</cdk-virtual-scroll-viewport>
<button *ngIf="!showAllSubject.value" class="tb-material-icons-show-more" mat-button color="primary" (click)="showAllSubject.next(true)">
{{ 'action.show-more' | translate }}
</button>
<ng-container *ngIf="notFound">
<div class="tb-no-data-available" [ngStyle]="{width: iconsPanelWidth}">
<div class="tb-no-data-bg"></div>
<div class="tb-no-data-text">{{ 'icon.no-icons-found' | translate:{iconSearch: searchIconControl.value} }}</div>
</div>
</ng-container>
</div>

61
ui-ngx/src/app/shared/components/material-icons.component.scss

@ -0,0 +1,61 @@
/**
* 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.
*/
.tb-material-icons-panel {
width: 100%;
display: flex;
flex-direction: column;
gap: 16px;
align-items: center;
.tb-material-icons-title {
font-size: 16px;
font-weight: 500;
line-height: 24px;
letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.87);
}
.tb-material-icons-title, .tb-material-icons-search, .tb-material-icons-show-more {
width: 100%;
}
.tb-material-icons-viewport {
min-height: 144px;
}
.tb-material-icons-row {
display: flex;
flex-direction: row;
gap: 12px;
}
.tb-material-icons-row + .tb-material-icons-row {
margin-top: 12px;
}
.tb-no-data-available {
min-height: 144px;
}
button.mat-mdc-button-base.tb-select-icon-button {
width: 36px;
min-width: 36px;
height: 36px;
padding: 6px;
&:not(.mat-primary) {
color: rgba(0, 0, 0, 0.54);
}
> .mat-icon {
width: 24px;
height: 24px;
font-size: 24px;
margin: 0;
}
}
}

135
ui-ngx/src/app/shared/components/material-icons.component.ts

@ -0,0 +1,135 @@
///
/// 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 { PageComponent } from '@shared/components/page.component';
import {
ChangeDetectorRef,
Component,
EventEmitter,
Input,
OnInit,
Output,
ViewChild,
ViewEncapsulation
} from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { UntypedFormControl } from '@angular/forms';
import { BehaviorSubject, combineLatest, debounce, Observable, of, timer } from 'rxjs';
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
import { getMaterialIcons, MaterialIcon } from '@shared/models/icon.models';
import { distinctUntilChanged, map, mergeMap, share, startWith, tap } from 'rxjs/operators';
import { ResourcesService } from '@core/services/resources.service';
import { TbPopoverComponent } from '@shared/components/popover.component';
import { BreakpointObserver } from '@angular/cdk/layout';
import { MediaBreakpoints } from '@shared/models/constants';
@Component({
selector: 'tb-material-icons',
templateUrl: './material-icons.component.html',
providers: [],
styleUrls: ['./material-icons.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class MaterialIconsComponent extends PageComponent implements OnInit {
@ViewChild('iconsPanel')
iconsPanel: CdkVirtualScrollViewport;
@Input()
selectedIcon: string;
@Input()
popover: TbPopoverComponent<MaterialIconsComponent>;
@Output()
iconSelected = new EventEmitter<string>();
iconRows$: Observable<MaterialIcon[][]>;
showAllSubject = new BehaviorSubject<boolean>(false);
searchIconControl: UntypedFormControl;
iconsRowHeight = 48;
iconsPanelHeight: string;
iconsPanelWidth: string;
notFound = false;
constructor(protected store: Store<AppState>,
private resourcesService: ResourcesService,
private breakpointObserver: BreakpointObserver,
private cd: ChangeDetectorRef) {
super(store);
this.searchIconControl = new UntypedFormControl('');
}
ngOnInit(): void {
const iconsRowSize = this.breakpointObserver.isMatched(MediaBreakpoints['lt-md']) ? 8 : 11;
this.calculatePanelSize(iconsRowSize);
const iconsRowSizeObservable = this.breakpointObserver
.observe(MediaBreakpoints['lt-md']).pipe(
map((state) => state.matches ? 8 : 11),
startWith(iconsRowSize),
);
this.iconRows$ = combineLatest({showAll: this.showAllSubject.asObservable(),
rowSize: iconsRowSizeObservable,
searchText: this.searchIconControl.valueChanges.pipe(
startWith(''),
debounce((searchText) => searchText ? timer(150) : of({})),
)}).pipe(
map((data) => {
if (data.searchText && !data.showAll) {
data.showAll = true;
this.showAllSubject.next(true);
}
return data;
}),
distinctUntilChanged((p, c) => c.showAll === p.showAll && c.searchText === p.searchText && c.rowSize === p.rowSize),
mergeMap((data) => getMaterialIcons(this.resourcesService, data.rowSize, data.showAll, data.searchText).pipe(
map(iconRows => ({iconRows, iconsRowSize: data.rowSize}))
)),
tap((data) => {
this.notFound = !data.iconRows.length;
this.calculatePanelSize(data.iconsRowSize, data.iconRows.length);
this.cd.markForCheck();
setTimeout(() => {
this.checkSize();
}, 0);
}),
map((data) => data.iconRows),
share()
);
}
clearSearch() {
this.searchIconControl.patchValue('', {emitEvent: true});
}
selectIcon(icon: MaterialIcon) {
this.iconSelected.emit(icon.name);
}
private calculatePanelSize(iconsRowSize: number, iconRows = 4) {
this.iconsPanelHeight = Math.min(iconRows * this.iconsRowHeight, 10 * this.iconsRowHeight) + 'px';
this.iconsPanelWidth = (iconsRowSize * 36 + (iconsRowSize - 1) * 12 + 6) + 'px';
}
private checkSize() {
this.iconsPanel?.checkViewportSize();
this.popover?.updatePosition();
}
}

20
ui-ngx/src/app/shared/components/popover.component.ts

@ -63,8 +63,10 @@ import { coerceBoolean } from '@shared/decorators/coercion';
export type TbPopoverTrigger = 'click' | 'focus' | 'hover' | null;
@Directive({
// eslint-disable-next-line @angular-eslint/directive-selector
selector: '[tb-popover]',
exportAs: 'tbPopover',
// eslint-disable-next-line @angular-eslint/no-host-metadata-property
host: {
'[class.tb-popover-open]': 'visible'
}
@ -265,12 +267,20 @@ export class TbPopoverDirective implements OnChanges, OnDestroy, AfterViewInit {
} else if (delay > 0) {
this.delayTimer = setTimeout(() => {
this.delayTimer = undefined;
isEnter ? this.show() : this.hide();
if (isEnter) {
this.show();
} else {
this.hide();
}
}, delay * 1000);
} else {
// `isOrigin` is used due to the tooltip will not hide immediately
// (may caused by the fade-out animation).
isEnter && isOrigin ? this.show() : this.hide();
if (isEnter && isOrigin) {
this.show();
} else {
this.hide();
}
}
}
@ -345,15 +355,15 @@ export class TbPopoverDirective implements OnChanges, OnDestroy, AfterViewInit {
</ng-template>
`
})
export class TbPopoverComponent implements OnDestroy, OnInit {
export class TbPopoverComponent<T = any> implements OnDestroy, OnInit {
@ViewChild('overlay', { static: false }) overlay!: CdkConnectedOverlay;
@ViewChild('popoverRoot', { static: false }) popoverRoot!: ElementRef<HTMLElement>;
@ViewChild('popover', { static: false }) popover!: ElementRef<HTMLElement>;
tbContent: string | TemplateRef<void> | null = null;
tbComponentFactory: ComponentFactory<any> | null = null;
tbComponentRef: ComponentRef<any> | null = null;
tbComponentFactory: ComponentFactory<T> | null = null;
tbComponentRef: ComponentRef<T> | null = null;
tbComponentContext: any;
tbComponentInjector: Injector | null = null;
tbComponentStyle: { [klass: string]: any } = {};

4
ui-ngx/src/app/shared/components/popover.service.ts

@ -65,7 +65,7 @@ export class TbPopoverService {
displayPopover<T>(trigger: Element, renderer: Renderer2, hostView: ViewContainerRef,
componentType: Type<T>, preferredPlacement: PopoverPlacement = 'top', hideOnClickOutside = true,
injector?: Injector, context?: any, overlayStyle: any = {}, popoverStyle: any = {}, style?: any,
showCloseButton = true): TbPopoverComponent {
showCloseButton = true): TbPopoverComponent<T> {
const componentRef = this.createPopoverRef(hostView);
return this.displayPopoverWithComponentRef(componentRef, trigger, renderer, componentType, preferredPlacement, hideOnClickOutside,
injector, context, overlayStyle, popoverStyle, style, showCloseButton);
@ -74,7 +74,7 @@ export class TbPopoverService {
displayPopoverWithComponentRef<T>(componentRef: ComponentRef<TbPopoverComponent>, trigger: Element, renderer: Renderer2,
componentType: Type<T>, preferredPlacement: PopoverPlacement = 'top',
hideOnClickOutside = true, injector?: Injector, context?: any, overlayStyle: any = {},
popoverStyle: any = {}, style?: any, showCloseButton = true): TbPopoverComponent {
popoverStyle: any = {}, style?: any, showCloseButton = true): TbPopoverComponent<T> {
const component = componentRef.instance;
this.popoverWithTriggers.push({
trigger,

1
ui-ngx/src/app/shared/components/public-api.ts

@ -26,3 +26,4 @@ export * from './resource/resource-autocomplete.component';
export * from './toggle-header.component';
export * from './toggle-select.component';
export * from './unit-input.component';
export * from './material-icons.component';

3
ui-ngx/src/app/shared/components/unit-input.component.html

@ -15,13 +15,14 @@
limitations under the License.
-->
<mat-form-field appearance="outline" class="tb-inline-field" subscriptSizing="dynamic">
<mat-form-field appearance="outline" class="tb-inline-field tb-suffix-show-on-hover" subscriptSizing="dynamic">
<input matInput #unitInput [formControl]="unitsFormControl"
placeholder="{{ 'widget-config.set' | translate }}"
(focusin)="onFocus()"
[matAutocomplete]="unitsAutocomplete">
<button *ngIf="unitsFormControl.value && !disabled"
type="button"
class="tb-icon-24"
matSuffix mat-icon-button aria-label="Clear"
(click)="clear()">
<mat-icon class="material-icons">close</mat-icon>

72
ui-ngx/src/app/shared/models/icon.models.ts

@ -0,0 +1,72 @@
///
/// 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 { ResourcesService } from '@core/services/resources.service';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { isNotEmptyStr } from '@core/utils';
export interface MaterialIcon {
name: string;
displayName?: string;
tags: string[];
}
export const iconByName = (icons: Array<MaterialIcon>, name: string): MaterialIcon => icons.find(i => i.name === name);
const searchIconTags = (icon: MaterialIcon, searchText: string): boolean =>
!!icon.tags.find(t => t.toUpperCase().includes(searchText.toUpperCase()));
const searchIcons = (_icons: Array<MaterialIcon>, searchText: string): Array<MaterialIcon> => _icons.filter(
i => i.name.toUpperCase().includes(searchText.toUpperCase()) ||
i.displayName.toUpperCase().includes(searchText.toUpperCase()) ||
searchIconTags(i, searchText)
);
const getCommonMaterialIcons = (icons: Array<MaterialIcon>, chunkSize: number): Array<MaterialIcon> => icons.slice(0, chunkSize * 4);
export const getMaterialIcons = (resourcesService: ResourcesService, chunkSize = 11,
all = false, searchText: string): Observable<MaterialIcon[][]> =>
resourcesService.loadJsonResource<Array<MaterialIcon>>('/assets/metadata/material-icons.json',
(icons) => {
for (const icon of icons) {
const words = icon.name.replace(/_/g, ' ').split(' ');
for (let i = 0; i < words.length; i++) {
words[i] = words[i].charAt(0).toUpperCase() + words[i].slice(1);
}
icon.displayName = words.join(' ');
}
return icons;
}
).pipe(
map((icons) => {
if (isNotEmptyStr(searchText)) {
return searchIcons(icons, searchText);
} else if (!all) {
return getCommonMaterialIcons(icons, chunkSize);
} else {
return icons;
}
}),
map((icons) => {
const iconChunks: MaterialIcon[][] = [];
for (let i = 0; i < icons.length; i += chunkSize) {
const chunk = icons.slice(i, i + chunkSize);
iconChunks.push(chunk);
}
return iconChunks;
})
);

9
ui-ngx/src/app/shared/models/rule-node.models.ts

@ -26,6 +26,8 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { AbstractControl, UntypedFormGroup } from '@angular/forms';
import { RuleChainType } from '@shared/models/rule-chain.models';
import { DebugRuleNodeEventBody } from '@shared/models/event.models';
import { TranslateService } from '@ngx-translate/core';
export interface RuleNodeConfiguration {
[key: string]: any;
@ -71,10 +73,14 @@ export interface RuleNodeConfigurationDescriptor {
export interface IRuleNodeConfigurationComponent {
ruleNodeId: string;
ruleChainId: string;
hasScript: boolean;
testScriptLabel?: string;
changeScript?: EventEmitter<void>;
ruleChainType: RuleChainType;
configuration: RuleNodeConfiguration;
configurationChanged: Observable<RuleNodeConfiguration>;
validate();
testScript? (debugEventBody?: DebugRuleNodeEventBody);
[key: string]: any;
}
@ -87,6 +93,8 @@ export abstract class RuleNodeConfigurationComponent extends PageComponent imple
ruleChainId: string;
hasScript: boolean = false;
ruleChainType: RuleChainType;
configurationValue: RuleNodeConfiguration;
@ -499,3 +507,4 @@ export function getRuleNodeHelpLink(component: RuleNodeComponentDescriptor): str
}
return 'ruleEngine';
}

6
ui-ngx/src/app/shared/shared.module.ts

@ -195,6 +195,8 @@ import { ToggleHeaderComponent, ToggleOption } from '@shared/components/toggle-h
import { RuleChainSelectComponent } from '@shared/components/rule-chain/rule-chain-select.component';
import { ToggleSelectComponent } from '@shared/components/toggle-select.component';
import { UnitInputComponent } from '@shared/components/unit-input.component';
import { MaterialIconsComponent } from '@shared/components/material-icons.component';
import { ColorPickerPanelComponent } from '@shared/components/color-picker/color-picker-panel.component';
export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) {
return markedOptionsService;
@ -364,11 +366,13 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
GtMdLgLayoutGapDirective,
GtMdLgShowHideDirective,
ColorPickerComponent,
ColorPickerPanelComponent,
ResourceAutocompleteComponent,
ToggleHeaderComponent,
ToggleOption,
ToggleSelectComponent,
UnitInputComponent,
MaterialIconsComponent,
RuleChainSelectComponent
],
imports: [
@ -595,11 +599,13 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
GtMdLgLayoutGapDirective,
GtMdLgShowHideDirective,
ColorPickerComponent,
ColorPickerPanelComponent,
ResourceAutocompleteComponent,
ToggleHeaderComponent,
ToggleOption,
ToggleSelectComponent,
UnitInputComponent,
MaterialIconsComponent,
RuleChainSelectComponent
]
})

0
ui-ngx/src/app/modules/home/pages/api-usage/api_usage_json.raw → ui-ngx/src/assets/dashboard/api_usage.json

0
ui-ngx/src/app/modules/home/pages/home-links/customer_user_home_page.raw → ui-ngx/src/assets/dashboard/customer_user_home_page.json

0
ui-ngx/src/app/modules/home/pages/home-links/sys_admin_home_page.raw → ui-ngx/src/assets/dashboard/sys_admin_home_page.json

0
ui-ngx/src/app/modules/home/pages/home-links/tenant_admin_home_page.raw → ui-ngx/src/assets/dashboard/tenant_admin_home_page.json

BIN
ui-ngx/src/assets/fonts/MaterialIcons-Regular.ttf

Binary file not shown.

14
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -68,7 +68,8 @@
"less": "Less",
"skip": "Skip",
"send": "Send",
"reset": "Reset"
"reset": "Reset",
"show-more": "Show more"
},
"aggregation": {
"aggregation": "Aggregation",
@ -3466,7 +3467,8 @@
"output": "Output",
"test": "Test",
"help": "Help",
"reset-debug-mode": "Reset debug mode in all nodes"
"reset-debug-mode": "Reset debug mode in all nodes",
"test-with-this-message": "{{test}} with this message"
},
"timezone": {
"timezone": "Timezone",
@ -5499,11 +5501,17 @@
}
}
},
"color": {
"color": "Color"
},
"icon": {
"icon": "Icon",
"icons": "Icons",
"select-icon": "Select icon",
"material-icons": "Material icons",
"show-all": "Show all icons"
"show-all": "Show all icons",
"search-icon": "Search icon",
"no-icons-found": "No icons found for '{{iconSearch}}'"
},
"phone-input": {
"phone-input-label": "Phone number",

6367
ui-ngx/src/assets/metadata/material-icons.json

File diff suppressed because it is too large

140
ui-ngx/src/form.scss

@ -13,6 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import './scss/constants';
.tb-default, .tb-dark {
.tb-form-panel {
box-shadow: 0 0 10px 6px rgba(11, 17, 51, 0.04);
@ -128,14 +131,11 @@
}
.tb-form-row {
height: 100%;
padding-top: 7px;
padding-bottom: 7px;
display: flex;
flex-direction: row;
align-items: center;
gap: 16px;
padding-left: 16px;
padding-right: 12px;
padding: 7px 7px 7px 16px;
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 6px;
&.same-padding {
@ -177,6 +177,13 @@
opacity: 0;
}
}
&:not(.mat-mdc-form-field-has-icon-prefix) {
.mat-mdc-text-field-wrapper {
&.mdc-text-field--outlined, &:not(.mdc-text-field--outlined) {
padding-left: 12px;
}
}
}
&:not(.mat-mdc-form-field-has-icon-suffix) {
.mat-mdc-text-field-wrapper {
&.mdc-text-field--outlined, &:not(.mdc-text-field--outlined) {
@ -186,7 +193,6 @@
}
.mat-mdc-text-field-wrapper {
&.mdc-text-field--outlined, &:not(.mdc-text-field--outlined) {
padding-left: 12px;
&:not(.mdc-text-field--focused):not(.mdc-text-field--disabled):not(:hover) {
.mdc-notched-outline__leading, .mdc-notched-outline__trailing {
border-color: rgba(0, 0, 0, 0.12);
@ -203,7 +209,7 @@
line-height: 20px;
}
}
.mat-mdc-form-field-icon-suffix {
.mat-mdc-form-field-icon-prefix, .mat-mdc-form-field-icon-suffix {
height: 40px;
font-size: 14px;
line-height: 40px;
@ -218,6 +224,19 @@
height: 20px;
font-size: 20px;
}
.mat-mdc-button-touch-target {
width: 40px;
height: 40px;
}
&.tb-icon-24 {
width: 24px;
height: 24px;
padding: 0;
.mat-mdc-button-touch-target {
width: 24px;
height: 24px;
}
}
}
> .mat-icon {
width: 20px;
@ -268,6 +287,22 @@
}
}
}
&.tb-suffix-show-on-hover {
.mat-mdc-text-field-wrapper {
.mat-mdc-form-field-icon-suffix {
padding: 0;
display: none;
}
}
&:hover {
.mat-mdc-text-field-wrapper {
.mat-mdc-form-field-icon-suffix {
display: flex;
align-items: center;
}
}
}
}
}
.tb-form-table {
@ -305,35 +340,80 @@
.tb-prompt {
height: 38px;
}
}
.tb-form-table-row {
height: 38px;
display: flex;
flex-direction: row;
gap: 12px;
padding-left: 12px;
.tb-form-table-row {
height: 38px;
display: flex;
flex-direction: row;
gap: 12px;
padding-left: 12px;
&.tb-draggable {
gap: 0;
padding-left: 0;
background: #fff;
}
&.tb-draggable {
gap: 0;
padding-left: 0;
background: #fff;
}
&-cell-buttons {
display: flex;
flex-direction: row;
button.mat-mdc-icon-button.mat-mdc-button-base {
padding: 7px;
width: 38px;
height: 38px;
.mat-icon {
color: rgba(0, 0, 0, 0.38);
}
&.tb-hidden {
visibility: hidden;
}
&-cell-buttons {
display: flex;
flex-direction: row;
button.mat-mdc-icon-button.mat-mdc-button-base {
padding: 7px;
width: 38px;
height: 38px;
.mat-icon {
color: rgba(0, 0, 0, 0.38);
}
&.tb-hidden {
visibility: hidden;
}
}
}
}
.tb-no-data-available {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.tb-no-data-bg {
margin: 10px;
position: relative;
flex: 1;
width: 100%;
max-height: 100px;
&:before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: #305680;
-webkit-mask-image: url(/assets/home/no_data_folder_bg.svg);
-webkit-mask-repeat: no-repeat;
-webkit-mask-size: contain;
-webkit-mask-position: center;
mask-image: url(/assets/home/no_data_folder_bg.svg);
mask-repeat: no-repeat;
mask-size: contain;
mask-position: center;
}
}
.tb-no-data-text {
font-weight: 500;
font-size: 14px;
line-height: 20px;
letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.54);
@media #{$mat-md-lg} {
font-size: 12px;
line-height: 16px;
}
}
}

Loading…
Cancel
Save