diff --git a/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.ts b/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.ts
index 580a8fb891..6628d14439 100644
--- a/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.ts
+++ b/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.ts
@@ -33,6 +33,7 @@ import { FloatLabelType, MatFormFieldAppearance, SubscriptSizing } from '@angula
import { coerceArray, coerceBoolean } from '@shared/decorators/coercion';
import { PageLink } from '@shared/models/page/page-link';
import { PageData } from '@shared/models/page/page-data';
+import { UtilsService } from '@core/services/utils.service';
@Component({
selector: 'tb-entity-subtype-list',
@@ -129,6 +130,7 @@ export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit,
private edgeService: EdgeService,
private entityViewService: EntityViewService,
private alarmService: AlarmService,
+ private utils: UtilsService,
private fb: FormBuilder) {
this.entitySubtypeListFormGroup = this.fb.group({
entitySubtypeList: [this.entitySubtypeList, this.required ? [Validators.required] : []],
@@ -298,7 +300,7 @@ export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit,
} else {
result = subTypes.filter(subType => searchText ? subType.toUpperCase().startsWith(searchText.toUpperCase()) : true);
}
- if (!result.length) {
+ if (!result.length && searchText.length) {
result = [searchText];
}
return result;
@@ -372,4 +374,8 @@ export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit,
}, 0);
}
+ customTranslate(entity: string) {
+ return this.utils.customTranslation(entity, entity);
+ }
+
}
diff --git a/ui-ngx/src/app/shared/components/markdown-editor.component.ts b/ui-ngx/src/app/shared/components/markdown-editor.component.ts
index 4747254011..9a775844b8 100644
--- a/ui-ngx/src/app/shared/components/markdown-editor.component.ts
+++ b/ui-ngx/src/app/shared/components/markdown-editor.component.ts
@@ -14,11 +14,23 @@
/// limitations under the License.
///
-import { Component, ElementRef, forwardRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
+import {
+ ChangeDetectorRef,
+ Component,
+ ElementRef,
+ forwardRef,
+ Input,
+ OnDestroy,
+ OnInit,
+ ViewChild,
+ ViewEncapsulation
+} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Ace } from 'ace-builds';
-import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { getAce } from '@shared/models/ace/ace.models';
+import { ResizeObserver } from '@juggle/resize-observer';
+import { coerceBoolean } from '@shared/decorators/coercion';
+import { CancelAnimationFrame, RafService } from '@core/services/raf.service';
@Component({
selector: 'tb-markdown-editor',
@@ -30,7 +42,8 @@ import { getAce } from '@shared/models/ace/ace.models';
useExisting: forwardRef(() => MarkdownEditorComponent),
multi: true
}
- ]
+ ],
+ encapsulation: ViewEncapsulation.None
})
export class MarkdownEditorComponent implements OnInit, ControlValueAccessor, OnDestroy {
@@ -42,11 +55,13 @@ export class MarkdownEditorComponent implements OnInit, ControlValueAccessor, On
@Input() helpId: string;
+ @Input()
+ @coerceBoolean()
+ required: boolean;
+
@ViewChild('markdownEditor', {static: true})
markdownEditorElmRef: ElementRef;
- private markdownEditor: Ace.Editor;
-
editorMode = true;
fullscreen = false;
@@ -54,22 +69,15 @@ export class MarkdownEditorComponent implements OnInit, ControlValueAccessor, On
markdownValue: string;
renderValue: string;
- ignoreChange = false;
-
- private propagateChange = null;
-
- private requiredValue: boolean;
+ private markdownEditor: Ace.Editor;
+ private ignoreChange = false;
- get required(): boolean {
- return this.requiredValue;
- }
+ private editorResize$: ResizeObserver;
+ private editorsResizeCaf: CancelAnimationFrame;
+ private propagateChange: (value: any) => void = () => {};
- @Input()
- set required(value: boolean) {
- this.requiredValue = coerceBooleanProperty(value);
- }
-
- constructor() {
+ constructor(private cd: ChangeDetectorRef,
+ private raf: RafService) {
}
ngOnInit(): void {
@@ -100,6 +108,10 @@ export class MarkdownEditorComponent implements OnInit, ControlValueAccessor, On
this.updateView();
}
});
+ this.editorResize$ = new ResizeObserver(() => {
+ this.onAceEditorResize();
+ });
+ this.editorResize$.observe(editorElement);
}
);
@@ -107,6 +119,13 @@ export class MarkdownEditorComponent implements OnInit, ControlValueAccessor, On
}
ngOnDestroy(): void {
+ if (this.editorResize$) {
+ this.editorResize$.disconnect();
+ }
+ if (this.editorsResizeCaf) {
+ this.editorsResizeCaf();
+ this.editorsResizeCaf = null;
+ }
if (this.markdownEditor) {
this.markdownEditor.destroy();
}
@@ -134,15 +153,6 @@ export class MarkdownEditorComponent implements OnInit, ControlValueAccessor, On
}
}
- updateView() {
- const editorValue = this.markdownEditor.getValue();
- if (this.markdownValue !== editorValue) {
- this.markdownValue = editorValue;
- this.renderValue = this.markdownValue ? this.markdownValue : ' ';
- this.propagateChange(this.markdownValue);
- }
- }
-
onFullscreen() {
if (this.markdownEditor) {
setTimeout(() => {
@@ -159,4 +169,25 @@ export class MarkdownEditorComponent implements OnInit, ControlValueAccessor, On
}, 0);
}
}
+
+ private updateView() {
+ const editorValue = this.markdownEditor.getValue();
+ if (this.markdownValue !== editorValue) {
+ this.markdownValue = editorValue;
+ this.renderValue = this.markdownValue ? this.markdownValue : ' ';
+ this.propagateChange(this.markdownValue);
+ this.cd.markForCheck();
+ }
+ }
+
+ private onAceEditorResize() {
+ if (this.editorsResizeCaf) {
+ this.editorsResizeCaf();
+ this.editorsResizeCaf = null;
+ }
+ this.editorsResizeCaf = this.raf.raf(() => {
+ this.markdownEditor.resize();
+ this.markdownEditor.renderer.updateFull();
+ });
+ }
}
diff --git a/ui-ngx/src/app/shared/components/tb-error.component.ts b/ui-ngx/src/app/shared/components/tb-error.component.ts
index 5ddd2d7896..c0055df1f1 100644
--- a/ui-ngx/src/app/shared/components/tb-error.component.ts
+++ b/ui-ngx/src/app/shared/components/tb-error.component.ts
@@ -14,17 +14,17 @@
/// limitations under the License.
///
-import { Component, Input } from '@angular/core';
+import { ChangeDetectorRef, Component, Input } from '@angular/core';
import { animate, state, style, transition, trigger } from '@angular/animations';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({
selector: 'tb-error',
template: `
-
-
- {{message}}
-
+
+
+ {{message}}
+
`,
styles: [`
@@ -36,21 +36,23 @@ import { coerceBoolean } from '@shared/decorators/coercion';
trigger('animation', [
state('show', style({
opacity: 1,
+ transform: 'translateY(0)'
})),
state('hide', style({
opacity: 0,
transform: 'translateY(-1rem)'
})),
- transition('show => hide', animate('200ms ease-out')),
- transition('* => show', animate('200ms ease-in'))
-
+ transition('* <=> *', animate('200ms ease-out'))
]),
]
})
export class TbErrorComponent {
- errorValue: any;
- state: any;
- message;
+ errorValue: string;
+ state = 'hide';
+ message: string;
+
+ constructor(private cd: ChangeDetectorRef) {
+ }
@Input()
@coerceBoolean()
@@ -58,15 +60,13 @@ export class TbErrorComponent {
@Input()
set error(value) {
- if (value && !this.message) {
- this.message = value;
- this.state = 'hide';
- setTimeout(() => {
- this.state = 'show';
- });
- } else {
+ if (this.errorValue !== value) {
this.errorValue = value;
+ if (value) {
+ this.message = value;
+ }
this.state = value ? 'show' : 'hide';
+ this.cd.markForCheck();
}
}
}
diff --git a/ui-ngx/src/app/shared/models/alarm.models.ts b/ui-ngx/src/app/shared/models/alarm.models.ts
index 49f780beeb..71d64f2df1 100644
--- a/ui-ngx/src/app/shared/models/alarm.models.ts
+++ b/ui-ngx/src/app/shared/models/alarm.models.ts
@@ -102,6 +102,8 @@ export interface Alarm extends BaseData
{
originator: EntityId;
severity: AlarmSeverity;
status: AlarmStatus;
+ acknowledged: boolean;
+ cleared: boolean;
startTs: number;
endTs: number;
ackTs: number;
@@ -181,6 +183,8 @@ export const simulatedAlarm: AlarmInfo = {
type: 'TEMPERATURE',
severity: AlarmSeverity.MAJOR,
status: AlarmStatus.ACTIVE_UNACK,
+ acknowledged: false,
+ cleared: false,
details: {
message: 'Temperature is high!'
},
diff --git a/ui-ngx/src/app/shared/models/constants.ts b/ui-ngx/src/app/shared/models/constants.ts
index 12469d6b90..4f904a4bf3 100644
--- a/ui-ngx/src/app/shared/models/constants.ts
+++ b/ui-ngx/src/app/shared/models/constants.ts
@@ -71,6 +71,13 @@ export const MediaBreakpoints = {
'md-lg': 'screen and (min-width: 960px) and (max-width: 1819px)'
};
+export const resolveBreakpoint = (breakpoint: string): string => {
+ if (MediaBreakpoints[breakpoint]) {
+ return MediaBreakpoints[breakpoint];
+ }
+ return breakpoint;
+};
+
export const helpBaseUrl = 'https://thingsboard.io';
export const HelpLinks = {
diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts
index fd99eb37ee..34829dd850 100644
--- a/ui-ngx/src/app/shared/models/widget.models.ts
+++ b/ui-ngx/src/app/shared/models/widget.models.ts
@@ -409,8 +409,8 @@ export interface Datasource {
export const datasourcesHasAggregation = (datasources?: Array): boolean => {
if (datasources) {
const foundDatasource = datasources.find(datasource => {
- const found = datasource.dataKeys && datasource.dataKeys.find(key => key.type === DataKeyType.timeseries &&
- key.aggregationType && key.aggregationType !== AggregationType.NONE);
+ const found = datasource.dataKeys && datasource.dataKeys.find(key => key?.type === DataKeyType.timeseries &&
+ key?.aggregationType && key.aggregationType !== AggregationType.NONE);
return !!found;
});
if (foundDatasource) {
diff --git a/ui-ngx/src/assets/dashboard/customer_user_home_page.json b/ui-ngx/src/assets/dashboard/customer_user_home_page.json
index 7c79162d5b..aef3367ff3 100644
--- a/ui-ngx/src/assets/dashboard/customer_user_home_page.json
+++ b/ui-ngx/src/assets/dashboard/customer_user_home_page.json
@@ -461,7 +461,7 @@
"keyFilters": [
{
"key": {
- "type": "ATTRIBUTE",
+ "type": "SERVER_ATTRIBUTE",
"key": "active"
},
"valueType": "BOOLEAN",
@@ -493,7 +493,7 @@
"keyFilters": [
{
"key": {
- "type": "ATTRIBUTE",
+ "type": "SERVER_ATTRIBUTE",
"key": "active"
},
"valueType": "BOOLEAN",
@@ -569,4 +569,4 @@
},
"externalId": null,
"name": "Customer User Home Page"
-}
\ No newline at end of file
+}
diff --git a/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json b/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json
index a2edebb7c4..37e8d82e45 100644
--- a/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json
+++ b/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json
@@ -943,7 +943,7 @@
"keyFilters": [
{
"key": {
- "type": "ATTRIBUTE",
+ "type": "SERVER_ATTRIBUTE",
"key": "active"
},
"valueType": "BOOLEAN",
@@ -975,7 +975,7 @@
"keyFilters": [
{
"key": {
- "type": "ATTRIBUTE",
+ "type": "SERVER_ATTRIBUTE",
"key": "active"
},
"valueType": "BOOLEAN",
@@ -1051,4 +1051,4 @@
},
"externalId": null,
"name": "Tenant Administrator Home Page"
-}
\ No newline at end of file
+}
diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json
index c81b6f2c11..dde8b8a056 100644
--- a/ui-ngx/src/assets/locale/locale.constant-en_US.json
+++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json
@@ -5338,6 +5338,27 @@
"Ok": "Ok"
}
},
+ "doughnut": {
+ "total": "Total",
+ "layout": "Layout",
+ "layout-default": "Default",
+ "layout-with-total": "With total",
+ "auto-scale": "Auto scale",
+ "central-total-value": "Central total value",
+ "legend-position-top": "Top",
+ "legend-position-bottom": "Bottom",
+ "legend-position-left": "Left",
+ "legend-position-right": "Right",
+ "legend-label": "Label",
+ "legend-value": "Value",
+ "tooltip": "Tooltip",
+ "tooltip-value": "Value",
+ "tooltip-value-type-absolute": "Absolute",
+ "tooltip-value-type-percentage": "Percentage",
+ "tooltip-background-color": "Background color",
+ "tooltip-background-blur": "Background blur",
+ "doughnut-card-style": "Doughnut card style"
+ },
"entities-hierarchy": {
"hierarchy-data-settings": "Hierarchy data settings",
"relations-query-function": "Node relations query function",
@@ -6293,6 +6314,7 @@
"pagination": "Pagination",
"rows": "Rows",
"timeseries-column-error": "At least one timeseries column should be specified",
+ "alarm-column-error": "At least one alarm column should be specified",
"table-tabs": "Table tabs",
"show-cell-actions-menu-mobile": "Show cell actions dropdown menu in mobile mode"
},
diff --git a/ui-ngx/src/assets/widget/doughnut/default-layout.svg b/ui-ngx/src/assets/widget/doughnut/default-layout.svg
new file mode 100644
index 0000000000..8402b596fd
--- /dev/null
+++ b/ui-ngx/src/assets/widget/doughnut/default-layout.svg
@@ -0,0 +1,29 @@
+
diff --git a/ui-ngx/src/assets/widget/doughnut/horizontal-default-layout.svg b/ui-ngx/src/assets/widget/doughnut/horizontal-default-layout.svg
new file mode 100644
index 0000000000..4730aba3a0
--- /dev/null
+++ b/ui-ngx/src/assets/widget/doughnut/horizontal-default-layout.svg
@@ -0,0 +1,29 @@
+
diff --git a/ui-ngx/src/assets/widget/doughnut/horizontal-with-total-layout.svg b/ui-ngx/src/assets/widget/doughnut/horizontal-with-total-layout.svg
new file mode 100644
index 0000000000..20cbbdbf3e
--- /dev/null
+++ b/ui-ngx/src/assets/widget/doughnut/horizontal-with-total-layout.svg
@@ -0,0 +1,28 @@
+
diff --git a/ui-ngx/src/assets/widget/doughnut/with-total-layout.svg b/ui-ngx/src/assets/widget/doughnut/with-total-layout.svg
new file mode 100644
index 0000000000..bf3b9cf2d6
--- /dev/null
+++ b/ui-ngx/src/assets/widget/doughnut/with-total-layout.svg
@@ -0,0 +1,28 @@
+
diff --git a/ui-ngx/src/styles.scss b/ui-ngx/src/styles.scss
index ee6f6374cf..40ab6b3b13 100644
--- a/ui-ngx/src/styles.scss
+++ b/ui-ngx/src/styles.scss
@@ -268,7 +268,7 @@ pre.tb-highlight {
letter-spacing: normal;
}
-.tb-timewindow-panel, .tb-legend-config-panel, .tb-filter-panel {
+.tb-timewindow-panel, .tb-legend-config-panel, .tb-filter-panel, .tb-panel-container {
overflow: hidden;
background: #fff;
border-radius: 4px;
diff --git a/ui-ngx/yarn.lock b/ui-ngx/yarn.lock
index 37ae52a5ec..50815ce794 100644
--- a/ui-ngx/yarn.lock
+++ b/ui-ngx/yarn.lock
@@ -5360,6 +5360,14 @@ ecc-jsbn@~0.1.1:
jsbn "~0.1.0"
safer-buffer "^2.1.0"
+echarts@^5.4.3:
+ version "5.4.3"
+ resolved "https://registry.yarnpkg.com/echarts/-/echarts-5.4.3.tgz#f5522ef24419164903eedcfd2b506c6fc91fb20c"
+ integrity sha512-mYKxLxhzy6zyTi/FaEbJMOZU1ULGEQHaeIeuMR5L+JnJTpz+YR03mnnpBhbR4+UYJAgiXgpyTVLffPAjOTLkZA==
+ dependencies:
+ tslib "2.3.0"
+ zrender "5.4.4"
+
editorconfig@^0.15.3:
version "0.15.3"
resolved "https://registry.yarnpkg.com/editorconfig/-/editorconfig-0.15.3.tgz#bef84c4e75fb8dcb0ce5cee8efd51c15999befc5"
@@ -10329,6 +10337,11 @@ tsconfig-paths@^4.1.0:
minimist "^1.2.6"
strip-bom "^3.0.0"
+tslib@2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e"
+ integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==
+
tslib@2.5.0, tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.2.0, tslib@^2.3.0, tslib@^2.3.1, tslib@^2.4.0, tslib@^2.4.1, tslib@^2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf"
@@ -11075,3 +11088,10 @@ zone.js@~0.13.0:
integrity sha512-7m3hNNyswsdoDobCkYNAy5WiUulkMd3+fWaGT9ij6iq3Zr/IwJo4RMCYPSDjT+r7tnPErmY9sZpKhWQ8S5k6XQ==
dependencies:
tslib "^2.3.0"
+
+zrender@5.4.4:
+ version "5.4.4"
+ resolved "https://registry.yarnpkg.com/zrender/-/zrender-5.4.4.tgz#8854f1d95ecc82cf8912f5a11f86657cb8c9e261"
+ integrity sha512-0VxCNJ7AGOMCWeHVyTrGzUgrK4asT4ml9PEkeGirAkKNYXYzoPJCLvmyfdoOXcjTHPs10OZVMfD1Rwg16AZyYw==
+ dependencies:
+ tslib "2.3.0"