From 425ba5bed769040a1e0f7cd26f7d740eb74a5f7f Mon Sep 17 00:00:00 2001 From: Martin Weise <martin.weise@tuwien.ac.at> Date: Fri, 10 Jan 2025 23:08:30 +0100 Subject: [PATCH] WIP Signed-off-by: Martin Weise <martin.weise@tuwien.ac.at> --- dbrepo-auth-service/init/app.py | 4 +- .../at/tuwien/endpoints/AccessEndpoint.java | 2 +- .../service/impl/TableServiceMariaDbImpl.java | 6 +- dbrepo-metadata-db/1_setup-schema.sql | 1 + .../java/at/tuwien/entities/user/User.java | 7 +- .../java/at/tuwien/mapper/MetadataMapper.java | 109 +++--- .../at/tuwien/repository/UserRepository.java | 3 + .../at/tuwien/endpoints/AccessEndpoint.java | 17 + .../at/tuwien/endpoints/DatabaseEndpoint.java | 6 +- .../at/tuwien/endpoints/TableEndpoint.java | 23 +- .../at/tuwien/endpoints/UserEndpoint.java | 22 +- .../endpoints/DatabaseEndpointUnitTest.java | 2 +- .../service/DatabaseServiceUnitTest.java | 2 +- .../at/tuwien/service/DatabaseService.java | 3 +- .../java/at/tuwien/service/UserService.java | 2 + .../service/impl/AccessServiceImpl.java | 7 +- .../service/impl/DatabaseServiceImpl.java | 22 +- .../tuwien/service/impl/UserServiceImpl.java | 24 +- dbrepo-ui/components/ResourceStatus.vue | 7 +- .../components/database/DatabaseCreate.vue | 20 +- .../components/database/DatabaseToolbar.vue | 25 +- dbrepo-ui/components/dialogs/EditAccess.vue | 22 +- dbrepo-ui/components/dialogs/EditTuple.vue | 10 +- .../components/dialogs/ViewVisibility.vue | 23 +- dbrepo-ui/components/identifier/Citation.vue | 5 +- dbrepo-ui/components/subset/Builder.vue | 7 +- dbrepo-ui/components/subset/SubsetToolbar.vue | 35 +- dbrepo-ui/components/table/TableHistory.vue | 5 +- dbrepo-ui/components/table/TableImport.vue | 6 +- dbrepo-ui/components/table/TableSchema.vue | 3 + dbrepo-ui/components/table/TableToolbar.vue | 23 +- dbrepo-ui/components/user/UserBadge.vue | 15 +- dbrepo-ui/components/view/ViewToolbar.vue | 72 +--- dbrepo-ui/layouts/default.vue | 8 +- dbrepo-ui/locales/en-US.json | 33 +- .../pages/database/[database_id]/info.vue | 19 +- .../pages/database/[database_id]/settings.vue | 90 ++--- .../[database_id]/subset/[subset_id]/data.vue | 13 +- .../[database_id]/subset/[subset_id]/info.vue | 93 ++---- .../[database_id]/table/[table_id]/data.vue | 13 +- .../[database_id]/table/[table_id]/info.vue | 16 +- .../table/[table_id]/settings.vue | 24 +- .../[database_id]/table/create/dataset.vue | 6 +- .../[database_id]/table/create/schema.vue | 53 +++ .../[database_id]/view/[view_id]/data.vue | 7 +- .../[database_id]/view/[view_id]/schema.vue | 27 +- .../[database_id]/view/[view_id]/settings.vue | 311 ++++++++++++++++++ dbrepo-ui/pages/login.vue | 8 +- dbrepo-ui/pages/signup.vue | 3 + dbrepo-ui/pages/user/info.vue | 11 +- dbrepo-ui/stores/cache.js | 34 +- dbrepo-ui/utils/index.ts | 7 + 52 files changed, 835 insertions(+), 481 deletions(-) create mode 100644 dbrepo-ui/pages/database/[database_id]/view/[view_id]/settings.vue diff --git a/dbrepo-auth-service/init/app.py b/dbrepo-auth-service/init/app.py index 28f88789c8..75f291da10 100644 --- a/dbrepo-auth-service/init/app.py +++ b/dbrepo-auth-service/init/app.py @@ -22,8 +22,8 @@ try: database=os.getenv('METADATA_DB', 'dbrepo')) cursor = conn.cursor() cursor.execute( - "INSERT IGNORE INTO `mdb_users` (`id`, `username`, `email`, `mariadb_password`) VALUES (?, ?, ?, PASSWORD(?))", - (ldap_user_id, system_username, 'some@admin', '1234567890')) + "INSERT IGNORE INTO `mdb_users` (`id`, `username`, `email`, `mariadb_password`, `is_internal`) VALUES (?, ?, LEFT(UUID(), 20), PASSWORD(LEFT(UUID(), 20)), true)", + (ldap_user_id, system_username)) conn.commit() conn.close() except mariadb.Error as e: diff --git a/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/AccessEndpoint.java b/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/AccessEndpoint.java index 5ca740f1cd..7947e87a49 100644 --- a/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/AccessEndpoint.java +++ b/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/AccessEndpoint.java @@ -139,7 +139,7 @@ public class AccessEndpoint extends AbstractEndpoint { access.getType()); final PrivilegedDatabaseDto database = credentialService.getDatabase(databaseId); final PrivilegedUserDto user = credentialService.getUser(userId); - if (database.getAccesses().stream().noneMatch(a -> a.getUser().getId().equals(userId))) { + if (database.getAccesses().stream().noneMatch(a -> a.getHuserid().equals(userId))) { log.error("Failed to update access to user with id {}: no access", userId); throw new NotAllowedException("Failed to update access to user with id " + userId + ": no access"); } diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/service/impl/TableServiceMariaDbImpl.java b/dbrepo-data-service/services/src/main/java/at/tuwien/service/impl/TableServiceMariaDbImpl.java index c8da7fd688..c34f057e01 100644 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/service/impl/TableServiceMariaDbImpl.java +++ b/dbrepo-data-service/services/src/main/java/at/tuwien/service/impl/TableServiceMariaDbImpl.java @@ -170,7 +170,11 @@ public class TableServiceMariaDbImpl extends HibernateConnector implements Table final long start = System.currentTimeMillis(); final PreparedStatement statement = connection.prepareStatement(mariaDbMapper.tableNameToUpdateTableRawQuery(table.getInternalName())); log.trace("prepare with arg 1={}", data.getDescription()); - statement.setString(1, data.getDescription()); + if (data.getDescription() == null) { + statement.setString(1, ""); + } else { + statement.setString(1, data.getDescription()); + } statement.executeUpdate(); log.debug("executed statement in {} ms", System.currentTimeMillis() - start); connection.commit(); diff --git a/dbrepo-metadata-db/1_setup-schema.sql b/dbrepo-metadata-db/1_setup-schema.sql index 92f2b1721e..c9ce89d1be 100644 --- a/dbrepo-metadata-db/1_setup-schema.sql +++ b/dbrepo-metadata-db/1_setup-schema.sql @@ -9,6 +9,7 @@ CREATE TABLE IF NOT EXISTS `mdb_users` email character varying(255) NOT NULL, orcid character varying(255), affiliation character varying(255), + is_internal BOOLEAN NOT NULL DEFAULT FALSE, mariadb_password character varying(255) NOT NULL, theme character varying(255) NOT NULL default ('light'), language character varying(3) NOT NULL default ('en'), diff --git a/dbrepo-metadata-service/entities/src/main/java/at/tuwien/entities/user/User.java b/dbrepo-metadata-service/entities/src/main/java/at/tuwien/entities/user/User.java index c732864969..4075600506 100644 --- a/dbrepo-metadata-service/entities/src/main/java/at/tuwien/entities/user/User.java +++ b/dbrepo-metadata-service/entities/src/main/java/at/tuwien/entities/user/User.java @@ -7,7 +7,6 @@ import lombok.extern.log4j.Log4j2; import org.hibernate.annotations.JdbcTypeCode; import org.springframework.data.jpa.domain.support.AuditingEntityListener; -import java.security.Principal; import java.util.List; import java.util.UUID; @@ -21,6 +20,9 @@ import java.util.UUID; @EqualsAndHashCode @EntityListeners(AuditingEntityListener.class) @Table(name = "mdb_users") +@NamedQueries({ + @NamedQuery(name = "User.findAllInternal", query = "select distinct u from User u where u.isInternal = true") +}) public class User { @Id @@ -61,4 +63,7 @@ public class User { @Column(name = "mariadb_password", nullable = false) private String mariadbPassword; + @Column(name = "is_internal", nullable = false, insertable = false, updatable = false) + private Boolean isInternal; + } diff --git a/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/mapper/MetadataMapper.java b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/mapper/MetadataMapper.java index 61c052933a..8972f04ea4 100644 --- a/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/mapper/MetadataMapper.java +++ b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/mapper/MetadataMapper.java @@ -523,11 +523,17 @@ public interface MetadataMapper { .build(); } - default TableDto customTableToTableDto(Table data) { - return customTableToTableDto(data, true, true, true); + default DatabaseAccess userToWriteAllAccess(Database database, User user) { + return DatabaseAccess.builder() + .type(AccessType.WRITE_ALL) + .hdbid(database.getId()) + .database(database) + .huserid(user.getId()) + .user(user) + .build(); } - default TableDto customTableToTableDto(Table data, Boolean broker, Boolean statistic, Boolean schema) { + default TableDto customTableToTableDto(Table data) { final TableDto table = TableDto.builder() .id(data.getId()) .name(data.getName()) @@ -548,58 +554,52 @@ public interface MetadataMapper { .map(this::identifierToIdentifierDto) .toList())); } - if (broker) { - table.setQueueName(data.getQueueName()); - table.setQueueType("quorum"); - table.setRoutingKey("dbrepo." + data.getTdbid() + "." + data.getId()); + table.setQueueName(data.getQueueName()); + table.setQueueType("quorum"); + table.setRoutingKey("dbrepo." + data.getTdbid() + "." + data.getId()); + table.setAvgRowLength(data.getAvgRowLength()); + table.setMaxDataLength(data.getMaxDataLength()); + table.setDataLength(data.getDataLength()); + table.setNumRows(data.getNumRows()); + table.getConstraints() + .getPrimaryKey() + .forEach(pk -> { + pk.getTable().setDatabaseId(data.getDatabase().getId()); + pk.getColumn().setTableId(data.getId()); + pk.getColumn().setDatabaseId(data.getDatabase().getId()); + }); + table.getConstraints() + .getForeignKeys() + .forEach(fk -> { + fk.getTable().setDatabaseId(table.getTdbid()); + fk.getReferencedTable().setDatabaseId(table.getTdbid()); + fk.getReferences() + .forEach(ref -> { + ref.setForeignKey(foreignKeyDtoToForeignKeyBriefDto(fk)); + ref.getColumn().setTableId(table.getId()); + ref.getColumn().setDatabaseId(table.getTdbid()); + ref.getReferencedColumn().setTableId(fk.getReferencedTable().getId()); + ref.getReferencedColumn().setDatabaseId(table.getTdbid()); + }); + }); + table.getConstraints() + .getUniques() + .forEach(uk -> { + uk.getTable().setDatabaseId(data.getDatabase().getId()); + uk.getColumns() + .forEach(column -> { + column.setTableId(data.getId()); + column.setDatabaseId(data.getDatabase().getId()); + }); + }); + if (data.getConstraints().getChecks() == null || data.getConstraints().getChecks().isEmpty()) { + table.getConstraints().setChecks(new LinkedHashSet<>()); } - if (statistic) { - table.setAvgRowLength(data.getAvgRowLength()); - table.setMaxDataLength(data.getMaxDataLength()); - table.setDataLength(data.getDataLength()); - table.setNumRows(data.getNumRows()); - } - if (schema) { - table.getConstraints() - .getPrimaryKey() - .forEach(pk -> { - pk.getTable().setDatabaseId(data.getDatabase().getId()); - pk.getColumn().setTableId(data.getId()); - pk.getColumn().setDatabaseId(data.getDatabase().getId()); - }); - table.getConstraints() - .getForeignKeys() - .forEach(fk -> { - fk.getTable().setDatabaseId(table.getTdbid()); - fk.getReferencedTable().setDatabaseId(table.getTdbid()); - fk.getReferences() - .forEach(ref -> { - ref.setForeignKey(foreignKeyDtoToForeignKeyBriefDto(fk)); - ref.getColumn().setTableId(table.getId()); - ref.getColumn().setDatabaseId(table.getTdbid()); - ref.getReferencedColumn().setTableId(fk.getReferencedTable().getId()); - ref.getReferencedColumn().setDatabaseId(table.getTdbid()); - }); - }); - table.getConstraints() - .getUniques() - .forEach(uk -> { - uk.getTable().setDatabaseId(data.getDatabase().getId()); - uk.getColumns() - .forEach(column -> { - column.setTableId(data.getId()); - column.setDatabaseId(data.getDatabase().getId()); - }); - }); - if (data.getConstraints().getChecks() == null || data.getConstraints().getChecks().isEmpty()) { - table.getConstraints().setChecks(new LinkedHashSet<>()); - } - if (data.getColumns() != null) { - table.setColumns(new LinkedList<>(data.getColumns() - .stream() - .map(this::tableColumnToColumnDto) - .toList())); - } + if (data.getColumns() != null) { + table.setColumns(new LinkedList<>(data.getColumns() + .stream() + .map(this::tableColumnToColumnDto) + .toList())); } return table; } @@ -900,6 +900,7 @@ public interface MetadataMapper { if (data.getAccesses() != null) { database.setAccesses(new LinkedList<>(data.getAccesses() .stream() + .filter(a -> !a.getUser().getIsInternal()) .map(this::databaseAccessToDatabaseAccessDto) .toList())); } diff --git a/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/repository/UserRepository.java b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/repository/UserRepository.java index f21596858a..7415fb422c 100644 --- a/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/repository/UserRepository.java +++ b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/repository/UserRepository.java @@ -4,6 +4,7 @@ import at.tuwien.entities.user.User; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.List; import java.util.Optional; import java.util.UUID; @@ -12,6 +13,8 @@ public interface UserRepository extends JpaRepository<User, UUID> { Optional<User> findByUsername(String username); + List<User> findAllInternal(); + boolean existsByUsername(String username); boolean existsByEmail(String email); diff --git a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/AccessEndpoint.java b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/AccessEndpoint.java index 2a0bd17cbc..7c194bd979 100644 --- a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/AccessEndpoint.java +++ b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/AccessEndpoint.java @@ -28,6 +28,7 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.*; import java.security.Principal; +import java.util.List; import java.util.UUID; @Log4j2 @@ -164,7 +165,15 @@ public class AccessEndpoint extends AbstractEndpoint { log.error("Failed to update access: not owner"); throw new NotAllowedException("Failed to update access: not owner"); } + if (database.getOwner().getId().equals(userId)) { + log.error("Failed to update access: the owner must have write-all access"); + throw new NotAllowedException("Failed to update access: the owner must have write-all access"); + } final User user = userService.findById(userId); + if (user.getIsInternal()) { + log.error("Failed to update access: the internal user must have write-all access"); + throw new NotAllowedException("Failed to update access: the internal user must have write-all access"); + } accessService.find(database, user); accessService.update(database, user, data.getType()); return ResponseEntity.accepted() @@ -261,7 +270,15 @@ public class AccessEndpoint extends AbstractEndpoint { log.error("Failed to revoke access: not owner"); throw new NotAllowedException("Failed to revoke access: not owner"); } + if (database.getOwner().getId().equals(userId)) { + log.error("Failed to revoke access: the owner must have write-all access"); + throw new NotAllowedException("Failed to revoke access: the owner must have write-all access"); + } final User user = userService.findById(userId); + if (user.getIsInternal()) { + log.error("Failed to revoke access: the internal user must have write-all access"); + throw new NotAllowedException("Failed to revoke access: the internal user must have write-all access"); + } accessService.find(database, user); accessService.delete(database, user); return ResponseEntity.accepted() diff --git a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/DatabaseEndpoint.java b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/DatabaseEndpoint.java index b2805f8d77..9b3379523c 100644 --- a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/DatabaseEndpoint.java +++ b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/DatabaseEndpoint.java @@ -170,7 +170,7 @@ public class DatabaseEndpoint extends AbstractEndpoint { final User caller = userService.findById(getId(principal)); return ResponseEntity.status(HttpStatus.CREATED) .body(databaseMapper.customDatabaseToDatabaseDto( - databaseService.create(container, data, caller))); + databaseService.create(container, data, caller, userService.findAllInternalUsers()))); } @PutMapping("/{databaseId}/metadata/table") @@ -527,8 +527,8 @@ public class DatabaseEndpoint extends AbstractEndpoint { .stream() .filter(v -> v.getIsPublic() || optional.isPresent()) .toList()); - if (!database.getOwner().getId().equals(getId(principal))) { - log.trace("authenticated user is not owner: remove access list"); + if (!isSystem(principal) && !database.getOwner().getId().equals(getId(principal))) { + log.trace("authenticated user {} is not owner: remove access list", principal.getName()); database.setAccesses(List.of()); } } else { diff --git a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/TableEndpoint.java b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/TableEndpoint.java index d1f40b29ba..21a1659923 100644 --- a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/TableEndpoint.java +++ b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/TableEndpoint.java @@ -483,20 +483,20 @@ public class TableEndpoint extends AbstractEndpoint { endpointValidator.validateOnlyPrivateDataAccess(database, principal); endpointValidator.validateOnlyPrivateDataHasRole(database, principal, "find-table"); final Table table = tableService.findById(database, tableId); - boolean hasAccess = isSystem(principal); boolean isOwner = false; - try { - if (principal != null) { + if (principal != null) { + isOwner = table.getOwner().getId().equals(getId(principal)); + try { accessService.find(table.getDatabase(), userService.findById(getId(principal))); - hasAccess = true; - isOwner = table.getOwner().getId().equals(getId(principal)); + } catch (UserNotFoundException | AccessNotFoundException e) { + /* ignore */ } - } catch (UserNotFoundException | AccessNotFoundException e) { - /* ignore */ } - final boolean includeSchema = isSystem(principal) || isOwner || table.getIsSchemaPublic(); - log.trace("user has access: {}", hasAccess); - log.trace("include schema in mapping: {}", includeSchema); + if (!table.getIsSchemaPublic() && !isOwner && !isSystem(principal)) { + log.debug("remove schema from table: {}.{}", database.getInternalName(), table.getInternalName()); + table.setColumns(List.of()); + table.setConstraints(null); + } final HttpHeaders headers = new HttpHeaders(); if (isSystem(principal)) { headers.set("X-Username", table.getDatabase().getContainer().getPrivilegedUsername()); @@ -510,8 +510,7 @@ public class TableEndpoint extends AbstractEndpoint { } return ResponseEntity.ok() .headers(headers) - .body(metadataMapper.customTableToTableDto(table, hasAccess, table.getDatabase().getIsPublic(), - includeSchema)); + .body(metadataMapper.customTableToTableDto(table)); } @DeleteMapping("/{tableId}") diff --git a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/UserEndpoint.java b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/UserEndpoint.java index 1b1947253f..4f49904baa 100644 --- a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/UserEndpoint.java +++ b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/UserEndpoint.java @@ -63,7 +63,7 @@ public class UserEndpoint extends AbstractEndpoint { @Transactional(readOnly = true) @Observed(name = "dbrepo_users_list") @Operation(summary = "List users", - description = "Lists users known to the metadata database.") + description = "Lists users known to the metadata database. Internal users are omitted from the result list. If the optional query parameter `username` is present, the result list can be filtered by matching this exact username.") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "List users", @@ -71,18 +71,23 @@ public class UserEndpoint extends AbstractEndpoint { mediaType = "application/json", array = @ArraySchema(schema = @Schema(implementation = UserBriefDto.class)))}), }) - public ResponseEntity<List<UserBriefDto>> findAll(@RequestParam(required = false) String username) { + public ResponseEntity<List<UserBriefDto>> findAll(@RequestParam(required = false) String username) + throws UserNotFoundException { log.debug("endpoint find all users, username={}", username); if (username == null) { return ResponseEntity.ok(userService.findAll() .stream() + .filter(user -> !user.getIsInternal()) .map(userMapper::userToUserBriefDto) .toList()); } + log.trace("filter by username: {}", username); try { - log.trace("filter by username: {}", username); - return ResponseEntity.ok(List.of(userMapper.userToUserBriefDto( - userService.findByUsername(username)))); + final User user = userService.findByUsername(username); + if (user.getIsInternal()) { + return ResponseEntity.ok(List.of()); + } + return ResponseEntity.ok(List.of(userMapper.userToUserBriefDto(user))); } catch (UserNotFoundException e) { log.trace("filter by username {} failed: return empty list", username); return ResponseEntity.ok(List.of()); @@ -149,7 +154,7 @@ public class UserEndpoint extends AbstractEndpoint { @PostMapping("/token") @Observed(name = "dbrepo_user_token") @Operation(summary = "Create token", - description = "Creates a user token via the auth service.") + description = "Creates a user token via the Auth Service.") @ApiResponses(value = { @ApiResponse(responseCode = "202", description = "Obtained user token", @@ -253,7 +258,7 @@ public class UserEndpoint extends AbstractEndpoint { @PreAuthorize("isAuthenticated()") @Observed(name = "dbrepo_user_find") @Operation(summary = "Get user", - description = "Gets user with id from the metadata database. Requires authentication.", + description = "Gets own user information from the metadata database. Requires authentication. Foreign user information can only be obtained if additional role `find-foreign-user` is present. Finding information about internal users results in a 404 error.", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "200", @@ -282,6 +287,9 @@ public class UserEndpoint extends AbstractEndpoint { log.error("Failed to find user: foreign user"); throw new NotAllowedException("Failed to find user: foreign user"); } + if (user.getIsInternal()) { + throw new UserNotFoundException("Failed to find user with username: " + user.getUsername()); + } final HttpHeaders headers = new HttpHeaders(); if (isSystem(principal)) { headers.set("X-Username", user.getUsername()); diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/DatabaseEndpointUnitTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/DatabaseEndpointUnitTest.java index 9a8c031e11..f276ad2c84 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/DatabaseEndpointUnitTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/DatabaseEndpointUnitTest.java @@ -110,7 +110,7 @@ public class DatabaseEndpointUnitTest extends AbstractUnitTest { /* mock */ when(containerService.find(CONTAINER_1_ID)) .thenReturn(CONTAINER_1); - when(databaseService.create(CONTAINER_1, request, USER_1)) + when(databaseService.create(CONTAINER_1, request, USER_1, List.of(USER_LOCAL))) .thenReturn(DATABASE_1); doNothing() .when(messageQueueService) diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/DatabaseServiceUnitTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/DatabaseServiceUnitTest.java index 7f84b3b09c..d7f7682398 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/DatabaseServiceUnitTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/DatabaseServiceUnitTest.java @@ -535,7 +535,7 @@ public class DatabaseServiceUnitTest extends AbstractUnitTest { .thenReturn(DATABASE_1); /* test */ - final Database response = databaseService.create(CONTAINER_1, DATABASE_1_CREATE, USER_1); + final Database response = databaseService.create(CONTAINER_1, DATABASE_1_CREATE, USER_1, List.of(USER_LOCAL)); assertTrue(response.getInternalName().startsWith(DATABASE_1_INTERNALNAME)); assertNotNull(response.getContainer()); assertNotNull(response.getTables()); diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/DatabaseService.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/DatabaseService.java index 416025270d..93abb7304c 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/DatabaseService.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/DatabaseService.java @@ -54,12 +54,13 @@ public interface DatabaseService { * @param container The container. * @param createDto The metadata. * @param user The user. + * @param internalUsers The list of internal users. * @return The database, if successful. * @throws UserNotFoundException If the container/user was not found in the metadata database. * @throws DataServiceException If the data service returned non-successfully. * @throws DataServiceConnectionException If failing to connect to the data service/search service. */ - Database create(Container container, DatabaseCreateDto createDto, User user) throws UserNotFoundException, + Database create(Container container, DatabaseCreateDto createDto, User user, List<User> internalUsers) throws UserNotFoundException, ContainerNotFoundException, DataServiceException, DataServiceConnectionException, DatabaseNotFoundException, SearchServiceException, SearchServiceConnectionException; diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/UserService.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/UserService.java index 9957100710..6416da9b80 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/UserService.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/UserService.java @@ -29,6 +29,8 @@ public interface UserService { */ User findByUsername(String username) throws UserNotFoundException; + List<User> findAllInternalUsers(); + /** * Finds a specific user in the metadata database by given id. * diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/AccessServiceImpl.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/AccessServiceImpl.java index a0f45fb34f..1c302c2068 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/AccessServiceImpl.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/AccessServiceImpl.java @@ -51,7 +51,7 @@ public class AccessServiceImpl implements AccessService { public DatabaseAccess find(Database database, User user) throws AccessNotFoundException { final Optional<DatabaseAccess> optional = database.getAccesses() .stream() - .filter(a -> a.getUser().getUsername().equals(user.getUsername())) + .filter(a -> a.getHuserid().equals(user.getId())) .findFirst(); if (optional.isEmpty()) { log.error("Failed to find database access for database with id: {}", database.getId()); @@ -94,7 +94,7 @@ public class AccessServiceImpl implements AccessService { /* update in metadata database */ final Optional<DatabaseAccess> optional = database.getAccesses() .stream() - .filter(a -> a.getUser().getId().equals(user.getId())) + .filter(a -> a.getHuserid().equals(user.getId())) .findFirst(); if (optional.isEmpty()) { log.error("Failed to update access for user with id: {}", user.getId()); @@ -116,7 +116,8 @@ public class AccessServiceImpl implements AccessService { /* delete in data database */ dataServiceGateway.deleteAccess(database.getId(), user.getId()); /* delete in metadata database */ - database.getAccesses().remove(find(database, user)); + database.getAccesses() + .remove(find(database, user)); databaseRepository.save(database); /* update in search service */ searchServiceGateway.update(databaseService.findById(database.getId())); diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/DatabaseServiceImpl.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/DatabaseServiceImpl.java index 828a1dec73..11ba1c0319 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/DatabaseServiceImpl.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/DatabaseServiceImpl.java @@ -8,7 +8,9 @@ import at.tuwien.api.database.internal.CreateDatabaseDto; import at.tuwien.api.database.table.TableDto; import at.tuwien.api.user.internal.UpdateUserPasswordDto; import at.tuwien.entities.container.Container; -import at.tuwien.entities.database.*; +import at.tuwien.entities.database.Database; +import at.tuwien.entities.database.View; +import at.tuwien.entities.database.ViewColumn; import at.tuwien.entities.database.table.Table; import at.tuwien.entities.database.table.columns.TableColumn; import at.tuwien.entities.database.table.constraints.foreignKey.ForeignKey; @@ -89,9 +91,9 @@ public class DatabaseServiceImpl implements DatabaseService { @Override @Transactional - public Database create(Container container, DatabaseCreateDto data, User user) throws UserNotFoundException, - ContainerNotFoundException, DataServiceException, DataServiceConnectionException, DatabaseNotFoundException, - SearchServiceException, SearchServiceConnectionException { + public Database create(Container container, DatabaseCreateDto data, User user, List<User> internalUsers) + throws UserNotFoundException, ContainerNotFoundException, DataServiceException, SearchServiceException, + DataServiceConnectionException, DatabaseNotFoundException, SearchServiceConnectionException { final Database entity = Database.builder() .isPublic(data.getIsPublic()) .isSchemaPublic(data.getIsSchemaPublic()) @@ -123,13 +125,11 @@ public class DatabaseServiceImpl implements DatabaseService { /* create in metadata database */ final Database entity1 = databaseRepository.save(entity); entity1.getAccesses() - .add(DatabaseAccess.builder() - .type(AccessType.WRITE_ALL) - .hdbid(entity1.getId()) - .database(entity1) - .huserid(user.getId()) - .user(user) - .build()); + .add(metadataMapper.userToWriteAllAccess(entity1, user)); + entity1.getAccesses() + .addAll(internalUsers.stream() + .map(internalUser -> metadataMapper.userToWriteAllAccess(entity1, internalUser)) + .toList()); final Database database = databaseRepository.save(entity1); /* create in search service */ searchServiceGateway.update(database); diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/UserServiceImpl.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/UserServiceImpl.java index f18d3c1da6..aa75200cc9 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/UserServiceImpl.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/UserServiceImpl.java @@ -3,6 +3,7 @@ package at.tuwien.service.impl; import at.tuwien.api.auth.SignupRequestDto; import at.tuwien.api.user.UserPasswordDto; import at.tuwien.api.user.UserUpdateDto; +import at.tuwien.config.KeycloakConfig; import at.tuwien.entities.user.User; import at.tuwien.exception.EmailExistsException; import at.tuwien.exception.UserExistsException; @@ -23,10 +24,12 @@ import java.util.UUID; @Service public class UserServiceImpl implements UserService { + private final KeycloakConfig keycloakConfig; private final UserRepository userRepository; @Autowired - public UserServiceImpl(UserRepository userRepository) { + public UserServiceImpl(KeycloakConfig keycloakConfig, UserRepository userRepository) { + this.keycloakConfig = keycloakConfig; this.userRepository = userRepository; } @@ -39,18 +42,23 @@ public class UserServiceImpl implements UserService { public User findByUsername(String username) throws UserNotFoundException { final Optional<User> optional = userRepository.findByUsername(username); if (optional.isEmpty()) { - log.error("Failed to find user with username {}", username); - throw new UserNotFoundException("Failed to find user with username " + username); + log.error("Failed to find user with username: {}", username); + throw new UserNotFoundException("Failed to find user with username: " + username); } return optional.get(); } + @Override + public List<User> findAllInternalUsers() { + return userRepository.findAllInternal(); + } + @Override public User findById(UUID id) throws UserNotFoundException { final Optional<User> optional = userRepository.findById(id); if (optional.isEmpty()) { - log.error("Failed to find user with id {}", id); - throw new UserNotFoundException("Failed to find user with id " + id); + log.error("Failed to find user with id: {}", id); + throw new UserNotFoundException("Failed to find user with id: " + id); } return optional.get(); } @@ -68,7 +76,7 @@ public class UserServiceImpl implements UserService { .build(); /* create at metadata database */ final User user = userRepository.save(entity); - log.info("Created user with id {}", user.getId()); + log.info("Created user with id: {}", user.getId()); return user; } @@ -82,7 +90,7 @@ public class UserServiceImpl implements UserService { user.setLanguage(data.getLanguage()); /* create at metadata database */ user = userRepository.save(user); - log.info("Modified user with id {}", user.getId()); + log.info("Modified user with id: {}", user.getId()); return user; } @@ -91,7 +99,7 @@ public class UserServiceImpl implements UserService { user.setMariadbPassword(getMariaDbPassword(data.getPassword())); /* update at metadata database */ userRepository.save(user); - log.info("Updated password of user with id {}", user.getId()); + log.info("Updated password of user with id: {}", user.getId()); } @Override diff --git a/dbrepo-ui/components/ResourceStatus.vue b/dbrepo-ui/components/ResourceStatus.vue index 50c7089999..9ff310ce1b 100644 --- a/dbrepo-ui/components/ResourceStatus.vue +++ b/dbrepo-ui/components/ResourceStatus.vue @@ -3,7 +3,7 @@ v-if="mode"> <v-chip v-if="!inline" - size="small" + :size="size" :color="color" variant="outlined"> {{ status }} @@ -26,6 +26,11 @@ export default { default: () => { return false } + }, + size: { + default: () => { + return 'small' + } } }, computed: { diff --git a/dbrepo-ui/components/database/DatabaseCreate.vue b/dbrepo-ui/components/database/DatabaseCreate.vue index 1797dbdad3..07fd9d34ea 100644 --- a/dbrepo-ui/components/database/DatabaseCreate.vue +++ b/dbrepo-ui/components/database/DatabaseCreate.vue @@ -17,7 +17,7 @@ <v-row dense> <v-col> <v-text-field - v-model="payload.name" + v-model="name" name="database" :variant="inputVariant" :label="$t('pages.database.subpages.create.name.label')" @@ -96,6 +96,8 @@ export default { loading: false, loadingContainers: false, engine: null, + draft: true, + name: null, engines: [], visibilityOptions: [ { @@ -108,13 +110,7 @@ export default { hint: this.$t('pages.database.subpages.create.visibility.private.hint'), value: false } - ], - draft: true, - payload: { - name: null, - is_public: true, - is_schema_public: true, - } + ] } }, computed: { @@ -158,16 +154,16 @@ export default { .catch(({code}) => { this.loadingContainers = false const toast = useToastInstance() + if (typeof code !== 'string') { + return + } toast.error(this.$t(code)) }) }, create () { this.loading = true - this.payload.container_id = this.engine.id - this.payload.is_public = this.mode.value - this.payload.is_schema_public = this.mode.value const databaseService = useDatabaseService() - databaseService.create(this.payload) + databaseService.create({ name: this.name, container_id: this.engine.id, is_public: !this.draft, is_schema_public: !this.draft }) .then(async (database) => { await this.$router.push(`/database/${database.id}/info`) this.loading = false diff --git a/dbrepo-ui/components/database/DatabaseToolbar.vue b/dbrepo-ui/components/database/DatabaseToolbar.vue index 3f7412d5cb..59b0bc6db6 100644 --- a/dbrepo-ui/components/database/DatabaseToolbar.vue +++ b/dbrepo-ui/components/database/DatabaseToolbar.vue @@ -8,50 +8,51 @@ type="subtitle" width="200" /> <span - v-if="database && $vuetify.display.lgAndUp"> + class="mr-2" + v-if="database && $vuetify.display.mdAndUp"> {{ database.name }} </span> <ResourceStatus - class="ml-2" + :size="$vuetify.display.mdAndUp ? 'small' : 'default'" :resource="database" /> </v-toolbar-title> <v-spacer /> <v-btn v-if="false" - :prepend-icon="$vuetify.display.lgAndUp ? 'mdi-chart-timeline-variant-shimmer' : null" + :prepend-icon="$vuetify.display.mdAndUp ? 'mdi-chart-timeline-variant-shimmer' : null" color="tertiary" :variant="buttonVariant" - :text="$t('toolbars.database.dashboard.permanent') + ($vuetify.display.lgAndUp ? ' ' + $t('toolbars.database.dashboard.xl') : '')" /> + :text="$t('toolbars.database.dashboard.permanent') + ($vuetify.display.mdAndUp ? ' ' + $t('toolbars.database.dashboard.xl') : '')" /> <v-btn v-if="canCreateTable" - :prepend-icon="$vuetify.display.lgAndUp ? 'mdi-table-large-plus' : null" + :prepend-icon="$vuetify.display.mdAndUp ? 'mdi-table-large-plus' : null" color="secondary" variant="flat" - :text="($vuetify.display.lgAndUp ? $t('toolbars.database.create-table.xl') + ' ' : '') + $t('toolbars.database.create-table.permanent')" + :text="($vuetify.display.mdAndUp ? $t('toolbars.database.create-table.xl') + ' ' : '') + $t('toolbars.database.create-table.permanent')" class="mr-2" :to="`/database/${$route.params.database_id}/table/create/dataset`" /> <v-btn v-if="canCreateSubset" - :prepend-icon="$vuetify.display.lgAndUp ? 'mdi-wrench' : null" + :prepend-icon="$vuetify.display.mdAndUp ? 'mdi-wrench' : null" color="secondary" variant="flat" - :text="($vuetify.display.lgAndUp ? $t('toolbars.database.create-subset.xl') + ' ' : '') + $t('toolbars.database.create-subset.permanent')" + :text="($vuetify.display.mdAndUp ? $t('toolbars.database.create-subset.xl') + ' ' : '') + $t('toolbars.database.create-subset.permanent')" class="mr-2 white--text" :to="`/database/${$route.params.database_id}/subset/create`" /> <v-btn v-if="canCreateView" - :prepend-icon="$vuetify.display.lgAndUp ? 'mdi-view-carousel-outline' : null" + :prepend-icon="$vuetify.display.mdAndUp ? 'mdi-view-carousel-outline' : null" color="secondary" variant="flat" - :text="($vuetify.display.lgAndUp ? $t('toolbars.database.create-view.xl') + ' ' : '') + $t('toolbars.database.create-view.permanent')" + :text="($vuetify.display.mdAndUp ? $t('toolbars.database.create-view.xl') + ' ' : '') + $t('toolbars.database.create-view.permanent')" class="mr-2 white--text" :to="`/database/${$route.params.database_id}/view/create`" /> <v-btn v-if="canCreateIdentifier" - :prepend-icon="$vuetify.display.lgAndUp ? 'mdi-identifier' : null" + :prepend-icon="$vuetify.display.mdAndUp ? 'mdi-identifier' : null" color="primary" variant="flat" - :text="($vuetify.display.lgAndUp ? $t('toolbars.database.create-pid.xl') + ' ' : '') + $t('toolbars.database.create-pid.permanent')" + :text="($vuetify.display.mdAndUp ? $t('toolbars.database.create-pid.xl') + ' ' : '') + $t('toolbars.database.create-pid.permanent')" class="mr-2" :to="`/database/${$route.params.database_id}/persist`" /> <template v-slot:extension> diff --git a/dbrepo-ui/components/dialogs/EditAccess.vue b/dbrepo-ui/components/dialogs/EditAccess.vue index 039b1c40e8..b3bd8dbf96 100644 --- a/dbrepo-ui/components/dialogs/EditAccess.vue +++ b/dbrepo-ui/components/dialogs/EditAccess.vue @@ -172,11 +172,14 @@ export default { accessService.remove(this.$route.params.database_id, this.localUserId) .then(() => { const toast = useToastInstance() - toast.success(this.$t('success.access.revoked')) + toast.success(this.$t('success.access.revoked', { access: this.modify.type })) this.$emit('close-dialog', { success: true }) }) .catch(({code, message}) => { const toast = useToastInstance() + if (typeof code !== 'string') { + return + } toast.error(message) }) .finally(() => { @@ -185,14 +188,17 @@ export default { }, modifyAccess () { const accessService = useAccessService() - accessService.modify(this.$route.params.database_id, this.localUserId, this.modify) + accessService.update(this.$route.params.database_id, this.localUserId, this.modify) .then(() => { const toast = useToastInstance() - toast.success(this.$t('success.access.modified')) + toast.success(this.$t('success.access.modified', { access: this.modify.type })) this.$emit('close-dialog', { success: true }) }) .catch(({code, message}) => { const toast = useToastInstance() + if (typeof code !== 'string') { + return + } toast.error(message) }) .finally(() => { @@ -204,11 +210,14 @@ export default { accessService.create(this.$route.params.database_id, this.localUserId, this.modify) .then(() => { const toast = useToastInstance() - toast.success(this.$t('success.access.created')) + toast.success(this.$t('success.access.created', { access: this.modify.type })) this.$emit('close-dialog', { success: true }) }) .catch(({code, message}) => { const toast = useToastInstance() + if (typeof code !== 'string') { + return + } toast.error(message) }) .finally(() => { @@ -220,10 +229,13 @@ export default { const userService = useUserService() userService.findAll() .then((users) => { - this.users = users.filter(u => u.username !== this.database.creator.username) + this.users = users.filter(u => u.username !== this.database.owner.username) }) .catch(({code}) => { const toast = useToastInstance() + if (typeof code !== 'string') { + return + } toast.error(this.$t(code)) }) .finally(() => { diff --git a/dbrepo-ui/components/dialogs/EditTuple.vue b/dbrepo-ui/components/dialogs/EditTuple.vue index ea0bfb3c5b..8935043316 100644 --- a/dbrepo-ui/components/dialogs/EditTuple.vue +++ b/dbrepo-ui/components/dialogs/EditTuple.vue @@ -462,9 +462,12 @@ export default { this.loading = false }) .catch(({message}) => { + this.loading = false const toast = useToastInstance() + if (typeof code !== 'string') { + return + } toast.error(message) - this.loading = false }) .finally(() => { this.loading = false @@ -492,9 +495,12 @@ export default { this.loading = false }) .catch(({message}) => { + this.loading = false const toast = useToastInstance() + if (typeof code !== 'string') { + return + } toast.error(message) - this.loading = false }) .finally(() => { this.loading = false diff --git a/dbrepo-ui/components/dialogs/ViewVisibility.vue b/dbrepo-ui/components/dialogs/ViewVisibility.vue index d8cc01790e..7d11311667 100644 --- a/dbrepo-ui/components/dialogs/ViewVisibility.vue +++ b/dbrepo-ui/components/dialogs/ViewVisibility.vue @@ -22,7 +22,7 @@ v => v !== null || $t('validation.required') ]" :label="$t('pages.database.resource.data.label')" - :hint="$t('pages.database.resource.data.hint')" /> + :hint="$t('pages.database.resource.data.hint', { resource: 'view' })" /> </v-col> <v-col md="6"> @@ -36,7 +36,7 @@ v => v !== null || $t('validation.required') ]" :label="$t('pages.database.resource.schema.label')" - :hint="$t('pages.database.resource.schema.hint')" /> + :hint="$t('pages.database.resource.schema.hint', { resource: 'view', schema: 'columns' })" /> </v-col> </v-row> </v-card-text> @@ -126,25 +126,6 @@ export default { cancel () { this.$emit('close', { success: false }) }, - updateVisibility () { - this.loading = true - const viewService = useViewService() - viewService.update(this.$route.params.database_id, this.$route.params.view_id, this.modify) - .then(() => { - this.loading = false - const toast = useToastInstance() - toast.success(this.$t('success.view.modified')) - this.$emit('close', { success: true }) - }) - .catch(({code, message}) => { - this.loading = false - const toast = useToastInstance() - toast.error(message) - }) - .finally(() => { - this.loading = false - }) - } } } </script> diff --git a/dbrepo-ui/components/identifier/Citation.vue b/dbrepo-ui/components/identifier/Citation.vue index 7cd99194b0..5722351f0a 100644 --- a/dbrepo-ui/components/identifier/Citation.vue +++ b/dbrepo-ui/components/identifier/Citation.vue @@ -68,9 +68,12 @@ export default { this.loading = false }) .catch(({code, message}) => { + this.loading = false const toast = useToastInstance() + if (typeof code !== 'string') { + return + } toast.error(this.$t(`${code}: ${message}`)) - this.loading = false }) } } diff --git a/dbrepo-ui/components/subset/Builder.vue b/dbrepo-ui/components/subset/Builder.vue index 9b85162457..2895476a59 100644 --- a/dbrepo-ui/components/subset/Builder.vue +++ b/dbrepo-ui/components/subset/Builder.vue @@ -92,7 +92,7 @@ v => !!v || $t('validation.required') ]" :label="$t('pages.database.resource.schema.label')" - :hint="$t('pages.database.resource.schema.hint')" /> + :hint="$t('pages.database.resource.schema.hint', { resource: 'subset', schema: 'query' })" /> </v-col> </v-row> <v-window @@ -489,9 +489,12 @@ export default { this.loadingColumns = false }) .catch(({code}) => { + this.loadingColumns = false const toast = useToastInstance() + if (typeof code !== 'string') { + return + } toast.error(this.$t(code)) - this.loadingColumns = false }) }, validViewName (name) { diff --git a/dbrepo-ui/components/subset/SubsetToolbar.vue b/dbrepo-ui/components/subset/SubsetToolbar.vue index 0fc5be7c88..4d1f8d7c4a 100644 --- a/dbrepo-ui/components/subset/SubsetToolbar.vue +++ b/dbrepo-ui/components/subset/SubsetToolbar.vue @@ -3,7 +3,6 @@ <v-toolbar flat> <v-btn - class="mr-2" variant="plain" size="small" icon="mdi-arrow-left" @@ -17,7 +16,7 @@ :loading="loadingSave" color="secondary" variant="flat" - class="mb-1 ml-2" + class="mr-2" :prepend-icon="$vuetify.display.lgAndUp ? 'mdi-star' : null" :text="$t('toolbars.subset.save.permanent')" @click.stop="save" /> @@ -26,15 +25,15 @@ :loading="loadingSave" color="warning" variant="flat" - class="mb-1 ml-2" + class="mr-2" :prepend-icon="$vuetify.display.lgAndUp ? 'mdi-star-off' : null" :text="$t('toolbars.subset.unsave.permanent')" @click.stop="forget" /> <v-btn v-if="canGetPid" - class="mb-1 ml-2" color="primary" variant="flat" + class="mr-2" :prepend-icon="$vuetify.display.lgAndUp ? 'mdi-content-save-outline' : null" :disabled="!executionUTC" :to="`/database/${$route.params.database_id}/subset/${$route.params.subset_id}/persist`"> @@ -73,7 +72,6 @@ export default { loading: false, loadingSave: false, downloadLoading: false, - subset: null, userStore: useUserStore(), cacheStore: useCacheStore() } @@ -97,11 +95,14 @@ export default { roles () { return this.userStore.getRoles }, + subset () { + return this.cacheStore.getSubset + }, identifiers () { - if (!this.database || !this.database.subsets || this.database.subsets.length === 0) { + if (!this.subset) { return [] } - return this.database.subsets.filter(s => s.query_id === Number(this.$route.params.subset_id)) + return this.subset.identifiers }, canViewData () { if (!this.database) { @@ -171,12 +172,6 @@ export default { return this.$vuetify.theme.global.name.toLowerCase().endsWith('contrast') ? runtimeConfig.public.variant.button.contrast : runtimeConfig.public.variant.button.normal } }, - mounted () { - /* load subset metadata */ - if (!this.subset) { - this.loadSubset() - } - }, methods: { save () { this.loadingSave = true @@ -206,20 +201,6 @@ export default { .finally(() => { this.loadingSave = false }) - }, - loadSubset () { - this.loading = true - const queryService = useQueryService() - queryService.findOne(this.$route.params.database_id, this.$route.params.subset_id) - .then((subset) => { - this.subset = subset - }) - .catch(() => { - this.loading = false - }) - .finally(() => { - this.loading = false - }) } } } diff --git a/dbrepo-ui/components/table/TableHistory.vue b/dbrepo-ui/components/table/TableHistory.vue index 34d45248e7..ccc270c46c 100644 --- a/dbrepo-ui/components/table/TableHistory.vue +++ b/dbrepo-ui/components/table/TableHistory.vue @@ -173,9 +173,12 @@ export default { } }) .catch(({message}) => { + this.loading = false const toast = useToastInstance() + if (typeof code !== 'string') { + return + } toast.error(message) - this.loading = false }) } } diff --git a/dbrepo-ui/components/table/TableImport.vue b/dbrepo-ui/components/table/TableImport.vue index c0e4d6f934..580839974e 100644 --- a/dbrepo-ui/components/table/TableImport.vue +++ b/dbrepo-ui/components/table/TableImport.vue @@ -434,10 +434,12 @@ export default { this.loadingImport = false }) .catch(({code, message}) => { + this.loadingImport = false const toast = useToastInstance() - console.error(code, message) + if (typeof code !== 'string') { + return + } toast.error(`${this.$t(code)}: ${message}`) - this.loadingImport = false }) .finally(() => { this.loadingImport = false diff --git a/dbrepo-ui/components/table/TableSchema.vue b/dbrepo-ui/components/table/TableSchema.vue index e9cb03c617..24c4fcd682 100644 --- a/dbrepo-ui/components/table/TableSchema.vue +++ b/dbrepo-ui/components/table/TableSchema.vue @@ -281,6 +281,9 @@ export default { .catch(({code}) => { this.loadingColumnTypes = false const toast = useToastInstance() + if (typeof code !== 'string') { + return + } toast.error(this.$t(code)) }) }, diff --git a/dbrepo-ui/components/table/TableToolbar.vue b/dbrepo-ui/components/table/TableToolbar.vue index e2f1ad4b23..b91ce9a6ee 100644 --- a/dbrepo-ui/components/table/TableToolbar.vue +++ b/dbrepo-ui/components/table/TableToolbar.vue @@ -9,48 +9,49 @@ <v-toolbar-title v-if="table"> <v-skeleton-loader - v-if="!table && $vuetify.display.lgAndUp" + v-if="!table && $vuetify.display.mdAndUp" type="subtitle" width="200" /> <span - v-if="table && $vuetify.display.lgAndUp"> + class="mr-2" + v-if="table && $vuetify.display.mdAndUp"> {{ table.name }} </span> <ResourceStatus - class="ml-2" + :size="$vuetify.display.mdAndUp ? 'small' : 'default'" :resource="table" /> </v-toolbar-title> <v-spacer /> <v-btn v-if="canImportCsv" - :prepend-icon="$vuetify.display.lgAndUp ? 'mdi-cloud-upload' : null" + :prepend-icon="$vuetify.display.mdAndUp ? 'mdi-cloud-upload' : null" color="tertiary" :variant="buttonVariant" - :text="$t('toolbars.database.import-csv.permanent') + ($vuetify.display.lgAndUp ? ' ' + $t('toolbars.database.import-csv.xl') : '')" + :text="$t('toolbars.database.import-csv.permanent') + ($vuetify.display.mdAndUp ? ' ' + $t('toolbars.database.import-csv.xl') : '')" class="mr-2" :to="`/database/${$route.params.database_id}/table/${$route.params.table_id}/import`" /> <v-btn v-if="canExecuteQuery" - :prepend-icon="$vuetify.display.lgAndUp ? 'mdi-wrench' : null" + :prepend-icon="$vuetify.display.mdAndUp ? 'mdi-wrench' : null" color="secondary" variant="flat" - :text="($vuetify.display.lgAndUp ? $t('toolbars.database.create-subset.xl') + ' ' : '') + $t('toolbars.database.create-subset.permanent')" + :text="($vuetify.display.mdAndUp ? $t('toolbars.database.create-subset.xl') + ' ' : '') + $t('toolbars.database.create-subset.permanent')" class="mr-2" :to="`/database/${$route.params.database_id}/subset/create?tid=${$route.params.table_id}`" /> <v-btn v-if="canCreateView" - :prepend-icon="$vuetify.display.lgAndUp ? 'mdi-view-carousel' : null" + :prepend-icon="$vuetify.display.mdAndUp ? 'mdi-view-carousel' : null" color="secondary" variant="flat" - :text="($vuetify.display.lgAndUp ? $t('toolbars.database.create-view.xl') + ' ' : '') + $t('toolbars.database.create-view.permanent')" + :text="($vuetify.display.mdAndUp ? $t('toolbars.database.create-view.xl') + ' ' : '') + $t('toolbars.database.create-view.permanent')" class="mr-2" :to="`/database/${$route.params.database_id}/view/create?tid=${$route.params.table_id}`" /> <v-btn v-if="canGetPid" - :prepend-icon="$vuetify.display.lgAndUp ? 'mdi-content-save-outline' : null" + :prepend-icon="$vuetify.display.mdAndUp ? 'mdi-content-save-outline' : null" color="primary" variant="flat" - :text="($vuetify.display.lgAndUp ? 'Get ' : '') + 'PID'" + :text="($vuetify.display.mdAndUp ? 'Get ' : '') + 'PID'" class="mr-2" :to="`/database/${$route.params.database_id}/table/${$route.params.table_id}/persist`" /> <template v-slot:extension> diff --git a/dbrepo-ui/components/user/UserBadge.vue b/dbrepo-ui/components/user/UserBadge.vue index f7bd18c60f..9eb679de3c 100644 --- a/dbrepo-ui/components/user/UserBadge.vue +++ b/dbrepo-ui/components/user/UserBadge.vue @@ -5,12 +5,15 @@ class="mr-1" :orcid="orcid" /> <span v-if="isSelf"> - <v-badge - inline - content="you" - color="code"> - {{ creatorName }} - </v-badge> + {{ creatorName }} + <v-chip + size="x-small" + inline> + {{ $t('navigation.you') }} + <v-icon + icon="mdi-account-outline" + end /> + </v-chip> </span> <span v-else> diff --git a/dbrepo-ui/components/view/ViewToolbar.vue b/dbrepo-ui/components/view/ViewToolbar.vue index 9e980e7a3b..6528dd3cd4 100644 --- a/dbrepo-ui/components/view/ViewToolbar.vue +++ b/dbrepo-ui/components/view/ViewToolbar.vue @@ -1,45 +1,28 @@ <template> <v-toolbar flat> <v-btn - class="mr-2" size="small" icon="mdi-arrow-left" :to="`/database/${$route.params.database_id}/view`" /> <v-toolbar-title v-if="view"> <span - v-if="$vuetify.display.lgAndUp"> + v-if="$vuetify.display.mdAndUp" + class="mr-2"> {{ title }} </span> <ResourceStatus - class="ml-2" + :size="$vuetify.display.mdAndUp ? 'small' : 'default'" :resource="view" /> </v-toolbar-title> <v-spacer /> - <v-btn - v-if="canDeleteView" - class="mr-2" - variant="flat" - :prepend-icon="$vuetify.display.lgAndUp ? 'mdi-delete' : null" - :loading="loadingDelete" - color="error" - :text="$t('navigation.delete')" - @click="deleteView" /> - <v-btn - v-if="canUpdateVisibility" - class="mr-2" - variant="flat" - :prepend-icon="$vuetify.display.lgAndUp ? 'mdi-eye' : null" - color="warning" - :text="$t('navigation.visibility')" - @click="updateViewDialog = true" /> <v-btn v-if="canCreatePid" class="mr-2" - :prepend-icon="$vuetify.display.lgAndUp ? 'mdi-content-save-outline' : null" + :prepend-icon="$vuetify.display.mdAndUp ? 'mdi-content-save-outline' : null" variant="flat" color="primary" - :text="($vuetify.display.lgAndUp ? $t('toolbars.view.pid.xl') + ' ' : '') + $t('toolbars.view.pid.permanent')" + :text="($vuetify.display.mdAndUp ? $t('toolbars.view.pid.xl') + ' ' : '') + $t('toolbars.view.pid.permanent')" :to="`/database/${$route.params.database_id}/view/${$route.params.view_id}/persist`" /> <v-dialog v-model="updateViewDialog" @@ -64,6 +47,10 @@ v-if="canViewSchema" :text="$t('navigation.schema')" :to="`/database/${$route.params.database_id}/view/${$route.params.view_id}/schema`" /> + <v-tab + v-if="canViewSettings" + :text="$t('navigation.settings')" + :to="`/database/${$route.params.database_id}/view/${$route.params.view_id}/settings`" /> </v-tabs> </template> </v-toolbar> @@ -128,17 +115,11 @@ export default { } return this.hasReadAccess || this.view.owner.id === this.user.id || this.database.owner.id === this.user.id }, - canDeleteView () { - if (!this.roles || !this.user || !this.view) { + canViewSettings () { + if (!this.user || !this.view) { return false } - return this.roles.includes('delete-database-view') && this.view.owner.id === this.user.id - }, - canUpdateVisibility () { - if (!this.roles || !this.user || !this.view) { - return false - } - return this.roles.includes('modify-view-visibility') && this.view.owner.id === this.user.id + return this.view.owner.id === this.user.id }, canCreatePid () { if (!this.roles || !this.user || !this.view) { @@ -186,35 +167,6 @@ export default { } return this.view.name } - }, - methods: { - deleteView () { - this.loadingDelete = true - const viewService = useViewService() - viewService.remove(this.$route.params.database_id, this.$route.params.view_id) - .then(() => { - const toast = useToastInstance() - toast.success(this.$t('success.view.delete')) - this.cacheStore.reloadDatabase() - this.$router.push(`/database/${this.$route.params.database_id}/view`) - }) - .catch(({code, message}) => { - const toast = useToastInstance() - if (typeof code !== 'string' || typeof message !== 'string') { - return - } - toast.error(this.$t(code) + ": " + message) - }) - .finally(() => { - this.loadingDelete = false - }) - }, - close ({success}) { - this.updateViewDialog = false - if (success) { - this.cacheStore.reloadDatabase() - } - } } } </script> diff --git a/dbrepo-ui/layouts/default.vue b/dbrepo-ui/layouts/default.vue index 952b66a00a..d59966925e 100644 --- a/dbrepo-ui/layouts/default.vue +++ b/dbrepo-ui/layouts/default.vue @@ -100,7 +100,7 @@ class="mr-2" color="secondary" variant="flat" - prepend-icon="mdi-login" + :prepend-icon="$vuetify.display.mdAndUp ? 'mdi-login' : null" to="/login"> {{ $t('navigation.login') }} </v-btn> @@ -108,7 +108,7 @@ v-if="!user" color="primary" variant="flat" - prepend-icon="mdi-account-plus" + :prepend-icon="$vuetify.display.mdAndUp ? 'mdi-account-plus' : null" to="/signup"> {{ $t('navigation.signup') }} </v-btn> @@ -259,6 +259,10 @@ export default { if (newObj.view_id) { this.cacheStore.setRouteView(newObj.database_id, newObj.view_id) } + /* load subset */ + if (newObj.subset_id) { + this.cacheStore.setRouteSubset(newObj.database_id, newObj.subset_id) + } }, deep: true, immediate: true diff --git a/dbrepo-ui/locales/en-US.json b/dbrepo-ui/locales/en-US.json index 6eb0b81974..737e880820 100644 --- a/dbrepo-ui/locales/en-US.json +++ b/dbrepo-ui/locales/en-US.json @@ -37,7 +37,8 @@ "modify": "Modify", "help": "Help", "visibility": "Visibility", - "update": "Update" + "update": "Update", + "you": "You" }, "pages": { "identifier": { @@ -293,11 +294,11 @@ }, "settings": { "title": "Metadata", - "subtitle": "Optional table description for humans and visibility settings." + "subtitle": "Optional table description for humans and visibility settings" }, "delete": { "title": "Delete this table", - "subtitle": "This action deletes {table} and all data in it. There is no going back." + "subtitle": "This action deletes {table} and all data in it, there is no going back" }, "description": { "title": "Description", @@ -612,13 +613,13 @@ "resource": { "data": { "label": "Transparency", - "hint": "Required, e.g. can hide the resource so it is hidden.", + "hint": "Required, e.g. can hide the {resource} from lists, search, etc.", "enabled": "Visible", "disabled": "Hidden" }, "schema": { "label": "Insights", - "hint": "Required, e.g. can show metadata on resources.", + "hint": "Required, e.g. can hide insights on the {resource} such as {schema}.", "enabled": "Visible", "disabled": "Hidden" } @@ -639,7 +640,7 @@ }, "subpages": { "access": { - "title": "Database Access", + "title": "Access to database", "subtitle": "Overview on users with their access to the database", "read": "Read all contents", "write-own": "Read all contents & write own tables", @@ -918,6 +919,14 @@ "visibility": { "title": "Visibility" }, + "delete": { + "title": "Delete this view", + "subtitle": "This action deletes {view}, there is no going back" + }, + "settings": { + "title": "Metadata", + "subtitle": "Visibility settings" + }, "subpages": { "create": { "title": "Create View", @@ -1286,18 +1295,18 @@ "dataset": "Successfully analysed dataset" }, "access": { - "created": "Successfully provisioned access", - "modified": "Successfully modified access", - "revoked": "Successfully revoked access" + "created": "Granted {access} access successfully", + "modified": "Updated {access} access successfully", + "revoked": "Revoked {access} access successfully" }, "data": { "add": "Successfully added data entry", "update": "Successfully updated data entry" }, "table": { - "created": "Successfully created table", + "created": "Created table {table} successfully", "semantics": "Successfully assigned semantic instance", - "updated": "Successfully updated table" + "updated": "Updated table {table} successfully" }, "schema": { "tables": "Successfully refreshed database tables metadata.", @@ -1323,7 +1332,7 @@ "info": "Successfully updated user information", "theme": "Successfully updated user theme", "password": "Successfully updated user password", - "login": "Successfully logged in" + "login": "Welcome back, {username}!" }, "view": { "create": "Successfully created view", diff --git a/dbrepo-ui/pages/database/[database_id]/info.vue b/dbrepo-ui/pages/database/[database_id]/info.vue index 20ba2dbe16..840e31bf50 100644 --- a/dbrepo-ui/pages/database/[database_id]/info.vue +++ b/dbrepo-ui/pages/database/[database_id]/info.vue @@ -58,21 +58,13 @@ </div> </v-list-item> <v-list-item + v-if="databaseSize" :title="$t('pages.database.size.title')" density="compact"> <div> {{ databaseSize }} </div> </v-list-item> - <v-list-item - :title="$t('pages.database.owner.title')" - density="compact"> - <div> - <UserBadge - :user="database.owner" - :other-user="user" /> - </div> - </v-list-item> <v-list-item v-if="access && access.type" :title="$t('pages.database.subpages.access.title')" @@ -95,6 +87,15 @@ </span> </div> </v-list-item> + <v-list-item + :title="$t('pages.database.owner.title')" + density="compact"> + <div> + <UserBadge + :user="database.owner" + :other-user="user" /> + </div> + </v-list-item> <v-list-item v-if="database.contact" :title="$t('pages.database.contact.title')" diff --git a/dbrepo-ui/pages/database/[database_id]/settings.vue b/dbrepo-ui/pages/database/[database_id]/settings.vue index b3d5b63461..0e833914a2 100644 --- a/dbrepo-ui/pages/database/[database_id]/settings.vue +++ b/dbrepo-ui/pages/database/[database_id]/settings.vue @@ -132,56 +132,35 @@ :title="$t('pages.database.subpages.settings.visibility.title')" :subtitle="$t('pages.database.subpages.settings.visibility.subtitle')"> <v-card-text> - <v-row> - <v-col md="8"> + <v-row + dense> + <v-col + md="4"> <v-select v-model="modifyVisibility.is_public" - :items="visibility" - :variant="inputVariant" - :label="$t('pages.database.subpages.settings.visibility.data.label')" - :hint="$t('pages.database.subpages.settings.visibility.data.hint')" + :items="dataOptions" persistent-hint - name="visibility"> - <template - v-slot:append> - <v-tooltip - location="bottom"> - <template - v-slot:activator="{ props }"> - <v-icon - v-bind="props" - icon="mdi-help-circle-outline" /> - </template> - {{ $t('pages.database.subpages.settings.visibility.data.help') }} - </v-tooltip> - </template> - </v-select> + :variant="inputVariant" + required + :rules="[ + v => v !== null || $t('validation.required') + ]" + :label="$t('pages.database.resource.data.label')" + :hint="$t('pages.database.resource.data.hint', { resource: 'database' })" /> </v-col> - </v-row> - <v-row> - <v-col md="8"> + <v-col + md="4"> <v-select v-model="modifyVisibility.is_schema_public" - :items="visibility" - :variant="inputVariant" - :label="$t('pages.database.subpages.settings.visibility.schema.label')" - :hint="$t('pages.database.subpages.settings.visibility.schema.hint')" + :items="schemaOptions" persistent-hint - name="schema-visibility"> - <template - v-slot:append> - <v-tooltip - location="bottom"> - <template - v-slot:activator="{ props }"> - <v-icon - v-bind="props" - icon="mdi-help-circle-outline" /> - </template> - {{ $t('pages.database.subpages.settings.visibility.schema.help') }} - </v-tooltip> - </template> - </v-select> + :variant="inputVariant" + required + :rules="[ + v => v !== null || $t('validation.required') + ]" + :label="$t('pages.database.resource.schema.label')" + :hint="$t('pages.database.resource.schema.hint', { resource: 'database', schema: 'tables, views, subsets' })" /> </v-col> </v-row> <v-row> @@ -302,15 +281,13 @@ export default { modifyImage: { key: null }, - visibility: [ - { - title: this.$t('toolbars.database.public'), - value: true - }, - { - title: this.$t('toolbars.database.private'), - value: false - } + dataOptions: [ + { title: this.$t('pages.database.resource.data.enabled'), value: true }, + { title: this.$t('pages.database.resource.data.disabled'), value: false }, + ], + schemaOptions: [ + { title: this.$t('pages.database.resource.schema.enabled'), value: true }, + { title: this.$t('pages.database.resource.schema.disabled'), value: false }, ], headers: [ { @@ -541,10 +518,13 @@ export default { this.modifyImage.key = null this.loadingImage = false }) - .catch(() => { - const toast = useToastInstance() - toast.error('Failed to modify image') + .catch(({code}) => { this.loadingImage = false + const toast = useToastInstance() + if (typeof code !== 'string') { + return + } + toast.error(this.$t(code)) }) .finally(() => { this.loadingImage = false diff --git a/dbrepo-ui/pages/database/[database_id]/subset/[subset_id]/data.vue b/dbrepo-ui/pages/database/[database_id]/subset/[subset_id]/data.vue index 1346f7d4e8..01ebb72efb 100644 --- a/dbrepo-ui/pages/database/[database_id]/subset/[subset_id]/data.vue +++ b/dbrepo-ui/pages/database/[database_id]/subset/[subset_id]/data.vue @@ -33,7 +33,9 @@ :loading="loadingSubset" @click="loadSubset" /> </v-toolbar> - <v-card tile> + <v-card + v-if="subset" + tile> <QueryResults id="query-results" ref="queryResults" @@ -151,8 +153,10 @@ export default { }) }, loadResult () { - this.$refs.queryResults.reExecute(this.subset.id) - this.$refs.queryResults.reExecuteCount(this.subset.id) + if (this.subset) { + this.$refs.queryResults.reExecute(this.subset.id) + this.$refs.queryResults.reExecuteCount(this.subset.id) + } }, download () { this.downloadLoading = true @@ -170,6 +174,9 @@ export default { .catch(({code}) => { this.downloadLoading = false const toast = useToastInstance() + if (typeof code !== 'string') { + return + } toast.error(this.$t(code)) }) .finally(() => { diff --git a/dbrepo-ui/pages/database/[database_id]/subset/[subset_id]/info.vue b/dbrepo-ui/pages/database/[database_id]/subset/[subset_id]/info.vue index ece7cde135..33f370e2f2 100644 --- a/dbrepo-ui/pages/database/[database_id]/subset/[subset_id]/info.vue +++ b/dbrepo-ui/pages/database/[database_id]/subset/[subset_id]/info.vue @@ -22,7 +22,7 @@ :title="$t('pages.subset.title')"> <v-card-text> <v-list - v-if="loadingSubset && !subset" + v-if="!subset" lines="two" dense> <v-skeleton-loader @@ -66,7 +66,8 @@ <v-list-item :title="`${$t('pages.subset.result.title')} ${$t('pages.subset.hash.title')}`" density="compact"> - <pre>{{ $t('pages.subset.hash.prefix') }}:{{ result_hash }}</pre> + <pre v-if="subset.result_hash">{{ $t('pages.subset.hash.prefix') }}:{{ subset.result_hash }}</pre> + <span v-else>(none)</span> </v-list-item> <v-list-item :title="$t('pages.subset.rows.title')" @@ -76,49 +77,10 @@ </v-list> </v-card-text> </v-card> - <v-divider /> - <v-card - :title="$t('pages.database.title')" - variant="flat" - rounded="0"> - <v-card-text> - <v-list - v-if="database" - dense> - <v-list-item - :title="$t('pages.database.visibility.title')"> - {{ database.is_public ? $t('toolbars.database.public') : $t('toolbars.database.private') }} - </v-list-item> - <v-list-item - :title="$t('pages.database.name.title')"> - <NuxtLink - class="text-primary" - :to="`/database/${database.id}`"> - {{ database.internal_name }} - </NuxtLink> - </v-list-item> - </v-list> - </v-card-text> - </v-card> <v-breadcrumbs :items="items" class="pa-0 mt-2" /> </div> </template> -<script setup> -const config = useRuntimeConfig() -const { database_id, subset_id } = useRoute().params -const requestConfig = { timeout: 90_000, headers: { Accept: 'application/json', 'Content-Type': 'application/json' } } -const userStore = useUserStore() -if (userStore.getToken) { - requestConfig.headers.Authorization = `Bearer ${userStore.getToken}` -} -const { data } = await useFetch(`${config.public.api.server}/api/database/${database_id}/subset/${subset_id}`, requestConfig) -if (data.value) { - const identifierService = useIdentifierService() - useServerHead(identifierService.subsetToServerHead(data.value)) - useServerSeoMeta(identifierService.subsetToServerSeoMeta(data.value)) -} -</script> <script> import Summary from '@/components/identifier/Summary.vue' import SubsetToolbar from '@/components/subset/SubsetToolbar.vue' @@ -135,6 +97,28 @@ export default { SubsetToolbar, UserBadge }, + setup () { + const config = useRuntimeConfig() + const userStore = useUserStore() + const { database_id, subset_id } = useRoute().params + const { error, data } = useFetch(`${config.public.api.server}/api/database/${database_id}/subset/${subset_id}`, { + immediate: true, + timeout: 90_000, + headers: { + Accept: 'application/json', + Authorization: userStore.getToken ? `Bearer ${userStore.getToken}` : null + } + }) + if (data.value) { + const identifierService = useIdentifierService() + useServerHead(identifierService.subsetToServerHead(data.value)) + useServerSeoMeta(identifierService.subsetToServerSeoMeta(data.value)) + } + return { + subset: data, + error + } + }, data () { return { items: [ @@ -164,11 +148,9 @@ export default { persistQueryDialog: false, loadingDatabase: false, loadingIdentifier: false, - loadingSubset: true, downloadLoading: false, error: false, promises: [], - subset: null, userStore: useUserStore(), cacheStore: useCacheStore() } @@ -214,12 +196,6 @@ export default { } return enTitle[0].title }, - result_hash () { - if (!this.subset.result_hash) { - return '(none)' - } - return this.subset.result_hash - }, publisher () { if (this.database.publisher === null) { return 'NA' @@ -232,25 +208,6 @@ export default { } return formatTimestampUTCLabel(this.subset.created) } - }, - mounted () { - this.loadSubset() - }, - methods: { - loadSubset () { - this.loadingSubset = true - const queryService = useQueryService() - queryService.findOne(this.$route.params.database_id, this.$route.params.subset_id) - .then((subset) => { - this.subset = subset - }) - .catch(() => { - this.loadingSubset = false - }) - .finally(() => { - this.loadingSubset = false - }) - } } } </script> diff --git a/dbrepo-ui/pages/database/[database_id]/table/[table_id]/data.vue b/dbrepo-ui/pages/database/[database_id]/table/[table_id]/data.vue index cac7b4ab3d..fc4b046df8 100644 --- a/dbrepo-ui/pages/database/[database_id]/table/[table_id]/data.vue +++ b/dbrepo-ui/pages/database/[database_id]/table/[table_id]/data.vue @@ -313,9 +313,12 @@ export default { } const tupleService = useTupleService() wait.push(tupleService.remove(this.$route.params.database_id, this.$route.params.table_id, { keys: constraints }) - .catch(({message}) => { + .catch(({code, message}) => { const toast = useToastInstance() - toast.error(message) + if (typeof code !== 'string') { + return + } + toast.error(this.$t(code)) })) } Promise.all(wait) @@ -345,6 +348,9 @@ export default { .catch(({code}) => { this.downloadLoading = false const toast = useToastInstance() + if (typeof code !== 'string') { + return + } toast.error(this.$t(code)) }) .finally(() => { @@ -364,6 +370,9 @@ export default { .catch(({code}) => { this.downloadLoading = false const toast = useToastInstance() + if (typeof code !== 'string') { + return + } toast.error(this.$t(code)) }) .finally(() => { diff --git a/dbrepo-ui/pages/database/[database_id]/table/[table_id]/info.vue b/dbrepo-ui/pages/database/[database_id]/table/[table_id]/info.vue index 9b1e053abe..5c258e75ec 100644 --- a/dbrepo-ui/pages/database/[database_id]/table/[table_id]/info.vue +++ b/dbrepo-ui/pages/database/[database_id]/table/[table_id]/info.vue @@ -24,10 +24,6 @@ <v-card-text> <v-list dense> - <v-list-item - :title="$t('pages.table.id.title')"> - {{ table.id }} - </v-list-item> <v-list-item :title="$t('pages.table.name.title')"> {{ table.internal_name }} @@ -45,12 +41,6 @@ :title="$t('pages.table.description.title')"> {{ hasDescription ? table.description : $t('pages.table.description.empty') }} </v-list-item> - <v-list-item - :title="$t('pages.table.owner.title')"> - <UserBadge - :user="table.owner" - :other-user="user" /> - </v-list-item> <v-list-item v-if="accessDescription" :title="$t('pages.database.subpages.access.title')"> @@ -70,6 +60,12 @@ </span> </span> </v-list-item> + <v-list-item + :title="$t('pages.table.owner.title')"> + <UserBadge + :user="table.owner" + :other-user="user" /> + </v-list-item> </v-list> </v-card-text> </v-card> diff --git a/dbrepo-ui/pages/database/[database_id]/table/[table_id]/settings.vue b/dbrepo-ui/pages/database/[database_id]/table/[table_id]/settings.vue index 44133802b8..936db0ab73 100644 --- a/dbrepo-ui/pages/database/[database_id]/table/[table_id]/settings.vue +++ b/dbrepo-ui/pages/database/[database_id]/table/[table_id]/settings.vue @@ -24,7 +24,7 @@ v-model="modify.description" rows="2" :rules="[ - v => (!!v || v.length <= 180) || ($t('validation.max-length') + 180), + v => !max(v, 180) || ($t('validation.max-length') + 180), ]" clearable counter="180" @@ -49,7 +49,7 @@ v => v !== null || $t('validation.required') ]" :label="$t('pages.database.resource.data.label')" - :hint="$t('pages.database.resource.data.hint')" /> + :hint="$t('pages.database.resource.data.hint', { resource: 'table' })" /> </v-col> <v-col md="4"> @@ -63,7 +63,7 @@ v => v !== null || $t('validation.required') ]" :label="$t('pages.database.resource.schema.label')" - :hint="$t('pages.database.resource.schema.hint')" /> + :hint="$t('pages.database.resource.schema.hint', { resource: 'table', schema: 'columns' })" /> </v-col> </v-row> <v-row> @@ -100,7 +100,7 @@ variant="flat" color="error" @click="askDelete"> - Delete + {{ $t('navigation.delete')}} </v-btn> </v-col> </v-row> @@ -115,6 +115,7 @@ </template> <script> +import { max } from '@/utils' import TableToolbar from '@/components/table/TableToolbar.vue' import { useUserStore } from '@/stores/user' import { useCacheStore } from '@/stores/cache' @@ -129,7 +130,7 @@ export default { valid: null, loading: false, modify: { - description: '', + description: null, is_public: null, is_schema_public: null }, @@ -159,8 +160,8 @@ export default { to: `/database/${this.$route.params.database_id}/table/${this.$route.params.table_id}` }, { - title: this.$t('navigation.schema'), - to: `/database/${this.$route.params.database_id}/table/${this.$route.params.table_id}/schema`, + title: this.$t('navigation.settings'), + to: `/database/${this.$route.params.database_id}/table/${this.$route.params.table_id}/settings`, disabled: true } ], @@ -252,6 +253,7 @@ export default { this.modify.description = this.table.description }, methods: { + max, submit () { this.$refs.form.validate() }, @@ -296,13 +298,16 @@ export default { .then(() => { this.loading = false const toast = useToastInstance() - toast.success(this.$t('success.table.updated')) + toast.success(this.$t('success.table.updated', { table: this.table.internal_name })) this.$emit('close', { success: true }) this.cacheStore.reloadTable() }) .catch(({ code }) => { this.loading = false const toast = useToastInstance() + if (typeof code !== 'string') { + return + } toast.error(this.$t(code)) }) .finally(() => { @@ -325,6 +330,9 @@ export default { }) .catch(({code, message}) => { const toast = useToastInstance() + if (typeof code !== 'string') { + return + } toast.error(this.$t(code)) }) .finally(() => { diff --git a/dbrepo-ui/pages/database/[database_id]/table/create/dataset.vue b/dbrepo-ui/pages/database/[database_id]/table/create/dataset.vue index 774f11907e..21b17f4fb5 100644 --- a/dbrepo-ui/pages/database/[database_id]/table/create/dataset.vue +++ b/dbrepo-ui/pages/database/[database_id]/table/create/dataset.vue @@ -107,7 +107,7 @@ v-model="tableCreate.is_public" name="public" :label="$t('pages.database.resource.data.label')" - :hint="$t('pages.database.resource.data.hint')" + :hint="$t('pages.database.resource.data.hint', { resource: 'table' })" persistent-hint :variant="inputVariant" :items="dataOptions" @@ -123,7 +123,7 @@ v-model="tableCreate.is_schema_public" name="schema-public" :label="$t('pages.database.resource.schema.label')" - :hint="$t('pages.database.resource.schema.hint')" + :hint="$t('pages.database.resource.schema.hint', { resource: 'table', schema: 'columns' })" persistent-hint :variant="inputVariant" :items="schemaOptions" @@ -374,7 +374,7 @@ export default { .then((table) => { this.table = table const toast = useToastInstance() - toast.success(this.$t('success.table.created')) + toast.success(this.$t('success.table.created', { table: table.internal_name })) this.step = 5 }) .catch(({code, message}) => { diff --git a/dbrepo-ui/pages/database/[database_id]/table/create/schema.vue b/dbrepo-ui/pages/database/[database_id]/table/create/schema.vue index 458294d1c7..6b62ba7a27 100644 --- a/dbrepo-ui/pages/database/[database_id]/table/create/schema.vue +++ b/dbrepo-ui/pages/database/[database_id]/table/create/schema.vue @@ -81,6 +81,41 @@ :label="$t('pages.table.subpages.import.description.label')" /> </v-col> </v-row> + <v-row + dense> + <v-col + md="4"> + <v-select + v-model="tableCreate.is_public" + name="public" + :label="$t('pages.database.resource.data.label')" + :hint="$t('pages.database.resource.data.hint', { resource: 'table' })" + persistent-hint + :variant="inputVariant" + :items="dataOptions" + item-title="title" + item-value="value" + :rules="[v => v !== null || $t('validation.required')]" + required> + </v-select> + </v-col> + <v-col + md="4"> + <v-select + v-model="tableCreate.is_schema_public" + name="schema-public" + :label="$t('pages.database.resource.schema.label')" + :hint="$t('pages.database.resource.schema.hint', { resource: 'table', schema: 'columns' })" + persistent-hint + :variant="inputVariant" + :items="schemaOptions" + item-title="title" + item-value="value" + :rules="[v => v !== null || $t('validation.required')]" + required> + </v-select> + </v-col> + </v-row> </v-container> </v-form> </v-stepper-window> @@ -171,10 +206,20 @@ export default { step: 1, table: null, error: false, + dataOptions: [ + { title: this.$t('pages.database.resource.data.enabled'), value: true }, + { title: this.$t('pages.database.resource.data.disabled'), value: false }, + ], + schemaOptions: [ + { title: this.$t('pages.database.resource.schema.enabled'), value: true }, + { title: this.$t('pages.database.resource.schema.disabled'), value: false }, + ], tableCreate: { name: null, description: null, columns: [], + is_public: true, + is_schema_public: true, constraints: { uniques: [], foreign_keys: [], @@ -253,6 +298,11 @@ export default { } }, mounted () { + if (!this.database) { + return + } + this.tableCreate.is_public = this.database.is_public + this.tableCreate.is_schema_public = this.database.is_schema_public }, methods: { notEmpty, @@ -273,6 +323,9 @@ export default { .catch(({code, message}) => { this.loading = false const toast = useToastInstance() + if (typeof code !== 'string') { + return + } toast.error(this.$t(`${code}: ${message}`)) }) .finally(() => { diff --git a/dbrepo-ui/pages/database/[database_id]/view/[view_id]/data.vue b/dbrepo-ui/pages/database/[database_id]/view/[view_id]/data.vue index 935e26314f..20ee33ea61 100644 --- a/dbrepo-ui/pages/database/[database_id]/view/[view_id]/data.vue +++ b/dbrepo-ui/pages/database/[database_id]/view/[view_id]/data.vue @@ -8,14 +8,14 @@ :title="$t('toolbars.database.current')" flat> <v-btn - :prepend-icon="$vuetify.display.lgAndUp ? 'mdi-download' : null" + :prepend-icon="$vuetify.display.mdAndUp ? 'mdi-download' : null" variant="flat" :loading="downloadLoading" :text="$t('toolbars.table.data.download')" class="mr-2" @click.stop="download" /> <v-btn - :prepend-icon="$vuetify.display.lgAndUp ? 'mdi-refresh' : null" + :prepend-icon="$vuetify.display.mdAndUp ? 'mdi-refresh' : null" variant="flat" :text="$t('toolbars.table.data.refresh')" class="mr-2" @@ -132,6 +132,9 @@ export default { .catch(({code}) => { this.downloadLoading = false const toast = useToastInstance() + if (typeof code !== 'string') { + return + } toast.error(this.$t(code)) }) .finally(() => { diff --git a/dbrepo-ui/pages/database/[database_id]/view/[view_id]/schema.vue b/dbrepo-ui/pages/database/[database_id]/view/[view_id]/schema.vue index 7426d50468..e9559bca7a 100644 --- a/dbrepo-ui/pages/database/[database_id]/view/[view_id]/schema.vue +++ b/dbrepo-ui/pages/database/[database_id]/view/[view_id]/schema.vue @@ -63,7 +63,6 @@ export default { data () { return { loading: false, - view: null, items: [ { title: this.$t('navigation.databases'), @@ -100,9 +99,6 @@ export default { cacheStore: useCacheStore() } }, - mounted () { - this.fetchView() - }, computed: { user () { return this.userStore.getUser @@ -110,6 +106,9 @@ export default { database () { return this.cacheStore.getDatabase }, + view () { + return this.cacheStore.getView + }, access () { return this.userStore.getAccess }, @@ -119,9 +118,6 @@ export default { } return this.access.type === 'read' || this.access.type === 'write_all' || this.access.type === 'write_own' }, - view () { - return this.cacheStore.getView - }, canViewSchema () { if (!this.view) { return false @@ -176,23 +172,6 @@ export default { }, hasConcept (item) { return item.concept && 'uri' in item.concept - }, - fetchView () { - this.loading = true - const viewService = useViewService() - viewService.findOne(this.$route.params.database_id, this.$route.params.view_id) - .then((view) => { - this.view = view - this.loading = false - }) - .catch(({code}) => { - this.loading = false - const toast = useToastInstance() - toast.error(this.$t(code)) - }) - .finally(() => { - this.loading = false - }) } } } diff --git a/dbrepo-ui/pages/database/[database_id]/view/[view_id]/settings.vue b/dbrepo-ui/pages/database/[database_id]/view/[view_id]/settings.vue new file mode 100644 index 0000000000..80c746292b --- /dev/null +++ b/dbrepo-ui/pages/database/[database_id]/view/[view_id]/settings.vue @@ -0,0 +1,311 @@ +<template> + <div + v-if="canViewSettings"> + <ViewToolbar /> + <v-form + v-if="canUpdateVisibility" + ref="form" + v-model="valid" + autocomplete="off" + @submit.prevent="submit"> + <v-card + variant="flat" + rounded="0" + :title="$t('pages.view.settings.title')" + :subtitle="$t('pages.view.settings.subtitle')"> + <v-card-text> + <v-row + dense> + <v-col + md="4"> + <v-select + v-model="modify.is_public" + :items="dataOptions" + persistent-hint + :variant="inputVariant" + required + :rules="[ + v => v !== null || $t('validation.required') + ]" + :label="$t('pages.database.resource.data.label')" + :hint="$t('pages.database.resource.data.hint', { resource: 'view' })" /> + </v-col> + <v-col + md="4"> + <v-select + v-model="modify.is_schema_public" + :items="schemaOptions" + persistent-hint + :variant="inputVariant" + required + :rules="[ + v => v !== null || $t('validation.required') + ]" + :label="$t('pages.database.resource.schema.label')" + :hint="$t('pages.database.resource.schema.hint', { resource: 'view', schema: 'query' })" /> + </v-col> + </v-row> + <v-row> + <v-col> + <v-btn + variant="flat" + size="small" + :disabled="!valid || !isChange" + :color="buttonColor" + :loading="loading" + type="submit" + :text="$t('navigation.modify')" + @click="update" /> + </v-col> + </v-row> + </v-card-text> + </v-card> + </v-form> + <v-divider + v-if="canDeleteView" /> + <v-card + v-if="canDeleteView" + variant="flat" + rounded="0" + :title="$t('pages.view.delete.title')" + :subtitle="$t('pages.view.delete.subtitle', { view: view.internal_name })"> + <v-card-text> + <v-row> + <v-col + md="8"> + <v-btn + size="small" + variant="flat" + color="error" + @click="askDelete"> + {{ $t('navigation.delete')}} + </v-btn> + </v-col> + </v-row> + </v-card-text> + </v-card> + <v-breadcrumbs + :items="items" + class="pa-0 mt-2" /> + </div> +</template> + +<script> +import ViewToolbar from '@/components/view/ViewToolbar.vue' +import { useUserStore } from '@/stores/user' +import { useCacheStore } from '@/stores/cache' + +export default { + components: { + ViewToolbar + }, + data () { + return { + valid: null, + loading: false, + modify: { + is_public: null, + is_schema_public: null + }, + dataOptions: [ + { title: this.$t('pages.database.resource.data.enabled'), value: true }, + { title: this.$t('pages.database.resource.data.disabled'), value: false }, + ], + schemaOptions: [ + { title: this.$t('pages.database.resource.schema.enabled'), value: true }, + { title: this.$t('pages.database.resource.schema.disabled'), value: false }, + ], + items: [ + { + title: this.$t('navigation.databases'), + to: '/database' + }, + { + title: `${this.$route.params.database_id}`, + to: `/database/${this.$route.params.database_id}/info` + }, + { + title: this.$t('navigation.views'), + to: `/database/${this.$route.params.database_id}/view` + }, + { + title: `${this.$route.params.view_id}`, + to: `/database/${this.$route.params.database_id}/view/${this.$route.params.view_id}` + }, + { + title: this.$t('navigation.settings'), + to: `/database/${this.$route.params.database_id}/view/${this.$route.params.view_id}/settings`, + disabled: true + } + ], + headers: [ + { value: 'internal_name', title: this.$t('pages.table.subpages.schema.internal-name.title') }, + { value: 'column_type', title: this.$t('pages.table.subpages.schema.column-type.title') }, + { value: 'extra', title: this.$t('pages.table.subpages.schema.extra.title') }, + { value: 'column_concept', title: this.$t('pages.table.subpages.schema.concept.title') }, + { value: 'column_unit', title: this.$t('pages.table.subpages.schema.unit.title') }, + { value: 'is_null_allowed', title: this.$t('pages.table.subpages.schema.nullable.title') }, + { value: 'description', title: this.$t('pages.table.subpages.schema.description.title') }, + ], + dateColumns: [], + userStore: useUserStore(), + cacheStore: useCacheStore() + } + }, + computed: { + user () { + return this.userStore.getUser + }, + database () { + return this.cacheStore.getDatabase + }, + view () { + return this.cacheStore.getView + }, + access () { + return this.userStore.getAccess + }, + hasReadAccess () { + if (!this.access) { + return false + } + return this.access.type === 'read' || this.access.type === 'write_all' || this.access.type === 'write_own' + }, + roles () { + return this.userStore.getRoles + }, + isChange () { + if (!this.view) { + return false + } + if (this.view.is_public !== this.modify.is_public) { + return true + } + return this.view.is_schema_public !== this.modify.is_schema_public + }, + canUpdateVisibility () { + if (!this.roles || !this.user || !this.view) { + return false + } + return this.roles.includes('modify-view-visibility') && this.view.owner.id === this.user.id + }, + canDeleteView () { + if (!this.roles || !this.user || !this.view) { + return false + } + return this.roles.includes('delete-database-view') && this.view.owner.id === this.user.id + }, + canViewSettings () { + if (!this.user || !this.view) { + return false + } + return this.view.owner.id === this.user.id + }, + inputVariant () { + const runtimeConfig = useRuntimeConfig() + return this.$vuetify.theme.global.name.toLowerCase().endsWith('contrast') ? runtimeConfig.public.variant.input.contrast : runtimeConfig.public.variant.input.normal + }, + buttonVariant () { + const runtimeConfig = useRuntimeConfig() + return this.$vuetify.theme.global.name.toLowerCase().endsWith('contrast') ? runtimeConfig.public.variant.button.contrast : runtimeConfig.public.variant.button.normal + }, + buttonColor () { + return !this.isChange ? null : 'warning' + } + }, + mounted() { + if (!this.view) { + return + } + this.modify.is_public = this.view.is_public + this.modify.is_schema_public = this.view.is_schema_public + this.modify.description = this.view.description + }, + methods: { + submit () { + this.$refs.form.validate() + }, + extra (column) { + if (column.column_type === 'float') { + return `precision=${column.size}` + } else if (['decimal', 'double'].includes(column.column_type)) { + let extra = '' + if (column.size !== null) { + extra += `size=${column.size}` + } + if (column.d !== null) { + if (extra.length > 0) { + extra += ', ' + } + extra += `d=${column.d}` + } + return extra + } else if (column.column_type === 'enum') { + return `(${column.enums.join(', ')})` + } else if (column.column_type === 'set') { + return `(${column.sets.join(', ')})` + } else if (['int', 'char', 'varchar', 'binary', 'varbinary', 'tinyint', 'size="small"int', 'mediumint', 'bigint'].includes(column.column_type)) { + return column.size !== null ? `size=${column.size}` : '' + } + return null + }, + closed (event) { + const { success } = event + console.debug('closed dialog', event) + if (success) { + const toast = useToastInstance() + toast.success(this.$t('success.table.semantics')) + this.cacheStore.reloadTable() + } + this.dialogSemantic = false + }, + askDelete () { + if (!confirm(this.$t('pages.view.delete.subtitle', { view: this.view.internal_name }))) { + return + } + this.loadingDelete = true + const viewService = useViewService() + viewService.remove(this.database.id, this.view.id) + .then(() => { + console.info('Deleted view with id ', this.view.id) + this.cacheStore.reloadDatabase() + const toast = useToastInstance() + toast.success('Successfully deleted view with id ' + this.view.id) + this.$router.push(`/database/${this.$route.params.database_id}/view`) + }) + .catch(({code, message}) => { + const toast = useToastInstance() + if (typeof code !== 'string') { + return + } + toast.error(this.$t(code)) + }) + .finally(() => { + this.loadingDelete = false + }) + }, + update () { + this.loading = true + const viewService = useViewService() + viewService.update(this.$route.params.database_id, this.$route.params.view_id, this.modify) + .then(() => { + this.loading = false + const toast = useToastInstance() + toast.success(this.$t('success.view.modified')) + this.cacheStore.reloadView() + }) + .catch(({code, message}) => { + this.loading = false + const toast = useToastInstance() + if (typeof code !== 'string') { + return + } + toast.error(message) + }) + .finally(() => { + this.loading = false + }) + } + } +} +</script> diff --git a/dbrepo-ui/pages/login.vue b/dbrepo-ui/pages/login.vue index 8a35efe59d..e1b255ed8b 100644 --- a/dbrepo-ui/pages/login.vue +++ b/dbrepo-ui/pages/login.vue @@ -117,7 +117,7 @@ export default { userService.findOne(userId) .then((user) => { const toast = useToastInstance() - toast.success(this.$t('success.user.login')) + toast.success(this.$t('success.user.login', { username : user.username })) switch (user.attributes.theme) { case 'dark': this.$vuetify.theme.global.name = 'tuwThemeDark' @@ -137,12 +137,18 @@ export default { }) .catch(({code}) => { const toast = useToastInstance() + if (typeof code !== 'string') { + return + } toast.error(this.$t(code)) }) }) .catch(({code}) => { this.loading = false const toast = useToastInstance() + if (typeof code !== 'string') { + return + } toast.error(this.$t(code)) }) .finally(() => { diff --git a/dbrepo-ui/pages/signup.vue b/dbrepo-ui/pages/signup.vue index 2548a6dfa9..54c0060225 100644 --- a/dbrepo-ui/pages/signup.vue +++ b/dbrepo-ui/pages/signup.vue @@ -130,6 +130,9 @@ export default { .catch(({code}) => { this.loading = false const toast = useToastInstance() + if (typeof code !== 'string') { + return + } toast.error(this.$t(code)) }) .finally(() => { diff --git a/dbrepo-ui/pages/user/info.vue b/dbrepo-ui/pages/user/info.vue index 3501818ca0..d2d1b2c8fe 100644 --- a/dbrepo-ui/pages/user/info.vue +++ b/dbrepo-ui/pages/user/info.vue @@ -14,11 +14,9 @@ <v-col md="6"> <v-text-field v-model="model.id" - readonly + disabled :variant="inputVariant" - :label="$t('pages.user.subpages.info.id.label')" - append-inner-icon="mdi-content-copy" - @click:append-inner="copy" /> + :label="$t('pages.user.subpages.info.id.label')" /> </v-col> </v-row> <v-row dense> @@ -284,11 +282,6 @@ export default { .finally(() => { this.orcidLoading = false }) - }, - copy () { - navigator.clipboard.writeText(this.model.id) - const toast = useToastInstance() - toast.success(this.$t('success.clipboard.user')) } } } diff --git a/dbrepo-ui/stores/cache.js b/dbrepo-ui/stores/cache.js index b19658d08d..13fcea5861 100644 --- a/dbrepo-ui/stores/cache.js +++ b/dbrepo-ui/stores/cache.js @@ -7,6 +7,7 @@ export const useCacheStore = defineStore('cache', { database: null, table: null, view: null, + subset: null, ontologies: [], messages: [], uploadProgress: null @@ -16,6 +17,7 @@ export const useCacheStore = defineStore('cache', { getDatabase: (state) => state.database, getTable: (state) => state.table, getView: (state) => state.view, + getSubset: (state) => state.subset, getOntologies: (state) => state.ontologies, getMessages: (state) => state.messages, getUploadProgress: (state) => state.uploadProgress, @@ -30,6 +32,9 @@ export const useCacheStore = defineStore('cache', { setView (view) { this.view = view }, + setSubset (subset) { + this.subset = subset + }, setOntologies (ontologies) { this.ontologies = ontologies }, @@ -68,6 +73,14 @@ export const useCacheStore = defineStore('cache', { console.error('Failed to reload table', error) }) }, + reloadView () { + const viewService = useViewService() + viewService.findOne(this.table.database_id, this.view.id) + .then(view => this.view = view) + .catch((error) => { + console.error('Failed to reload view', error) + }) + }, setRouteDatabase (databaseId) { if (!databaseId) { this.database = null @@ -94,18 +107,31 @@ export const useCacheStore = defineStore('cache', { console.error('Failed to set route table', error) }) }, - setRouteView (databaseId, view_id) { - if (!databaseId || !view_id) { + setRouteView (databaseId, viewId) { + if (!databaseId || !viewId) { this.view = null - console.error('Cannot set route view: missing view id', databaseId, 'or view id', view_id) + console.error('Cannot set route view: database view id', databaseId, 'or view id', viewId) return } const viewService = useViewService() - viewService.findOne(databaseId, view_id) + viewService.findOne(databaseId, viewId) .then(view => this.view = view) .catch((error) => { console.error('Failed to set route view', error) }) + }, + setRouteSubset (databaseId, subsetId) { + if (!databaseId || !subsetId) { + this.subset = null + console.error('Cannot set route subset: missing database id', databaseId, 'or subset id', subsetId) + return + } + const subsetService = useQueryService() + subsetService.findOne(databaseId, subsetId) + .then(subset => this.subset = subset) + .catch((error) => { + console.error('Failed to set route subset', error) + }) } }, }) diff --git a/dbrepo-ui/utils/index.ts b/dbrepo-ui/utils/index.ts index 3946a70938..4a9b239497 100644 --- a/dbrepo-ui/utils/index.ts +++ b/dbrepo-ui/utils/index.ts @@ -10,6 +10,13 @@ export function notEmpty(str: string) { return str.trim().length > 0 } +export function max(str: string, len: number) { + if (str === null) { + return false + } + return str.trim().length <= len +} + export function notFile(files: [File[]]) { if (!files) { return false -- GitLab