Browse Source

Merge remote-tracking branch 'upstream/develop/3.5.2' into feature/test-rulenode-script-function-with-selected-event

pull/8836/head
rusikv 3 years ago
parent
commit
71abdcc586
  1. 30
      application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
  2. 6
      ui-ngx/src/app/core/services/dialog.service.ts
  3. 2142
      ui-ngx/src/app/core/services/material-icons-codepoints.raw
  4. 25
      ui-ngx/src/app/core/services/resources.service.ts
  5. 75
      ui-ngx/src/app/core/services/utils.service.ts
  6. 2
      ui-ngx/src/app/modules/common/modules-map.ts
  7. 6
      ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.scss
  8. 27
      ui-ngx/src/app/modules/home/components/widget/config/basic/alarm/alarms-table-basic-config.component.html
  9. 27
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/entities-table-basic-config.component.html
  10. 22
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.html
  11. 27
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/timeseries-table-basic-config.component.html
  12. 27
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/flot-basic-config.component.html
  13. 11
      ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.html
  14. 2
      ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.html
  15. 32
      ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.ts
  16. 40
      ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts
  17. 46
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page-widget.scss
  18. 11
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.html
  19. 11
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-latest-key-settings.component.html
  20. 54
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.html
  21. 27
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.html
  22. 18
      ui-ngx/src/app/modules/home/pages/api-usage/api-usage-routing.module.ts
  23. 16
      ui-ngx/src/app/modules/home/pages/api-usage/api-usage.component.ts
  24. 110
      ui-ngx/src/app/modules/home/pages/home-links/home-links-routing.module.ts
  25. 13
      ui-ngx/src/app/shared/components/color-input.component.html
  26. 9
      ui-ngx/src/app/shared/components/color-input.component.scss
  27. 42
      ui-ngx/src/app/shared/components/color-input.component.ts
  28. 30
      ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.html
  29. 36
      ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.scss
  30. 55
      ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.ts
  31. 14
      ui-ngx/src/app/shared/components/color-picker/color-picker.component.html
  32. 39
      ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss
  33. 27
      ui-ngx/src/app/shared/components/color-picker/color-picker.component.ts
  34. 30
      ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.html
  35. 22
      ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.scss
  36. 24
      ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.ts
  37. 71
      ui-ngx/src/app/shared/components/dialog/material-icons-dialog.component.html
  38. 35
      ui-ngx/src/app/shared/components/dialog/material-icons-dialog.component.scss
  39. 56
      ui-ngx/src/app/shared/components/dialog/material-icons-dialog.component.ts
  40. 12
      ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts
  41. 12
      ui-ngx/src/app/shared/components/material-icon-select.component.html
  42. 31
      ui-ngx/src/app/shared/components/material-icon-select.component.scss
  43. 36
      ui-ngx/src/app/shared/components/material-icon-select.component.ts
  44. 65
      ui-ngx/src/app/shared/components/material-icons.component.html
  45. 61
      ui-ngx/src/app/shared/components/material-icons.component.scss
  46. 135
      ui-ngx/src/app/shared/components/material-icons.component.ts
  47. 20
      ui-ngx/src/app/shared/components/popover.component.ts
  48. 4
      ui-ngx/src/app/shared/components/popover.service.ts
  49. 1
      ui-ngx/src/app/shared/components/public-api.ts
  50. 3
      ui-ngx/src/app/shared/components/unit-input.component.html
  51. 72
      ui-ngx/src/app/shared/models/icon.models.ts
  52. 6
      ui-ngx/src/app/shared/shared.module.ts
  53. 0
      ui-ngx/src/assets/dashboard/api_usage.json
  54. 0
      ui-ngx/src/assets/dashboard/customer_user_home_page.json
  55. 0
      ui-ngx/src/assets/dashboard/sys_admin_home_page.json
  56. 0
      ui-ngx/src/assets/dashboard/tenant_admin_home_page.json
  57. BIN
      ui-ngx/src/assets/fonts/MaterialIcons-Regular.ttf
  58. 11
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  59. 6367
      ui-ngx/src/assets/metadata/material-icons.json
  60. 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.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.security.SecurityProperties; 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.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.DefaultAuthenticationEventPublisher; import org.springframework.security.authentication.DefaultAuthenticationEventPublisher;
