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