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:
--values deploy/meilisearch/values.yaml
--values deploy/meilisearch/values.${ENV:-development}.yaml
# 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}')
- |
until kubectl -n=${KUBE_NAMESPACE} wait --for=condition=ready pod ${MEILI_POD} --timeout=1s
do
date
sleep 1
kubectl -n=${KUBE_NAMESPACE} get po
done
# - 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
# do
# date
# sleep 1
# kubectl -n=${KUBE_NAMESPACE} get po
# done
deploy:meilisearch:dev:
......@@ -184,7 +184,7 @@ lint:
stage: lint
script:
- 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
# Update Meili search indexes
......@@ -272,10 +272,10 @@ get-zotero:
extends: .df-wiki-cli-run
stage: get-data
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:
paths:
- public/articles.json
- content/_data/_articles.json
rules:
- if: $CI_COMMIT_BRANCH == "main"
......@@ -322,6 +322,9 @@ build:dev:wiki:
# - get-pfam
variables:
BASE_URL: /wiki/
before_script:
- *docker-login
# - "sed -i 's/MEILISEARCH_API_KEY/${$MEILI_API_KEY}/g' nuxt.config.ts"
rules:
- if: $CI_COMMIT_BRANCH != "main"
......
......@@ -17,6 +17,10 @@ ARG MEILI_HOST=http://localhost:7700
ARG MEILI_API_KEY=api_key
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_API_KEY=${MEILI_API_KEY}
......@@ -24,6 +28,7 @@ ENV NUXT_PUBLIC_MEILI_API_KEY=${MEILI_API_KEY}
WORKDIR /usr/src/app
COPY --from=install /usr/src/app ./
COPY . /usr/src/app
EXPOSE 3000 24678 4000
CMD ["npm", "run", "dev"]
......@@ -51,8 +56,13 @@ ARG BASE_URL=/
ARG MEILI_HOST=http://localhost:7700
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}
# 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_API_KEY=${MEILI_API_KEY}
......@@ -60,6 +70,7 @@ ENV NUXT_PUBLIC_MEILI_API_KEY=${MEILI_API_KEY}
WORKDIR /usr/src/app
COPY --from=install /usr/src/app ./
COPY . /usr/src/app
RUN npm run generate
### STAGE: NGINX ###
......
<script setup lang="ts">
interface item {
title: string;
href?: string | undefined
}
export interface Props {
accessions: string[];
items: item[];
itemsToDisplay?: number;
baseUrl: string;
}
const props = withDefaults(defineProps<Props>(), {
pfamString: null,
itemsToDisplay: 2,
items: () => [],
itemsToDisplay: 1,
});
// const accessions = computed(() => {
// if (props.accessionString === null) {
// return [];
// } else {
// return props.accessionString.split(",").map((acc) => acc.trim());
// }
// });
const show = ref(false);
function constructUrl(accession: string) {
return new URL(accession, props.baseUrl).toString();
}
</script>
<template>
<!-- class="d-inline-flex justify-start align-center" -->
<span v-if="show" class="d-flex flex-wrap align-center justify-start">
<template v-if="accessions.length > itemsToDisplay">
<template v-for="(acc) in accessions" :key="acc">
<v-chip :href="constructUrl(acc)" target="_blank" color="info" class="mr-1 my-1 align-self-center"
size="small">
{{ acc }}
<template v-if="items.length > itemsToDisplay">
<template v-for="item in items" :key="item.title">
<v-chip :href="item?.href" :target="item?.href === undefined ? item?.href : '_blank'" color="info"
class="mr-1 my-1 align-self-center" size="small">
{{ item.title }}
</v-chip>
</template>
</template>
<v-btn v-if="itemsToDisplay < accessions.length" variant="text" :icon="'mdi-chevron-up'"
@click="show = !show"></v-btn>
<v-btn v-if="itemsToDisplay < items.length" variant="text" :icon="'mdi-chevron-up'" @click="show = !show"></v-btn>
</span>
<span v-else class="d-flex flex-wrap align-center justify-start">
<template v-for="(acc, index) in accessions" :key="acc">
<v-chip v-if="index < itemsToDisplay || itemsToDisplay < 0" :href="constructUrl(acc)" target="_blank"
color="info" class="mr-1 my-1 align-self-center" size="small">
{{ acc }}
<template v-for="(item, index) in items" :key="item.title">
<v-chip v-if="index < itemsToDisplay || itemsToDisplay < 0" :href="item?.href"
:target="item?.href === undefined ? item?.href : '_blank'" color="info" class="mr-1 my-1 align-self-center"
size="small">
{{ item.title }}
</v-chip>
<template v-if="index === itemsToDisplay">
<v-chip v-if="!show" variant="text" class="text-grey text-caption align-self-center px-1"
@click="show = !show">
(+{{ accessions.length - itemsToDisplay }} others)
(+{{ items.length - itemsToDisplay }} others)
</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>
</template>
</template>
......
......@@ -3,12 +3,14 @@ export interface Props {
fluid?: boolean
toc?: boolean
edit?: boolean
navDrawer?: boolean
}
const props = withDefaults(defineProps<Props>(), {
fluid: false,
toc: true,
edit: true
edit: true,
navDrawer: true
});
const drawer = ref(true);
......@@ -28,16 +30,23 @@ function onScroll() {
<VApp>
<v-main style="min-height: 300px">
<v-container v-scroll="onScroll" :fluid="fluid">
<slot />
<!-- </v-card-text>
<v-row justify="center">
<v-col cols="auto">
<v-card flat color="transparent" :max-width="fluid ? undefined : 1280">
<slot />
<!-- </v-card-text>
</v-card> -->
<EditGitlab v-if="edit" />
<NavPrevNext v-if="edit" />
<EditGitlab v-if="edit" />
<NavPrevNext v-if="edit" />
</v-card>
</v-col>
</v-row>
</v-container>
<!-- <Footer></Footer> -->
</v-main>
<NavNavbar v-model:drawer="drawer" :density="density" />
<slot name="drawer" :drawer="drawer">
<NavNavbar v-model:drawer="drawer" :density="density" :drawer-enabled="navDrawer"/>
<slot v-if="navDrawer" name="drawer" :drawer="drawer">
<NavDrawer :drawer="drawer" />
</slot>
<NavTableOfContent v-if="toc" :links="page.body.toc.links" />
......
......@@ -13,18 +13,19 @@ import { useDisplay, useTheme } from "vuetify";
const { navigation } = useContent();
// const drawer = ref(true);
// const computedNavigation = computed(() => {
// return navigation.value
// .filter(({ _path }) => {
// return _path !== "/refseq";
// })
const computedNavigation = computed(() => {
console.log(navigation.value)
return navigation.value
.filter(({ layout }) => {
return layout !== "db"
})
// });
});
</script>
<template>
<v-navigation-drawer :model-value="drawer" :border="1" color="background">
<v-list nav density="compact" :lines="false">
<NavNavigation :navigation="navigation" />
<NavNavigation :navigation="computedNavigation" />
</v-list>
</v-navigation-drawer>
</template>
\ No newline at end of file
......@@ -5,6 +5,7 @@ import { useDisplay, useTheme } from "vuetify";
export interface Props {
density: 'prominent' | 'compact'
drawer: boolean
drawerEnabled: boolean
}
const runtimeConfig = useRuntimeConfig();
......@@ -15,7 +16,8 @@ const theme = useTheme();
const switchTheme = ref(false)
const props = withDefaults(defineProps<Props>(), {
density: "prominent",
drawer: true
drawer: true,
drawerEnabled: true
});
const emit = defineEmits(['update:drawer'])
function toggleTheme() {
......@@ -52,7 +54,7 @@ function toggleDrawer() {
</script>
<template>
<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>
<!-- <Logo height="45px" /> -->
</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">
import { useDisplay } from "vuetify";
import { useArticlesStore } from '@/stores/articles'
export interface Props {
index?: number;
......@@ -14,10 +15,27 @@ const props = withDefaults(defineProps<Props>(), {
enumerate: true,
divider: false,
});
// onBeforeMount(async () => {
// await useArticlesStore().initialize()
// })
const { article } = useFetchArticle(props.doi);
const { mobile } = useDisplay();
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(() => {
return props?.title ?? article?.value?.title ?? props.doi;
});
......@@ -26,8 +44,8 @@ const articleAbstract = computed(() => {
});
</script>
<template>
<v-list-item :href="article?.href" :id="props.doi" :target="article?.target" density="compact" color="transparent"
class="px-1">
<v-list-item :href="article?.href" :id="props.doi" :target="article?.target" density="compact"
color="transparent" class="px-1">
<template #prepend v-if="!mobile && enumerate">
<v-avatar color="primary" size="small" density="compact" variant="tonal">
{{ 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'
import { ref, computed, watchEffect, toValue } from "vue"
// import { useFetch } from '#app';
// import { useFetch } from "nuxt"
import { useFetch } from '#imports'
import { type MaybeRef, useFetch } from '#imports'
import article from "@/public/articles.json"
export interface ArticleMessage {
export interface CrossrefArticle {
DOI: string;
issue: number;
title: string | string[];
type: string;
title: string[];
author: Array<{ family: string; given: string }>;
"container-title-short": string;
// "container-title-short": string;
"short-container-title": string;
"container-title": string;
abstract: string;
abstract?: string;
published: {
"date-parts": string[];
};
......@@ -23,7 +25,7 @@ export interface ArticleMessage {
export interface Article {
export interface WikiArticle {
DOI: string
title: string
subtitle: string
......@@ -36,115 +38,125 @@ export interface Article {
prependIcon: string
}
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 rawArticle = ref<RawArticle>()
const srcArticle = ref<SrcArticle | null>(null)
const store = useArticlesStore()
const pending = ref(false)
const doiBaseUrl = ref(new URL("https://doi.org/"));
const url = ref(new URL(`/works/${doi}`, " https://api.crossref.org/").href);
const article = computed(() => {
if (srcArticle.value != undefined) {
const url = ref(new URL(`/works/${toValue(doi)}`, " https://api.crossref.org/").href);
const article = ref()
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 {
DOI,
title,
"container-title-short": cts,
"container-title": ct,
journalAbbreviation,
abstract,
published,
issued,
author,
...rest
} = srcArticle.value;
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
} = zoteroArticle;
return {
DOI,
title: sanitizedTitle,
title,
subtitle: toAuthorsString(author || []),
author,
containerTitle: sanitizedContainerTitle,
abstract: sanitizedAbstract,
year: published?.["date-parts"][0][0] ?? issued?.["date-parts"][0][0] ?? '',
containerTitle: ct,
abstract,
year: issued?.["date-parts"][0][0] ?? '',
href: getReferenceUrl(DOI),
target: "_blank",
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
})
}
watchEffect(() => {
// no article in the store
if (store.articles.size === 0) {
fetchLocalArticles()
function crossrefToArticle(article: CrossrefArticle): WikiArticle {
const { title, DOI, type, "container-title": ct, "short-container-title": sct, abstract, author, issued } = article
// let sanitizedAbstract = abstract
const sanitizedAbstract = abstract ? /(?:\<jats\:p\>)?(.*)(?:\<\/jats\:p\>)?/.exec(abstract)?.[1] ?? '' : ''
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)) {
srcArticle.value = store.articles.get(doi)
return
} else {
fetchCrossRef()
}
function crossrefToCsl(article: CrossrefArticle): CslJson {
const { title, DOI, type, "container-title": ct, "short-container-title": sct, abstract, author, issued } = article
// let sanitizedAbstract = abstract
const sanitizedAbstract = abstract ? /(?:\<jats\:p\>)?(.*)(?:\<\/jats\:p\>)?/.exec(abstract)?.[1] ?? '' : ''
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) {
return new URL(doi, doiBaseUrl.value).href;
if (store.articles.has(toValue(doi))) {
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 }
}
......
import { MeiliSearch } from 'meilisearch'
import { useRuntimeConfig, watchEffect, type MaybeRef, ref, toValue } from '#imports'
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(""),
search: Ref<string> = ref(""),
filter: Ref<string> = ref(''),
......@@ -18,12 +21,11 @@ export function useFetchMsDocument(
apiKey: runtimeConfig.public.meiliApiKey
})
const pending = ref(false)
const filterError = ref(null)
const filterError: Ref<string | null> = ref(null)
const hits: Ref<Hits<Record<string, any>>> = ref([])
const totalHits = ref(0)
const totalPages = ref(0)
const facetDistribution: Ref<FacetDistribution | undefined> = ref({})
// reset page when filter and search change
watch(filter, () => {
page.value = 1
......@@ -32,34 +34,36 @@ export function useFetchMsDocument(
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 }
}
---
layout: db
navigation: false
---
\ No newline at end of file
---
::refseq-db
::
---
layout: db
navigation: false
---
::structure-db
::
\ No newline at end of file
......@@ -25,6 +25,6 @@ appVersion: "1.16.0"
dependencies:
- name: meilisearch
version: 0.2.8
version: 0.3.0
repository: "https://meilisearch.github.io/meilisearch-kubernetes"
......@@ -7,7 +7,7 @@ services:
args:
BASE_URL: /wiki/
MEILI_HOST: http://localhost:7700
MEILI_API_KEY: 269d546c85959e3d125cf7b34975a91503e0a63f0547c395c06ddf696a7cf12d
MEILI_API_KEY: f9cc073016cbb392365aae86517878cb3f3408bb85c1fafd06e27f73ccb35e3d
container_name: nuxt
environment:
HOST: 0.0.0.0
......@@ -30,7 +30,7 @@ services:
- main
meilisearch:
image: getmeili/meilisearch:v1.4
image: getmeili/meilisearch:v1.5
# command:
# - meilisearch
# - --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>
<LayoutWrapper>
<slot />
......
......@@ -5,16 +5,7 @@ import { useFacetsStore, type Facets } from '~~/stores/facets'
const facetStore = useFacetsStore()
</script>
<template>
<LayoutWrapper :fluid="true" :toc="false" :edit="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>
<LayoutWrapper :fluid="true" :toc="false" :edit="false" :nav-drawer="false">
<slot />
</LayoutWrapper>
</template>
......
......@@ -6,6 +6,7 @@ export default defineNuxtConfig({
'vuetify-nuxt-module',
'@vueuse/nuxt',
'@pinia/nuxt',
'nuxt-meilisearch',
// '@unocss/nuxt',
],
content: {
......@@ -32,6 +33,11 @@ export default defineNuxtConfig({
}
},
meilisearch: {
hostUrl: 'https://my-meilisearch-server.example.com',
searchApiKey: 'api_key',
serverSideUsage: false // default false
},
devtools: {
enabled: false
},
......@@ -39,6 +45,10 @@ export default defineNuxtConfig({
public: {
defenseFinderWebservice: '/',
meilisearchClient: {
hostUrl: 'http://localhost:7700',
searchApiKey: 'api_key',
},
meiliHost: 'http://localhost:7700',
meiliApiKey: 'api_key'
}
......