Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • mdm-lab/wiki
  • hvaysset/wiki
  • jsousa/wiki
  • tclabby/wiki
4 results
Show changes
Commits on Source (4)
Showing
with 1371 additions and 172 deletions
...@@ -102,14 +102,14 @@ build:df-wiki-cli: ...@@ -102,14 +102,14 @@ build:df-wiki-cli:
--values deploy/meilisearch/values.yaml --values deploy/meilisearch/values.yaml
--values deploy/meilisearch/values.${ENV:-development}.yaml --values deploy/meilisearch/values.${ENV:-development}.yaml
# wait for it to start # wait for it to start
- MEILI_POD=$(kubectl -n=${KUBE_NAMESPACE} get po -l app.kubernetes.io\/instance=${CI_PROJECT_NAME}-${CI_ENVIRONMENT_NAME}-meilisearch,app.kubernetes.io\/name=meilisearch --output jsonpath='{.items[0].metadata.name}') # - MEILI_POD=$(kubectl -n=${KUBE_NAMESPACE} get po -l app.kubernetes.io\/instance=${CI_PROJECT_NAME}-${CI_ENVIRONMENT_NAME}-meilisearch,app.kubernetes.io\/name=meilisearch --output jsonpath='{.items[0].metadata.name}')
- | # - |
until kubectl -n=${KUBE_NAMESPACE} wait --for=condition=ready pod ${MEILI_POD} --timeout=1s # until kubectl -n=${KUBE_NAMESPACE} wait --for=condition=ready pod ${MEILI_POD} --timeout=1s
do # do
date # date
sleep 1 # sleep 1
kubectl -n=${KUBE_NAMESPACE} get po # kubectl -n=${KUBE_NAMESPACE} get po
done # done
deploy:meilisearch:dev: deploy:meilisearch:dev:
...@@ -184,7 +184,7 @@ lint: ...@@ -184,7 +184,7 @@ lint:
stage: lint stage: lint
script: script:
- cd content/3.defense-systems - cd content/3.defense-systems
- find . -name '*.md' ! -name '0.index.md' -print0 | xargs -0 -I {} df-wiki-cli content lint --file {} - find . -name '*.md' ! -name '0.index.md' | sort | xargs -I {} df-wiki-cli content lint --file {}
when: manual when: manual
# Update Meili search indexes # Update Meili search indexes
...@@ -272,10 +272,10 @@ get-zotero: ...@@ -272,10 +272,10 @@ get-zotero:
extends: .df-wiki-cli-run extends: .df-wiki-cli-run
stage: get-data stage: get-data
script: script:
- df-wiki-cli articles --key ${ZOTERO_API_KEY} --output public/articles.json - df-wiki-cli articles --key ${ZOTERO_API_KEY} --output content/_data/_articles.json
artifacts: artifacts:
paths: paths:
- public/articles.json - content/_data/_articles.json
rules: rules:
- if: $CI_COMMIT_BRANCH == "main" - if: $CI_COMMIT_BRANCH == "main"
...@@ -322,6 +322,9 @@ build:dev:wiki: ...@@ -322,6 +322,9 @@ build:dev:wiki:
# - get-pfam # - get-pfam
variables: variables:
BASE_URL: /wiki/ BASE_URL: /wiki/
before_script:
- *docker-login
# - "sed -i 's/MEILISEARCH_API_KEY/${$MEILI_API_KEY}/g' nuxt.config.ts"
rules: rules:
- if: $CI_COMMIT_BRANCH != "main" - if: $CI_COMMIT_BRANCH != "main"
......
...@@ -17,6 +17,10 @@ ARG MEILI_HOST=http://localhost:7700 ...@@ -17,6 +17,10 @@ ARG MEILI_HOST=http://localhost:7700
ARG MEILI_API_KEY=api_key ARG MEILI_API_KEY=api_key
ENV NUXT_APP_BASE_URL=${BASE_URL} ENV NUXT_APP_BASE_URL=${BASE_URL}
# nuxt module
ENV NUXT_PUBLIC_MEILISEARCH_CLIENT_HOST_URL=${MEILI_HOST}
ENV NUXT_PUBLIC_MEILISEARCH_CLIENT_SEARCH_API_KEY=${MEILI_API_KEY}
ENV NUXT_PUBLIC_MEILI_HOST=${MEILI_HOST} ENV NUXT_PUBLIC_MEILI_HOST=${MEILI_HOST}
ENV NUXT_PUBLIC_MEILI_API_KEY=${MEILI_API_KEY} ENV NUXT_PUBLIC_MEILI_API_KEY=${MEILI_API_KEY}
...@@ -24,6 +28,7 @@ ENV NUXT_PUBLIC_MEILI_API_KEY=${MEILI_API_KEY} ...@@ -24,6 +28,7 @@ ENV NUXT_PUBLIC_MEILI_API_KEY=${MEILI_API_KEY}
WORKDIR /usr/src/app WORKDIR /usr/src/app
COPY --from=install /usr/src/app ./ COPY --from=install /usr/src/app ./
COPY . /usr/src/app COPY . /usr/src/app
EXPOSE 3000 24678 4000 EXPOSE 3000 24678 4000
CMD ["npm", "run", "dev"] CMD ["npm", "run", "dev"]
...@@ -51,8 +56,13 @@ ARG BASE_URL=/ ...@@ -51,8 +56,13 @@ ARG BASE_URL=/
ARG MEILI_HOST=http://localhost:7700 ARG MEILI_HOST=http://localhost:7700
ARG MEILI_API_KEY ARG MEILI_API_KEY
ENV NODE_OPTIONS=--max_old_space_size=8192 ENV NODE_OPTIONS=--max_old_space_size=12288
ENV NUXT_APP_BASE_URL=${BASE_URL} ENV NUXT_APP_BASE_URL=${BASE_URL}
# nuxt module
ENV NUXT_PUBLIC_MEILISEARCH_CLIENT_HOST_URL=${MEILI_HOST}
ENV NUXT_PUBLIC_MEILISEARCH_CLIENT_SEARCH_API_KEY=${MEILI_API_KEY}
ENV NUXT_PUBLIC_MEILI_HOST=${MEILI_HOST} ENV NUXT_PUBLIC_MEILI_HOST=${MEILI_HOST}
ENV NUXT_PUBLIC_MEILI_API_KEY=${MEILI_API_KEY} ENV NUXT_PUBLIC_MEILI_API_KEY=${MEILI_API_KEY}
...@@ -60,6 +70,7 @@ ENV NUXT_PUBLIC_MEILI_API_KEY=${MEILI_API_KEY} ...@@ -60,6 +70,7 @@ ENV NUXT_PUBLIC_MEILI_API_KEY=${MEILI_API_KEY}
WORKDIR /usr/src/app WORKDIR /usr/src/app
COPY --from=install /usr/src/app ./ COPY --from=install /usr/src/app ./
COPY . /usr/src/app COPY . /usr/src/app
RUN npm run generate RUN npm run generate
### STAGE: NGINX ### ### STAGE: NGINX ###
......
<script setup lang="ts"> <script setup lang="ts">
interface item {
title: string;
href?: string | undefined
}
export interface Props { export interface Props {
accessions: string[]; items: item[];
itemsToDisplay?: number; itemsToDisplay?: number;
baseUrl: string;
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
pfamString: null, items: () => [],
itemsToDisplay: 2, itemsToDisplay: 1,
}); });
// const accessions = computed(() => {
// if (props.accessionString === null) {
// return [];
// } else {
// return props.accessionString.split(",").map((acc) => acc.trim());
// }
// });
const show = ref(false); const show = ref(false);
function constructUrl(accession: string) {
return new URL(accession, props.baseUrl).toString();
}
</script> </script>
<template> <template>
<!-- class="d-inline-flex justify-start align-center" --> <!-- class="d-inline-flex justify-start align-center" -->
<span v-if="show" class="d-flex flex-wrap align-center justify-start"> <span v-if="show" class="d-flex flex-wrap align-center justify-start">
<template v-if="accessions.length > itemsToDisplay"> <template v-if="items.length > itemsToDisplay">
<template v-for="(acc) in accessions" :key="acc"> <template v-for="item in items" :key="item.title">
<v-chip :href="constructUrl(acc)" target="_blank" color="info" class="mr-1 my-1 align-self-center" <v-chip :href="item?.href" :target="item?.href === undefined ? item?.href : '_blank'" color="info"
size="small"> class="mr-1 my-1 align-self-center" size="small">
{{ acc }} {{ item.title }}
</v-chip> </v-chip>
</template> </template>
</template> </template>
<v-btn v-if="itemsToDisplay < accessions.length" variant="text" :icon="'mdi-chevron-up'" <v-btn v-if="itemsToDisplay < items.length" variant="text" :icon="'mdi-chevron-up'" @click="show = !show"></v-btn>
@click="show = !show"></v-btn>
</span> </span>
<span v-else class="d-flex flex-wrap align-center justify-start"> <span v-else class="d-flex flex-wrap align-center justify-start">
<template v-for="(acc, index) in accessions" :key="acc"> <template v-for="(item, index) in items" :key="item.title">
<v-chip v-if="index < itemsToDisplay || itemsToDisplay < 0" :href="constructUrl(acc)" target="_blank" <v-chip v-if="index < itemsToDisplay || itemsToDisplay < 0" :href="item?.href"
color="info" class="mr-1 my-1 align-self-center" size="small"> :target="item?.href === undefined ? item?.href : '_blank'" color="info" class="mr-1 my-1 align-self-center"
{{ acc }} size="small">
{{ item.title }}
</v-chip> </v-chip>
<template v-if="index === itemsToDisplay"> <template v-if="index === itemsToDisplay">
<v-chip v-if="!show" variant="text" class="text-grey text-caption align-self-center px-1" <v-chip v-if="!show" variant="text" class="text-grey text-caption align-self-center px-1"
@click="show = !show"> @click="show = !show">
(+{{ accessions.length - itemsToDisplay }} others) (+{{ items.length - itemsToDisplay }} others)
</v-chip> </v-chip>
<v-btn v-if="itemsToDisplay < accessions.length && !show" variant="text" :icon="'mdi-chevron-down'" <v-btn v-if="itemsToDisplay < items.length && !show" variant="text" :icon="'mdi-chevron-down'"
@click="show = !show"></v-btn> @click="show = !show"></v-btn>
</template> </template>
</template> </template>
......
...@@ -3,12 +3,14 @@ export interface Props { ...@@ -3,12 +3,14 @@ export interface Props {
fluid?: boolean fluid?: boolean
toc?: boolean toc?: boolean
edit?: boolean edit?: boolean
navDrawer?: boolean
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
fluid: false, fluid: false,
toc: true, toc: true,
edit: true edit: true,
navDrawer: true
}); });
const drawer = ref(true); const drawer = ref(true);
...@@ -28,16 +30,23 @@ function onScroll() { ...@@ -28,16 +30,23 @@ function onScroll() {
<VApp> <VApp>
<v-main style="min-height: 300px"> <v-main style="min-height: 300px">
<v-container v-scroll="onScroll" :fluid="fluid"> <v-container v-scroll="onScroll" :fluid="fluid">
<slot /> <v-row justify="center">
<!-- </v-card-text> <v-col cols="auto">
<v-card flat color="transparent" :max-width="fluid ? undefined : 1280">
<slot />
<!-- </v-card-text>
</v-card> --> </v-card> -->
<EditGitlab v-if="edit" /> <EditGitlab v-if="edit" />
<NavPrevNext v-if="edit" /> <NavPrevNext v-if="edit" />
</v-card>
</v-col>
</v-row>
</v-container> </v-container>
<!-- <Footer></Footer> --> <!-- <Footer></Footer> -->
</v-main> </v-main>
<NavNavbar v-model:drawer="drawer" :density="density" /> <NavNavbar v-model:drawer="drawer" :density="density" :drawer-enabled="navDrawer"/>
<slot name="drawer" :drawer="drawer"> <slot v-if="navDrawer" name="drawer" :drawer="drawer">
<NavDrawer :drawer="drawer" /> <NavDrawer :drawer="drawer" />
</slot> </slot>
<NavTableOfContent v-if="toc" :links="page.body.toc.links" /> <NavTableOfContent v-if="toc" :links="page.body.toc.links" />
......
...@@ -13,18 +13,19 @@ import { useDisplay, useTheme } from "vuetify"; ...@@ -13,18 +13,19 @@ import { useDisplay, useTheme } from "vuetify";
const { navigation } = useContent(); const { navigation } = useContent();
// const drawer = ref(true); // const drawer = ref(true);
// const computedNavigation = computed(() => { const computedNavigation = computed(() => {
// return navigation.value console.log(navigation.value)
// .filter(({ _path }) => { return navigation.value
// return _path !== "/refseq"; .filter(({ layout }) => {
// }) return layout !== "db"
})
// }); });
</script> </script>
<template> <template>
<v-navigation-drawer :model-value="drawer" :border="1" color="background"> <v-navigation-drawer :model-value="drawer" :border="1" color="background">
<v-list nav density="compact" :lines="false"> <v-list nav density="compact" :lines="false">
<NavNavigation :navigation="navigation" /> <NavNavigation :navigation="computedNavigation" />
</v-list> </v-list>
</v-navigation-drawer> </v-navigation-drawer>
</template> </template>
\ No newline at end of file
...@@ -5,6 +5,7 @@ import { useDisplay, useTheme } from "vuetify"; ...@@ -5,6 +5,7 @@ import { useDisplay, useTheme } from "vuetify";
export interface Props { export interface Props {
density: 'prominent' | 'compact' density: 'prominent' | 'compact'
drawer: boolean drawer: boolean
drawerEnabled: boolean
} }
const runtimeConfig = useRuntimeConfig(); const runtimeConfig = useRuntimeConfig();
...@@ -15,7 +16,8 @@ const theme = useTheme(); ...@@ -15,7 +16,8 @@ const theme = useTheme();
const switchTheme = ref(false) const switchTheme = ref(false)
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
density: "prominent", density: "prominent",
drawer: true drawer: true,
drawerEnabled: true
}); });
const emit = defineEmits(['update:drawer']) const emit = defineEmits(['update:drawer'])
function toggleTheme() { function toggleTheme() {
...@@ -52,7 +54,7 @@ function toggleDrawer() { ...@@ -52,7 +54,7 @@ function toggleDrawer() {
</script> </script>
<template> <template>
<v-app-bar :elevation="0" border name="app-bar" :density="density" color="background"> <v-app-bar :elevation="0" border name="app-bar" :density="density" color="background">
<template #prepend> <template v-if="drawerEnabled" #prepend>
<v-app-bar-nav-icon @click.stop="toggleDrawer"></v-app-bar-nav-icon> <v-app-bar-nav-icon @click.stop="toggleDrawer"></v-app-bar-nav-icon>
<!-- <Logo height="45px" /> --> <!-- <Logo height="45px" /> -->
</template> </template>
......
<script setup lang="ts">
// import type { FacetDistribution } from "meilisearch";
import { useDisplay } from "vuetify";
import { useFacetsStore, type Facets } from '~~/stores/facets'
import { useMeiliSearch } from "#imports"
interface SortItem {
key: string,
order: boolean | 'asc' | 'desc'
}
export interface Props {
title?: string
db?: string
sortBy?: SortItem[]
facets: string[]
headers: { title: string, key: string }[]
itemValue: string
}
export interface FilterItem {
type: 'facet' | 'operator' | 'value' | 'text'
value: string
title: string
count?: number
deletable: boolean
props: {
[key: string]: any
// title: string
// value: any
}
// raw?: any
}
const props = withDefaults(defineProps<Props>(), {
title: '',
db: 'refseq',
sortBy: () => [{ key: "type", order: "asc" }],
});
const sortByRef = ref(toValue(props.sortBy))
const facetsRef = toRef(() => props.facets)
const { search: msSearch, result: msResult } = useMeiliSearch(props.db)
const facetStore = useFacetsStore()
const search: Ref<string> = ref("");
const filterOrSearch: Ref<FilterItem[] | null> = ref(null)
const hitsPerPage: Ref<number> = ref(25)
const limit = ref(1000)
const filterError: Ref<string | null> = ref(null)
const msFilter: Ref<string | undefined> = ref(undefined)
const page = ref(1)
let loading = ref(false)
const { height } = useDisplay();
const minTableHeight = ref(400)
const computedTableHeight = computed(() => {
const computedHeight = height.value - 350
return computedHeight > minTableHeight.value ? computedHeight : minTableHeight.value
})
const filterInputValues = computed(() => {
console.log("recompouted FILTER value")
if (filterOrSearch.value != null) {
return filterOrSearch.value.filter(({ props }) => props.type !== 'text')
} else {
return null
}
})
const queryInputValue = computed(() => {
console.log("recompouted TEXT value")
if (filterOrSearch.value !== null) {
const phrase = filterOrSearch.value
.filter((f) => {
return f.props.type === 'text'
})
.map((f) => {
return f.value
})
if (phrase.length > 1) {
return `${phrase.join(" ")}`
}
else { return phrase[0] }
} else {
return null
}
})
const isFilter = computed(() => {
return Array.isArray(filterOrSearch.value)
})
const msSortBy = computed(() => {
if (sortByRef.value.length > 0) {
return sortByRef.value.map((curr) => {
if (curr?.key && curr?.order) {
return `${curr.key}:${curr.order}`
}
else { return "" }
})
} else { return undefined }
})
const reactiveParams = reactive({
hitsPerPage: 25,
page: 1,
limit: 1000,
facets: ["*"],
filter: [],
sort: ["type:asc"],
// prefix_length: 3,
// attributesToHighlight: ["*"]
})
watch([reactiveParams, msSortBy, page], ([newParams, newSort, newPage]) => {
searchOrFilter()
})
onMounted(async () => {
searchOrFilter()
})
// Fetch results
const msError = computed(() => {
if (filterError.value?.type && filterError.value?.message) {
return filterError.value?.message
} else { return false }
})
async function searchOrFilter() {
try {
loading.value = true
// const q = queryInputValue.value === null ? "" : queryInputValue.value
const q = search.value
await msSearch(q, { ...reactiveParams, filter: msFilter.value, sort: msSortBy.value })
} catch (error: any) {
filterError.value = error
console.log(error)
}
finally {
loading.value = false
}
}
function clearFilterOrSearch() {
filterOrSearch.value = null
searchOrFilter()
}
watch(msFilter, async (fos) => {
console.log("the filter change")
console.log(msFilter)
console.log(fos)
searchOrFilter()
search.value = ''
})
watch(msResult, (newRes) => {
console.log(msResult)
console.log(newRes)
facetStore.setFacets({ facetDistribution: newRes.facetDistribution, facetStat: newRes.facetStat })
}, { deep: true })
watch(filterInputValues, (newSoF) => {
if (isFilter.value && filterInputValues.value !== null && filterInputValues.value?.length % 3 === 0) {
msFilter.value = filterInputValues.value.map((it, index) => {
const sanitizedValue = it.value.split("-")[0]
if (index >= 1 && (index + 1) % 3 === 1) {
return ` AND ${sanitizedValue}`
} else if ((index + 1) % 3 === 0) {
return `"${sanitizedValue}"`
} else {
return `${sanitizedValue}`
}
}).join("")
}
})
watch(search, () => { searchOrFilter() })
// watch(queryInputValue, (newQuery) => {
// searchOrFilter()
// })
const filterStep = computed(() => {
return filterInputValues.value !== null && filterInputValues.value.length > 0 ? filterInputValues.value?.length % 3 : null
})
const operatorItems = ref([
{
type: "operator", value: '=', title: "is", deletable: false, props: {
type: "operator", deletable: false
}
}, {
type: "operator", value: '!=', title: "is not", deletable: false, props: {
type: "operator",
deletable: false
}
},
])
const autocompleteItems = computed(() => {
const index = filterOrSearch.value?.length ?? 0
console.log(index)
if (filterStep.value === null || filterStep.value === 0) {
return props.facets.map(value => {
return {
type: "facet",
value: `${value}-${index}`,
title: value,
deletable: false,
props: {
deletable: false,
type: "facet"
}
}
})
}
if (filterStep.value === 1) {
return operatorItems.value.map(it => { return { ...it, value: `${it.value}-${index}`, } })
}
if (filterStep.value === 2) {
// get the facet value
if (Array.isArray(filterOrSearch.value)) {
const { type, value } = filterOrSearch.value?.slice(-2, -1)[0]
const sanitizedValue = value.split("-")[0]
console.log("compute new facets")
const facetDistri = facetStore.facets?.facetDistribution
console.log(facetDistri)
return facetDistri?.[sanitizedValue] ? Object.entries(facetDistri[sanitizedValue]).map(([key, val]) => {
return {
type: "value", value: `${key}-${index}`, title: key, count: val, deletable: true, props: {
type: "value", count: val, deletable: true
}
}
}) : []
}
}
})
const canAddTextSearch = computed(() => {
if (filterOrSearch.value !== null && filterOrSearch.value.length > 0) {
const lastItem = filterOrSearch.value.slice(-1)[0]
return lastItem?.props.type === 'value' || lastItem?.props.type === "text"
}
return true
})
function selectItem(item) {
filterOrSearch.value = Array.isArray(filterOrSearch.value) ? [...filterOrSearch.value, item] : [item]
}
function deleteOneFilter(index: number) {
console.log("deleteOnefilter")
console.log(index)
console.log(isFilter.value)
console.log(filterOrSearch)
if (isFilter.value) {
filterOrSearch.value?.splice(index - 2, 2)
console.log(filterOrSearch.value)
}
}
function deleteTextFilter(index: number) {
console.log("delete text filter")
console.log(index)
console.log(isFilter.value)
console.log(filterOrSearch)
console.log(filterOrSearch.value?.length)
if (isFilter.value) {
if (index === 0) {
filterOrSearch.value?.shift()
} else {
filterOrSearch.value?.splice(index, 1)
}
console.log(filterOrSearch.value?.length)
console.log(filterOrSearch.value)
}
}
function clearSearch() {
search.value = ""
}
function runTextSearch() {
if (canAddTextSearch) {
const item: FilterItem = reactive({
type: 'text', title: search.value, value: search.value, deletable: true, props: { type: "text", deletable: true, }
})
if (Array.isArray(filterOrSearch.value)) {
filterOrSearch.value = [
...filterOrSearch.value, item
]
} else {
filterOrSearch.value = [item]
}
search.value = ""
searchOrFilter()
}
}
function namesToCollapsibleChips(names: string[]) {
return names.filter((it) => it !== "").map(it => ({ title: it }))
}
function namesToAccessionChips(names: string[]) {
return namesToCollapsibleChips(names).map(it => {
return { ...it, href: new URL(it.title, "http://toto.pasteur.cloud").toString() }
})
}
</script>
<template>
<v-card flat>
<v-card-text>
<v-row>
<v-col cols="5">
<v-text-field v-model="search" label="Search..." hide-details prepend-inner-icon="mdi-magnify"
single-line clearable></v-text-field>
</v-col>
<v-col>
<v-autocomplete ref="autocompleteInput" hide-details v-model:model-value="filterOrSearch"
auto-select-first chips clearable label="Filter results..." :items="autocompleteItems" single-line
item-value="value" item-title="title" multiple return-object prepend-inner-icon="md:search"
@click:appendInner="searchOrFilter" @click:clear="clearFilterOrSearch"
@update:modelValue="() => clearSearch()">
<template #chip="{ props, item, index }">
<v-chip v-bind="props" :text="item.raw.title" :closable="item.props.deletable"
@click:close="item.props.type === 'text' ? deleteTextFilter(index) : deleteOneFilter(index)"></v-chip>
<!-- <v-chip v-if="(index + 1) % 3 === 0" v-bind="props" :text="item.raw.title" closable
@click:close="deleteOneFilter(index)"></v-chip>
<v-chip v-else v-bind="props" :text="item.raw.title"></v-chip> -->
</template>
<template #item="{ props, item }">
<v-list-item v-bind="{ ...props, active: false, onClick: () => selectItem(item) }"
:title="item.title" :subtitle="item.raw?.count ? item.raw.count : ''" :value="props.value">
</v-list-item>
</template>
<!-- <template #no-data></template>
<template #prepend-item>
<v-list-item v-if="canAddTextSearch" :title="`Text search: ${search}`" @click="runTextSearch"> </v-list-item>
</template> -->
</v-autocomplete>
</v-col>
</v-row>
</v-card-text>
<v-data-table-server v-if="!msError" v-model:page="reactiveParams.page"
v-model:items-per-page="reactiveParams.hitsPerPage" v-model:sortBy="sortByRef" fixed-header :loading="loading"
:headers="headers" :items="msResult?.hits ?? []" :items-length="msResult?.totalHits ?? 0"
:item-value="itemValue" multi-sort density="compact" :height="computedTableHeight" class="elevation-1 mt-2">
<template #[`item.accession_in_sys`]="{ item }">
<CollapsibleChips :items="namesToAccessionChips(item.accession_in_sys)"></CollapsibleChips>
</template>
<template #[`item.proteins_in_the_prediction`]="{ item }">
<CollapsibleChips :items="namesToCollapsibleChips(item.proteins_in_the_prediction)"></CollapsibleChips>
</template>
<template #[`item.system_genes`]="{ item }">
<CollapsibleChips :items="namesToCollapsibleChips(item.system_genes)"></CollapsibleChips>
</template>
<template #[`item.completed`]="{ item }">
<v-icon v-if="item.completed" color="success" icon="md:check"></v-icon>
<v-icon v-else color="warning" icon="md:dangerous"></v-icon>
</template>
</v-data-table-server>
<v-alert v-else type="error">
{{ msError }}
</v-alert>
</v-card>
</template>
\ No newline at end of file
<script setup lang="ts"> <script setup lang="ts">
import { useDisplay } from "vuetify"; import { useDisplay } from "vuetify";
import { useArticlesStore } from '@/stores/articles'
export interface Props { export interface Props {
index?: number; index?: number;
...@@ -14,10 +15,27 @@ const props = withDefaults(defineProps<Props>(), { ...@@ -14,10 +15,27 @@ const props = withDefaults(defineProps<Props>(), {
enumerate: true, enumerate: true,
divider: false, divider: false,
}); });
// onBeforeMount(async () => {
// await useArticlesStore().initialize()
// })
const { article } = useFetchArticle(props.doi); const { article } = useFetchArticle(props.doi);
const { mobile } = useDisplay(); const { mobile } = useDisplay();
const show = ref(false); const show = ref(false);
// const computedArticle = computed(() => { return { ...article.value } })
// watch(article, (newArticle) => {
// console.log("article updated")
// }, { deep: true })
console.log("aritcle dans composant")
console.log(article)
const articleTitle = computed(() => { const articleTitle = computed(() => {
return props?.title ?? article?.value?.title ?? props.doi; return props?.title ?? article?.value?.title ?? props.doi;
}); });
...@@ -26,8 +44,8 @@ const articleAbstract = computed(() => { ...@@ -26,8 +44,8 @@ const articleAbstract = computed(() => {
}); });
</script> </script>
<template> <template>
<v-list-item :href="article?.href" :id="props.doi" :target="article?.target" density="compact" color="transparent" <v-list-item :href="article?.href" :id="props.doi" :target="article?.target" density="compact"
class="px-1"> color="transparent" class="px-1">
<template #prepend v-if="!mobile && enumerate"> <template #prepend v-if="!mobile && enumerate">
<v-avatar color="primary" size="small" density="compact" variant="tonal"> <v-avatar color="primary" size="small" density="compact" variant="tonal">
{{ props?.index ?? "#" }} {{ props?.index ?? "#" }}
......
<script setup lang="ts">
import { useFacetsStore } from '~~/stores/facets'
import * as Plot from "@observablehq/plot";
import PlotFigure from "~/components/PlotFigure";
import { useDisplay } from "vuetify";
const facetStore = useFacetsStore()
const sortBy: Ref<{ key: string, order: string }[]> = ref([{ key: 'type', order: "asc" }])
const itemValue = ref("id");
const { width } = useDisplay();
const distriTool: Ref<string[]> = ref([])
const facets = ref([
"type",
"Superkingdom",
"phylum",
"order",
"family",
"genus",
"species",
])
const availableTaxo: Ref<string[]> = ref([
"species",
"genus",
"family",
"order",
"phylum",
"Superkingdom"
]);
const selectedTaxoRank = ref("phylum");
const headers = ref([
{ title: "Replicon", key: "replicon" },
{
title: "Type",
key: "type",
},
{
title: "Subtype",
key: "subtype",
},
{
title: "Accessions",
key: "accession_in_sys"
}
])
const logTransform = computed(() => {
return distriTool.value.includes('log')
})
const fullWidth = computed(() => {
return distriTool.value.includes('fullwidth')
})
const computedHeaders = computed(() => {
return [...headers.value, ...availableTaxo.value.map(taxo => {
return {
title: capitalize(taxo),
key: taxo
}
})]
})
const computedWidth = computed(() => {
return Math.max(width.value, 550);
});
const plotHeight = computed(() => {
return computedWidth.value / 3;
// return 500
});
const defaultBarPlotOptions = computed(() => {
const y = logTransform.value ? { nice: true, grid: true, type: 'symlog' } : { nice: true, grid: true, type: "linear" }
// const y = { nice: true, grid: true }
return {
x: { label: null, tickRotate: 70 },
y,
color: { legend: true },
width: computedWidth.value,
height: plotHeight.value,
}
})
const computedSystemDistribution = computed(() => {
if (facetStore.facets?.facetDistribution?.type) {
return Object.entries(facetStore.facets.facetDistribution.type).map(([key, value]) => {
return {
type: key,
// count: logTransform.value ? Math.log(value) : value
count: value
}
}).sort()
} else { return [] }
})
const computedDistriSystemOptions = computed(() => {
return {
...defaultBarPlotOptions.value,
marginBottom: 120,
marks: [
// Plot.frame(),
Plot.barY(
toValue(computedSystemDistribution),
{
y: "count", x: 'type', tip: true,
fill: "#6750a4",
sort: { x: "-y" },
},
),
],
};
});
const computedTaxonomyDistribution = computed(() => {
if (facetStore.facets?.facetDistribution?.[selectedTaxoRank.value]) {
return Object.entries(facetStore.facets.facetDistribution[selectedTaxoRank.value]).map(([key, value]) => {
return {
[selectedTaxoRank.value]: key,
count: value
}
}).sort()
} else { return [] }
})
const computedDistriTaxoOptions = computed(() => {
return {
...defaultBarPlotOptions.value,
marginBottom: 200,
marks: [
Plot.barY(
toValue(computedTaxonomyDistribution),
{
y: "count",
x: selectedTaxoRank.value,
tip: true,
fill: "#6750a4",
sort: { x: "-y" },
}
),
],
};
});
function capitalize([first, ...rest]) {
return first.toUpperCase() + rest.join('').toLowerCase();
}
</script>
<template>
<v-card flat class="mb-2">
<v-toolbar density="compact">
<v-toolbar-title>Distributions</v-toolbar-title>
<v-btn-toggle v-model="distriTool" multiple density="compact" rounded="false" variant="text" color="primary"
class="mx-2">
<v-btn icon="md:fullscreen" value="fullwidth"></v-btn>
<v-btn icon="mdi-math-log" value="log"></v-btn>
</v-btn-toggle>
</v-toolbar>
<v-row align="start" class="mb-2">
<v-col :cols="fullWidth ? 12 : 6">
<v-card flat class="my-3">
<v-card-title>Systems </v-card-title>
<v-card-text>
<PlotFigure :options="unref(computedDistriSystemOptions)" defer></PlotFigure>
</v-card-text>
</v-card>
</v-col>
<v-col :cols="fullWidth ? 12 : 6">
<v-card flat>
<v-card-title>Taxonomic</v-card-title>
<v-card-text>
<v-select v-model="selectedTaxoRank" :items="availableTaxo" density="compact"
label="Select taxonomic rank"></v-select>
<PlotFigure defer :options="unref(computedDistriTaxoOptions)"></PlotFigure>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-card>
<ServerDbTable title="RefSeq" db="refseq" :sortBy="sortBy" :headers="computedHeaders" :item-value="itemValue"
:facets="facets">
</ServerDbTable>
</template>
\ No newline at end of file
<script setup lang="ts">
import { useFacetsStore } from '~~/stores/facets'
const sortBy: Ref<{ key: string, order: string }[]> = ref([{ key: 'system', order: "asc" }])
const itemValue = ref("id");
const facets = ref(["system", "completed",
"plddts",])
const headers: Ref<Object[]> = ref([
{ title: "System", key: "system" },
{ title: "Proteins in structure", key: 'proteins_in_the_prediction', sortable: false },
{ title: "System genes", key: "system_genes", sortable: false },
{ title: "Completed", key: "completed" },
{ title: "Predition type", key: "prediction_type" },
{ title: "Num of genes", key: "system_number_of_genes" },
{ title: "pLDDT", key: "plddts" },
{ title: "iptm+ptm", key: "iptm+ptm" },
{ title: "pDockQ", key: "pDockQ" },
{ title: "Type", key: "type" }
])
</script>
<template>
<ServerDbTable title="Predicted Strucutres" db="structure" :sortBy="sortBy" :headers="headers" :item-value="itemValue"
:facets="facets">
</ServerDbTable>
</template>
\ No newline at end of file
...@@ -2,17 +2,19 @@ import { useArticlesStore, type CslJson } from '../stores/articles' ...@@ -2,17 +2,19 @@ import { useArticlesStore, type CslJson } from '../stores/articles'
import { ref, computed, watchEffect, toValue } from "vue" import { ref, computed, watchEffect, toValue } from "vue"
// import { useFetch } from '#app'; // import { useFetch } from '#app';
// import { useFetch } from "nuxt" // import { useFetch } from "nuxt"
import { useFetch } from '#imports' import { type MaybeRef, useFetch } from '#imports'
import article from "@/public/articles.json"
export interface CrossrefArticle {
export interface ArticleMessage {
DOI: string; DOI: string;
issue: number; issue: number;
title: string | string[]; type: string;
title: string[];
author: Array<{ family: string; given: string }>; author: Array<{ family: string; given: string }>;
"container-title-short": string; // "container-title-short": string;
"short-container-title": string;
"container-title": string; "container-title": string;
abstract: string; abstract?: string;
published: { published: {
"date-parts": string[]; "date-parts": string[];
}; };
...@@ -23,7 +25,7 @@ export interface ArticleMessage { ...@@ -23,7 +25,7 @@ export interface ArticleMessage {
export interface Article { export interface WikiArticle {
DOI: string DOI: string
title: string title: string
subtitle: string subtitle: string
...@@ -36,115 +38,125 @@ export interface Article { ...@@ -36,115 +38,125 @@ export interface Article {
prependIcon: string prependIcon: string
} }
export interface RawArticle { export interface RawArticle {
message: ArticleMessage message: CrossrefArticle
} }
type SrcArticle = ArticleMessage | CslJson type SrcArticle = CrossrefArticle | CslJson
export function useFetchArticle(doi: string) { export function useFetchArticle(doi: MaybeRef<string> = ref("")) {
// const article = ref<Article>() // const article = ref<Article>()
// const rawArticle = ref<RawArticle>() // const rawArticle = ref<RawArticle>()
const srcArticle = ref<SrcArticle | null>(null) const srcArticle = ref<SrcArticle | null>(null)
const store = useArticlesStore() const store = useArticlesStore()
const pending = ref(false) const pending = ref(false)
const doiBaseUrl = ref(new URL("https://doi.org/")); const doiBaseUrl = ref(new URL("https://doi.org/"));
const url = ref(new URL(`/works/${doi}`, " https://api.crossref.org/").href); const url = ref(new URL(`/works/${toValue(doi)}`, " https://api.crossref.org/").href);
const article = computed(() => { const article = ref()
if (srcArticle.value != undefined) { const zoteroArticles = ref()
function toAuthorsString(authors: Array<{ family: string; given: string }>) {
return authors
.map((curr) => {
return `${curr.family} ${curr.given}`;
})
.join(", ");
}
function getReferenceUrl(doi: string) {
return new URL(doi, doiBaseUrl.value).href;
}
function zoteroArticleToArticle(zoteroArticle: CslJson) {
if (zoteroArticle != undefined) {
const { const {
DOI, DOI,
title, title,
"container-title-short": cts,
"container-title": ct, "container-title": ct,
journalAbbreviation,
abstract, abstract,
published,
issued, issued,
author, author,
...rest ...rest
} = srcArticle.value; } = zoteroArticle;
let sanitizedAbstract = abstract
if (sanitizedAbstract) {
sanitizedAbstract = /(?:\<jats\:p\>)?(.*)(?:\<\/jats\:p\>)?/.exec(sanitizedAbstract)?.[1] ?? ''
}
const sanitizedTitle = (Array.isArray(title)) ? title[0] : title
const sanitizedContainerTitle = (Array.isArray(ct)) ? cts?.length > 0 ? cts[0] : ct?.length > 0 ? ct[0] : "" : journalAbbreviation || ct
return { return {
DOI, DOI,
title: sanitizedTitle, title,
subtitle: toAuthorsString(author || []), subtitle: toAuthorsString(author || []),
author, author,
containerTitle: sanitizedContainerTitle, containerTitle: ct,
abstract: sanitizedAbstract, abstract,
year: published?.["date-parts"][0][0] ?? issued?.["date-parts"][0][0] ?? '', year: issued?.["date-parts"][0][0] ?? '',
href: getReferenceUrl(DOI), href: getReferenceUrl(DOI),
target: "_blank", target: "_blank",
prependIcon: "mdi-newspaper-variant-outline", prependIcon: "mdi-newspaper-variant-outline",
} }
} else { return srcArticle.value }
})
const zoteroArticles = ref([])
// const config = useRuntimeConfig()
// console.log(config.value)
const fetchLocalArticles = () => {
useFetch<RawArticle[]>(
"/articles.json",
{ lazy: true, server: false }
).then(({ data }) => {
zoteroArticles.value = data.value
}) // localPending.value = articlesPending.value
if (zoteroArticles.value?.length > 0) {
for (const article of zoteroArticles.value) {
// console.log("article files : ", article.DOI)
store.add(article)
}
} }
}
const fetchCrossRef = () => {
useFetch<RawArticle>(toValue(url), {
lazy: true, server: false,
}).then(({ data, pending: pendingUseFetch }) => {
if (data.value?.message) {
srcArticle.value = data.value.message
}
pending.value = pendingUseFetch.value
})
} }
function crossrefToArticle(article: CrossrefArticle): WikiArticle {
watchEffect(() => { const { title, DOI, type, "container-title": ct, "short-container-title": sct, abstract, author, issued } = article
// no article in the store // let sanitizedAbstract = abstract
if (store.articles.size === 0) { const sanitizedAbstract = abstract ? /(?:\<jats\:p\>)?(.*)(?:\<\/jats\:p\>)?/.exec(abstract)?.[1] ?? '' : ''
fetchLocalArticles() const sanitizedContainerTitle = sct?.length > 0 ? sct[0] : ct?.length > 0 ? ct[0] : ""
return {
title: title?.length > 0 ? title[0] : "",
DOI,
abstract: sanitizedAbstract,
containerTitle: sanitizedContainerTitle,
subtitle: toAuthorsString(author || []),
author,
year: issued?.["date-parts"][0][0] ?? '',
href: getReferenceUrl(DOI),
target: "_blank",
prependIcon: "mdi-newspaper-variant-outline"
} }
}
if (store.articles.has(doi)) { function crossrefToCsl(article: CrossrefArticle): CslJson {
srcArticle.value = store.articles.get(doi) const { title, DOI, type, "container-title": ct, "short-container-title": sct, abstract, author, issued } = article
return // let sanitizedAbstract = abstract
} else { const sanitizedAbstract = abstract ? /(?:\<jats\:p\>)?(.*)(?:\<\/jats\:p\>)?/.exec(abstract)?.[1] ?? '' : ''
fetchCrossRef() const sanitizedContainerTitle = sct?.length > 0 ? sct[0] : ct?.length > 0 ? ct[0] : ""
} return {
title: title?.length > 0 ? title[0] : "",
type,
DOI,
abstract: sanitizedAbstract,
author,
"container-title": sanitizedContainerTitle,
issued
}) }
function toAuthorsString(authors: Array<{ family: string; given: string }>) {
return authors
.map((curr) => {
return `${curr.family} ${curr.given}`;
})
.join(", ");
} }
function getReferenceUrl(doi: string) { if (store.articles.has(toValue(doi))) {
return new URL(doi, doiBaseUrl.value).href; const cslArticle = store.articles.get(toValue(doi))
article.value = cslArticle ? zoteroArticleToArticle(cslArticle) : undefined
} }
else {
useFetch<RawArticle>(toValue(url), {
lazy: true, server: false,
}).then(({ data, pending: pendingUseFetch }) => {
if (data.value?.message) {
article.value = crossrefToArticle(data.value.message)
store.add(crossrefToCsl(data.value.message))
}
pending.value = pendingUseFetch.value
})
}
// const fetchCrossRef = () => {
// useFetch<RawArticle>(toValue(url), {
// lazy: true, server: false,
// }).then(({ data, pending: pendingUseFetch }) => {
// if (data.value?.message) {
// srcArticle.value = data.value.message
// }
// pending.value = pendingUseFetch.value
// })
// }
return { article, pending } return { article, pending }
} }
......
import { MeiliSearch } from 'meilisearch' import { MeiliSearch } from 'meilisearch'
import { useRuntimeConfig, watchEffect, type MaybeRef, ref, toValue } from '#imports' import { useRuntimeConfig, watchEffect, type MaybeRef, ref, toValue } from '#imports'
import type { FacetDistribution, Hits } from 'meilisearch'; import type { FacetDistribution, Hits } from 'meilisearch';
export function useFetchMsDocument( import { useAsyncState } from '@vueuse/core'
import { errorMonitor } from 'events';
export async function useFetchMsDocument(
index: MaybeRef<string> = ref(""), index: MaybeRef<string> = ref(""),
search: Ref<string> = ref(""), search: Ref<string> = ref(""),
filter: Ref<string> = ref(''), filter: Ref<string> = ref(''),
...@@ -18,12 +21,11 @@ export function useFetchMsDocument( ...@@ -18,12 +21,11 @@ export function useFetchMsDocument(
apiKey: runtimeConfig.public.meiliApiKey apiKey: runtimeConfig.public.meiliApiKey
}) })
const pending = ref(false) const pending = ref(false)
const filterError = ref(null) const filterError: Ref<string | null> = ref(null)
const hits: Ref<Hits<Record<string, any>>> = ref([]) const hits: Ref<Hits<Record<string, any>>> = ref([])
const totalHits = ref(0) const totalHits = ref(0)
const totalPages = ref(0) const totalPages = ref(0)
const facetDistribution: Ref<FacetDistribution | undefined> = ref({}) const facetDistribution: Ref<FacetDistribution | undefined> = ref({})
// reset page when filter and search change
watch(filter, () => { watch(filter, () => {
page.value = 1 page.value = 1
...@@ -32,34 +34,36 @@ export function useFetchMsDocument( ...@@ -32,34 +34,36 @@ export function useFetchMsDocument(
page.value = 1 page.value = 1
}) })
watchEffect(async () => {
try {
pending.value = true
const res = await client
.index(toValue(index))
.search(toValue(search), {
limit: toValue(limit),
filter: toValue(filter),
hitsPerPage: toValue(hitsPerPage),
page: toValue(page),
facets: toValue(facets),
sort: toValue(sort),
})
filterError.value = null
const { hits: resHits, totalHits: resTotalHits, totalPages: resTotalPages, facetDistribution: facetD } = res
totalHits.value = resTotalHits
hits.value = resHits
totalPages.value = resTotalPages
facetDistribution.value = facetD
} catch ({ code, message }) {
if (code === 'invalid_search_filter') {
filterError.value = message
}
} finally {
pending.value = false
}
})
try {
pending.value = true
console.log(pending.value)
const res = await client
.index(toValue(index))
.search(toValue(search), {
limit: toValue(limit),
filter: toValue(filter),
hitsPerPage: toValue(hitsPerPage),
page: toValue(page),
facets: toValue(facets),
sort: toValue(sort),
})
filterError.value = null
const { hits: resHits, totalHits: resTotalHits, totalPages: resTotalPages, facetDistribution: facetD } = res
totalHits.value = resTotalHits
hits.value = resHits
totalPages.value = resTotalPages
facetDistribution.value = facetD
pending.value = false
} catch (e: any) {
filterError.value = e
}
finally {
pending.value = false
}
// })
console.log(hits)
return { hits, totalHits, pending, filterError, totalPages, facetDistribution } return { hits, totalHits, pending, filterError, totalPages, facetDistribution }
} }
--- ---
layout: db layout: db
navigation: false ---
---
\ No newline at end of file
::refseq-db
::
--- ---
layout: db layout: db
navigation: false
--- ---
::structure-db
::
\ No newline at end of file
...@@ -25,6 +25,6 @@ appVersion: "1.16.0" ...@@ -25,6 +25,6 @@ appVersion: "1.16.0"
dependencies: dependencies:
- name: meilisearch - name: meilisearch
version: 0.2.8 version: 0.3.0
repository: "https://meilisearch.github.io/meilisearch-kubernetes" repository: "https://meilisearch.github.io/meilisearch-kubernetes"
...@@ -7,7 +7,7 @@ services: ...@@ -7,7 +7,7 @@ services:
args: args:
BASE_URL: /wiki/ BASE_URL: /wiki/
MEILI_HOST: http://localhost:7700 MEILI_HOST: http://localhost:7700
MEILI_API_KEY: 269d546c85959e3d125cf7b34975a91503e0a63f0547c395c06ddf696a7cf12d MEILI_API_KEY: f9cc073016cbb392365aae86517878cb3f3408bb85c1fafd06e27f73ccb35e3d
container_name: nuxt container_name: nuxt
environment: environment:
HOST: 0.0.0.0 HOST: 0.0.0.0
...@@ -30,7 +30,7 @@ services: ...@@ -30,7 +30,7 @@ services:
- main - main
meilisearch: meilisearch:
image: getmeili/meilisearch:v1.4 image: getmeili/meilisearch:v1.5
# command: # command:
# - meilisearch # - meilisearch
# - --http-addr # - --http-addr
......
<script setup lang="ts"></script> <script setup lang="ts">
import { useArticlesStore } from '@/stores/articles'
const store = useArticlesStore()
const { data } = await useAsyncData('zotero-articles', async () => queryContent('_data/_articles').where({ _partial: true }).findOne())
if (data.value) {
const dataValue = toValue(data)?.body
for (const cslArticle of dataValue) {
if (cslArticle?.DOI) {
store.add(cslArticle)
}
}
}
</script>
<template> <template>
<LayoutWrapper> <LayoutWrapper>
<slot /> <slot />
......
...@@ -5,16 +5,7 @@ import { useFacetsStore, type Facets } from '~~/stores/facets' ...@@ -5,16 +5,7 @@ import { useFacetsStore, type Facets } from '~~/stores/facets'
const facetStore = useFacetsStore() const facetStore = useFacetsStore()
</script> </script>
<template> <template>
<LayoutWrapper :fluid="true" :toc="false" :edit="false"> <LayoutWrapper :fluid="true" :toc="false" :edit="false" :nav-drawer="false">
<template #drawer="{ drawer }">
<v-navigation-drawer :model-value="drawer" :border="1" color="background">
<v-list>
<v-list-item v-for="(value, key) in facetStore.facets.facetDistribution" :key="key"
:title="key"></v-list-item>
</v-list>
</v-navigation-drawer>
</template>
<slot /> <slot />
</LayoutWrapper> </LayoutWrapper>
</template> </template>
......
...@@ -6,6 +6,7 @@ export default defineNuxtConfig({ ...@@ -6,6 +6,7 @@ export default defineNuxtConfig({
'vuetify-nuxt-module', 'vuetify-nuxt-module',
'@vueuse/nuxt', '@vueuse/nuxt',
'@pinia/nuxt', '@pinia/nuxt',
'nuxt-meilisearch',
// '@unocss/nuxt', // '@unocss/nuxt',
], ],
content: { content: {
...@@ -32,6 +33,11 @@ export default defineNuxtConfig({ ...@@ -32,6 +33,11 @@ export default defineNuxtConfig({
} }
}, },
meilisearch: {
hostUrl: 'https://my-meilisearch-server.example.com',
searchApiKey: 'api_key',
serverSideUsage: false // default false
},
devtools: { devtools: {
enabled: false enabled: false
}, },
...@@ -39,6 +45,10 @@ export default defineNuxtConfig({ ...@@ -39,6 +45,10 @@ export default defineNuxtConfig({
public: { public: {
defenseFinderWebservice: '/', defenseFinderWebservice: '/',
meilisearchClient: {
hostUrl: 'http://localhost:7700',
searchApiKey: 'api_key',
},
meiliHost: 'http://localhost:7700', meiliHost: 'http://localhost:7700',
meiliApiKey: 'api_key' meiliApiKey: 'api_key'
} }
......