From d914dcbf474b9a9b951dd59775efb27881c2137e Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Mon, 29 Dec 2025 18:05:23 +0200 Subject: [PATCH 001/202] Add few missing API endpoints to REST client --- .../server/msa/AbstractContainerTest.java | 20 +- .../msa/connectivity/JavaRestClientTest.java | 176 +++++++++++++++--- rest-client/pom.xml | 4 + .../thingsboard/rest/client/RestClient.java | 63 ++++++- 4 files changed, 217 insertions(+), 46 deletions(-) diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java index f352171382..fea1ca3eba 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java @@ -44,7 +44,6 @@ import java.util.Map; import java.util.Random; import java.util.function.Consumer; - @Slf4j @Listeners(TestListener.class) public abstract class AbstractContainerTest { @@ -170,19 +169,14 @@ public abstract class AbstractContainerTest { DeviceProfileProvisionConfiguration provisionConfiguration; String testProvisionDeviceKey = TEST_PROVISION_DEVICE_KEY; deviceProfile.setProvisionType(provisionType); - switch(provisionType) { - case ALLOW_CREATE_NEW_DEVICES: - provisionConfiguration = new AllowCreateNewDevicesDeviceProfileProvisionConfiguration(TEST_PROVISION_DEVICE_SECRET); - break; - case CHECK_PRE_PROVISIONED_DEVICES: - provisionConfiguration = new CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration(TEST_PROVISION_DEVICE_SECRET); - break; - default: - case DISABLED: + provisionConfiguration = switch (provisionType) { + case ALLOW_CREATE_NEW_DEVICES -> new AllowCreateNewDevicesDeviceProfileProvisionConfiguration(TEST_PROVISION_DEVICE_SECRET); + case CHECK_PRE_PROVISIONED_DEVICES -> new CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration(TEST_PROVISION_DEVICE_SECRET); + default -> { testProvisionDeviceKey = null; - provisionConfiguration = new DisabledDeviceProfileProvisionConfiguration(null); - break; - } + yield new DisabledDeviceProfileProvisionConfiguration(null); + } + }; DeviceProfileData deviceProfileData = deviceProfile.getProfileData(); deviceProfileData.setProvisionConfiguration(provisionConfiguration); deviceProfile.setProfileData(deviceProfileData); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/JavaRestClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/JavaRestClientTest.java index 5b98f4859b..dd3140c109 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/JavaRestClientTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/JavaRestClientTest.java @@ -16,6 +16,7 @@ package org.thingsboard.server.msa.connectivity; import com.google.gson.JsonObject; +import org.apache.commons.lang3.RandomStringUtils; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.HttpClients; import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; @@ -26,7 +27,6 @@ import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; import org.apache.hc.core5.ssl.SSLContexts; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.web.client.RestTemplate; -import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeClass; import org.testng.annotations.BeforeMethod; @@ -34,6 +34,10 @@ import org.testng.annotations.Test; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rest.client.RestClient; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileInfo; +import org.thingsboard.server.common.data.DeviceProfileType; +import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; @@ -41,6 +45,11 @@ import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.asset.AssetProfileInfo; +import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration; +import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTransportConfiguration; +import org.thingsboard.server.common.data.device.profile.DeviceProfileData; import org.thingsboard.server.common.data.domain.Domain; import org.thingsboard.server.common.data.domain.DomainInfo; import org.thingsboard.server.common.data.id.NotificationTargetId; @@ -94,6 +103,7 @@ import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -109,8 +119,10 @@ public class JavaRestClientTest extends AbstractContainerTest { public static final String DEFAULT_NOTIFICATION_SUBJECT = "Just a test"; public static final NotificationType DEFAULT_NOTIFICATION_TYPE = NotificationType.GENERAL; private RestClient restClient; - private Tenant tenant; - private User user; + private Tenant tenant1; + private Tenant tenant2; + private User tenantAdmin1; + private User tenantAdmin2; @BeforeClass public void beforeClass() throws Exception { @@ -140,31 +152,45 @@ public class JavaRestClientTest extends AbstractContainerTest { public void setUp() throws Exception { restClient.login("sysadmin@thingsboard.org", "sysadmin"); - // create tenant and tenant admin - tenant = new Tenant(); - tenant.setTitle("Java Rest Client Test Tenant " + RandomStringUtils.randomAlphabetic(5)); - tenant = restClient.saveTenant(tenant); + // create tenant 1 and tenant admin 1 + tenant1 = new Tenant(); + tenant1.setTitle("Java Rest Client Test Tenant " + RandomStringUtils.insecure().randomAlphabetic(5)); + tenant1 = restClient.saveTenant(tenant1); - String email = RandomStringUtils.randomAlphabetic(5) + "@gmail.com"; - user = restClient.saveUser(defaultTenantAdmin(tenant.getId(), email), false); - restClient.activateUser(user.getId(), "password123", false); - restClient.login(email, "password123"); + String email1 = RandomStringUtils.insecure().randomAlphabetic(5) + "@gmail.com"; + tenantAdmin1 = restClient.saveUser(defaultTenantAdmin(tenant1.getId(), email1), false); + restClient.activateUser(tenantAdmin1.getId(), "password123", false); + + // create tenant 2 and tenant admin 2 + tenant2 = new Tenant(); + tenant2.setTitle("Java Rest Client Test Tenant " + RandomStringUtils.insecure().randomAlphabetic(5)); + tenant2 = restClient.saveTenant(tenant2); + + String email2 = RandomStringUtils.insecure().randomAlphabetic(5) + "@gmail.com"; + tenantAdmin2 = restClient.saveUser(defaultTenantAdmin(tenant2.getId(), email2), false); + restClient.activateUser(tenantAdmin2.getId(), "password123", false); + + // tenant 1 tenant admin by default + restClient.login(tenantAdmin1.getEmail(), "password123"); } @AfterMethod public void tearDown() { restClient.login("sysadmin@thingsboard.org", "sysadmin"); - if (tenant != null) { - restClient.deleteTenant(tenant.getId()); + if (tenant1 != null) { + restClient.deleteTenant(tenant1.getId()); + } + if (tenant2 != null) { + restClient.deleteTenant(tenant2.getId()); } } @Test public void testGetAlarmsV2() { - Device device = restClient.saveDevice(defaultDevicePrototype(RandomStringUtils.randomAlphabetic(5))); + Device device = restClient.saveDevice(defaultDevicePrototype(RandomStringUtils.insecure().randomAlphabetic(5))); assertThat(device).isNotNull(); - String type = "High temp" + RandomStringUtils.randomAlphabetic(5); + String type = "High temp" + RandomStringUtils.insecure().randomAlphabetic(5); Alarm alarm = Alarm.builder() .originator(device.getId()) .severity(AlarmSeverity.CRITICAL) @@ -202,7 +228,7 @@ public class JavaRestClientTest extends AbstractContainerTest { @Test public void testTimeSeriesByReadTsKvQueries() { - Device device = restClient.saveDevice(defaultDevicePrototype(RandomStringUtils.randomAlphabetic(5))); + Device device = restClient.saveDevice(defaultDevicePrototype(RandomStringUtils.insecure().randomAlphabetic(5))); assertThat(device).isNotNull(); DeviceCredentials deviceCredentials = restClient.getDeviceCredentialsByDeviceId(device.getId()).get(); @@ -230,7 +256,7 @@ public class JavaRestClientTest extends AbstractContainerTest { @Test public void testFindNotifications() { - NotificationTarget notificationTarget = createNotificationTarget(user.getId()); + NotificationTarget notificationTarget = createNotificationTarget(tenantAdmin1.getId()); String notificationText1 = "Notification 1"; NotificationTemplate notificationTemplate = createNotificationTemplate(DEFAULT_NOTIFICATION_TYPE, DEFAULT_NOTIFICATION_SUBJECT, notificationText1, new NotificationDeliveryMethod[]{WEB}); NotificationRequest notificationRequest = submitNotificationRequest(notificationTarget.getId(), notificationTemplate.getId()); @@ -248,7 +274,7 @@ public class JavaRestClientTest extends AbstractContainerTest { NotificationRequestPreview requestPreview = restClient.getNotificationRequestPreview(notificationRequest, 10); assertThat(requestPreview.getTotalRecipientsCount()).isEqualTo(1); - assertThat(requestPreview.getRecipientsPreview()).isEqualTo(List.of(user.getEmail())); + assertThat(requestPreview.getRecipientsPreview()).isEqualTo(List.of(tenantAdmin1.getEmail())); PageData notifications = restClient.getNotifications(false, WEB, new PageLink(30)); assertThat(notifications.getTotalElements()).isEqualTo(2); @@ -316,7 +342,7 @@ public class JavaRestClientTest extends AbstractContainerTest { restClient.login("sysadmin@thingsboard.org", "sysadmin"); Domain domain = new Domain(); - String prefix = RandomStringUtils.randomAlphabetic(5).toLowerCase(); + String prefix = RandomStringUtils.insecure().randomAlphabetic(5).toLowerCase(); domain.setName(prefix + ".test.com"); Domain savedDomain = restClient.saveDomain(domain); assertThat(savedDomain.getName()).isEqualTo(domain.getName()); @@ -330,10 +356,10 @@ public class JavaRestClientTest extends AbstractContainerTest { restClient.login("sysadmin@thingsboard.org", "sysadmin"); MobileApp mobileApp = new MobileApp(); - String prefix = RandomStringUtils.randomAlphabetic(5).toLowerCase(); + String prefix = RandomStringUtils.insecure().randomAlphabetic(5).toLowerCase(); mobileApp.setPkgName(prefix + "test.app.apple"); mobileApp.setPlatformType(PlatformType.ANDROID); - mobileApp.setAppSecret(RandomStringUtils.randomAlphabetic(20)); + mobileApp.setAppSecret(RandomStringUtils.insecure().randomAlphabetic(20)); mobileApp.setStatus(MobileAppStatus.DRAFT); MobileApp savedMobileApp = restClient.saveMobileApp(mobileApp); @@ -343,7 +369,7 @@ public class JavaRestClientTest extends AbstractContainerTest { assertThat(retrieved.getData()).hasSize(1); MobileAppBundle mobileAppBundle = new MobileAppBundle(); - String bundlePrefix = RandomStringUtils.randomAlphabetic(5).toLowerCase(); + String bundlePrefix = RandomStringUtils.insecure().randomAlphabetic(5).toLowerCase(); mobileAppBundle.setTitle(bundlePrefix + "Test Bundle"); mobileAppBundle.setAndroidAppId(savedMobileApp.getId()); @@ -357,7 +383,7 @@ public class JavaRestClientTest extends AbstractContainerTest { filter.setUsersIds(Arrays.stream(usersIds).map(UUIDBased::getId).toList()); NotificationTarget notificationTarget = new NotificationTarget(); - notificationTarget.setName(filter + RandomStringUtils.randomNumeric(5)); + notificationTarget.setName(filter + RandomStringUtils.insecure().randomNumeric(5)); PlatformUsersNotificationTargetConfig targetConfig = new PlatformUsersNotificationTargetConfig(); targetConfig.setUsersFilter(filter); notificationTarget.setConfiguration(targetConfig); @@ -367,7 +393,7 @@ public class JavaRestClientTest extends AbstractContainerTest { private NotificationTemplate createNotificationTemplate(NotificationType notificationType, String subject, String text, NotificationDeliveryMethod... deliveryMethods) { NotificationTemplate notificationTemplate = new NotificationTemplate(); - notificationTemplate.setName("Notification template: " + RandomStringUtils.randomAlphabetic(5)); + notificationTemplate.setName("Notification template: " + RandomStringUtils.insecure().randomAlphabetic(5)); notificationTemplate.setNotificationType(notificationType); NotificationTemplateConfig config = new NotificationTemplateConfig(); config.setDeliveryMethodsTemplates(new HashMap<>()); @@ -418,9 +444,9 @@ public class JavaRestClientTest extends AbstractContainerTest { public void testApiKeyOperations() { // Create an API key ApiKeyInfo apiKeyInfo = new ApiKeyInfo(); - apiKeyInfo.setDescription("Test API Key " + RandomStringUtils.randomAlphabetic(5)); + apiKeyInfo.setDescription("Test API Key " + RandomStringUtils.insecure().randomAlphabetic(5)); apiKeyInfo.setEnabled(true); - apiKeyInfo.setUserId(user.getId()); + apiKeyInfo.setUserId(tenantAdmin1.getId()); apiKeyInfo.setExpirationTime(0); ApiKey savedApiKey = restClient.saveApiKey(apiKeyInfo); @@ -428,18 +454,18 @@ public class JavaRestClientTest extends AbstractContainerTest { assertThat(savedApiKey.getId()).isNotNull(); assertThat(savedApiKey.getDescription()).isEqualTo(apiKeyInfo.getDescription()); assertThat(savedApiKey.isEnabled()).isTrue(); - assertThat(savedApiKey.getUserId()).isEqualTo(user.getId()); - assertThat(savedApiKey.getTenantId()).isEqualTo(tenant.getId()); + assertThat(savedApiKey.getUserId()).isEqualTo(tenantAdmin1.getId()); + assertThat(savedApiKey.getTenantId()).isEqualTo(tenant1.getId()); assertThat(savedApiKey.getValue()).isNotNull(); // Get user API keys - PageData apiKeys = restClient.getUserApiKeys(user.getId(), new PageLink(10)); + PageData apiKeys = restClient.getUserApiKeys(tenantAdmin1.getId(), new PageLink(10)); assertThat(apiKeys).isNotNull(); assertThat(apiKeys.getData()).hasSize(1); assertThat(apiKeys.getData().get(0).getId()).isEqualTo(savedApiKey.getId()); // Update API key description - String updatedDescription = "Updated description " + RandomStringUtils.randomAlphabetic(5); + String updatedDescription = "Updated description " + RandomStringUtils.insecure().randomAlphabetic(5); ApiKeyInfo updatedApiKeyInfo = restClient.updateApiKeyDescription(savedApiKey.getId(), updatedDescription); assertThat(updatedApiKeyInfo).isNotNull(); assertThat(updatedApiKeyInfo.getDescription()).isEqualTo(updatedDescription); @@ -458,8 +484,96 @@ public class JavaRestClientTest extends AbstractContainerTest { restClient.deleteApiKey(savedApiKey.getId()); // Verify the API key is deleted - PageData apiKeysAfterDelete = restClient.getUserApiKeys(user.getId(), new PageLink(10)); + PageData apiKeysAfterDelete = restClient.getUserApiKeys(tenantAdmin1.getId(), new PageLink(10)); assertThat(apiKeysAfterDelete.getData()).isEmpty(); } + @Test + public void testGetDeviceProfileInfosByIds() { + var profileData = new DeviceProfileData(); + profileData.setConfiguration(new DefaultDeviceProfileConfiguration()); + profileData.setTransportConfiguration(new DefaultDeviceProfileTransportConfiguration()); + + // Create a device profile in tenant1 (current tenant) + var deviceProfile1 = new DeviceProfile(); + deviceProfile1.setTenantId(tenant1.getId()); + deviceProfile1.setName("Device Profile 1"); + deviceProfile1.setType(DeviceProfileType.DEFAULT); + deviceProfile1.setTransportType(DeviceTransportType.DEFAULT); + deviceProfile1.setProfileData(profileData); + deviceProfile1 = restClient.saveDeviceProfile(deviceProfile1); + + var deviceProfile2 = new DeviceProfile(); + deviceProfile2.setTenantId(tenant1.getId()); + deviceProfile2.setName("Device Profile 2"); + deviceProfile2.setType(DeviceProfileType.DEFAULT); + deviceProfile2.setTransportType(DeviceTransportType.DEFAULT); + deviceProfile2.setProfileData(profileData); + deviceProfile2 = restClient.saveDeviceProfile(deviceProfile2); + + // Create two more device profiles in tenant2 (different tenant) + restClient.login(tenantAdmin2.getEmail(), "password123"); + + var deviceProfile3 = new DeviceProfile(); + deviceProfile3.setTenantId(tenant2.getId()); + deviceProfile3.setName("Device Profile 3"); + deviceProfile3.setType(DeviceProfileType.DEFAULT); + deviceProfile3.setTransportType(DeviceTransportType.DEFAULT); + deviceProfile3.setProfileData(profileData); + deviceProfile3 = restClient.saveDeviceProfile(deviceProfile3); + + var deviceProfile4 = new DeviceProfile(); + deviceProfile4.setTenantId(tenant2.getId()); + deviceProfile4.setName("Device Profile 4"); + deviceProfile4.setType(DeviceProfileType.DEFAULT); + deviceProfile4.setTransportType(DeviceTransportType.DEFAULT); + deviceProfile4.setProfileData(profileData); + deviceProfile4 = restClient.saveDeviceProfile(deviceProfile4); + + // Attempt to fetch profiles 1 and 3 while acting as Tenant 2. + // - Profile 1: Filtered out because it belongs to a different tenant. + // - Profile 2: Filtered out because it belongs to a different tenant and was not requested. + // - Profile 3: Should be returned. + // - Profile 4: Filtered out; it belongs to the correct tenant, but was not requested by ID. + List profiles = restClient.getDeviceProfileInfosByIds(Set.of(deviceProfile1.getUuidId(), deviceProfile3.getUuidId())); + assertThat(profiles).hasSize(1); + assertThat(profiles.get(0).getId()).isEqualTo(deviceProfile3.getId()); + } + + @Test + public void testGetAssetProfilesByIds() { + // Create two asset profiles in tenant1 (current tenant) + var assetProfile1 = new AssetProfile(); + assetProfile1.setTenantId(tenant1.getId()); + assetProfile1.setName("Asset Profile 1"); + assetProfile1 = restClient.saveAssetProfile(assetProfile1); + + var assetProfile2 = new AssetProfile(); + assetProfile2.setTenantId(tenant1.getId()); + assetProfile2.setName("Asset Profile 2"); + assetProfile2 = restClient.saveAssetProfile(assetProfile2); + + // Create two more asset profiles in tenant2 (different tenant) + restClient.login(tenantAdmin2.getEmail(), "password123"); + + var assetProfile3 = new AssetProfile(); + assetProfile3.setTenantId(tenant2.getId()); + assetProfile3.setName("Asset Profile 3"); + assetProfile3 = restClient.saveAssetProfile(assetProfile3); + + var assetProfile4 = new AssetProfile(); + assetProfile4.setTenantId(tenant2.getId()); + assetProfile4.setName("Asset Profile 4"); + assetProfile4 = restClient.saveAssetProfile(assetProfile4); + + // Attempt to fetch profiles 1 and 3 while acting as Tenant 2. + // - Profile 1: Filtered out because it belongs to a different tenant. + // - Profile 2: Filtered out because it belongs to a different tenant and was not requested. + // - Profile 3: Should be returned. + // - Profile 4: Filtered out; it belongs to the correct tenant, but was not requested by ID. + List profiles = restClient.getAssetProfilesByIds(Set.of(assetProfile1.getUuidId(), assetProfile3.getUuidId())); + assertThat(profiles).hasSize(1); + assertThat(profiles.get(0).getId()).isEqualTo(assetProfile3.getId()); + } + } diff --git a/rest-client/pom.xml b/rest-client/pom.xml index fd5783227b..33fa4b0c34 100644 --- a/rest-client/pom.xml +++ b/rest-client/pom.xml @@ -51,6 +51,10 @@ com.auth0 java-jwt + + org.apache.httpcomponents.core5 + httpcore5 + diff --git a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java index 83029c4f78..5ecfb07b64 100644 --- a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java +++ b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java @@ -23,6 +23,7 @@ import lombok.Getter; import lombok.SneakyThrows; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.concurrent.LazyInitializer; +import org.apache.hc.core5.net.URIBuilder; import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.Resource; @@ -157,11 +158,11 @@ import org.thingsboard.server.common.data.oauth2.PlatformType; import org.thingsboard.server.common.data.ota.ChecksumAlgorithm; import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.common.data.pat.ApiKey; -import org.thingsboard.server.common.data.pat.ApiKeyInfo; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.SortOrder; import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.common.data.pat.ApiKey; +import org.thingsboard.server.common.data.pat.ApiKeyInfo; import org.thingsboard.server.common.data.plugin.ComponentDescriptor; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.query.AlarmCountQuery; @@ -210,17 +211,21 @@ import org.thingsboard.server.common.data.widget.WidgetsBundle; import java.io.Closeable; import java.io.IOException; import java.net.URI; +import java.net.URISyntaxException; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.UUID; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import static java.util.stream.Collectors.joining; import static org.thingsboard.server.common.data.StringUtils.isEmpty; public class RestClient implements Closeable { @@ -1588,6 +1593,33 @@ public class RestClient implements Closeable { }, activeOnly).getBody(); } + public List getDeviceProfileInfosByIds(Set ids) { + URIBuilder builder; + try { + builder = new URIBuilder(baseURL); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Invalid base URL: " + baseURL, e); + } + + builder.appendPath("/api/deviceProfileInfos"); + + String commaSeparatedIds = ids.stream() + .filter(Objects::nonNull) + .map(UUID::toString) + .collect(joining(",")); + + builder.addParameter("deviceProfileIds", commaSeparatedIds); + + URI uri; + try { + uri = builder.build(); + } catch (URISyntaxException e) { + throw new IllegalStateException("Failed to construct API URI from base URL and provided params", e); + } + + return restTemplate.exchange(uri, HttpMethod.GET, null, new ParameterizedTypeReference>() {}).getBody(); + } + public JsonNode claimDevice(String deviceName, ClaimRequest claimRequest) { return restTemplate.exchange( baseURL + "/api/customer/device/{deviceName}/claim", @@ -1824,6 +1856,33 @@ public class RestClient implements Closeable { }, params).getBody(); } + public List getAssetProfilesByIds(Set ids) { + URIBuilder builder; + try { + builder = new URIBuilder(baseURL); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Invalid base URL: " + baseURL, e); + } + + builder.appendPath("/api/assetProfileInfos"); + + String commaSeparatedIds = ids.stream() + .filter(Objects::nonNull) + .map(UUID::toString) + .collect(joining(",")); + + builder.addParameter("assetProfileIds", commaSeparatedIds); + + URI uri; + try { + uri = builder.build(); + } catch (URISyntaxException e) { + throw new IllegalStateException("Failed to construct API URI from base URL and provided params", e); + } + + return restTemplate.exchange(uri, HttpMethod.GET, null, new ParameterizedTypeReference>() {}).getBody(); + } + public Long countEntitiesByQuery(EntityCountQuery query) { return restTemplate.postForObject(baseURL + "/api/entitiesQuery/count", query, Long.class); } From 7b17e191f7355879d3d7f6e3c343c2d03af199fa Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Thu, 29 Jan 2026 10:30:52 +0200 Subject: [PATCH 002/202] UI: Fixed import --- ui-ngx/src/app/shared/models/alarm.models.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/ui-ngx/src/app/shared/models/alarm.models.ts b/ui-ngx/src/app/shared/models/alarm.models.ts index 162b898513..41e8940d55 100644 --- a/ui-ngx/src/app/shared/models/alarm.models.ts +++ b/ui-ngx/src/app/shared/models/alarm.models.ts @@ -28,7 +28,6 @@ import { UserId } from '@shared/models/id/user-id'; import { AlarmFilter } from '@shared/models/query/query.models'; import { HasTenantId } from '@shared/models/entity.models'; import { isDefinedAndNotNull, isNotEmptyStr } from '@core/utils'; -import { defaults } from 'lodash'; export enum AlarmsMode { ALL, From ec443f116c97e2f9f6ad6233306249bdb9d24e9f Mon Sep 17 00:00:00 2001 From: Oleksandra Matviienko Date: Mon, 31 Mar 2025 15:16:24 +0200 Subject: [PATCH 003/202] Audit logging for TenantProfile Signed-off-by: Oleksandra Matviienko --- .../server/controller/TenantProfileController.java | 4 ++-- .../tenant/profile/DefaultTbTenantProfileService.java | 11 +++++++++-- .../tenant/profile/TbTenantProfileService.java | 5 +++-- .../server/controller/AbstractWebTest.java | 3 ++- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java index c0f7b9eb40..2dc6d6310c 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java @@ -191,7 +191,7 @@ public class TenantProfileController extends BaseController { oldProfile = checkTenantProfileId(tenantProfile.getId(), Operation.WRITE); } - return tbTenantProfileService.save(getTenantId(), tenantProfile, oldProfile); + return tbTenantProfileService.save(getTenantId(), tenantProfile, oldProfile, getCurrentUser()); } @ApiOperation(value = "Delete Tenant Profile (deleteTenantProfile)", @@ -204,7 +204,7 @@ public class TenantProfileController extends BaseController { checkParameter("tenantProfileId", strTenantProfileId); TenantProfileId tenantProfileId = new TenantProfileId(toUUID(strTenantProfileId)); TenantProfile profile = checkTenantProfileId(tenantProfileId, Operation.DELETE); - tbTenantProfileService.delete(getTenantId(), profile); + tbTenantProfileService.delete(getTenantId(), profile, getCurrentUser()); } @ApiOperation(value = "Make tenant profile default (setDefaultTenantProfile)", diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/DefaultTbTenantProfileService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/DefaultTbTenantProfileService.java index fce44972d8..8a1553b267 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/DefaultTbTenantProfileService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/DefaultTbTenantProfileService.java @@ -19,8 +19,10 @@ import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.User; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.dao.tenant.TenantService; @@ -41,18 +43,23 @@ public class DefaultTbTenantProfileService extends AbstractTbEntityService imple private final TbTenantProfileCache tenantProfileCache; @Override - public TenantProfile save(TenantId tenantId, TenantProfile tenantProfile, TenantProfile oldTenantProfile) throws ThingsboardException { + public TenantProfile save(TenantId tenantId, TenantProfile tenantProfile, TenantProfile oldTenantProfile, User user) throws ThingsboardException { + ActionType actionType = tenantProfile.getId() == null ? ActionType.ADDED : ActionType.UPDATED; TenantProfile savedTenantProfile = checkNotNull(tenantProfileService.saveTenantProfile(tenantId, tenantProfile)); tenantProfileCache.put(savedTenantProfile); List tenantIds = tenantService.findTenantIdsByTenantProfileId(savedTenantProfile.getId()); tbQueueService.updateQueuesByTenants(tenantIds, savedTenantProfile, oldTenantProfile); + logEntityActionService.logEntityAction(tenantId, savedTenantProfile.getId(), savedTenantProfile, null, + actionType, user); return savedTenantProfile; } @Override - public void delete(TenantId tenantId, TenantProfile tenantProfile) throws ThingsboardException { + public void delete(TenantId tenantId, TenantProfile tenantProfile, User user) throws ThingsboardException { + ActionType actionType = ActionType.DELETED; tenantProfileService.deleteTenantProfile(tenantId, tenantProfile.getId()); + logEntityActionService.logEntityAction(tenantId, tenantProfile.getId(), tenantProfile, null, actionType, user); } } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/TbTenantProfileService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/TbTenantProfileService.java index 3ca8f0570b..564ced935b 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/TbTenantProfileService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/TbTenantProfileService.java @@ -18,10 +18,11 @@ package org.thingsboard.server.service.entitiy.tenant.profile; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.User; public interface TbTenantProfileService { - TenantProfile save(TenantId tenantId, TenantProfile tenantProfile, TenantProfile oldTenantProfile) throws ThingsboardException; + TenantProfile save(TenantId tenantId, TenantProfile tenantProfile, TenantProfile oldTenantProfile, User user) throws ThingsboardException; - void delete(TenantId tenantId, TenantProfile tenantProfile) throws ThingsboardException; + void delete(TenantId tenantId, TenantProfile tenantProfile, User user) throws ThingsboardException; } diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index 6fbe9d9864..52423619d8 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -1265,10 +1265,11 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { } protected void updateDefaultTenantProfile(Consumer updater) throws ThingsboardException { + User user = Mockito.mock(User.class); TenantProfile oldTenantProfile = tenantProfileService.findDefaultTenantProfile(TenantId.SYS_TENANT_ID); TenantProfile tenantProfile = JacksonUtil.clone(oldTenantProfile); updater.accept(tenantProfile); - tbTenantProfileService.save(TenantId.SYS_TENANT_ID, tenantProfile, oldTenantProfile); + tbTenantProfileService.save(TenantId.SYS_TENANT_ID, tenantProfile, oldTenantProfile, user); } protected OAuth2Client createOauth2Client(TenantId tenantId, String title) { From 164ff0d467f82e2aef24069dd3a85d2e7e7d016d Mon Sep 17 00:00:00 2001 From: Oleksandra Matviienko Date: Wed, 20 Aug 2025 17:11:42 +0200 Subject: [PATCH 004/202] Refactored audit logs for save/delete tenant profile operations; moved setDefaultTenantProfile to the service layer; added Awaitility-based audit log checks in controller tests; allowed SYS_ADMIN access to audit endpoints; made BuildProperties optional with version fallback; used tenantAdminUser in updateDefaultTenantProfile; updated logging config for audit debugging. Signed-off-by: Oleksandra_Matviienko --- .../server/controller/AuditLogController.java | 4 +- .../controller/TenantProfileController.java | 2 +- .../DefaultTbTenantProfileService.java | 67 ++++++++++++++++--- .../profile/TbTenantProfileService.java | 2 + .../controller/AbstractNotifyEntityTest.java | 5 ++ .../server/controller/AbstractWebTest.java | 3 +- .../TenantProfileControllerTest.java | 32 ++++++++- .../src/test/resources/logback-test.xml | 1 + .../server/dao/audit/AuditLogServiceImpl.java | 2 +- 9 files changed, 100 insertions(+), 18 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java b/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java index e22c0aa156..5d34959530 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java @@ -139,7 +139,7 @@ public class AuditLogController extends BaseController { "Basically, this API call is used to get the full lifecycle of some specific entity. " + "For example to see when a device was created, updated, assigned to some customer, or even deleted from the system. " + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @RequestMapping(value = "/audit/logs/entity/{entityType}/{entityId}", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody public PageData getAuditLogsByEntityId( @@ -174,7 +174,7 @@ public class AuditLogController extends BaseController { @ApiOperation(value = "Get all audit logs (getAuditLogs)", notes = "Returns a page of audit logs related to all entities in the scope of the current user's Tenant. " + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @RequestMapping(value = "/audit/logs", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody public PageData getAuditLogs( diff --git a/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java index 2dc6d6310c..18bc488394 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java @@ -217,7 +217,7 @@ public class TenantProfileController extends BaseController { checkParameter("tenantProfileId", strTenantProfileId); TenantProfileId tenantProfileId = new TenantProfileId(toUUID(strTenantProfileId)); TenantProfile tenantProfile = checkTenantProfileId(tenantProfileId, Operation.WRITE); - tenantProfileService.setDefaultTenantProfile(getTenantId(), tenantProfileId); + tenantProfile = tbTenantProfileService.setDefaultTenantProfile(getTenantId(), tenantProfile, getCurrentUser()); return tenantProfile; } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/DefaultTbTenantProfileService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/DefaultTbTenantProfileService.java index 8a1553b267..c2155678bd 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/DefaultTbTenantProfileService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/DefaultTbTenantProfileService.java @@ -18,11 +18,14 @@ package org.thingsboard.server.service.entitiy.tenant.profile; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.dao.tenant.TenantService; @@ -43,23 +46,67 @@ public class DefaultTbTenantProfileService extends AbstractTbEntityService imple private final TbTenantProfileCache tenantProfileCache; @Override - public TenantProfile save(TenantId tenantId, TenantProfile tenantProfile, TenantProfile oldTenantProfile, User user) throws ThingsboardException { + public TenantProfile + save(TenantId tenantId, TenantProfile tenantProfile, TenantProfile oldTenantProfile, User user) throws ThingsboardException { ActionType actionType = tenantProfile.getId() == null ? ActionType.ADDED : ActionType.UPDATED; - TenantProfile savedTenantProfile = checkNotNull(tenantProfileService.saveTenantProfile(tenantId, tenantProfile)); - tenantProfileCache.put(savedTenantProfile); + try { + TenantProfile savedTenantProfile = checkNotNull(tenantProfileService.saveTenantProfile(tenantId, tenantProfile)); + tenantProfileCache.put(savedTenantProfile); - List tenantIds = tenantService.findTenantIdsByTenantProfileId(savedTenantProfile.getId()); - tbQueueService.updateQueuesByTenants(tenantIds, savedTenantProfile, oldTenantProfile); - logEntityActionService.logEntityAction(tenantId, savedTenantProfile.getId(), savedTenantProfile, null, - actionType, user); + List tenantIds = tenantService.findTenantIdsByTenantProfileId(savedTenantProfile.getId()); + tbQueueService.updateQueuesByTenants(tenantIds, savedTenantProfile, oldTenantProfile); + logEntityActionService.logEntityAction(tenantId, savedTenantProfile.getId(), savedTenantProfile, null, + actionType, user); + + return savedTenantProfile; + } catch (ThingsboardException e) { + log.error("Failed to save tenant profile because ThingsboardException [{}]", tenantProfile, e); + logEntityActionService.logEntityAction(tenantId, emptyId(EntityType.TENANT_PROFILE), tenantProfile, actionType, user, e); + throw e; + } catch (DataValidationException e) { + log.error("Failed to save tenant profile because data validation [{}]", tenantProfile, e); + logEntityActionService.logEntityAction(tenantId, emptyId(EntityType.TENANT_PROFILE), tenantProfile, actionType, user, e); + throw new ThingsboardException(e.getMessage(), ThingsboardErrorCode.BAD_REQUEST_PARAMS); + } catch (Exception e) { + log.error("Failed to save tenant profile because Exception [{}]", tenantProfile, e); + logEntityActionService.logEntityAction(tenantId, emptyId(EntityType.TENANT_PROFILE), tenantProfile, actionType, user, e); + throw new ThingsboardException(e, ThingsboardErrorCode.GENERAL); + } - return savedTenantProfile; } @Override public void delete(TenantId tenantId, TenantProfile tenantProfile, User user) throws ThingsboardException { ActionType actionType = ActionType.DELETED; - tenantProfileService.deleteTenantProfile(tenantId, tenantProfile.getId()); - logEntityActionService.logEntityAction(tenantId, tenantProfile.getId(), tenantProfile, null, actionType, user); + try { + tenantProfileService.deleteTenantProfile(tenantId, tenantProfile.getId()); + logEntityActionService.logEntityAction(tenantId, tenantProfile.getId(), tenantProfile, null, actionType, user); + } catch (Exception e) { + logEntityActionService.logEntityAction(tenantId, tenantProfile.getId(), tenantProfile, null, actionType, user); + throw e; + } + } + + @Override + public TenantProfile setDefaultTenantProfile(TenantId tenantId, TenantProfile tenantProfile, User user) throws ThingsboardException { + ActionType actionType = ActionType.UPDATED; + try { + boolean changed = tenantProfileService.setDefaultTenantProfile(tenantId, tenantProfile.getId()); + TenantProfile result = tenantProfileService.findTenantProfileById(tenantId, tenantProfile.getId()); + if (changed && result != null) { + // Update application-level cache + tenantProfileCache.put(result); + } + logEntityActionService.logEntityAction(tenantId, result != null ? result.getId() : tenantProfile.getId(), result, null, actionType, user); + return result != null ? result : tenantProfile; + } catch (DataValidationException e) { + log.error("Failed to set default tenant profile due to data validation [{}]", tenantProfile, e); + logEntityActionService.logEntityAction(tenantId, emptyId(EntityType.TENANT_PROFILE), tenantProfile, actionType, user, e); + throw new ThingsboardException(e.getMessage(), ThingsboardErrorCode.BAD_REQUEST_PARAMS); + } catch (Exception e) { + log.error("Failed to set default tenant profile [{}]", tenantProfile, e); + logEntityActionService.logEntityAction(tenantId, emptyId(EntityType.TENANT_PROFILE), tenantProfile, actionType, user, e); + throw new ThingsboardException(e, ThingsboardErrorCode.GENERAL); + } } } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/TbTenantProfileService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/TbTenantProfileService.java index 564ced935b..66ddbbca2a 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/TbTenantProfileService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/TbTenantProfileService.java @@ -25,4 +25,6 @@ public interface TbTenantProfileService { TenantProfile save(TenantId tenantId, TenantProfile tenantProfile, TenantProfile oldTenantProfile, User user) throws ThingsboardException; void delete(TenantId tenantId, TenantProfile tenantProfile, User user) throws ThingsboardException; + + TenantProfile setDefaultTenantProfile(TenantId tenantId, TenantProfile tenantProfile, User user) throws ThingsboardException; } diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java index fcbd60eb4d..62a266f26a 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java @@ -18,6 +18,8 @@ package org.thingsboard.server.controller; import lombok.extern.slf4j.Slf4j; import org.mockito.ArgumentMatcher; import org.mockito.Mockito; +import org.springframework.boot.info.BuildProperties; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.mock.mockito.SpyBean; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.EdgeUtils; @@ -59,6 +61,9 @@ public abstract class AbstractNotifyEntityTest extends AbstractWebTest { @SpyBean protected AuditLogService auditLogService; + @MockBean + BuildProperties buildProperties; + protected final String msgErrorPermission = "You don't have permission to perform this operation!"; protected final String msgErrorShouldBeSpecified = "should be specified"; protected final String msgErrorNotFound = "Requested item wasn't found!"; diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index 52423619d8..6ed5f85147 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -1265,11 +1265,10 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { } protected void updateDefaultTenantProfile(Consumer updater) throws ThingsboardException { - User user = Mockito.mock(User.class); TenantProfile oldTenantProfile = tenantProfileService.findDefaultTenantProfile(TenantId.SYS_TENANT_ID); TenantProfile tenantProfile = JacksonUtil.clone(oldTenantProfile); updater.accept(tenantProfile); - tbTenantProfileService.save(TenantId.SYS_TENANT_ID, tenantProfile, oldTenantProfile, user); + tbTenantProfileService.save(TenantId.SYS_TENANT_ID, tenantProfile, oldTenantProfile, tenantAdminUser); } protected OAuth2Client createOauth2Client(TenantId tenantId, String title) { diff --git a/application/src/test/java/org/thingsboard/server/controller/TenantProfileControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/TenantProfileControllerTest.java index 42a27eb6d8..feca8ed97b 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TenantProfileControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TenantProfileControllerTest.java @@ -25,9 +25,12 @@ import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.common.data.audit.AuditLog; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.queue.ProcessingStrategy; import org.thingsboard.server.common.data.queue.ProcessingStrategyType; @@ -39,6 +42,7 @@ import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfi import org.thingsboard.server.common.data.validation.RateLimit; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.queue.TbQueueCallback; +import org.awaitility.Awaitility; import java.lang.reflect.Field; import java.util.ArrayList; @@ -47,6 +51,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.stream.Collectors; @@ -81,12 +86,17 @@ public class TenantProfileControllerTest extends AbstractControllerTest { testBroadcastEntityStateChangeEventTimeManyTimeTenantProfile(savedTenantProfile, ComponentLifecycleEvent.CREATED, 1); + awaitAuditLog("Wait for async audit log to be persisted (ADDED expected)", savedTenantProfile.getId(), ActionType.ADDED); + savedTenantProfile.setName("New tenant profile"); doPost("/api/tenantProfile", savedTenantProfile, TenantProfile.class); TenantProfile foundTenantProfile = doGet("/api/tenantProfile/" + savedTenantProfile.getId().getId().toString(), TenantProfile.class); Assert.assertEquals(foundTenantProfile.getName(), savedTenantProfile.getName()); testBroadcastEntityStateChangeEventTimeManyTimeTenantProfile(savedTenantProfile, ComponentLifecycleEvent.UPDATED, 1); + + awaitAuditLog("Wait for async audit log to be persisted (UPDATED expected)", savedTenantProfile.getId(), ActionType.UPDATED); + } @Test @@ -180,7 +190,8 @@ public class TenantProfileControllerTest extends AbstractControllerTest { Assert.assertNotNull(foundDefaultTenantProfile); Assert.assertEquals(savedTenantProfile.getName(), foundDefaultTenantProfile.getName()); Assert.assertEquals(savedTenantProfile.getId(), foundDefaultTenantProfile.getId()); - } + + awaitAuditLog("Wait for async audit log to be persisted (UPDATED expected for NEW DEFAULT Tenant Profile)", savedTenantProfile.getId(), ActionType.UPDATED); } @Test public void testSaveTenantProfileWithEmptyName() throws Exception { @@ -245,6 +256,8 @@ public class TenantProfileControllerTest extends AbstractControllerTest { doDelete("/api/tenantProfile/" + savedTenantProfile.getId().getId().toString()) .andExpect(status().isOk()); + awaitAuditLog("Wait for async audit log to be persisted for deleted tenant profile", savedTenantProfile.getId(), ActionType.DELETED); + testBroadcastEntityStateChangeEventTimeManyTimeTenantProfile(savedTenantProfile, ComponentLifecycleEvent.DELETED, 1); doGet("/api/tenantProfile/" + savedTenantProfile.getId().getId().toString()) @@ -393,6 +406,22 @@ public class TenantProfileControllerTest extends AbstractControllerTest { testBroadcastEntityStateChangeEventNeverTenantProfile(); } + private void awaitAuditLog(String awaitMessage, TenantProfileId tenantProfileId, ActionType expectedAction) throws Exception { + Awaitility.await(awaitMessage) + .atMost(TIMEOUT, TimeUnit.SECONDS) + .until(() -> + doGetTypedWithTimePageLink( + "/api/audit/logs/entity/TENANT_PROFILE/" + tenantProfileId.getId() + "?", + new TypeReference>() { + }, + new TimePageLink(5)) + .getData() + .stream() + .anyMatch(log -> log.getActionType() == expectedAction) + ); + + } + private TenantProfile createTenantProfile(String name) { TenantProfile tenantProfile = new TenantProfile(); tenantProfile.setName(name); @@ -429,7 +458,6 @@ public class TenantProfileControllerTest extends AbstractControllerTest { tenantProfile.setProfileData(profileData); } - private void testBroadcastEntityStateChangeEventTimeManyTimeTenantProfile(TenantProfile tenantProfile, ComponentLifecycleEvent event, int cntTime) { ArgumentMatcher matcherTenantProfile = cntTime == 1 ? argument -> argument.equals(tenantProfile) : argument -> argument.getClass().equals(TenantProfile.class); diff --git a/application/src/test/resources/logback-test.xml b/application/src/test/resources/logback-test.xml index 6faaf97536..b5a1ac86ae 100644 --- a/application/src/test/resources/logback-test.xml +++ b/application/src/test/resources/logback-test.xml @@ -16,6 +16,7 @@ + diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java index 27a52e1cc7..90fe102fce 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java @@ -119,7 +119,7 @@ public class AuditLogServiceImpl implements AuditLogService { public ListenableFuture logEntityAction(TenantId tenantId, CustomerId customerId, UserId userId, String userName, I entityId, E entity, ActionType actionType, Exception e, Object... additionalInfo) { - if (canLog(entityId.getEntityType(), actionType)) { + if (canLog(entityId.getEntityType(), actionType) || tenantId.isSysTenantId()) { JsonNode actionData = constructActionData(entityId, entity, actionType, additionalInfo); ActionStatus actionStatus = ActionStatus.SUCCESS; String failureDetails = ""; From 90404d80fd89bffc6a95bdd1c2a7f803ee666cae Mon Sep 17 00:00:00 2001 From: Oleksandra Matviienko Date: Mon, 26 Jan 2026 22:57:03 +0100 Subject: [PATCH 005/202] sysadmin audit log refactored after review Signed-off-by: Oleksandra_Matviienko --- .../server/controller/AuditLogController.java | 5 +++-- .../server/controller/BaseController.java | 6 ++---- .../profile/DefaultTbTenantProfileService.java | 15 +++++---------- .../server/controller/AbstractWebTest.java | 1 + .../controller/TenantProfileControllerTest.java | 7 ++++--- .../server/dao/tenant/TenantProfileService.java | 2 +- .../server/dao/audit/AuditLogServiceImpl.java | 2 +- .../dao/tenant/TenantProfileServiceImpl.java | 12 ++++-------- .../dao/service/TenantProfileServiceTest.java | 8 ++++---- 9 files changed, 25 insertions(+), 33 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java b/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java index 5d34959530..8c0d0d2243 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java @@ -50,6 +50,7 @@ import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PA import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.controller.ControllerConstants.USER_ID_PARAM_DESCRIPTION; @@ -138,7 +139,7 @@ public class AuditLogController extends BaseController { notes = "Returns a page of audit logs related to the actions on the targeted entity. " + "Basically, this API call is used to get the full lifecycle of some specific entity. " + "For example to see when a device was created, updated, assigned to some customer, or even deleted from the system. " + - PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) + PAGE_DATA_PARAMETERS + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @RequestMapping(value = "/audit/logs/entity/{entityType}/{entityId}", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody @@ -173,7 +174,7 @@ public class AuditLogController extends BaseController { @ApiOperation(value = "Get all audit logs (getAuditLogs)", notes = "Returns a page of audit logs related to all entities in the scope of the current user's Tenant. " + - PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) + PAGE_DATA_PARAMETERS + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @RequestMapping(value = "/audit/logs", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index bd2a160d5b..48e0f11552 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -881,10 +881,8 @@ public abstract class BaseController { protected > void logEntityAction(SecurityUser user, EntityType entityType, E entity, E savedEntity, ActionType actionType, Exception e) { EntityId entityId = savedEntity != null ? savedEntity.getId() : emptyId(entityType); - if (!user.isSystemAdmin()) { - entityActionService.logEntityAction(user, entityId, savedEntity != null ? savedEntity : entity, - user.getCustomerId(), actionType, e); - } + entityActionService.logEntityAction(user, entityId, savedEntity != null ? savedEntity : entity, + user.getCustomerId(), actionType, e); } protected > E doSaveAndLog(EntityType entityType, E entity, BiFunction savingFunction) throws Exception { diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/DefaultTbTenantProfileService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/DefaultTbTenantProfileService.java index c2155678bd..cecc365c7c 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/DefaultTbTenantProfileService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/DefaultTbTenantProfileService.java @@ -52,11 +52,11 @@ public class DefaultTbTenantProfileService extends AbstractTbEntityService imple try { TenantProfile savedTenantProfile = checkNotNull(tenantProfileService.saveTenantProfile(tenantId, tenantProfile)); tenantProfileCache.put(savedTenantProfile); + logEntityActionService.logEntityAction(tenantId, savedTenantProfile.getId(), savedTenantProfile, null, + actionType, user); List tenantIds = tenantService.findTenantIdsByTenantProfileId(savedTenantProfile.getId()); tbQueueService.updateQueuesByTenants(tenantIds, savedTenantProfile, oldTenantProfile); - logEntityActionService.logEntityAction(tenantId, savedTenantProfile.getId(), savedTenantProfile, null, - actionType, user); return savedTenantProfile; } catch (ThingsboardException e) { @@ -91,14 +91,9 @@ public class DefaultTbTenantProfileService extends AbstractTbEntityService imple public TenantProfile setDefaultTenantProfile(TenantId tenantId, TenantProfile tenantProfile, User user) throws ThingsboardException { ActionType actionType = ActionType.UPDATED; try { - boolean changed = tenantProfileService.setDefaultTenantProfile(tenantId, tenantProfile.getId()); - TenantProfile result = tenantProfileService.findTenantProfileById(tenantId, tenantProfile.getId()); - if (changed && result != null) { - // Update application-level cache - tenantProfileCache.put(result); - } - logEntityActionService.logEntityAction(tenantId, result != null ? result.getId() : tenantProfile.getId(), result, null, actionType, user); - return result != null ? result : tenantProfile; + TenantProfile savedTenantProfile = tenantProfileService.setDefaultTenantProfile(tenantId, tenantProfile.getId()); + logEntityActionService.logEntityAction(tenantId, tenantProfile.getId(), savedTenantProfile, null, actionType, user); + return savedTenantProfile; } catch (DataValidationException e) { log.error("Failed to set default tenant profile due to data validation [{}]", tenantProfile, e); logEntityActionService.logEntityAction(tenantId, emptyId(EntityType.TENANT_PROFILE), tenantProfile, actionType, user, e); diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index 6ed5f85147..71cbed6d10 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -1268,6 +1268,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { TenantProfile oldTenantProfile = tenantProfileService.findDefaultTenantProfile(TenantId.SYS_TENANT_ID); TenantProfile tenantProfile = JacksonUtil.clone(oldTenantProfile); updater.accept(tenantProfile); + // user should be sysadmin as this operation allowed only for sysadmins. But for the simplification of the test - already existed variable provided. This affects only an audit log content tbTenantProfileService.save(TenantId.SYS_TENANT_ID, tenantProfile, oldTenantProfile, tenantAdminUser); } diff --git a/application/src/test/java/org/thingsboard/server/controller/TenantProfileControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/TenantProfileControllerTest.java index feca8ed97b..2454dae974 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TenantProfileControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TenantProfileControllerTest.java @@ -16,6 +16,7 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.core.type.TypeReference; +import org.awaitility.Awaitility; import org.junit.Assert; import org.junit.Test; import org.mockito.ArgumentMatcher; @@ -25,12 +26,12 @@ import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.audit.AuditLog; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.TimePageLink; -import org.thingsboard.server.common.data.audit.AuditLog; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.queue.ProcessingStrategy; import org.thingsboard.server.common.data.queue.ProcessingStrategyType; @@ -42,7 +43,6 @@ import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfi import org.thingsboard.server.common.data.validation.RateLimit; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.queue.TbQueueCallback; -import org.awaitility.Awaitility; import java.lang.reflect.Field; import java.util.ArrayList; @@ -191,7 +191,8 @@ public class TenantProfileControllerTest extends AbstractControllerTest { Assert.assertEquals(savedTenantProfile.getName(), foundDefaultTenantProfile.getName()); Assert.assertEquals(savedTenantProfile.getId(), foundDefaultTenantProfile.getId()); - awaitAuditLog("Wait for async audit log to be persisted (UPDATED expected for NEW DEFAULT Tenant Profile)", savedTenantProfile.getId(), ActionType.UPDATED); } + awaitAuditLog("Wait for async audit log to be persisted (UPDATED expected for NEW DEFAULT Tenant Profile)", savedTenantProfile.getId(), ActionType.UPDATED); + } @Test public void testSaveTenantProfileWithEmptyName() throws Exception { diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileService.java index ccac3cc5c1..f8f308c8f4 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileService.java @@ -46,7 +46,7 @@ public interface TenantProfileService extends EntityDaoService { EntityInfo findDefaultTenantProfileInfo(TenantId tenantId); - boolean setDefaultTenantProfile(TenantId tenantId, TenantProfileId tenantProfileId); + TenantProfile setDefaultTenantProfile(TenantId tenantId, TenantProfileId tenantProfileId); void deleteTenantProfiles(TenantId tenantId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java index 90fe102fce..a70331da0c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java @@ -119,7 +119,7 @@ public class AuditLogServiceImpl implements AuditLogService { public ListenableFuture logEntityAction(TenantId tenantId, CustomerId customerId, UserId userId, String userName, I entityId, E entity, ActionType actionType, Exception e, Object... additionalInfo) { - if (canLog(entityId.getEntityType(), actionType) || tenantId.isSysTenantId()) { + if (canLog(entityId.getEntityType(), actionType) || (tenantId != null && tenantId.isSysTenantId())) { JsonNode actionData = constructActionData(entityId, entity, actionType, additionalInfo); ActionStatus actionStatus = ActionStatus.SUCCESS; String failureDetails = ""; diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java index 7be47f4a0e..af62feb226 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java @@ -190,7 +190,7 @@ public class TenantProfileServiceImpl extends AbstractCachedEntityService INCORRECT_TENANT_ID + id); validateId(tenantProfileId, id -> INCORRECT_TENANT_PROFILE_ID + id); @@ -198,22 +198,18 @@ public class TenantProfileServiceImpl extends AbstractCachedEntityService Date: Thu, 14 Aug 2025 19:39:48 +0200 Subject: [PATCH 006/202] Audit log UI for sysadmin --- ui-ngx/src/app/core/services/menu.models.ts | 3 ++- .../modules/home/pages/audit-log/audit-log-routing.module.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/core/services/menu.models.ts b/ui-ngx/src/app/core/services/menu.models.ts index c2d4fc9e21..ab2160ec0f 100644 --- a/ui-ngx/src/app/core/services/menu.models.ts +++ b/ui-ngx/src/app/core/services/menu.models.ts @@ -816,7 +816,8 @@ const defaultUserMenuMap = new Map([ {id: MenuId.domains}, {id: MenuId.clients} ] - } + }, + {id: MenuId.audit_log} ] } ] diff --git a/ui-ngx/src/app/modules/home/pages/audit-log/audit-log-routing.module.ts b/ui-ngx/src/app/modules/home/pages/audit-log/audit-log-routing.module.ts index 2a4cd70c90..b9943eb41f 100644 --- a/ui-ngx/src/app/modules/home/pages/audit-log/audit-log-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/audit-log/audit-log-routing.module.ts @@ -25,7 +25,7 @@ export const auditLogsRoutes: Routes = [ path: 'auditLogs', component: AuditLogTableComponent, data: { - auth: [Authority.TENANT_ADMIN], + auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN], title: 'audit-log.audit-logs', breadcrumb: { menuId: MenuId.audit_log From a8de7ec7d5afd4086a0503a5cabd3034d4d8499d Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Thu, 14 Aug 2025 20:07:12 +0200 Subject: [PATCH 007/202] Audit log UI tab added for sysadmin for TenantProfile card --- .../pages/tenant-profile/tenant-profile-tabs.component.html | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile-tabs.component.html b/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile-tabs.component.html index dcad41d86a..b24e5e247a 100644 --- a/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile-tabs.component.html +++ b/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile-tabs.component.html @@ -31,4 +31,8 @@ [entityName]="entity.name"> + + + } From 84752472b73f7063906becc9b3846f69a8f1e37a Mon Sep 17 00:00:00 2001 From: Oleksandra_Matviienko Date: Thu, 29 Jan 2026 11:04:07 +0100 Subject: [PATCH 008/202] Added @NotNull for null-safety for audit/entity services. Added getOrEmptyId method to validate if entityId is zero. Changed logs to debug in DefaultTbTenantProfileService. Added Throwable cause to Exceptions in catch blocks in DefaultTbTenantProfileService. Signed-off-by: Oleksandra_Matviienko --- .../service/action/EntityActionService.java | 3 +- .../entitiy/AbstractTbEntityService.java | 4 ++ .../DefaultTbLogEntityActionService.java | 3 +- .../entitiy/TbLogEntityActionService.java | 3 +- .../DefaultTbTenantProfileService.java | 38 ++++++++++--------- .../profile/TbTenantProfileService.java | 5 ++- .../src/test/resources/logback-test.xml | 1 - .../server/dao/audit/AuditLogService.java | 3 +- .../server/dao/audit/AuditLogServiceImpl.java | 3 +- 9 files changed, 37 insertions(+), 26 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java b/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java index 774396a704..3b72ac662f 100644 --- a/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java +++ b/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java @@ -17,6 +17,7 @@ package org.thingsboard.server.service.action; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import jakarta.validation.constraints.NotNull; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -235,7 +236,7 @@ public class EntityActionService { } } - public void logEntityAction(User user, I entityId, E entity, CustomerId customerId, + public void logEntityAction(User user, @NotNull I entityId, E entity, CustomerId customerId, ActionType actionType, Exception e, Object... additionalInfo) { if (customerId == null || customerId.isNullUid()) { customerId = user.getCustomerId(); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java index 279ed918c8..efa000e521 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java @@ -97,6 +97,10 @@ public abstract class AbstractTbEntityService { return (I) EntityIdFactory.getByTypeAndUuid(entityType, ModelConstants.NULL_UUID); } + protected I getOrEmptyId(I entityId, EntityType entityType) { + return entityId == null ? emptyId(entityType) : entityId; + } + protected ListenableFuture autoCommit(User user, EntityId entityId) { if (vcService != null) { return vcService.autoCommit(user, entityId); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbLogEntityActionService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbLogEntityActionService.java index 05ec142e9d..685e447887 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbLogEntityActionService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbLogEntityActionService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.entitiy; +import jakarta.validation.constraints.NotNull; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -60,7 +61,7 @@ public class DefaultTbLogEntityActionService implements TbLogEntityActionService } @Override - public void logEntityAction(TenantId tenantId, I entityId, E entity, + public void logEntityAction(TenantId tenantId, @NotNull I entityId, E entity, CustomerId customerId, ActionType actionType, User user, Exception e, Object... additionalInfo) { if (user != null) { diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/TbLogEntityActionService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/TbLogEntityActionService.java index 712bca5b49..f574d4df23 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/TbLogEntityActionService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/TbLogEntityActionService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.entitiy; +import jakarta.validation.constraints.NotNull; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; @@ -37,7 +38,7 @@ public interface TbLogEntityActionService { void logEntityAction(TenantId tenantId, I entityId, E entity, CustomerId customerId, ActionType actionType, User user, Object... additionalInfo); - void logEntityAction(TenantId tenantId, I entityId, E entity, CustomerId customerId, + void logEntityAction(TenantId tenantId, @NotNull I entityId, E entity, CustomerId customerId, ActionType actionType, User user, Exception e, Object... additionalInfo); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/DefaultTbTenantProfileService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/DefaultTbTenantProfileService.java index cecc365c7c..a178832075 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/DefaultTbTenantProfileService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/DefaultTbTenantProfileService.java @@ -15,10 +15,10 @@ */ package org.thingsboard.server.service.entitiy.tenant.profile; +import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; -import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; @@ -35,6 +35,8 @@ import org.thingsboard.server.service.entitiy.queue.TbQueueService; import java.util.List; +import static org.thingsboard.server.common.data.EntityType.TENANT_PROFILE; + @Slf4j @Service @TbCoreComponent @@ -60,48 +62,48 @@ public class DefaultTbTenantProfileService extends AbstractTbEntityService imple return savedTenantProfile; } catch (ThingsboardException e) { - log.error("Failed to save tenant profile because ThingsboardException [{}]", tenantProfile, e); - logEntityActionService.logEntityAction(tenantId, emptyId(EntityType.TENANT_PROFILE), tenantProfile, actionType, user, e); + log.debug("Failed to save tenant profile because ThingsboardException [{}]", tenantProfile, e); + logEntityActionService.logEntityAction(tenantId, getOrEmptyId(tenantProfile.getId(), TENANT_PROFILE), tenantProfile, actionType, user, e); throw e; } catch (DataValidationException e) { - log.error("Failed to save tenant profile because data validation [{}]", tenantProfile, e); - logEntityActionService.logEntityAction(tenantId, emptyId(EntityType.TENANT_PROFILE), tenantProfile, actionType, user, e); - throw new ThingsboardException(e.getMessage(), ThingsboardErrorCode.BAD_REQUEST_PARAMS); + log.debug("Failed to save tenant profile because data validation [{}]", tenantProfile, e); + logEntityActionService.logEntityAction(tenantId, getOrEmptyId(tenantProfile.getId(), TENANT_PROFILE), tenantProfile, actionType, user, e); + throw new ThingsboardException(e.getMessage(), e, ThingsboardErrorCode.BAD_REQUEST_PARAMS); } catch (Exception e) { - log.error("Failed to save tenant profile because Exception [{}]", tenantProfile, e); - logEntityActionService.logEntityAction(tenantId, emptyId(EntityType.TENANT_PROFILE), tenantProfile, actionType, user, e); - throw new ThingsboardException(e, ThingsboardErrorCode.GENERAL); + log.debug("Failed to save tenant profile because Exception [{}]", tenantProfile, e); + logEntityActionService.logEntityAction(tenantId, getOrEmptyId(tenantProfile.getId(), TENANT_PROFILE), tenantProfile, actionType, user, e); + throw new ThingsboardException(e.getMessage(), e, ThingsboardErrorCode.GENERAL); } } @Override - public void delete(TenantId tenantId, TenantProfile tenantProfile, User user) throws ThingsboardException { + public void delete(TenantId tenantId, @NotNull TenantProfile tenantProfile, User user) throws ThingsboardException { ActionType actionType = ActionType.DELETED; try { tenantProfileService.deleteTenantProfile(tenantId, tenantProfile.getId()); logEntityActionService.logEntityAction(tenantId, tenantProfile.getId(), tenantProfile, null, actionType, user); } catch (Exception e) { - logEntityActionService.logEntityAction(tenantId, tenantProfile.getId(), tenantProfile, null, actionType, user); + logEntityActionService.logEntityAction(tenantId, tenantProfile.getId(), tenantProfile, null, actionType, user, e); throw e; } } @Override - public TenantProfile setDefaultTenantProfile(TenantId tenantId, TenantProfile tenantProfile, User user) throws ThingsboardException { + public TenantProfile setDefaultTenantProfile(TenantId tenantId, @NotNull TenantProfile tenantProfile, User user) throws ThingsboardException { ActionType actionType = ActionType.UPDATED; try { TenantProfile savedTenantProfile = tenantProfileService.setDefaultTenantProfile(tenantId, tenantProfile.getId()); logEntityActionService.logEntityAction(tenantId, tenantProfile.getId(), savedTenantProfile, null, actionType, user); return savedTenantProfile; } catch (DataValidationException e) { - log.error("Failed to set default tenant profile due to data validation [{}]", tenantProfile, e); - logEntityActionService.logEntityAction(tenantId, emptyId(EntityType.TENANT_PROFILE), tenantProfile, actionType, user, e); - throw new ThingsboardException(e.getMessage(), ThingsboardErrorCode.BAD_REQUEST_PARAMS); + log.debug("Failed to set default tenant profile due to data validation [{}]", tenantProfile, e); + logEntityActionService.logEntityAction(tenantId, tenantProfile.getId(), tenantProfile, actionType, user, e); + throw new ThingsboardException(e.getMessage(), e, ThingsboardErrorCode.BAD_REQUEST_PARAMS); } catch (Exception e) { - log.error("Failed to set default tenant profile [{}]", tenantProfile, e); - logEntityActionService.logEntityAction(tenantId, emptyId(EntityType.TENANT_PROFILE), tenantProfile, actionType, user, e); - throw new ThingsboardException(e, ThingsboardErrorCode.GENERAL); + log.debug("Failed to set default tenant profile [{}]", tenantProfile, e); + logEntityActionService.logEntityAction(tenantId, tenantProfile.getId(), tenantProfile, actionType, user, e); + throw new ThingsboardException(e.getMessage(), e, ThingsboardErrorCode.GENERAL); } } } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/TbTenantProfileService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/TbTenantProfileService.java index 66ddbbca2a..d15e9c84c8 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/TbTenantProfileService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/TbTenantProfileService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.entitiy.tenant.profile; +import jakarta.validation.constraints.NotNull; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; @@ -24,7 +25,7 @@ public interface TbTenantProfileService { TenantProfile save(TenantId tenantId, TenantProfile tenantProfile, TenantProfile oldTenantProfile, User user) throws ThingsboardException; - void delete(TenantId tenantId, TenantProfile tenantProfile, User user) throws ThingsboardException; + void delete(TenantId tenantId, @NotNull TenantProfile tenantProfile, User user) throws ThingsboardException; - TenantProfile setDefaultTenantProfile(TenantId tenantId, TenantProfile tenantProfile, User user) throws ThingsboardException; + TenantProfile setDefaultTenantProfile(TenantId tenantId, @NotNull TenantProfile tenantProfile, User user) throws ThingsboardException; } diff --git a/application/src/test/resources/logback-test.xml b/application/src/test/resources/logback-test.xml index b5a1ac86ae..6faaf97536 100644 --- a/application/src/test/resources/logback-test.xml +++ b/application/src/test/resources/logback-test.xml @@ -16,7 +16,6 @@ - diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java index df5833576a..07ea2243dc 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.audit; import com.google.common.util.concurrent.ListenableFuture; +import jakarta.validation.constraints.NotNull; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.AuditLog; @@ -43,7 +44,7 @@ public interface AuditLogService { CustomerId customerId, UserId userId, String userName, - I entityId, + @NotNull I entityId, E entity, ActionType actionType, Exception e, Object... additionalInfo); diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java index a70331da0c..8aef814657 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import jakarta.validation.constraints.NotNull; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -117,7 +118,7 @@ public class AuditLogServiceImpl implements AuditLogService { @Override public ListenableFuture - logEntityAction(TenantId tenantId, CustomerId customerId, UserId userId, String userName, I entityId, E entity, + logEntityAction(TenantId tenantId, CustomerId customerId, UserId userId, String userName, @NotNull I entityId, E entity, ActionType actionType, Exception e, Object... additionalInfo) { if (canLog(entityId.getEntityType(), actionType) || (tenantId != null && tenantId.isSysTenantId())) { JsonNode actionData = constructActionData(entityId, entity, actionType, additionalInfo); From bb3237bf3f7e91f7fded72e74b7aeff0da1212e8 Mon Sep 17 00:00:00 2001 From: Oleksandra_Matviienko Date: Thu, 29 Jan 2026 15:29:38 +0100 Subject: [PATCH 009/202] fixed import DataValidationException Signed-off-by: Oleksandra_Matviienko --- .../entitiy/tenant/profile/DefaultTbTenantProfileService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/DefaultTbTenantProfileService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/DefaultTbTenantProfileService.java index a178832075..25dadbe316 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/DefaultTbTenantProfileService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/DefaultTbTenantProfileService.java @@ -25,10 +25,10 @@ import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.User; -import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.dao.tenant.TenantService; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; import org.thingsboard.server.service.entitiy.queue.TbQueueService; From e5a31c605e091eecaa58d17299bf2c34bae1e7b5 Mon Sep 17 00:00:00 2001 From: Oleksandra Matviienko Date: Tue, 3 Feb 2026 15:45:49 +0100 Subject: [PATCH 010/202] test: audit log under sysadmin added. refactor: extracted getAuditLogs from multiple repeated code lines Signed-off-by: Oleksandra Matviienko --- .../controller/AuditLogControllerTest.java | 73 +++++++++---------- 1 file changed, 33 insertions(+), 40 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest.java index a76da5f161..7fec5c8b72 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest.java @@ -28,6 +28,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.mock.mockito.SpyBean; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.AuditLog; @@ -111,48 +112,51 @@ public class AuditLogControllerTest extends AbstractControllerTest { doPost("/api/device", device, Device.class); } - List loadedAuditLogs = new ArrayList<>(); - TimePageLink pageLink = new TimePageLink(5); + List loadedAuditLogs = getAuditLogs(5, "/api/audit/logs?"); + TimePageLink pageLink; PageData pageData; - do { - pageData = doGetTypedWithTimePageLink("/api/audit/logs?", - new TypeReference>() { - }, pageLink); - loadedAuditLogs.addAll(pageData.getData()); - if (pageData.hasNext()) { - pageLink = pageLink.nextPageLink(); - } - } while (pageData.hasNext()); Assert.assertEquals(11 + 1, loadedAuditLogs.size()); - loadedAuditLogs = new ArrayList<>(); - pageLink = new TimePageLink(5); - do { - pageData = doGetTypedWithTimePageLink("/api/audit/logs/customer/" + ModelConstants.NULL_UUID + "?", - new TypeReference>() { - }, pageLink); - loadedAuditLogs.addAll(pageData.getData()); - if (pageData.hasNext()) { - pageLink = pageLink.nextPageLink(); - } - } while (pageData.hasNext()); + loadedAuditLogs = getAuditLogs(5, "/api/audit/logs/customer/" + ModelConstants.NULL_UUID + "?"); Assert.assertEquals(11 + 1, loadedAuditLogs.size()); - loadedAuditLogs = new ArrayList<>(); - pageLink = new TimePageLink(5); + loadedAuditLogs = getAuditLogs(5, "/api/audit/logs/user/" + tenantAdmin.getId().getId().toString() + "?"); + + Assert.assertEquals(11 + 1, loadedAuditLogs.size()); + } + + @Test + public void testAuditLogsSysAdmin() throws Exception { + loginSysAdmin(); + List loadedAuditLogsBefore = getAuditLogs(100, "/api/audit/logs?"); + + for (int i = 0; i < 3; i++) { + TenantProfile tenantProfile = new TenantProfile(); + tenantProfile.setName("Profile " + UUID.randomUUID()); + doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); + } + + List loadedAuditLogs = getAuditLogs(100, "/api/audit/logs?"); + + Assert.assertEquals("Have X audit log before this test + New tenant profiles in the test", loadedAuditLogsBefore.size() + 3, loadedAuditLogs.size()); + } + + private List getAuditLogs(int pageSize, String urlTemplate) throws Exception { + List loadedAuditLogs = new ArrayList<>(); + TimePageLink pageLink = new TimePageLink(pageSize); + PageData pageData; do { - pageData = doGetTypedWithTimePageLink("/api/audit/logs/user/" + tenantAdmin.getId().getId().toString() + "?", - new TypeReference>() { + pageData = doGetTypedWithTimePageLink(urlTemplate, + new TypeReference<>() { }, pageLink); loadedAuditLogs.addAll(pageData.getData()); if (pageData.hasNext()) { pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); - - Assert.assertEquals(11 + 1, loadedAuditLogs.size()); + return loadedAuditLogs; } @Test @@ -166,18 +170,7 @@ public class AuditLogControllerTest extends AbstractControllerTest { savedDevice = doPost("/api/device", savedDevice, Device.class); } - List loadedAuditLogs = new ArrayList<>(); - TimePageLink pageLink = new TimePageLink(5); - PageData pageData; - do { - pageData = doGetTypedWithTimePageLink("/api/audit/logs/entity/DEVICE/" + savedDevice.getId().getId() + "?", - new TypeReference>() { - }, pageLink); - loadedAuditLogs.addAll(pageData.getData()); - if (pageData.hasNext()) { - pageLink = pageLink.nextPageLink(); - } - } while (pageData.hasNext()); + List loadedAuditLogs = getAuditLogs(5, "/api/audit/logs/entity/DEVICE/" + savedDevice.getId().getId() + "?"); Assert.assertEquals(11 + 1, loadedAuditLogs.size()); } From 4ef75c709f36df2d1792596317a761a914521042 Mon Sep 17 00:00:00 2001 From: Oleksandra Matviienko Date: Tue, 3 Feb 2026 18:24:08 +0100 Subject: [PATCH 011/202] Removed unused variables Signed-off-by: Oleksandra Matviienko --- .../thingsboard/server/controller/AuditLogControllerTest.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest.java index 7fec5c8b72..ca2371fab2 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest.java @@ -113,8 +113,6 @@ public class AuditLogControllerTest extends AbstractControllerTest { } List loadedAuditLogs = getAuditLogs(5, "/api/audit/logs?"); - TimePageLink pageLink; - PageData pageData; Assert.assertEquals(11 + 1, loadedAuditLogs.size()); From 8b494c008d04781e49e3d7a6add2b1ba0dfe088f Mon Sep 17 00:00:00 2001 From: Oleksandra Matviienko Date: Tue, 3 Feb 2026 22:02:52 +0100 Subject: [PATCH 012/202] test: audit log by TenantId and EntityId under sysadmin added. Signed-off-by: Oleksandra Matviienko --- .../controller/AuditLogControllerTest.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest.java index ca2371fab2..af5df3d733 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest.java @@ -173,6 +173,27 @@ public class AuditLogControllerTest extends AbstractControllerTest { Assert.assertEquals(11 + 1, loadedAuditLogs.size()); } + @Test + public void testAuditLogs_byTenantIdAndEntityId_Sysadmin() throws Exception { + loginSysAdmin(); + + //created + TenantProfile tenantProfile = new TenantProfile(); + tenantProfile.setName("Profile " + UUID.randomUUID()); + tenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); + + //updated + tenantProfile.setName(tenantProfile.getName() + "(old)"); + tenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); + + List loadedAuditLogs = getAuditLogs(5, "/api/audit/logs/entity/" +tenantProfile.getId().getEntityType()+ "/" + tenantProfile.getId().getId() + "?"); + + Assert.assertEquals("Audit logs count by Tenant Profile entity", 2, loadedAuditLogs.size()); + + //cleanup + doDelete("/api/tenantProfile/" + tenantProfile.getId().getId().toString()); + } + @Test public void whenSavingNewAuditLog_thenCheckAndCreatePartitionIfNotExists() throws ParseException { long entityTs = ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.parse("2024-01-01T01:43:11Z").getTime(); From b07858b1137d993de1758e8049387073406bc352 Mon Sep 17 00:00:00 2001 From: Vladyslav Prykhodko Date: Mon, 9 Feb 2026 15:33:51 +0200 Subject: [PATCH 013/202] UI: Improved default tenant home dashboard (#15000) --- .../recent-dashboards-widget.component.html | 11 ++++- .../recent-dashboards-widget.component.ts | 45 +++++++++++++++++++ .../dashboard/tenant_admin_home_page.json | 2 +- 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.html index f832b1982e..e8fba081ee 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.html @@ -24,8 +24,15 @@ {{ 'widgets.recent-dashboards.last' | translate }} {{ 'widgets.recent-dashboards.starred' | translate }} - {{ 'dashboard.add' | translate }} + + @if (hasDevice) { + {{ 'dashboard.add' | translate }} + } @else { + {{ 'dashboard.add' | translate }} + } + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.ts index 899ffa5aea..5dfc520856 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.ts @@ -48,6 +48,13 @@ import { Direction, SortOrder } from '@shared/models/page/sort-order'; import { MatSort } from '@angular/material/sort'; import { DashboardInfo } from '@shared/models/dashboard.models'; import { DashboardAutocompleteComponent } from '@shared/components/dashboard-autocomplete.component'; +import { UtilsService } from '@core/services/utils.service'; +import { Datasource, DatasourceType, widgetType } from '@shared/models/widget.models'; +import { IWidgetSubscription, WidgetSubscriptionOptions } from '@core/api/widget-api.models'; +import { formattedDataFormDatasourceData } from '@core/utils'; +import { AliasFilterType } from '@shared/models/alias.models'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { EntityType } from '@shared/models/entity-type.models'; @Component({ selector: 'tb-recent-dashboards-widget', @@ -78,13 +85,16 @@ export class RecentDashboardsWidgetComponent extends PageComponent implements On starredDashboardValue = null; hasDashboardsAccess = true; + hasDevice = true; dirty = false; public customerId: string; private isFullscreenMode = getCurrentAuthState(this.store).forceFullscreen; + private subscription: IWidgetSubscription; constructor(protected store: Store, private cd: ChangeDetectorRef, + private utils: UtilsService, private userSettingService: UserSettingsService) { super(store); } @@ -96,6 +106,41 @@ export class RecentDashboardsWidgetComponent extends PageComponent implements On this.hasDashboardsAccess = [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER].includes(this.authUser.authority); if (this.hasDashboardsAccess) { this.reload(); + + if (window.location.pathname.startsWith('/home') && this.authUser.authority === Authority.TENANT_ADMIN) { + const ds: Datasource = { + type: DatasourceType.entityCount, + name: '', + entityFilter: { + entityType: EntityType.DEVICE, + type: AliasFilterType.entityType + }, + dataKeys: [this.utils.createKey({ name: 'count'}, DataKeyType.count)] + } + + const apiUsageSubscriptionOptions: WidgetSubscriptionOptions = { + datasources: [ds], + useDashboardTimewindow: false, + type: widgetType.latest, + callbacks: { + onDataUpdated: (subscription) => { + const data = formattedDataFormDatasourceData(subscription.data); + this.hasDevice = (data[0].count || 0) !== 0; + this.cd.detectChanges(); + } + } + }; + this.ctx.subscriptionApi.createSubscription(apiUsageSubscriptionOptions, true).subscribe((subscription) => { + this.subscription = subscription; + }); + } + } + } + + ngOnDestroy() { + super.ngOnDestroy(); + if (this.subscription) { + this.ctx.subscriptionApi.removeSubscription(this.subscription.id); } } diff --git a/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json b/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json index f09ab73de7..9fb2d25cf8 100644 --- a/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json +++ b/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json @@ -224,7 +224,7 @@ "padding": "16px", "settings": { "useMarkdownTextFunction": false, - "markdownTextPattern": "", + "markdownTextPattern": "", "applyDefaultMarkdownStyle": false, "markdownCss": ".tb-card-content {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: row;\n}\n\n.tb-content-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n justify-content: flex-start;\n gap: 12px;\n}\n\n.tb-card-header {\n height: 36px;\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n}\n\n.tb-item-cards {\n flex: 1;\n display: flex;\n flex-direction: row;\n gap: 12px;\n overflow: hidden;\n}\n\na.tb-item-card {\n flex: 1;\n display: flex;\n flex-direction: column;\n padding: 8px 12px;\n border: 1px solid;\n border-radius: 10px;\n margin-bottom: 12px;\n overflow: hidden;\n justify-content: space-evenly;\n}\n\na.tb-item-card.tb-inactive {\n background: rgba(209, 39, 48, 0.04);\n border-color: rgba(209, 39, 48, 0.06);\n}\n\na.tb-item-card.tb-active {\n background: rgba(48, 86, 128, 0.04);\n border-color: rgba(48, 86, 128, 0.12);\n}\n\na.tb-item-card.tb-total {\n background: rgba(0, 0, 0, 0.01);\n border-color: rgba(0, 0, 0, 0.05);\n}\n\n.tb-item-title-container {\n display: grid;\n}\n\n.tb-item-title {\n font-weight: 400;\n font-size: 14px;\n line-height: 20px;\n letter-spacing: 0.2px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis; \n color: rgba(0, 0, 0, 0.76);\n}\n\n.tb-item-title.tb-home-widget-link:after {\n position: absolute;\n right: 0;\n}\n\na.tb-item-card:hover .tb-item-title.tb-home-widget-link:after { \n color: rgba(0, 0, 0, 0.38);\n}\n\na.tb-item-card:hover {\n box-shadow: 0px 4px 10px rgba(23, 33, 90, 0.08);\n}\n\n.tb-count-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n justify-content: center;\n}\n\n.tb-count {\n font-style: normal;\n font-weight: 500;\n font-size: 24px;\n line-height: 36px;\n white-space: nowrap;\n color: rgba(0, 0, 0, 0.87);\n}\n\n@media screen and (max-width: 959px) {\n .tb-item-cards {\n flex-direction: column;\n }\n a.tb-item-card {\n margin-bottom: 0;\n }\n}\n\n@media screen and (max-width: 1279px) {\n a.tb-item-card {\n flex-direction: row;\n align-items: center;\n }\n .tb-item-title.tb-home-widget-link:after {\n position: relative;\n }\n .tb-count-container {\n align-items: flex-end;\n }\n}\n\n@media screen and (min-width: 960px) and (max-width: 1819px) {\n .tb-item-title {\n font-size: 11px;\n line-height: 16px;\n }\n .tb-count {\n font-size: 16px;\n line-height: 24px;\n }\n a.tb-item-card {\n padding: 4px 8px;\n margin-bottom: 6px;\n }\n a.tb-item-card:hover {\n box-shadow: 0px 2px 5px rgba(23, 33, 90, 0.08);\n }\n}\n" }, From e0151095f743dbd2dcc499b5654a6f51c2b7a82c Mon Sep 17 00:00:00 2001 From: Vladyslav Prykhodko Date: Mon, 9 Feb 2026 15:40:32 +0200 Subject: [PATCH 014/202] Changed default "Add" button style in entity tables (#14984) * UI: Use accent text button as default "Add" style in entity tables * UI: Change default text button style in entity table * UI: Change default text button style in image gallery --------- Co-authored-by: Viacheslav Klimov --- .../alarm-rules/alarm-rules-table-config.ts | 4 +- .../api-key/api-keys-table-config.ts | 1 - .../calculated-fields-table-config.ts | 2 + .../entity/entities-table.component.html | 104 ++++++++++++------ .../vc/entity-versions-table.component.html | 18 +-- .../entity/entities-table-config.models.ts | 2 +- .../ai-model/ai-model-table-config.resolve.ts | 1 - .../mobile-app-table-config.resolver.ts | 1 - .../mobile-bundle-table-config.resolve.ts | 1 - .../recipient-table-config.resolver.ts | 1 - .../rule/rule-table-config.resolver.ts | 1 - .../template-table-config.resolver.ts | 1 - .../image/image-gallery.component.html | 6 +- .../app/shared/models/entity-type.models.ts | 1 + .../assets/locale/locale.constant-en_US.json | 1 + 15 files changed, 94 insertions(+), 51 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts index cadac572c6..039ee7d5fd 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts @@ -106,6 +106,8 @@ export class AlarmRulesTableConfig extends EntityTableConfig { this.entityType = EntityType.API_KEY; this.detailsPanelEnabled = false; - this.addAsTextButton = true; this.pageMode = false; this.entityTranslations = entityTypeTranslations.get(EntityType.API_KEY); diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts index efb767f92e..aac49a3f37 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts @@ -110,6 +110,8 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig -
- - - - - - - - + - - -
@@ -118,6 +94,72 @@ matTooltipPosition="above"> search +
+ + + + + + + + + + + + + + + + +
+ + + diff --git a/ui-ngx/src/app/modules/home/components/vc/entity-versions-table.component.html b/ui-ngx/src/app/modules/home/components/vc/entity-versions-table.component.html index cc3fb23da5..829c1414db 100644 --- a/ui-ngx/src/app/modules/home/components/vc/entity-versions-table.component.html +++ b/ui-ngx/src/app/modules/home/components/vc/entity-versions-table.component.html @@ -32,7 +32,8 @@ -
+
-
+
diff --git a/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts b/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts index 913e754fb7..336ac035da 100644 --- a/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts +++ b/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts @@ -175,7 +175,7 @@ export class EntityTableConfig, P extends PageLink = P selectionEnabled = true; searchEnabled = true; addEnabled = true; - addAsTextButton = false; + addAsTextButton = true; entitiesDeleteEnabled = true; detailsPanelEnabled = true; hideDetailsTabsOnEdit = true; diff --git a/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-config.resolve.ts b/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-config.resolve.ts index 2a73488d7e..7c78bd7c02 100644 --- a/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-config.resolve.ts +++ b/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-config.resolve.ts @@ -47,7 +47,6 @@ export class AiModelsTableConfigResolver { ) { this.config.selectionEnabled = true; this.config.entityType = EntityType.AI_MODEL; - this.config.addAsTextButton = true; this.config.rowPointer = true; this.config.detailsPanelEnabled = false; this.config.entityTranslations = entityTypeTranslations.get(EntityType.AI_MODEL); diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-config.resolver.ts index 13698ac4a5..274edf5933 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-config.resolver.ts @@ -57,7 +57,6 @@ export class MobileAppTableConfigResolver { ) { this.config.selectionEnabled = false; this.config.entityType = EntityType.MOBILE_APP; - this.config.addAsTextButton = true; this.config.entitiesDeleteEnabled = false; this.config.rowPointer = true; this.config.entityTranslations = entityTypeTranslations.get(EntityType.MOBILE_APP); diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-table-config.resolve.ts b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-table-config.resolve.ts index 69be801980..ba20624852 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-table-config.resolve.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-table-config.resolve.ts @@ -63,7 +63,6 @@ export class MobileBundleTableConfigResolver { ) { this.config.selectionEnabled = false; this.config.entityType = EntityType.MOBILE_APP_BUNDLE; - this.config.addAsTextButton = true; this.config.rowPointer = true; this.config.detailsPanelEnabled = false; this.config.entityTranslations = entityTypeTranslations.get(EntityType.MOBILE_APP_BUNDLE); diff --git a/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-table-config.resolver.ts index c2fc08fb7c..8e729e94ef 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-table-config.resolver.ts @@ -48,7 +48,6 @@ export class RecipientTableConfigResolver { this.config.entityType = EntityType.NOTIFICATION_TARGET; this.config.detailsPanelEnabled = false; - this.config.addAsTextButton = true; this.config.rowPointer = true; this.config.entityTranslations = entityTypeTranslations.get(EntityType.NOTIFICATION_TARGET); diff --git a/ui-ngx/src/app/modules/home/pages/notification/rule/rule-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/notification/rule/rule-table-config.resolver.ts index 066db92c0a..f71197ae95 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/rule/rule-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/notification/rule/rule-table-config.resolver.ts @@ -49,7 +49,6 @@ export class RuleTableConfigResolver { this.config.entityType = EntityType.NOTIFICATION_RULE; this.config.detailsPanelEnabled = false; - this.config.addAsTextButton = true; this.config.rowPointer = true; this.config.entityTranslations = entityTypeTranslations.get(EntityType.NOTIFICATION_RULE); diff --git a/ui-ngx/src/app/modules/home/pages/notification/template/template-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/notification/template/template-table-config.resolver.ts index 0ee0b921f4..b1bc3eef3e 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/template/template-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/notification/template/template-table-config.resolver.ts @@ -47,7 +47,6 @@ export class TemplateTableConfigResolver { this.config.entityType = EntityType.NOTIFICATION_TEMPLATE; this.config.detailsPanelEnabled = false; - this.config.addAsTextButton = true; this.config.rowPointer = true; this.config.entityTranslations = entityTypeTranslations.get(EntityType.NOTIFICATION_TEMPLATE); diff --git a/ui-ngx/src/app/shared/components/image/image-gallery.component.html b/ui-ngx/src/app/shared/components/image/image-gallery.component.html index f442556b18..b31dc16920 100644 --- a/ui-ngx/src/app/shared/components/image/image-gallery.component.html +++ b/ui-ngx/src/app/shared/components/image/image-gallery.component.html @@ -74,10 +74,10 @@ mdi:file-import