diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.scss
index 193fb5e793..20345a97f4 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.scss
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.scss
@@ -83,13 +83,105 @@
color: rgba(0, 0, 0, 0.87);
}
- .tb-get-started .mat-vertical-content p {
- font-style: normal;
- font-weight: 400;
- font-size: 14px;
- line-height: 20px;
- letter-spacing: 0.2px;
- color: rgba(0, 0, 0, 0.54);
+ .tb-get-started .mat-vertical-content {
+ p, li {
+ font-style: normal;
+ font-weight: 400;
+ font-size: 14px;
+ line-height: 20px;
+ letter-spacing: 0.2px;
+ color: rgba(0, 0, 0, 0.54);
+ em {
+ font-style: normal;
+ font-weight: 500;
+ font-size: 13px;
+ color: rgba(0, 0, 0, 0.38);
+ }
+ }
+ ul {
+ padding-inline-start: 20px;
+ }
+ .tb-bordered-content {
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+ border: 1px solid rgba(0, 0, 0, 0.05);
+ border-radius: 8px;
+ padding: 16px;
+ p {
+ margin-top: 16px;
+ margin-bottom: 8px;
+ }
+ }
+ .tb-markdown-view {
+ .tb-getting-started-code {
+ .code-wrapper {
+ padding: 0;
+ pre[class*=language-] {
+ margin: 0;
+ padding: 9px 38px 9px 16px;
+ background: rgba(0, 0, 0, 0.03);
+ border-radius: 6px;
+ border: none;
+ }
+ code[class*="language-"], pre[class*="language-"] {
+ font-family: 'Roboto';
+ font-style: normal;
+ font-weight: 400;
+ font-size: 12px;
+ line-height: 16px;
+ letter-spacing: 0.25px;
+ color: rgba(0, 0, 0, 0.38);
+ overflow: hidden;
+ white-space: break-spaces;
+ word-break: break-all;
+ & * {
+ color: rgba(0, 0, 0, 0.38);
+ cursor: inherit;
+ background: transparent;
+ }
+ }
+ button.clipboard-btn {
+ right: 0;
+ height: 34px;
+ p, div {
+ background: transparent;
+ }
+ p {
+ margin: 0;
+ padding: 7px;
+ color: #305680;
+ }
+ div {
+ top: 0;
+ padding: 8px;
+ height: 34px;
+ width: 34px;
+ img {
+ display: none;
+ }
+ &:after {
+ content: "";
+ position: initial;
+ display: block;
+ width: 18px;
+ height: 18px;
+ background: #305680;
+ -webkit-mask-image: url(/assets/copy-code-icon.svg);
+ -webkit-mask-repeat: no-repeat;
+ mask-image: url(/assets/copy-code-icon.svg);
+ mask-repeat: no-repeat;
+ }
+ }
+ }
+ }
+ }
+ }
+ @media #{$mat-md-lg} {
+ .tb-bordered-content {
+ padding: 4px;
+ }
+ }
}
@media #{$mat-md-lg} {
@@ -107,15 +199,16 @@
line-height: 16px;
}
- .tb-get-started .mat-vertical-content p {
- font-size: 12px;
- line-height: 16px;
- letter-spacing: 0.25px;
+ .tb-get-started .mat-vertical-content {
+ p, li {
+ font-size: 12px;
+ line-height: 16px;
+ letter-spacing: 0.25px;
+ }
}
.tb-get-started .mat-vertical-content {
padding: 0 16px 16px 16px;
}
-
}
}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.ts
index 9c07e62978..7566ae8579 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.ts
@@ -27,7 +27,7 @@ import {
} from '@home/components/widget/lib/home-page/getting-started-completed-dialog.component';
import { GettingStarted } from '@shared/models/user-settings.models';
import { CdkStep, StepperSelectionEvent } from '@angular/cdk/stepper';
-import { isUndefined } from '@core/utils';
+import { baseUrl, isUndefined } from '@core/utils';
import { MatStepper } from '@angular/material/stepper';
import { first } from 'rxjs/operators';
import { Authority } from '@shared/models/authority.enum';
@@ -54,6 +54,8 @@ export class GettingStartedWidgetComponent extends PageComponent implements OnIn
};
allCompleted = false;
+ baseUrl = baseUrl();
+
constructor(protected store: Store
,
private cd: ChangeDetectorRef,
private userSettingsService: UserSettingsService,
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page-widget.scss b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page-widget.scss
new file mode 100644
index 0000000000..912ebbb63e
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page-widget.scss
@@ -0,0 +1,70 @@
+/**
+ * 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 "../../../../../../../scss/constants";
+
+:host {
+
+ .tb-card-content {
+ width: 100%;
+ height: 100%;
+ }
+
+ .tb-title {
+ font-style: normal;
+ 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;
+ }
+ }
+
+ .tb-title-link {
+ font-style: normal;
+ font-weight: 500;
+ font-size: 14px;
+ line-height: 20px;
+ letter-spacing: 0.25px;
+ color: rgba(0, 0, 0, 0.54);
+ position: relative;
+ border-bottom: none;
+ @media #{$mat-md-lg} {
+ font-size: 12px;
+ line-height: 16px;
+ }
+ &:hover, &:focus {
+ border-bottom: none;
+ }
+ &::after {
+ content: 'arrow_forward';
+ display: inline-block;
+ transform: rotate(315deg);
+ font-family: 'Material Icons';
+ font-weight: normal;
+ font-style: normal;
+ font-size: 18px;
+ color: rgba(0, 0, 0, 0.12);
+ vertical-align: bottom;
+ margin-left: 6px;
+ }
+ &:hover::after {
+ color: inherit;
+ }
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page-widgets.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page-widgets.module.ts
index 3be30dad30..84aeef4118 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page-widgets.module.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page-widgets.module.ts
@@ -23,11 +23,19 @@ import { VersionInfoComponent } from '@home/components/widget/lib/home-page/vers
import { DocLinksWidgetComponent } from '@home/components/widget/lib/home-page/doc-links-widget.component';
import { DocLinkComponent } from '@home/components/widget/lib/home-page/doc-link.component';
import { AddDocLinkDialogComponent } from '@home/components/widget/lib/home-page/add-doc-link-dialog.component';
-import { EditDocLinksDialogComponent } from '@home/components/widget/lib/home-page/edit-doc-links-dialog.component';
+import { EditLinksDialogComponent } from '@home/components/widget/lib/home-page/edit-links-dialog.component';
import { GettingStartedWidgetComponent } from '@home/components/widget/lib/home-page/getting-started-widget.component';
import {
GettingStartedCompletedDialogComponent
} from '@home/components/widget/lib/home-page/getting-started-completed-dialog.component';
+import { ToggleHeaderComponent } from '@home/components/widget/lib/home-page/toggle-header.component';
+import { UsageInfoWidgetComponent } from '@home/components/widget/lib/home-page/usage-info-widget.component';
+import { QuickLinksWidgetComponent } from '@home/components/widget/lib/home-page/quick-links-widget.component';
+import { QuickLinkComponent } from '@home/components/widget/lib/home-page/quick-link.component';
+import { AddQuickLinkDialogComponent } from '@home/components/widget/lib/home-page/add-quick-link-dialog.component';
+import {
+ RecentDashboardsWidgetComponent
+} from '@home/components/widget/lib/home-page/recent-dashboards-widget.component';
@NgModule({
declarations:
@@ -38,9 +46,15 @@ import {
DocLinksWidgetComponent,
DocLinkComponent,
AddDocLinkDialogComponent,
- EditDocLinksDialogComponent,
+ EditLinksDialogComponent,
GettingStartedWidgetComponent,
- GettingStartedCompletedDialogComponent
+ GettingStartedCompletedDialogComponent,
+ ToggleHeaderComponent,
+ UsageInfoWidgetComponent,
+ QuickLinksWidgetComponent,
+ QuickLinkComponent,
+ AddQuickLinkDialogComponent,
+ RecentDashboardsWidgetComponent
],
imports: [
CommonModule,
@@ -53,9 +67,15 @@ import {
DocLinksWidgetComponent,
DocLinkComponent,
AddDocLinkDialogComponent,
- EditDocLinksDialogComponent,
+ EditLinksDialogComponent,
GettingStartedWidgetComponent,
- GettingStartedCompletedDialogComponent
+ GettingStartedCompletedDialogComponent,
+ ToggleHeaderComponent,
+ UsageInfoWidgetComponent,
+ QuickLinksWidgetComponent,
+ QuickLinkComponent,
+ AddQuickLinkDialogComponent,
+ RecentDashboardsWidgetComponent
]
})
export class HomePageWidgetsModule { }
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page.scss b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page.scss
new file mode 100644
index 0000000000..a488d77c6e
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page.scss
@@ -0,0 +1,33 @@
+/**
+ * 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-autocomplete.tb-quick-links {
+ .mat-mdc-option {
+ border-bottom: none;
+ font-weight: 400;
+ font-size: 14px;
+ line-height: 20px;
+ letter-spacing: 0.2px;
+ color: rgba(0, 0, 0, 0.76);
+ .mat-icon {
+ margin-right: 10px;
+ color: rgba(0, 0, 0, 0.54);
+ font-size: 20px;
+ width: 20px;
+ height: 20px;
+ }
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-link.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/link.component.scss
similarity index 93%
rename from ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-link.component.scss
rename to ui-ngx/src/app/modules/home/components/widget/lib/home-page/link.component.scss
index 1d9de4fc38..3b71124d4f 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-link.component.scss
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/link.component.scss
@@ -14,7 +14,7 @@
* limitations under the License.
*/
:host {
- .tb-edit-doc-link {
+ .tb-edit-link {
padding: 16px;
border: 1px solid rgba(0, 0, 0, 0.05);
box-shadow: 0 5px 16px rgba(0, 0, 0, 0.04);
@@ -23,7 +23,7 @@
border: 1px solid rgba(48, 86, 128, 0.32);
}
}
- .tb-doc-link {
+ .tb-link {
height: 55px;
display: flex;
flex-direction: column;
@@ -33,18 +33,18 @@
border: 1px solid rgba(0, 0, 0, 0.05);
box-shadow: 0 5px 16px rgba(0, 0, 0, 0.04);
border-radius: 10px;
- .tb-doc-container {
+ .tb-link-container {
display: flex;
flex-direction: row;
align-items: center;
- .tb-doc-icon-container {
+ .tb-link-icon-container {
height: 40px;
padding: 8px;
background: #F3F6FA;
border-radius: 6px;
margin-right: 8px;
}
- .tb-doc-text {
+ .tb-link-text {
font-weight: 400;
font-size: 14px;
line-height: 20px;
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-links-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/links-widget.component.scss
similarity index 73%
rename from ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-links-widget.component.scss
rename to ui-ngx/src/app/modules/home/components/widget/lib/home-page/links-widget.component.scss
index ddc5a40eec..3e52d6451d 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-links-widget.component.scss
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/links-widget.component.scss
@@ -17,11 +17,6 @@
@import "../../../../../../../scss/constants";
:host {
- .tb-card-content {
- width: 100%;
- height: 100%;
- }
-
.tb-card-header {
display: flex;
flex-direction: row;
@@ -29,39 +24,6 @@
align-items: flex-end;
}
- .tb-title-link {
- font-style: normal;
- font-weight: 500;
- font-size: 14px;
- line-height: 20px;
- letter-spacing: 0.25px;
- color: rgba(0, 0, 0, 0.54);
- position: relative;
- border-bottom: none;
- @media #{$mat-md-lg} {
- font-size: 12px;
- line-height: 16px;
- }
- &:hover, &:focus {
- border-bottom: none;
- }
- &::after {
- content: 'arrow_forward';
- display: inline-block;
- transform: rotate(315deg);
- font-family: 'Material Icons';
- font-weight: normal;
- font-style: normal;
- font-size: 18px;
- color: rgba(0, 0, 0, 0.12);
- vertical-align: bottom;
- margin-left: 6px;
- }
- &:hover::after {
- color: inherit;
- }
- }
-
.tb-title-icon {
color: rgba(0, 0, 0, 0.38);
width: 32px;
@@ -69,15 +31,15 @@
padding: 4px;
}
- .tb-docs-list {
+ .tb-links-list {
overflow: auto;
}
- .mat-grid-tile.tb-docs-tile {
+ .mat-grid-tile.tb-links-tile {
overflow: visible;
}
- .tb-doc-button {
+ .tb-link-button {
height: 55px;
display: flex;
overflow: hidden;
@@ -96,11 +58,11 @@
border: 1px solid rgba(0, 0, 0, 0.12);
box-shadow: 0 4px 10px rgba(23, 33, 90, 0.08);
}
- .tb-doc-container {
+ .tb-link-container {
display: flex;
flex-direction: row;
align-items: center;
- .tb-doc-icon-container {
+ .tb-link-icon-container {
height: 40px;
padding: 8px;
background: #F3F6FA;
@@ -110,7 +72,7 @@
display: none;
}
}
- .tb-doc-text {
+ .tb-link-text {
font-weight: 400;
font-size: 14px;
line-height: 20px;
@@ -127,7 +89,7 @@
}
}
- .tb-add-doc-button {
+ .tb-add-link-button {
height: 55px;
cursor: pointer;
display: flex;
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-link.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-link.component.html
new file mode 100644
index 0000000000..0dec41e71d
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-link.component.html
@@ -0,0 +1,91 @@
+
+
+
+
+ widgets.quick-links.quick-link
+
+ {{ quickLink.icon }}
+
+
+
+
+ {{ link.icon }}
+
+
+
+
+
+ {{ translate.get('widgets.quick-links.no-links-matching', {name: searchText}) | async }}
+
+
+
+
+ {{ 'widgets.quick-links.quick-link-required' | translate }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ quickLink?.icon }}
+
+
+
{{ displayLinkFn(quickLink) }}
+
+
+
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-link.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-link.component.ts
new file mode 100644
index 0000000000..359ae4468d
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-link.component.ts
@@ -0,0 +1,318 @@
+///
+/// Copyright © 2016-2023 The Thingsboard Authors
+///
+/// Licensed under the Apache License, Version 2.0 (the "License");
+/// you may not use this file except in compliance with the License.
+/// You may obtain a copy of the License at
+///
+/// http://www.apache.org/licenses/LICENSE-2.0
+///
+/// Unless required by applicable law or agreed to in writing, software
+/// distributed under the License is distributed on an "AS IS" BASIS,
+/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+/// See the License for the specific language governing permissions and
+/// limitations under the License.
+///
+
+import {
+ Component,
+ ElementRef,
+ EventEmitter,
+ forwardRef,
+ Input,
+ OnInit,
+ Output,
+ SkipSelf,
+ ViewChild
+} from '@angular/core';
+import {
+ AbstractControl,
+ ControlValueAccessor,
+ FormGroupDirective,
+ NG_VALUE_ACCESSOR,
+ NgForm,
+ UntypedFormBuilder,
+ UntypedFormControl,
+ UntypedFormGroup,
+ ValidationErrors
+} from '@angular/forms';
+import { PageComponent } from '@shared/components/page.component';
+import { Store } from '@ngrx/store';
+import { AppState } from '@core/core.state';
+import { ErrorStateMatcher } from '@angular/material/core';
+import { MenuService } from '@core/services/menu.service';
+import { Observable, of } from 'rxjs';
+import { MenuSection } from '@core/services/menu.models';
+import { TranslateService } from '@ngx-translate/core';
+import { catchError, distinctUntilChanged, map, publishReplay, refCount, share, switchMap, tap } from 'rxjs/operators';
+import { PageLink } from '@shared/models/page/page-link';
+import { Direction } from '@shared/models/page/sort-order';
+import { emptyPageData, PageData } from '@shared/models/page/page-data';
+import { deepClone } from '@core/utils';
+
+@Component({
+ selector: 'tb-quick-link',
+ templateUrl: './quick-link.component.html',
+ styleUrls: ['./link.component.scss'],
+ providers: [
+ {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => QuickLinkComponent),
+ multi: true
+ },
+ {provide: ErrorStateMatcher, useExisting: QuickLinkComponent}
+ ]
+})
+export class QuickLinkComponent extends PageComponent implements OnInit, ControlValueAccessor, ErrorStateMatcher {
+
+ @Input()
+ disabled: boolean;
+
+ @Input()
+ addOnly = false;
+
+ @Input()
+ disableEdit = false;
+
+ @Output()
+ quickLinkAdded = new EventEmitter();
+
+ @Output()
+ quickLinkAddCanceled = new EventEmitter();
+
+ @Output()
+ quickLinkUpdated = new EventEmitter();
+
+ @Output()
+ quickLinkDeleted = new EventEmitter();
+
+ @Output()
+ editModeChanged = new EventEmitter();
+
+ @ViewChild('linkInput', {static: false}) linkInput: ElementRef;
+
+ filteredLinks: Observable>;
+
+ private allLinksObservable$: Observable> = null;
+
+ searchText = '';
+
+ editMode = false;
+ addMode = false;
+
+ quickLink: MenuSection;
+
+ private propagateChange = null;
+
+ public editQuickLinkFormGroup: UntypedFormGroup;
+
+ private submitted = false;
+
+ private dirty = false;
+
+ constructor(protected store: Store,
+ private fb: UntypedFormBuilder,
+ private menuService: MenuService,
+ public translate: TranslateService,
+ @SkipSelf() private errorStateMatcher: ErrorStateMatcher) {
+ super(store);
+ }
+
+ ngOnInit(): void {
+ this.addMode = this.addOnly;
+ this.editQuickLinkFormGroup = this.fb.group({
+ link: [null, [this.requiredLinkValidator]]
+ });
+ this.filteredLinks = this.editQuickLinkFormGroup.get('link').valueChanges
+ .pipe(
+ tap(value => {
+ let modelValue;
+ if (typeof value === 'string' || !value) {
+ modelValue = null;
+ } else {
+ modelValue = value;
+ }
+ this.updateView(modelValue);
+ }),
+ map(value => value ? (typeof value === 'string' ? value :
+ ((value as any).translated ? value.name : this.translate.instant(value.name))) : ''),
+ distinctUntilChanged(),
+ switchMap(name => this.fetchLinks(name) ),
+ share()
+ );
+ }
+
+ requiredLinkValidator(control: AbstractControl): ValidationErrors | null {
+ const value = control.value;
+ if (!value || typeof value === 'string') {
+ return {required: true};
+ }
+ return null;
+ }
+
+ isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean {
+ const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
+ const customErrorState = !!(control && control.invalid && this.submitted);
+ return originalErrorState || customErrorState;
+ }
+
+ registerOnChange(fn: any): void {
+ this.propagateChange = fn;
+ }
+
+ registerOnTouched(fn: any): void {
+ }
+
+ setDisabledState(isDisabled: boolean): void {
+ this.disabled = isDisabled;
+ if (isDisabled) {
+ this.editQuickLinkFormGroup.disable({emitEvent: false});
+ } else {
+ this.editQuickLinkFormGroup.enable({emitEvent: false});
+ }
+ }
+
+ writeValue(value: string): void {
+ if (value) {
+ this.menuService.menuLinkById(value).subscribe(
+ (link) => {
+ this.quickLink = link;
+ this.editQuickLinkFormGroup.get('link').patchValue(
+ link, {emitEvent: false}
+ );
+ }
+ );
+ } else {
+ this.quickLink = null;
+ this.editQuickLinkFormGroup.get('link').patchValue(
+ value, {emitEvent: false}
+ );
+ if (!this.editQuickLinkFormGroup.valid) {
+ this.addMode = true;
+ this.editModeChanged.emit(true);
+ }
+ }
+ this.dirty = true;
+ }
+
+ updateView(value: MenuSection | null) {
+ if (this.quickLink !== value) {
+ this.quickLink = value;
+ }
+ }
+
+ displayLinkFn = (link?: MenuSection): string | undefined =>
+ link ? ((link as any).translated ? link.name : this.translate.instant(link.fullName || link.name)) : undefined;
+
+ fetchLinks(searchText?: string): Observable> {
+ this.searchText = searchText;
+ const pageLink = new PageLink(100, 0, searchText, {
+ property: 'name',
+ direction: Direction.ASC
+ });
+ return this.getLinks(pageLink).pipe(
+ catchError(() => of(emptyPageData())),
+ map(pageData => pageData.data)
+ );
+ }
+
+ getLinks(pageLink: PageLink): Observable> {
+ return this.allLinks().pipe(
+ map((links) => pageLink.filterData(links))
+ );
+ }
+
+ allLinks(): Observable> {
+ if (this.allLinksObservable$ === null) {
+ this.allLinksObservable$ = this.menuService.availableMenuLinks().pipe(
+ map((links) => {
+ const result = deepClone(links);
+ for (const link of result) {
+ link.name = this.translate.instant(link.fullName || link.name);
+ (link as any).translated = true;
+ }
+ return result;
+ }),
+ publishReplay(1),
+ refCount()
+ );
+ }
+ return this.allLinksObservable$;
+ }
+
+ onFocus() {
+ if (this.dirty) {
+ this.editQuickLinkFormGroup.get('link').updateValueAndValidity({onlySelf: true});
+ this.dirty = false;
+ }
+ }
+
+ clear() {
+ this.editQuickLinkFormGroup.get('link').patchValue('');
+ setTimeout(() => {
+ this.linkInput.nativeElement.blur();
+ this.linkInput.nativeElement.focus();
+ }, 0);
+ }
+
+ switchToEditMode() {
+ if (!this.disableEdit && !this.editMode) {
+ this.submitted = false;
+ this.editQuickLinkFormGroup.get('link').patchValue(
+ this.quickLink, {emitEvent: false}
+ );
+ this.editMode = true;
+ this.editModeChanged.emit(true);
+ }
+ }
+
+ apply() {
+ this.submitted = true;
+ this.updateModel();
+ if (this.quickLink) {
+ this.editMode = false;
+ this.editModeChanged.emit(false);
+ this.quickLinkUpdated.next(this.quickLink.id);
+ }
+ }
+
+ cancelEdit() {
+ this.submitted = false;
+ this.editMode = false;
+ this.editModeChanged.emit(false);
+ }
+
+ add() {
+ this.submitted = true;
+ this.updateModel();
+ if (this.quickLink) {
+ if (!this.addOnly) {
+ this.addMode = false;
+ this.editModeChanged.emit(false);
+ }
+ this.quickLinkAdded.next(this.quickLink.id);
+ }
+ }
+
+ cancelAdd() {
+ this.editModeChanged.emit(false);
+ this.quickLinkAddCanceled.emit();
+ }
+
+ delete() {
+ this.quickLinkDeleted.emit();
+ }
+
+ isEditing() {
+ return this.editMode || this.addMode;
+ }
+
+ private updateModel() {
+ if (this.quickLink) {
+ this.propagateChange(this.quickLink.id);
+ } else {
+ this.propagateChange(null);
+ }
+ }
+
+}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-links-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-links-widget.component.html
new file mode 100644
index 0000000000..95c2818f1c
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-links-widget.component.html
@@ -0,0 +1,51 @@
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-links-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-links-widget.component.ts
new file mode 100644
index 0000000000..1b16601a7c
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-links-widget.component.ts
@@ -0,0 +1,168 @@
+///
+/// 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 { ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core';
+import { PageComponent } from '@shared/components/page.component';
+import { Store } from '@ngrx/store';
+import { AppState } from '@core/core.state';
+import { Authority } from '@shared/models/authority.enum';
+import { map, Observable, of, Subscription } from 'rxjs';
+import { QuickLinks } from '@shared/models/user-settings.models';
+import { UserSettingsService } from '@core/http/user-settings.service';
+import { getCurrentAuthUser } from '@core/auth/auth.selectors';
+import { WidgetContext } from '@home/models/widget-component.models';
+import { MatDialog } from '@angular/material/dialog';
+import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout';
+import { MediaBreakpoints } from '@shared/models/constants';
+import { MenuService } from '@core/services/menu.service';
+import { MenuSection } from '@core/services/menu.models';
+import { AddQuickLinkDialogComponent } from '@home/components/widget/lib/home-page/add-quick-link-dialog.component';
+import {
+ EditLinksDialogComponent,
+ EditLinksDialogData
+} from '@home/components/widget/lib/home-page/edit-links-dialog.component';
+
+const defaultQuickLinksMap = new Map(
+ [
+ [Authority.SYS_ADMIN, {
+ links: ['tenants', 'tenant_profiles']
+ }],
+ [Authority.TENANT_ADMIN, {
+ links: ['alarms', 'dashboards', 'devices']
+ }],
+ [Authority.CUSTOMER_USER, {
+ links: ['alarms', 'dashboards', 'devices']
+ }]
+ ]
+);
+
+interface QuickLinksWidgetSettings {
+ columns: number;
+}
+
+@Component({
+ selector: 'tb-quick-links-widget',
+ templateUrl: './quick-links-widget.component.html',
+ styleUrls: ['./home-page-widget.scss', './links-widget.component.scss']
+})
+export class QuickLinksWidgetComponent extends PageComponent implements OnInit, OnDestroy {
+
+ @Input()
+ ctx: WidgetContext;
+
+ settings: QuickLinksWidgetSettings;
+ columns: number;
+ rowHeight = '55px';
+ gutterSize = '12px';
+
+ quickLinks: QuickLinks;
+ authUser = getCurrentAuthUser(this.store);
+
+ private observeBreakpointSubscription: Subscription;
+
+ constructor(protected store: Store,
+ private cd: ChangeDetectorRef,
+ private userSettingsService: UserSettingsService,
+ private dialog: MatDialog,
+ private menuService: MenuService,
+ private breakpointObserver: BreakpointObserver) {
+ super(store);
+ }
+
+ ngOnInit() {
+ this.settings = this.ctx.settings;
+ this.columns = this.settings.columns || 3;
+ const isMdLg = this.breakpointObserver.isMatched(MediaBreakpoints['md-lg']);
+ this.rowHeight = isMdLg ? '18px' : '55px';
+ this.gutterSize = isMdLg ? '8px' : '12px';
+ this.observeBreakpointSubscription = this.breakpointObserver
+ .observe(MediaBreakpoints['md-lg'])
+ .subscribe((state: BreakpointState) => {
+ if (state.matches) {
+ this.rowHeight = '18px';
+ this.gutterSize = '8px';
+ } else {
+ this.rowHeight = '55px';
+ this.gutterSize = '12px';
+ }
+ this.cd.markForCheck();
+ }
+ );
+ this.loadQuickLinks();
+ }
+
+ ngOnDestroy() {
+ if (this.observeBreakpointSubscription) {
+ this.observeBreakpointSubscription.unsubscribe();
+ }
+ super.ngOnDestroy();
+ }
+
+ menuLinks$(): Observable> {
+ return this.quickLinks ? this.menuService.menuLinksByIds(this.quickLinks.links) : of([]);
+ }
+
+ private loadQuickLinks() {
+ this.userSettingsService.getQuickLinks().pipe(
+ map((quickLinks) => {
+ if (!quickLinks || !quickLinks.links) {
+ return defaultQuickLinksMap.get(this.authUser.authority);
+ } else {
+ return quickLinks;
+ }
+ })
+ ).subscribe(
+ (quickLinks) => {
+ this.quickLinks = quickLinks;
+ this.cd.markForCheck();
+ }
+ );
+ }
+
+ edit() {
+ this.dialog.open(EditLinksDialogComponent, {
+ disableClose: true,
+ autoFocus: false,
+ data: {
+ mode: 'quickLinks',
+ links: this.quickLinks
+ },
+ panelClass: ['tb-dialog', 'tb-fullscreen-dialog']
+ }).afterClosed().subscribe(
+ (result) => {
+ if (result) {
+ this.loadQuickLinks();
+ }
+ });
+ }
+
+ addLink() {
+ this.dialog.open(AddQuickLinkDialogComponent, {
+ disableClose: true,
+ autoFocus: false,
+ panelClass: ['tb-dialog', 'tb-fullscreen-dialog']
+ }).afterClosed().subscribe(
+ (link) => {
+ if (link) {
+ this.quickLinks.links.push(link);
+ this.cd.markForCheck();
+ this.userSettingsService.updateQuickLinks(this.quickLinks).subscribe();
+ }
+ });
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.html
new file mode 100644
index 0000000000..40a1eefe40
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.html
@@ -0,0 +1,106 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ lastVisitedDashboard.starred ? 'star' : 'star_border' }}
+
+
+
+
+ {{ 'widgets.recent-dashboards.name' | translate }}
+
+
+ {{ lastVisitedDashboard.title }}
+
+
+
+
+ {{ 'widgets.recent-dashboards.last-viewed' | translate }}
+
+
+ {{ lastVisitedDashboard.lastVisited | dateAgo:{applyAgo: true} }}
+
+
+
+
+
+
+
+
+
+
{{ 'widgets.recent-dashboards.no-last-viewed-dashboards' | translate }}
+
+
+
+
+
+
+
+ {{ dashboard.starred ? 'star' : 'star_border' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.scss
new file mode 100644
index 0000000000..29235a0d1b
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.scss
@@ -0,0 +1,240 @@
+/**
+ * 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 "../../../../../../../scss/constants";
+
+:host {
+ .tb-card-header {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+ @media #{$mat-lt-md} {
+ flex-direction: column;
+ align-items: stretch;
+ gap: 12px;
+ }
+ }
+ .tb-no-dashboards {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ }
+ .tb-no-last-visited-bg {
+ width: 60px;
+ height: 60px;
+ margin: 10px;
+ position: relative;
+ &:before {
+ content: "";
+ border-radius: 8px;
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: #305680;
+ -webkit-mask-image: url(/assets/home/no_data_bg.svg);
+ -webkit-mask-repeat: no-repeat;
+ mask-image: url(/assets/home/no_data_bg.svg);
+ mask-repeat: no-repeat;
+ }
+ }
+ .tb-no-last-visited-dashboards-text {
+ font-weight: 500;
+ font-size: 14px;
+ line-height: 20px;
+ letter-spacing: 0.25px;
+ color: rgba(0, 0, 0, 0.54);
+ }
+
+ .tb-starred-dashboard-row {
+ height: 40px;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ @media #{$mat-md-lg} {
+ height: 34px;
+ }
+ }
+
+ .tb-cell {
+ box-sizing: content-box;
+ &:first-child {
+ padding: 0 12px;
+ }
+ &:nth-child(n+2):nth-last-child(n+2) {
+ padding: 0 28px 0 0;
+ }
+ }
+
+ .tb-star-dashboard-autocomplete {
+ margin-left: 64px;
+ margin-right: 64px;
+ @media #{$mat-md-lg} {
+ margin-left: 48px;
+ margin-right: 48px;
+ }
+ }
+}
+
+:host ::ng-deep {
+ .mat-mdc-cell, .mat-mdc-header-cell, .mdc-data-table__row:last-child .mdc-data-table__cell {
+ border-bottom: none;
+ }
+
+ .mat-mdc-table {
+ .mdc-data-table__row, .mdc-data-table__header-row {
+ height: 40px;
+ @media #{$mat-md-lg} {
+ height: 34px;
+ }
+
+ &:hover {
+ background-color: inherit;
+ }
+ }
+ }
+
+ .tb-cell:nth-child(n+2):nth-last-child(n+2) {
+ padding: 0 28px 0 0;
+ }
+
+ .mat-mdc-row:not(.mat-row-select) .mat-mdc-cell:nth-child(n+2):nth-last-child(n+2) {
+ @media #{$mat-md-lg} {
+ padding: 0 12px 0 0;
+ }
+ }
+
+ .mat-mdc-header-cell, .mat-mdc-cell, .tb-cell {
+ &.star-cell {
+ width: 40px;
+ min-width: 40px;
+ @media #{$mat-md-lg} {
+ width: 24px;
+ min-width: 24px;
+ max-width: 24px;
+ }
+ }
+
+ &.title {
+ width: 100%;
+ }
+
+ &.last-visited {
+ width: 80px;
+ @media #{$mat-md-lg} {
+ width: 50px;
+ }
+ }
+ }
+
+ .mat-mdc-header-cell {
+ font-weight: 400;
+ font-size: 12px;
+ line-height: 16px;
+ letter-spacing: 0.25px;
+ color: rgba(0, 0, 0, 0.38);
+ @media #{$mat-md-lg} {
+ font-size: 11px;
+ }
+ }
+ .mat-mdc-cell.title {
+ max-width: 0;
+ }
+ .mat-mdc-cell, .tb-cell {
+ font-weight: 400;
+ font-size: 14px;
+ line-height: 20px;
+ letter-spacing: 0.2px;
+ @media #{$mat-md-lg} {
+ font-size: 11px;
+ line-height: 16px;
+ }
+ &.title {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ a {
+ color: rgba(0, 0, 0, 0.87);
+ border-bottom: none;
+ &:hover, &:focus {
+ border-bottom: none;
+ }
+ }
+ }
+ &.last-visited {
+ cursor: default;
+ color: rgba(0, 0, 0, 0.54);
+ }
+ .star {
+ cursor: pointer;
+ vertical-align: middle;
+ color: rgba(0, 0, 0, 0.12);
+ &:hover {
+ color: rgba(0, 0, 0, 0.18);
+ }
+ &.starred {
+ color: #FAE205;
+ &:hover {
+ color: #CDBD2C;
+ }
+ }
+ }
+ }
+ .tb-star-dashboard-autocomplete {
+ .mat-mdc-form-field {
+ font-weight: 400;
+ font-size: 14px;
+ line-height: 20px;
+ letter-spacing: 0.2px;
+ color: rgba(0, 0, 0, 0.54);
+ @media #{$mat-md-lg} {
+ font-size: 11px;
+ line-height: 16px;
+ }
+ }
+ .mat-mdc-text-field-wrapper.mdc-text-field--outlined .mat-mdc-form-field-infix {
+ padding-top: 4px;
+ padding-bottom: 4px;
+ min-height: 28px;
+ @media #{$mat-md-lg} {
+ min-height: 24px;
+ }
+ }
+ .mat-mdc-form-field-hint-wrapper {
+ height: 0;
+ }
+ .mdc-text-field--outlined .mdc-notched-outline {
+ .mdc-notched-outline__leading, .mdc-notched-outline__trailing {
+ border-top-style: dashed;
+ border-bottom-style: dashed;
+ }
+ .mdc-notched-outline__leading {
+ border-left-style: dashed;
+ }
+ .mdc-notched-outline__trailing {
+ border-right-style: dashed;
+ }
+ }
+ .mat-mdc-icon-button.mat-mdc-button-base {
+ width: 28px;
+ height: 28px;
+ padding: 2px;
+ }
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.ts
new file mode 100644
index 0000000000..33b273ec9f
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.ts
@@ -0,0 +1,222 @@
+///
+/// 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 {
+ AfterViewInit,
+ ChangeDetectorRef,
+ Component,
+ Input,
+ OnDestroy,
+ OnInit,
+ QueryList, ViewChild,
+ ViewChildren
+} from '@angular/core';
+import { PageComponent } from '@shared/components/page.component';
+import { Store } from '@ngrx/store';
+import { AppState } from '@core/core.state';
+import { Authority } from '@shared/models/authority.enum';
+import { BehaviorSubject, Observable, of } from 'rxjs';
+import { getCurrentAuthUser } from '@core/auth/auth.selectors';
+import { WidgetContext } from '@home/models/widget-component.models';
+import {
+ AbstractUserDashboardInfo,
+ LastVisitedDashboardInfo, StarredDashboardInfo,
+ UserDashboardAction,
+ UserDashboardsInfo
+} from '@shared/models/user-settings.models';
+import { UserSettingsService } from '@core/http/user-settings.service';
+import { CollectionViewer, DataSource } from '@angular/cdk/collections';
+import { emptyPageData, PageData } from '@shared/models/page/page-data';
+import { MAX_SAFE_PAGE_SIZE, PageLink } from '@shared/models/page/page-link';
+import { map, share } from 'rxjs/operators';
+import { Direction, SortOrder } from '@shared/models/page/sort-order';
+import { MatSort } from '@angular/material/sort';
+import { DashboardInfo } from '@shared/models/dashboard.models';
+import { DashboardAutocompleteComponent } from '@shared/components/dashboard-autocomplete.component';
+
+@Component({
+ selector: 'tb-recent-dashboards-widget',
+ templateUrl: './recent-dashboards-widget.component.html',
+ styleUrls: ['./home-page-widget.scss', './recent-dashboards-widget.component.scss']
+})
+export class RecentDashboardsWidgetComponent extends PageComponent implements OnInit, AfterViewInit, OnDestroy {
+
+ @Input()
+ ctx: WidgetContext;
+
+ @ViewChildren(MatSort) lastVisitedDashboardsSort: QueryList;
+
+ @ViewChild('starDashboardAutocomplete', {static: false})
+ starDashboardAutocomplete: DashboardAutocompleteComponent;
+
+ authority = Authority;
+
+ userDashboardsInfo: UserDashboardsInfo;
+ authUser = getCurrentAuthUser(this.store);
+
+ toggleValue: 'last' | 'starred' = 'last';
+
+ lastVisitedDashboardsColumns = ['starred', 'title', 'lastVisited'];
+ lastVisitedDashboardsDataSource: LastVisitedDashboardsDataSource;
+ lastVisitedDashboardsPageLink: PageLink;
+
+ starredDashboardValue = null;
+
+ dirty = false;
+
+ constructor(protected store: Store,
+ private cd: ChangeDetectorRef,
+ private userSettingService: UserSettingsService) {
+ super(store);
+ }
+
+ ngOnInit() {
+ this.reload();
+ }
+
+ reload() {
+ this.userDashboardsInfo = null;
+ this.cd.markForCheck();
+ (this.authUser.authority !== Authority.SYS_ADMIN ?
+ this.userSettingService.getUserDashboardsInfo() : of({last: [], starred: []})).subscribe(
+ (userDashboardsInfo) => {
+ this.userDashboardsInfo = userDashboardsInfo;
+ for (const starredDashboard of this.userDashboardsInfo?.starred) {
+ starredDashboard.starred = true;
+ }
+ this.userDashboardsInfo?.starred.sort((a, b) => a.starredAt - b.starredAt);
+ if (this.hasLastVisitedDashboards()) {
+ this.initLastVisitedDashboardsDataSource();
+ }
+ this.cd.markForCheck();
+ }
+ );
+ }
+
+ toggleValueChange(value: 'last' | 'starred') {
+ this.toggleValue = value;
+ if (this.dirty) {
+ this.dirty = false;
+ this.reload();
+ } else {
+ if (value === 'last' && this.hasLastVisitedDashboards()) {
+ this.initLastVisitedDashboardsDataSource();
+ }
+ }
+ }
+
+ private initLastVisitedDashboardsDataSource() {
+ this.lastVisitedDashboardsDataSource = new LastVisitedDashboardsDataSource(this.userDashboardsInfo.last);
+ const sortOrder: SortOrder = {
+ property: 'lastVisited',
+ direction: Direction.DESC
+ };
+ this.lastVisitedDashboardsPageLink = new PageLink(MAX_SAFE_PAGE_SIZE, 0, null, sortOrder);
+ this.lastVisitedDashboardsDataSource.loadData(this.lastVisitedDashboardsPageLink);
+ }
+
+ ngAfterViewInit() {
+ this.lastVisitedDashboardsSort.changes.subscribe(() => {
+ if (this.lastVisitedDashboardsSort.length) {
+ this.lastVisitedDashboardsSort.get(0).sortChange.subscribe(() =>
+ this.updateLastVisitedDashboardsData(this.lastVisitedDashboardsSort.get(0)));
+ }
+ });
+ }
+
+ updateLastVisitedDashboardsData(sort: MatSort) {
+ this.lastVisitedDashboardsPageLink.sortOrder.property = sort.active;
+ this.lastVisitedDashboardsPageLink.sortOrder.direction = Direction[sort.direction.toUpperCase()];
+ this.lastVisitedDashboardsDataSource.loadData(this.lastVisitedDashboardsPageLink);
+ }
+
+ hasLastVisitedDashboards(): boolean {
+ return !!(this.userDashboardsInfo && this.userDashboardsInfo.last && this.userDashboardsInfo.last.length);
+ }
+
+ toggleDashboardStar(dashboard: AbstractUserDashboardInfo): void {
+ const action: UserDashboardAction = dashboard.starred ? UserDashboardAction.UNSTAR : UserDashboardAction.STAR;
+ dashboard.starred = !dashboard.starred;
+ this.userSettingService.reportUserDashboardAction(dashboard.id, action, {ignoreLoading: true}).subscribe();
+ this.dirty = true;
+ if (this.toggleValue === 'starred') {
+ const index = this.userDashboardsInfo.starred.findIndex((d) => d.id === dashboard.id);
+ if (index > -1) {
+ this.userDashboardsInfo.starred.splice(index, 1);
+ this.cd.markForCheck();
+ }
+ }
+ }
+
+ onStarDashboard(dashboard: DashboardInfo) {
+ if (dashboard) {
+ this.starDashboardAutocomplete.clear();
+ const index = this.userDashboardsInfo.starred.findIndex((d) => d.id === dashboard.id.id);
+ if (index === -1) {
+ const starredDashboard: StarredDashboardInfo = {
+ starredAt: Date.now(),
+ id: dashboard.id.id,
+ starred: true,
+ title: dashboard.title
+ };
+ this.userDashboardsInfo.starred.push(starredDashboard);
+ this.userSettingService.reportUserDashboardAction(dashboard.id.id, UserDashboardAction.STAR, {ignoreLoading: true}).subscribe();
+ }
+ this.cd.markForCheck();
+ }
+ }
+
+}
+
+export class LastVisitedDashboardsDataSource implements DataSource {
+
+ private lastVisitedDashboardsSubject = new BehaviorSubject([]);
+ private pageDataSubject = new BehaviorSubject>(emptyPageData());
+
+ public pageData$ = this.pageDataSubject.asObservable();
+
+ constructor(private lastVisitedDashboards: Array) {
+ }
+
+ connect(collectionViewer: CollectionViewer): Observable> {
+ return this.lastVisitedDashboardsSubject.asObservable();
+ }
+
+ disconnect(collectionViewer: CollectionViewer): void {
+ this.lastVisitedDashboardsSubject.complete();
+ this.pageDataSubject.complete();
+ }
+
+ loadData(pageLink: PageLink): void {
+ const result = pageLink.filterData(this.lastVisitedDashboards);
+ this.lastVisitedDashboardsSubject.next(result.data);
+ this.pageDataSubject.next(result);
+ }
+
+ isEmpty(): Observable {
+ return this.lastVisitedDashboardsSubject.pipe(
+ map((entities) => !entities.length),
+ share()
+ );
+ }
+
+ total(): Observable {
+ return this.pageDataSubject.pipe(
+ map((pageData) => pageData.totalElements),
+ share()
+ );
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.html
new file mode 100644
index 0000000000..ed39fca25d
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.html
@@ -0,0 +1,28 @@
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.scss
new file mode 100644
index 0000000000..1e901b056a
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.scss
@@ -0,0 +1,134 @@
+/**
+ * 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 "../../../../../../../scss/constants";
+
+:host ::ng-deep {
+ .mat-button-toggle-group.mat-button-toggle-group-appearance-standard.tb-toggle-header {
+ width: 100%;
+ border-radius: 100px;
+ height: 32px;
+ padding: 2px;
+ border: none;
+ background: rgba(0, 0, 0, 0.06);
+ .mat-button-toggle + .mat-button-toggle {
+ border-left: none;
+ }
+ .mat-button-toggle.mat-button-toggle-appearance-standard {
+ flex: 1;
+ color: rgba(0, 0, 0, 0.38);
+ background: transparent;
+
+ .mat-button-toggle-focus-overlay, .mat-button-toggle-ripple {
+ border-radius: 20px;
+ }
+ .mat-button-toggle-button {
+ height: 28px;
+ .mat-button-toggle-label-content {
+ line-height: 26px;
+ font-weight: 400;
+ font-size: 14px;
+ letter-spacing: 0.2px;
+ }
+ }
+ &.mat-button-toggle-checked {
+ .mat-button-toggle-button {
+ background: #F3F6FA;
+ color: #305680;
+ border: 1px solid #305680;
+ border-radius: 20px;
+
+ .mat-button-toggle-label-content {
+ font-weight: 500;
+ line-height: 24px;
+ }
+ }
+ }
+ }
+ }
+ @media #{$mat-md-lg} {
+ .mat-button-toggle-group.mat-button-toggle-group-appearance-standard.tb-toggle-header {
+ height: 24px;
+ .mat-button-toggle.mat-button-toggle-appearance-standard {
+ .mat-button-toggle-button {
+ height: 20px;
+ display: grid;
+ .mat-button-toggle-label-content {
+ line-height: 20px;
+ font-size: 10px;
+ padding: 0 2px;
+ }
+ }
+ &.mat-button-toggle-checked {
+ .mat-button-toggle-button {
+ .mat-button-toggle-label-content {
+ line-height: 18px;
+ }
+ }
+ }
+ }
+ }
+ }
+ .tb-toggle-header-select {
+ &.mat-mdc-form-field {
+ line-height: 16px;
+ font-size: 12px;
+ }
+ .mat-mdc-text-field-wrapper.mdc-text-field--outlined .mat-mdc-form-field-infix {
+ min-height: 0;
+ width: auto;
+ padding-top: 8px;
+ padding-bottom: 8px;
+ }
+ .mdc-text-field--outlined {
+ padding-left: 8px;
+ padding-right: 6px;
+ }
+ .mat-mdc-select {
+ font-weight: 400;
+ font-size: 12px;
+ line-height: 16px;
+ letter-spacing: 0.25px;
+ }
+ .mat-mdc-select-value {
+ color: rgba(0, 0, 0, 0.38);
+ }
+ .mat-mdc-select-arrow-wrapper {
+ height: 12px;
+ padding-left: 6px;
+ }
+ .mat-mdc-select-arrow {
+ width: 12px;
+ height: 12px;
+ & > svg {
+ display: none;
+ }
+ &:after {
+ font-family: 'Material Icons';
+ content: "expand_more";
+ position: absolute;
+ display: inline-block;
+ font-size: 20px;
+ line-height: 12px;
+ top: 0;
+ left: -6px;
+ right: 0;
+ bottom: 0;
+ color: rgba(0, 0, 0, 0.38);
+ }
+ }
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.ts
new file mode 100644
index 0000000000..b6bb189f15
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.ts
@@ -0,0 +1,90 @@
+///
+/// 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 {
+ AfterContentInit,
+ AfterViewInit,
+ ChangeDetectorRef,
+ Component,
+ ContentChildren, EventEmitter,
+ Input,
+ OnInit, Output,
+ QueryList,
+ ViewChild
+} from '@angular/core';
+import { PageComponent } from '@shared/components/page.component';
+import { Store } from '@ngrx/store';
+import { AppState } from '@core/core.state';
+import { AdminService } from '@core/http/admin.service';
+import { UpdateMessage } from '@shared/models/settings.models';
+import { getCurrentAuthUser } from '@core/auth/auth.selectors';
+import { Authority } from '@shared/models/authority.enum';
+import { of, Subscription } from 'rxjs';
+import { MatStepper } from '@angular/material/stepper';
+import { MatButtonToggle, MatButtonToggleGroup } from '@angular/material/button-toggle';
+import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout';
+import { MediaBreakpoints } from '@shared/models/constants';
+import { coerceBoolean } from '@shared/decorators/coerce-boolean';
+
+export interface ToggleHeaderOption {
+ name: string;
+ value: any;
+}
+
+@Component({
+ selector: 'tb-toggle-header',
+ templateUrl: './toggle-header.component.html',
+ styleUrls: ['./toggle-header.component.scss']
+})
+export class ToggleHeaderComponent extends PageComponent implements OnInit {
+
+ @Input()
+ value: any;
+
+ @Output()
+ valueChange = new EventEmitter();
+
+ @Input()
+ options: ToggleHeaderOption[];
+
+ @Input()
+ name: string;
+
+ @Input()
+ @coerceBoolean()
+ useSelectOnMdLg = true;
+
+ isMdLg: boolean;
+
+ private observeBreakpointSubscription: Subscription;
+
+ constructor(protected store: Store,
+ private cd: ChangeDetectorRef,
+ private breakpointObserver: BreakpointObserver) {
+ super(store);
+ }
+
+ ngOnInit() {
+ this.isMdLg = this.breakpointObserver.isMatched(MediaBreakpoints['md-lg']);
+ this.observeBreakpointSubscription = this.breakpointObserver
+ .observe(MediaBreakpoints['md-lg'])
+ .subscribe((state: BreakpointState) => {
+ this.isMdLg = state.matches;
+ this.cd.markForCheck();
+ }
+ );
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.html
new file mode 100644
index 0000000000..264b6970ad
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.html
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
+
device.devices
+
asset.assets
+
user.users
+
dashboard.dashboards
+
customer.customers
+
+
+
+
{{ usageInfo?.devices | shortNumber }} / {{ maxValue(usageInfo?.maxDevices) }}
+
{{ usageInfo?.assets | shortNumber }} / {{ maxValue(usageInfo?.maxAssets) }}
+
{{ usageInfo?.users | shortNumber }} / {{ maxValue(usageInfo?.maxUsers) }}
+
{{ usageInfo?.dashboards | shortNumber }} / {{ maxValue(usageInfo?.maxDashboards) }}
+
{{ usageInfo?.customers | shortNumber }} / {{ maxValue(usageInfo?.maxCustomers) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
api-usage.transport-messages
+
api-usage.javascript
+
api-usage.alarms-created
+
api-usage.email
+
api-usage.sms
+
+
+
+
{{ usageInfo?.transportMessages | shortNumber }} / {{ maxValue(usageInfo?.maxTransportMessages) }}
+
{{ usageInfo?.jsExecutions | shortNumber }} / {{ maxValue(usageInfo?.maxJsExecutions) }}
+
{{ usageInfo?.alarms | shortNumber }} / {{ maxValue(usageInfo?.maxAlarms) }}
+
{{ usageInfo?.emails | shortNumber }} / {{ maxValue(usageInfo?.maxEmails) }}
+
{{ usageInfo?.sms | shortNumber }} / {{ maxValue(usageInfo?.maxSms) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.scss
new file mode 100644
index 0000000000..8d85e176a8
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.scss
@@ -0,0 +1,113 @@
+/**
+ * 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 "../../../../../../../scss/constants";
+
+:host {
+ .tb-card-header {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+ @media #{$mat-lt-md} {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 12px;
+ }
+ }
+
+ .tb-usage-list {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ overflow-y: auto;
+ }
+
+ .tb-usage-items, .tb-usage-items-counts, .tb-usage-items-progress {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-evenly;
+ align-items: flex-start;
+ overflow: hidden;
+ }
+
+ .tb-usage-items-progress {
+ width: 34px;
+ @media #{$mat-md} {
+ display: none;
+ }
+ .mdc-linear-progress {
+ height: 8px;
+ margin-top: 6px;
+ margin-bottom: 6px;
+ border-radius: 2px;
+ @media #{$mat-md-lg} {
+ margin-top: 4px;
+ margin-bottom: 4px;
+ }
+ }
+ }
+
+ .tb-usage-items-values {
+ display: flex;
+ flex-direction: row;
+ align-items: stretch;
+ gap: 16px;
+ }
+
+ .tb-usage-item, .tb-usage-item-counts {
+ font-weight: 400;
+ font-size: 14px;
+ line-height: 20px;
+ letter-spacing: 0.2px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ width: 100%;
+ @media #{$mat-md-lg} {
+ font-size: 11px;
+ line-height: 16px;
+ }
+ &.critical {
+ color: #D12730;
+ }
+ }
+
+ .tb-usage-item {
+ color: rgba(0, 0, 0, 0.38);
+ }
+
+ .tb-usage-item-counts {
+ color: rgba(0, 0, 0, 0.76);
+ }
+}
+
+:host ::ng-deep {
+ .tb-usage-items-progress {
+ .mat-mdc-progress-bar {
+ .mdc-linear-progress__bar-inner {
+ border-top-width: 8px;
+ }
+ &.critical {
+ .mdc-linear-progress__buffer-bar {
+ background: rgba(209, 39, 48, 0.06);
+ }
+ .mdc-linear-progress__bar-inner {
+ border-top-color: #D12730;
+ }
+ }
+ }
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.ts
new file mode 100644
index 0000000000..ed5ff7dafc
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.ts
@@ -0,0 +1,111 @@
+///
+/// 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 { ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core';
+import { PageComponent } from '@shared/components/page.component';
+import { Store } from '@ngrx/store';
+import { AppState } from '@core/core.state';
+import { Authority } from '@shared/models/authority.enum';
+import { of } from 'rxjs';
+import { getCurrentAuthUser } from '@core/auth/auth.selectors';
+import { WidgetContext } from '@home/models/widget-component.models';
+import { UsageInfo } from '@shared/models/usage.models';
+import { UsageInfoService } from '@core/http/usage-info.service';
+import { ShortNumberPipe } from '@shared/pipe/short-number.pipe';
+
+@Component({
+ selector: 'tb-usage-info-widget',
+ templateUrl: './usage-info-widget.component.html',
+ styleUrls: ['./home-page-widget.scss', './usage-info-widget.component.scss']
+})
+export class UsageInfoWidgetComponent extends PageComponent implements OnInit, OnDestroy {
+
+ @Input()
+ ctx: WidgetContext;
+
+ usageInfo: UsageInfo;
+ authUser = getCurrentAuthUser(this.store);
+
+ toggleValue: 'entities' | 'apiCalls' = 'entities';
+
+ entityItemCritical: {[key: string]: boolean} = {};
+ apiCallItemCritical: {[key: string]: boolean} = {};
+
+ constructor(protected store: Store,
+ private cd: ChangeDetectorRef,
+ private shortNumberPipe: ShortNumberPipe,
+ private usageInfoService: UsageInfoService) {
+ super(store);
+ }
+
+ ngOnInit() {
+ (this.authUser.authority === Authority.TENANT_ADMIN ?
+ this.usageInfoService.getUsageInfo() : of(null)).subscribe(
+ (usageInfo) => {
+ this.usageInfo = usageInfo;
+ this.entityItemCritical.devices = this.isItemCritical(this.usageInfo?.devices, this.usageInfo?.maxDevices);
+ this.entityItemCritical.assets = this.isItemCritical(this.usageInfo?.assets, this.usageInfo?.maxAssets);
+ this.entityItemCritical.users = this.isItemCritical(this.usageInfo?.users, this.usageInfo?.maxUsers);
+ this.entityItemCritical.dashboards = this.isItemCritical(this.usageInfo?.dashboards, this.usageInfo?.maxDashboards);
+ this.entityItemCritical.customers = this.isItemCritical(this.usageInfo?.customers, this.usageInfo?.maxCustomers);
+ this.apiCallItemCritical.transportMessages = this.isItemCritical(this.usageInfo?.transportMessages,
+ this.usageInfo?.maxTransportMessages);
+ this.apiCallItemCritical.jsExecutions = this.isItemCritical(this.usageInfo?.jsExecutions, this.usageInfo?.maxJsExecutions);
+ this.apiCallItemCritical.alarms = this.isItemCritical(this.usageInfo?.alarms, this.usageInfo?.maxAlarms);
+ this.apiCallItemCritical.emails = this.isItemCritical(this.usageInfo?.emails, this.usageInfo?.maxEmails);
+ this.apiCallItemCritical.sms = this.isItemCritical(this.usageInfo?.sms, this.usageInfo?.maxSms);
+ let entitiesHasCriticalItem = false;
+ let apiCallsHasCriticalItem = false;
+ for (const key of Object.keys(this.entityItemCritical)) {
+ if (this.entityItemCritical[key]) {
+ entitiesHasCriticalItem = true;
+ break;
+ }
+ }
+ for (const key of Object.keys(this.apiCallItemCritical)) {
+ if (this.apiCallItemCritical[key]) {
+ apiCallsHasCriticalItem = true;
+ break;
+ }
+ }
+ if (apiCallsHasCriticalItem && !entitiesHasCriticalItem) {
+ this.toggleValue = 'apiCalls';
+ }
+ this.cd.markForCheck();
+ }
+ );
+ }
+
+ maxValue(max: number): number | string {
+ return max ? this.shortNumberPipe.transform(max) : '∞';
+ }
+
+ progressValue(value: number, max: number): number {
+ if (max && value) {
+ return (value / max) * 100;
+ }
+ return 0;
+ }
+
+ private isItemCritical(value: number, max: number): boolean {
+ if (max && value) {
+ return (value / max) >= 0.85;
+ } else {
+ return false;
+ }
+ }
+
+}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/version-info.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/version-info.component.scss
index 133a0d498e..2ad903afcf 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/version-info.component.scss
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/version-info.component.scss
@@ -17,11 +17,6 @@
@import "../../../../../../../scss/constants";
:host {
- .tb-card-content {
- width: 100%;
- height: 100%;
- }
-
.tb-card-header {
display: flex;
flex-direction: row;
@@ -33,19 +28,6 @@
}
}
- .tb-title {
- font-style: normal;
- 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;
- }
- }
-
.tb-contact-us {
@media #{$mat-md-lg} {
height: 24px;
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/version-info.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/version-info.component.ts
index 27acef15f4..00265619f0 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/version-info.component.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/version-info.component.ts
@@ -27,7 +27,7 @@ import { of } from 'rxjs';
@Component({
selector: 'tb-version-info',
templateUrl: './version-info.component.html',
- styleUrls: ['./version-info.component.scss']
+ styleUrls: ['./home-page-widget.scss', './version-info.component.scss']
})
export class VersionInfoComponent extends PageComponent implements OnInit {
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.html
index 6c484fd579..2bebfeef8a 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.html
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.html
@@ -51,9 +51,13 @@
widgets.chart.line-width
-
+
{{ 'widgets.chart.fill-line' | translate }}
+
+ widgets.chart.fill-line-opacity
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.ts
index c70529bd8b..b2c88e5fcb 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.ts
@@ -48,6 +48,7 @@ export function flotDataKeyDefaultSettings(chartType: ChartType): TbFlotKeySetti
showLines: chartType === 'graph',
lineWidth: 1,
fillLines: false,
+ fillLinesOpacity: 0.4,
// Points settings
showPoints: false,
@@ -146,6 +147,7 @@ export class FlotKeySettingsComponent extends PageComponent implements OnInit, C
showLines: [this.chartType === 'graph', []],
lineWidth: [1, [Validators.min(0)]],
fillLines: [false, []],
+ fillLinesOpacity: [0.4, [Validators.min(0), Validators.max(1)]],
// Points settings
@@ -188,15 +190,19 @@ export class FlotKeySettingsComponent extends PageComponent implements OnInit, C
});
this.flotKeySettingsFormGroup.get('showLines').valueChanges.subscribe(() => {
- this.updateValidators(true);
+ this.updateValidators(false);
+ });
+
+ this.flotKeySettingsFormGroup.get('fillLines').valueChanges.subscribe(() => {
+ this.updateValidators(false);
});
this.flotKeySettingsFormGroup.get('showPoints').valueChanges.subscribe(() => {
- this.updateValidators(true);
+ this.updateValidators(false);
});
this.flotKeySettingsFormGroup.get('comparisonSettings.showValuesForComparison').valueChanges.subscribe(() => {
- this.updateValidators(true);
+ this.updateValidators(false);
});
this.flotKeySettingsFormGroup.valueChanges.subscribe(() => {
@@ -254,15 +260,22 @@ export class FlotKeySettingsComponent extends PageComponent implements OnInit, C
private updateValidators(emitEvent?: boolean): void {
const showLines: boolean = this.flotKeySettingsFormGroup.get('showLines').value;
+ const fillLines: boolean = this.flotKeySettingsFormGroup.get('fillLines').value;
const showPoints: boolean = this.flotKeySettingsFormGroup.get('showPoints').value;
const showValuesForComparison: boolean = this.flotKeySettingsFormGroup.get('comparisonSettings.showValuesForComparison').value;
if (showLines) {
this.flotKeySettingsFormGroup.get('lineWidth').enable({emitEvent});
this.flotKeySettingsFormGroup.get('fillLines').enable({emitEvent});
+ if (fillLines) {
+ this.flotKeySettingsFormGroup.get('fillLinesOpacity').enable({emitEvent});
+ } else {
+ this.flotKeySettingsFormGroup.get('fillLinesOpacity').disable({emitEvent});
+ }
} else {
this.flotKeySettingsFormGroup.get('lineWidth').disable({emitEvent});
this.flotKeySettingsFormGroup.get('fillLines').disable({emitEvent});
+ this.flotKeySettingsFormGroup.get('fillLinesOpacity').disable({emitEvent});
}
if (showPoints) {
@@ -287,6 +300,7 @@ export class FlotKeySettingsComponent extends PageComponent implements OnInit, C
this.flotKeySettingsFormGroup.get('lineWidth').updateValueAndValidity({emitEvent: false});
this.flotKeySettingsFormGroup.get('fillLines').updateValueAndValidity({emitEvent: false});
+ this.flotKeySettingsFormGroup.get('fillLinesOpacity').updateValueAndValidity({emitEvent: false});
this.flotKeySettingsFormGroup.get('showPointsLineWidth').updateValueAndValidity({emitEvent: false});
this.flotKeySettingsFormGroup.get('showPointsRadius').updateValueAndValidity({emitEvent: false});
this.flotKeySettingsFormGroup.get('showPointShape').updateValueAndValidity({emitEvent: false});
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/home-page/quick-links-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/home-page/quick-links-widget-settings.component.html
new file mode 100644
index 0000000000..f2936d7bba
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/home-page/quick-links-widget-settings.component.html
@@ -0,0 +1,23 @@
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/home-page/quick-links-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/home-page/quick-links-widget-settings.component.ts
new file mode 100644
index 0000000000..af22e4e06b
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/home-page/quick-links-widget-settings.component.ts
@@ -0,0 +1,52 @@
+///
+/// Copyright © 2016-2023 The Thingsboard Authors
+///
+/// Licensed under the Apache License, Version 2.0 (the "License");
+/// you may not use this file except in compliance with the License.
+/// You may obtain a copy of the License at
+///
+/// http://www.apache.org/licenses/LICENSE-2.0
+///
+/// Unless required by applicable law or agreed to in writing, software
+/// distributed under the License is distributed on an "AS IS" BASIS,
+/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+/// See the License for the specific language governing permissions and
+/// limitations under the License.
+///
+
+import { Component } from '@angular/core';
+import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models';
+import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
+import { Store } from '@ngrx/store';
+import { AppState } from '@core/core.state';
+
+@Component({
+ selector: 'tb-quick-links-widget-settings',
+ templateUrl: './quick-links-widget-settings.component.html',
+ styleUrls: ['./../widget-settings.scss']
+})
+export class QuickLinksWidgetSettingsComponent extends WidgetSettingsComponent {
+
+ quickLinksWidgetSettingsForm: UntypedFormGroup;
+
+ constructor(protected store: Store,
+ private fb: UntypedFormBuilder) {
+ super(store);
+ }
+
+ protected settingsForm(): UntypedFormGroup {
+ return this.quickLinksWidgetSettingsForm;
+ }
+
+ protected defaultSettings(): WidgetSettings {
+ return {
+ columns: 3
+ };
+ }
+
+ protected onSettingsSet(settings: WidgetSettings) {
+ this.quickLinksWidgetSettingsForm = this.fb.group({
+ columns: [settings.columns, [Validators.required, Validators.min(1), Validators.max(20)]]
+ });
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts
index 73ff1433a9..11aaf06a93 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts
@@ -262,6 +262,9 @@ import {
import {
DocLinksWidgetSettingsComponent
} from '@home/components/widget/lib/settings/home-page/doc-links-widget-settings.component';
+import {
+ QuickLinksWidgetSettingsComponent
+} from '@home/components/widget/lib/settings/home-page/quick-links-widget-settings.component';
@NgModule({
declarations: [
@@ -361,7 +364,8 @@ import {
MapWidgetSettingsComponent,
RouteMapWidgetSettingsComponent,
TripAnimationWidgetSettingsComponent,
- DocLinksWidgetSettingsComponent
+ DocLinksWidgetSettingsComponent,
+ QuickLinksWidgetSettingsComponent
],
imports: [
CommonModule,
@@ -465,7 +469,8 @@ import {
MapWidgetSettingsComponent,
RouteMapWidgetSettingsComponent,
TripAnimationWidgetSettingsComponent,
- DocLinksWidgetSettingsComponent
+ DocLinksWidgetSettingsComponent,
+ QuickLinksWidgetSettingsComponent
]
})
export class WidgetSettingsModule {
@@ -533,5 +538,6 @@ export const widgetSettingsComponentsMap: {[key: string]: Type
-
-
- alarm.alarm-status-list
-
-
- {{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }}
-
-
-
-
- alarm.alarm-severity-list
-
-
- {{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }}
-
-
-
-
-
-
- alarm.alarm-type-list
-
-
- {{type}}
- cancel
-
-
-
-
-
- {{ 'alarm.search-propagated-alarms' | translate }}
-
-
+