diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 9fd01a308b4556bb00d617623068afc9734154d7..7292c2cabca2555b1c5b8415aeed636a71b8bc0a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -12,7 +12,7 @@ workflow: # Functions that should be executed before the build script is run variables: - HELM_VERSION: "3.9.3" + HELM_VERSION: "3.13.3" IMAGE_NAME: "df-wiki" # dev HOST_DEV: 'defense-finder.dev.pasteur.cloud' @@ -31,6 +31,7 @@ cache: stages: + - delete-release - build-df-cli - lint @@ -39,9 +40,9 @@ stages: - update-meilisearch-indexes - get-meili-key - build - - build-wiki + # - build-wiki - deploy - + # - load-website .docker-login: &docker-login - i=0; while [ "$i" -lt 12 ]; do docker info && break; sleep 5; i=$(( i + 1 )) ; done @@ -353,25 +354,94 @@ build:prod:wiki: - if: $CI_COMMIT_BRANCH == "main" +# build-wiki:dev: +# stage: build-wiki +# needs: +# - set-meili-env:dev +# rules: +# - if: $CI_COMMIT_BRANCH != "main" +# image: node:21.1-bookworm-slim +# variables: +# NODE_OPTIONS: --max_old_space_size=12288 +# NUXT_APP_BASE_URL: /wiki/ +# NUXT_PUBLIC_MEILISEARCH_CLIENT_HOST_URL: ${MEILI_HOST} +# NUXT_PUBLIC_MEILISEARCH_CLIENT_SEARCH_API_KEY: ${MEILI_API_KEY} +# NUXT_PUBLIC_MEILI_HOST: ${MEILI_HOST} +# NUXT_PUBLIC_MEILI_API_KEY: ${MEILI_API_KEY} +# before_script: +# - npm install +# script: +# - npm run generate +# artifacts: +# paths: +# - .output/public + +# build-wiki:prod: +# stage: build-wiki +# rules: +# - if: $CI_COMMIT_BRANCH == "main" +# needs: +# - set-meili-env:prod +# - get-zotero +# image: node:21.1-bookworm-slim +# variables: +# NODE_OPTIONS: --max_old_space_size=12288 +# NUXT_APP_BASE_URL: /wiki/ +# NUXT_PUBLIC_MEILISEARCH_CLIENT_HOST_URL: ${MEILI_HOST} +# NUXT_PUBLIC_MEILISEARCH_CLIENT_SEARCH_API_KEY: ${MEILI_API_KEY} +# NUXT_PUBLIC_MEILI_HOST: ${MEILI_HOST} +# NUXT_PUBLIC_MEILI_API_KEY: ${MEILI_API_KEY} +# before_script: +# - npm install +# script: +# - npm run generate +# artifacts: +# paths: +# - .output/public + + +# load-website:dev: +# image: harbor.pasteur.fr/kube-system/helm-kubectl:$HELM_VERSION +# stage: load-website +# needs: +# - build-wiki:dev +# - deploy:dev +# variables: +# NAMESPACE: "defense-finder-dev" +# environment: +# name: "k8sdev-01" +# rules: +# - if: $CI_COMMIT_BRANCH != "main" +# script: +# - kubectl --namespace ${NAMESPACE} wait pod -l "app.kubernetes.io/name=df-wiki" --for condition=Ready --timeout=600s +# - echo "Le pod est ready" +# - WIKI_POD=$(kubectl --namespace ${NAMESPACE} get pods -l "app.kubernetes.io/name=df-wiki" --output jsonpath='{.items[0].metadata.name}') +# - kubectl --namespace ${NAMESPACE} cp .output/public/ ${WIKI_POD}:/website +# - | +# kubectl --namespace ${NAMESPACE} +# exec ${WIKI_POD} -- bash -c 'cd /structure-data/sanitized-dump && find * -type d -exec sh -c "for d in "$@"; do (cd "/usr/share/nginx/html/$d"; cp --archive --recursive --symbolic-link /structure-data/sanitized-dump/$d/* .) done" argv0 {} +' + + +# load-website:prod: +# image: harbor.pasteur.fr/kube-system/helm-kubectl:$HELM_VERSION +# stage: load-website +# needs: +# - build-wiki:prod +# - deploy:prod +# environment: +# name: "k8sprod-02" +# variables: +# NAMESPACE: "defense-finder-prod" +# rules: +# - if: $CI_COMMIT_BRANCH == "main" +# script: +# - kubectl --namespace ${NAMESPACE} wait pod -l "app.kubernetes.io/name=df-wiki" --for condition=Ready --timeout=600s +# - echo "Le pod est ready" +# - WIKI_POD=$(kubectl --namespace ${NAMESPACE} get pods -l "app.kubernetes.io/name=df-wiki" --output jsonpath='{.items[0].metadata.name}') -build-wiki: - stage: build-wiki - image: node:21.1-bookworm-slim - variables: - NODE_OPTIONS: --max_old_space_size=12288 - NUXT_APP_BASE_URL: /wiki/ - NUXT_PUBLIC_MEILISEARCH_CLIENT_HOST_URL: ${MEILI_HOST} - NUXT_PUBLIC_MEILISEARCH_CLIENT_SEARCH_API_KEY: ${MEILI_API_KEY} - NUXT_PUBLIC_MEILI_HOST: ${MEILI_HOST} - NUXT_PUBLIC_MEILI_API_KEY: ${MEILI_API_KEY} - before_script: - - npm install - script: - - npm run generate - artifacts: - paths: - - .output/public - +# - kubectl --namespace ${NAMESPACE} cp .output/public/ ${WIKI_POD}:/website +# - kubectl --namespace ${NAMESPACE} cp scripts/copy-structure-data.sh ${WIKI_POD}:/structure-data/sanitized-dump +# - kubectl --namespace ${NAMESPACE} exec ${WIKI_POD} -- bash -c 'cd /structure-data/sanitized-dump && bash copy-structure-data.sh' @@ -397,6 +467,16 @@ build-wiki: --set env="${ENV:-development}" --values deploy/df-wiki/values.yaml --values deploy/df-wiki/values.${ENV:-development}.yaml + after_script: + - kubectl --namespace ${KUBE_NAMESPACE} wait pod -l "app.kubernetes.io/name=df-wiki" --for condition=Ready --timeout=600s + - echo "Wiki pod is ready" + - WIKI_POD=$(kubectl --namespace ${KUBE_NAMESPACE} get pods -l "app.kubernetes.io/name=df-wiki" --output jsonpath='{.items[0].metadata.name}') + - echo ${WIKI_POD} + - kubectl --namespace ${KUBE_NAMESPACE} cp .output/public/ ${WIKI_POD}:/website + - kubectl --namespace ${KUBE_NAMESPACE} cp scripts/copy-structure-data.sh ${WIKI_POD}:/structure-data/sanitized-dump + - kubectl --namespace ${KUBE_NAMESPACE} exec ${WIKI_POD} -- bash -c 'cd /structure-data/sanitized-dump && bash copy-structure-data.sh' + + deploy:dev: extends: .deploy @@ -464,3 +544,5 @@ delete-helm-release:prod: script: - echo "Removing $CI_PROJECT_NAME-$CI_ENVIRONMENT_NAME" - helm delete -n ${NAMESPACE} $CI_PROJECT_NAME-$CI_ENVIRONMENT_NAME + + diff --git a/components/ServerDbTable.vue b/components/ServerDbTable.vue index 4850583886fa8991795d8593645e192358e19bd7..66a56ee6c6b6851004bcd82c81e6a36e43bedf4e 100644 --- a/components/ServerDbTable.vue +++ b/components/ServerDbTable.vue @@ -1,23 +1,38 @@ <script setup lang="ts"> // import type { FacetDistribution } from "meilisearch"; +import { useCsvDownload } from "@/composables/useCsvDownload" import { useSlots } from 'vue' import { useDisplay } from "vuetify"; import * as Plot from "@observablehq/plot"; import PlotFigure from "~/components/PlotFigure"; -import * as d3 from "d3"; import { useThrottleFn } from '@vueuse/core' + +import * as d3 from "d3"; import { useMeiliSearch } from "#imports" +// import { saveAs } from "file-saver"; +import { useFileSystemAccess } from '@vueuse/core' export interface SortItem { key: string, order: boolean | 'asc' | 'desc' } +export interface NumericalFilter { + id: string + name: string + range: [number, number] +} + +export interface NumericalFilterModel extends NumericalFilter { + model: [number, number] +} export interface Props { title?: string db?: string sortBy?: SortItem[] facets: MaybeRef<string[]> + numericalFilters?: MaybeRef<string | undefined> dataTableServerProps: Record<string, any> + columnsToDownload?: MaybeRef<string[] | undefined> } export interface FilterItem { @@ -32,7 +47,10 @@ export interface FilterItem { const props = withDefaults(defineProps<Props>(), { title: '', db: 'refseq', + columnsToDownload: undefined, sortBy: () => [{ key: "type", order: "asc" }], + numericalFilters: undefined + }); @@ -51,18 +69,16 @@ const msFilter: Ref<string | undefined> = ref(undefined) const page = ref(1) let loading = ref(false) const expanded = ref([]) -const { height } = useDisplay(); +const { height, mobile } = useDisplay(); const minTableHeight = ref(400) const computedTableHeight = computed(() => { const computedHeight = height.value - 350 return computedHeight > minTableHeight.value ? computedHeight : minTableHeight.value }) -const plddtRange = ref([0, 100]) -// const { pending: pendingDownloadData, downloadCsv } = useCsvDownload(props.db, `df-${props.db}`) +const pendingDownloadData = ref(false) const filterInputValues = computed(() => { - // console.log("recompouted FILTER value") if (filterOrSearch.value != null) { return filterOrSearch.value.filter(({ props }) => props.type !== 'text') } else { @@ -70,24 +86,6 @@ const filterInputValues = computed(() => { } }) -// const queryInputValue = computed(() => { -// 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) }) @@ -127,39 +125,18 @@ onMounted(async () => { searchOrFilter() }) -const hasPlddt = computed(() => props.db === 'structure') - -// Fetch results -const plddtFilter = computed(() => { - const plddtRangeValue = plddtRange.value - if (hasPlddt.value && plddtRangeValue?.length === 2 && (plddtRangeValue[0] !== 0 || plddtRangeValue[1] !== 100)) { - return `plddts ${plddtRangeValue[0]} TO ${plddtRangeValue[1]}` - } else { - return undefined - } -}) const computedFilter = computed(() => { - if (toValue(msFilter)) { - if (toValue(plddtFilter)) { - return `${toValue(msFilter)} AND ${toValue(plddtFilter)}` - } - else { - return toValue(msFilter) - } - } else { - if (toValue(plddtFilter)) { - return `${toValue(plddtFilter)}` - } - else { - return undefined - } - } + return [toValue(msFilter), props.numericalFilters].filter(f => f !== undefined).join(" AND ") }) +watch(computedFilter, () => { + searchOrFilter() +}) + const msError = computed(() => { if (filterError.value?.type && filterError.value?.message) { @@ -167,8 +144,9 @@ const msError = computed(() => { } else { return false } }) -const throttleSearch = useThrottleFn(async () => { searchOrFilter() }, 300) - +const throttleSearch = useThrottleFn(async () => { + searchOrFilter() +}, 300) async function searchOrFilter(pagination = true) { // do something, it will be called at most 1 time per second @@ -292,13 +270,7 @@ const autocompleteItems = computed(() => { } }) -// 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] @@ -314,28 +286,45 @@ function clearSearch() { search.value = "" } +async function downloadData() { + pendingDownloadData.value = true + try { + const { data } = await useAsyncMeiliSearch({ + index: props.db, + params: { ...toValue(notPaginatedParams), filter: toValue(computedFilter), sort: toValue(msSortBy) }, + query: toValue(search), + }) - - -// const groupSortDomain = computed(() => { -// console.log(msResult.value) -// return msResult.value ? d3.groupSort(msResult.value?.hits?.filter((d) => d.phylum), (g) => d3.median(g, (d) => d.phylum), (d) => d.type) : [] -// }) - - - + useCsvDownload(data, props.columnsToDownload, props.title) + } finally { + pendingDownloadData.value = false + } +} </script> <template> <v-card flat color="transparent"> <v-card-text> </v-card-text> <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> + <slot name="numerical-filters" :search="throttleSearch"></slot> + </v-card-text> + <v-toolbar flat color="transparent"> </v-toolbar> + <v-data-table-server v-if="!msError" v-model:page="page" color="primary" v-bind="dataTableServerProps" + v-model:items-per-page="hitsPerPage" v-model:sortBy="sortByRef" v-model:expanded="expanded" fixed-header + :loading="loading" :items="msResult?.hits ?? []" :items-length="totalHits" density="compact" + :items-per-page-options="itemsPerPage" :height="computedTableHeight" class="elevation-1 mt-2"> + + <template #top> + <v-toolbar floating><v-toolbar-title class="text-capitalize"> + <v-badge :content="totalHits" color="primary" inline> + <v-btn prepend-icon="md:download" :loading="pendingDownloadData" variant="text" color="primary" + @click="downloadData()">{{ + props.title }} + </v-btn> + </v-badge> + </v-toolbar-title> + <v-text-field v-model="search" label="Search..." hide-details="auto" prepend-inner-icon="mdi-magnify" + single-line clearable class="mr-2"></v-text-field> <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" @@ -352,29 +341,9 @@ function clearSearch() { </v-list-item> </template> </v-autocomplete> - </v-col> - </v-row> - <v-row v-if="props.db === 'structure'"> - <v-col> - <v-range-slider v-model="plddtRange" density="compact" hide-details="auto" label="pLDDT" step="0.1" - @update:modelValue="throttleSearch()"> - <template v-slot:prepend> - <span hide-details single-line type="number" variant="outlined" density="compact" - style="width: 70px">{{ plddtRange[0] }}</span> - </template> - <template v-slot:append> - <span hide-details single-line type="number" variant="outlined" style="width: 70px" - density="compact">{{ plddtRange[1] }}</span> - </template> - </v-range-slider> - </v-col> - </v-row> - </v-card-text> - <v-data-table-server v-if="!msError" v-model:page="page" color="primary" v-bind="dataTableServerProps" - v-model:items-per-page="hitsPerPage" v-model:sortBy="sortByRef" v-model:expanded="expanded" fixed-header - :loading="loading" :items="msResult?.hits ?? []" :items-length="totalHits" density="compact" - :items-per-page-options="itemsPerPage" :height="computedTableHeight" class="elevation-1 mt-2"> + </v-toolbar> + </template> <template v-for="(slot, index) of Object.keys(slots)" :key="index" v-slot:[slot]="data"> <slot :name="slot" v-bind="data"></slot> diff --git a/components/content/ListSystems.vue b/components/content/ListSystems.vue deleted file mode 100644 index ed19ba4b3731bf747e0580b70e1c212341c8af9e..0000000000000000000000000000000000000000 --- a/components/content/ListSystems.vue +++ /dev/null @@ -1,66 +0,0 @@ -<script setup lang="ts"> -import { usePfamStore } from "@/stores/pfam"; - -export interface Props { - headers: Array<Object>; - systems: Array<Object>; - height?: number; -} - -const props = withDefaults(defineProps<Props>(), { - height: 800, -}); - -const itemsPerParge = ref(25); -const search = ref(""); -const sortBy = ref([{ key: "system", order: "asc" }]); -const expanded = ref([]); - -function filterOnlyCapsText(value, query, item) { - if (value != null && query != null) { - if (typeof value === "string") { - return value.toString().toLowerCase().indexOf(query.toLowerCase()) !== -1; - } - if (typeof value == "object") { - if (value?.name) { - return ( - value.name.toString().toLowerCase().indexOf(query.toLowerCase()) !== - -1 - ); - } - } - } - return false; -} -const { initPfam } = usePfamStore(); -onMounted(() => { - initPfam(); -}) -</script> -<template> - <v-card> - <v-toolbar density="compact"> - <v-toolbar-title>Defense Systems</v-toolbar-title> - - <v-text-field v-model="search" density="compact" variant="underlined" prepend-inner-icon="mdi-magnify" - label="Search for defense systems" single-line hide-details class="mx-2" clearable></v-text-field> - </v-toolbar> - <v-data-table-virtual fixed-header :height="height" :items-per-page="itemsPerParge" v-model:sort-by="sortBy" - :headers="props.headers" density="compact" :custom-filter="filterOnlyCapsText" :items="props.systems" - :search="search" item-value="system.name"> - <template #[`item.system`]="{ item }"> - <v-chip variant="text" link :to="item?.system?.path ? `${item?.system?.path}` : null">{{ - item?.system?.name ?? 'None' - }}</v-chip> - </template> - <template #[`item.article`]="{ item }"> - <ArticleDoi v-if="item?.article" :doi="item.article.doi" :title="item.article?.title" - :abstract="item.article?.abstract" :divider="false" :enumerate="false" /> - </template> - <template #[`item.PFAM`]="{ item }"> - - <pfam-chips v-if="item?.PFAM" :pfam-string="item.PFAM"></pfam-chips> - </template> - </v-data-table-virtual> - </v-card> -</template> \ No newline at end of file diff --git a/components/content/Mermaid.vue b/components/content/Mermaid.vue index c747d1f5827731593d1744e6ce5650111a1a6879..aa40306337a254d04b3673364a35c87e574fdefe 100644 --- a/components/content/Mermaid.vue +++ b/components/content/Mermaid.vue @@ -10,7 +10,7 @@ <script setup lang="ts"> const slot = useSlots() -const el = ref(null) +const el: Ref<HTMLElement | null> = ref(null) const rendered = ref(false) async function render() { diff --git a/components/content/MolstarPdbePlugin.vue b/components/content/MolstarPdbePlugin.vue index 5f1570b7e066d4767a41b54fc4397bfb8b15c017..87903b6fd7767210e53a536cbd224e98234098cd 100644 --- a/components/content/MolstarPdbePlugin.vue +++ b/components/content/MolstarPdbePlugin.vue @@ -105,86 +105,43 @@ useHead({ const pdbeMolstarComponent = ref(null) // const selectedPdb = ref("/wiki/avs/AVAST_I,AVAST_I__Avs1A,0,V-plddts_85.07081.pdb") const selectedPdb: Ref<string | null> = ref(null) - +const structureToDownload: Ref<string | null> = ref(null) const selectedPaePath = computed(() => { return selectedPdb.value ? `${selectedPdb.value.split(".").slice(0, -1).join('.')}.png` : null }) -// const paeData = ref([]) -// watch(selectedPaePath, async (newPaePath) => { -// if (newPaePath !== null) { -// try { -// const data = await d3.tsv(newPaePath); -// if (data.length > 500) { -// paeError.value = `The PAE is too large to be displayed (${data.length} residus)` -// paeData.value = [] -// } -// else if (data?.[0]?.[0] === undefined) { -// paeError.value = "The PAE cannot be downloaded" -// paeData.value = [] -// } -// else { -// paeData.value = data -// paeError.value = null -// } - -// } -// catch (error) { -// console.log(error) -// } -// } else { -// paeData.value = [] -// } - -// }) - -// const sanitizedPaeData = computed(() => { -// return paeData.value.reduce((acc, curr, index) => { -// const scoredResidue = index -// // let newAcc = [...acc] -// for (const [alignedResidue, value] of Object.entries(curr)) { -// // console.log(value) -// acc.push({ alignedResidue: parseInt(alignedResidue), scoredResidue: parseInt(scoredResidue), value: parseFloat(value) }) -// // newAcc = [...newAcc, ...[{ alignedResidue: parseInt(alignedResidue), scoredResidue: parseInt(scoredResidue), value: parseFloat(value) }]] -// } - -// return acc -// }, []) -// }) - - -// const plotPaeOptions = computed(() => { -// return { -// width: 640, -// height: 640, -// color: { scheme: "Greens", legend: true, reverse: true, label: "Expected position error (Ångströms)" }, -// y: { reverse: true }, -// marks: [ -// Plot.dot(sanitizedPaeData.value, { x: "scoredResidue", y: "alignedResidue", stroke: "value" }) -// ] -// } -// }) - watch(selectedPdb, (newSelectedPdb, prevSelectPdb) => { viewPdb(newSelectedPdb) + structureToDownload.value = newSelectedPdb }) +onMounted(() => { + const urls = toValue(refinedDataUrls) + if (props.uniq && urls.length >= 1) { + structureToDownload.value = urls[0] + + } +}) function viewPdb(pdbPath: string | null) { if (pdbPath !== null) { dialog.value = true const format = toValue(pdbPath)?.split(".").slice(-1)[0]?.toLowerCase() ?? "pdb" moleculeFormat.value = format if (pdbeMolstarComponent.value?.viewerInstance) { - console.log(pdbeMolstarComponent.value) const viewerInstance = pdbeMolstarComponent.value.viewerInstance const customData = { url: pdbPath, format: format, binary: false } viewerInstance.visual.update({ customData }) - + } } } + + + + + function setSelectedPdbToFirst() { const urls = toValue(refinedDataUrls) if (urls.length >= 1) { @@ -200,107 +157,116 @@ const moleculeFormat: Ref<string> = ref("pdb") <template> - <span class="d-flex flex-wrap align-center justify-center"> + <template v-if="uniq"> + <v-row> + <v-btn size="x-small" variant="tonal" icon="md:visibility" @click="setSelectedPdbToFirst()"></v-btn> + <v-btn :disabled="!structureToDownload" size="x-small" variant="tonal" icon="md:download" class="ml-1" + :href="structureToDownload"></v-btn> + </v-row> + </template> + <v-row v-else> <v-col> - <v-btn v-if="uniq" size="x-small" variant="tonal" icon="md:visibility" @click="setSelectedPdbToFirst()"></v-btn> - <v-select v-else v-model="selectedPdb" label="Select PDB" :items="refinedDataUrls" hide-details="auto"> - </v-select> + <span class="d-flex flex-wrap align-center justify-center"> + <v-select v-model="selectedPdb" label="Select PDB" :items="refinedDataUrls" hide-details="auto"> + </v-select> + </span> </v-col> - - <v-dialog v-model="dialog" transition="dialog-bottom-transition" fullscreen :scrim="false"> - <v-card flat :rounded="false"> - <v-toolbar> - <v-toolbar-title>Structures</v-toolbar-title> - <v-select v-model="selectedPdb" label="Select PDB" :items="refinedDataUrls" - hide-details="auto"></v-select> - <v-spacer></v-spacer> - - <v-btn :disabled="!selectedPdb" icon="md:download" :href="selectedPdb"></v-btn> - <v-btn icon @click="closeStructure"> - <v-icon>mdi-close</v-icon> - </v-btn> - - </v-toolbar> - <v-card-text> - <v-row> - <v-col :cols="mobile ? 12 : 'auto'"> - <v-sheet v-if="selectedPdb" - class="d-flex align-center justify-center flex-wrap text-center mx-auto px-4 my-3" - :height="computedHeight" :width="computedWidth" position="relative"> - <pdbe-molstar ref="pdbeMolstarComponent" hide-controls="true" landscape="true" - :custom-data-url="selectedPdb" alphafold-view="true" - :custom-data-format="moleculeFormat"></pdbe-molstar> - </v-sheet> - </v-col> - <v-col :cols="mobile ? 12 : undefined"> - <v-img :src="selectedPaePath"></v-img> - - <!-- <PlotFigure v-if="sanitizedPaeData?.length > 0 && paeError === null" defer + </v-row> + <v-dialog v-model="dialog" transition="dialog-bottom-transition" fullscreen :scrim="false"> + <v-card flat :rounded="false"> + <v-toolbar> + <v-toolbar-title>Structures</v-toolbar-title> + <v-select v-model="selectedPdb" label="Select PDB" :items="refinedDataUrls" hide-details="auto"></v-select> + <v-spacer></v-spacer> + + <v-btn :disabled="!selectedPdb" icon="md:download" :href="structureToDownload"></v-btn> + <v-btn icon @click="closeStructure"> + <v-icon>mdi-close</v-icon> + </v-btn> + + </v-toolbar> + <v-card-text> + <v-row> + <v-col :cols="mobile ? 12 : 'auto'"> + <v-sheet v-if="selectedPdb" + class="d-flex align-center justify-center flex-wrap text-center mx-auto px-4 my-3" + :height="computedHeight" :width="computedWidth" position="relative"> + <pdbe-molstar ref="pdbeMolstarComponent" landscape :custom-data-url="selectedPdb" + alphafold-view="true" :custom-data-format="moleculeFormat"></pdbe-molstar> + </v-sheet> + </v-col> + <v-col :cols="mobile ? 12 : undefined"> + <v-img :src="selectedPaePath"></v-img> + + <!-- <PlotFigure v-if="sanitizedPaeData?.length > 0 && paeError === null" defer :options="plotPaeOptions"></PlotFigure> <v-alert v-else type="warning" variant="tonal">{{ paeError }}</v-alert> --> - <v-card flat color="transparent"> - <v-card-title>Model Confidence</v-card-title> - <v-card-text> - AlphaFold produces a per-residue model - confidence score (pLDDT) between 0 and 100. Some regions below 50 pLDDT may be - unstructured - in isolation. - </v-card-text> - <v-list> - <v-list-item> - <template #prepend> - <div class="legendColor mr-2" style="background-color: rgb(0, 83, 214);"> - </div> - </template> - <v-list-item-title> - Very high (pLDDT > 90) - </v-list-item-title> - - </v-list-item> - <v-list-item> - <template #prepend> - <div class="legendColor mr-2" style="background-color: rgb(101, 203, 243);"> - </div> - </template> - <v-list-item-title> - High (90 > pLDDT > 70) - </v-list-item-title> - - </v-list-item> - <v-list-item> - <template #prepend> - <div class="legendColor mr-2" style="background-color: rgb(255, 219, 19);"> - </div> - </template> - <v-list-item-title> - Low (70 > pLDDT > 50) </v-list-item-title> - - </v-list-item> - <v-list-item> - <template #prepend> - <div class="legendColor mr-2" style="background-color: rgb(255, 125, 69);"> - </div> - </template> - <v-list-item-title> - Very low (pLDDT < 50) </v-list-item-title> - - </v-list-item> - </v-list> - </v-card> - </v-col> - </v-row> - </v-card-text> - </v-card> - </v-dialog> - </span> + <v-card flat color="transparent"> + <v-card-title>Model Confidence</v-card-title> + <v-card-text> + AlphaFold produces a per-residue model + confidence score (pLDDT) between 0 and 100. Some regions below 50 pLDDT may be + unstructured + in isolation. + </v-card-text> + <v-list> + <v-list-item> + <template #prepend> + <div class="legendColor mr-2" style="background-color: rgb(0, 83, 214);"> + </div> + </template> + <v-list-item-title> + Very high (pLDDT > 90) + </v-list-item-title> + + </v-list-item> + <v-list-item> + <template #prepend> + <div class="legendColor mr-2" style="background-color: rgb(101, 203, 243);"> + </div> + </template> + <v-list-item-title> + High (90 > pLDDT > 70) + </v-list-item-title> + + </v-list-item> + <v-list-item> + <template #prepend> + <div class="legendColor mr-2" style="background-color: rgb(255, 219, 19);"> + </div> + </template> + <v-list-item-title> + Low (70 > pLDDT > 50) </v-list-item-title> + + </v-list-item> + <v-list-item> + <template #prepend> + <div class="legendColor mr-2" style="background-color: rgb(255, 125, 69);"> + </div> + </template> + <v-list-item-title> + Very low (pLDDT < 50) </v-list-item-title> + + </v-list-item> + </v-list> + </v-card> + </v-col> + </v-row> + </v-card-text> + </v-card> + </v-dialog> </template> -<style scoped> +<style> .msp-plugin .msp-plugin-content { color: black !important; } +div.msp-plugin-content.msp-layout-expanded { + z-index: 99 !important +} + .legendColor { height: 16px; width: 16px; diff --git a/components/content/RefseqDb.vue b/components/content/RefseqDb.vue index ee4e713bbcd0f8a247d5283afe1a40fa7230f3a9..38260b1fb067278cddf555a49412b847f18253ea 100644 --- a/components/content/RefseqDb.vue +++ b/components/content/RefseqDb.vue @@ -11,6 +11,8 @@ const itemValue = ref("id"); const { width } = useDisplay(); const scaleTransform: Ref<string[]> = ref([]) + + const facets = ref([ "replicon", "type", @@ -80,10 +82,7 @@ onMounted(async () => { async function getAllHits(params) { if (params.index === 'refseq') { - console.log(params.index) const { data, error } = await useAsyncMeiliSearch(params) - console.log(error.value) - console.log(data.value) allHits.value = data.value } } @@ -207,7 +206,8 @@ const binPlotOptions = ref({ }) const binPlotDataOptions = computed(() => { - return allHits.value?.hits?.length > 0 ? { + const toValueAllHits = toValue(allHits) + return toValueAllHits?.hits?.length > 0 ? { ...binPlotOptions.value, color: { ...binPlotOptions.value.color, @@ -215,7 +215,7 @@ const binPlotDataOptions = computed(() => { }, // fy: { domain: groupSortDomain.value }, marks: [ - Plot.cell(allHits.value?.hits ?? [], Plot.group({ fill: "count" }, { x: "type", y: selectedTaxoRank.value, tip: true, inset: 0.5, sort: { y: "fill" } })), + Plot.cell(toValueAllHits?.hits ?? [], Plot.group({ fill: "count" }, { x: "type", y: selectedTaxoRank.value, tip: true, inset: 0.5, sort: { y: "fill" } })), ] } : null @@ -282,13 +282,7 @@ const scaleType = ref("linear") <ServerDbTable title="RefSeq" db="refseq" :sortBy="sortBy" :facets="facets" :data-table-server-props="dataTableServerProps" @refresh:search="getAllHits"> - <template #top> - <v-toolbar><v-toolbar-title class="text-capitalize"> - RefSeq - </v-toolbar-title><v-spacer></v-spacer> - </v-toolbar> - </template> <template #[`item.accession_in_sys`]="{ item }"> <CollapsibleChips :items="namesToAccessionChips(item.accession_in_sys)"> </CollapsibleChips> diff --git a/components/content/StructureDb.vue b/components/content/StructureDb.vue index 711accda2a6ebdde83b6545beb50a1b957f8bc8e..e80e1719eaf4927c49185454b60e92a57858a0f7 100644 --- a/components/content/StructureDb.vue +++ b/components/content/StructureDb.vue @@ -2,6 +2,9 @@ import * as Plot from "@observablehq/plot"; import PlotFigure from "~/components/PlotFigure"; import type { SortItem } from "@/components/ServerDbTable.vue" +import { useNumericalFilter } from "@/composables/useNumericalfilter" + + import { ServerDbTable } from "#components" const sortBy: Ref<SortItem[]> = ref([{ key: 'system', order: "asc" }]) const itemValue = ref("id"); @@ -24,7 +27,16 @@ const headers: Ref<Object[]> = ref([ // { title: "Type", key: "type", removable: true }, ]) + const { search: msSearch, result: msResult } = useMeiliSearch('structure') +const { range: plddtsRange, stringifyFilter: plddtsFilter, reset: plddtsReset } = useNumericalFilter("plddts", 0, 100) +const { range: iptmRange, stringifyFilter: iptmFilter, reset: iptmReset } = useNumericalFilter("iptm+ptm", 0, 1) +const { range: pdockqRange, stringifyFilter: pdockqFilter, reset: pdockqReset } = useNumericalFilter("pDockQ", 0, 1) + +const numericalFilters = computed(() => { + const listFilters = [plddtsFilter, iptmFilter, pdockqFilter].map(f => toValue(f)).filter(f => f !== undefined) + return listFilters.length > 0 ? listFilters.join(" AND ") : undefined +}) const defaultDataTableServerProps = ref({ showExpand: false @@ -41,6 +53,7 @@ const dataTableServerProps = computed(() => { + function namesToCollapsibleChips(names: string[], file: string | null = null) { if (file === null) { return names.filter((it) => it !== "").map(it => ({ title: it.split("__")[1] })) @@ -69,21 +82,46 @@ const plddtDistribution = computed(() => { } }) -function remove(key) { - headers.value = headers.value.filter(header => header.key !== key) -} + + + </script> <template> <ServerDbTable title="Predicted Structures" db="structure" :sortBy="sortBy" :facets="facets" - :data-table-server-props="dataTableServerProps"> - - <template #top> - <v-toolbar><v-toolbar-title class="text-capitalize"> - Predicted Structures - </v-toolbar-title><v-spacer></v-spacer> - - </v-toolbar> + :data-table-server-props="dataTableServerProps" :numerical-filters="numericalFilters"> + + + + <template #numerical-filters="{ search }"> + <v-row> + <v-col cols="12" md="12" lg="4"> + <v-range-slider v-model="plddtsRange" strict density="compact" hide-details="auto" label="pLDDT" + step="0.5" :min="0" :max="100" thumb-label="always" @update:modelValue="search()"> + <template #append> + <v-btn variant="text" icon="md:restart_alt" @click="plddtsReset()"></v-btn> + </template> + </v-range-slider> + </v-col> + <v-col cols="12" md="12" lg="4"> + + <v-range-slider v-model="iptmRange" strict density="compact" hide-details="auto" label="iptm+ptm" + step="0.1" :min="0" :max="1" thumb-label="always" @update:modelValue="search()"> + <template #append> + <v-btn variant="text" icon="md:restart_alt" @click="iptmReset()"></v-btn> + </template> + </v-range-slider> + </v-col> + <!-- pdockqReset --> + <v-col cols="12" md="12" lg="4"> + <v-range-slider v-model="pdockqRange" strict density="compact" hide-details="auto" label="pDockQ" + step="0.1" :min="0" :max="1" thumb-label="always" @update:modelValue="search()"> + <template #append> + <v-btn variant="text" icon="md:restart_alt" @click="pdockqReset()"></v-btn> + </template> + </v-range-slider> + </v-col> + </v-row> </template> <template #[`item.proteins_in_the_prediction`]="{ item }"> @@ -97,9 +135,9 @@ function remove(key) { <MolstarPdbePlugin v-if="item?.pdb && item.pdb !== 'na'" :data-urls="[`/${toSystemName(item.system_genes)}/${pdbNameToCif(item.pdb)}`]" uniq> </MolstarPdbePlugin> - <span v-else class="d-flex flex-wrap align-center justify-center"> - <v-icon color="warning" icon="md:dangerous"></v-icon> - </span> + + <!-- <v-icon v-else color="warning" icon="md:dangerous"></v-icon> --> + </template> <template #[`item.completed`]="{ item }"> <v-icon v-if="item.completed" color="success" icon="md:check"></v-icon> diff --git a/components/content/SystemDb.vue b/components/content/SystemDb.vue index 72d1642f4e757b4da5e2a7b1881039bf534d7ec6..65e3c152f8b1ad61c37f0701272afdf07d82167d 100644 --- a/components/content/SystemDb.vue +++ b/components/content/SystemDb.vue @@ -3,7 +3,7 @@ import type { SortItem } from "@/components/ServerDbTable.vue" import { ServerDbTable } from "#components" const sortBy: Ref<SortItem[]> = ref([{ key: 'title', order: "asc" }]) const itemValue = ref("title"); -const facets: Ref<string[]> = ref(["title", "Sensor", "Effector", "Activator", "PFAM.AC"]) +const facets: Ref<string[]> = ref(["title", "Sensor", "Effector", "Activator", "PFAM.AC", "PFAM.DE"]) const headers: Ref<Object[]> = ref([ { title: "System", key: "title", removable: false }, { title: "Article", key: "doi", removable: false }, @@ -30,16 +30,13 @@ const dataTableServerProps = computed(() => { } }) +const columnsToDownload = ref(['title', 'doi', 'Sensor', 'Activator', 'Effector', 'PFAM', 'contributors',]) + </script> <template> - <ServerDbTable title="systems" db="systems" :sortBy="sortBy" :facets="facets" - :data-table-server-props="dataTableServerProps"> + <ServerDbTable title="List Systems" db="systems" :sortBy="sortBy" :facets="facets" + :data-table-server-props="dataTableServerProps" :columns-to-download="columnsToDownload"> - <template #top> - <v-toolbar><v-toolbar-title class="text-capitalize"> - List Systems </v-toolbar-title><v-spacer></v-spacer> - </v-toolbar> - </template> <template #[`item.title`]="{ item }"> <v-chip color="info" link :to="`/defense-systems/${item.title.toLowerCase()}`">{{ diff --git a/composables/useCsvDownload.ts b/composables/useCsvDownload.ts index 9534544a1654e9d66d4bf0888787fa55152ed0b0..a84e6ad90db80aaf3a27d3f9c0acef621fc25b26 100644 --- a/composables/useCsvDownload.ts +++ b/composables/useCsvDownload.ts @@ -1,48 +1,40 @@ -import { ref } from 'vue'; import Papa from 'papaparse'; -import { saveAs } from "file-saver"; +// import { saveAs } from "file-saver"; -export function useCsvDownload(index: MaybeRef<string>, baseName: MaybeRef<string> = "df" +export function useCsvDownload( + rawData: MaybeRef<Record<string, any>>, + columns: MaybeRef<string[] | undefined> = undefined, + baseName: MaybeRef<string> = 'data' ) { - const pending = ref(false) - const { search: msSearch, result: msResult } = useMeiliSearch(toValue(index)) - const filename = ref(`${toValue(baseName)}-data.csv`) - const downloadCsv = async ( - - query: MaybeRef<string>, - filter: MaybeRef<string>, - sortBy: MaybeRef<string[]>, - params: MaybeRef<Record<string, any>>, - ) => { - - - filename.value = `${toValue(baseName)}-${toValue(filter)}.csv` - pending.value = true - try { - await msSearch( - toValue(query), - { - ...toValue(params), - filter: toValue(filter), - sort: toValue(sortBy) - }) - const csvContent = Papa.unparse(toValue(msResult).hits); - // console.log(csvContent) - const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); - saveAs(blob, `${toValue(filename)}`); - } finally { - pending.value = false - } - + const filename = ref(`df-${toValue(baseName)}.csv`) + const data = ref() + const blob = ref() + if (toValue(rawData)?.hits?.length > 0) { + data.value = toValue(rawData).hits.map(row => { + let sanitizedRow = { ...row } + if (sanitizedRow?.PFAM?.length > 0) { + sanitizedRow = { + ...sanitizedRow, + PFAM: sanitizedRow.PFAM.map(({ AC }) => AC).join(", ") + } + } + if (sanitizedRow?.contributors?.length > 0) { + sanitizedRow = { + ...sanitizedRow, + contributors: sanitizedRow.contributors.join(", ") + } + } + return sanitizedRow + + }) + const csvContent = Papa.unparse(toValue(data), { columns: toValue(columns) }); + blob.value = new Blob([csvContent], { type: "text/csv" }); + var a = document.createElement("a"); + a.href = URL.createObjectURL(blob.value); + a.download = filename.value; + a.click(); + URL.revokeObjectURL(a.href); } - // watch(msResult, (newRes) => { - // console.log("save file !!!!!!") - - - // }) - - return { - pending, downloadCsv, - }; + return { data, filename } } diff --git a/composables/useNumericalfilter.ts b/composables/useNumericalfilter.ts new file mode 100644 index 0000000000000000000000000000000000000000..9ee97d80a4239e558dfdea1e57e563cc500519af --- /dev/null +++ b/composables/useNumericalfilter.ts @@ -0,0 +1,41 @@ +export function useNumericalFilter( + id: MaybeRef<string>, + min: MaybeRef<number>, + max: MaybeRef<number>, +) { + + + const range: Ref<[number, number]> = ref([toValue(min), toValue(max)]) + + + + + const stringifyFilter: Ref<string | undefined> = ref(`${toValue(id)} ${toValue(min)} TO ${toValue(max)}`) + + + + watchEffect(() => { + console.log("watch reange") + console.log(range.value) + if (range.value[0] === toValue(min) && range.value[1] === toValue(max)) { + stringifyFilter.value = undefined + } else { + stringifyFilter.value = `'${toValue(id)}' ${range.value[0]} TO ${range.value[1]}` + } + }) + function reset() { + range.value = [toValue(min), toValue(max)] + } + // watch(() => range, () => { + // console.log("watch reange") + // console.log(range) + // if (range.value[0] === toValue(min) && range.value[1] === toValue(max)) { + // stringifyFilter.value = undefined + // } else { + // stringifyFilter.value = `${toValue(id)} ${toValue(min)} TO ${toValue(max)}` + // } + // }, { deep: true }) + + + return { range, stringifyFilter, reset } +} \ No newline at end of file diff --git a/content/5.structure.md b/content/5.structure.md index 923ce4a93f5dcaef3fff697cbbf03112275a0d52..a21e7e49c3db8db7c3b1ff6c281bb25c02ae321b 100644 --- a/content/5.structure.md +++ b/content/5.structure.md @@ -5,7 +5,7 @@ navigation: icon: "mdi-database" --- -# Structures' prediction DB +# Structure's prediction DB In the following tables are various structures that were generated by Alphafold for all monomers, hetero- and homo-dimers for a given system. In the page for each system is the structure for the monomers and real structure when it exists. diff --git a/deploy/df-wiki/templates/deployment.yaml b/deploy/df-wiki/templates/deployment.yaml index c9f378b1571e9f9bf6fbb75b877e346c7450ddcc..8a362cad8a72b65dfe00b6f95f9772857141c70b 100644 --- a/deploy/df-wiki/templates/deployment.yaml +++ b/deploy/df-wiki/templates/deployment.yaml @@ -56,7 +56,9 @@ spec: mountPath: /etc/nginx/conf.d/ - name: structure-data-pvc mountPath: /structure-data - + - name: website-pvc + mountPath: /website + {{- with .Values.nodeSelector }} nodeSelector: @@ -79,3 +81,6 @@ spec: - name: structure-data-pvc persistentVolumeClaim: claimName: structure-data-pvc + - name: website-pvc + persistentVolumeClaim: + claimName: website-pvc diff --git a/deploy/df-wiki/templates/pvc-structure.yaml b/deploy/df-wiki/templates/pvc-structure.yaml index acbe7073e7a77902ee95ab2b60fcbede0353a6f4..29334d59b0efa7867b171bd746ac6a536615d267 100644 --- a/deploy/df-wiki/templates/pvc-structure.yaml +++ b/deploy/df-wiki/templates/pvc-structure.yaml @@ -9,6 +9,6 @@ spec: - ReadWriteMany resources: requests: - storage: 30Gi + storage: 35Gi storageClassName: isilon volumeMode: Filesystem \ No newline at end of file diff --git a/deploy/df-wiki/templates/pvc-website.yaml b/deploy/df-wiki/templates/pvc-website.yaml new file mode 100644 index 0000000000000000000000000000000000000000..ee06f21b7a4d30247da089bccdd9ae81bc5be921 --- /dev/null +++ b/deploy/df-wiki/templates/pvc-website.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: website-pvc + annotations: + "helm.sh/resource-policy": keep +spec: + accessModes: + - ReadWriteMany + resources: + requests: + storage: 1Gi + storageClassName: isilon + volumeMode: Filesystem \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index db81ba7dee9d57a906c9b44fd256bcc57aa1be44..7253b32debde86f4054e4bc8a765f1cf55c099b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,6 @@ "@observablehq/plot": "^0.6.13", "@pinia/nuxt": "^0.5.1", "d3": "^7.8.5", - "file-saver": "^2.0.5", "meilisearch": "^0.36.0", "mermaid": "^10.6.1", "papaparse": "^5.4.1", @@ -5704,10 +5703,6 @@ "node": "^10.12.0 || >=12.0.0" } }, - "node_modules/file-saver": { - "version": "2.0.5", - "license": "MIT" - }, "node_modules/file-uri-to-path": { "version": "1.0.0", "dev": true, diff --git a/package.json b/package.json index a45f22d73d4c9a0b78363f37736be7080d390062..d1dcd76fdcd518f015043bf684e1d0776ba82c8c 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,6 @@ "@observablehq/plot": "^0.6.13", "@pinia/nuxt": "^0.5.1", "d3": "^7.8.5", - "file-saver": "^2.0.5", "meilisearch": "^0.36.0", "mermaid": "^10.6.1", "papaparse": "^5.4.1", diff --git a/packages/df-wiki-cli/df_wiki_cli/meilisearch/__init__.py b/packages/df-wiki-cli/df_wiki_cli/meilisearch/__init__.py index b3ec461a9fc5d686aae9b8b60c4f63c03e7a87b2..4ab37da8982f27631c916cdcd21a98295fd92039 100644 --- a/packages/df-wiki-cli/df_wiki_cli/meilisearch/__init__.py +++ b/packages/df-wiki-cli/df_wiki_cli/meilisearch/__init__.py @@ -61,6 +61,7 @@ class StrucutreStatistics(BaseModel): iptm_ptm: NaFloat = Field(..., alias="iptm+ptm") pDockQ: Optional[NaFloat] plddts: Optional[NaFloat] + Foldseek_name: Optional[str] def update_refseq( @@ -193,7 +194,7 @@ def update_systems( ) print(pagination_settings_task) attr_task = index.update_filterable_attributes( - body=["title", "Sensor", "Activator", "Effector", "PFAM.AC"] + body=["title", "Sensor", "Activator", "Effector", "PFAM.AC", "PFAM.DE"] ) params = { "maxValuesPerFacet": 1000000, diff --git a/scripts/copy-structure-data.sh b/scripts/copy-structure-data.sh new file mode 100644 index 0000000000000000000000000000000000000000..424beb09227f423d91788d0f355e503f1b4e4132 --- /dev/null +++ b/scripts/copy-structure-data.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +find * -type d -exec sh -c 'for d in "$@"; do (cd "/usr/share/nginx/html/$d"; cp --archive --recursive --symbolic-link /structure-data/sanitized-dump/$d/* .) done' argv0 {} +