From bcc5eba8e6e136eb8fe0ecb0eb14a2afecf8e834 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 14 Apr 2023 19:18:35 +0300 Subject: [PATCH] UI: Tenant admin home page initial implementation --- .../getting-started-widget.component.html | 112 +++ .../getting-started-widget.component.scss | 117 ++- .../getting-started-widget.component.ts | 4 +- .../lib/home-page/home-page-widgets.module.ts | 7 +- .../home-page/toggle-header.component.html | 20 + .../home-page/toggle-header.component.scss | 87 +++ .../lib/home-page/toggle-header.component.ts | 82 +++ .../home-links/home-links-routing.module.ts | 15 +- .../home-links/tenant_admin_home_page.raw | 667 ++++++++++++++++++ .../shared/components/markdown.component.html | 2 +- .../shared/components/markdown.component.ts | 127 ++-- .../assets/locale/locale.constant-en_US.json | 49 ++ 12 files changed, 1216 insertions(+), 73 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.ts create mode 100644 ui-ngx/src/app/modules/home/pages/home-links/tenant_admin_home_page.raw diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.html index b2425a0231..ee84a63ad6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.html @@ -91,6 +91,118 @@ description{{ 'widgets.getting-started.sys-admin.step6.how-to-configure-notifications' | translate }} + + + +
+
widgets.getting-started.tenant-admin.step1.title
+ {{ 'device.devices' | translate }} +
+
+
+ + description{{ 'widgets.getting-started.tenant-admin.step1.how-to-create-device' | translate }} +
+ + +
+
widgets.getting-started.tenant-admin.step2.title
+
+
+
+
+ + Ubuntu + MacOS + Windows + + + +

+ +
+ +

+ +
+ +

+
+
+

+ +
+
+ + description{{ 'widgets.getting-started.tenant-admin.step2.how-to-connect-device' | translate }} +
+ + +
+
widgets.getting-started.tenant-admin.step3.title
+ {{ 'dashboard.dashboards' | translate }} +
+
+
+ + description{{ 'widgets.getting-started.tenant-admin.step3.how-to-create-dashboard' | translate }} +
+ + +
+
widgets.getting-started.tenant-admin.step4.title
+ {{ 'widgets.getting-started.tenant-admin.step4.alarm-rules' | translate }} +
+
+
+ + description{{ 'widgets.getting-started.tenant-admin.step4.how-to-configure-alarm-rules' | translate }} +
+ + +
+
widgets.getting-started.tenant-admin.step5.title
+
+
+
+

+ +
+ + description{{ 'widgets.getting-started.tenant-admin.step5.how-to-create-alarm' | translate }} +
+ + +
+
widgets.getting-started.tenant-admin.step6.title
+ {{ 'customer.customers' | translate }} +
+
+
+ + description{{ 'widgets.getting-started.tenant-admin.step6.how-to-create-customer-and-assign-dashboard' | translate }} +
+
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-widgets.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page-widgets.module.ts index 3be30dad30..debd1d6ca0 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 @@ -28,6 +28,7 @@ import { GettingStartedWidgetComponent } from '@home/components/widget/lib/home- 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'; @NgModule({ declarations: @@ -40,7 +41,8 @@ import { AddDocLinkDialogComponent, EditDocLinksDialogComponent, GettingStartedWidgetComponent, - GettingStartedCompletedDialogComponent + GettingStartedCompletedDialogComponent, + ToggleHeaderComponent ], imports: [ CommonModule, @@ -55,7 +57,8 @@ import { AddDocLinkDialogComponent, EditDocLinksDialogComponent, GettingStartedWidgetComponent, - GettingStartedCompletedDialogComponent + GettingStartedCompletedDialogComponent, + ToggleHeaderComponent ] }) export class HomePageWidgetsModule { } 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..3cbf61fdae --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.html @@ -0,0 +1,20 @@ + + + + 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..0dce288424 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.scss @@ -0,0 +1,87 @@ +/** + * 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); + margin-bottom: 8px; + .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; + margin-bottom: 0; + .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; + } + } + } + } + } + } +} 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..1d7876eb36 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.ts @@ -0,0 +1,82 @@ +/// +/// 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, + Input, + OnInit, + 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 } from 'rxjs'; +import { MatStepper } from '@angular/material/stepper'; +import { MatButtonToggle, MatButtonToggleGroup } from '@angular/material/button-toggle'; + +@Component({ + selector: 'tb-toggle-header', + templateUrl: './toggle-header.component.html', + styleUrls: ['./toggle-header.component.scss'] +}) +export class ToggleHeaderComponent extends PageComponent implements OnInit, AfterViewInit { + + @ViewChild('toggleGroup') + toggleGroup: MatButtonToggleGroup; + + @ContentChildren(MatButtonToggle) + _buttonToggles: QueryList; + + innerValue: any; + + @Input() + set value(value: any) { + this.innerValue = value; + } + + get value(): any { + return this.toggleGroup?.value; + } + + @Input() + name: string; + + constructor(protected store: Store) { + super(store); + } + + ngOnInit() { + } + + ngAfterViewInit() { + for (const toggle of this._buttonToggles) { + toggle.buttonToggleGroup = this.toggleGroup; + if (this.innerValue === toggle.value) { + toggle.checked = true; + } + } + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/home-links/home-links-routing.module.ts b/ui-ngx/src/app/modules/home/pages/home-links/home-links-routing.module.ts index be408da234..3e0f797be1 100644 --- a/ui-ngx/src/app/modules/home/pages/home-links/home-links-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/home-links/home-links-routing.module.ts @@ -27,6 +27,7 @@ import { AppState } from '@core/core.state'; import { map } from 'rxjs/operators'; import { getCurrentAuthUser } 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'; @Injectable() export class HomeDashboardResolver implements Resolve { @@ -39,8 +40,18 @@ export class HomeDashboardResolver implements Resolve { return this.dashboardService.getHomeDashboard().pipe( map((dashboard) => { if (!dashboard) { - if (getCurrentAuthUser(this.store).authority === Authority.SYS_ADMIN) { - dashboard = JSON.parse(sysAdminHomePageDashboardJson); + const authority = getCurrentAuthUser(this.store).authority; + switch (authority) { + case Authority.SYS_ADMIN: + dashboard = JSON.parse(sysAdminHomePageDashboardJson); + break; + case Authority.TENANT_ADMIN: + dashboard = JSON.parse(tenantAdminHomePageDashboardJson); + break; + case Authority.CUSTOMER_USER: + break; + } + if (dashboard) { dashboard.hideDashboardToolbar = true; } } diff --git a/ui-ngx/src/app/modules/home/pages/home-links/tenant_admin_home_page.raw b/ui-ngx/src/app/modules/home/pages/home-links/tenant_admin_home_page.raw new file mode 100644 index 0000000000..3044499da0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/home-links/tenant_admin_home_page.raw @@ -0,0 +1,667 @@ +{ + "title": "Tenant Administrator Home Page", + "image": null, + "mobileHide": false, + "mobileOrder": null, + "configuration": { + "description": "", + "widgets": { + "d70cc256-4c7b-ee06-9905-b8c5e546605f": { + "isSystemType": true, + "bundleAlias": "cards", + "typeAlias": "markdown_card", + "type": "latest", + "title": "New widget", + "image": null, + "description": null, + "sizeX": 5, + "sizeY": 3.5, + "config": { + "datasources": [], + "timewindow": { + "displayValue": "", + "selectedTab": 0, + "realtime": { + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1680168340431, + "endTimeMs": 1680254740431 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 + } + }, + "showTitle": false, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "16px", + "settings": { + "useMarkdownTextFunction": false, + "markdownTextPattern": "
\n
\n
{{ 'widgets.activity.title' | translate }}
\n \n {{ 'device.devices' | translate }}\n {{ 'widgets.transport-messages.title' | translate }}\n \n
\n \n \n \n \n \n \n \n \n
", + "applyDefaultMarkdownStyle": false, + "markdownCss": ".tb-card-content {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n}\n" + }, + "title": "Transport messages", + "showTitleIcon": false, + "iconColor": "rgba(0, 0, 0, 0.87)", + "iconSize": "24px", + "titleTooltip": "", + "dropShadow": false, + "enableFullscreen": false, + "widgetStyle": {}, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "showLegend": false, + "useDashboardTimewindow": true, + "widgetCss": "", + "pageSize": 1024, + "noDataDisplayMessage": "" + }, + "row": 0, + "col": 0, + "id": "d70cc256-4c7b-ee06-9905-b8c5e546605f" + }, + "8ee72d43-678c-4e25-e9a8-7d4cfd7a5f8e": { + "isSystemType": true, + "bundleAlias": "charts", + "typeAlias": "timeseries_bars_flot", + "type": "timeseries", + "title": "New widget", + "image": null, + "description": null, + "sizeX": 8, + "sizeY": 5, + "config": { + "datasources": [ + { + "type": "entity", + "name": null, + "entityAliasId": "d9229b29-3f46-de8d-7fe8-eb0c43c75079", + "filterId": null, + "dataKeys": [ + { + "name": "transportMsgCountHourly", + "type": "timeseries", + "label": "{i18n:widgets.transport-messages.title}", + "color": "#305680", + "settings": {}, + "_hash": 0.2880464219129071, + "aggregationType": null, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ], + "latestDataKeys": [] + } + ], + "timewindow": { + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false, + "hideAggregation": true, + "hideAggInterval": false, + "hideTimezone": false, + "selectedTab": 1, + "history": { + "historyType": 0, + "timewindowMs": 2592000000, + "interval": 86400000, + "fixedTimewindow": { + "startTimeMs": 1680443065451, + "endTimeMs": 1680529465451 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "SUM", + "limit": 50000 + } + }, + "showTitle": false, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "0px", + "settings": { + "stack": true, + "fontSize": 10, + "fontColor": "#545454", + "showTooltip": true, + "tooltipIndividual": false, + "tooltipCumulative": false, + "hideZeros": false, + "grid": { + "verticalLines": false, + "horizontalLines": false, + "outlineWidth": 0, + "color": "#545454", + "backgroundColor": null, + "tickColor": "#DDDDDD" + }, + "xaxis": { + "title": null, + "showLabels": true, + "color": "#545454" + }, + "yaxis": { + "min": 0, + "max": null, + "title": null, + "showLabels": true, + "color": "#545454", + "tickSize": null, + "tickDecimals": 0, + "ticksFormatter": "return value % 1000 === 0 ? ((value / 1000) + 'k') : '';" + }, + "defaultBarWidth": 1800000, + "barAlignment": "left", + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "xaxisSecond": { + "axisPosition": "top", + "title": null, + "showLabels": true + }, + "customLegendEnabled": false, + "dataKeysListForLabels": [] + }, + "title": "Transport messages", + "dropShadow": false, + "enableFullscreen": false, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "widgetStyle": { + "padding": "0" + }, + "useDashboardTimewindow": false, + "showLegend": false, + "actions": {}, + "displayTimewindow": true, + "showTitleIcon": false, + "titleTooltip": "", + "widgetCss": ".tb-widget-container > .tb-widget {\n border: none !important;\n border-radius: 0 !important;\n box-shadow: none !important;\n}\n\n.tb-widget-container > .tb-widget .flot-base {\n opacity: 0.48;\n}\n", + "pageSize": 1024, + "noDataDisplayMessage": "", + "legendConfig": { + "direction": "column", + "position": "bottom", + "sortDataKeys": false, + "showMin": false, + "showMax": false, + "showAvg": true, + "showTotal": false, + "showLatest": false + } + }, + "row": 0, + "col": 0, + "id": "8ee72d43-678c-4e25-e9a8-7d4cfd7a5f8e" + }, + "867f82cf-ecf2-2d5c-35cb-08c6f2edc3a4": { + "isSystemType": true, + "bundleAlias": "home_page_widgets", + "typeAlias": "documentation_links", + "type": "static", + "title": "New widget", + "image": null, + "description": null, + "sizeX": 7.5, + "sizeY": 3, + "config": { + "datasources": [], + "timewindow": { + "realtime": { + "timewindowMs": 60000 + } + }, + "showTitle": false, + "backgroundColor": "rgb(255, 255, 255)", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "16px", + "settings": { + "columns": 2 + }, + "title": "Documentation", + "dropShadow": false, + "enableFullscreen": false, + "widgetStyle": {}, + "widgetCss": "", + "pageSize": 1024, + "noDataDisplayMessage": "", + "showLegend": false + }, + "row": 0, + "col": 0, + "id": "867f82cf-ecf2-2d5c-35cb-08c6f2edc3a4" + }, + "a23185ad-dc46-806c-0e50-5b21fb080ace": { + "isSystemType": true, + "bundleAlias": "home_page_widgets", + "typeAlias": "getting_started", + "type": "static", + "title": "New widget", + "image": null, + "description": null, + "sizeX": 7.5, + "sizeY": 6.5, + "config": { + "datasources": [], + "timewindow": { + "realtime": { + "timewindowMs": 60000 + } + }, + "showTitle": false, + "backgroundColor": "rgb(255, 255, 255)", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "16px", + "settings": { + "columns": 3 + }, + "title": "Getting started", + "dropShadow": false, + "enableFullscreen": false, + "widgetStyle": {}, + "widgetCss": "", + "pageSize": 1024, + "noDataDisplayMessage": "", + "showLegend": false + }, + "row": 0, + "col": 0, + "id": "a23185ad-dc46-806c-0e50-5b21fb080ace" + }, + "d26e5cd7-75ef-d475-00c7-1a2d1114efe8": { + "isSystemType": true, + "bundleAlias": "charts", + "typeAlias": "basic_timeseries", + "type": "timeseries", + "title": "New widget", + "image": null, + "description": null, + "sizeX": 8, + "sizeY": 5, + "config": { + "datasources": [ + { + "type": "entity", + "name": null, + "entityAliasId": "d9229b29-3f46-de8d-7fe8-eb0c43c75079", + "filterId": null, + "dataKeys": [ + { + "name": "activeDevicesCount", + "type": "timeseries", + "label": "{i18n:device.devices}", + "color": "#305680", + "settings": { + "hideDataByDefault": false, + "disableDataHiding": false, + "removeFromLegend": false, + "excludeFromStacking": false, + "showLines": true, + "lineWidth": 3, + "fillLines": true, + "showPoints": false, + "showPointsLineWidth": 5, + "showPointsRadius": 3, + "showPointShape": "circle", + "pointShapeFormatter": "var size = radius * Math.sqrt(Math.PI) / 2;\nctx.moveTo(x - size, y - size);\nctx.lineTo(x + size, y + size);\nctx.moveTo(x - size, y + size);\nctx.lineTo(x + size, y - size);", + "showSeparateAxis": false, + "axisPosition": "left", + "comparisonSettings": { + "showValuesForComparison": true, + "comparisonValuesLabel": "", + "color": "" + }, + "thresholds": [] + }, + "_hash": 0.9688095820365725, + "aggregationType": null, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ], + "latestDataKeys": [] + } + ], + "timewindow": { + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false, + "hideAggregation": true, + "hideAggInterval": true, + "hideTimezone": false, + "selectedTab": 1, + "history": { + "historyType": 0, + "timewindowMs": 2592000000, + "interval": 7200000, + "fixedTimewindow": { + "startTimeMs": 1681400576338, + "endTimeMs": 1681486976338 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "NONE", + "limit": 25000 + } + }, + "showTitle": false, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "0px", + "settings": { + "stack": false, + "fontSize": 10, + "fontColor": "#545454", + "showTooltip": true, + "tooltipIndividual": false, + "tooltipCumulative": false, + "hideZeros": false, + "grid": { + "verticalLines": false, + "horizontalLines": false, + "outlineWidth": 0, + "color": "#545454", + "backgroundColor": null, + "tickColor": "#DDDDDD" + }, + "xaxis": { + "title": null, + "showLabels": true, + "color": "#545454" + }, + "yaxis": { + "min": null, + "max": null, + "title": null, + "showLabels": true, + "color": "#545454", + "tickSize": null, + "tickDecimals": 0, + "ticksFormatter": "" + }, + "shadowSize": 0, + "smoothLines": true, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "xaxisSecond": { + "axisPosition": "top", + "title": null, + "showLabels": true + }, + "customLegendEnabled": false, + "dataKeysListForLabels": [] + }, + "title": "Devices activity", + "dropShadow": false, + "enableFullscreen": false, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "useDashboardTimewindow": false, + "displayTimewindow": true, + "showTitleIcon": false, + "titleTooltip": "", + "widgetStyle": { + "padding": "0" + }, + "widgetCss": ".tb-widget-container > .tb-widget {\n border: none !important;\n border-radius: 0 !important;\n box-shadow: none !important;\n}", + "pageSize": 1024, + "noDataDisplayMessage": "", + "showLegend": false, + "legendConfig": { + "direction": "column", + "position": "bottom", + "sortDataKeys": false, + "showMin": false, + "showMax": false, + "showAvg": true, + "showTotal": false, + "showLatest": false + } + }, + "row": 0, + "col": 0, + "id": "d26e5cd7-75ef-d475-00c7-1a2d1114efe8" + } + }, + "states": { + "default": { + "name": "Tenant Administrator Home Page", + "root": true, + "layouts": { + "main": { + "widgets": { + "d70cc256-4c7b-ee06-9905-b8c5e546605f": { + "sizeX": 41, + "sizeY": 16, + "row": 26, + "col": 44, + "mobileOrder": 7, + "mobileHeight": 8 + }, + "867f82cf-ecf2-2d5c-35cb-08c6f2edc3a4": { + "sizeX": 31, + "sizeY": 16, + "row": 42, + "col": 26, + "mobileHide": true + }, + "a23185ad-dc46-806c-0e50-5b21fb080ace": { + "sizeX": 35, + "sizeY": 58, + "row": 0, + "col": 85, + "mobileHide": true + } + }, + "gridSettings": { + "backgroundColor": "#eeeeee", + "columns": 120, + "margin": 12, + "backgroundSizeMode": "100%", + "autoFillHeight": true, + "backgroundImageUrl": null, + "mobileAutoFillHeight": false, + "mobileRowHeight": 20, + "outerMargin": true + } + } + } + }, + "transport_messages": { + "name": "Transport messages", + "root": false, + "layouts": { + "main": { + "widgets": { + "8ee72d43-678c-4e25-e9a8-7d4cfd7a5f8e": { + "sizeX": 24, + "sizeY": 11, + "row": 0, + "col": 0 + } + }, + "gridSettings": { + "backgroundColor": "#ffffff", + "columns": 24, + "margin": 0, + "backgroundSizeMode": "100%", + "autoFillHeight": true, + "backgroundImageUrl": null, + "mobileAutoFillHeight": true, + "mobileRowHeight": 70, + "outerMargin": true + } + } + } + }, + "devices_activity": { + "name": "Devices activity", + "root": false, + "layouts": { + "main": { + "widgets": { + "d26e5cd7-75ef-d475-00c7-1a2d1114efe8": { + "sizeX": 24, + "sizeY": 11, + "row": 0, + "col": 0 + } + }, + "gridSettings": { + "backgroundColor": "#ffffff", + "columns": 24, + "margin": 0, + "outerMargin": true, + "backgroundSizeMode": "100%", + "autoFillHeight": true, + "backgroundImageUrl": null, + "mobileAutoFillHeight": true, + "mobileRowHeight": 70 + } + } + } + } + }, + "entityAliases": { + "ae870700-071f-b3bc-406c-16ba554c5a55": { + "id": "ae870700-071f-b3bc-406c-16ba554c5a55", + "alias": "Tenants", + "filter": { + "type": "entityType", + "resolveMultiple": true, + "entityType": "TENANT" + } + }, + "ca4d90e4-f9ae-6fca-6b09-85815a48d52b": { + "id": "ca4d90e4-f9ae-6fca-6b09-85815a48d52b", + "alias": "TenantProfiles", + "filter": { + "type": "entityType", + "resolveMultiple": true, + "entityType": "TENANT" + } + }, + "a1ddb8fa-90ff-5598-e7f2-e254194d055d": { + "id": "a1ddb8fa-90ff-5598-e7f2-e254194d055d", + "alias": "Devices", + "filter": { + "type": "entityType", + "resolveMultiple": true, + "entityType": "DEVICE" + } + }, + "619cdf00-a042-3b55-124e-194c1b28c236": { + "id": "619cdf00-a042-3b55-124e-194c1b28c236", + "alias": "Assets", + "filter": { + "type": "entityType", + "resolveMultiple": true, + "entityType": "ASSET" + } + }, + "1d97ff7f-8b42-5882-f87b-16f3d0dee4f2": { + "id": "1d97ff7f-8b42-5882-f87b-16f3d0dee4f2", + "alias": "Users", + "filter": { + "type": "entityType", + "resolveMultiple": true, + "entityType": "USER" + } + }, + "0dd2b154-59f4-0f97-da1a-d85f5b5cfe31": { + "id": "0dd2b154-59f4-0f97-da1a-d85f5b5cfe31", + "alias": "Customers", + "filter": { + "type": "entityType", + "resolveMultiple": true, + "entityType": "CUSTOMER" + } + }, + "d9229b29-3f46-de8d-7fe8-eb0c43c75079": { + "id": "d9229b29-3f46-de8d-7fe8-eb0c43c75079", + "alias": "Api Usage State", + "filter": { + "type": "apiUsageState", + "resolveMultiple": true + } + } + }, + "filters": {}, + "timewindow": { + "displayValue": "", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false, + "hideAggregation": false, + "hideAggInterval": false, + "hideTimezone": false, + "selectedTab": 0, + "realtime": { + "realtimeType": 0, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1680168326072, + "endTimeMs": 1680254726072 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 + } + }, + "settings": { + "stateControllerId": "entity", + "showTitle": false, + "showDashboardsSelect": true, + "showEntitiesSelect": true, + "showDashboardTimewindow": true, + "showDashboardExport": true, + "toolbarAlwaysOpen": true, + "titleColor": "rgba(0,0,0,0.870588)", + "showDashboardLogo": false, + "dashboardLogoUrl": null, + "hideToolbar": true, + "showFilters": true, + "showUpdateDashboardImage": true, + "dashboardCss": ".tb-widget-container > .tb-widget {\n border: 1px solid rgba(0, 0, 0, 0.05);\n box-shadow: 0px 5px 16px rgba(0, 0, 0, 0.04);\n border-radius: 12px;\n}\n\n.tb-widget-container > .tb-widget:not([style*=\"padding: 0\"]) {\n padding: 16px !important;\n}\n\n.tb-card-title {\n display: grid;\n}\n\n.tb-home-widget-title {\n font-style: normal;\n font-weight: 500;\n font-size: 14px;\n line-height: 20px;\n letter-spacing: 0.25px;\n color: rgba(0, 0, 0, 0.54);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.tb-home-widget-link {\n position: relative;\n border-bottom: none;\n}\n\n.tb-home-widget-link:hover {\n border-bottom: none;\n}\n\n.tb-home-widget-link:focus {\n border-bottom: none;\n}\n\n.tb-home-widget-link::after {\n content: 'arrow_forward';\n display: inline-block;\n transform: rotate(315deg);\n font-family: 'Material Icons';\n font-weight: normal;\n font-style: normal;\n font-size: 18px;\n color: rgba(0, 0, 0, 0.12);\n vertical-align: bottom;\n margin-left: 6px; \n}\n\n.tb-home-widget-link:hover::after {\n color: inherit;\n}\n\n.tb-home-widget-info-icon {\n color: rgba(0, 0, 0, 0.12);\n font-size: 16px;\n width: 16px;\n height: 16px;\n line-height: 15px;\n vertical-align: middle;\n}\n\n.tb-widget-container > .tb-widget .tb-timewindow {\n font-size: 14px;\n line-height: 20px;\n letter-spacing: 0.25px;\n color: rgba(0, 0, 0, 0.54);\n padding: 0;\n}\n\n.tb-widget-container > .tb-widget .tb-legend-keys .tb-legend-label {\n cursor: pointer;\n user-select: none;\n font-weight: 400;\n font-size: 14px;\n line-height: 20px;\n letter-spacing: 0.2px;\n color: rgba(0, 0, 0, 0.54);\n}\n\n@media screen and (min-width: 960px) and (max-width: 1279px) {\n .tb-widget-container > .tb-widget {\n border-radius: 4px;\n }\n .tb-widget-container > .tb-widget:not([style*=\"padding: 0\"]) {\n padding: 2px !important;\n }\n .tb-hide-md {\n display: none;\n }\n}\n\n@media screen and (min-width: 1280px) and (max-width: 1819px) {\n .tb-widget-container > .tb-widget:not([style*=\"padding: 0\"]) {\n padding: 8px !important;\n }\n .tb-hide-lg {\n display: none;\n }\n}\n\n@media screen and (min-width: 960px) and (max-width: 1819px) {\n .tb-hide-md-lg {\n display: none;\n }\n\n .tb-home-widget-title {\n font-size: 12px;\n line-height: 16px;\n }\n \n .tb-widget-container > .tb-widget .tb-widget-title {\n padding: 0;\n }\n\n .tb-widget-container > .tb-widget .tb-timewindow {\n font-size: 12px;\n line-height: 16px;\n min-height: 24px;\n padding: 0;\n }\n\n .tb-widget-container > .tb-widget .tb-timewindow .mat-mdc-icon-button.tb-mat-32 {\n width: 24px;\n height: 24px;\n line-height: 24px;\n }\n\n .tb-widget-container > .tb-widget .tb-timewindow .mat-mdc-icon-button.tb-mat-32 .mat-icon {\n width: 18px;\n height: 18px;\n font-size: 18px;\n }\n \n .tb-widget-container > .tb-widget tb-legend {\n padding-bottom: 0 !important;\n }\n \n .tb-widget-container > .tb-widget .tb-legend-keys .tb-legend-label {\n font-size: 11px;\n line-height: 16px;\n letter-spacing: 0.25px;\n }\n}\n\n@media screen and (max-width: 959px), screen and (min-width: 1820px) {\n .tb-hide-not-md-lg {\n display: none;\n }\n}\n" + } + }, + "externalId": null, + "name": "Tenant Administrator Home Page" +} \ No newline at end of file diff --git a/ui-ngx/src/app/shared/components/markdown.component.html b/ui-ngx/src/app/shared/components/markdown.component.html index 5b8b9f8af0..67e8cd0da2 100644 --- a/ui-ngx/src/app/shared/components/markdown.component.html +++ b/ui-ngx/src/app/shared/components/markdown.component.html @@ -22,5 +22,5 @@ background: #efefef;"> {{error}}
-
+
diff --git a/ui-ngx/src/app/shared/components/markdown.component.ts b/ui-ngx/src/app/shared/components/markdown.component.ts index 5af7ccce6b..753effc24f 100644 --- a/ui-ngx/src/app/shared/components/markdown.component.ts +++ b/ui-ngx/src/app/shared/components/markdown.component.ts @@ -39,6 +39,7 @@ import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { SHARED_MODULE_TOKEN } from '@shared/components/tokens'; import { deepClone, guid, isDefinedAndNotNull } from '@core/utils'; import { Observable, of, ReplaySubject } from 'rxjs'; +import { coerceBoolean } from '@shared/decorators/coerce-boolean'; let defaultMarkdownStyle; @@ -76,6 +77,10 @@ export class TbMarkdownComponent implements OnChanges { get fallbackToPlainMarkdown(): boolean { return this.fallbackToPlainMarkdownValue; } set fallbackToPlainMarkdown(value: boolean) { this.fallbackToPlainMarkdownValue = coerceBooleanProperty(value); } + @Input() + @coerceBoolean() + usePlainMarkdown = false; + @Output() ready = new EventEmitter(); private lineNumbersValue = false; @@ -124,13 +129,8 @@ export class TbMarkdownComponent implements OnChanges { } template = this.sanitizeCurlyBraces(template); this.markdownContainer.clear(); - const parent = this; - let readyObservable: Observable; - let compileModules = [this.sharedModule]; - if (this.additionalCompileModules) { - compileModules = compileModules.concat(this.additionalCompileModules); - } let styles: string[] = []; + let readyObservable: Observable; if (this.applyDefaultMarkdownStyle) { if (!defaultMarkdownStyle) { defaultMarkdownStyle = deepClone(TbMarkdownComponent['ɵcmp'].styles)[0].replace(/\[_nghost\-%COMP%\]/g, '') @@ -141,70 +141,87 @@ export class TbMarkdownComponent implements OnChanges { if (this.additionalStyles) { styles = styles.concat(this.additionalStyles); } - this.dynamicComponentFactoryService.createDynamicComponentFactory( - class TbMarkdownInstance { - ngOnDestroy(): void { - parent.destroyMarkdownInstanceResources(); - } - }, - template, - compileModules, - true, 1, styles - ).subscribe((factory) => { - this.tbMarkdownInstanceComponentFactory = factory; - const injector: Injector = Injector.create({providers: [], parent: this.markdownContainer.injector}); - try { - this.tbMarkdownInstanceComponentRef = - this.markdownContainer.createComponent(this.tbMarkdownInstanceComponentFactory, 0, injector); - if (this.context) { - for (const propName of Object.keys(this.context)) { - this.tbMarkdownInstanceComponentRef.instance[propName] = this.context[propName]; - } - } - this.tbMarkdownInstanceComponentRef.instance.style = this.style; - readyObservable = this.handleImages(this.tbMarkdownInstanceComponentRef.location.nativeElement); - this.cd.detectChanges(); - this.error = null; - } catch (error) { - readyObservable = this.handleError(template, error, styles); - } - readyObservable.subscribe(() => { - this.ready.emit(); - }); - }, - (error) => { - readyObservable = this.handleError(template, error, styles); + if (this.usePlainMarkdown) { + readyObservable = this.plainMarkdown(template, styles); this.cd.detectChanges(); readyObservable.subscribe(() => { this.ready.emit(); }); - }); + } else { + const parent = this; + let compileModules = [this.sharedModule]; + if (this.additionalCompileModules) { + compileModules = compileModules.concat(this.additionalCompileModules); + } + this.dynamicComponentFactoryService.createDynamicComponentFactory( + class TbMarkdownInstance { + ngOnDestroy(): void { + parent.destroyMarkdownInstanceResources(); + } + }, + template, + compileModules, + true, 1, styles + ).subscribe((factory) => { + this.tbMarkdownInstanceComponentFactory = factory; + const injector: Injector = Injector.create({providers: [], parent: this.markdownContainer.injector}); + try { + this.tbMarkdownInstanceComponentRef = + this.markdownContainer.createComponent(this.tbMarkdownInstanceComponentFactory, 0, injector); + if (this.context) { + for (const propName of Object.keys(this.context)) { + this.tbMarkdownInstanceComponentRef.instance[propName] = this.context[propName]; + } + } + this.tbMarkdownInstanceComponentRef.instance.style = this.style; + readyObservable = this.handleImages(this.tbMarkdownInstanceComponentRef.location.nativeElement); + this.cd.detectChanges(); + this.error = null; + } catch (error) { + readyObservable = this.handleError(template, error, styles); + } + readyObservable.subscribe(() => { + this.ready.emit(); + }); + }, + (error) => { + readyObservable = this.handleError(template, error, styles); + this.cd.detectChanges(); + readyObservable.subscribe(() => { + this.ready.emit(); + }); + }); + } } private handleError(template: string, error, styles?: string[]): Observable { this.error = (error ? error + '' : 'Failed to render markdown!').replace(/\n/g, '
'); this.markdownContainer.clear(); if (this.fallbackToPlainMarkdownValue) { - const element = this.fallbackElement.nativeElement; - let styleElement; - if (styles?.length) { - const markdownClass = 'tb-markdown-view-' + guid(); - let innerStyle = styles.join('\n'); - innerStyle = innerStyle.replace(/\.tb-markdown-view/g, '.' + markdownClass); - template = template.replace(/tb-markdown-view/g, markdownClass); - styleElement = this.renderer.createElement('style'); - styleElement.innerHTML = innerStyle; - } - element.innerHTML = template; - if (styleElement) { - this.renderer.appendChild(element, styleElement); - } - return this.handleImages(element); + return this.plainMarkdown(template, styles); } else { return of(null); } } + private plainMarkdown(template: string, styles?: string[]): Observable { + const element = this.fallbackElement.nativeElement; + let styleElement; + if (styles?.length) { + const markdownClass = 'tb-markdown-view-' + guid(); + let innerStyle = styles.join('\n'); + innerStyle = innerStyle.replace(/\.tb-markdown-view/g, '.' + markdownClass); + template = template.replace(/tb-markdown-view/g, markdownClass); + styleElement = this.renderer.createElement('style'); + styleElement.innerHTML = innerStyle; + } + element.innerHTML = template; + if (styleElement) { + this.renderer.appendChild(element, styleElement); + } + return this.handleImages(element); + } + private handlePlugins(element: HTMLElement): void { if (this.lineNumbers) { this.setPluginClass(element, PrismPlugin.LineNumbers); 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 610cdb209e..0c318d4849 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -5114,6 +5114,9 @@ "title": "Transport messages", "info": "All the messages that came from devices" }, + "activity": { + "title": "Activity" + }, "documentation": { "title": "Documentation", "add-link": "Add link", @@ -5201,6 +5204,52 @@ "content": "

Some text

Follow the documentation on how to do it:

", "how-to-configure-notifications": "How to configure Notifications" } + }, + "tenant-admin": { + "step1": { + "title": "Create device", + "content": "

We will manually provision the device using the UI. Follow the documentation on how to do it:

", + "how-to-create-device": "How to create Device" + }, + "step2": { + "title": "Connect device", + "content-before": "

To connect the device you need to get the device credentials. But we recommend using the default auto-generated one.

  • Go to device table
  • Click on the device row to open device details
  • Press the button \"Copy access token\"

Use simple commands to publish data over HTTP:

", + "ubuntu": { + "install-curl": "Install cURL for Ubuntu:" + }, + "macos": { + "install-curl": "Install cURL for MacOS:" + }, + "windows": { + "install-curl": "Starting Windows 10 b17063, cURL is available by default." + }, + "replace-access-token": "Replace $ACCESS_TOKEN with your device's token:", + "content-after": "

You can also use other protocols such as MQTT, CoAP, etc.

Follow the documentation on how to do it:

", + "how-to-connect-device": "How to connect Device" + }, + "step3": { + "title": "Create dashboard", + "content": "

Create a dashboard to visualize data from entities such as assets, devices, etc.

Follow the documentation on how to do it:

", + "how-to-create-dashboard": "How to create Dashboard" + }, + "step4": { + "title": "Configure alarm rules", + "alarm-rules": "Alarm rules", + "content": "

When the temperature reaches 25°C, we will raise an alarm. Follow the documentation on how to do it:

", + "how-to-configure-alarm-rules": "How to configure Alarm rules" + }, + "step5": { + "title": "Create alarm", + "content-before": "

To trigger the alarm, send a new telemetry value of 26°C or higher.

", + "replace-access-token": "Replace $ACCESS_TOKEN with your device's token:", + "content-after": "

Follow the documentation on how to do it:

", + "how-to-create-alarm": "How to create Alarm" + }, + "step6": { + "title": "Create customer and assign dashboard", + "content": "

By creating end-user dashboards, a customer user can only see his own devices, and data from another customer will be hidden.

Follow the documentation on how to do it:

", + "how-to-create-customer-and-assign-dashboard": "How to create Customer and assign Dashboard" + } } } },