diff --git a/fda-query-service/rest-service/src/main/java/at/tuwien/handlers/ApiExceptionHandler.java b/fda-query-service/rest-service/src/main/java/at/tuwien/handlers/ApiExceptionHandler.java index 3bc8edeb585a6aa84d69c33916d5533a9bf4be65..b5d0c1658bf704de3856a2321cdf4d2057412c66 100644 --- a/fda-query-service/rest-service/src/main/java/at/tuwien/handlers/ApiExceptionHandler.java +++ b/fda-query-service/rest-service/src/main/java/at/tuwien/handlers/ApiExceptionHandler.java @@ -158,13 +158,13 @@ public class ApiExceptionHandler extends ResponseEntityExceptionHandler { return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus()); } - @ResponseStatus(HttpStatus.CONFLICT) + @ResponseStatus(HttpStatus.GATEWAY_TIMEOUT) @ExceptionHandler(QueryStoreException.class) public ResponseEntity<ApiErrorDto> handle(QueryStoreException e, WebRequest request) { final ApiErrorDto response = ApiErrorDto.builder() - .status(HttpStatus.CONFLICT) + .status(HttpStatus.GATEWAY_TIMEOUT) .message(e.getLocalizedMessage()) - .code("error.query.storeexists") + .code("error.query.store") .build(); return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus()); } diff --git a/fda-query-service/services/src/main/java/at/tuwien/exception/QueryStoreException.java b/fda-query-service/services/src/main/java/at/tuwien/exception/QueryStoreException.java index 34acccda4c0479256ec172ee4c1fc95db5ee77ea..b1f472f2a1179200475b7952dd459af0ee7f7b6a 100644 --- a/fda-query-service/services/src/main/java/at/tuwien/exception/QueryStoreException.java +++ b/fda-query-service/services/src/main/java/at/tuwien/exception/QueryStoreException.java @@ -3,7 +3,7 @@ package at.tuwien.exception; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; -@ResponseStatus(code = HttpStatus.CONFLICT) +@ResponseStatus(code = HttpStatus.GATEWAY_TIMEOUT) public class QueryStoreException extends Exception { public QueryStoreException(String msg) { diff --git a/fda-query-service/services/src/main/java/at/tuwien/service/impl/StoreServiceImpl.java b/fda-query-service/services/src/main/java/at/tuwien/service/impl/StoreServiceImpl.java index 98a558385b1db42affae7f047b4ba48dd48e74ea..bd7bd8a0caa5b7f5621e7eb84b6f26c0dfda5e67 100644 --- a/fda-query-service/services/src/main/java/at/tuwien/service/impl/StoreServiceImpl.java +++ b/fda-query-service/services/src/main/java/at/tuwien/service/impl/StoreServiceImpl.java @@ -59,7 +59,7 @@ public class StoreServiceImpl extends HibernateConnector implements StoreService return storeMapper.resultSetToQueryList(resultSet); } catch (SQLException e) { log.error("Failed to find queries: {}", e.getMessage()); - throw new QueryStoreException("Failed to find queries"); + throw new QueryStoreException("Failed to find queries: " + e.getMessage()); } finally { dataSource.close(); } diff --git a/fda-ui/components/QueryList.vue b/fda-ui/components/QueryList.vue index 1c9b15ce5dbb42597902316dba625bd92a256ba9..6bf99aad462b3870c3c3a3cea8ee45076bfc4c85 100644 --- a/fda-ui/components/QueryList.vue +++ b/fda-ui/components/QueryList.vue @@ -1,11 +1,11 @@ <template> <div> - <v-progress-linear v-if="loading" indeterminate /> + <v-progress-linear v-if="loading || error" :color="loadingColor" :value="loadProgress" /> <v-tabs-items> <v-card v-if="!loading && queries.length === 0" flat> <v-card-text v-text="emptyMessage" /> </v-card> - <v-expansion-panels v-if="!loading && queries.length > 0" accordion> + <v-expansion-panels v-if="queries.length > 0" accordion> <v-expansion-panel v-for="(item, i) in queries" :key="i" @click="details(item)"> <v-expansion-panel-header> <pre>{{ item.query }}</pre> @@ -91,6 +91,8 @@ export default { data () { return { loading: false, + loadProgress: 0, + error: false, queries: [], user: { username: null @@ -127,6 +129,9 @@ export default { headers: { Authorization: `Bearer ${this.token}` } } }, + loadingColor () { + return this.error ? 'error' : 'primary' + }, executionUTC () { return formatTimestampUTCLabel(this.queryDetails.execution) }, @@ -146,8 +151,11 @@ export default { mounted () { this.loadUser() this.loadDatabase() - .then(() => this.loadQueries()) .then(() => this.loadIdentifiers()) + .then(() => { + this.simulateProgress() + this.loadQueries() + }) }, methods: { async loadQueries () { @@ -157,10 +165,17 @@ export default { try { this.loading = true const res = await this.$axios.get(`/api/container/${this.$route.params.container_id}/database/${this.$route.params.database_id}/query?persisted=true`, this.config) - this.queries = res.data + res.data.forEach((query) => { + if (this.queries.filter(q => q.id === query.id).length > 0) { + return + } + this.queries.push(query) + }) console.debug('queries', this.queries) } catch (err) { - this.$toast.error('Could not list queries') + this.error = true + console.error('Connection to query store failed', err.response.data) + this.$toast.error(err.response.data.message) } this.loading = false }, @@ -172,15 +187,24 @@ export default { this.loading = true const res = await this.$axios.get(`/api/identifier?dbid=${this.$route.params.database_id}`, this.config) const identifiers = res.data.filter(i => i.type === 'subset') - this.queries.forEach((query) => { - const id = identifiers.filter(i => i.container_id === query.cid && i.database_id === query.dbid && i.query_id === query.id) - if (id.length === 1) { - query.identifier = id[0] + const queries = identifiers.map((identifier) => { + const query = { + id: identifier.query_id, + identifier, + type: identifier.type, + query: identifier.query, + query_hash: identifier.query_hash, + result_hash: identifier.result_hash, + created: identifier.created, + execution: identifier.execution } + return query }) - console.debug('identifiers', identifiers) + this.queries = queries + console.debug('identifier queries', queries) } catch (err) { - this.$toast.error('Could not list identifiers') + console.error('Failed to load identifiers', err.response.data) + this.$toast.error('Failed to load identifiers') } this.loading = false }, @@ -205,6 +229,20 @@ export default { } this.user.username = decodeJwt(this.token).sub }, + simulateProgress () { + if (this.loadProgress !== 0) { + return + } + const timeout = 30 * 1000 /* ms */ + const ticks = 100 /* ms */ + let i = 0 + setInterval(() => { + if (i++ >= timeout && !this.error) { + return + } + this.loadProgress = ((i * 100) / timeout) * 100 + }, ticks) + }, async loadDatabase () { try { this.loading = true @@ -213,7 +251,7 @@ export default { console.debug('database', this.database) } catch (err) { this.error = true - this.$toast.error('Could not get database details.') + this.$toast.error('Could not get database details') } this.loading = false } diff --git a/fda-ui/pages/container/_container_id/database/_database_id/query/_query_id/index.vue b/fda-ui/pages/container/_container_id/database/_database_id/query/_query_id/index.vue index b8f8e449911a9c058899bafc8bcc466f74b91faf..5535f5f4a36de0aff892c896a623857df1ddbddf 100644 --- a/fda-ui/pages/container/_container_id/database/_database_id/query/_query_id/index.vue +++ b/fda-ui/pages/container/_container_id/database/_database_id/query/_query_id/index.vue @@ -34,6 +34,7 @@ </v-btn> </v-toolbar-title> </v-toolbar> + <v-progress-linear v-if="loadingQuery || loadingIdentifier || loadingDatabase || error" :color="loadingColor" :value="loadProgress" /> <v-card flat tile> <v-card-title> Subset Information @@ -158,22 +159,22 @@ Query Statement </v-list-item-title> <v-list-item-content> - <v-skeleton-loader v-if="loadingQuery" type="text" class="skeleton-large" /> - <pre v-if="!loadingQuery">{{ query_statement }}</pre> + <v-skeleton-loader v-if="!query_statement" type="text" class="skeleton-large" /> + <pre v-if="query_statement">{{ query_statement }}</pre> </v-list-item-content> <v-list-item-title class="mt-2"> Subset Hash </v-list-item-title> <v-list-item-content> - <v-skeleton-loader v-if="loadingQuery" type="text" class="skeleton-medium" /> - <pre v-if="!loadingQuery">{{ query_hash }}</pre> + <v-skeleton-loader v-if="!query_hash" type="text" class="skeleton-medium" /> + <pre v-if="query_hash">{{ query_hash }}</pre> </v-list-item-content> <v-list-item-title class="mt-2"> Subset Creator </v-list-item-title> <v-list-item-content> - <v-skeleton-loader v-if="loadingQuery" type="text" class="skeleton-small" /> - <span v-if="!loadingQuery"> + <v-skeleton-loader v-if="!creator" type="text" class="skeleton-small" /> + <span v-if="creator"> {{ creator }} <sup> <v-icon v-if="database.creator.email_verified" small color="primary">mdi-check-decagram</v-icon> </sup> @@ -183,8 +184,8 @@ Subset Creation </v-list-item-title> <v-list-item-content> - <v-skeleton-loader v-if="loadingQuery" type="text" class="skeleton-small" /> - <span v-if="!loadingQuery">{{ executionUTC }}</span> + <v-skeleton-loader v-if="!executionUTC" type="text" class="skeleton-small" /> + <span v-if="executionUTC">{{ executionUTC }}</span> </v-list-item-content> </v-list-item-content> </v-list-item> @@ -300,11 +301,16 @@ export default { query_normalized: null, query_hash: null, result_number: null, + result_hash: null, execution: null, publication_year: null, publication_month: null, publication_day: null, related: [], + creator: { + username: null, + id: null + }, doi: null, creators: [] }, @@ -330,6 +336,7 @@ export default { metadataLoading: false, downloadLoading: false, error: false, + loadProgress: 0, promises: [] } }, @@ -341,7 +348,7 @@ export default { return location.protocol + '//' + location.host }, loadingColor () { - return this.error ? 'red' : 'primary' + return this.error ? 'error' : 'primary' }, token () { return this.$store.state.token @@ -428,6 +435,13 @@ export default { return this.identifier.id ? formatTimestampUTCLabel(this.identifier.execution) : formatTimestampUTCLabel(this.query.execution) }, creator () { + if (this.identifier.creator.username !== null) { + if (this.identifier.creator.firstname === null || this.identifier.creator.lastname === null) { + return this.identifier.creator.username + } else { + return this.identifier.creator.firstname + ' ' + this.identifier.creator.lastname + } + } if (this.query.creator.username === null) { return null } @@ -440,15 +454,21 @@ export default { return this.identifier.id ? this.identifier.creators : null }, erroneous () { + if (this.identifier.result_number) { + return false + } return !this.query.result_hash } }, mounted () { this.loadUser() this.loadDatabase() - .then(() => this.loadQuery()) - .then(() => this.loadResult()) .then(() => this.loadMetadata()) + .then(() => { + this.simulateProgress() + this.loadQuery() + }) + .then(() => this.loadResult()) }, methods: { loadResult () { @@ -483,6 +503,20 @@ export default { this.downloadLoading = false this.metadataLoading = false }, + simulateProgress () { + if (this.loadProgress !== 0) { + return + } + const timeout = 30 * 1000 /* ms */ + const ticks = 100 /* ms */ + let i = 0 + setInterval(() => { + if (i++ >= timeout && !this.error) { + return + } + this.loadProgress = ((i * 100) / timeout) * 100 + }, ticks) + }, async downloadData () { this.downloadLoading = true try {