Skip to content
Snippets Groups Projects
Unverified Commit 750e6a2a authored by Martin Weise's avatar Martin Weise
Browse files

Own component for advanced search

parent 926683fd
Branches
Tags
4 merge requests!231CI: Remove build for log-service,!228Better error message handling in the frontend,!223Release of version 1.4.0,!215Resolve "Fix the unit independent search"
...@@ -26,34 +26,42 @@ public class ContainerDto { ...@@ -26,34 +26,42 @@ public class ContainerDto {
private Long id; private Long id;
@NotBlank @NotBlank
@Field(name = "name", type = FieldType.Keyword)
@Schema(example = "Air Quality") @Schema(example = "Air Quality")
private String name; private String name;
@NotBlank @NotBlank
@JsonProperty("internal_name") @JsonProperty("internal_name")
@Field(name = "internal_name") @Field(name = "internal_name", type = FieldType.Keyword)
@Schema(example = "data-db") @Schema(example = "data-db")
private String internalName; private String internalName;
@NotBlank @NotBlank
@Field(name = "host", type = FieldType.Keyword)
private String host; private String host;
@Field(name = "port", type = FieldType.Keyword)
private Integer port; private Integer port;
@NotBlank @NotBlank
@JsonProperty("sidecar_host") @JsonProperty("sidecar_host")
@Field(name = "sidecar_host", type = FieldType.Keyword)
private String sidecarHost; private String sidecarHost;
@NotNull @NotNull
@JsonProperty("sidecar_port") @JsonProperty("sidecar_port")
@Field(name = "sidecar_port", type = FieldType.Keyword)
private Integer sidecarPort; private Integer sidecarPort;
@JsonProperty("ui_host") @JsonProperty("ui_host")
@Field(name = "ui_host", type = FieldType.Keyword)
private String uiHost; private String uiHost;
@JsonProperty("ui_port") @JsonProperty("ui_port")
@Field(name = "ui_port", type = FieldType.Keyword)
private Integer uiPort; private Integer uiPort;
@Field(name = "image", includeInParent = true, type = FieldType.Nested)
private ImageBriefDto image; private ImageBriefDto image;
@NotNull @NotNull
......
...@@ -5,6 +5,8 @@ import jakarta.validation.constraints.NotBlank; ...@@ -5,6 +5,8 @@ import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.*; import lombok.*;
import lombok.extern.jackson.Jacksonized; import lombok.extern.jackson.Jacksonized;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
@Getter @Getter
@Setter @Setter
...@@ -16,13 +18,16 @@ import lombok.extern.jackson.Jacksonized; ...@@ -16,13 +18,16 @@ import lombok.extern.jackson.Jacksonized;
public class ImageBriefDto { public class ImageBriefDto {
@NotNull @NotNull
@Field(name = "id", type = FieldType.Keyword)
private Long id; private Long id;
@NotBlank @NotBlank
@Field(name = "name", type = FieldType.Keyword)
@Schema(example = "mariadb") @Schema(example = "mariadb")
private String name; private String name;
@NotBlank @NotBlank
@Field(name = "version", type = FieldType.Keyword)
@Schema(example = "10.5") @Schema(example = "10.5")
private String version; private String version;
......
...@@ -31,6 +31,7 @@ import java.util.UUID; ...@@ -31,6 +31,7 @@ import java.util.UUID;
@UniqueConstraint(columnNames = {"cid", "internalName"}) @UniqueConstraint(columnNames = {"cid", "internalName"})
}) })
@NamedQueries({ @NamedQueries({
@NamedQuery(name = "Database.findAll", query = "select d from Database d order by d.created desc"),
@NamedQuery(name = "Database.findReadAccess", query = "select distinct d from Database d join DatabaseAccess a on a.hdbid = d.id and a.huserid = ?1"), @NamedQuery(name = "Database.findReadAccess", query = "select distinct d from Database d join DatabaseAccess a on a.hdbid = d.id and a.huserid = ?1"),
@NamedQuery(name = "Database.findWriteAccess", query = "select distinct d from Database d join DatabaseAccess a on a.hdbid = d.id and a.huserid = ?1 where a.type = 'WRITE_OWN' or a.type = 'WRITE_ALL'"), @NamedQuery(name = "Database.findWriteAccess", query = "select distinct d from Database d join DatabaseAccess a on a.hdbid = d.id and a.huserid = ?1 where a.type = 'WRITE_OWN' or a.type = 'WRITE_ALL'"),
@NamedQuery(name = "Database.findConfigureAccess", query = "select distinct d from Database d where d.ownedBy = ?1"), @NamedQuery(name = "Database.findConfigureAccess", query = "select distinct d from Database d where d.ownedBy = ?1"),
......
""" """
The opensearch_client.py is used by the different API endpoints in routes.py to handle requests to the opensearch db The opensearch_client.py is used by the different API endpoints in routes.py to handle requests to the opensearch db
""" """
import json
import logging import logging
import re
from flask import current_app from flask import current_app
from collections.abc import MutableMapping from collections.abc import MutableMapping
...@@ -127,7 +126,7 @@ def general_search(search_term=None, t1=None, t2=None, fieldValuePairs=None): ...@@ -127,7 +126,7 @@ def general_search(search_term=None, t1=None, t2=None, fieldValuePairs=None):
searchable_indices = ["database", "user", "table", "column", "identifier", "view", "concept", "unit"] searchable_indices = ["database", "user", "table", "column", "identifier", "view", "concept", "unit"]
index = searchable_indices index = searchable_indices
field_list = [ field_list = [
"name", "table.name",
"identifier.titles.title", "identifier.titles.title",
"identifier.descriptions.description", "identifier.descriptions.description",
"identifier.publisher", "identifier.publisher",
...@@ -137,13 +136,19 @@ def general_search(search_term=None, t1=None, t2=None, fieldValuePairs=None): ...@@ -137,13 +136,19 @@ def general_search(search_term=None, t1=None, t2=None, fieldValuePairs=None):
"column.column_type", "column.column_type",
"column.is_null_allowed", "column.is_null_allowed",
"column.is_primary_key", "column.is_primary_key",
"unit.uri",
"unit.name",
"unit.description",
"concept.uri",
"concept.name",
"concept.description",
"funders", "funders",
"title", "title",
"description", "description",
"creator.username", "creator.username",
"concept.name",
"concept.uri",
"author", "author",
"name",
"uri",
"database.*", "database.*",
"internal_name", "internal_name",
"is_public", "is_public",
...@@ -151,14 +156,23 @@ def general_search(search_term=None, t1=None, t2=None, fieldValuePairs=None): ...@@ -151,14 +156,23 @@ def general_search(search_term=None, t1=None, t2=None, fieldValuePairs=None):
queries = [] queries = []
if search_term is not None: if search_term is not None:
logging.debug('query has search_term present') logging.debug('query has search_term present')
text_query = { fuzzy_body = {
"query": {
"multi_match": { "multi_match": {
"fields": field_list,
"query": search_term, "query": search_term,
"fuzziness": "AUTO", "fuzziness": "AUTO",
"fuzzy_transpositions": True,
"minimum_should_match": 3
}
} }
} }
queries.append(text_query) logging.debug('search body: %s', fuzzy_body)
response = current_app.opensearch_client.search(
index=index,
body=fuzzy_body
)
response["status"] = 200
return response
if t1 and t2 is not None: if t1 and t2 is not None:
logging.debug('query has time range present') logging.debug('query has time range present')
time_range_query = { time_range_query = {
...@@ -170,28 +184,27 @@ def general_search(search_term=None, t1=None, t2=None, fieldValuePairs=None): ...@@ -170,28 +184,27 @@ def general_search(search_term=None, t1=None, t2=None, fieldValuePairs=None):
} }
} }
queries.append(time_range_query) queries.append(time_range_query)
if fieldValuePairs is not None: if fieldValuePairs is not None and len(fieldValuePairs) > 0:
logging.debug('query has fieldValuePairs present') logging.debug('query has fieldValuePairs present')
musts = [] musts = []
for field, value in fieldValuePairs.items(): for key, value in fieldValuePairs.items():
if field == "type" and value in searchable_indices: if key == "type" and value in searchable_indices:
logging.debug("search for specific index: %s", value) logging.debug("search for specific index: %s", value)
index = value index = value
continue continue
if field in field_list: if key in field_list:
if field.startswith(index) and "." in field: if re.match(f"{key}\\.", key):
new_field = field[field.index(".") + 1:len(field)] new_field = key[key.index(".") + 1:len(key)]
logging.debug( logging.debug(
f"field name {field} starts with index name {index}: flattened field name to {new_field}") f"field name {key} starts with index name {index}: flattened field name to {new_field}")
field = new_field key = new_field
musts.append({ musts.append({
"match": { "match": {
field: {"query": value, "minimum_should_match": "90%"} key: {"query": value, "minimum_should_match": "90%"}
} }
}) })
specific_query = {"bool": {"must": musts}} specific_query = {"bool": {"must": musts}}
queries.append(specific_query) queries.append(specific_query)
logging.debug("queries: %s", queries)
body = { body = {
"query": {"bool": {"must": queries}}, "query": {"bool": {"must": queries}},
"_source": [ "_source": [
......
...@@ -23,7 +23,7 @@ class SearchService { ...@@ -23,7 +23,7 @@ class SearchService {
// transform values to what the search API expects // transform values to what the search API expects
const searchTerm = searchData.search_term const searchTerm = searchData.search_term
delete searchData.search_term delete searchData.search_term
searchData = Object.fromEntries(Object.entries(searchData).filter(([_, v]) => v != null)) // https://stackoverflow.com/questions/286141/remove-blank-attributes-from-an-object-in-javascript searchData = Object.fromEntries(Object.entries(searchData).filter(([_, v]) => v != null && v !== '')) // https://stackoverflow.com/questions/286141/remove-blank-attributes-from-an-object-in-javascript
const payload = { const payload = {
search_term: searchTerm, search_term: searchTerm,
field_value_pairs: { ...searchData } field_value_pairs: { ...searchData }
......
<template>
<div>
<v-card flat tile>
<v-card-text class="pt-0 pl-4 pb-6 pr-4">
<v-row dense>
<v-col cols="3">
<v-select
v-model="advancedSearchData.type"
:items="fieldItems"
item-text="name"
item-value="value"
solo
label="Type" />
</v-col>
</v-row>
<v-row dense>
<v-col cols="3">
<v-text-field
v-model="advancedSearchData.id"
clearable
label="ID" />
</v-col>
<v-col cols="3">
<v-text-field
v-if="!hideFields.hideNameField"
v-model="advancedSearchData.name"
clearable
label="Name" />
</v-col>
<v-col cols="3">
<v-text-field
v-if="!hideFields.hideInternalNameField"
v-model="advancedSearchData.internal_name"
clearable
label="Internal Name" />
</v-col>
</v-row>
<v-row v-if="loadingFields" dense>
<v-progress-circular color="primary" indeterminate />
</v-row>
<v-row v-if="!loadingFields && renderedFields" dense>
<v-col v-for="field in renderedFields" :key="`f-${field.attribute_name}`" cols="3">
<v-select
v-if="field.type === 'boolean'"
v-model="advancedSearchData[generateDynamicVModelKey(field)]"
clearable
:items="booleanItems"
item-text="name"
item-value="value"
:label="generateFriendlyName(field)" />
<v-text-field
v-if="(field.type === 'keyword' && field.attribute_name !== 'column_type') || field.type === 'text' || field.type === 'date'"
v-model="advancedSearchData[generateDynamicVModelKey(field)]"
type="text"
:label="generateFriendlyName(field)"
clearable />
<v-select
v-if="field.type === 'keyword' && field.attribute_name === 'column_type'"
v-model="advancedSearchData[generateDynamicVModelKey(field)]"
:items="columnTypes"
item-value="value"
clearable
:label="generateFriendlyName(field)" />
<v-text-field
v-if="field.type === 'integer'"
v-model="advancedSearchData[generateDynamicVModelKey(field)]"
type="number"
:label="generateFriendlyName(field)"
clearable />
</v-col>
</v-row>
<v-row dense>
<v-btn class="mr-2" color="primary" :loading="loading" small @click="advancedSearch">
Search
</v-btn>
</v-row>
</v-card-text>
</v-card>
</div>
</template>
<script>
import SearchService from '@/api/search.service'
import QueryMapper from '@/api/query.mapper'
export default {
data () {
return {
loading: false,
loadingFields: false,
showAdvancedSearch: false,
columnTypes: QueryMapper.mySql8DataTypes().map((datatype) => {
datatype.value = datatype.value.toUpperCase()
return datatype
}),
fieldItems: [
{ name: 'Database', value: 'database' },
{ name: 'Table', value: 'table' },
{ name: 'Column', value: 'column' },
{ name: 'User', value: 'user' },
{ name: 'Identifier', value: 'identifier' },
{ name: 'Concept', value: 'concept' },
{ name: 'Unit', value: 'unit' },
{ name: 'View', value: 'view' }
],
booleanItems: [
{ name: 'True', value: true },
{ name: 'False', value: false }
],
fieldsResponse: null,
renderedFields: [],
advancedSearchData: {
name: null,
internal_name: null,
id: null,
type: 'database'
}
}
},
computed: {
hideFields () {
const selectedOption = this.advancedSearchData.type
return {
hideNameField: selectedOption === 'identifier',
hideInternalNameField: ['identifier', 'user', 'concept', 'unit'].includes(selectedOption)
}
}
},
watch: {
'advancedSearchData.type': {
handler (newType, oldType) {
if (!newType) {
return
}
console.debug('switched advanced search type to', newType)
this.resetAdvancedSearchFields()
this.loadingFields = true
SearchService.getFields(newType)
.then((response) => {
this.loadingFields = false
const { fields } = response
this.renderedFields = fields.filter(field => this.shouldRenderItem(field))
})
.finally(() => {
this.loadingFields = false
})
},
immediate: true
}
},
mounted () {
this.advancedSearch()
},
methods: {
/* Removes all advanced search fields when switching the type */
resetAdvancedSearchFields () {
Object.keys(this.advancedSearchData)
.filter(k => !['name', 'internal_name', 'id', 'type'].includes(k))
.forEach(k => delete this.advancedSearchData[k])
},
advancedSearch () {
console.debug('performing advanced search')
if (this.search) {
this.advancedSearchData.search_term = this.search
} else {
delete this.advancedSearchData.search_term
}
this.loading = true
SearchService.search(this.advancedSearchData)
.then((response) => {
this.$emit('search-result', response.map(h => h._source))
})
.finally(() => {
this.loading = false
})
},
isAdvancedSearchEmpty () {
return !(
this.advancedSearchData.type ||
this.advancedSearchData.id ||
this.advancedSearchData.name ||
this.advancedSearchData.internal_name
)
},
dynamicFieldsMap () {
// Defines a mapping to narrow down the fields rendered for the advanced search
return {
database: ['created', 'description', 'is_public'],
table: ['created', 'description', 'is_public'],
column: ['column_type', 'is_primary_key', 'is_null_allowed'],
user: ['firstname', 'lastname', 'username'],
identifier: [
'creators.properties.creator_name', 'creators.properties.name_identifier',
'descriptions.properties.description', 'doi', 'funders.properties.funder_identifier',
'licenses', 'publication_year', 'titles.properties.title', 'visibility'
],
view: ['is_public', 'query'],
concept: ['uri'],
unit: ['uri']
}
},
getLastFlattenedItem (str) {
// Returns substring after the last dot otherwise the string itself if no dots are contained
if (!str) { return '' }
// Check if string is a flattened nested object
return str.includes('.') ? str.split('.').slice(-1)[0] : str
},
generateFriendlyName (item) {
// Generates a proper name to be displayed with the dynamic component
if (!item) { return '' }
const specialAbbreviations = {
doi: 'DOI',
uri: 'URI'
// Add more abbreviations here, if needed
}
const str = this.getLastFlattenedItem(item.attribute_name)
return str.split('_').map((word) => {
const lowerWord = word.toLowerCase()
return specialAbbreviations[lowerWord] || (word.charAt(0).toUpperCase() + word.slice(1))
}).join(' ')
},
generateDynamicVModelKey (item) {
// Generates a dynamic v-model; It will be attached to the advancedSearchData object
if (!item) { return '' }
return `${this.advancedSearchData.type}.${item.attribute_name}`
},
shouldRenderItem (item) {
// Checks if item's attribute_name matches any wanted field
// The expected response is of a flattened format, so this method must be modified accordingly if the response is changed
return this.dynamicFieldsMap()[this.advancedSearchData.type].includes(item.attribute_name)
}
}
}
</script>
...@@ -98,9 +98,6 @@ ...@@ -98,9 +98,6 @@
append-icon="mdi-magnify" append-icon="mdi-magnify"
:placeholder="$t('search.fuzzy.placeholder', { name: 'vue-i18n' })" :placeholder="$t('search.fuzzy.placeholder', { name: 'vue-i18n' })"
@click:append="retrieve" /> @click:append="retrieve" />
<v-btn class="ml-2" plain type="submit" name="search-advanced" @click="toggleAdvancedSearch">
Advanced
</v-btn>
<v-spacer /> <v-spacer />
<v-btn <v-btn
v-if="!user" v-if="!user"
...@@ -133,14 +130,6 @@ ...@@ -133,14 +130,6 @@
</v-btn> </v-btn>
</template> </template>
<v-list> <v-list>
<!--
<v-list-item
v-for="locale in []"
:key="locale.code"
:to="switchLocalePath(locale.code)">
<v-list-item-title>{{ locale.name }}</v-list-item-title>
</v-list-item>
-->
<v-list-item <v-list-item
v-if="user" v-if="user"
@click="logout"> @click="logout">
...@@ -151,87 +140,6 @@ ...@@ -151,87 +140,6 @@
</v-app-bar> </v-app-bar>
</v-form> </v-form>
<v-main> <v-main>
<!-- Advanced Search card -->
<v-card v-if="showAdvancedSearch" id="advanced_search" flat tile>
<v-card-text>
<v-container fluid>
<v-row>
<v-col cols="auto">
<v-select
v-model="advancedSearchData.type"
:items="fieldItems"
item-text="name"
item-value="value"
label="Type" />
</v-col>
<v-col cols="auto">
<v-text-field v-model="advancedSearchData.id" clearable label="ID" variant="underlined" />
</v-col>
<v-col cols="auto">
<v-text-field
v-if="!hideFields.hideNameField"
v-model="advancedSearchData.name"
clearable
label="Name"
variant="underlined" />
</v-col>
<v-col cols="auto">
<v-text-field
v-if="!hideFields.hideInternalNameField"
v-model="advancedSearchData.internal_name"
clearable
label="Internal Name"
variant="underlined" />
</v-col>
</v-row>
<v-row v-if="fieldsResponse">
<!-- Loop through fields of Response -->
<span v-for="field in fieldsResponse.fields" :key="`${field.attribute_name}`">
<!-- Loop through "fields" list -->
<template v-if="shouldRenderItem(field)">
<v-col cols="auto">
<v-select
v-if="field.type === 'boolean'"
v-model="advancedSearchData[generateDynamicVModelKey(field)]"
clearable
:items="booleanItems"
item-text="name"
item-value="value"
:label="generateFriendlyName(field)" />
<v-text-field
v-if="(field.type === 'keyword' && field.attribute_name !== 'column_type') || field.type === 'text' || field.type === 'date'"
v-model="advancedSearchData[generateDynamicVModelKey(field)]"
type="text"
:label="generateFriendlyName(field)"
clearable />
<v-select
v-if="field.type === 'keyword' && field.attribute_name === 'column_type'"
v-model="advancedSearchData[generateDynamicVModelKey(field)]"
:items="columnTypes"
item-value="value"
clearable
:label="generateFriendlyName(field)" />
<v-text-field
v-if="field.type === 'integer'"
v-model="advancedSearchData[generateDynamicVModelKey(field)]"
type="number"
:label="generateFriendlyName(field)"
clearable />
</v-col>
</template>
</span>
</v-row>
</v-container>
</v-card-text>
<v-card-text>
<v-btn class="mr-2" color="primary" small @click="advancedSearch">
Search
</v-btn>
<v-btn small @click="toggleAdvancedSearch">
Cancel
</v-btn>
</v-card-text>
</v-card>
<v-container> <v-container>
<nuxt /> <nuxt />
</v-container> </v-container>
...@@ -240,12 +148,9 @@ ...@@ -240,12 +148,9 @@
</template> </template>
<script> <script>
import SearchService from '@/api/search.service'
import AuthenticationService from '@/api/authentication.service' import AuthenticationService from '@/api/authentication.service'
import DatabaseService from '@/api/database.service' import DatabaseService from '@/api/database.service'
import EventBus from '@/api/eventBus'
import TableService from '@/api/table.service' import TableService from '@/api/table.service'
import QueryMapper from '@/api/query.mapper'
export default { export default {
data () { data () {
...@@ -259,33 +164,7 @@ export default { ...@@ -259,33 +164,7 @@ export default {
loadingUser: true, loadingUser: true,
loadingSearch: false, loadingSearch: false,
loadingDatabases: false, loadingDatabases: false,
search: null, search: null
showAdvancedSearch: false,
columnTypes: QueryMapper.mySql8DataTypes().map((datatype) => {
datatype.value = datatype.value.toUpperCase()
return datatype
}),
fieldItems: [
{ name: 'Database', value: 'database' },
{ name: 'Table', value: 'table' },
{ name: 'Column', value: 'column' },
{ name: 'User', value: 'user' },
{ name: 'Identifier', value: 'identifier' },
{ name: 'Concept', value: 'concept' },
{ name: 'Unit', value: 'unit' },
{ name: 'View', value: 'view' }
],
booleanItems: [
{ name: 'True', value: true },
{ name: 'False', value: false }
],
fieldsResponse: null,
advancedSearchData: {
name: null,
internal_name: null,
id: null,
type: null
}
} }
}, },
computed: { computed: {
...@@ -333,13 +212,6 @@ export default { ...@@ -333,13 +212,6 @@ export default {
}, },
databaseCount () { databaseCount () {
return this.$store.state.databaseCount return this.$store.state.databaseCount
},
hideFields () {
const selectedOption = this.advancedSearchData.type
return {
hideNameField: selectedOption === 'identifier',
hideInternalNameField: ['identifier', 'user', 'concept', 'unit'].includes(selectedOption)
}
} }
}, },
watch: { watch: {
...@@ -374,20 +246,6 @@ export default { ...@@ -374,20 +246,6 @@ export default {
}, },
deep: true, deep: true,
immediate: true immediate: true
},
'advancedSearchData.type': {
handler (newType, oldType) {
if (!newType) {
return
}
console.debug('switched advanced search type to', newType)
this.resetAdvancedSearchFields()
SearchService.getFields(newType)
.then((response) => {
this.fieldsResponse = response
})
},
immediate: true
} }
}, },
mounted () { mounted () {
...@@ -410,12 +268,6 @@ export default { ...@@ -410,12 +268,6 @@ export default {
submit () { submit () {
this.$refs.form.validate() this.$refs.form.validate()
}, },
/* Removes all advanced search fields when switching the type */
resetAdvancedSearchFields () {
Object.keys(this.advancedSearchData)
.filter(k => !['name', 'internal_name', 'id', 'type'].includes(k))
.forEach(k => delete this.advancedSearchData[k])
},
login () { login () {
const redirect = ![undefined, '/', '/login'].includes(this.$router.currentRoute.path) const redirect = ![undefined, '/', '/login'].includes(this.$router.currentRoute.path)
this.$router.push({ path: '/login', query: redirect ? { redirect: this.$router.currentRoute.path } : {} }) this.$router.push({ path: '/login', query: redirect ? { redirect: this.$router.currentRoute.path } : {} })
...@@ -487,89 +339,12 @@ export default { ...@@ -487,89 +339,12 @@ export default {
this.$store.commit('SET_SEARCH_PASSWORD', this.$config.searchPassword) this.$store.commit('SET_SEARCH_PASSWORD', this.$config.searchPassword)
this.$store.commit('SET_DOI_URL', this.$config.doiUrl) this.$store.commit('SET_DOI_URL', this.$config.doiUrl)
console.debug('runtime config', this.$config) console.debug('runtime config', this.$config)
},
advancedSearch () {
console.debug('performing advanced search')
if (this.search) {
this.advancedSearchData.search_term = this.search
} else {
delete this.advancedSearchData.search_term
}
EventBus.$emit('advancedSearchButtonClicked')
this.$router.push({ path: '/search' })
},
toggleAdvancedSearch () {
this.showAdvancedSearch = !this.showAdvancedSearch
},
isAdvancedSearchEmpty () {
return !(
this.advancedSearchData.type ||
this.advancedSearchData.id ||
this.advancedSearchData.name ||
this.advancedSearchData.internal_name
)
},
dynamicFieldsMap () {
// Defines a mapping to narrow down the fields rendered for the advanced search
return {
database: ['created', 'description', 'is_public'],
table: ['created', 'description', 'is_public'],
column: ['column_type', 'is_primary_key', 'is_null_allowed'],
user: ['firstname', 'lastname', 'username'],
identifier: [
'creators.properties.creator_name', 'creators.properties.name_identifier',
'descriptions.properties.description', 'doi', 'funders.properties.funder_identifier',
'licenses', 'publication_year', 'titles.properties.title', 'visibility'
],
view: ['is_public', 'query'],
concept: ['uri'],
unit: ['uri']
}
},
getLastFlattenedItem (str) {
// Returns substring after the last dot otherwise the string itself if no dots are contained
if (!str) { return '' }
// Check if string is a flattened nested object
return str.includes('.') ? str.split('.').slice(-1)[0] : str
},
generateFriendlyName (item) {
// Generates a proper name to be displayed with the dynamic component
if (!item) { return '' }
const specialAbbreviations = {
doi: 'DOI',
uri: 'URI'
// Add more abbreviations here, if needed
}
const str = this.getLastFlattenedItem(item.attribute_name)
return str.split('_').map((word) => {
const lowerWord = word.toLowerCase()
return specialAbbreviations[lowerWord] || (word.charAt(0).toUpperCase() + word.slice(1))
}).join(' ')
},
generateDynamicVModelKey (item) {
// Generates a dynamic v-model; It will be attached to the advancedSearchData object
if (!item) { return '' }
return `${this.advancedSearchData.type}.${item.attribute_name}`
},
shouldRenderItem (item) {
// Checks if item's attribute_name matches any wanted field
// The expected response is of a flattened format, so this method must be modified accordingly if the response is changed
return this.dynamicFieldsMap()[this.advancedSearchData.type].includes(item.attribute_name)
} }
}, },
head () { head () {
return { return {
title: this.$config.title title: this.$config.title
} }
},
provide () {
return {
advancedSearchData: this.advancedSearchData
}
} }
} }
</script> </script>
......
<template> <template>
<div> <div>
<v-toolbar flat>
<v-toolbar-title v-text="$t('databases.recent', { name: 'vue-i18n' })" />
</v-toolbar>
<v-card flat tile> <v-card flat tile>
<v-divider class="mx-4" />
<v-card-text v-if="infoLinks && infoLinks.length > 0"> <v-card-text v-if="infoLinks && infoLinks.length > 0">
<div class="mb-2">Important Links</div> <div class="mb-2">Important Links</div>
<div class="text--primary"> <div class="text--primary">
...@@ -13,11 +17,7 @@ ...@@ -13,11 +17,7 @@
</ul> </ul>
</div> </div>
</v-card-text> </v-card-text>
<v-divider class="mx-4" />
</v-card> </v-card>
<v-toolbar flat>
<v-toolbar-title v-text="$t('databases.recent', { name: 'vue-i18n' })" />
</v-toolbar>
<DatabaseList ref="databases" /> <DatabaseList ref="databases" />
</div> </div>
</template> </template>
......
<template> <template>
<div> <div>
<v-toolbar flat> <v-toolbar flat tile>
<v-toolbar-title v-text="header" /> <v-toolbar-title v-text="header" />
<v-spacer /> <v-spacer />
<v-toolbar-title> <v-toolbar-title>
...@@ -9,7 +9,9 @@ ...@@ -9,7 +9,9 @@
</v-btn> </v-btn>
</v-toolbar-title> </v-toolbar-title>
</v-toolbar> </v-toolbar>
<v-progress-linear v-if="loading" color="primary" /> <v-card flat tile>
<AdvancedSearch ref="adv" @search-result="onSearchResult" />
</v-card>
<v-card <v-card
v-for="(result, idx) in results" v-for="(result, idx) in results"
:key="idx" :key="idx"
...@@ -45,15 +47,15 @@ ...@@ -45,15 +47,15 @@
</template> </template>
<script> <script>
import EventBus from '@/api/eventBus'
import SearchService from '@/api/search.service' import SearchService from '@/api/search.service'
import CreateDB from '@/components/dialogs/CreateDB' import CreateDB from '@/components/dialogs/CreateDB'
import AdvancedSearch from '@/components/search/AdvancedSearch'
export default { export default {
components: { components: {
CreateDB CreateDB,
AdvancedSearch
}, },
inject: ['advancedSearchData'],
data () { data () {
return { return {
results: [], results: [],
...@@ -92,39 +94,14 @@ export default { ...@@ -92,39 +94,14 @@ export default {
}, },
watch: { watch: {
'$route.query.q': { '$route.query.q': {
handler (query) { handler () {
if (this.advancedSearchData) {
return
}
this.retrieve() this.retrieve()
},
deep: true,
immediate: true
},
'$route.query.t': {
handler (type) {
if (this.advancedSearchData) {
return
} }
this.retrieve()
},
deep: true,
immediate: true
} }
}, },
created () {
EventBus.$on('advancedSearchButtonClicked', () => {
this.doAdvancedSearch(this.advancedSearchData)
})
},
beforeDestroy () {
EventBus.$off('advancedSearchButtonClicked')
},
mounted () { mounted () {
if (Object.keys(this.advancedSearchData).some(key => key !== 'search_term')) { if (this.query) {
this.doAdvancedSearch(this.advancedSearchData) this.retrieve()
} else if (this.query) {
this.retrieve(this.query)
} }
}, },
methods: { methods: {
...@@ -133,7 +110,7 @@ export default { ...@@ -133,7 +110,7 @@ export default {
return return
} }
this.loading = true this.loading = true
SearchService.search(this.query) SearchService.search({ search_term: this.query })
.then((hits) => { .then((hits) => {
this.results = hits.map(h => h._source) this.results = hits.map(h => h._source)
}) })
...@@ -141,16 +118,6 @@ export default { ...@@ -141,16 +118,6 @@ export default {
this.loading = false this.loading = false
}) })
}, },
doAdvancedSearch (advancedSearchData) {
console.debug('advanced search data:', advancedSearchData)
SearchService.search(advancedSearchData)
.then((response) => {
this.results = response.map(h => h._source)
})
.finally(() => {
this.loading = false
})
},
isDatabase (item) { isDatabase (item) {
if (!item) { if (!item) {
return false return false
...@@ -302,6 +269,9 @@ export default { ...@@ -302,6 +269,9 @@ export default {
if (event.success) { if (event.success) {
this.$router.push('/database?f=my') this.$router.push('/database?f=my')
} }
},
onSearchResult (results) {
this.results = results
} }
} }
} }
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment