Browse Source

UI: Tenant admin home page

feature/home-page
Igor Kulikov 3 years ago
parent
commit
8b0931e02a
  1. 48
      application/src/main/data/json/system/widget_bundles/home_page_widgets.json
  2. 1
      ui-ngx/src/app/core/http/public-api.ts
  3. 37
      ui-ngx/src/app/core/http/usage-info.service.ts
  4. 12
      ui-ngx/src/app/core/http/user-settings.service.ts
  5. 168
      ui-ngx/src/app/core/services/menu.service.ts
  6. 1
      ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts
  7. 2
      ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts
  8. 14
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/configured-features.component.scss
  9. 2
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/configured-features.component.ts
  10. 16
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-links-widget.component.html
  11. 6
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-links-widget.component.ts
  12. 19
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.html
  13. 70
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page-widget.scss
  14. 10
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page-widgets.module.ts
  15. 52
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/links-widget.component.scss
  16. 51
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-links-widget.component.html
  17. 162
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-links-widget.component.ts
  18. 11
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.html
  19. 51
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.scss
  20. 57
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.ts
  21. 89
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.html
  22. 108
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.scss
  23. 111
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.ts
  24. 18
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/version-info.component.scss
  25. 2
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/version-info.component.ts
  26. 6
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.html
  27. 20
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.ts
  28. 23
      ui-ngx/src/app/modules/home/components/widget/lib/settings/home-page/quick-links-widget-settings.component.html
  29. 52
      ui-ngx/src/app/modules/home/components/widget/lib/settings/home-page/quick-links-widget-settings.component.ts
  30. 12
      ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts
  31. 89
      ui-ngx/src/app/modules/home/pages/home-links/tenant_admin_home_page.raw
  32. 1
      ui-ngx/src/app/shared/models/public-api.ts
  33. 39
      ui-ngx/src/app/shared/models/usage.models.ts
  34. 4
      ui-ngx/src/app/shared/models/user-settings.models.ts
  35. 53
      ui-ngx/src/app/shared/pipe/short-number.pipe.ts
  36. 7
      ui-ngx/src/app/shared/shared.module.ts
  37. 11
      ui-ngx/src/assets/locale/locale.constant-en_US.json

48
application/src/main/data/json/system/widget_bundles/home_page_widgets.json

@ -8,6 +8,25 @@
"name": "Home page widgets"
},
"widgetTypes": [
{
"alias": "getting_started",
"name": "Getting started",
"image": null,
"description": null,
"descriptor": {
"type": "static",
"sizeX": 7.5,
"sizeY": 6.5,
"resources": [],
"templateHtml": "<tb-getting-started-widget\n [ctx]=\"ctx\">\n</tb-getting-started-widget>\n",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n}",
"settingsSchema": "",
"dataKeySettingsSchema": "",
"settingsDirective": "",
"defaultConfig": "{\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Getting started\",\"dropShadow\":true}"
}
},
{
"alias": "documentation_links",
"name": "Documentation links",
@ -24,12 +43,12 @@
"settingsSchema": "",
"dataKeySettingsSchema": "",
"settingsDirective": "tb-doc-links-widget-settings",
"defaultConfig": "{\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"columns\":3},\"title\":\"Documentation links\",\"dropShadow\":true}"
"defaultConfig": "{\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"columns\":3},\"title\":\"Documentation links\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showLegend\":false}"
}
},
{
"alias": "getting_started",
"name": "Getting started",
"alias": "usage_info",
"name": "Usage info",
"image": null,
"description": null,
"descriptor": {
@ -37,13 +56,32 @@
"sizeX": 7.5,
"sizeY": 6.5,
"resources": [],
"templateHtml": "<tb-getting-started-widget\n [ctx]=\"ctx\">\n</tb-getting-started-widget>\n",
"templateHtml": "<tb-usage-info-widget\n [ctx]=\"ctx\">\n</tb-usage-info-widget>\n",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n}",
"settingsSchema": "",
"dataKeySettingsSchema": "",
"settingsDirective": "",
"defaultConfig": "{\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Getting started\",\"dropShadow\":true}"
"defaultConfig": "{\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Usage info\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showLegend\":false}"
}
},
{
"alias": "quick_links",
"name": "Quick links",
"image": null,
"description": null,
"descriptor": {
"type": "static",
"sizeX": 7.5,
"sizeY": 3,
"resources": [],
"templateHtml": "<tb-quick-links-widget\n [ctx]=\"ctx\">\n</tb-quick-links-widget>\n",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n}",
"settingsSchema": "",
"dataKeySettingsSchema": "",
"settingsDirective": "tb-quick-links-widget-settings",
"defaultConfig": "{\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"columns\":3},\"title\":\"Quick links\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showLegend\":false}"
}
}
]

1
ui-ngx/src/app/core/http/public-api.ts

@ -42,3 +42,4 @@ export * from './ui-settings.service';
export * from './user.service';
export * from './user-settings.service';
export * from './widget.service';
export * from './usage-info.service';

37
ui-ngx/src/app/core/http/usage-info.service.ts

@ -0,0 +1,37 @@
///
/// 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 { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils';
import { UsageInfo } from '@shared/models/usage.models';
@Injectable({
providedIn: 'root'
})
export class UsageInfoService {
constructor(
private http: HttpClient
) {}
public getUsageInfo(config?: RequestConfig): Observable<UsageInfo> {
return this.http.get<UsageInfo>('/api/usage',
defaultHttpOptionsFromConfig(config));
}
}

12
ui-ngx/src/app/core/http/user-settings.service.ts

