Merge pull request #13301 from yuliaklochai/ui-trendz-settings

Added Trendz setting
This commit is contained in:
Viacheslav Klimov 2025-05-06 11:00:20 +03:00 committed by GitHub
commit 0e20e27f6f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 607 additions and 10 deletions

View File

@ -42,6 +42,7 @@ import org.thingsboard.server.common.data.settings.UserSettings;
import org.thingsboard.server.common.data.settings.UserSettingsType;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.dao.mobile.QrCodeSettingService;
import org.thingsboard.server.dao.trendz.TrendzSettingsService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
@ -87,6 +88,9 @@ public class SystemInfoController extends BaseController {
@Autowired
private DebugModeRateLimitsConfig debugModeRateLimitsConfig;
@Autowired
private TrendzSettingsService trendzSettingsService;
@PostConstruct
public void init() {
JsonNode info = buildInfoObject();
@ -158,6 +162,7 @@ public class SystemInfoController extends BaseController {
}
systemParams.setMaxArgumentsPerCF(tenantProfileConfiguration.getMaxArgumentsPerCF());
systemParams.setMaxDataPointsPerRollingArg(tenantProfileConfiguration.getMaxDataPointsPerRollingArg());
systemParams.setTrendzSettings(trendzSettingsService.findTrendzSettings(currentUser.getTenantId()));
}
systemParams.setMobileQrEnabled(Optional.ofNullable(qrCodeSettingService.findQrCodeSettings(TenantId.SYS_TENANT_ID))
.map(QrCodeSettings::getQrCodeConfig).map(QRCodeConfig::isShowOnHomePage)

View File

@ -0,0 +1,80 @@
/**
* Copyright © 2016-2025 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.
*/
package org.thingsboard.server.controller;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.trendz.TrendzSettings;
import org.thingsboard.server.config.annotations.ApiOperation;
import org.thingsboard.server.dao.trendz.TrendzSettingsService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_END;
import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_START;
import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;
@RestController
@TbCoreComponent
@RequiredArgsConstructor
@RequestMapping("/api")
public class TrendzController extends BaseController {
private final TrendzSettingsService trendzSettingsService;
@ApiOperation(value = "Save Trendz settings (saveTrendzSettings)",
notes = "Saves Trendz settings for this tenant.\n" + NEW_LINE +
"Here is an example of the Trendz settings:\n" +
MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"enabled\": true,\n" +
" \"baseUrl\": \"https://some.domain.com:18888/also_necessary_prefix\"\n" +
"}" +
MARKDOWN_CODE_BLOCK_END +
TENANT_AUTHORITY_PARAGRAPH)
@PostMapping("/trendz/settings")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
public TrendzSettings saveTrendzSettings(@RequestBody TrendzSettings trendzSettings,
@AuthenticationPrincipal SecurityUser user) throws ThingsboardException {
accessControlService.checkPermission(user, Resource.ADMIN_SETTINGS, Operation.WRITE);
TenantId tenantId = user.getTenantId();
trendzSettingsService.saveTrendzSettings(tenantId, trendzSettings);
return trendzSettings;
}
@ApiOperation(value = "Get Trendz Settings (getTrendzSettings)",
notes = "Retrieves Trendz settings for this tenant." +
TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@GetMapping("/trendz/settings")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
public TrendzSettings getTrendzSettings(@AuthenticationPrincipal SecurityUser user) {
TenantId tenantId = user.getTenantId();
return trendzSettingsService.findTrendzSettings(tenantId);
}
}

View File

@ -23,7 +23,7 @@ import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.service.security.model.SecurityUser;
@Component(value="sysAdminPermissions")
@Component(value = "sysAdminPermissions")
public class SysAdminPermissions extends AbstractPermissions {
public SysAdminPermissions() {

View File

@ -644,6 +644,9 @@ cache:
mobileSecretKey:
timeToLiveInMinutes: "${CACHE_MOBILE_SECRET_KEY_TTL:2}" # QR secret key cache TTL
maxSize: "${CACHE_MOBILE_SECRET_KEY_MAX_SIZE:10000}" # 0 means the cache is disabled
trendzSettings:
timeToLiveInMinutes: "${CACHE_SPECS_TRENDZ_SETTINGS_TTL:1440}" # Trendz settings cache TTL
maxSize: "${CACHE_SPECS_TRENDZ_SETTINGS_MAX_SIZE:10000}" # 0 means the cache is disabled
# Deliberately placed outside the 'specs' group above
notificationRules:

View File

@ -0,0 +1,77 @@
/**
* Copyright © 2016-2025 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.
*/
package org.thingsboard.server.controller;
import org.junit.Before;
import org.junit.Test;
import org.thingsboard.server.common.data.trendz.TrendzSettings;
import org.thingsboard.server.dao.service.DaoSqlTest;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@DaoSqlTest
public class TrendzControllerTest extends AbstractControllerTest {
private final String trendzUrl = "https://some.domain.com:18888/also_necessary_prefix";
@Before
public void setUp() throws Exception {
loginTenantAdmin();
TrendzSettings trendzSettings = new TrendzSettings();
trendzSettings.setEnabled(true);
trendzSettings.setBaseUrl(trendzUrl);
doPost("/api/trendz/settings", trendzSettings).andExpect(status().isOk());
}
@Test
public void testTrendzSettingsWhenTenant() throws Exception {
loginTenantAdmin();
TrendzSettings trendzSettings = doGet("/api/trendz/settings", TrendzSettings.class);
assertThat(trendzSettings).isNotNull();
assertThat(trendzSettings.isEnabled()).isTrue();
assertThat(trendzSettings.getBaseUrl()).isEqualTo(trendzUrl);
String updatedUrl = "https://some.domain.com:18888/tenant_trendz";
trendzSettings.setBaseUrl(updatedUrl);
doPost("/api/trendz/settings", trendzSettings).andExpect(status().isOk());
TrendzSettings updatedTrendzSettings = doGet("/api/trendz/settings", TrendzSettings.class);
assertThat(updatedTrendzSettings).isEqualTo(trendzSettings);
}
@Test
public void testTrendzSettingsWhenCustomer() throws Exception {
loginCustomerUser();
TrendzSettings newTrendzSettings = new TrendzSettings();
newTrendzSettings.setEnabled(true);
newTrendzSettings.setBaseUrl("https://some.domain.com:18888/customer_trendz");
doPost("/api/trendz/settings", newTrendzSettings).andExpect(status().isForbidden());
TrendzSettings fetchedTrendzSettings = doGet("/api/trendz/settings", TrendzSettings.class);
assertThat(fetchedTrendzSettings).isNotNull();
assertThat(fetchedTrendzSettings.isEnabled()).isTrue();
assertThat(fetchedTrendzSettings.getBaseUrl()).isEqualTo(trendzUrl);
}
}

View File

@ -0,0 +1,29 @@
/**
* Copyright © 2016-2025 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.
*/
package org.thingsboard.server.dao.trendz;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.trendz.TrendzSettings;
public interface TrendzSettingsService {
void saveTrendzSettings(TenantId tenantId, TrendzSettings settings);
TrendzSettings findTrendzSettings(TenantId tenantId);
void deleteTrendzSettings(TenantId tenantId);
}

View File

@ -35,6 +35,7 @@ public class CacheConstants {
public static final String DEVICE_PROFILE_CACHE = "deviceProfiles";
public static final String NOTIFICATION_SETTINGS_CACHE = "notificationSettings";
public static final String SENT_NOTIFICATIONS_CACHE = "sentNotifications";
public static final String TRENDZ_SETTINGS_CACHE = "trendzSettings";
public static final String ASSET_PROFILE_CACHE = "assetProfiles";
public static final String ATTRIBUTES_CACHE = "attributes";

View File

@ -17,6 +17,7 @@ package org.thingsboard.server.common.data;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Data;
import org.thingsboard.server.common.data.trendz.TrendzSettings;
import java.util.List;
@ -37,4 +38,5 @@ public class SystemParams {
String calculatedFieldDebugPerTenantLimitsConfiguration;
long maxArgumentsPerCF;
long maxDataPointsPerRollingArg;
TrendzSettings trendzSettings;
}

View File

@ -0,0 +1,26 @@
/**
* Copyright © 2016-2025 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.
*/
package org.thingsboard.server.common.data.trendz;
import lombok.Data;
@Data
public class TrendzSettings {
private boolean enabled;
private String baseUrl;
}

View File

@ -44,6 +44,7 @@ import org.thingsboard.server.dao.service.PaginatedRemover;
import org.thingsboard.server.dao.service.Validator;
import org.thingsboard.server.dao.service.validator.TenantDataValidator;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.dao.trendz.TrendzSettingsService;
import org.thingsboard.server.dao.usagerecord.ApiUsageStateService;
import org.thingsboard.server.dao.user.UserService;
@ -81,6 +82,8 @@ public class TenantServiceImpl extends AbstractCachedEntityService<TenantId, Ten
@Autowired
private QrCodeSettingService qrCodeSettingService;
@Autowired
private TrendzSettingsService trendzSettingsService;
@Autowired
private TenantDataValidator tenantValidator;
@Autowired
protected TbTransactionalCache<TenantId, Boolean> existsTenantCache;
@ -163,9 +166,10 @@ public class TenantServiceImpl extends AbstractCachedEntityService<TenantId, Ten
Validator.validateId(tenantId, id -> INCORRECT_TENANT_ID + id);
userService.deleteAllByTenantId(tenantId);
notificationSettingsService.deleteNotificationSettings(tenantId);
trendzSettingsService.deleteTrendzSettings(tenantId);
adminSettingsService.deleteAdminSettingsByTenantId(tenantId);
qrCodeSettingService.deleteByTenantId(tenantId);
notificationSettingsService.deleteNotificationSettings(tenantId);
tenantDao.removeById(tenantId, tenantId.getId());
publishEvictEvent(new TenantEvictEvent(tenantId, true));

View File

@ -0,0 +1,69 @@
/**
* Copyright © 2016-2025 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.
*/
package org.thingsboard.server.dao.trendz;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.trendz.TrendzSettings;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import java.util.Optional;
@Service
@RequiredArgsConstructor
@Slf4j
public class DefaultTrendzSettingsService implements TrendzSettingsService {
private final AdminSettingsService adminSettingsService;
private static final String SETTINGS_KEY = "trendz";
@CacheEvict(cacheNames = CacheConstants.TRENDZ_SETTINGS_CACHE, key = "#tenantId")
@Override
public void saveTrendzSettings(TenantId tenantId, TrendzSettings settings) {
AdminSettings adminSettings = Optional.ofNullable(adminSettingsService.findAdminSettingsByTenantIdAndKey(tenantId, SETTINGS_KEY))
.orElseGet(() -> {
AdminSettings newAdminSettings = new AdminSettings();
newAdminSettings.setTenantId(tenantId);
newAdminSettings.setKey(SETTINGS_KEY);
return newAdminSettings;
});
adminSettings.setJsonValue(JacksonUtil.valueToTree(settings));
adminSettingsService.saveAdminSettings(tenantId, adminSettings);
}
@Cacheable(cacheNames = CacheConstants.TRENDZ_SETTINGS_CACHE, key = "#tenantId")
@Override
public TrendzSettings findTrendzSettings(TenantId tenantId) {
return Optional.ofNullable(adminSettingsService.findAdminSettingsByTenantIdAndKey(tenantId, SETTINGS_KEY))
.map(adminSettings -> JacksonUtil.treeToValue(adminSettings.getJsonValue(), TrendzSettings.class))
.orElseGet(TrendzSettings::new);
}
@CacheEvict(cacheNames = CacheConstants.TRENDZ_SETTINGS_CACHE, key = "#tenantId")
@Override
public void deleteTrendzSettings(TenantId tenantId) {
adminSettingsService.deleteAdminSettingsByTenantIdAndKey(tenantId, SETTINGS_KEY);
}
}

View File

@ -108,6 +108,9 @@ cache.specs.qrCodeSettings.maxSize=10000
cache.specs.mobileSecretKey.timeToLiveInMinutes=1440
cache.specs.mobileSecretKey.maxSize=10000
cache.specs.trendzSettings.timeToLiveInMinutes=1440
cache.specs.trendzSettings.maxSize=10000
redis.connection.host=localhost
redis.connection.port=6379
redis.connection.db=0

View File

@ -16,6 +16,7 @@
import { AuthUser, User } from '@shared/models/user.model';
import { UserSettings } from '@shared/models/user-settings.models';
import { TrendzSettings } from '@shared/models/trendz-settings.models';
export interface SysParamsState {
userTokenAccessEnabled: boolean;
@ -32,6 +33,7 @@ export interface SysParamsState {
maxArgumentsPerCF: number;
ruleChainDebugPerTenantLimitsConfiguration?: string;
calculatedFieldDebugPerTenantLimitsConfiguration?: string;
trendzSettings: TrendzSettings;
}
export interface SysParams extends SysParamsState {

View File

@ -17,6 +17,7 @@
import { AuthPayload, AuthState } from './auth.models';
import { AuthActions, AuthActionTypes } from './auth.actions';
import { initialUserSettings, UserSettings } from '@shared/models/user-settings.models';
import { initialTrendzSettings } from '@shared/models/trendz-settings.models';
import { unset } from '@core/utils';
const emptyUserAuthState: AuthPayload = {
@ -34,7 +35,8 @@ const emptyUserAuthState: AuthPayload = {
maxArgumentsPerCF: 0,
maxDataPointsPerRollingArg: 0,
maxDebugModeDurationMinutes: 0,
userSettings: initialUserSettings
userSettings: initialUserSettings,
trendzSettings: initialTrendzSettings
};
export const initialState: AuthState = {

View File

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

View File

@ -0,0 +1,39 @@
///
/// Copyright © 2016-2025 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 { TrendzSettings } from '@shared/models/trendz-settings.models';
import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils';
@Injectable({
providedIn: 'root'
})
export class TrendzSettingsService {
constructor(
private http: HttpClient
) {}
public getTrendzSettings(config?: RequestConfig): Observable<TrendzSettings> {
return this.http.get<TrendzSettings>(`/api/trendz/settings`, defaultHttpOptionsFromConfig(config))
}
public saveTrendzSettings(trendzSettings: TrendzSettings, config?: RequestConfig): Observable<TrendzSettings> {
return this.http.post<TrendzSettings>(`/api/trendz/settings`, trendzSettings, defaultHttpOptionsFromConfig(config))
}
}

View File

@ -104,7 +104,8 @@ export enum MenuId {
features = 'features',
otaUpdates = 'otaUpdates',
version_control = 'version_control',
api_usage = 'api_usage'
api_usage = 'api_usage',
trendz_settings = 'trendz_settings'
}
declare type MenuFilter = (authState: AuthState) => boolean;
@ -684,6 +685,17 @@ export const menuSectionMap = new Map<MenuId, MenuSection>([
path: '/usage',
icon: 'insert_chart'
}
],
[
MenuId.trendz_settings,
{
id: MenuId.trendz_settings,
name: 'admin.trendz',
fullName: 'admin.trendz-settings',
type: 'link',
path: '/settings/trendz',
icon: 'trendz-settings'
}
]
]);
@ -843,7 +855,8 @@ const defaultUserMenuMap = new Map<Authority, MenuReference[]>([
{id: MenuId.home_settings},
{id: MenuId.notification_settings},
{id: MenuId.repository_settings},
{id: MenuId.auto_commit_settings}
{id: MenuId.auto_commit_settings},
{id: MenuId.trendz_settings}
]
},
{
@ -946,7 +959,7 @@ const defaultHomeSectionMap = new Map<Authority, HomeSectionReference[]>([
},
{
name: 'admin.system-settings',
places: [MenuId.home_settings, MenuId.resources_library, MenuId.repository_settings, MenuId.auto_commit_settings]
places: [MenuId.home_settings, MenuId.resources_library, MenuId.repository_settings, MenuId.auto_commit_settings, MenuId.trendz_settings]
}
]
],

View File

@ -52,6 +52,7 @@ import { UiSettingsService } from '@core/http/ui-settings.service';
import { UsageInfoService } from '@core/http/usage-info.service';
import { EventService } from '@core/http/event.service';
import { AuditLogService } from '@core/http/audit-log.service';
import { TrendzSettingsService } from '@core/http/trendz-settings.service';
export const ServicesMap = new Map<string, Type<any>>(
[
@ -91,6 +92,7 @@ export const ServicesMap = new Map<string, Type<any>>(
['usageInfoService', UsageInfoService],
['notificationService', NotificationService],
['eventService', EventService],
['auditLogService', AuditLogService]
['auditLogService', AuditLogService],
['trendzSettingsService', TrendzSettingsService]
]
);

View File

@ -46,6 +46,7 @@ import { ScadaSymbolData } from '@home/pages/scada-symbol/scada-symbol-editor.mo
import { MenuId } from '@core/services/menu.models';
import { catchError } from 'rxjs/operators';
import { JsLibraryTableConfigResolver } from '@home/pages/admin/resource/js-library-table-config.resolver';
import { TrendzSettingsComponent } from '@home/pages/admin/trendz-settings.component';
export const scadaSymbolResolver: ResolveFn<ScadaSymbolData> =
(route: ActivatedRouteSnapshot,
@ -349,6 +350,18 @@ const routes: Routes = [
}
}
},
{
path: 'trendz',
component: TrendzSettingsComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
auth: [Authority.TENANT_ADMIN],
title: 'admin.trendz-settings',
breadcrumb: {
menuId: MenuId.trendz_settings
}
}
},
{
path: 'security-settings',
redirectTo: '/security-settings/general'

View File

@ -37,6 +37,7 @@ import { OAuth2Module } from '@home/pages/admin/oauth2/oauth2.module';
import { JsLibraryTableHeaderComponent } from '@home/pages/admin/resource/js-library-table-header.component';
import { JsResourceComponent } from '@home/pages/admin/resource/js-resource.component';
import { NgxFlowModule } from '@flowjs/ngx-flow';
import { TrendzSettingsComponent } from '@home/pages/admin/trendz-settings.component';
@NgModule({
declarations:
@ -55,7 +56,8 @@ import { NgxFlowModule } from '@flowjs/ngx-flow';
QueueComponent,
RepositoryAdminSettingsComponent,
AutoCommitAdminSettingsComponent,
TwoFactorAuthSettingsComponent
TwoFactorAuthSettingsComponent,
TrendzSettingsComponent
],
imports: [
CommonModule,

View File

@ -0,0 +1,51 @@
<!--
Copyright © 2016-2025 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>
<mat-card appearance="outlined" class="settings-card">
<mat-card-header>
<mat-card-title>
<span class="mat-headline-5" translate>admin.trendz-settings</span>
</mat-card-title>
<span class="flex-1"></span>
<div tb-help="trendzSettings"></div>
</mat-card-header>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<mat-card-content>
<form [formGroup]="trendzSettingsForm" (ngSubmit)="save()">
<fieldset [disabled]="isLoading$ | async">
<section class="tb-trendz-section flex flex-col gt-sm:flex-row">
<mat-form-field class="tb-trendz-url mat-block flex-1" subscriptSizing="dynamic">
<mat-label translate>admin.trendz-url</mat-label>
<input matInput formControlName="trendzUrl">
</mat-form-field>
<mat-checkbox class="flex flex-1" formControlName="isTrendzEnabled">
{{ 'admin.trendz-enable' | translate }}
</mat-checkbox>
</section>
<div class="flex w-full flex-row flex-wrap items-center justify-end">
<button mat-button mat-raised-button color="primary" [disabled]="(isLoading$ | async) || trendzSettingsForm.invalid || !trendzSettingsForm.dirty" type="submit">
{{'action.save' | translate}}
</button>
</div>
</fieldset>
</form>
</mat-card-content>
</mat-card>
</div>

View File

@ -0,0 +1,36 @@
/**
* Copyright © 2016-2025 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 {
.mat-mdc-card-header {
min-height: 64px;
}
.tb-trendz-section {
margin: 16px 0;
}
.tb-trendz-url {
@media #{$mat-gt-sm} {
padding-right: 12px;
}
@media #{$mat-lt-md} {
padding-bottom: 12px;
}
}
}

View File

@ -0,0 +1,94 @@
///
/// Copyright © 2016-2025 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, OnInit, DestroyRef } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { TrendzSettingsService } from '@core/http/trendz-settings.service';
import { TrendzSettings } from '@shared/models/trendz-settings.models';
import { isDefinedAndNotNull } from '@core/utils';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({
selector: 'tb-trendz-settings',
templateUrl: './trendz-settings.component.html',
styleUrls: ['./trendz-settings.component.scss', './settings-card.scss']
})
export class TrendzSettingsComponent extends PageComponent implements OnInit, HasConfirmForm {
trendzSettingsForm: FormGroup;
constructor(private fb: FormBuilder,
private trendzSettingsService: TrendzSettingsService,
private destroyRef: DestroyRef) {
super();
}
ngOnInit() {
this.trendzSettingsForm = this.fb.group({
trendzUrl: [null, [Validators.pattern(/^(https?:\/\/)[^\s/$.?#].[^\s]*$/i)]],
isTrendzEnabled: [false]
});
this.trendzSettingsService.getTrendzSettings().subscribe((trendzSettings) => {
this.setTrendzSettings(trendzSettings);
});
this.trendzSettingsForm.get('isTrendzEnabled').valueChanges
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((enabled: boolean) => this.toggleUrlRequired(enabled));
}
toggleUrlRequired(enabled: boolean) {
const trendzUrlControl = this.trendzSettingsForm.get('trendzUrl')!;
if (enabled) {
trendzUrlControl.addValidators(Validators.required);
} else {
trendzUrlControl.removeValidators(Validators.required);
}
trendzUrlControl.updateValueAndValidity();
}
setTrendzSettings(trendzSettings: TrendzSettings) {
this.trendzSettingsForm.reset({
trendzUrl: trendzSettings?.baseUrl,
isTrendzEnabled: trendzSettings?.enabled ?? false
});
this.toggleUrlRequired(this.trendzSettingsForm.get('isTrendzEnabled').value);
}
confirmForm(): FormGroup {
return this.trendzSettingsForm;
}
save(): void {
const trendzUrl = this.trendzSettingsForm.get('trendzUrl').value;
const isTrendzEnabled = this.trendzSettingsForm.get('isTrendzEnabled').value;
const trendzSettings: TrendzSettings = {
baseUrl: trendzUrl,
enabled: isTrendzEnabled
};
this.trendzSettingsService.saveTrendzSettings(trendzSettings).subscribe(() => {
this.setTrendzSettings(trendzSettings);
})
}
}

View File

@ -200,6 +200,7 @@ export const HelpLinks = {
mobileQrCode: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/mobile-qr-code/`,
calculatedField: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/calculated-fields/`,
timewindowSettings: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/dashboards/#time-window`,
trendzSettings: `${helpBaseUrl}/docs/trendz/`
}
};
/* eslint-enable max-len */

View File

@ -62,7 +62,19 @@ export const svgIcons: {[key: string]: string} = {
'4.6760606 4.678212,7.3604329 7.3397982,4.6839955 4.6657413,2.0041717 6.6653477,2.2309572e-4 9.3360035,2.6766286 11.997681,' +
'0 14.659287,2.6765011 Z m -5.332255,4.0079963 1.999613,2.003945 -7.99844,8.0158157 -1.9996133,-2.004017 z m 1.676684,4.3522483 ' +
'1.999613,2.0039454 -6.6654242,6.679793 -1.9996133,-2.003874 z m 2.988987,7.0033574 -1.999544,-2.003945 -4.6658108,4.675848 ' +
'1.9996128,2.004015 z"/></svg>'
'1.9996128,2.004015 z"/></svg>',
'trendz-settings': '<svg viewBox="0 0 25 17"><path fill-rule="evenodd" clip-rule="evenodd" d="M7.04334 0.28949H12.2804V5.7615L11.5894 ' +
'6.4537L10.4605 5.32674L7.04334 8.73801V0.28949ZM7.04334 10.0127V11.0075L7.54073 10.5093L7.04334 10.0127ZM7.04334 ' +
'12.2649V13.2424L7.53209 12.7545L7.04334 12.2649ZM18.3903 13.243V12.2646L17.901 12.7546L18.3903 13.243ZM18.3903 ' +
'11.0079V10.0123L17.8925 10.5093L18.3903 11.0079ZM18.3903 8.73841V3.34443H13.1532V5.76189L13.8438 6.45362L14.9727 ' +
'5.32661L17.0542 7.40453L18.3903 8.73841ZM24.8335 1.16233H19.2631V13.8185H24.8335V1.16233ZM0.833481 5.52653H6.1705V13.8185H0.833481V5.52653Z"/>' +
'<path fill-rule="evenodd" clip-rule="evenodd" d="M14.9729 6.55688L15.819 7.40149L14.6876 8.53099L15.8137 9.65905L16.947 ' +
'8.52767L17.7931 9.37227L16.6583 10.5051L17.7844 11.6331L16.6669 12.7526L17.7931 13.8768L16.947 14.7214L15.8223 13.5986L14.6962 ' +
'14.7267L15.819 15.8476L14.973 16.6921L13.8516 15.5727L12.7169 16.7094L11.5821 15.5727L10.4607 16.6921L9.61465 15.8476L10.7375 ' +
'14.7267L9.61134 13.5985L8.48664 14.7213L7.64059 13.8767L8.76673 12.7525L7.64928 11.6331L8.77533 10.5051L7.64059 9.37227L8.48664 ' +
'8.52767L9.61993 9.65905L10.7461 8.53103L9.61465 7.40158L10.4607 6.55697L11.5907 7.68499L12.7169 6.55688L13.843 7.68494L14.9729 ' +
'6.55688ZM12.7169 8.24609L13.5629 9.0907L10.1787 12.4691L9.33268 11.6245L12.7169 8.24609ZM13.4262 10.0805L14.2723 10.9251L11.4521 ' +
'13.7404L10.6061 12.8958L13.4262 10.0805ZM14.6909 13.0321L13.8449 12.1875L11.8708 14.1583L12.7168 15.0029L14.6909 13.0321Z"/></svg>'
};
export const svgIconsUrl: { [key: string]: string } = {

View File

@ -62,3 +62,4 @@ export * from './window-message.model';
export * from './usage.models';
export * from './query/query.models';
export * from './regex.constants';
export * from './trendz-settings.models'

View File

@ -0,0 +1,25 @@
///
/// Copyright © 2016-2025 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 TrendzSettings {
baseUrl: string,
enabled: boolean
}
export const initialTrendzSettings: TrendzSettings = {
baseUrl: null,
enabled: false
}

View File

@ -545,7 +545,11 @@
"slack-settings": "Slack settings",
"mobile-settings": "Mobile settings",
"firebase-service-account-file": "Firebase service account credentials JSON file",
"select-firebase-service-account-file": "Drag and drop your Firebase service account credentials file or "
"select-firebase-service-account-file": "Drag and drop your Firebase service account credentials file or ",
"trendz": "Trendz",
"trendz-settings": "Trendz settings",
"trendz-url": "Trendz URL",
"trendz-enable": "Enable Trendz"
},
"alarm": {
"alarm": "Alarm",