diff --git a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/DatabaseDto.java b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/DatabaseDto.java index dcdb1b944893b63fee80c385aad735d443b43c8a..d68e6802da3141d362eafed4c1e258eeeecdf3de 100644 --- a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/DatabaseDto.java +++ b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/DatabaseDto.java @@ -78,8 +78,8 @@ public class DatabaseDto { @NotNull private UserDto owner; - @ToString.Exclude - private byte[] image; + @JsonProperty("preview_image") + private String previewImage; @NotNull @Schema(example = "2021-03-12T15:26:21Z") diff --git a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/internal/PrivilegedDatabaseDto.java b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/internal/PrivilegedDatabaseDto.java index e54a6c552dc4053b3aa0bcc3fe1135c1ce095833..2ad2c1a968bf2b59ac6a9812d5b9dc09bf2ca53a 100644 --- a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/internal/PrivilegedDatabaseDto.java +++ b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/internal/PrivilegedDatabaseDto.java @@ -75,9 +75,6 @@ public class PrivilegedDatabaseDto { @NotNull private UserDto owner; - @ToString.Exclude - private byte[] image; - @NotNull @Schema(example = "2021-03-12T15:26:21Z") @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", timezone = "UTC") diff --git a/dbrepo-metadata-service/metrics.md b/dbrepo-metadata-service/metrics.md index c8cd48baf08069f079f5ce9df58123751be65f2d..440f20b8e05ea12053ab275bb019a8cd444b1180 100644 --- a/dbrepo-metadata-service/metrics.md +++ b/dbrepo-metadata-service/metrics.md @@ -12,6 +12,7 @@ | `dbrepo_database_find` | Find database | | `dbrepo_database_findall` | List databases | | `dbrepo_database_image` | Update database preview image | +| `dbrepo_database_image_view` | Get database preview image | | `dbrepo_database_transfer` | Update database owner | | `dbrepo_database_visibility` | Update database visibility | | `dbrepo_identifier_create` | Create identifier | 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 bc20219105e23371712fe61ff822e5628383f67b..82e09d1c0f2f5867a2064a67853691a5bb034778 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 @@ -865,7 +865,7 @@ public interface MetadataMapper { .internalName(data.getInternalName()) .description(data.getDescription()) .exchangeName(data.getExchangeName()) - .image(data.getImage()) + .previewImage(data.getImage() != null ? "/api/database/" + data.getId() + "/image" : null) .isPublic(data.getIsPublic()) .container(containerToContainerDto(data.getContainer())) .creator(userToUserDto(data.getCreator())) 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 b270bdb20615ab725535242fac0982114e4e42db..2eeb947c5586bbcfedceebed30918c3ea7212bd8 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 @@ -26,6 +26,7 @@ import lombok.extern.log4j.Log4j2; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.transaction.annotation.Transactional; @@ -439,6 +440,30 @@ public class DatabaseEndpoint { .body(dto); } + @GetMapping("/{databaseId}/image") + @Transactional + @Observed(name = "dbrepo_database_image_view") + @Operation(summary = "Get database preview image", + description = "Gets the database with id on the preview image.", + security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "View of image was successful"), + @ApiResponse(responseCode = "404", + description = "Database or user could not be found", + content = {@Content( + mediaType = "application/json", + schema = @Schema(implementation = ApiErrorDto.class))}) + }) + public ResponseEntity<byte[]> findPreviewImage(@NotNull @PathVariable("databaseId") Long databaseId) + throws DatabaseNotFoundException { + log.debug("endpoint get database preview image, databaseId={}", databaseId); + final Database database = databaseService.findById(databaseId); + return ResponseEntity.ok() + .contentType(MediaType.parseMediaType("image/webp")) + .body(database.getImage()); + } + @GetMapping("/{databaseId}") @Transactional(readOnly = true) @Observed(name = "dbrepo_database_find") 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 8ffe328aa95342c6b8ce6fe93a910658ad2ab043..2d6033ed92c7982b0d87bef1cf433dd1176ba106 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 @@ -20,6 +20,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.test.context.support.WithAnonymousUser; import org.springframework.security.test.context.support.WithMockUser; @@ -408,6 +409,52 @@ public class DatabaseEndpointUnitTest extends AbstractUnitTest { assertEquals(2, accessList.size()); } + @Test + @WithAnonymousUser + public void findPreviewImage_anonymous_succeeds() throws DatabaseNotFoundException { + + /* test */ + final ResponseEntity<byte[]> response = findPreviewImage_generic(DATABASE_1_ID, DATABASE_1); + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals(MediaType.parseMediaType("image/webp"), response.getHeaders().getContentType()); + final byte[] body = response.getBody(); + assertNotNull(body); + } + + @Test + @WithMockUser(username = USER_1_USERNAME) + public void findPreviewImage_noRoles_succeeds() throws DatabaseNotFoundException { + + /* test */ + final ResponseEntity<byte[]> response = findPreviewImage_generic(DATABASE_1_ID, DATABASE_1); + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals(MediaType.parseMediaType("image/webp"), response.getHeaders().getContentType()); + final byte[] body = response.getBody(); + assertNotNull(body); + } + + @Test + @WithAnonymousUser + public void findPreviewImage_noImage_succeeds() throws DatabaseNotFoundException { + + /* test */ + final ResponseEntity<byte[]> response = findPreviewImage_generic(DATABASE_2_ID, DATABASE_2); + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals(MediaType.parseMediaType("image/webp"), response.getHeaders().getContentType()); + final byte[] body = response.getBody(); + assertNull(body); + } + + @Test + @WithAnonymousUser + public void findPreviewImage_notFound_fails() { + + /* test */ + assertThrows(DatabaseNotFoundException.class, () -> { + findPreviewImage_generic(DATABASE_1_ID, null); + }); + } + /* ################################################################################################### */ /* ## GENERIC TEST CASES ## */ /* ################################################################################################### */ @@ -496,4 +543,20 @@ public class DatabaseEndpointUnitTest extends AbstractUnitTest { return body; } + public ResponseEntity<byte[]> findPreviewImage_generic(Long databaseId, Database database) throws DatabaseNotFoundException { + + /* mock */ + if (database != null) { + when(databaseService.findById(databaseId)) + .thenReturn(database); + } else { + doThrow(DatabaseNotFoundException.class) + .when(databaseService) + .findById(databaseId); + } + + /* test */ + return databaseEndpoint.findPreviewImage(databaseId); + } + } diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/mvc/PrometheusEndpointMvcTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/mvc/PrometheusEndpointMvcTest.java index 0b804dbdeab1fe3777614fafc7e4b790249bf569..9a43b234d939a760b19765a4075574379ce4a65c 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/mvc/PrometheusEndpointMvcTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/mvc/PrometheusEndpointMvcTest.java @@ -231,9 +231,14 @@ public class PrometheusEndpointMvcTest extends AbstractUnitTest { } catch (Exception e) { /* ignore */ } + try { + databaseEndpoint.findPreviewImage(DATABASE_1_ID); + } catch (Exception e) { + /* ignore */ + } /* test */ - for (String metric : List.of("dbrepo_database_findall", "dbrepo_database_create", "dbrepo_database_visibility", "dbrepo_database_transfer", "dbrepo_database_find", "dbrepo_database_image")) { + for (String metric : List.of("dbrepo_database_findall", "dbrepo_database_create", "dbrepo_database_visibility", "dbrepo_database_transfer", "dbrepo_database_find", "dbrepo_database_image", "dbrepo_database_image_view")) { assertThat(registry) .hasObservationWithNameEqualTo(metric); } 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 1e7633b851f9c052fd8fe60d14193698565421f3..8f37dde792c63c54334682e8d830761459fb235b 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 @@ -291,7 +291,7 @@ public class DatabaseServiceUnitTest extends AbstractUnitTest { assertNotNull(response.getContact()); assertNotNull(response.getCreatedBy()); assertNotNull(response.getOwner()); - assertNull(response.getImage()); + assertNotNull(response.getImage()); assertNotNull(response.getExchangeName()); return response; } diff --git a/dbrepo-metadata-service/test/src/main/java/at/tuwien/test/BaseTest.java b/dbrepo-metadata-service/test/src/main/java/at/tuwien/test/BaseTest.java index 7e6154be62dd72623c558816b1f679139b7f3a18..4586f68d4f9c2d809bb568c0c5e878e86ca345d8 100644 --- a/dbrepo-metadata-service/test/src/main/java/at/tuwien/test/BaseTest.java +++ b/dbrepo-metadata-service/test/src/main/java/at/tuwien/test/BaseTest.java @@ -7049,6 +7049,7 @@ public abstract class BaseTest { .creator(USER_1) .ownedBy(DATABASE_1_OWNER) .owner(USER_1) + .image(new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}) .contactPerson(USER_1_ID) .contact(USER_1) .subsets(new LinkedList<>()) diff --git a/dbrepo-metric-db/prometheus.yml b/dbrepo-metric-db/prometheus.yml index f715711ee104ce762cc0e8813461feadce49059b..ce0a6eb43ca2e1a0987bb1a4e953e3ca43d85751 100644 --- a/dbrepo-metric-db/prometheus.yml +++ b/dbrepo-metric-db/prometheus.yml @@ -16,8 +16,4 @@ scrape_configs: - job_name: 'metrics scrape' metrics_path: '/metrics' static_configs: - - targets: ['ui:3000', 'auth-service:9000', 'analyse-service:8080', 'search-service:8080', 'storage-service:9090', 'upload-service:8080', 'dashboard-service:3000'] -# - job_name: 'gateway scrape' -# metrics_path: '/metrics' -# static_configs: -# - targets: ['dbrepo-gateway-service-sidecar:9113'] + - targets: ['ui:3000', 'auth-service:9000', 'analyse-service:8080', 'search-service:8080', 'storage-service:9090', 'upload-service:8080', 'dashboard-service:3000', 'broker-service:15692'] diff --git a/dbrepo-ui/composables/database-service.ts b/dbrepo-ui/composables/database-service.ts index 3890477822cf58a5b0d375b4c45df87931039a52..b96d75ab80e880f52d416fef382f9d22de444502 100644 --- a/dbrepo-ui/composables/database-service.ts +++ b/dbrepo-ui/composables/database-service.ts @@ -99,6 +99,22 @@ export const useDatabaseService = (): any => { }); } + async function findPreviewImage(id: number): Promise<string> { + const axios = useAxiosInstance(); + console.debug('find database preview image with id', id); + return new Promise((resolve, reject) => { + axios.get<string>(`/api/database/${id}/image`) + .then((response) => { + console.info('Found database preview image with id', id); + resolve(response.data); + }) + .catch((error) => { + console.error('Failed to find database preview image', error); + reject(axiosErrorToApiError(error)); + }); + }); + } + async function updateVisibility(id: number, payload: DatabaseModifyVisibilityDto): Promise<DatabaseDto | null> { const axios = useAxiosInstance() console.debug('update database visibility for database with id', id); @@ -220,6 +236,7 @@ export const useDatabaseService = (): any => { refreshTablesMetadata, refreshViewsMetadata, findOne, + findPreviewImage, findCount, getServerTime, updateVisibility, diff --git a/dbrepo-ui/dto/index.ts b/dbrepo-ui/dto/index.ts index 7658b4d1715af0b26d0b43bfd16493c84a02d391..ba4c41304209a08200b99de17077eed49d63e357 100644 --- a/dbrepo-ui/dto/index.ts +++ b/dbrepo-ui/dto/index.ts @@ -12,8 +12,8 @@ interface DatabaseDto { container: ContainerBriefDto; identifiers: IdentifierDto[] | []; subsets: IdentifierDto[] | []; - image: string; accesses: DatabaseAccessDto[]; + has_preview_image: boolean; identifier: IdentifierDto[]; tables: TableDto[]; views: ViewDto[]; diff --git a/dbrepo-ui/locales/en-US.json b/dbrepo-ui/locales/en-US.json index cd357f2068e8513d8cb0c8858935cfba328c670a..8b6807bdbf24edd94b08087ef538d499e47784eb 100644 --- a/dbrepo-ui/locales/en-US.json +++ b/dbrepo-ui/locales/en-US.json @@ -581,8 +581,8 @@ "database": { "title": "Database", "image": { - "title": "Database Image", - "alt": "Database logo/default image" + "title": "Preview Image", + "alt": "Database preview image representing the dataset" }, "name": { "title": "Name" @@ -1122,7 +1122,8 @@ "image": { "exists": "Image already exists in metadata database", "missing": "Failed to find image in metadata database", - "invalid": "Image metadata is malformed" + "invalid": "Image metadata is malformed", + "size": "Image size is too large" }, "license": { "missing": "Failed to find license in metadata database" @@ -1465,6 +1466,9 @@ }, "pattern": { "timestamp": "Must be in format yyyy-MM-dd HH:mm:ss" + }, + "image": { + "size": "Image size is too large" } } } diff --git a/dbrepo-ui/pages/database/[database_id]/info.vue b/dbrepo-ui/pages/database/[database_id]/info.vue index 432b14e21a60968048440da6f0084eb535aa06a7..d012f7ef3418b0235d04f46cf49ec138ed9634a2 100644 --- a/dbrepo-ui/pages/database/[database_id]/info.vue +++ b/dbrepo-ui/pages/database/[database_id]/info.vue @@ -33,12 +33,13 @@ lines="two" dense> <v-list-item - v-if="databaseImage" + v-if="previewImage" :title="$t('pages.database.image.title')" density="compact"> <v-img - :src="databaseImage" + :src="previewImage" :alt="$t('pages.database.image.alt')" + :title="$t('pages.database.image.alt')" :max-width="maxWidth" :max-height="maxHeight" /> </v-list-item> @@ -209,10 +210,6 @@ export default { }, data () { return { - loading: false, - loadingStart: false, - loadingStop: false, - editDialog: false, items: [ { title: this.$t('navigation.databases'), @@ -373,11 +370,11 @@ export default { this.database.tables.forEach((t) => { sum += t.data_length }) return sizeToHumanLabel(sum) }, - databaseImage () { - if (!this.database || !this.database.image) { + previewImage () { + if (!this.database) { return null } - return `data:image/webp;base64,${this.database.image}` + return this.database.preview_image } } } diff --git a/dbrepo-ui/pages/database/[database_id]/settings.vue b/dbrepo-ui/pages/database/[database_id]/settings.vue index c205e8c431dc6851d9b9b21c0dc8adc916d9d7f5..65dd3b03bfc3ec57447ebbea6dcd24635f303693 100644 --- a/dbrepo-ui/pages/database/[database_id]/settings.vue +++ b/dbrepo-ui/pages/database/[database_id]/settings.vue @@ -18,14 +18,31 @@ v-model="validUpload" @submit.prevent="submit"> <v-row - v-if="databaseImage" + v-if="previewImage" dense> <v-col md="8"> + <v-alert + v-if="file" + border="start" + color="warning"> + This is a only preview of your dataset image and changes are not yet saved. + </v-alert> <v-img - :src="databaseImage" + class="mt-2" + :src="previewImage" :alt="$t('pages.database.image.alt')" + :title="$t('pages.database.image.alt')" :max-width="maxWidth" :max-height="maxHeight" /> + <v-btn + v-if="database.preview_image" + size="small" + variant="flat" + color="error" + class="ml-2 mt-4" + :text="$t('pages.database.subpages.settings.image-remove.text')" + :loading="loadingDeleteImage" + @click="removeDatabaseImage" /> </v-col> </v-row> <v-row dense> @@ -39,6 +56,7 @@ :variant="inputVariant" variant="underlined" :loading="loadingUpload" + :error-messages="uploadErrorMessages" :show-size="1000" counter :label="$t('pages.database.subpages.settings.image.label')" @@ -63,15 +81,6 @@ :text="$t('pages.database.subpages.settings.submit.text')" :loading="loadingImage" @click="updateDatabaseImage" /> - <v-btn - v-if="database.image" - size="small" - variant="flat" - color="error" - class="ml-2 mt-4" - :text="$t('pages.database.subpages.settings.image-remove.text')" - :loading="loadingDeleteImage" - @click="removeDatabaseImage" /> </v-col> </v-row> </v-form> @@ -378,14 +387,14 @@ export default { } return this.roles.includes('modify-database-image') }, - databaseImage () { + previewImage () { if (this.file) { return URL.createObjectURL(this.file) } - if (!this.database || !this.database.image) { + if (!this.database) { return null } - return `data:image/webp;base64,${this.database.image}` + return this.database.preview_image }, maxWidth () { return this.$config.public.database.image.width @@ -393,6 +402,12 @@ export default { maxHeight () { return this.$config.public.database.image.height }, + uploadErrorMessages () { + if (!this.file || this.file.size < 1_000_000) { + return [] + } + return [this.$t('validation.image.size')] + }, inputVariant () { const runtimeConfig = useRuntimeConfig() return this.$vuetify.theme.global.name.toLowerCase().endsWith('contrast') ? runtimeConfig.public.variant.input.contrast : runtimeConfig.public.variant.input.normal @@ -446,8 +461,12 @@ export default { }) }, uploadFile () { - this.loadingUpload = true console.debug('upload file', this.file) + if (this.file.size > 1_000_000) { + const toast = useToastInstance() + toast.error(this.$t('error.image.size')) + } + this.loadingUpload = true const uploadService = useUploadService() uploadService.create(this.file) .then((s3key) => { 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 60bfe33a13f367ba88349a94a7808567dbde92d3..bc460da08bc4fe45d180fb81367e88f6eebf6b1f 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 @@ -122,9 +122,6 @@ export default { .catch(({code}) => { this.downloadLoading = false const toast = useToastInstance() - if (typeof code !== 'string') { - return - } toast.error(this.$t(code)) }) .finally(() => {