@ -19,7 +19,7 @@ import { Observable } from 'rxjs';
import {
DocumentationLink, DocumentationLinks,
GettingStarted,
initialUserSettings,
initialUserSettings, QuickLinks,
UserSettings,
UserSettingsType
} from '@shared/models/user-settings.models';
@ -67,6 +67,16 @@ export class UserSettingsService {
defaultHttpOptionsFromConfig(config));
}
public getQuickLinks(config?: RequestConfig): Observable<QuickLinks> {
return this.http.get<QuickLinks>(`/api/user/settings/${UserSettingsType.QUICK_LINKS}`,
defaultHttpOptionsFromConfig(config));
}
public updateQuickLinks(quickLinks: QuickLinks, config?: RequestConfig): Observable<void> {
return this.http.put<void>(`/api/user/settings/${UserSettingsType.QUICK_LINKS}`, quickLinks,
defaultHttpOptionsFromConfig(config));
}
public getGettingStarted(config?: RequestConfig): Observable<GettingStarted> {
return this.http.get<GettingStarted>(`/api/user/settings/${UserSettingsType.GETTING_STARTED}`,
defaultHttpOptionsFromConfig(config));

168
ui-ngx/src/app/core/services/menu.service.ts

@ -18,11 +18,10 @@ import { Injectable } from '@angular/core';
import { select, Store } from '@ngrx/store';
import { AppState } from '../core.state';
import { getCurrentOpenedMenuSections, selectAuth, selectIsAuthenticated } from '../auth/auth.selectors';
import { filter, take } from 'rxjs/operators';
import { filter, map, take } from 'rxjs/operators';
import { HomeSection, MenuSection } from '@core/services/menu.models';
import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { Authority } from '@shared/models/authority.enum';
import { guid } from '@core/utils';
import { AuthState } from '@core/auth/auth.models';
import { NavigationEnd, Router } from '@angular/router';
@ -34,6 +33,9 @@ export class MenuService {
currentMenuSections: Array<MenuSection>;
menuSections$: Subject<Array<MenuSection>> = new BehaviorSubject<Array<MenuSection>>([]);
homeSections$: Subject<Array<HomeSection>> = new BehaviorSubject<Array<HomeSection>>([]);
availableMenuLinks$ = this.menuSections$.pipe(
map((items) => this.allMenuLinks(items))
);
constructor(private store: Store<AppState>,
private router: Router) {
@ -91,21 +93,21 @@ export class MenuService {
const sections: Array<MenuSection> = [];
sections.push(
{
id: guid(),
id: 'home',
name: 'home.home',
type: 'link',
path: '/home',
icon: 'home'
},
{
id: guid(),
id: 'tenants',
name: 'tenant.tenants',
type: 'link',
path: '/tenants',
icon: 'supervisor_account'
},
{
id: guid(),
id: 'tenantProfiles',
name: 'tenant-profile.tenant-profiles',
type: 'link',
path: '/tenantProfiles',
@ -113,21 +115,21 @@ export class MenuService {
isMdiIcon: true
},
{
id: guid(),
id: 'resources',
name: 'admin.resources',
type: 'toggle',
path: '/resources',
icon: 'folder',
pages: [
{
id: guid(),
id: 'resourcesWidgetsBundles',
name: 'widget.widget-library',
type: 'link',
path: '/resources/widgets-bundles',
icon: 'now_widgets'
},
{
id: guid(),
id: 'resourcesResourcesLibrary',
name: 'resource.resources-library',
type: 'link',
path: '/resources/resources-library',
@ -137,7 +139,7 @@ export class MenuService {
]
},
{
id: guid(),
id: 'notification',
name: 'notification.notification-center',
type: 'link',
path: '/notification',
@ -145,28 +147,28 @@ export class MenuService {
isMdiIcon: true,
pages: [
{
id: guid(),
id: 'notificationInbox',
name: 'notification.inbox',
type: 'link',
path: '/notification/inbox',
icon: 'inbox'
},
{
id: guid(),
id: 'notificationSent',
name: 'notification.sent',
type: 'link',
path: '/notification/sent',
icon: 'outbox'
},
{
id: guid(),
id: 'notificationRecipients',
name: 'notification.recipients',
type: 'link',
path: '/notification/recipients',
icon: 'contacts'
},
{
id: guid(),
id: 'notificationTemplates',
name: 'notification.templates',
type: 'link',
path: '/notification/templates',
@ -174,7 +176,7 @@ export class MenuService {
isMdiIcon: true
},
{
id: guid(),
id: 'notificationRules',
name: 'notification.rules',
type: 'link',
path: '/notification/rules',
@ -184,28 +186,28 @@ export class MenuService {
]
},
{
id: guid(),
id: 'settings',
name: 'admin.settings',
type: 'link',
path: '/settings',
icon: 'settings',
pages: [
{
id: guid(),
id: 'settingsGeneral',
name: 'admin.general',
type: 'link',
path: '/settings/general',
icon: 'settings_applications'
},
{
id: guid(),
id: 'settingsOutgoingMail',
name: 'admin.outgoing-mail',
type: 'link',
path: '/settings/outgoing-mail',
icon: 'mail'
},
{
id: guid(),
id: 'settingsNotifications',
name: 'admin.notifications',
type: 'link',
path: '/settings/notifications',
@ -213,7 +215,7 @@ export class MenuService {
isMdiIcon: true
},
{
id: guid(),
id: 'settingsQueues',
name: 'admin.queues',
type: 'link',
path: '/settings/queues',
@ -222,21 +224,21 @@ export class MenuService {
]
},
{
id: guid(),
id: 'securitySettings',
name: 'security.security',
type: 'toggle',
path: '/security-settings',
icon: 'security',
pages: [
{
id: guid(),
id: 'securitySettingsGeneral',
name: 'admin.general',
type: 'link',
path: '/security-settings/general',
icon: 'settings_applications'
},
{
id: guid(),
id: 'securitySettings2fa',
name: 'admin.2fa.2fa',
type: 'link',
path: '/security-settings/2fa',
@ -244,7 +246,7 @@ export class MenuService {
isMdiIcon: true
},
{
id: guid(),
id: 'securitySettingsOauth2',
name: 'admin.oauth2.oauth2',
type: 'link',
path: '/security-settings/oauth2',
@ -340,49 +342,49 @@ export class MenuService {
const sections: Array<MenuSection> = [];
sections.push(
{
id: guid(),
id: 'home',
name: 'home.home',
type: 'link',
path: '/home',
icon: 'home'
},
{
id: guid(),
id: 'alarms',
name: 'alarm.alarms',
type: 'link',
path: '/alarms',
icon: 'notifications'
},
{
id: guid(),
id: 'dashboards',
name: 'dashboard.dashboards',
type: 'link',
path: '/dashboards',
icon: 'dashboards'
},
{
id: guid(),
id: 'entities',
name: 'entity.entities',
type: 'toggle',
path: '/entities',
icon: 'category',
pages: [
{
id: guid(),
id: 'entitiesDevices',
name: 'device.devices',
type: 'link',
path: '/entities/devices',
icon: 'devices_other'
},
{
id: guid(),
id: 'entitiesAssets',
name: 'asset.assets',
type: 'link',
path: '/entities/assets',
icon: 'domain'
},
{
id: guid(),
id: 'entitiesEntityViews',
name: 'entity-view.entity-views',
type: 'link',
path: '/entities/entityViews',
@ -391,14 +393,14 @@ export class MenuService {
]
},
{
id: guid(),
id: 'profiles',
name: 'profiles.profiles',
type: 'toggle',
path: '/profiles',
icon: 'badge',
pages: [
{
id: guid(),
id: 'profilesDeviceProfiles',
name: 'device-profile.device-profiles',
type: 'link',
path: '/profiles/deviceProfiles',
@ -406,7 +408,7 @@ export class MenuService {
isMdiIcon: true
},
{
id: guid(),
id: 'profilesAssetProfiles',
name: 'asset-profile.asset-profiles',
type: 'link',
path: '/profiles/assetProfiles',
@ -416,14 +418,14 @@ export class MenuService {
]
},
{
id: guid(),
id: 'customers',
name: 'customer.customers',
type: 'link',
path: '/customers',
icon: 'supervisor_account'
},
{
id: guid(),
id: 'ruleChains',
name: 'rulechain.rulechains',
type: 'link',
path: '/ruleChains',
@ -433,21 +435,21 @@ export class MenuService {
if (authState.edgesSupportEnabled) {
sections.push(
{
id: guid(),
id: 'edgeManagement',
name: 'edge.management',
type: 'toggle',
path: '/edgeManagement',
icon: 'settings_input_antenna',
pages: [
{
id: guid(),
id: 'edgeManagementInstances',
name: 'edge.instances',
type: 'link',
path: '/edgeManagement/instances',
icon: 'router'
},
{
id: guid(),
id: 'edgeManagementRuleChains',
name: 'edge.rulechain-templates',
type: 'link',
path: '/edgeManagement/ruleChains',
@ -459,21 +461,21 @@ export class MenuService {
}
sections.push(
{
id: guid(),
id: 'features',
name: 'feature.advanced-features',
type: 'toggle',
path: '/features',
icon: 'construction',
pages: [
{
id: guid(),
id: 'featuresOtaUpdates',
name: 'ota-update.ota-updates',
type: 'link',
path: '/features/otaUpdates',
icon: 'memory'
},
{
id: guid(),
id: 'featuresVc',
name: 'version-control.version-control',
type: 'link',
path: '/features/vc',
@ -482,21 +484,21 @@ export class MenuService {
]
},
{
id: guid(),
id: 'resources',
name: 'admin.resources',
type: 'toggle',
path: '/resources',
icon: 'folder',
pages: [
{
id: guid(),
id: 'resourcesWidgetsBundles',
name: 'widget.widget-library',
type: 'link',
path: '/resources/widgets-bundles',
icon: 'now_widgets'
},
{
id: guid(),
id: 'resourcesResourcesLibrary',
name: 'resource.resources-library',
type: 'link',
path: '/resources/resources-library',
@ -506,7 +508,7 @@ export class MenuService {
]
},
{
id: guid(),
id: 'notification',
name: 'notification.notification-center',
type: 'link',
path: '/notification',
@ -514,28 +516,28 @@ export class MenuService {
isMdiIcon: true,
pages: [
{
id: guid(),
id: 'notificationInbox',
name: 'notification.inbox',
type: 'link',
path: '/notification/inbox',
icon: 'inbox'
},
{
id: guid(),
id: 'notificationSent',
name: 'notification.sent',
type: 'link',
path: '/notification/sent',
icon: 'outbox'
},
{
id: guid(),
id: 'notificationRecipients',
name: 'notification.recipients',
type: 'link',
path: '/notification/recipients',
icon: 'contacts'
},
{
id: guid(),
id: 'notificationTemplates',
name: 'notification.templates',
type: 'link',
path: '/notification/templates',
@ -543,7 +545,7 @@ export class MenuService {
isMdiIcon: true
},
{
id: guid(),
id: 'notificationRules',
name: 'notification.rules',
type: 'link',
path: '/notification/rules',
@ -553,28 +555,28 @@ export class MenuService {
]
},
{
id: guid(),
id: 'usage',
name: 'api-usage.api-usage',
type: 'link',
path: '/usage',
icon: 'insert_chart'
},
{
id: guid(),
id: 'settings',
name: 'admin.settings',
type: 'link',
path: '/settings',
icon: 'settings',
pages: [
{
id: guid(),
id: 'settingsHome',
name: 'admin.home',
type: 'link',
path: '/settings/home',
icon: 'settings_applications'
},
{
id: guid(),
id: 'settingsNotifications',
name: 'admin.notifications',
type: 'link',
path: '/settings/notifications',
@ -582,14 +584,14 @@ export class MenuService {
isMdiIcon: true
},
{
id: guid(),
id: 'settingsRepository',
name: 'admin.repository',
type: 'link',
path: '/settings/repository',
icon: 'manage_history'
},
{
id: guid(),
id: 'settingsAutoCommit',
name: 'admin.auto-commit',
type: 'link',
path: '/settings/auto-commit',
@ -598,14 +600,14 @@ export class MenuService {
]
},
{
id: guid(),
id: 'securitySettings',
name: 'security.security',
type: 'toggle',
path: '/security-settings',
icon: 'security',
pages: [
{
id: guid(),
id: 'securitySettingsAuditLogs',
name: 'audit-log.audit-logs',
type: 'link',
path: '/security-settings/auditLogs',
@ -781,49 +783,49 @@ export class MenuService {
const sections: Array<MenuSection> = [];
sections.push(
{
id: guid(),
id: 'home',
name: 'home.home',
type: 'link',
path: '/home',
icon: 'home'
},
{
id: guid(),
id: 'alarms',
name: 'alarm.alarms',
type: 'link',
path: '/alarms',
icon: 'notifications'
},
{
id: guid(),
id: 'dashboards',
name: 'dashboard.dashboards',
type: 'link',
path: '/dashboards',
icon: 'dashboards'
},
{
id: guid(),
id: 'entities',
name: 'entity.entities',
type: 'toggle',
path: '/entities',
icon: 'category',
pages: [
{
id: guid(),
id: 'entitiesDevices',
name: 'device.devices',
type: 'link',
path: '/entities/devices',
icon: 'devices_other'
},
{
id: guid(),
id: 'entitiesAssets',
name: 'asset.assets',
type: 'link',
path: '/entities/assets',
icon: 'domain'
},
{
id: guid(),
id: 'entitiesEntityViews',
name: 'entity-view.entity-views',
type: 'link',
path: '/entities/entityViews',
@ -835,7 +837,7 @@ export class MenuService {
if (authState.edgesSupportEnabled) {
sections.push(
{
id: guid(),
id: 'edgeManagementInstances',
name: 'edge.edge-instances',
type: 'link',
path: '/edgeManagement/instances',
@ -845,7 +847,7 @@ export class MenuService {
}
sections.push(
{
id: guid(),
id: 'notification',
name: 'notification.notification-center',
type: 'link',
path: '/notification',
@ -853,7 +855,7 @@ export class MenuService {
isMdiIcon: true,
pages: [
{
id: guid(),
id: 'notificationInbox',
name: 'notification.inbox',
type: 'link',
path: '/notification/inbox',
@ -928,6 +930,19 @@ export class MenuService {
return homeSections;
}
private allMenuLinks(sections: Array<MenuSection>): Array<MenuSection> {
const result: Array<MenuSection> = [];
for (const section of sections) {
if (section.type === 'link') {
result.push(section);
}
if (section.pages && section.pages.length) {
result.push(...this.allMenuLinks(section.pages));
}
}
return result;
}
public menuSections(): Observable<Array<MenuSection>> {
return this.menuSections$;
}
@ -936,5 +951,20 @@ export class MenuService {
return this.homeSections$;
}
}
public availableMenuLinks(): Observable<Array<MenuSection>> {
return this.availableMenuLinks$;
}
public menuLinkById(id: string): Observable<MenuSection | undefined> {
return this.availableMenuLinks$.pipe(
map((links) => links.find(link => link.id === id))
);
}
public menuLinksByIds(ids: string[]): Observable<Array<MenuSection>> {
return this.availableMenuLinks$.pipe(
map((links) => links.filter(link => ids.includes(link.id)))
);
}
}

1
ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts

@ -217,6 +217,7 @@ export interface TbFlotKeySettings {
removeFromLegend: boolean;
showLines: boolean;
fillLines: boolean;
fillLinesOpacity: number;
showPoints: boolean;
showPointShape: string;
pointShapeFormatter: string;

2
ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts

@ -408,7 +408,7 @@ export class TbFlot {
}
}
series.lines = {
fill: keySettings.fillLines === true
fill: keySettings.fillLines === true ? (keySettings.fillLinesOpacity || 0.4) : false
};
if (this.settings.stack && !this.comparisonEnabled) {

14
ui-ngx/src/app/modules/home/components/widget/lib/home-page/configured-features.component.scss

@ -17,23 +17,9 @@
@import "../../../../../../../scss/constants";
:host {
.tb-card-content {
width: 100%;
height: 100%;
}
.tb-title {
margin-top: 8px;
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-info-icon {

2
ui-ngx/src/app/modules/home/components/widget/lib/home-page/configured-features.component.ts

@ -30,7 +30,7 @@ import { MediaBreakpoints } from '@shared/models/constants';
@Component({
selector: 'tb-configured-features',
templateUrl: './configured-features.component.html',
styleUrls: ['./configured-features.component.scss']
styleUrls: ['./home-page-widget.scss', './configured-features.component.scss']
})
export class ConfiguredFeaturesComponent extends PageComponent implements OnInit, OnDestroy {

16
ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-links-widget.component.html

@ -26,20 +26,20 @@
<mat-icon>edit</mat-icon>
</button>
</div>
<mat-grid-list class="tb-docs-list" fxFlex [cols]="columns" [rowHeight]="rowHeight" [gutterSize]="gutterSize">
<mat-grid-tile class="tb-docs-tile" *ngFor="let docLink of documentationLinks?.links">
<a fxFlex class="tb-doc-button"
<mat-grid-list class="tb-links-list" fxFlex [cols]="columns" [rowHeight]="rowHeight" [gutterSize]="gutterSize">
<mat-grid-tile class="tb-links-tile" *ngFor="let docLink of documentationLinks?.links">
<a fxFlex class="tb-link-button"
[href]="docLink.link" target="_blank">
<div class="tb-doc-container">
<div class="tb-doc-icon-container">
<div class="tb-link-container">
<div class="tb-link-icon-container">
<mat-icon color="primary">{{ docLink.icon }}</mat-icon>
</div>
<div class="tb-doc-text">{{ docLink.name }}</div>
<div class="tb-link-text">{{ docLink.name }}</div>
</div>
</a>
</mat-grid-tile>
<mat-grid-tile class="tb-docs-tile">
<div fxFlex class="tb-add-doc-button"
<mat-grid-tile class="tb-links-tile">
<div fxFlex class="tb-add-link-button"
matTooltip="{{ 'widgets.documentation.add-link' | translate }}"
matTooltipPosition="above"
(click)="addLink()">

6
ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-links-widget.component.ts

@ -24,10 +24,6 @@ import { DocumentationLink, DocumentationLinks } from '@shared/models/user-setti
import { UserSettingsService } from '@core/http/user-settings.service';
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { WidgetContext } from '@home/models/widget-component.models';
import {
ImportDialogCsvComponent,
ImportDialogCsvData
} from '@home/components/import-export/import-dialog-csv.component';
import { MatDialog } from '@angular/material/dialog';
import { AddDocLinkDialogComponent } from '@home/components/widget/lib/home-page/add-doc-link-dialog.component';
import {
@ -100,7 +96,7 @@ interface DocLinksWidgetSettings {
@Component({
selector: 'tb-doc-links-widget',
templateUrl: './doc-links-widget.component.html',
styleUrls: ['./doc-links-widget.component.scss']
styleUrls: ['./home-page-widget.scss', './links-widget.component.scss']
})
export class DocLinksWidgetComponent extends PageComponent implements OnInit, OnDestroy {

19
ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.html

@ -111,10 +111,21 @@
</ng-template>
<div [innerHTML]="'widgets.getting-started.tenant-admin.step2.content-before' | translate | safe: 'html'"></div>
<div class="tb-bordered-content">
<tb-toggle-header #publishCommandSteps value="ubuntu" name="publishCommandSteps">
<mat-button-toggle value="ubuntu">Ubuntu</mat-button-toggle>
<mat-button-toggle value="macos">MacOS</mat-button-toggle>
<mat-button-toggle value="windows">Windows</mat-button-toggle>
<tb-toggle-header #publishCommandSteps value="ubuntu" name="publishCommandSteps" useSelectOnMdLg="false"
[options]="[
{
name: 'Ubuntu',
value: 'ubuntu'
},
{
name: 'MacOS',
value: 'macos'
},
{
name: 'Windows',
value: 'windows'
}
]">
</tb-toggle-header>
<ng-container [ngSwitch]="publishCommandSteps.value">
<ng-template [ngSwitchCase]="'ubuntu'">

70
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;
}
}
}

10
ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page-widgets.module.ts

@ -29,6 +29,8 @@ 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';
@NgModule({
declarations:
@ -42,7 +44,9 @@ import { ToggleHeaderComponent } from '@home/components/widget/lib/home-page/tog
EditDocLinksDialogComponent,
GettingStartedWidgetComponent,
GettingStartedCompletedDialogComponent,
ToggleHeaderComponent
ToggleHeaderComponent,
UsageInfoWidgetComponent,
QuickLinksWidgetComponent
],
imports: [
CommonModule,
@ -58,7 +62,9 @@ import { ToggleHeaderComponent } from '@home/components/widget/lib/home-page/tog
EditDocLinksDialogComponent,
GettingStartedWidgetComponent,
GettingStartedCompletedDialogComponent,
ToggleHeaderComponent
ToggleHeaderComponent,
UsageInfoWidgetComponent,
QuickLinksWidgetComponent
]
})
export class HomePageWidgetsModule { }

52
ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-links-widget.component.scss → 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;

51
ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-links-widget.component.html

@ -0,0 +1,51 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-card-content" fxLayout="column" fxLayoutGap="8px">
<div class="tb-card-header">
<div class="tb-title">{{ 'widgets.quick-links.title' | translate }}</div>
<button class="tb-title-icon"
matTooltip="{{ 'action.edit' | translate }}"
matTooltipPosition="above"
mat-icon-button
(click)="edit()">
<mat-icon>edit</mat-icon>
</button>
</div>
<mat-grid-list class="tb-links-list" fxFlex [cols]="columns" [rowHeight]="rowHeight" [gutterSize]="gutterSize">
<mat-grid-tile class="tb-links-tile" *ngFor="let quickLink of menuLinks$() | async">
<a fxFlex class="tb-link-button"
[routerLink]="quickLink.path">
<div class="tb-link-container">
<div class="tb-link-icon-container">
<mat-icon *ngIf="!quickLink.isMdiIcon" color="primary">{{ quickLink.icon }}</mat-icon>
<mat-icon *ngIf="quickLink.isMdiIcon" color="primary" [svgIcon]="quickLink.icon"></mat-icon>
</div>
<div class="tb-link-text">{{ quickLink.name | translate }}</div>
</div>
</a>
</mat-grid-tile>
<mat-grid-tile class="tb-links-tile">
<div fxFlex class="tb-add-link-button"
matTooltip="{{ 'widgets.quick-links.add-link' | translate }}"
matTooltipPosition="above"
(click)="addLink()">
<mat-icon class="tb-add-icon">add</mat-icon>
</div>
</mat-grid-tile>
</mat-grid-list>
</div>

162
ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-links-widget.component.ts

@ -0,0 +1,162 @@
///
/// 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';
const defaultQuickLinksMap = new Map<Authority, QuickLinks>(
[
[Authority.SYS_ADMIN, {
links: ['tenants', 'tenantProfiles']
}],
[Authority.TENANT_ADMIN, {
links: ['alarms', 'dashboards', 'entitiesDevices']
}],
[Authority.CUSTOMER_USER, {
links: ['alarms', 'dashboards', 'entitiesDevices']
}]
]
);
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<AppState>,
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<Array<MenuSection>> {
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<EditDocLinksDialogComponent, EditDocLinksDialogData,
boolean>(EditDocLinksDialogComponent, {
disableClose: true,
autoFocus: false,
data: {
docLinks: this.documentationLinks
},
panelClass: ['tb-dialog', 'tb-fullscreen-dialog']
}).afterClosed().subscribe(
(result) => {
if (result) {
this.loadDocLinks();
}
}); */
}
addLink() {
/* this.dialog.open<AddDocLinkDialogComponent, any,
DocumentationLink>(AddDocLinkDialogComponent, {
disableClose: true,
autoFocus: false,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog']
}).afterClosed().subscribe(
(docLink) => {
if (docLink) {
this.documentationLinks.links.push(docLink);
this.cd.markForCheck();
this.userSettingsService.updateDocumentationLinks(this.documentationLinks).subscribe();
}
}); */
}
}

11
ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.html

@ -15,6 +15,13 @@
limitations under the License.
-->
<mat-button-toggle-group class="tb-toggle-header" #toggleGroup="matButtonToggleGroup" [name]="name">
<ng-content></ng-content>
<mat-button-toggle-group *ngIf="!isMdLg || !useSelectOnMdLg; else select" class="tb-toggle-header" [name]="name" [(value)]="value">
<mat-button-toggle *ngFor="let option of options" [value]="option.value">{{ option.name }}</mat-button-toggle>
</mat-button-toggle-group>
<ng-template #select>
<mat-form-field appearance="outline" class="tb-toggle-header-select" subscriptSizing="dynamic">
<mat-select [(value)]="value">
<mat-option *ngFor="let option of options" [value]="option.value"> {{ option.name }}</mat-option>
</mat-select>
</mat-form-field>
</ng-template>

51
ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.scss

@ -24,7 +24,6 @@
padding: 2px;
border: none;
background: rgba(0, 0, 0, 0.06);
margin-bottom: 8px;
.mat-button-toggle + .mat-button-toggle {
border-left: none;
}
@ -63,7 +62,6 @@
@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;
@ -84,4 +82,53 @@
}
}
}
.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);
}
}
}
}

57
ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.ts

@ -32,51 +32,56 @@ 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 { 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, AfterViewInit {
export class ToggleHeaderComponent extends PageComponent implements OnInit {
@ViewChild('toggleGroup')
toggleGroup: MatButtonToggleGroup;
@Input()
value: any;
@ContentChildren(MatButtonToggle)
_buttonToggles: QueryList<MatButtonToggle>;
@Input()
options: ToggleHeaderOption[];
innerValue: any;
@Input()
name: string;
@Input()
set value(value: any) {
this.innerValue = value;
}
@coerceBoolean()
useSelectOnMdLg = true;
get value(): any {
return this.toggleGroup?.value;
}
isMdLg: boolean;
@Input()
name: string;
private observeBreakpointSubscription: Subscription;
constructor(protected store: Store<AppState>) {
constructor(protected store: Store<AppState>,
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();
}
);
}
ngAfterViewInit() {
for (const toggle of this._buttonToggles) {
toggle.buttonToggleGroup = this.toggleGroup;
if (this.innerValue === toggle.value) {
toggle.checked = true;
}
}
}
}

89
ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.html

@ -0,0 +1,89 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-card-content" fxLayout="column" fxLayoutGap="8px">
<div class="tb-card-header">
<a class="tb-title-link" routerLink="/usage">{{ 'widgets.usage-info.title' | translate }}</a>
<tb-toggle-header #usageToggle [value]="toggleValue" name="usageToggle" [options]="[
{
name: ctx.translate.instant('widgets.usage-info.entities'),
value: 'entities'
},
{
name: ctx.translate.instant('widgets.usage-info.api-calls'),
value: 'apiCalls'
}
]">
</tb-toggle-header>
</div>
<ng-container [ngSwitch]="usageToggle.value">
<ng-template [ngSwitchCase]="'entities'">
<div fxFlex class="tb-usage-list">
<div class="tb-usage-items">
<div class="tb-usage-item" [ngClass]="{'critical': entityItemCritical.devices}" translate>device.devices</div>
<div class="tb-usage-item" [ngClass]="{'critical': entityItemCritical.assets}" translate>asset.assets</div>
<div class="tb-usage-item" [ngClass]="{'critical': entityItemCritical.users}" translate>user.users</div>
<div class="tb-usage-item" [ngClass]="{'critical': entityItemCritical.dashboards}" translate>dashboard.dashboards</div>
<div class="tb-usage-item" [ngClass]="{'critical': entityItemCritical.customers}" translate>customer.customers</div>
</div>
<div class="tb-usage-items-values">
<div class="tb-usage-items-counts">
<div class="tb-usage-item-counts" [ngClass]="{'critical': entityItemCritical.devices}">{{ usageInfo?.devices | shortNumber }} / {{ maxValue(usageInfo?.maxDevices) }}</div>
<div class="tb-usage-item-counts" [ngClass]="{'critical': entityItemCritical.assets}">{{ usageInfo?.assets | shortNumber }} / {{ maxValue(usageInfo?.maxAssets) }}</div>
<div class="tb-usage-item-counts" [ngClass]="{'critical': entityItemCritical.users}">{{ usageInfo?.users | shortNumber }} / {{ maxValue(usageInfo?.maxUsers) }}</div>
<div class="tb-usage-item-counts" [ngClass]="{'critical': entityItemCritical.dashboards}">{{ usageInfo?.dashboards | shortNumber }} / {{ maxValue(usageInfo?.maxDashboards) }}</div>
<div class="tb-usage-item-counts" [ngClass]="{'critical': entityItemCritical.customers}">{{ usageInfo?.customers | shortNumber }} / {{ maxValue(usageInfo?.maxCustomers) }}</div>
</div>
<div class="tb-usage-items-progress">
<mat-progress-bar [ngClass]="{'critical': entityItemCritical.devices}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.devices, usageInfo?.maxDevices)"></mat-progress-bar>
<mat-progress-bar [ngClass]="{'critical': entityItemCritical.assets}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.assets, usageInfo?.maxAssets)"></mat-progress-bar>
<mat-progress-bar [ngClass]="{'critical': entityItemCritical.users}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.users, usageInfo?.maxUsers)"></mat-progress-bar>
<mat-progress-bar [ngClass]="{'critical': entityItemCritical.dashboards}"color="primary" mode="determinate" [value]="progressValue(usageInfo?.dashboards, usageInfo?.maxDashboards)"></mat-progress-bar>
<mat-progress-bar [ngClass]="{'critical': entityItemCritical.customers}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.customers, usageInfo?.maxCustomers)"></mat-progress-bar>
</div>
</div>
</div>
</ng-template>
<ng-template [ngSwitchCase]="'apiCalls'">
<div fxFlex class="tb-usage-list">
<div class="tb-usage-items">
<div class="tb-usage-item" [ngClass]="{'critical': apiCallItemCritical.transportMessages}" translate>api-usage.transport-messages</div>
<div class="tb-usage-item" [ngClass]="{'critical': apiCallItemCritical.jsExecutions}" translate>api-usage.javascript</div>
<div class="tb-usage-item" [ngClass]="{'critical': apiCallItemCritical.alarms}" translate>api-usage.alarms-created</div>
<div class="tb-usage-item" [ngClass]="{'critical': apiCallItemCritical.emails}" translate>api-usage.email</div>
<div class="tb-usage-item" [ngClass]="{'critical': apiCallItemCritical.sms}" translate>api-usage.sms</div>
</div>
<div class="tb-usage-items-values">
<div class="tb-usage-items-counts">
<div class="tb-usage-item-counts" [ngClass]="{'critical': apiCallItemCritical.transportMessages}">{{ usageInfo?.transportMessages | shortNumber }} / {{ maxValue(usageInfo?.maxTransportMessages) }}</div>
<div class="tb-usage-item-counts" [ngClass]="{'critical': apiCallItemCritical.jsExecutions}">{{ usageInfo?.jsExecutions | shortNumber }} / {{ maxValue(usageInfo?.maxJsExecutions) }}</div>
<div class="tb-usage-item-counts" [ngClass]="{'critical': apiCallItemCritical.alarms}">{{ usageInfo?.alarms | shortNumber }} / {{ maxValue(usageInfo?.maxAlarms) }}</div>
<div class="tb-usage-item-counts" [ngClass]="{'critical': apiCallItemCritical.emails}">{{ usageInfo?.emails | shortNumber }} / {{ maxValue(usageInfo?.maxEmails) }}</div>
<div class="tb-usage-item-counts" [ngClass]="{'critical': apiCallItemCritical.sms}">{{ usageInfo?.sms | shortNumber }} / {{ maxValue(usageInfo?.maxSms) }}</div>
</div>
<div class="tb-usage-items-progress">
<mat-progress-bar [ngClass]="{'critical': apiCallItemCritical.transportMessages}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.transportMessages, usageInfo?.maxTransportMessages)"></mat-progress-bar>
<mat-progress-bar [ngClass]="{'critical': apiCallItemCritical.jsExecutions}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.jsExecutions, usageInfo?.maxJsExecutions)"></mat-progress-bar>
<mat-progress-bar [ngClass]="{'critical': apiCallItemCritical.alarms}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.alarms, usageInfo?.maxAlarms)"></mat-progress-bar>
<mat-progress-bar [ngClass]="{'critical': apiCallItemCritical.emails}"color="primary" mode="determinate" [value]="progressValue(usageInfo?.emails, usageInfo?.maxEmails)"></mat-progress-bar>
<mat-progress-bar [ngClass]="{'critical': apiCallItemCritical.sms}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.sms, usageInfo?.maxSms)"></mat-progress-bar>
</div>
</div>
</div>
</ng-template>
</ng-container>
</div>

108
ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.scss

@ -0,0 +1,108 @@
/**
* 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;
}
.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;
}
}
}
}
}

111
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<AppState>,
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;
}
}
}

18
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;

2
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 {

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

@ -51,9 +51,13 @@
<mat-label translate>widgets.chart.line-width</mat-label>
<input matInput type="number" min="0" formControlName="lineWidth">
</mat-form-field>
<mat-checkbox fxFlex formControlName="fillLines">
<mat-checkbox formControlName="fillLines">
{{ 'widgets.chart.fill-line' | translate }}
</mat-checkbox>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.chart.fill-line-opacity</mat-label>
<input matInput type="number" min="0" max="1" step="0.1" formControlName="fillLinesOpacity">
</mat-form-field>
</section>
</ng-template>
</mat-expansion-panel>

20
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});

23
ui-ngx/src/app/modules/home/components/widget/lib/settings/home-page/quick-links-widget-settings.component.html

@ -0,0 +1,23 @@
<!--
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.
-->
<section class="tb-widget-settings" [formGroup]="quickLinksWidgetSettingsForm" fxLayout="column">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.quick-links.columns</mat-label>
<input required matInput type="number" step="1" min="1" max="20" formControlName="columns">
</mat-form-field>
</section>

52
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<AppState>,
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)]]
});
}
}

12
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<IWidgetSettingsCo
'tb-map-widget-settings': MapWidgetSettingsComponent,
'tb-route-map-widget-settings': RouteMapWidgetSettingsComponent,
'tb-trip-animation-widget-settings': TripAnimationWidgetSettingsComponent,
'tb-doc-links-widget-settings': DocLinksWidgetSettingsComponent
'tb-doc-links-widget-settings': DocLinksWidgetSettingsComponent,
'tb-quick-links-widget-settings': QuickLinksWidgetSettingsComponent
};

89
ui-ngx/src/app/modules/home/pages/home-links/tenant_admin_home_page.raw

@ -48,7 +48,7 @@
"padding": "16px",
"settings": {
"useMarkdownTextFunction": false,
"markdownTextPattern": "<div class=\"tb-card-content\">\n <div fxLayout=\"row\" fxLayoutAlign=\"space-between start\">\n <div class=\"tb-home-widget-title\">{{ 'widgets.activity.title' | translate }}</div>\n <tb-toggle-header #activityStates value=\"devices\" name=\"activityStates\">\n <mat-button-toggle value=\"devices\">{{ 'device.devices' | translate }}</mat-button-toggle>\n <mat-button-toggle value=\"transportMessages\">{{ 'widgets.transport-messages.title' | translate }}</mat-button-toggle>\n </tb-toggle-header>\n </div>\n <ng-container [ngSwitch]=\"activityStates.value\">\n <ng-template [ngSwitchCase]=\"'devices'\">\n <tb-dashboard-state fxFlex stateId=\"devices_activity\" [ctx]=\"ctx\"></tb-dashboard-state>\n </ng-template>\n <ng-template [ngSwitchCase]=\"'transportMessages'\">\n <tb-dashboard-state fxFlex stateId=\"transport_messages\" [ctx]=\"ctx\"></tb-dashboard-state>\n </ng-template>\n </ng-container>\n</div>",
"markdownTextPattern": "<div class=\"tb-card-content\">\n <div fxLayout=\"row\" fxLayoutAlign=\"space-between start\">\n <div class=\"tb-home-widget-title\">{{ 'widgets.activity.title' | translate }}</div>\n <tb-toggle-header #activityStates value=\"devices\" name=\"activityStates\"\n [options]=\"[\n {\n name: ctx.translate.instant('device.devices'),\n value: 'devices'\n },\n {\n name: ctx.translate.instant('widgets.transport-messages.title'),\n value: 'transportMessages'\n }\n ]\">\n </tb-toggle-header>\n </div>\n <ng-container [ngSwitch]=\"activityStates.value\">\n <ng-template [ngSwitchCase]=\"'devices'\">\n <tb-dashboard-state fxFlex stateId=\"devices_activity\" [ctx]=\"ctx\"></tb-dashboard-state>\n </ng-template>\n <ng-template [ngSwitchCase]=\"'transportMessages'\">\n <tb-dashboard-state fxFlex stateId=\"transport_messages\" [ctx]=\"ctx\"></tb-dashboard-state>\n </ng-template>\n </ng-container>\n</div>",
"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"
},
@ -308,7 +308,7 @@
"filterId": null,
"dataKeys": [
{
"name": "activeDevicesCount",
"name": "activeDevicesCountHourly",
"type": "timeseries",
"label": "{i18n:device.devices}",
"color": "#305680",
@ -320,11 +320,8 @@
"showLines": true,
"lineWidth": 3,
"fillLines": true,
"fillLinesOpacity": 0.04,
"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": {
@ -431,7 +428,7 @@
"widgetStyle": {
"padding": "0"
},
"widgetCss": ".tb-widget-container > .tb-widget {\n border: none !important;\n border-radius: 0 !important;\n box-shadow: none !important;\n}",
"widgetCss": ".tb-widget-container > .tb-widget {\n border: none !important;\n border-radius: 0 !important;\n box-shadow: none !important;\n}\n",
"pageSize": 1024,
"noDataDisplayMessage": "",
"showLegend": false,
@ -449,6 +446,68 @@
"row": 0,
"col": 0,
"id": "d26e5cd7-75ef-d475-00c7-1a2d1114efe8"
},
"ebbd0a6e-8a47-e770-5086-7f4974250f2d": {
"isSystemType": true,
"bundleAlias": "home_page_widgets",
"typeAlias": "usage_info",
"type": "static",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 7.5,
"sizeY": 6.5,
"config": {
"showTitle": false,
"backgroundColor": "rgb(255, 255, 255)",
"color": "rgba(0, 0, 0, 0.87)",
"padding": "16px",
"settings": {},
"title": "New Usage info",
"dropShadow": false,
"enableFullscreen": false,
"widgetStyle": {},
"widgetCss": "",
"pageSize": 1024,
"noDataDisplayMessage": "",
"showLegend": false,
"datasources": []
},
"row": 0,
"col": 0,
"id": "ebbd0a6e-8a47-e770-5086-7f4974250f2d"
},
"9e3ef045-d8bc-1640-a3f4-2dd10b19d50e": {
"isSystemType": true,
"bundleAlias": "home_page_widgets",
"typeAlias": "quick_links",
"type": "static",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 7.5,
"sizeY": 3,
"config": {
"showTitle": false,
"backgroundColor": "rgb(255, 255, 255)",
"color": "rgba(0, 0, 0, 0.87)",
"padding": "16px",
"settings": {
"columns": 2
},
"title": "New Quick links",
"dropShadow": false,
"enableFullscreen": false,
"widgetStyle": {},
"widgetCss": "",
"pageSize": 1024,
"noDataDisplayMessage": "",
"showLegend": false,
"datasources": []
},
"row": 0,
"col": 0,
"id": "9e3ef045-d8bc-1640-a3f4-2dd10b19d50e"
}
},
"states": {
@ -467,10 +526,10 @@
"mobileHeight": 8
},
"867f82cf-ecf2-2d5c-35cb-08c6f2edc3a4": {
"sizeX": 31,
"sizeX": 29,
"sizeY": 16,
"row": 42,
"col": 26,
"col": 28,
"mobileHide": true
},
"a23185ad-dc46-806c-0e50-5b21fb080ace": {
@ -479,6 +538,18 @@
"row": 0,
"col": 85,
"mobileHide": true
},
"ebbd0a6e-8a47-e770-5086-7f4974250f2d": {
"sizeX": 28,
"sizeY": 16,
"row": 42,
"col": 57
},
"9e3ef045-d8bc-1640-a3f4-2dd10b19d50e": {
"sizeX": 28,
"sizeY": 16,
"row": 42,
"col": 0
}
},
"gridSettings": {

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

@ -56,3 +56,4 @@ export * from './user-settings.models';
export * from './widget.models';
export * from './widgets-bundle.model';
export * from './window-message.model';
export * from './usage.models';

39
ui-ngx/src/app/shared/models/usage.models.ts

@ -0,0 +1,39 @@
///
/// 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.
///
export interface UsageInfo {
devices: number;
maxDevices: number;
assets: number;
maxAssets: number;
customers: number;
maxCustomers: number;
users: number;
maxUsers: number;
dashboards: number;
maxDashboards: number;
transportMessages: number;
maxTransportMessages: number;
jsExecutions: number;
maxJsExecutions: number;
emails: number;
maxEmails: number;
sms: number;
maxSms: number;
alarms: number;
maxAlarms: number;
}

4
ui-ngx/src/app/shared/models/user-settings.models.ts

@ -40,6 +40,10 @@ export interface DocumentationLinks {
links?: DocumentationLink[];
}
export interface QuickLinks {
links?: string[];
}
export interface GettingStarted {
maxSelectedIndex?: number;
lastSelectedIndex?: number;

53
ui-ngx/src/app/shared/pipe/short-number.pipe.ts

@ -0,0 +1,53 @@
///
/// 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 { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'shortNumber'
})
export class ShortNumberPipe implements PipeTransform {
transform(number: number, args?: any): any {
if (isNaN(number)) return 0;
if (number === null) return 0;
if (number === 0) return 0;
let abs = Math.abs(number);
const rounder = Math.pow(10, 1);
const isNegative = number < 0;
const isLong = args && args.long;
let key = '';
const powers = [
{key: 'Q', longKey: ' quadrillion', value: Math.pow(10, 15)},
{key: 'T', longKey: ' trillion', value: Math.pow(10, 12)},
{key: 'B', longKey: ' billion', value: Math.pow(10, 9)},
{key: 'M', longKey: ' million', value: Math.pow(10, 6)},
{key: 'K', longKey: ' thousand', value: 1000}
];
for (let i = 0; i < powers.length; i++) {
let reduced = abs / powers[i].value;
reduced = Math.round(reduced * rounder) / rounder;
if (reduced >= 1) {
abs = reduced;
key = isLong ? powers[i].longKey : powers[i].key;
break;
}
}
return (isNegative ? '-' : '') + abs + key;
}
}

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

@ -187,6 +187,7 @@ import {
GtMdLgLayoutGapDirective,
GtMdLgShowHideDirective
} from '@shared/layout/layout.directives';
import { ShortNumberPipe } from '@shared/pipe/short-number.pipe';
export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) {
return markedOptionsService;
@ -203,7 +204,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
FileSizePipe,
DateAgoPipe,
SafePipe,
DateAgoPipe,
ShortNumberPipe,
{
provide: FlowInjectionToken,
useValue: Flow
@ -327,6 +328,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
FileSizePipe,
DateAgoPipe,
SafePipe,
ShortNumberPipe,
SelectableColumnsPipe,
KeyboardShortcutPipe,
TbJsonToStringDirective,
@ -345,7 +347,6 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
NotificationComponent,
TemplateAutocompleteComponent,
SlackConversationAutocompleteComponent,
DateAgoPipe,
MdLgLayoutDirective,
MdLgLayoutAlignDirective,
MdLgLayoutGapDirective,
@ -550,6 +551,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
FileSizePipe,
DateAgoPipe,
SafePipe,
ShortNumberPipe,
SelectableColumnsPipe,
RouterModule,
TranslateModule,
@ -568,7 +570,6 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
NotificationComponent,
TemplateAutocompleteComponent,
SlackConversationAutocompleteComponent,
DateAgoPipe,
MdLgLayoutDirective,
MdLgLayoutAlignDirective,
MdLgLayoutGapDirective,

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

@ -4170,6 +4170,7 @@
"line-settings": "Line settings",
"show-line": "Show line",
"fill-line": "Fill line",
"fill-line-opacity": "Fill opacity",
"points-settings": "Points settings",
"show-points": "Show points",
"points-line-width": "Line width of points",
@ -5127,6 +5128,11 @@
"link-required": "Link is required.",
"columns": "Columns"
},
"quick-links": {
"title": "Quick links",
"add-link": "Add link",
"columns": "Columns"
},
"configured-features": {
"title": "Configured features",
"info": "Status of features that require configuration",
@ -5148,6 +5154,11 @@
"upgrade": "Upgrade",
"version-is-up-to-date": "Version is up to date"
},
"usage-info": {
"title": "Usage",
"entities": "Entities",
"api-calls": "API calls"
},
"functions": {
"title": "Functions",
"pe-feature-tooltip": "Only on ThingsBoard\nProfessional Edition",

Loading…
Cancel
Save