diff --git a/application/src/main/data/upgrade/3.4.4/schema_update.sql b/application/src/main/data/upgrade/3.4.4/schema_update.sql index 6860adc612..5965837898 100644 --- a/application/src/main/data/upgrade/3.4.4/schema_update.sql +++ b/application/src/main/data/upgrade/3.4.4/schema_update.sql @@ -167,11 +167,12 @@ CREATE INDEX IF NOT EXISTS idx_notification_recipient_id_created_time ON notific ALTER TABLE tb_user ADD COLUMN IF NOT EXISTS phone VARCHAR(255); CREATE TABLE IF NOT EXISTS user_settings ( - user_id uuid NOT NULL CONSTRAINT user_settings_pkey PRIMARY KEY, - settings varchar(100000), - CONSTRAINT fk_user_id FOREIGN KEY (user_id) REFERENCES tb_user(id) ON DELETE CASCADE + user_id uuid NOT NULL, + type VARCHAR(50) NOT NULL, + settings varchar(10000), + CONSTRAINT fk_user_id FOREIGN KEY (user_id) REFERENCES tb_user(id) ON DELETE CASCADE, + CONSTRAINT user_settings_pkey PRIMARY KEY (user_id, type) ); - -- ALARM INFO VIEW DROP VIEW IF EXISTS alarm_info CASCADE; 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 a68955d9b4..34f065d500 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -103,6 +103,7 @@ import org.thingsboard.server.common.data.rpc.Rpc; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleNode; +import org.thingsboard.server.common.data.settings.UserDashboardAction; import org.thingsboard.server.common.data.util.ThrowingBiFunction; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetsBundle; @@ -146,6 +147,7 @@ import org.thingsboard.server.service.component.ComponentDiscoveryService; import org.thingsboard.server.service.edge.instructions.EdgeInstallService; import org.thingsboard.server.service.edge.rpc.EdgeRpcService; import org.thingsboard.server.service.entitiy.TbNotificationEntityService; +import org.thingsboard.server.service.entitiy.user.TbUserSettingsService; import org.thingsboard.server.service.ota.OtaPackageStateService; import org.thingsboard.server.service.profile.TbAssetProfileCache; import org.thingsboard.server.service.profile.TbDeviceProfileCache; @@ -168,6 +170,7 @@ import java.util.Set; import java.util.UUID; import java.util.function.BiConsumer; import java.util.function.BiFunction; +import java.util.function.Function; import java.util.stream.Collectors; import static org.thingsboard.server.common.data.StringUtils.isNotEmpty; @@ -202,7 +205,7 @@ public abstract class BaseController { protected UserService userService; @Autowired - protected UserSettingsService userSettingsService; + protected TbUserSettingsService userSettingsService; @Autowired protected DeviceService deviceService; @@ -445,6 +448,14 @@ public abstract class BaseController { } } + protected T checkEnumParameter(String name, String param, Function valueOf) throws ThingsboardException { + try { + return valueOf.apply(param.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new ThingsboardException(name + " \"" + param + "\" is not supported!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); + } + } + UUID toUUID(String id) throws ThingsboardException { try { return UUID.fromString(id); diff --git a/application/src/main/java/org/thingsboard/server/controller/UserController.java b/application/src/main/java/org/thingsboard/server/controller/UserController.java index 06963e466f..fadb920938 100644 --- a/application/src/main/java/org/thingsboard/server/controller/UserController.java +++ b/application/src/main/java/org/thingsboard/server/controller/UserController.java @@ -38,12 +38,14 @@ import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.MailService; +import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.UserEmailInfo; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; @@ -55,9 +57,13 @@ import org.thingsboard.server.common.data.query.EntityTypeFilter; import org.thingsboard.server.common.data.query.TsValue; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.UserCredentials; -import org.thingsboard.server.common.data.security.UserSettings; +import org.thingsboard.server.common.data.settings.LastVisitedDashboardInfo; +import org.thingsboard.server.common.data.settings.UserDashboardAction; +import org.thingsboard.server.common.data.settings.UserDashboardsInfo; +import org.thingsboard.server.common.data.settings.UserSettings; import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent; import org.thingsboard.server.common.data.security.model.JwtPair; +import org.thingsboard.server.common.data.settings.UserSettingsType; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.user.TbUserService; import org.thingsboard.server.service.query.EntityQueryService; @@ -76,6 +82,7 @@ import java.util.Map; import static org.thingsboard.server.common.data.query.EntityKeyType.ENTITY_FIELD; import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID; import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.DASHBOARD_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.DEFAULT_DASHBOARD; import static org.thingsboard.server.controller.ControllerConstants.HOME_DASHBOARD; import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; @@ -438,13 +445,14 @@ public class UserController extends BaseController { } @ApiOperation(value = "Save user settings (saveUserSettings)", - notes = "Save user settings represented in json format for authorized user. " ) + notes = "Save user settings represented in json format for authorized user. ") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PostMapping(value = "/user/settings") public JsonNode saveUserSettings(@RequestBody JsonNode settings) throws ThingsboardException { SecurityUser currentUser = getCurrentUser(); UserSettings userSettings = new UserSettings(); + userSettings.setType(UserSettingsType.GENERAL); userSettings.setSettings(settings); userSettings.setUserId(currentUser.getId()); return userSettingsService.saveUserSettings(currentUser.getTenantId(), userSettings).getSettings(); @@ -458,31 +466,108 @@ public class UserController extends BaseController { @PutMapping(value = "/user/settings") public void putUserSettings(@RequestBody JsonNode settings) throws ThingsboardException { SecurityUser currentUser = getCurrentUser(); - userSettingsService.updateUserSettings(currentUser.getTenantId(), currentUser.getId(), settings); + userSettingsService.updateUserSettings(currentUser.getTenantId(), currentUser.getId(), UserSettingsType.GENERAL, settings); } @ApiOperation(value = "Get user settings (getUserSettings)", - notes = "Fetch the User settings based on authorized user. " ) + notes = "Fetch the User settings based on authorized user. ") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/user/settings") public JsonNode getUserSettings() throws ThingsboardException { SecurityUser currentUser = getCurrentUser(); - UserSettings userSettings = userSettingsService.findUserSettings(currentUser.getTenantId(), currentUser.getId()); - return userSettings == null ? JacksonUtil.newObjectNode(): userSettings.getSettings(); + UserSettings userSettings = userSettingsService.findUserSettings(currentUser.getTenantId(), currentUser.getId(), UserSettingsType.GENERAL); + return userSettings == null ? JacksonUtil.newObjectNode() : userSettings.getSettings(); } @ApiOperation(value = "Delete user settings (deleteUserSettings)", notes = "Delete user settings by specifying list of json element xpaths. \n " + - "Example: to delete B and C element in { \"A\": {\"B\": 5}, \"C\": 15} send A.B,C in jsonPaths request parameter" ) + "Example: to delete B and C element in { \"A\": {\"B\": 5}, \"C\": 15} send A.B,C in jsonPaths request parameter") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/user/settings/{paths}", method = RequestMethod.DELETE) public void deleteUserSettings(@ApiParam(value = PATHS) - @PathVariable(PATHS) String paths) throws ThingsboardException { + @PathVariable(PATHS) String paths) throws ThingsboardException { checkParameter(USER_ID, paths); SecurityUser currentUser = getCurrentUser(); - userSettingsService.deleteUserSettings(currentUser.getTenantId(), currentUser.getId(), Arrays.asList(paths.split(","))); + userSettingsService.deleteUserSettings(currentUser.getTenantId(), currentUser.getId(), UserSettingsType.GENERAL, Arrays.asList(paths.split(","))); + } + + @ApiOperation(value = "Update user settings (saveUserSettings)", + notes = "Update user settings for authorized user. Only specified json elements will be updated." + + "Example: you have such settings: {A:5, B:{C:10, D:20}}. Updating it with {B:{C:10, D:30}} will result in" + + "{A:5, B:{C:10, D:30}}. The same could be achieved by putting {B.D:30}") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @PutMapping(value = "/user/settings/{type}") + public void putUserSettings(@ApiParam(value = "Settings type, case insensitive, one of: \"general\", \"quick_links\", \"doc_links\" or \"dashboards\".") + @PathVariable("type") String strType, @RequestBody JsonNode settings) throws ThingsboardException { + SecurityUser currentUser = getCurrentUser(); + UserSettingsType type = checkEnumParameter("Settings type", strType, UserSettingsType::valueOf); + checkNotReserved(strType, type); + userSettingsService.updateUserSettings(currentUser.getTenantId(), currentUser.getId(), type, settings); + } + + @ApiOperation(value = "Get user settings (getUserSettings)", + notes = "Fetch the User settings based on authorized user. ") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/user/settings/{type}") + public JsonNode getUserSettings(@ApiParam(value = "Settings type, case insensitive, one of: \"general\", \"quick_links\", \"doc_links\" or \"dashboards\".") + @PathVariable("type") String strType) throws ThingsboardException { + SecurityUser currentUser = getCurrentUser(); + UserSettingsType type = checkEnumParameter("Settings type", strType, UserSettingsType::valueOf); + checkNotReserved(strType, type); + UserSettings userSettings = userSettingsService.findUserSettings(currentUser.getTenantId(), currentUser.getId(), type); + return userSettings == null ? JacksonUtil.newObjectNode() : userSettings.getSettings(); + } + + @ApiOperation(value = "Delete user settings (deleteUserSettings)", + notes = "Delete user settings by specifying list of json element xpaths. \n " + + "Example: to delete B and C element in { \"A\": {\"B\": 5}, \"C\": 15} send A.B,C in jsonPaths request parameter") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/user/settings/{type}/{paths}", method = RequestMethod.DELETE) + public void deleteUserSettings(@ApiParam(value = PATHS) + @PathVariable(PATHS) String paths, + @ApiParam(value = "Settings type, case insensitive, one of: \"general\", \"quick_links\", \"doc_links\" or \"dashboards\".") + @PathVariable("type") String strType) throws ThingsboardException { + checkParameter(USER_ID, paths); + UserSettingsType type = checkEnumParameter("Settings type", strType, UserSettingsType::valueOf); + checkNotReserved(strType, type); + SecurityUser currentUser = getCurrentUser(); + userSettingsService.deleteUserSettings(currentUser.getTenantId(), currentUser.getId(), type, Arrays.asList(paths.split(","))); + } + + @ApiOperation(value = "Get information about last visited and starred dashboards (getLastVisitedDashboards)", + notes = "Fetch the list of last visited and starred dashboards. Both lists are limited to 10 items." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/user/dashboards") + public UserDashboardsInfo getUserDashboardsInfo() throws ThingsboardException { + SecurityUser currentUser = getCurrentUser(); + return userSettingsService.findUserDashboardsInfo(currentUser.getTenantId(), currentUser.getId()); + } + + @ApiOperation(value = "Report action of User over the dashboard (reportUserDashboardAction)", + notes = "Report action of User over the dashboard. " + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/user/dashboards/{dashboardId}/{action}", method = RequestMethod.GET) + @ResponseBody + public UserDashboardsInfo reportUserDashboardAction( + @ApiParam(value = DASHBOARD_ID_PARAM_DESCRIPTION) + @PathVariable(DashboardController.DASHBOARD_ID) String strDashboardId, + @ApiParam(value = "Dashboard action, one of: \"visit\", \"star\" or \"unstar\".") + @PathVariable("action") String strAction) throws ThingsboardException { + checkParameter(DashboardController.DASHBOARD_ID, strDashboardId); + checkParameter("action", strAction); + UserDashboardAction action = checkEnumParameter("Action", strAction, UserDashboardAction::valueOf); + DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); + checkDashboardInfoId(dashboardId, Operation.READ); + SecurityUser currentUser = getCurrentUser(); + return userSettingsService.reportUserDashboardAction(currentUser.getTenantId(), currentUser.getId(), dashboardId, action); + } + + private void checkNotReserved(String strType, UserSettingsType type) throws ThingsboardException { + if (type.isReserved()) { + throw new ThingsboardException("Settings with type: " + strType + " are reserved for internal use!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); + } } } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/user/DefaultTbUserSettingsService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/user/DefaultTbUserSettingsService.java new file mode 100644 index 0000000000..795ed1f145 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/user/DefaultTbUserSettingsService.java @@ -0,0 +1,202 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.entitiy.user; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.HasTitle; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.settings.AbstractUserDashboardInfo; +import org.thingsboard.server.common.data.settings.LastVisitedDashboardInfo; +import org.thingsboard.server.common.data.settings.StarredDashboardInfo; +import org.thingsboard.server.common.data.settings.UserDashboardAction; +import org.thingsboard.server.common.data.settings.UserDashboardsInfo; +import org.thingsboard.server.common.data.settings.UserSettings; +import org.thingsboard.server.common.data.settings.UserSettingsType; +import org.thingsboard.server.dao.dashboard.DashboardService; +import org.thingsboard.server.dao.user.UserSettingsService; +import org.thingsboard.server.queue.util.TbCoreComponent; + +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +@Service +@TbCoreComponent +@AllArgsConstructor +@Slf4j +public class DefaultTbUserSettingsService implements TbUserSettingsService { + + private static final int MAX_DASHBOARD_INFO_LIST_SIZE = 10; + private static final Predicate EMPTY_TITLE = i -> StringUtils.isEmpty(i.getTitle()); + + private final UserSettingsService settingsService; + private final DashboardService dashboardService; + + @Override + public UserSettings saveUserSettings(TenantId tenantId, UserSettings userSettings) { + return settingsService.saveUserSettings(tenantId, userSettings); + } + + @Override + public void updateUserSettings(TenantId tenantId, UserId userId, UserSettingsType type, JsonNode settings) { + settingsService.updateUserSettings(tenantId, userId, type, settings); + } + + @Override + public UserSettings findUserSettings(TenantId tenantId, UserId userId, UserSettingsType type) { + return settingsService.findUserSettings(tenantId, userId, type); + } + + @Override + public void deleteUserSettings(TenantId tenantId, UserId userId, UserSettingsType type, List jsonPaths) { + settingsService.deleteUserSettings(tenantId, userId, type, jsonPaths); + } + + @Override + public UserDashboardsInfo findUserDashboardsInfo(TenantId tenantId, UserId id) { + UserSettings us = findUserSettings(tenantId, id, UserSettingsType.VISITED_DASHBOARDS); + if (us == null) { + return UserDashboardsInfo.EMPTY; + } + UserDashboardsInfo stored = JacksonUtil.convertValue(us.getSettings(), UserDashboardsInfo.class); + return refreshDashboardTitles(tenantId, stored); + } + + @Override + public UserDashboardsInfo reportUserDashboardAction(TenantId tenantId, UserId id, DashboardId dashboardId, UserDashboardAction action) { + UserSettings us = findUserSettings(tenantId, id, UserSettingsType.VISITED_DASHBOARDS); + UserDashboardsInfo stored = null; + if (us != null) { + stored = JacksonUtil.convertValue(us.getSettings(), UserDashboardsInfo.class); + } + if (stored == null) { + stored = new UserDashboardsInfo(); + } + + switch (action) { + case STAR: + addToStarred(stored, dashboardId); + break; + case UNSTAR: + removeFromStarred(stored, dashboardId); + break; + case VISIT: + addToVisited(stored, dashboardId); + break; + } + + stored = refreshDashboardTitles(tenantId, stored); + + us = new UserSettings(); + us.setUserId(id); + us.setType(UserSettingsType.VISITED_DASHBOARDS); + us.setSettings(JacksonUtil.valueToTree(stored)); + saveUserSettings(tenantId, us); + return stored; + } + + private void addToVisited(UserDashboardsInfo stored, DashboardId dashboardId) { + UUID id = dashboardId.getId(); + long ts = System.currentTimeMillis(); + var opt = stored.getLast().stream().filter(filterById(id)).findFirst(); + if (opt.isPresent()) { + opt.get().setLastVisited(ts); + } else { + var newInfo = new LastVisitedDashboardInfo(); + newInfo.setId(id); + newInfo.setStarred(stored.getStarred().stream().anyMatch(filterById(id))); + newInfo.setLastVisited(System.currentTimeMillis()); + stored.getLast().add(newInfo); + } + stored.getLast().sort(Comparator.comparing(LastVisitedDashboardInfo::getLastVisited).reversed()); + if (stored.getLast().size() > MAX_DASHBOARD_INFO_LIST_SIZE) { + stored.setLast(stored.getLast().stream().limit(MAX_DASHBOARD_INFO_LIST_SIZE).collect(Collectors.toList())); + } + } + + private void removeFromStarred(UserDashboardsInfo stored, DashboardId dashboardId) { + UUID id = dashboardId.getId(); + stored.getStarred().removeIf(filterById(id)); + stored.getLast().stream().filter(d -> id.equals(d.getId())).findFirst().ifPresent(d -> d.setStarred(false)); + } + + private void addToStarred(UserDashboardsInfo stored, DashboardId dashboardId) { + UUID id = dashboardId.getId(); + long ts = System.currentTimeMillis(); + var opt = stored.getStarred().stream().filter(filterById(id)).findFirst(); + if (opt.isPresent()) { + opt.get().setStarredAt(ts); + } else { + var newInfo = new StarredDashboardInfo(); + newInfo.setId(id); + newInfo.setStarredAt(System.currentTimeMillis()); + stored.getStarred().add(newInfo); + } + stored.getStarred().sort(Comparator.comparing(StarredDashboardInfo::getStarredAt).reversed()); + if (stored.getStarred().size() > MAX_DASHBOARD_INFO_LIST_SIZE) { + stored.setStarred(stored.getStarred().stream().limit(MAX_DASHBOARD_INFO_LIST_SIZE).collect(Collectors.toList())); + } + Set starredMap = + stored.getStarred().stream().map(AbstractUserDashboardInfo::getId).collect(Collectors.toSet()); + stored.getLast().forEach(d -> d.setStarred(starredMap.contains(d.getId()))); + } + + private Predicate filterById(UUID id) { + return d -> id.equals(d.getId()); + } + + private UserDashboardsInfo refreshDashboardTitles(TenantId tenantId, UserDashboardsInfo stored) { + if (stored == null) { + return UserDashboardsInfo.EMPTY; + } + stored.getLast().forEach(i -> i.setTitle(null)); + stored.getStarred().forEach(i -> i.setTitle(null)); + + Set uniqueIds = new HashSet<>(); + stored.getLast().stream().map(AbstractUserDashboardInfo::getId).forEach(uniqueIds::add); + stored.getStarred().stream().map(AbstractUserDashboardInfo::getId).forEach(uniqueIds::add); + + Map dashboardTitles = new HashMap<>(); + uniqueIds.forEach(id -> { + var title = dashboardService.findDashboardTitleById(tenantId, new DashboardId(id)); + if (StringUtils.isNotEmpty(title)) { + dashboardTitles.put(id, title); + } + } + ); + + stored.getLast().forEach(i -> i.setTitle(dashboardTitles.get(i.getId()))); + stored.getLast().removeIf(EMPTY_TITLE); + stored.getStarred().forEach(i -> i.setTitle(dashboardTitles.get(i.getId()))); + stored.getStarred().removeIf(EMPTY_TITLE); + return stored; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/user/TbUserSettingsService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/user/TbUserSettingsService.java new file mode 100644 index 0000000000..a81dbcad2f --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/user/TbUserSettingsService.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.entitiy.user; + +import com.fasterxml.jackson.databind.JsonNode; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.settings.UserDashboardAction; +import org.thingsboard.server.common.data.settings.UserDashboardsInfo; +import org.thingsboard.server.common.data.settings.UserSettings; +import org.thingsboard.server.common.data.settings.UserSettingsType; + +import java.util.List; + +public interface TbUserSettingsService { + + void updateUserSettings(TenantId tenantId, UserId userId, UserSettingsType type, JsonNode settings); + + UserSettings saveUserSettings(TenantId tenantId, UserSettings userSettings); + + UserSettings findUserSettings(TenantId tenantId, UserId userId, UserSettingsType type); + + void deleteUserSettings(TenantId tenantId, UserId userId, UserSettingsType type, List jsonPaths); + + UserDashboardsInfo findUserDashboardsInfo(TenantId tenantId, UserId id); + + UserDashboardsInfo reportUserDashboardAction(TenantId tenantId, UserId id, DashboardId dashboardId, UserDashboardAction action); +} diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index d96e50ca7f..e040e0bc5e 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -474,6 +474,10 @@ cache: userSettings: timeToLiveInMinutes: "${CACHE_SPECS_USER_SETTINGS_TTL:1440}" maxSize: "${CACHE_SPECS_USER_SETTINGS_MAX_SIZE:100000}" + dashboardTitles: + timeToLiveInMinutes: "${CACHE_SPECS_DASHBOARD_TITLES_TTL:1440}" + maxSize: "${CACHE_SPECS_DASHBOARD_TITLES_MAX_SIZE:100000}" + #Disable this because it is not required. spring.data.redis.repositories.enabled: false diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java index 7149f52630..680cf19822 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java @@ -18,6 +18,7 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang3.RandomStringUtils; import org.junit.After; import org.junit.Assert; import org.junit.Ignore; @@ -31,6 +32,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.web.servlet.ResultActions; import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; @@ -42,6 +44,8 @@ import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.common.data.settings.StarredDashboardInfo; +import org.thingsboard.server.common.data.settings.UserDashboardsInfo; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.user.UserDao; @@ -356,7 +360,7 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest { String userIdStr = savedUser.getId().getId().toString(); doGet("/api/user/" + userIdStr) .andExpect(status().isNotFound()) - .andExpect(statusReason(containsString( msgErrorNoFound("User",userIdStr)))); + .andExpect(statusReason(containsString(msgErrorNoFound("User", userIdStr)))); } @Test @@ -574,9 +578,9 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest { String email1 = "testEmail1"; String email2 = "testEmail2"; List customerUsersEmail1 = new ArrayList<>(); - List customerUsersEmail2= new ArrayList<>(); + List customerUsersEmail2 = new ArrayList<>(); for (int i = 0; i < 45; i++) { - User customerUser = createCustomerUser( customerId); + User customerUser = createCustomerUser(customerId); customerUser.setEmail(email1 + StringUtils.randomAlphanumeric((int) (5 + Math.random() * 10)) + "@thingsboard.org"); customerUsersEmail1.add(doPost("/api/user", customerUser, User.class)); @@ -685,7 +689,7 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest { JsonNode retrievedSettings = doGet("/api/user/settings", JsonNode.class); Assert.assertEquals(retrievedSettings, userSettings); - } + } @Test public void testShouldNotSaveJsonWithRestrictedSymbols() throws Exception { @@ -860,7 +864,7 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest { List expectedUserInfos = customerUsersContainingWord.stream().map(customerUser -> new UserEmailInfo(customerUser.getId(), customerUser.getEmail(), customerUser.getFirstName() == null ? "" : customerUser.getFirstName(), - customerUser.getLastName() == null ? "" : customerUser.getLastName())) + customerUser.getLastName() == null ? "" : customerUser.getLastName())) .sorted(userDataIdComparator).collect(Collectors.toList()); usersInfo.sort(userDataIdComparator); @@ -912,8 +916,8 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest { List usersInfo = getUsersInfo(pageLink); List expectedUserInfos = usersContainingWord.stream().map(customerUser -> new UserEmailInfo(customerUser.getId(), - customerUser.getEmail(), customerUser.getFirstName() == null ? "" : customerUser.getFirstName(), - customerUser.getLastName() == null ? "" : customerUser.getLastName())) + customerUser.getEmail(), customerUser.getFirstName() == null ? "" : customerUser.getFirstName(), + customerUser.getLastName() == null ? "" : customerUser.getLastName())) .sorted(userDataIdComparator).collect(Collectors.toList()); usersInfo.sort(userDataIdComparator); @@ -922,7 +926,7 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest { // find user by full last name pageLink = new PageLink(10, 0, searchText + "3"); usersInfo = getUsersInfo(pageLink); - Assert.assertEquals(2, usersInfo.size()); + Assert.assertEquals(2, usersInfo.size()); //clear users doDelete("/api/customer/" + customerId.getId().toString()) @@ -941,6 +945,7 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest { private static User createCustomerUser(CustomerId customerId) { return createCustomerUser(null, null, customerId); } + private static User createCustomerUser(String firstName, String lastName, CustomerId customerId) { String suffix = StringUtils.randomAlphanumeric((int) (5 + Math.random() * 10)); return createCustomerUser(firstName, lastName, "testMail" + suffix + "@thingsboard.org", customerId); @@ -959,6 +964,7 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest { private User createTenantAdminUser() { return createTenantAdminUser(null, null); } + private User createTenantAdminUser(String firstName, String lastName) { String suffix = StringUtils.randomAlphanumeric((int) (5 + Math.random() * 10)); @@ -975,7 +981,8 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest { List loadedCustomerUsers = new ArrayList<>(); PageData pageData = null; do { - pageData = doGetTypedWithPageLink("/api/users/info?", new TypeReference<>() {}, pageLink); + pageData = doGetTypedWithPageLink("/api/users/info?", new TypeReference<>() { + }, pageLink); loadedCustomerUsers.addAll(pageData.getData()); if (pageData.hasNext()) { pageLink = pageLink.nextPageLink(); @@ -984,4 +991,167 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest { return loadedCustomerUsers; } + @Test + public void testEmptyDashboardSettings() throws Exception { + loginCustomerUser(); + + UserDashboardsInfo retrievedSettings = doGet("/api/user/dashboards", UserDashboardsInfo.class); + Assert.assertNotNull(retrievedSettings); + Assert.assertNotNull(retrievedSettings.getLast()); + Assert.assertTrue(retrievedSettings.getLast().isEmpty()); + Assert.assertNotNull(retrievedSettings.getStarred()); + Assert.assertTrue(retrievedSettings.getStarred().isEmpty()); + } + + @Test + public void testDashboardSettingsFlow() throws Exception { + loginTenantAdmin(); + + Dashboard dashboard1 = new Dashboard(); + dashboard1.setTitle("My dashboard 1"); + Dashboard savedDashboard1 = doPost("/api/dashboard", dashboard1, Dashboard.class); + Dashboard dashboard2 = new Dashboard(); + dashboard2.setTitle("My dashboard 2"); + Dashboard savedDashboard2 = doPost("/api/dashboard", dashboard2, Dashboard.class); + + UserDashboardsInfo retrievedSettings = doGet("/api/user/dashboards", UserDashboardsInfo.class); + Assert.assertNotNull(retrievedSettings); + Assert.assertNotNull(retrievedSettings.getLast()); + Assert.assertTrue(retrievedSettings.getLast().isEmpty()); + Assert.assertNotNull(retrievedSettings.getStarred()); + Assert.assertTrue(retrievedSettings.getStarred().isEmpty()); + + UserDashboardsInfo newSettings = doGet("/api/user/dashboards/" + savedDashboard1.getId().getId() + "/visit", UserDashboardsInfo.class); + Assert.assertNotNull(newSettings); + Assert.assertNotNull(newSettings.getLast()); + Assert.assertEquals(1, newSettings.getLast().size()); + var lastVisited = newSettings.getLast().get(0); + Assert.assertEquals(savedDashboard1.getId().getId(), lastVisited.getId()); + Assert.assertEquals(savedDashboard1.getTitle(), lastVisited.getTitle()); + Assert.assertNotNull(retrievedSettings.getStarred()); + Assert.assertTrue(retrievedSettings.getStarred().isEmpty()); + + newSettings = doGet("/api/user/dashboards/" + savedDashboard2.getId().getId() + "/visit", UserDashboardsInfo.class); + Assert.assertNotNull(newSettings); + Assert.assertNotNull(newSettings.getLast()); + Assert.assertEquals(2, newSettings.getLast().size()); + lastVisited = newSettings.getLast().get(0); + Assert.assertEquals(savedDashboard2.getId().getId(), lastVisited.getId()); + Assert.assertEquals(savedDashboard2.getTitle(), lastVisited.getTitle()); + Assert.assertNotNull(retrievedSettings.getStarred()); + Assert.assertTrue(retrievedSettings.getStarred().isEmpty()); + + newSettings = doGet("/api/user/dashboards", UserDashboardsInfo.class); + Assert.assertNotNull(newSettings); + Assert.assertNotNull(newSettings.getLast()); + Assert.assertEquals(2, newSettings.getLast().size()); + lastVisited = newSettings.getLast().get(0); + Assert.assertEquals(savedDashboard2.getId().getId(), lastVisited.getId()); + Assert.assertEquals(savedDashboard2.getTitle(), lastVisited.getTitle()); + Assert.assertNotNull(retrievedSettings.getStarred()); + Assert.assertTrue(retrievedSettings.getStarred().isEmpty()); + + newSettings = doGet("/api/user/dashboards/" + savedDashboard1.getId().getId() + "/star", UserDashboardsInfo.class); + Assert.assertNotNull(newSettings); + Assert.assertNotNull(newSettings.getLast()); + Assert.assertEquals(2, newSettings.getLast().size()); + lastVisited = newSettings.getLast().get(0); + Assert.assertEquals(savedDashboard2.getId().getId(), lastVisited.getId()); + Assert.assertEquals(savedDashboard2.getTitle(), lastVisited.getTitle()); + Assert.assertFalse(lastVisited.isStarred()); + lastVisited = newSettings.getLast().get(1); + Assert.assertEquals(savedDashboard1.getId().getId(), lastVisited.getId()); + Assert.assertEquals(savedDashboard1.getTitle(), lastVisited.getTitle()); + Assert.assertTrue(lastVisited.isStarred()); + Assert.assertNotNull(retrievedSettings.getStarred()); + Assert.assertEquals(1, newSettings.getStarred().size()); + StarredDashboardInfo starred = newSettings.getStarred().get(0); + Assert.assertEquals(savedDashboard1.getId().getId(), starred.getId()); + Assert.assertEquals(savedDashboard1.getTitle(), starred.getTitle()); + + newSettings = doGet("/api/user/dashboards/" + savedDashboard2.getId().getId() + "/star", UserDashboardsInfo.class); + Assert.assertNotNull(newSettings); + Assert.assertNotNull(newSettings.getLast()); + Assert.assertEquals(2, newSettings.getLast().size()); + lastVisited = newSettings.getLast().get(0); + Assert.assertEquals(savedDashboard2.getId().getId(), lastVisited.getId()); + Assert.assertEquals(savedDashboard2.getTitle(), lastVisited.getTitle()); + Assert.assertTrue(lastVisited.isStarred()); + lastVisited = newSettings.getLast().get(1); + Assert.assertEquals(savedDashboard1.getId().getId(), lastVisited.getId()); + Assert.assertEquals(savedDashboard1.getTitle(), lastVisited.getTitle()); + Assert.assertTrue(lastVisited.isStarred()); + Assert.assertNotNull(retrievedSettings.getStarred()); + Assert.assertEquals(2, newSettings.getStarred().size()); + starred = newSettings.getStarred().get(0); + Assert.assertEquals(savedDashboard2.getId().getId(), starred.getId()); + Assert.assertEquals(savedDashboard2.getTitle(), starred.getTitle()); + + newSettings = doGet("/api/user/dashboards/" + savedDashboard1.getId().getId() + "/unstar", UserDashboardsInfo.class); + Assert.assertNotNull(newSettings); + Assert.assertNotNull(newSettings.getLast()); + Assert.assertEquals(2, newSettings.getLast().size()); + lastVisited = newSettings.getLast().get(0); + Assert.assertEquals(savedDashboard2.getId().getId(), lastVisited.getId()); + Assert.assertEquals(savedDashboard2.getTitle(), lastVisited.getTitle()); + Assert.assertTrue(lastVisited.isStarred()); + lastVisited = newSettings.getLast().get(1); + Assert.assertEquals(savedDashboard1.getId().getId(), lastVisited.getId()); + Assert.assertEquals(savedDashboard1.getTitle(), lastVisited.getTitle()); + Assert.assertFalse(lastVisited.isStarred()); + Assert.assertNotNull(retrievedSettings.getStarred()); + Assert.assertEquals(1, newSettings.getStarred().size()); + starred = newSettings.getStarred().get(0); + Assert.assertEquals(savedDashboard2.getId().getId(), starred.getId()); + Assert.assertEquals(savedDashboard2.getTitle(), starred.getTitle()); + + //TEST renaming in the cache. + savedDashboard1.setTitle(RandomStringUtils.randomAlphanumeric(10)); + savedDashboard1 = doPost("/api/dashboard", savedDashboard1, Dashboard.class); + savedDashboard2.setTitle(RandomStringUtils.randomAlphanumeric(10)); + savedDashboard2 = doPost("/api/dashboard", savedDashboard2, Dashboard.class); + + newSettings = doGet("/api/user/dashboards/" + savedDashboard1.getId().getId() + "/unstar", UserDashboardsInfo.class); + Assert.assertNotNull(newSettings); + Assert.assertNotNull(newSettings.getLast()); + Assert.assertEquals(2, newSettings.getLast().size()); + lastVisited = newSettings.getLast().get(0); + Assert.assertEquals(savedDashboard2.getId().getId(), lastVisited.getId()); + Assert.assertEquals(savedDashboard2.getTitle(), lastVisited.getTitle()); + Assert.assertTrue(lastVisited.isStarred()); + lastVisited = newSettings.getLast().get(1); + Assert.assertEquals(savedDashboard1.getId().getId(), lastVisited.getId()); + Assert.assertEquals(savedDashboard1.getTitle(), lastVisited.getTitle()); + Assert.assertFalse(lastVisited.isStarred()); + Assert.assertNotNull(retrievedSettings.getStarred()); + Assert.assertEquals(1, newSettings.getStarred().size()); + starred = newSettings.getStarred().get(0); + Assert.assertEquals(savedDashboard2.getId().getId(), starred.getId()); + Assert.assertEquals(savedDashboard2.getTitle(), starred.getTitle()); + + doDelete("/api/dashboard/" + savedDashboard1.getId().getId().toString()).andExpect(status().isOk()); + + newSettings = doGet("/api/user/dashboards", UserDashboardsInfo.class); + Assert.assertNotNull(newSettings); + Assert.assertNotNull(newSettings.getLast()); + Assert.assertEquals(1, newSettings.getLast().size()); + lastVisited = newSettings.getLast().get(0); + Assert.assertEquals(savedDashboard2.getId().getId(), lastVisited.getId()); + Assert.assertEquals(savedDashboard2.getTitle(), lastVisited.getTitle()); + Assert.assertTrue(lastVisited.isStarred()); + Assert.assertEquals(1, newSettings.getStarred().size()); + starred = newSettings.getStarred().get(0); + Assert.assertEquals(savedDashboard2.getId().getId(), starred.getId()); + Assert.assertEquals(savedDashboard2.getTitle(), starred.getTitle()); + + doDelete("/api/dashboard/" + savedDashboard2.getId().getId().toString()).andExpect(status().isOk()); + + retrievedSettings = doGet("/api/user/dashboards", UserDashboardsInfo.class); + Assert.assertNotNull(retrievedSettings); + Assert.assertNotNull(retrievedSettings.getLast()); + Assert.assertTrue(retrievedSettings.getLast().isEmpty()); + Assert.assertNotNull(retrievedSettings.getStarred()); + Assert.assertTrue(retrievedSettings.getStarred().isEmpty()); + } + } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java index 7c47efafc3..c9974bba69 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java @@ -36,6 +36,8 @@ public interface DashboardService extends EntityDaoService { DashboardInfo findDashboardInfoById(TenantId tenantId, DashboardId dashboardId); + String findDashboardTitleById(TenantId tenantId, DashboardId dashboardId); + ListenableFuture findDashboardInfoByIdAsync(TenantId tenantId, DashboardId dashboardId); Dashboard saveDashboard(Dashboard dashboard); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserSettingsService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserSettingsService.java index 2c6b149607..58ac697409 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserSettingsService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserSettingsService.java @@ -18,18 +18,19 @@ package org.thingsboard.server.dao.user; import com.fasterxml.jackson.databind.JsonNode; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; -import org.thingsboard.server.common.data.security.UserSettings; +import org.thingsboard.server.common.data.settings.UserSettings; +import org.thingsboard.server.common.data.settings.UserSettingsType; import java.util.List; public interface UserSettingsService { - void updateUserSettings(TenantId tenantId, UserId userId, JsonNode settings); + void updateUserSettings(TenantId tenantId, UserId userId, UserSettingsType type, JsonNode settings); UserSettings saveUserSettings(TenantId tenantId, UserSettings userSettings); - UserSettings findUserSettings(TenantId tenantId, UserId userId); + UserSettings findUserSettings(TenantId tenantId, UserId userId, UserSettingsType type); - void deleteUserSettings(TenantId tenantId, UserId userId, List jsonPaths); + void deleteUserSettings(TenantId tenantId, UserId userId, UserSettingsType type, List jsonPaths); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java index eedf925d61..b09d92bd19 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java @@ -42,4 +42,5 @@ public class CacheConstants { public static final String TWO_FA_VERIFICATION_CODES_CACHE = "twoFaVerificationCodes"; public static final String VERSION_CONTROL_TASK_CACHE = "versionControlTask"; public static final String USER_SETTINGS_CACHE = "userSettings"; + public static final String DASHBOARD_TITLES_CACHE = "dashboardTitles"; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/settings/AbstractUserDashboardInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/settings/AbstractUserDashboardInfo.java new file mode 100644 index 0000000000..ce214cb603 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/settings/AbstractUserDashboardInfo.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.settings; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.thingsboard.server.common.data.HasTitle; + +import java.io.Serializable; +import java.util.UUID; + +@ApiModel +@Data +public abstract class AbstractUserDashboardInfo implements HasTitle, Serializable { + + private static final long serialVersionUID = -6461562426034242608L; + + @ApiModelProperty(position = 1, value = "JSON object with Dashboard id.", accessMode = ApiModelProperty.AccessMode.READ_ONLY) + private UUID id; + @ApiModelProperty(position = 2, value = "Title of the dashboard.") + private String title; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/settings/LastVisitedDashboardInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/settings/LastVisitedDashboardInfo.java new file mode 100644 index 0000000000..e49910eb43 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/settings/LastVisitedDashboardInfo.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.settings; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; + +@EqualsAndHashCode(callSuper = true) +@ApiModel +@Data +public class LastVisitedDashboardInfo extends AbstractUserDashboardInfo implements Serializable { + + private static final long serialVersionUID = -6461562426034242608L; + + @ApiModelProperty(position = 3, value = "Starred flag") + private boolean starred; + @ApiModelProperty(position = 4, value = "Last visit timestamp") + private long lastVisited; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/settings/StarredDashboardInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/settings/StarredDashboardInfo.java new file mode 100644 index 0000000000..1e281bd95d --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/settings/StarredDashboardInfo.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.settings; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.HasTitle; +import org.thingsboard.server.common.data.id.DashboardId; + +import java.io.Serializable; + +@EqualsAndHashCode(callSuper = true) +@ApiModel +@Data +public class StarredDashboardInfo extends AbstractUserDashboardInfo implements Serializable { + + private static final long serialVersionUID = -7830828696329673361L; + @ApiModelProperty(position = 4, value = "Starred timestamp") + private long starredAt; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/settings/UserDashboardAction.java b/common/data/src/main/java/org/thingsboard/server/common/data/settings/UserDashboardAction.java new file mode 100644 index 0000000000..f89e29a45f --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/settings/UserDashboardAction.java @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.settings; + +public enum UserDashboardAction { + + VISIT, STAR, UNSTAR + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/settings/UserDashboardsInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/settings/UserDashboardsInfo.java new file mode 100644 index 0000000000..e5423f0d5d --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/settings/UserDashboardsInfo.java @@ -0,0 +1,46 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.settings; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@ApiModel +@Data +@AllArgsConstructor +public class UserDashboardsInfo implements Serializable { + + private static final long serialVersionUID = 2628320657987010348L; + public static final UserDashboardsInfo EMPTY = new UserDashboardsInfo(Collections.emptyList(), Collections.emptyList()); + + @ApiModelProperty(position = 1, value = "List of last visited dashboards.", accessMode = ApiModelProperty.AccessMode.READ_ONLY) + private List last; + + @ApiModelProperty(position = 2, value = "List of starred dashboards.", accessMode = ApiModelProperty.AccessMode.READ_ONLY) + private List starred; + + public UserDashboardsInfo() { + this(new ArrayList<>(), new ArrayList<>()); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/UserSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/settings/UserSettings.java similarity index 88% rename from common/data/src/main/java/org/thingsboard/server/common/data/security/UserSettings.java rename to common/data/src/main/java/org/thingsboard/server/common/data/settings/UserSettings.java index 5a7fb19ffa..3969c6d851 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/UserSettings.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/settings/UserSettings.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.security; +package org.thingsboard.server.common.data.settings; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.core.JsonProcessingException; @@ -43,7 +43,12 @@ public class UserSettings implements Serializable { @ApiModelProperty(position = 1, value = "JSON object with User id.", accessMode = ApiModelProperty.AccessMode.READ_ONLY) private UserId userId; - @ApiModelProperty(position = 2, value = "JSON object with user settings.", dataType = "com.fasterxml.jackson.databind.JsonNode") + @ApiModelProperty(position = 2, value = "Type of the settings.") + @NoXss + @Length(fieldName = "type", max = 50) + private UserSettingsType type; + + @ApiModelProperty(position = 3, value = "JSON object with user settings.", dataType = "com.fasterxml.jackson.databind.JsonNode") @NoXss @Length(fieldName = "settings", max = 100000) private transient JsonNode settings; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/settings/UserSettingsCompositeKey.java b/common/data/src/main/java/org/thingsboard/server/common/data/settings/UserSettingsCompositeKey.java new file mode 100644 index 0000000000..eba902fd7f --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/settings/UserSettingsCompositeKey.java @@ -0,0 +1,44 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.settings; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.util.UUID; + +@NoArgsConstructor +@AllArgsConstructor +@Data +public class UserSettingsCompositeKey implements Serializable { + + private static final long serialVersionUID = -7883642552545291489L; + + private UUID userId; + private String type; + + public UserSettingsCompositeKey(UserSettings userSettings) { + this.userId = userSettings.getUserId().getId(); + this.type = userSettings.getType().name(); + } + + @Override + public String toString() { + return userId.toString() + "_" + type; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/settings/UserSettingsType.java b/common/data/src/main/java/org/thingsboard/server/common/data/settings/UserSettingsType.java new file mode 100644 index 0000000000..97ef85a5f2 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/settings/UserSettingsType.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.settings; + +import lombok.Getter; + +public enum UserSettingsType { + + GENERAL, VISITED_DASHBOARDS(true), QUICK_LINKS, DOC_LINKS, DASHBOARDS; + + @Getter + private final boolean reserved; + + UserSettingsType() { + this.reserved = false; + } + + UserSettingsType(boolean reserved) { + this.reserved = reserved; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardInfoDao.java index e0d1777112..cb6a5050e5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardInfoDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardInfoDao.java @@ -16,6 +16,8 @@ package org.thingsboard.server.dao.dashboard; import org.thingsboard.server.common.data.DashboardInfo; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.Dao; @@ -77,4 +79,5 @@ public interface DashboardInfoDao extends Dao { DashboardInfo findFirstByTenantIdAndName(UUID tenantId, String name); + String findTitleById(UUID tenantId, UUID dashboardId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java index 96d56148eb..d1198d4d7a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java @@ -19,8 +19,12 @@ import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionalEventListener; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.thingsboard.server.cache.TbTransactionalCache; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.DashboardInfo; @@ -36,6 +40,7 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.common.data.settings.UserSettingsCompositeKey; import org.thingsboard.server.dao.customer.CustomerDao; import org.thingsboard.server.dao.edge.EdgeDao; import org.thingsboard.server.dao.entity.AbstractEntityService; @@ -43,6 +48,7 @@ import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; +import org.thingsboard.server.dao.user.UserSettingsEvictEvent; import java.util.List; import java.util.Optional; @@ -70,6 +76,25 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb @Autowired private DataValidator dashboardValidator; + @Autowired + protected TbTransactionalCache cache; + + @Autowired + private ApplicationEventPublisher eventPublisher; + + protected void publishEvictEvent(DashboardTitleEvictEvent event) { + if (TransactionSynchronizationManager.isActualTransactionActive()) { + eventPublisher.publishEvent(event); + } else { + handleEvictEvent(event); + } + } + + @TransactionalEventListener(classes = DashboardTitleEvictEvent.class) + public void handleEvictEvent(DashboardTitleEvictEvent event) { + cache.evict(event.getKey()); + } + @Override public Dashboard findDashboardById(TenantId tenantId, DashboardId dashboardId) { log.trace("Executing findDashboardById [{}]", dashboardId); @@ -91,6 +116,12 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb return dashboardInfoDao.findById(tenantId, dashboardId.getId()); } + @Override + public String findDashboardTitleById(TenantId tenantId, DashboardId dashboardId) { + return cache.getAndPutInTransaction(dashboardId, + () -> dashboardInfoDao.findTitleById(tenantId.getId(), dashboardId.getId()), true); + } + @Override public ListenableFuture findDashboardInfoByIdAsync(TenantId tenantId, DashboardId dashboardId) { log.trace("Executing findDashboardInfoByIdAsync [{}]", dashboardId); @@ -103,8 +134,13 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb log.trace("Executing saveDashboard [{}]", dashboard); dashboardValidator.validate(dashboard, DashboardInfo::getTenantId); try { - return dashboardDao.save(dashboard.getTenantId(), dashboard); + var saved = dashboardDao.save(dashboard.getTenantId(), dashboard); + publishEvictEvent(new DashboardTitleEvictEvent(saved.getId())); + return saved; } catch (Exception e) { + if (dashboard.getId() != null) { + publishEvictEvent(new DashboardTitleEvictEvent(dashboard.getId())); + } checkConstraintViolation(e, "dashboard_external_id_unq_key", "Dashboard with such external id already exists!"); throw e; } @@ -170,6 +206,7 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb deleteEntityRelations(tenantId, dashboardId); try { dashboardDao.removeById(tenantId, dashboardId.getId()); + publishEvictEvent(new DashboardTitleEvictEvent(dashboardId)); } catch (Exception t) { ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("fk_default_dashboard_device_profile")) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardTitleEvictEvent.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardTitleEvictEvent.java new file mode 100644 index 0000000000..1e4e985988 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardTitleEvictEvent.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.dashboard; + +import lombok.Data; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.settings.UserSettingsCompositeKey; + +@Data +public class DashboardTitleEvictEvent { + private final DashboardId key; +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardTitlesCaffeineCache.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardTitlesCaffeineCache.java new file mode 100644 index 0000000000..fca9a552a3 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardTitlesCaffeineCache.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.dashboard; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Service; +import org.thingsboard.server.cache.CaffeineTbTransactionalCache; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.id.DashboardId; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) +@Service("DashboardTitlesCache") +public class DashboardTitlesCaffeineCache extends CaffeineTbTransactionalCache { + + public DashboardTitlesCaffeineCache(CacheManager cacheManager) { + super(cacheManager, CacheConstants.DASHBOARD_TITLES_CACHE); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardTitlesRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardTitlesRedisCache.java new file mode 100644 index 0000000000..990a175b71 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardTitlesRedisCache.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.dashboard; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.stereotype.Service; +import org.thingsboard.server.cache.CacheSpecsMap; +import org.thingsboard.server.cache.RedisTbTransactionalCache; +import org.thingsboard.server.cache.TBRedisCacheConfiguration; +import org.thingsboard.server.cache.TbFSTRedisSerializer; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.id.DashboardId; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") +@Service("DashboardTitlesCache") +public class DashboardTitlesRedisCache extends RedisTbTransactionalCache { + + public DashboardTitlesRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { + super(CacheConstants.DASHBOARD_TITLES_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbFSTRedisSerializer<>()); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index a0804e0c71..6197d8f908 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -94,6 +94,7 @@ public class ModelConstants { */ public static final String USER_SETTINGS_COLUMN_FAMILY_NAME = "user_settings"; public static final String USER_SETTINGS_USER_ID_PROPERTY = USER_ID_PROPERTY; + public static final String USER_SETTINGS_TYPE_PROPERTY = "type"; public static final String USER_SETTINGS_SETTINGS = "settings"; /** diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/UserSettingsEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/UserSettingsEntity.java index d7abf7ea23..a00efff52f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/UserSettingsEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/UserSettingsEntity.java @@ -21,7 +21,9 @@ import lombok.NoArgsConstructor; import org.hibernate.annotations.Type; import org.hibernate.annotations.TypeDef; import org.thingsboard.server.common.data.id.UserId; -import org.thingsboard.server.common.data.security.UserSettings; +import org.thingsboard.server.common.data.settings.UserSettings; +import org.thingsboard.server.common.data.settings.UserSettingsCompositeKey; +import org.thingsboard.server.common.data.settings.UserSettingsType; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.model.ToData; import org.thingsboard.server.dao.util.mapping.JsonStringType; @@ -29,6 +31,7 @@ import org.thingsboard.server.dao.util.mapping.JsonStringType; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Id; +import javax.persistence.IdClass; import javax.persistence.Table; import java.util.UUID; @@ -37,19 +40,24 @@ import java.util.UUID; @TypeDef(name = "json", typeClass = JsonStringType.class) @Entity @Table(name = ModelConstants.USER_SETTINGS_COLUMN_FAMILY_NAME) +@IdClass(UserSettingsCompositeKey.class) public class UserSettingsEntity implements ToData { @Id @Column(name = ModelConstants.USER_SETTINGS_USER_ID_PROPERTY) private UUID userId; + @Id + @Column(name = ModelConstants.USER_SETTINGS_TYPE_PROPERTY) + private String type; @Type(type = "json") @Column(name = ModelConstants.USER_SETTINGS_SETTINGS) private JsonNode settings; public UserSettingsEntity(UserSettings userSettings) { this.userId = userSettings.getUserId().getId(); + this.type = userSettings.getType().name(); if (userSettings.getSettings() != null) { - this.settings= userSettings.getSettings(); + this.settings = userSettings.getSettings(); } } @@ -57,6 +65,7 @@ public class UserSettingsEntity implements ToData { public UserSettings toData() { UserSettings userSettings = new UserSettings(); userSettings.setUserId(new UserId(userId)); + userSettings.setType(UserSettingsType.valueOf(type)); if (settings != null) { userSettings.setSettings(settings); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java index 0ede2325ac..735cffa5f7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java @@ -72,4 +72,6 @@ public interface DashboardInfoRepository extends JpaRepository { +public interface UserSettingsRepository extends JpaRepository { } diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsCaffeineCache.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsCaffeineCache.java index 3154e5d8c0..f8c55c5d94 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsCaffeineCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsCaffeineCache.java @@ -20,14 +20,13 @@ import org.springframework.cache.CacheManager; import org.springframework.stereotype.Service; import org.thingsboard.server.cache.CaffeineTbTransactionalCache; import org.thingsboard.server.common.data.CacheConstants; -import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.id.UserId; -import org.thingsboard.server.common.data.security.UserSettings; -import org.thingsboard.server.dao.asset.AssetCacheKey; +import org.thingsboard.server.common.data.settings.UserSettings; +import org.thingsboard.server.common.data.settings.UserSettingsCompositeKey; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) @Service("UserSettingsCache") -public class UserSettingsCaffeineCache extends CaffeineTbTransactionalCache { +public class UserSettingsCaffeineCache extends CaffeineTbTransactionalCache { public UserSettingsCaffeineCache(CacheManager cacheManager) { super(cacheManager, CacheConstants.USER_SETTINGS_CACHE); diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsDao.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsDao.java index 16a7675a64..8ebd6a534a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsDao.java @@ -17,14 +17,15 @@ package org.thingsboard.server.dao.user; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; -import org.thingsboard.server.common.data.security.UserSettings; +import org.thingsboard.server.common.data.settings.UserSettings; +import org.thingsboard.server.common.data.settings.UserSettingsCompositeKey; public interface UserSettingsDao { UserSettings save(TenantId tenantId, UserSettings userSettings); - UserSettings findById(TenantId tenantId, UserId userId); + UserSettings findById(TenantId tenantId, UserSettingsCompositeKey key); - void removeById(TenantId tenantId, UserId userId); + void removeById(TenantId tenantId, UserSettingsCompositeKey key); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsEvictEvent.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsEvictEvent.java index e0d2de7ece..bfe0142cc6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsEvictEvent.java +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsEvictEvent.java @@ -17,8 +17,9 @@ package org.thingsboard.server.dao.user; import lombok.Data; import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.settings.UserSettingsCompositeKey; @Data public class UserSettingsEvictEvent { - private final UserId userId; + private final UserSettingsCompositeKey key; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsRedisCache.java index a510e6c864..de321f3eb9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsRedisCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsRedisCache.java @@ -23,14 +23,13 @@ import org.thingsboard.server.cache.RedisTbTransactionalCache; import org.thingsboard.server.cache.TBRedisCacheConfiguration; import org.thingsboard.server.cache.TbFSTRedisSerializer; import org.thingsboard.server.common.data.CacheConstants; -import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.id.UserId; -import org.thingsboard.server.common.data.security.UserSettings; -import org.thingsboard.server.dao.asset.AssetCacheKey; +import org.thingsboard.server.common.data.settings.UserSettings; +import org.thingsboard.server.common.data.settings.UserSettingsCompositeKey; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") @Service("UserSettingsCache") -public class UserSettingsRedisCache extends RedisTbTransactionalCache { +public class UserSettingsRedisCache extends RedisTbTransactionalCache { public UserSettingsRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { super(CacheConstants.USER_SETTINGS_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbFSTRedisSerializer<>()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsServiceImpl.java index d94e060383..86e63d3529 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsServiceImpl.java @@ -15,38 +15,36 @@ */ package org.thingsboard.server.dao.user; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.github.fge.jackson.NodeType; import com.jayway.jsonpath.DocumentContext; import com.jayway.jsonpath.JsonPath; -import com.jayway.jsonpath.PathNotFoundException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.event.TransactionalEventListener; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; -import org.thingsboard.server.common.data.security.UserSettings; +import org.thingsboard.server.common.data.settings.UserSettings; +import org.thingsboard.server.common.data.settings.UserSettingsCompositeKey; +import org.thingsboard.server.common.data.settings.UserSettingsType; import org.thingsboard.server.dao.entity.AbstractCachedService; import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.service.ConstraintValidator; import java.util.ArrayList; import java.util.Iterator; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; import static org.thingsboard.server.dao.service.Validator.validateId; @Service("UserSettingsDaoService") @Slf4j @RequiredArgsConstructor -public class UserSettingsServiceImpl extends AbstractCachedService implements UserSettingsService { +public class UserSettingsServiceImpl extends AbstractCachedService implements UserSettingsService { public static final String INCORRECT_USER_ID = "Incorrect userId "; private final UserSettingsDao userSettingsDao; @@ -58,33 +56,37 @@ public class UserSettingsServiceImpl extends AbstractCachedService userSettingsDao.findById(tenantId, userId), true); + var key = new UserSettingsCompositeKey(userId.getId(), type.name()); + return cache.getAndPutInTransaction(key, + () -> userSettingsDao.findById(tenantId, key), true); } @Override - public void deleteUserSettings(TenantId tenantId, UserId userId, List jsonPaths) { + public void deleteUserSettings(TenantId tenantId, UserId userId, UserSettingsType type, List jsonPaths) { log.trace("Executing deleteUserSettings for user [{}]", userId); validateId(userId, INCORRECT_USER_ID + userId); - UserSettings userSettings = userSettingsDao.findById(tenantId, userId); + var key = new UserSettingsCompositeKey(userId.getId(), type.name()); + UserSettings userSettings = userSettingsDao.findById(tenantId, key); if (userSettings == null) { return; } @@ -95,7 +97,7 @@ public class UserSettingsServiceImpl extends AbstractCachedService keys = new ArrayList<>(); - keys.add(event.getUserId()); - cache.evict(keys); + cache.evict(event.getKey()); } private void validateJsonKeys(JsonNode userSettings) { diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 4e60c7105e..4197dc2b07 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -850,9 +850,11 @@ CREATE TABLE IF NOT EXISTS notification ( ) PARTITION BY RANGE (created_time); CREATE TABLE IF NOT EXISTS user_settings ( - user_id uuid NOT NULL CONSTRAINT user_settings_pkey PRIMARY KEY, + user_id uuid NOT NULL, + type VARCHAR(50) NOT NULL, settings varchar(10000), - CONSTRAINT fk_user_id FOREIGN KEY (user_id) REFERENCES tb_user(id) ON DELETE CASCADE + CONSTRAINT fk_user_id FOREIGN KEY (user_id) REFERENCES tb_user(id) ON DELETE CASCADE, + CONSTRAINT user_settings_pkey PRIMARY KEY (user_id, type) ); DROP VIEW IF EXISTS alarm_info CASCADE; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseUserServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseUserServiceTest.java index 14917f6807..c7ff515fb5 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseUserServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseUserServiceTest.java @@ -23,7 +23,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.StringUtils; -import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; @@ -32,7 +31,7 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.UserCredentials; -import org.thingsboard.server.common.data.security.UserSettings; +import org.thingsboard.server.common.data.settings.UserSettings; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.user.UserService; diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/user/JpaUserSettingsDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/user/JpaUserSettingsDaoTest.java index 4260221a73..da38929ed7 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/user/JpaUserSettingsDaoTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/user/JpaUserSettingsDaoTest.java @@ -27,7 +27,9 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.Authority; -import org.thingsboard.server.common.data.security.UserSettings; +import org.thingsboard.server.common.data.settings.UserSettings; +import org.thingsboard.server.common.data.settings.UserSettingsCompositeKey; +import org.thingsboard.server.common.data.settings.UserSettingsType; import org.thingsboard.server.dao.AbstractJpaDaoTest; import org.thingsboard.server.dao.service.AbstractServiceTest; import org.thingsboard.server.dao.user.UserDao; @@ -66,17 +68,18 @@ public class JpaUserSettingsDaoTest extends AbstractJpaDaoTest { public void testFindSettingsByUserId() { UserSettings userSettings = createUserSettings(user.getId()); - UserSettings retrievedUserSettings = userSettingsDao.findById(SYSTEM_TENANT_ID, user.getId()); + UserSettings retrievedUserSettings = userSettingsDao.findById(SYSTEM_TENANT_ID, new UserSettingsCompositeKey(user.getId().getId(), UserSettingsType.GENERAL.name())); assertEquals(retrievedUserSettings.getSettings(), userSettings.getSettings()); - userSettingsDao.removeById(SYSTEM_TENANT_ID, user.getId()); + userSettingsDao.removeById(SYSTEM_TENANT_ID, new UserSettingsCompositeKey(user.getId().getId(), UserSettingsType.GENERAL.name())); - UserSettings retrievedUserSettings2 = userSettingsDao.findById(SYSTEM_TENANT_ID, user.getId()); + UserSettings retrievedUserSettings2 = userSettingsDao.findById(SYSTEM_TENANT_ID, new UserSettingsCompositeKey(user.getId().getId(), UserSettingsType.GENERAL.name())); assertNull(retrievedUserSettings2); } private UserSettings createUserSettings(UserId userId) { UserSettings userSettings = new UserSettings(); + userSettings.setType(UserSettingsType.GENERAL); userSettings.setSettings(JacksonUtil.newObjectNode().put("text", RandomStringUtils.randomAlphanumeric(10))); userSettings.setUserId(userId); return userSettingsDao.save(SYSTEM_TENANT_ID, userSettings); diff --git a/dao/src/test/resources/application-test.properties b/dao/src/test/resources/application-test.properties index fc46ba431a..2d3b5197ae 100644 --- a/dao/src/test/resources/application-test.properties +++ b/dao/src/test/resources/application-test.properties @@ -68,6 +68,9 @@ cache.specs.edges.maxSize=100000 cache.specs.notificationRules.timeToLiveInMinutes=1440 cache.specs.notificationRules.maxSize=10000 +cache.specs.dashboardTitles.timeToLiveInMinutes=1440 +cache.specs.dashboardTitles.maxSize=10000 + redis.connection.host=localhost redis.connection.port=6379 redis.connection.db=0