diff --git a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java index 4ee86871d6..29f4daa783 100644 --- a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java +++ b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java @@ -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) diff --git a/application/src/main/java/org/thingsboard/server/controller/TrendzController.java b/application/src/main/java/org/thingsboard/server/controller/TrendzController.java new file mode 100644 index 0000000000..8261071670 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/TrendzController.java @@ -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); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java index e64f5b49dd..6bd7aacf54 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java @@ -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() { diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 418263076b..a8050b00ed 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -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: diff --git a/application/src/test/java/org/thingsboard/server/controller/TrendzControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/TrendzControllerTest.java new file mode 100644 index 0000000000..115b895521 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/TrendzControllerTest.java @@ -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); + } + +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/trendz/TrendzSettingsService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/trendz/TrendzSettingsService.java new file mode 100644 index 0000000000..31f9495345 --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/trendz/TrendzSettingsService.java @@ -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); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java index 35f69e1544..5b167c88a2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java @@ -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"; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java b/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java index b1ef4d7f22..fe3eb4e4d8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java @@ -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; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/trendz/TrendzSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/trendz/TrendzSettings.java new file mode 100644 index 0000000000..3c3b49399c --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/trendz/TrendzSettings.java @@ -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; + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java index 1dbca5af12..8c40ca3e14 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java @@ -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 existsTenantCache; @@ -163,9 +166,10 @@ public class TenantServiceImpl extends AbstractCachedEntityService 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)); diff --git a/dao/src/main/java/org/thingsboard/server/dao/trendz/DefaultTrendzSettingsService.java b/dao/src/main/java/org/thingsboard/server/dao/trendz/DefaultTrendzSettingsService.java new file mode 100644 index 0000000000..730d98cb6a --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/trendz/DefaultTrendzSettingsService.java @@ -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); + } + +} diff --git a/dao/src/test/resources/application-test.properties b/dao/src/test/resources/application-test.properties index a1bb335ad0..a44303107c 100644 --- a/dao/src/test/resources/application-test.properties +++ b/dao/src/test/resources/application-test.properties @@ -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 diff --git a/ui-ngx/src/app/core/auth/auth.models.ts b/ui-ngx/src/app/core/auth/auth.models.ts index e5cc1424ab..142d845cf4 100644 --- a/ui-ngx/src/app/core/auth/auth.models.ts +++ b/ui-ngx/src/app/core/auth/auth.models.ts @@ -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 { diff --git a/ui-ngx/src/app/core/auth/auth.reducer.ts b/ui-ngx/src/app/core/auth/auth.reducer.ts index 3ecf70074c..fde778284d 100644 --- a/ui-ngx/src/app/core/auth/auth.reducer.ts +++ b/ui-ngx/src/app/core/auth/auth.reducer.ts @@ -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 = { diff --git a/ui-ngx/src/app/core/http/public-api.ts b/ui-ngx/src/app/core/http/public-api.ts index 997689d98e..c28d80d173 100644 --- a/ui-ngx/src/app/core/http/public-api.ts +++ b/ui-ngx/src/app/core/http/public-api.ts @@ -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' diff --git a/ui-ngx/src/app/core/http/trendz-settings.service.ts b/ui-ngx/src/app/core/http/trendz-settings.service.ts new file mode 100644 index 0000000000..82f6973e25 --- /dev/null +++ b/ui-ngx/src/app/core/http/trendz-settings.service.ts @@ -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 { + return this.http.get(`/api/trendz/settings`, defaultHttpOptionsFromConfig(config)) + } + + public saveTrendzSettings(trendzSettings: TrendzSettings, config?: RequestConfig): Observable { + return this.http.post(`/api/trendz/settings`, trendzSettings, defaultHttpOptionsFromConfig(config)) + } +} diff --git a/ui-ngx/src/app/core/services/menu.models.ts b/ui-ngx/src/app/core/services/menu.models.ts index 4775a3a771..607c5c6dff 100644 --- a/ui-ngx/src/app/core/services/menu.models.ts +++ b/ui-ngx/src/app/core/services/menu.models.ts @@ -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([ 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([ {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([ }, { 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] } ] ], diff --git a/ui-ngx/src/app/modules/home/models/services.map.ts b/ui-ngx/src/app/modules/home/models/services.map.ts index e4bbc15936..517b219904 100644 --- a/ui-ngx/src/app/modules/home/models/services.map.ts +++ b/ui-ngx/src/app/modules/home/models/services.map.ts @@ -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>( [ @@ -91,6 +92,7 @@ export const ServicesMap = new Map>( ['usageInfoService', UsageInfoService], ['notificationService', NotificationService], ['eventService', EventService], - ['auditLogService', AuditLogService] + ['auditLogService', AuditLogService], + ['trendzSettingsService', TrendzSettingsService] ] ); diff --git a/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts b/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts index 8bd724de4a..2836224a9a 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts @@ -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 = (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' diff --git a/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts b/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts index 63878ebac9..a5f18122fd 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts @@ -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, diff --git a/ui-ngx/src/app/modules/home/pages/admin/trendz-settings.component.html b/ui-ngx/src/app/modules/home/pages/admin/trendz-settings.component.html new file mode 100644 index 0000000000..45ba62dace --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/trendz-settings.component.html @@ -0,0 +1,51 @@ + +
+ + + + admin.trendz-settings + + +
+
+ + +
+ +
+
+
+ + admin.trendz-url + + + + {{ 'admin.trendz-enable' | translate }} + +
+
+ +
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/admin/trendz-settings.component.scss b/ui-ngx/src/app/modules/home/pages/admin/trendz-settings.component.scss new file mode 100644 index 0000000000..cbb8e698bd --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/trendz-settings.component.scss @@ -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; + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/trendz-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/trendz-settings.component.ts new file mode 100644 index 0000000000..c7f5846063 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/trendz-settings.component.ts @@ -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); + }) + } +} diff --git a/ui-ngx/src/app/shared/models/constants.ts b/ui-ngx/src/app/shared/models/constants.ts index af576882b8..3f424b35bd 100644 --- a/ui-ngx/src/app/shared/models/constants.ts +++ b/ui-ngx/src/app/shared/models/constants.ts @@ -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 */ diff --git a/ui-ngx/src/app/shared/models/icon.models.ts b/ui-ngx/src/app/shared/models/icon.models.ts index c9de8ec3da..6643bd9c7f 100644 --- a/ui-ngx/src/app/shared/models/icon.models.ts +++ b/ui-ngx/src/app/shared/models/icon.models.ts @@ -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"/>' + '1.9996128,2.004015 z"/>', + 'trendz-settings': '' + + '' }; export const svgIconsUrl: { [key: string]: string } = { diff --git a/ui-ngx/src/app/shared/models/public-api.ts b/ui-ngx/src/app/shared/models/public-api.ts index 53e4bb286e..9f7470523e 100644 --- a/ui-ngx/src/app/shared/models/public-api.ts +++ b/ui-ngx/src/app/shared/models/public-api.ts @@ -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' diff --git a/ui-ngx/src/app/shared/models/trendz-settings.models.ts b/ui-ngx/src/app/shared/models/trendz-settings.models.ts new file mode 100644 index 0000000000..e09797bd7e --- /dev/null +++ b/ui-ngx/src/app/shared/models/trendz-settings.models.ts @@ -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 +} diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index b160a44b0e..8255d4e10b 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -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",