import org.springframework.security.config.annotation.ObjectPostProcessor; 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.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; 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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver; 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.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; 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.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter; import org.springframework.web.filter.CorsFilter;
import org.springframework.web.filter.ShallowEtagHeaderFilter;
import org.thingsboard.server.dao.oauth2.OAuth2Configuration; import org.thingsboard.server.dao.oauth2.OAuth2Configuration;
import org.thingsboard.server.exception.ThingsboardErrorResponseHandler; import org.thingsboard.server.exception.ThingsboardErrorResponseHandler;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
@ -119,6 +122,17 @@ public class ThingsboardSecurityConfiguration {
@Autowired private RateLimitProcessingFilter rateLimitProcessingFilter; @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 @Bean
protected RestLoginProcessingFilter buildRestLoginProcessingFilter() throws Exception { protected RestLoginProcessingFilter buildRestLoginProcessingFilter() throws Exception {
RestLoginProcessingFilter filter = new RestLoginProcessingFilter(FORM_BASED_LOGIN_ENTRY_POINT, successHandler, failureHandler); RestLoginProcessingFilter filter = new RestLoginProcessingFilter(FORM_BASED_LOGIN_ENTRY_POINT, successHandler, failureHandler);
@ -181,8 +195,18 @@ public class ThingsboardSecurityConfiguration {
private OAuth2AuthorizationRequestResolver oAuth2AuthorizationRequestResolver; private OAuth2AuthorizationRequestResolver oAuth2AuthorizationRequestResolver;
@Bean @Bean
public WebSecurityCustomizer webSecurityCustomizer() { @Order(0)
return (web) -> web.ignoring().antMatchers("/*.js","/*.css","/*.ico","/assets/**","/static/**"); 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 @Bean

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

@ -103,7 +103,8 @@ export class DialogService {
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: { data: {
color color
} },
autoFocus: false
}).afterClosed(); }).afterClosed();
} }
@ -114,7 +115,8 @@ export class DialogService {
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: { data: {
icon icon
} },
autoFocus: false
}).afterClosed(); }).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 { export class ResourcesService {
private loadedJsonResources: { [url: string]: ReplaySubject<any> } = {};
private loadedResources: { [url: string]: ReplaySubject<void> } = {}; private loadedResources: { [url: string]: ReplaySubject<void> } = {};
private loadedModules: { [url: string]: ReplaySubject<Type<any>[]> } = {}; private loadedModules: { [url: string]: ReplaySubject<Type<any>[]> } = {};
private loadedModulesAndFactories: { [url: string]: ReplaySubject<ModulesWithFactories> } = {}; private loadedModulesAndFactories: { [url: string]: ReplaySubject<ModulesWithFactories> } = {};
@ -61,6 +62,30 @@ export class ResourcesService {
this.store.pipe(select(selectIsAuthenticated)).subscribe(() => this.clearModulesCache()); 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> { public loadResource(url: string): Observable<any> {
if (this.loadedResources[url]) { if (this.loadedResources[url]) {
return this.loadedResources[url].asObservable(); return this.loadedResources[url].asObservable();

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 { WINDOW } from '@core/services/window.service';
import { ExceptionData } from '@app/shared/models/error.models'; import { ExceptionData } from '@app/shared/models/error.models';
import { import {
base64toObj,
base64toString,
baseUrl, baseUrl,
createLabelFromDatasource, createLabelFromDatasource,
deepClone, deepClone,
deleteNullProperties, deleteNullProperties,
guid, hashCode, guid,
hashCode,
isDefined, isDefined,
isDefinedAndNotNull, isDefinedAndNotNull,
isString, isString,
isUndefined, isUndefined,
objToBase64, objToBase64,
objToBase64URI, objToBase64URI
base64toString,
base64toObj
} from '@core/utils'; } from '@core/utils';
import { WindowMessage } from '@shared/models/window-message.model'; import { WindowMessage } from '@shared/models/window-message.model';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { customTranslationsPrefix, i18nPrefix } from '@app/shared/models/constants'; import { customTranslationsPrefix, i18nPrefix } from '@app/shared/models/constants';
import { DataKey, Datasource, DatasourceType, KeyInfo } from '@shared/models/widget.models'; 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 { 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 { materialColors } from '@app/shared/models/material.models';
import { WidgetInfo } from '@home/models/widget-component.models'; import { WidgetInfo } from '@home/models/widget-component.models';
import jsonSchemaDefaults from 'json-schema-defaults'; import jsonSchemaDefaults from 'json-schema-defaults';
import materialIconsCodepoints from '!raw-loader!./material-icons-codepoints.raw'; import { Observable } from 'rxjs';
import { Observable, of, ReplaySubject } from 'rxjs';
import { publishReplay, refCount } from 'rxjs/operators'; import { publishReplay, refCount } from 'rxjs/operators';
import { WidgetContext } from '@app/modules/home/models/widget-component.models'; import { WidgetContext } from '@app/modules/home/models/widget-component.models';
import { import {
@ -56,6 +55,8 @@ import {
TelemetryType TelemetryType
} from '@shared/models/telemetry/telemetry.models'; } from '@shared/models/telemetry/telemetry.models';
import { EntityId } from '@shared/models/id/entity-id'; 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'); const i18nRegExp = new RegExp(`{${i18nPrefix}:[^{}]+}`, 'g');
@ -86,13 +87,6 @@ const defaultAlarmFields: Array<string> = [
alarmFields.status.keyName 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 // @dynamic
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -121,10 +115,9 @@ export class UtilsService {
defaultAlarmDataKeys: Array<DataKey> = []; defaultAlarmDataKeys: Array<DataKey> = [];
materialIcons: Array<string> = [];
constructor(@Inject(WINDOW) private window: Window, constructor(@Inject(WINDOW) private window: Window,
private zone: NgZone, private zone: NgZone,
private datePipe: DatePipe,
private translate: TranslateService) { private translate: TranslateService) {
let frame: Element = null; let frame: Element = null;
try { try {
@ -180,6 +173,27 @@ export class UtilsService {
return deepClone(this.defaultAlarmDataKeys); 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 { public generateObjectFromJsonSchema(schema: any): any {
const obj = jsonSchemaDefaults(schema); const obj = jsonSchemaDefaults(schema);
deleteNullProperties(obj); deleteNullProperties(obj);
@ -306,31 +320,6 @@ export class UtilsService {
return datasources; 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) { public getMaterialColor(index: number) {
const colorIndex = index % materialColors.length; const colorIndex = index % materialColors.length;
return materialColors[colorIndex].value; return materialColors[colorIndex].value;
@ -411,7 +400,7 @@ export class UtilsService {
public stringToHslColor(str: string, saturationPercentage: number, lightnessPercentage: number): string { public stringToHslColor(str: string, saturationPercentage: number, lightnessPercentage: number): string {
if (str && str.length) { if (str && str.length) {
let hue = hashCode(str) % 360; const hue = hashCode(str) % 360;
return `hsl(${hue}, ${saturationPercentage}%, ${lightnessPercentage}%)`; 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 ToggleHeaderComponent from '@shared/components/toggle-header.component';
import * as ToggleSelectComponent from '@shared/components/toggle-select.component'; import * as ToggleSelectComponent from '@shared/components/toggle-select.component';
import * as UnitInputComponent from '@shared/components/unit-input.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 AddEntityDialogComponent from '@home/components/entity/add-entity-dialog.component';
import * as EntitiesTableComponent from '@home/components/entity/entities-table.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-header.component': ToggleHeaderComponent,
'@shared/components/toggle-select.component': ToggleSelectComponent, '@shared/components/toggle-select.component': ToggleSelectComponent,
'@shared/components/unit-input.component': UnitInputComponent, '@shared/components/unit-input.component': UnitInputComponent,
'@shared/components/material-icons.component': MaterialIconsComponent,
'@home/components/entity/add-entity-dialog.component': AddEntityDialogComponent, '@home/components/entity/add-entity-dialog.component': AddEntityDialogComponent,
'@home/components/entity/entities-table.component': EntitiesTableComponent, '@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; margin-right: 8px;
} }
} }
.drop-down-icon {
&.inline {
margin-right: -12px;
}
}

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 }}"> <input matInput formControlName="title" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field> </mat-form-field>
</div> </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"> <mat-slide-toggle class="mat-slide" formControlName="showTitleIcon">
{{ 'widget-config.card-icon' | translate }} {{ 'widget-config.card-icon' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px"> <div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-material-icon-select asBoxInput <tb-material-icon-select asBoxInput
[color]="alarmsTableWidgetConfigForm.get('iconColor').value" [color]="alarmsTableWidgetConfigForm.get('iconColor').value"
formControlName="titleIcon"> formControlName="titleIcon">
</tb-material-icon-select> </tb-material-icon-select>
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput <tb-color-input asBoxInput
formControlName="iconColor"> formControlName="iconColor">
</tb-color-input> </tb-color-input>
@ -82,23 +81,17 @@
<mat-chip-option value="fullscreen">{{ 'fullscreen.fullscreen' | translate }}</mat-chip-option> <mat-chip-option value="fullscreen">{{ 'fullscreen.fullscreen' | translate }}</mat-chip-option>
</mat-chip-listbox> </mat-chip-listbox>
</div> </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>{{ 'widget-config.text-color' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px"> <tb-color-input asBoxInput
<mat-divider vertical></mat-divider> formControlName="color">
<tb-color-input asBoxInput </tb-color-input>
formControlName="color">
</tb-color-input>
</div>
</div> </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>{{ 'widget-config.background-color' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px"> <tb-color-input asBoxInput
<mat-divider vertical></mat-divider> formControlName="backgroundColor">
<tb-color-input asBoxInput </tb-color-input>
formControlName="backgroundColor">
</tb-color-input>
</div>
</div> </div>
</div> </div>
<tb-widget-actions-panel <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 }}"> <input matInput formControlName="title" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field> </mat-form-field>
</div> </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"> <mat-slide-toggle class="mat-slide" formControlName="showTitleIcon">
{{ 'widget-config.card-icon' | translate }} {{ 'widget-config.card-icon' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px"> <div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-material-icon-select asBoxInput <tb-material-icon-select asBoxInput
[color]="entitiesTableWidgetConfigForm.get('iconColor').value" [color]="entitiesTableWidgetConfigForm.get('iconColor').value"
formControlName="titleIcon"> formControlName="titleIcon">
</tb-material-icon-select> </tb-material-icon-select>
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput <tb-color-input asBoxInput
formControlName="iconColor"> formControlName="iconColor">
</tb-color-input> </tb-color-input>
@ -70,23 +69,17 @@
<mat-chip-option value="fullscreen">{{ 'fullscreen.fullscreen' | translate }}</mat-chip-option> <mat-chip-option value="fullscreen">{{ 'fullscreen.fullscreen' | translate }}</mat-chip-option>
</mat-chip-listbox> </mat-chip-listbox>
</div> </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>{{ 'widget-config.text-color' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px"> <tb-color-input asBoxInput
<mat-divider vertical></mat-divider> formControlName="color">
<tb-color-input asBoxInput </tb-color-input>
formControlName="color">
</tb-color-input>
</div>
</div> </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>{{ 'widget-config.background-color' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px"> <tb-color-input asBoxInput
<mat-divider vertical></mat-divider> formControlName="backgroundColor">
<tb-color-input asBoxInput </tb-color-input>
formControlName="backgroundColor">
</tb-color-input>
</div>
</div> </div>
</div> </div>
<tb-widget-actions-panel <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-option value="fullscreen">{{ 'fullscreen.fullscreen' | translate }}</mat-chip-option>
</mat-chip-listbox> </mat-chip-listbox>
</div> </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>{{ 'widget-config.text-color' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px"> <tb-color-input asBoxInput
<mat-divider vertical></mat-divider> formControlName="color">
<tb-color-input asBoxInput </tb-color-input>
formControlName="color">
</tb-color-input>
</div>
</div> </div>
<div class="tb-form-row space-between same-padding"> <div class="tb-form-row space-between">
<div>{{ 'widget-config.background' | translate }}</div> <div>{{ 'widget-config.background' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px"> <tb-color-input asBoxInput
<mat-divider vertical></mat-divider> formControlName="backgroundColor">
<tb-color-input asBoxInput </tb-color-input>
formControlName="backgroundColor">
</tb-color-input>
</div>
</div> </div>
</div> </div>
<tb-widget-actions-panel <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 }}"> <input matInput formControlName="title" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field> </mat-form-field>
</div> </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"> <mat-slide-toggle class="mat-slide" formControlName="showTitleIcon">
{{ 'widget-config.card-icon' | translate }} {{ 'widget-config.card-icon' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px"> <div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-material-icon-select asBoxInput <tb-material-icon-select asBoxInput
[color]="timeseriesTableWidgetConfigForm.get('iconColor').value" [color]="timeseriesTableWidgetConfigForm.get('iconColor').value"
formControlName="titleIcon"> formControlName="titleIcon">
</tb-material-icon-select> </tb-material-icon-select>
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput <tb-color-input asBoxInput
formControlName="iconColor"> formControlName="iconColor">
</tb-color-input> </tb-color-input>
@ -70,23 +69,17 @@
<mat-chip-option value="fullscreen">{{ 'fullscreen.fullscreen' | translate }}</mat-chip-option> <mat-chip-option value="fullscreen">{{ 'fullscreen.fullscreen' | translate }}</mat-chip-option>
</mat-chip-listbox> </mat-chip-listbox>
</div> </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>{{ 'widget-config.text-color' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px"> <tb-color-input asBoxInput
<mat-divider vertical></mat-divider> formControlName="color">
<tb-color-input asBoxInput </tb-color-input>
formControlName="color">
</tb-color-input>
</div>
</div> </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>{{ 'widget-config.background-color' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px"> <tb-color-input asBoxInput
<mat-divider vertical></mat-divider> formControlName="backgroundColor">
<tb-color-input asBoxInput </tb-color-input>
formControlName="backgroundColor">
</tb-color-input>
</div>
</div> </div>
</div> </div>
<tb-widget-actions-panel <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 }}"> <input matInput formControlName="title" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field> </mat-form-field>
</div> </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"> <mat-slide-toggle class="mat-slide" formControlName="showTitleIcon">
{{ 'widget-config.card-icon' | translate }} {{ 'widget-config.card-icon' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px"> <div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-material-icon-select asBoxInput <tb-material-icon-select asBoxInput
[color]="flotWidgetConfigForm.get('iconColor').value" [color]="flotWidgetConfigForm.get('iconColor').value"
formControlName="titleIcon"> formControlName="titleIcon">
</tb-material-icon-select> </tb-material-icon-select>
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput <tb-color-input asBoxInput
formControlName="iconColor"> formControlName="iconColor">
</tb-color-input> </tb-color-input>
@ -68,23 +67,17 @@
<mat-chip-option value="fullscreen">{{ 'fullscreen.fullscreen' | translate }}</mat-chip-option> <mat-chip-option value="fullscreen">{{ 'fullscreen.fullscreen' | translate }}</mat-chip-option>
</mat-chip-listbox> </mat-chip-listbox>
</div> </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>{{ 'widget-config.text-color' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px"> <tb-color-input asBoxInput
<mat-divider vertical></mat-divider> formControlName="color">
<tb-color-input asBoxInput </tb-color-input>
formControlName="color">
</tb-color-input>
</div>
</div> </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>{{ 'widget-config.background-color' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px"> <tb-color-input asBoxInput
<mat-divider vertical></mat-divider> formControlName="backgroundColor">
<tb-color-input asBoxInput </tb-color-input>
formControlName="backgroundColor">
</tb-color-input>
</div>
</div> </div>
</div> </div>
<div class="tb-form-panel"> <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> </mat-form-field>
</div> </div>
</ng-container> </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>{{ 'datakey.color' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px"> <tb-color-input asBoxInput
<mat-divider vertical></mat-divider> formControlName="color">
<tb-color-input asBoxInput </tb-color-input>
formControlName="color">
</tb-color-input>
</div>
</div> </div>
<ng-container *ngIf="widgetType === widgetTypes.latest && modelValue.type === dataKeyTypes.timeseries"> <ng-container *ngIf="widgetType === widgetTypes.latest && modelValue.type === dataKeyTypes.timeseries">
<mat-form-field subscriptSizing="dynamic"> <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> </div>
<div *ngIf="!hideDataKeyColor" style="padding: 3px;"> <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 class="tb-color-result" [ngStyle]="{background: key.color}"></div>
</div> </div>
</div> </div>

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

@ -27,6 +27,7 @@ import {
SimpleChanges, SimpleChanges,
SkipSelf, SkipSelf,
ViewChild, ViewChild,
ViewContainerRef,
ViewEncapsulation ViewEncapsulation
} from '@angular/core'; } from '@angular/core';
import { import {
@ -56,7 +57,6 @@ import { alarmFields } from '@shared/models/alarm.models';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { ErrorStateMatcher } from '@angular/material/core'; import { ErrorStateMatcher } from '@angular/material/core';
import { TruncatePipe } from '@shared/pipe/truncate.pipe'; import { TruncatePipe } from '@shared/pipe/truncate.pipe';
import { DialogService } from '@core/services/dialog.service';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { import {
DataKeyConfigDialogComponent, DataKeyConfigDialogComponent,
@ -69,6 +69,8 @@ import { DndDropEvent } from 'ngx-drag-drop/lib/dnd-dropzone.directive';
import { moveItemInArray } from '@angular/cdk/drag-drop'; import { moveItemInArray } from '@angular/cdk/drag-drop';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
import { DatasourceComponent } from '@home/components/widget/config/datasource.component'; 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({ @Component({
selector: 'tb-data-keys', selector: 'tb-data-keys',
@ -208,10 +210,11 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
private datasourceComponent: DatasourceComponent, private datasourceComponent: DatasourceComponent,
public translate: TranslateService, public translate: TranslateService,
private utils: UtilsService, private utils: UtilsService,
private dialogs: DialogService,
private dialog: MatDialog, private dialog: MatDialog,
private fb: UntypedFormBuilder, private fb: UntypedFormBuilder,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
private popoverService: TbPopoverService,
private viewContainerRef: ViewContainerRef,
private renderer: Renderer2, private renderer: Renderer2,
public truncate: TruncatePipe) { public truncate: TruncatePipe) {
} }
@ -471,15 +474,30 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
this.propagateChange(this.modelValue); this.propagateChange(this.modelValue);
} }
showColorPicker(key: DataKey) { openColorPickerPopup(key: DataKey, $event: Event, keyColorButton: HTMLDivElement) {
this.dialogs.colorPicker(key.color).subscribe( if ($event) {
(color) => { $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) { if (color && key.color !== color) {
key.color = color; key.color = color;
this.propagateChange(this.modelValue); this.propagateChange(this.modelValue);
} }
} });
); }
} }
editDataKey(key: DataKey, index: number) { 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, Injector,
Input, Input,
NgZone, NgZone,
OnDestroy,
OnInit, OnInit,
StaticProvider, StaticProvider,
ViewChild, ViewChild,
@ -52,7 +53,6 @@ import { Direction } from '@shared/models/page/sort-order';
import { CollectionViewer, DataSource, SelectionModel } from '@angular/cdk/collections'; import { CollectionViewer, DataSource, SelectionModel } from '@angular/cdk/collections';
import { BehaviorSubject, forkJoin, fromEvent, merge, Observable, Subscription } from 'rxjs'; import { BehaviorSubject, forkJoin, fromEvent, merge, Observable, Subscription } from 'rxjs';
import { emptyPageData, PageData } from '@shared/models/page/page-data'; 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 { debounceTime, distinctUntilChanged, map, take, tap } from 'rxjs/operators';
import { MatPaginator } from '@angular/material/paginator'; import { MatPaginator } from '@angular/material/paginator';
import { MatSort, SortDirection } from '@angular/material/sort'; import { MatSort, SortDirection } from '@angular/material/sort';
@ -92,15 +92,7 @@ import {
DisplayColumnsPanelComponent, DisplayColumnsPanelComponent,
DisplayColumnsPanelData DisplayColumnsPanelData
} from '@home/components/widget/lib/display-columns-panel.component'; } from '@home/components/widget/lib/display-columns-panel.component';
import { import { AlarmDataInfo, alarmFields, AlarmInfo, alarmSeverityColors, AlarmStatus } from '@shared/models/alarm.models';
AlarmDataInfo,
alarmFields,
AlarmInfo,
alarmSeverityColors,
alarmSeverityTranslations,
AlarmStatus,
alarmStatusTranslations
} from '@shared/models/alarm.models';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { import {
AlarmDetailsDialogComponent, AlarmDetailsDialogComponent,
@ -139,7 +131,6 @@ import {
AlarmFilterConfigData AlarmFilterConfigData
} from '@home/components/alarm/alarm-filter-config.component'; } from '@home/components/alarm/alarm-filter-config.component';
import { getCurrentAuthUser } from '@core/auth/auth.selectors'; import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { UserId } from '@shared/models/id/user-id';
interface AlarmsTableWidgetSettings extends TableWidgetSettings { interface AlarmsTableWidgetSettings extends TableWidgetSettings {
alarmsTitle: string; alarmsTitle: string;
@ -167,7 +158,7 @@ interface AlarmWidgetActionDescriptor extends TableCellButtonActionDescriptor {
templateUrl: './alarms-table-widget.component.html', templateUrl: './alarms-table-widget.component.html',
styleUrls: ['./alarms-table-widget.component.scss', './table-widget.scss'] 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() @Input()
ctx: WidgetContext; ctx: WidgetContext;
@ -431,7 +422,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
keySettings.columnWidth = '120px'; keySettings.columnWidth = '120px';
} }
if (alarmField && alarmField.keyName === alarmFields.assignee.keyName) { if (alarmField && alarmField.keyName === alarmFields.assignee.keyName) {
keySettings.columnWidth = '120px' keySettings.columnWidth = '120px';
} }
} }
this.stylesInfo[dataKey.def] = getCellStyleInfo(keySettings, 'value, alarm, ctx'); this.stylesInfo[dataKey.def] = getCellStyleInfo(keySettings, 'value, alarm, ctx');
@ -543,14 +534,12 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
overlayRef.dispose(); overlayRef.dispose();
}); });
const columns: DisplayColumn[] = this.columns.map(column => { const columns: DisplayColumn[] = this.columns.map(column => ({
return {
title: column.title, title: column.title,
def: column.def, def: column.def,
display: this.displayedColumns.indexOf(column.def) > -1, display: this.displayedColumns.indexOf(column.def) > -1,
selectable: this.columnSelectionAvailability[column.def] selectable: this.columnSelectionAvailability[column.def]
}; }));
});
const providers: StaticProvider[] = [ const providers: StaticProvider[] = [
{ {
@ -1010,7 +999,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
data: { data: {
alarmId: alarm.id.id alarmId: alarm.id.id
} }
}).afterClosed() }).afterClosed();
} }
} }
@ -1018,20 +1007,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
if (isDefined(value)) { if (isDefined(value)) {
const alarmField = alarmFields[key.name]; const alarmField = alarmFields[key.name];
if (alarmField) { if (alarmField) {
if (alarmField.time) { return this.utils.defaultAlarmFieldContent(key, value);
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;
}
} }
const entityField = entityFields[key.name]; const entityField = entityFields[key.name];
if (entityField) { if (entityField) {

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

@ -67,50 +67,4 @@
color: inherit; 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 }}"> <input matInput formControlName="comparisonValuesLabel" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field> </mat-form-field>
</div> </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>{{ 'widgets.chart.comparison-line-color' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px"> <tb-color-input asBoxInput
<mat-divider vertical></mat-divider> formControlName="color">
<tb-color-input asBoxInput </tb-color-input>
formControlName="color">
</tb-color-input>
</div>
</div> </div>
</ng-template> </ng-template>
</mat-expansion-panel> </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> <span matSuffix>px</span>
</mat-form-field> </mat-form-field>
</div> </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>{{ 'widgets.chart.threshold-color' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px"> <tb-color-input asBoxInput
<mat-divider vertical></mat-divider> formControlName="thresholdColor">
<tb-color-input asBoxInput </tb-color-input>
formControlName="thresholdColor">
</tb-color-input>
</div>
</div> </div>
</ng-template> </ng-template>
</mat-expansion-panel> </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 }}"> <input matInput formControlName="thresholdsLineWidth" type="number" min="0" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field> </mat-form-field>
</div> </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>{{ '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"> <mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="fontSize" type="number" min="0" placeholder="{{ 'widget-config.set' | translate }}"> <input matInput formControlName="fontSize" type="number" min="0" placeholder="{{ 'widget-config.set' | translate }}">
<span matSuffix>px</span> <span matSuffix>px</span>
</mat-form-field> </mat-form-field>
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput <tb-color-input asBoxInput
formControlName="fontColor"> formControlName="fontColor">
</tb-color-input> </tb-color-input>
@ -132,14 +131,11 @@
</mat-panel-description> </mat-panel-description>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<ng-template matExpansionPanelContent> <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>{{ 'widget-config.color' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px"> <tb-color-input asBoxInput
<mat-divider vertical></mat-divider> formControlName="color">
<tb-color-input asBoxInput </tb-color-input>
formControlName="color">
</tb-color-input>
</div>
</div> </div>
<div class="tb-form-row space-between"> <div class="tb-form-row space-between">
<div translate>widget-config.decimals-short</div> <div translate>widget-config.decimals-short</div>
@ -187,14 +183,11 @@
</mat-panel-description> </mat-panel-description>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<ng-template matExpansionPanelContent> <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>{{ 'widget-config.color' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px"> <tb-color-input asBoxInput
<mat-divider vertical></mat-divider> formControlName="color">
<tb-color-input asBoxInput </tb-color-input>
formControlName="color">
</tb-color-input>
</div>
</div> </div>
</ng-template> </ng-template>
</mat-expansion-panel> </mat-expansion-panel>
@ -213,36 +206,29 @@
{{ 'widgets.chart.horizontal-grid-lines' | translate }} {{ 'widgets.chart.horizontal-grid-lines' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
</div> </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>{{ 'widgets.chart.grid-lines-color' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px"> <tb-color-input asBoxInput
<mat-divider vertical></mat-divider> formControlName="tickColor">
<tb-color-input asBoxInput </tb-color-input>
formControlName="tickColor">
</tb-color-input>
</div>
</div> </div>
<div class="tb-form-row space-between same-padding"> <div class="tb-form-row space-between">
<div>{{ 'widgets.chart.border' | translate }}</div> <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"> <mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="outlineWidth" type="number" min="0" placeholder="{{ 'widget-config.set' | translate }}"> <input matInput formControlName="outlineWidth" type="number" min="0" placeholder="{{ 'widget-config.set' | translate }}">
<span matSuffix>px</span> <span matSuffix>px</span>
</mat-form-field> </mat-form-field>
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput <tb-color-input asBoxInput
formControlName="color"> formControlName="color">
</tb-color-input> </tb-color-input>
</div> </div>
</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>{{ 'widgets.chart.background-color' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px"> <tb-color-input asBoxInput
<mat-divider vertical></mat-divider> formControlName="backgroundColor">
<tb-color-input asBoxInput </tb-color-input>
formControlName="backgroundColor">
</tb-color-input>
</div>
</div> </div>
</div> </div>
<div class="tb-form-panel tb-slide-toggle"> <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 }}"> <input matInput formControlName="titleTooltip" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field> </mat-form-field>
</div> </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"> <mat-slide-toggle class="mat-slide" formControlName="showTitleIcon">
{{ 'widget-config.display-icon' | translate }} {{ 'widget-config.display-icon' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px"> <div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-material-icon-select asBoxInput <tb-material-icon-select asBoxInput
[color]="widgetSettings.get('iconColor').value" [color]="widgetSettings.get('iconColor').value"
formControlName="titleIcon"> formControlName="titleIcon">
@ -60,7 +60,6 @@
<mat-form-field appearance="outline" subscriptSizing="dynamic"> <mat-form-field appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="iconSize"> <input matInput formControlName="iconSize">
</mat-form-field> </mat-form-field>
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput <tb-color-input asBoxInput
formControlName="iconColor"> formControlName="iconColor">
</tb-color-input> </tb-color-input>
@ -84,23 +83,17 @@
</div> </div>
<div class="tb-form-panel"> <div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.card-style</div> <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>{{ 'widget-config.text-color' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px"> <tb-color-input asBoxInput
<mat-divider vertical></mat-divider> formControlName="color">
<tb-color-input asBoxInput </tb-color-input>
formControlName="color">
</tb-color-input>
</div>
</div> </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>{{ 'widget-config.background-color' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px"> <tb-color-input asBoxInput
<mat-divider vertical></mat-divider> formControlName="backgroundColor">
<tb-color-input asBoxInput </tb-color-input>
formControlName="backgroundColor">
</tb-color-input>
</div>
</div> </div>
<div class="tb-form-row space-between"> <div class="tb-form-row space-between">
<div>{{ 'widget-config.padding' | translate }}</div> <div>{{ 'widget-config.padding' | translate }}</div>

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

@ -14,10 +14,21 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { NgModule } from '@angular/core'; import { inject, NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { ActivatedRouteSnapshot, ResolveFn, RouterModule, RouterStateSnapshot, Routes } from '@angular/router';
import { Authority } from '@shared/models/authority.enum'; import { Authority } from '@shared/models/authority.enum';
import { ApiUsageComponent } from '@home/pages/api-usage/api-usage.component'; 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 = [ const routes: Routes = [
{ {
@ -30,6 +41,9 @@ const routes: Routes = [
label: 'api-usage.api-usage', label: 'api-usage.api-usage',
icon: 'insert_chart' 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. /// limitations under the License.
/// ///
import { Component, OnInit } from '@angular/core'; import { Component } from '@angular/core';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
import apiUsageDashboardJson from '!raw-loader!./api_usage_json.raw';
import { Dashboard } from '@shared/models/dashboard.models'; import { Dashboard } from '@shared/models/dashboard.models';
import { ActivatedRoute } from '@angular/router';
@Component({ @Component({
selector: 'tb-api-usage', selector: 'tb-api-usage',
templateUrl: './api-usage.component.html', templateUrl: './api-usage.component.html',
styleUrls: ['./api-usage.component.scss'] 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); 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. /// limitations under the License.
/// ///
import { Injectable, NgModule } from '@angular/core'; import { inject, NgModule } from '@angular/core';
import { Resolve, RouterModule, Routes } from '@angular/router'; import { ActivatedRouteSnapshot, ResolveFn, RouterModule, RouterStateSnapshot, Routes } from '@angular/router';
import { HomeLinksComponent } from './home-links.component'; import { HomeLinksComponent } from './home-links.component';
import { Authority } from '@shared/models/authority.enum'; 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 { select, Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { import { getCurrentAuthUser, selectPersistDeviceStateToTelemetry } from '@core/auth/auth.selectors';
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 { EntityKeyType } from '@shared/models/query/query.models'; import { EntityKeyType } from '@shared/models/query/query.models';
import { ResourcesService } from '@core/services/resources.service';
@Injectable() const sysAdminHomePageJson = '/assets/dashboard/sys_admin_home_page.json';
export class HomeDashboardResolver implements Resolve<HomeDashboard> { const tenantAdminHomePageJson = '/assets/dashboard/tenant_admin_home_page.json';
const customerUserHomePageJson = '/assets/dashboard/customer_user_home_page.json';
constructor(private dashboardService: DashboardService, const updateDeviceActivityKeyFilterIfNeeded = (store: Store<AppState>,
private store: Store<AppState>) { dashboard$: Observable<HomeDashboard>): Observable<HomeDashboard> =>
} store.pipe(select(selectPersistDeviceStateToTelemetry)).pipe(
mergeMap((persistToTelemetry) => dashboard$.pipe(
resolve(): Observable<HomeDashboard> { map((dashboard) => {
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) => {
if (persistToTelemetry) { if (persistToTelemetry) {
for (const filterId of Object.keys(dashboard.configuration.filters)) { for (const filterId of Object.keys(dashboard.configuration.filters)) {
if (['Active Devices', 'Inactive Devices'].includes(dashboard.configuration.filters[filterId].filter)) { if (['Active Devices', 'Inactive Devices'].includes(dashboard.configuration.filters[filterId].filter)) {
@ -85,9 +47,44 @@ export class HomeDashboardResolver implements Resolve<HomeDashboard> {
} }
return dashboard; 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 = [ const routes: Routes = [
{ {
@ -102,16 +99,13 @@ const routes: Routes = [
} }
}, },
resolve: { resolve: {
homeDashboard: HomeDashboardResolver homeDashboard: homeDashboardResolver
} }
} }
]; ];
@NgModule({ @NgModule({
imports: [RouterModule.forChild(routes)], imports: [RouterModule.forChild(routes)],
exports: [RouterModule], exports: [RouterModule]
providers: [
HomeDashboardResolver
]
}) })
export class HomeLinksRoutingModule { } export class HomeLinksRoutingModule { }

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

@ -35,7 +35,14 @@
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
<ng-template #boxInput> <ng-template #boxInput>
<div class="tb-color-preview no-margin box" [ngClass]="{'disabled': disabled}" (click)="!disabled && showColorPicker($event)"> <button type="button"
<div class="tb-color-result" [ngStyle]="!disabled ? {background: colorFormGroup.get('color').value} : {}"></div> mat-stroked-button
</div> 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> </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 * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
@import './../../../scss/mixins';
:host { :host {
.mat-mdc-form-field { .mat-mdc-form-field {
width: 100%; width: 100%;
@ -29,4 +32,10 @@
margin: 0; 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. /// 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 { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { 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 { TranslateService } from '@ngx-translate/core';
import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { DialogService } from '@core/services/dialog.service'; import { DialogService } from '@core/services/dialog.service';
import { coerceBoolean } from '@shared/decorators/coercion'; 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({ @Component({
selector: 'tb-color-input', selector: 'tb-color-input',
@ -100,6 +109,9 @@ export class ColorInputComponent extends PageComponent implements OnInit, Contro
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private dialogs: DialogService, private dialogs: DialogService,
private translate: TranslateService, private translate: TranslateService,
private popoverService: TbPopoverService,
private renderer: Renderer2,
private viewContainerRef: ViewContainerRef,
private fb: UntypedFormBuilder, private fb: UntypedFormBuilder,
private cd: ChangeDetectorRef) { private cd: ChangeDetectorRef) {
super(store); 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() { clear() {
this.colorFormGroup.get('color').patchValue(null, {emitEvent: true}); this.colorFormGroup.get('color').patchValue(null, {emitEvent: true});
this.cd.markForCheck(); 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"> <div class="control-component">
<indicator-component class="indicator-component" <indicator-component class="indicator-component"
[colorType]="presentations[selectedPresentation]" [colorType]="presentations[presentationControl.value]"
[color]="control.value"> [color]="control.value">
</indicator-component> </indicator-component>
<div class="hue-alpha-range"> <div class="hue-alpha-range">
@ -29,7 +29,12 @@
</div> </div>
<div class="color-input-block"> <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 <rgba-input-component *ngSwitchCase="'rgba'" label
[(color)]="control.value" [(hue)]="control.hue"></rgba-input-component> [(color)]="control.value" [(hue)]="control.hue"></rgba-input-component>
<hsla-input-component *ngSwitchCase="'hsla'" label <hsla-input-component *ngSwitchCase="'hsla'" label
@ -37,5 +42,8 @@
<hex-input-component *ngSwitchCase="'hex'" label prefix="#" [(color)]="control.value" <hex-input-component *ngSwitchCase="'hex'" label prefix="#" [(color)]="control.value"
[(hue)]="control.hue"></hex-input-component> [(hue)]="control.hue"></hex-input-component>
</div> </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> </div>

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

@ -17,12 +17,10 @@
width: 100%; width: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 16px; gap: 32px;
.saturation-component { .saturation-component {
height: 100%; height: 238px;
min-height: 200px;
max-height: 300px;
border-radius: 8px; border-radius: 8px;
} }
@ -55,6 +53,12 @@
.color-input-block { .color-input-block {
display: flex; display: flex;
gap: 20px;
.presentation-select {
font-size: 14px;
width: 56px;
}
.color-input { .color-input {
flex: 1; flex: 1;
@ -63,16 +67,13 @@
color: initial; color: initial;
} }
} }
}
.type-btn { .color-presets-block {
height: 26px; .color-presets-component {
width: 20px; display: flex;
background: transparent url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAAgCAMAAAAootjDAAAAM1BMVEUAAAAzMzMzMzMzMzMzMzM0NDQzMzMzMzM0NDQzMzMzMzM0NDQzMzMzMzMyMjIrKyszMzPF8UZlAAAAEHRSTlMA1fHr4ZxxSRP45sG+sCkGH2+Z6QAAAHJJREFUKM+9kkkSgCAQA0FEVLb5/2tViqgQvNrHviSzKGCt6nDGuNass8i8NsrLiX+bZbrUtDwm7VLYE0zWUtEZ+RvUZpEvN8YhH9QmQRoC8kFpEnVHVP/DJUZVeSAem5fDKxwtms/BR+PT8gN8vwk/0wE1gQzNVYryIwAAAABJRU5ErkJggg==') no-repeat center; flex-direction: column;
background-size: 6px 12px; gap: 12px;
&:hover {
background-color: #eee;
}
} }
} }
} }
@ -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 { Component, forwardRef, OnDestroy } from '@angular/core';
import { Color, ColorPickerControl } from '@iplab/ngx-color-picker'; import { Color, ColorPickerControl } from '@iplab/ngx-color-picker';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormControl } from '@angular/forms';
export enum ColorType { export enum ColorType {
hex = 'hex', hex = 'hex',
@ -29,6 +29,10 @@ export enum ColorType {
cmyk = 'cmyk' 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({ @Component({
selector: `tb-color-picker`, selector: `tb-color-picker`,
templateUrl: `./color-picker.component.html`, templateUrl: `./color-picker.component.html`,
@ -43,10 +47,13 @@ export enum ColorType {
}) })
export class ColorPickerComponent implements ControlValueAccessor, OnDestroy { export class ColorPickerComponent implements ControlValueAccessor, OnDestroy {
selectedPresentation = 0;
presentations = [ColorType.hex, ColorType.rgba, ColorType.hsla]; presentations = [ColorType.hex, ColorType.rgba, ColorType.hsla];
control = new ColorPickerControl(); control = new ColorPickerControl();
presentationControl = new UntypedFormControl(0);
colorPresets: Color[] = colorPresetsHex.map(c => Color.from(c));
private modelValue: string; private modelValue: string;
private subscriptions: Array<Subscription> = []; 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 { registerOnChange(fn: any): void {
@ -86,12 +98,11 @@ export class ColorPickerComponent implements ControlValueAccessor, OnDestroy {
} else if (this.control.initType === ColorType.hsl) { } else if (this.control.initType === ColorType.hsl) {
this.control.initType = ColorType.hsla; this.control.initType = ColorType.hsla;
} }
this.presentationControl.patchValue(this.presentations.indexOf(this.control.initType), {emitEvent: false});
this.selectedPresentation = this.presentations.indexOf(this.control.initType);
} }
private updateModel() { 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) { if (this.modelValue !== color) {
this.modelValue = color; this.modelValue = color;
this.propagateChange(color); this.propagateChange(color);
@ -103,12 +114,6 @@ export class ColorPickerComponent implements ControlValueAccessor, OnDestroy {
this.subscriptions.length = 0; 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 { getValueByType(color: Color, type: ColorType): string {
switch (type) { switch (type) {
case ColorType.hex: case ColorType.hex:

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

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

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. /// 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 { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { DialogComponent } from '@shared/components/dialog.component'; import { DialogComponent } from '@shared/components/dialog.component';
@ -29,33 +28,26 @@ export interface ColorPickerDialogData {
@Component({ @Component({
selector: 'tb-color-picker-dialog', selector: 'tb-color-picker-dialog',
templateUrl: './color-picker-dialog.component.html', templateUrl: './color-picker-dialog.component.html',
styleUrls: [] styleUrls: ['./color-picker-dialog.component.scss']
}) })
export class ColorPickerDialogComponent extends DialogComponent<ColorPickerDialogComponent, string> export class ColorPickerDialogComponent extends DialogComponent<ColorPickerDialogComponent, string> {
implements OnInit {
colorPickerFormGroup: FormGroup; color: string;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
protected router: Router, protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: ColorPickerDialogData, @Inject(MAT_DIALOG_DATA) public data: ColorPickerDialogData,
public dialogRef: MatDialogRef<ColorPickerDialogComponent, string>, public dialogRef: MatDialogRef<ColorPickerDialogComponent, string>) {
public fb: FormBuilder) {
super(store, router, dialogRef); super(store, router, dialogRef);
this.color = data.color;
} }
ngOnInit(): void { selectColor(color: string) {
this.colorPickerFormGroup = this.fb.group({ this.dialogRef.close(color);
color: [this.data.color, [Validators.required]]
});
} }
cancel(): void { cancel(): void {
this.dialogRef.close(null); 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. limitations under the License.
--> -->
<form class="tb-material-icons-dialog" style="min-width: 600px;"> <div mat-dialog-content>
<mat-toolbar fxLayout="row" color="primary"> <button class="tb-close-button"
<h2>{{ 'icon.select-icon' | translate }}</h2> mat-icon-button
<span fxFlex></span> (click)="cancel()"
<section fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> type="button">
<mat-slide-toggle [formControl]="showAllControl"> <mat-icon>close</mat-icon>
</mat-slide-toggle> </button>
<label translate>icon.show-all</label> <tb-material-icons [selectedIcon]="selectedIcon"
</section> (iconSelected)="selectIcon($event)">
<button mat-icon-button </tb-material-icons>
(click)="cancel()" </div>
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>

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

@ -14,36 +14,9 @@
* limitations under the License. * limitations under the License.
*/ */
:host { :host {
.tb-material-icons-dialog { .tb-close-button {
position: relative; position: absolute;
} top: 6px;
.tb-icons-load { right: 6px;
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;
}
}
} }
} }

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

@ -14,16 +14,12 @@
/// limitations under the License. /// 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 { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { DialogComponent } from '@shared/components/dialog.component'; 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 { export interface MaterialIconsDialogData {
icon: string; icon: string;
@ -35,64 +31,16 @@ export interface MaterialIconsDialogData {
providers: [], providers: [],
styleUrls: ['./material-icons-dialog.component.scss'] styleUrls: ['./material-icons-dialog.component.scss']
}) })
export class MaterialIconsDialogComponent extends DialogComponent<MaterialIconsDialogComponent, string> export class MaterialIconsDialogComponent extends DialogComponent<MaterialIconsDialogComponent, string> {
implements OnInit, AfterViewInit {
@ViewChildren('iconButtons') iconButtons: QueryList<HTMLElement>;
selectedIcon: string; selectedIcon: string;
icons$: Observable<Array<string>>;
loadingIcons$: Observable<boolean>;
showAllControl: UntypedFormControl;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
protected router: Router, protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: MaterialIconsDialogData, @Inject(MAT_DIALOG_DATA) public data: MaterialIconsDialogData,
private utils: UtilsService,
public dialogRef: MatDialogRef<MaterialIconsDialogComponent, string>) { public dialogRef: MatDialogRef<MaterialIconsDialogComponent, string>) {
super(store, router, dialogRef); super(store, router, dialogRef);
this.selectedIcon = data.icon; 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) { selectIcon(icon: string) {

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

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

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

@ -29,7 +29,13 @@
</mat-form-field> </mat-form-field>
</div> </div>
<ng-template #boxInput> <ng-template #boxInput>
<mat-icon class="icon-box" [ngStyle]="color && !disabled ? { color: color } : {}" <button type="button"
[ngClass]="{'disabled': disabled}" mat-stroked-button
(click)="openIconDialog()">{{materialIconFormGroup.get('icon').value}}</mat-icon> 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> </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); border: solid 1px rgba(0, 0, 0, .27);
box-sizing: initial; box-sizing: initial;
} }
&.icon-box { }
border: 1px solid rgba(0, 0, 0, 0.12); }
border-radius: 4px;
cursor: pointer; :host ::ng-deep {
box-sizing: border-box; button.mat-mdc-button-base.icon-box {
padding: 8px; width: 40px;
height: 40px; min-width: 40px;
width: 40px; height: 40px;
font-size: 22px; padding: 7px;
vertical-align: middle; &:not(:disabled) {
&.disabled { color: rgba(0, 0, 0, 0.87);
cursor: initial; }
color: rgba(0, 0, 0, 0.38); > .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. /// 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 { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { 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 { DialogService } from '@core/services/dialog.service';
import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { coerceBoolean } from '@shared/decorators/coercion'; 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({ @Component({
selector: 'tb-material-icon-select', selector: 'tb-material-icon-select',
@ -81,6 +84,9 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private dialogs: DialogService, private dialogs: DialogService,
private translate: TranslateService, private translate: TranslateService,
private popoverService: TbPopoverService,
private renderer: Renderer2,
private viewContainerRef: ViewContainerRef,
private fb: UntypedFormBuilder, private fb: UntypedFormBuilder,
private cd: ChangeDetectorRef) { private cd: ChangeDetectorRef) {
super(store); 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() { clear() {
this.materialIconFormGroup.get('icon').patchValue(null, {emitEvent: true}); this.materialIconFormGroup.get('icon').patchValue(null, {emitEvent: true});
this.cd.markForCheck(); 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; export type TbPopoverTrigger = 'click' | 'focus' | 'hover' | null;
@Directive({ @Directive({
// eslint-disable-next-line @angular-eslint/directive-selector
selector: '[tb-popover]', selector: '[tb-popover]',
exportAs: 'tbPopover', exportAs: 'tbPopover',
// eslint-disable-next-line @angular-eslint/no-host-metadata-property
host: { host: {
'[class.tb-popover-open]': 'visible' '[class.tb-popover-open]': 'visible'
} }
@ -265,12 +267,20 @@ export class TbPopoverDirective implements OnChanges, OnDestroy, AfterViewInit {
} else if (delay > 0) { } else if (delay > 0) {
this.delayTimer = setTimeout(() => { this.delayTimer = setTimeout(() => {
this.delayTimer = undefined; this.delayTimer = undefined;
isEnter ? this.show() : this.hide(); if (isEnter) {
this.show();
} else {
this.hide();
}
}, delay * 1000); }, delay * 1000);
} else { } else {
// `isOrigin` is used due to the tooltip will not hide immediately // `isOrigin` is used due to the tooltip will not hide immediately
// (may caused by the fade-out animation). // (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> </ng-template>
` `
}) })
export class TbPopoverComponent implements OnDestroy, OnInit { export class TbPopoverComponent<T = any> implements OnDestroy, OnInit {
@ViewChild('overlay', { static: false }) overlay!: CdkConnectedOverlay; @ViewChild('overlay', { static: false }) overlay!: CdkConnectedOverlay;
@ViewChild('popoverRoot', { static: false }) popoverRoot!: ElementRef<HTMLElement>; @ViewChild('popoverRoot', { static: false }) popoverRoot!: ElementRef<HTMLElement>;
@ViewChild('popover', { static: false }) popover!: ElementRef<HTMLElement>; @ViewChild('popover', { static: false }) popover!: ElementRef<HTMLElement>;
tbContent: string | TemplateRef<void> | null = null; tbContent: string | TemplateRef<void> | null = null;
tbComponentFactory: ComponentFactory<any> | null = null; tbComponentFactory: ComponentFactory<T> | null = null;
tbComponentRef: ComponentRef<any> | null = null; tbComponentRef: ComponentRef<T> | null = null;
tbComponentContext: any; tbComponentContext: any;
tbComponentInjector: Injector | null = null; tbComponentInjector: Injector | null = null;
tbComponentStyle: { [klass: string]: any } = {}; 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, displayPopover<T>(trigger: Element, renderer: Renderer2, hostView: ViewContainerRef,
componentType: Type<T>, preferredPlacement: PopoverPlacement = 'top', hideOnClickOutside = true, componentType: Type<T>, preferredPlacement: PopoverPlacement = 'top', hideOnClickOutside = true,
injector?: Injector, context?: any, overlayStyle: any = {}, popoverStyle: any = {}, style?: any, injector?: Injector, context?: any, overlayStyle: any = {}, popoverStyle: any = {}, style?: any,
showCloseButton = true): TbPopoverComponent { showCloseButton = true): TbPopoverComponent<T> {
const componentRef = this.createPopoverRef(hostView); const componentRef = this.createPopoverRef(hostView);
return this.displayPopoverWithComponentRef(componentRef, trigger, renderer, componentType, preferredPlacement, hideOnClickOutside, return this.displayPopoverWithComponentRef(componentRef, trigger, renderer, componentType, preferredPlacement, hideOnClickOutside,
injector, context, overlayStyle, popoverStyle, style, showCloseButton); injector, context, overlayStyle, popoverStyle, style, showCloseButton);
@ -74,7 +74,7 @@ export class TbPopoverService {
displayPopoverWithComponentRef<T>(componentRef: ComponentRef<TbPopoverComponent>, trigger: Element, renderer: Renderer2, displayPopoverWithComponentRef<T>(componentRef: ComponentRef<TbPopoverComponent>, trigger: Element, renderer: Renderer2,
componentType: Type<T>, preferredPlacement: PopoverPlacement = 'top', componentType: Type<T>, preferredPlacement: PopoverPlacement = 'top',
hideOnClickOutside = true, injector?: Injector, context?: any, overlayStyle: any = {}, 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; const component = componentRef.instance;
this.popoverWithTriggers.push({ this.popoverWithTriggers.push({
trigger, 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-header.component';
export * from './toggle-select.component'; export * from './toggle-select.component';
export * from './unit-input.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. 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" <input matInput #unitInput [formControl]="unitsFormControl"
placeholder="{{ 'widget-config.set' | translate }}" placeholder="{{ 'widget-config.set' | translate }}"
(focusin)="onFocus()" (focusin)="onFocus()"
[matAutocomplete]="unitsAutocomplete"> [matAutocomplete]="unitsAutocomplete">
<button *ngIf="unitsFormControl.value && !disabled" <button *ngIf="unitsFormControl.value && !disabled"
type="button" type="button"
class="tb-icon-24"
matSuffix mat-icon-button aria-label="Clear" matSuffix mat-icon-button aria-label="Clear"
(click)="clear()"> (click)="clear()">
<mat-icon class="material-icons">close</mat-icon> <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;
})
);

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 { RuleChainSelectComponent } from '@shared/components/rule-chain/rule-chain-select.component';
import { ToggleSelectComponent } from '@shared/components/toggle-select.component'; import { ToggleSelectComponent } from '@shared/components/toggle-select.component';
import { UnitInputComponent } from '@shared/components/unit-input.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) { export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) {
return markedOptionsService; return markedOptionsService;
@ -364,11 +366,13 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
GtMdLgLayoutGapDirective, GtMdLgLayoutGapDirective,
GtMdLgShowHideDirective, GtMdLgShowHideDirective,
ColorPickerComponent, ColorPickerComponent,
ColorPickerPanelComponent,
ResourceAutocompleteComponent, ResourceAutocompleteComponent,
ToggleHeaderComponent, ToggleHeaderComponent,
ToggleOption, ToggleOption,
ToggleSelectComponent, ToggleSelectComponent,
UnitInputComponent, UnitInputComponent,
MaterialIconsComponent,
RuleChainSelectComponent RuleChainSelectComponent
], ],
imports: [ imports: [
@ -595,11 +599,13 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
GtMdLgLayoutGapDirective, GtMdLgLayoutGapDirective,
GtMdLgShowHideDirective, GtMdLgShowHideDirective,
ColorPickerComponent, ColorPickerComponent,
ColorPickerPanelComponent,
ResourceAutocompleteComponent, ResourceAutocompleteComponent,
ToggleHeaderComponent, ToggleHeaderComponent,
ToggleOption, ToggleOption,
ToggleSelectComponent, ToggleSelectComponent,
UnitInputComponent, UnitInputComponent,
MaterialIconsComponent,
RuleChainSelectComponent 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.

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

@ -68,7 +68,8 @@
"less": "Less", "less": "Less",
"skip": "Skip", "skip": "Skip",
"send": "Send", "send": "Send",
"reset": "Reset" "reset": "Reset",
"show-more": "Show more"
}, },
"aggregation": { "aggregation": {
"aggregation": "Aggregation", "aggregation": "Aggregation",
@ -5500,11 +5501,17 @@
} }
} }
}, },
"color": {
"color": "Color"
},
"icon": { "icon": {
"icon": "Icon", "icon": "Icon",
"icons": "Icons",
"select-icon": "Select icon", "select-icon": "Select icon",
"material-icons": "Material icons", "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": {
"phone-input-label": "Phone number", "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 * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
@import './scss/constants';
.tb-default, .tb-dark { .tb-default, .tb-dark {
.tb-form-panel { .tb-form-panel {
box-shadow: 0 0 10px 6px rgba(11, 17, 51, 0.04); box-shadow: 0 0 10px 6px rgba(11, 17, 51, 0.04);
@ -128,14 +131,11 @@
} }
.tb-form-row { .tb-form-row {
height: 100%; height: 100%;
padding-top: 7px;
padding-bottom: 7px;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
gap: 16px; gap: 16px;
padding-left: 16px; padding: 7px 7px 7px 16px;
padding-right: 12px;
border: 1px solid rgba(0, 0, 0, 0.12); border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 6px; border-radius: 6px;
&.same-padding { &.same-padding {
@ -177,6 +177,13 @@
opacity: 0; 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) { &:not(.mat-mdc-form-field-has-icon-suffix) {
.mat-mdc-text-field-wrapper { .mat-mdc-text-field-wrapper {
&.mdc-text-field--outlined, &:not(.mdc-text-field--outlined) { &.mdc-text-field--outlined, &:not(.mdc-text-field--outlined) {
@ -186,7 +193,6 @@
} }
.mat-mdc-text-field-wrapper { .mat-mdc-text-field-wrapper {
&.mdc-text-field--outlined, &:not(.mdc-text-field--outlined) { &.mdc-text-field--outlined, &:not(.mdc-text-field--outlined) {
padding-left: 12px;
&:not(.mdc-text-field--focused):not(.mdc-text-field--disabled):not(:hover) { &:not(.mdc-text-field--focused):not(.mdc-text-field--disabled):not(:hover) {
.mdc-notched-outline__leading, .mdc-notched-outline__trailing { .mdc-notched-outline__leading, .mdc-notched-outline__trailing {
border-color: rgba(0, 0, 0, 0.12); border-color: rgba(0, 0, 0, 0.12);
@ -203,7 +209,7 @@
line-height: 20px; line-height: 20px;
} }
} }
.mat-mdc-form-field-icon-suffix { .mat-mdc-form-field-icon-prefix, .mat-mdc-form-field-icon-suffix {
height: 40px; height: 40px;
font-size: 14px; font-size: 14px;
line-height: 40px; line-height: 40px;
@ -218,6 +224,19 @@
height: 20px; height: 20px;
font-size: 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 { > .mat-icon {
width: 20px; 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 { .tb-form-table {
@ -305,35 +340,80 @@
.tb-prompt { .tb-prompt {
height: 38px; height: 38px;
} }
}
.tb-form-table-row { .tb-form-table-row {
height: 38px; height: 38px;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: 12px; gap: 12px;
padding-left: 12px; padding-left: 12px;
&.tb-draggable { &.tb-draggable {
gap: 0; gap: 0;
padding-left: 0; padding-left: 0;
background: #fff; background: #fff;
} }
&-cell-buttons { &-cell-buttons {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
button.mat-mdc-icon-button.mat-mdc-button-base { button.mat-mdc-icon-button.mat-mdc-button-base {
padding: 7px; padding: 7px;
width: 38px; width: 38px;
height: 38px; height: 38px;
.mat-icon { .mat-icon {
color: rgba(0, 0, 0, 0.38); color: rgba(0, 0, 0, 0.38);
} }
&.tb-hidden { &.tb-hidden {
visibility: 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