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 (325)
Showing
with 1597 additions and 242 deletions
...@@ -4,10 +4,15 @@ workflow: ...@@ -4,10 +4,15 @@ workflow:
when: never when: never
- when: always - when: always
# Functions that should be executed before the build script is run # Functions that should be executed before the build script is run
variables: variables:
HELM_VERSION: "3.9.3" HELM_VERSION: "3.13.3"
IMAGE_NAME: "df-wiki" IMAGE_NAME: "df-wiki"
# dev # dev
HOST_DEV: 'defense-finder.dev.pasteur.cloud' HOST_DEV: 'defense-finder.dev.pasteur.cloud'
...@@ -15,6 +20,8 @@ variables: ...@@ -15,6 +20,8 @@ variables:
# prod # prod
HOST_PROD: 'defense-finder.pasteur.cloud' HOST_PROD: 'defense-finder.pasteur.cloud'
MEILI_HOST_PROD: 'defense-finder-meilisearch.pasteur.cloud' MEILI_HOST_PROD: 'defense-finder-meilisearch.pasteur.cloud'
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
cache: cache:
...@@ -24,15 +31,18 @@ cache: ...@@ -24,15 +31,18 @@ cache:
stages: stages:
- delete-release - delete-release
- build-df-cli - build-df-cli
- lint
- get-data - get-data
- deploy-meilisearch - deploy-meilisearch
- update-meilisearch-indexes - update-meilisearch-indexes
- get-meili-key - get-meili-key
- build - build
# - build-wiki
- deploy - deploy
# - load-website
.docker-login: &docker-login .docker-login: &docker-login
- i=0; while [ "$i" -lt 12 ]; do docker info && break; sleep 5; i=$(( i + 1 )) ; done - i=0; while [ "$i" -lt 12 ]; do docker info && break; sleep 5; i=$(( i + 1 )) ; done
...@@ -43,9 +53,17 @@ stages: ...@@ -43,9 +53,17 @@ stages:
# Build df-wiki-cli package # Build df-wiki-cli package
.df-wiki-cli-run:
image: python:3.11-bullseye
cache: # Pip's cache doesn't store the python packages
paths: # https://pip.pypa.io/en/stable/topics/caching/
- .cache/pip
before_script:
- pip install df-wiki-cli --index-url https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.pasteur.fr/api/v4/projects/5222/packages/pypi/simple
build:df-wiki-cli: build:df-wiki-cli:
stage: build-df-cli
image: python:3.11-bullseye image: python:3.11-bullseye
stage: build-df-cli
before_script: before_script:
- cd packages/df-wiki-cli/ - cd packages/df-wiki-cli/
- pip install poetry - pip install poetry
...@@ -61,10 +79,8 @@ build:df-wiki-cli: ...@@ -61,10 +79,8 @@ build:df-wiki-cli:
- echo "Build done ..." - echo "Build done ..."
- poetry publish --repository gitlab --skip-existing - poetry publish --repository gitlab --skip-existing
- echo "Publishing done!" - echo "Publishing done!"
rules: when: manual
- changes: allow_failure: true
- packages/df-wiki-cli/**/*.{py, toml} # ... or whatever your file extension is
allow_failure: false
################ DEPLOY MEILISEARCH ################# ################ DEPLOY MEILISEARCH #################
.deploy:meilisearch: .deploy:meilisearch:
...@@ -88,14 +104,14 @@ build:df-wiki-cli: ...@@ -88,14 +104,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:
...@@ -163,15 +179,23 @@ delete-meili-helm-release:prod: ...@@ -163,15 +179,23 @@ delete-meili-helm-release:prod:
- helm delete -n ${NAMESPACE} ${CI_PROJECT_NAME}-${CI_ENVIRONMENT_NAME}-meilisearch - helm delete -n ${NAMESPACE} ${CI_PROJECT_NAME}-${CI_ENVIRONMENT_NAME}-meilisearch
# lint
lint:
extends: .df-wiki-cli-run
stage: lint
script:
- cd content/3.defense-systems
- find . -name '*.md' ! -name '0.index.md' | sort | xargs -I {} df-wiki-cli content lint --file {}
when: manual
# Update Meili search indexes # Update Meili search indexes
.update-meilisearch-index: .update-meilisearch-index:
extends: .df-wiki-cli-run
stage: update-meilisearch-indexes stage: update-meilisearch-indexes
image: python:3.11-bullseye
variables: variables:
MEILI_HOST: "http://localhost:7700" MEILI_HOST: "http://localhost:7700"
before_script:
- pip install df-wiki-cli --index-url https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.pasteur.fr/api/v4/projects/5222/packages/pypi/simple
script: script:
- > - >
df-wiki-cli df-wiki-cli
...@@ -187,8 +211,18 @@ delete-meili-helm-release:prod: ...@@ -187,8 +211,18 @@ delete-meili-helm-release:prod:
--host ${MEILI_HOST} --host ${MEILI_HOST}
--key ${MEILI_MASTER_KEY} --key ${MEILI_MASTER_KEY}
update update
--file data/all_predictions_statistics.csv --file data/all_predictions_statistics_clean.csv
--document structure --document structure
- >
df-wiki-cli
meilisearch
--host ${MEILI_HOST}
--key ${MEILI_MASTER_KEY}
update
--file data/list-systems.json
--document systems
allow_failure: false
update-meilisearch-index:dev: update-meilisearch-index:dev:
rules: rules:
...@@ -212,12 +246,10 @@ update-meilisearch-index:prod: ...@@ -212,12 +246,10 @@ update-meilisearch-index:prod:
############# get-meili-key ############### ############# get-meili-key ###############
.set-meili-env: .set-meili-env:
image: python:3.11-bullseye extends: .df-wiki-cli-run
stage: get-meili-key stage: get-meili-key
variables: variables:
MEILI_HOST: "http://localhost:7700" MEILI_HOST: "http://localhost:7700"
before_script:
- pip install df-wiki-cli --index-url https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.pasteur.fr/api/v4/projects/5222/packages/pypi/simple
script: script:
- > - >
df-wiki-cli df-wiki-cli
...@@ -229,6 +261,7 @@ update-meilisearch-index:prod: ...@@ -229,6 +261,7 @@ update-meilisearch-index:prod:
artifacts: artifacts:
reports: reports:
dotenv: build.env dotenv: build.env
allow_failure: false
set-meili-env:dev: set-meili-env:dev:
...@@ -249,28 +282,24 @@ set-meili-env:prod: ...@@ -249,28 +282,24 @@ set-meili-env:prod:
############################## ##############################
get-zotero: get-zotero:
image: python:3.11-bullseye extends: .df-wiki-cli-run
stage: get-data stage: get-data
before_script:
- pip install df-wiki-cli --index-url https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.pasteur.fr/api/v4/projects/5222/packages/pypi/simple
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"
get-pfam: # get-pfam:
image: python:3.11-bullseye # extends: .df-wiki-cli-run
stage: get-data # stage: get-data
before_script: # script:
- pip install df-wiki-cli --index-url https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.pasteur.fr/api/v4/projects/5222/packages/pypi/simple # - df-wiki-cli pfam --output public/pfam-a-hmm.csv
script: # artifacts:
- df-wiki-cli pfam --output public/pfam-a-hmm.csv # paths:
artifacts: # - public/pfam-a-hmm.csv
paths:
- public/pfam-a-hmm.csv
# rules: # rules:
# - if: $CI_COMMIT_BRANCH == "main" # - if: $CI_COMMIT_BRANCH == "main"
...@@ -303,9 +332,12 @@ build:dev:wiki: ...@@ -303,9 +332,12 @@ build:dev:wiki:
extends: .build extends: .build
needs: needs:
- set-meili-env:dev - set-meili-env:dev
- 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"
...@@ -315,13 +347,105 @@ build:prod:wiki: ...@@ -315,13 +347,105 @@ build:prod:wiki:
needs: needs:
- set-meili-env:prod - set-meili-env:prod
- get-zotero - get-zotero
- get-pfam # - get-pfam
variables: variables:
BASE_URL: /wiki/ BASE_URL: /wiki/
rules: rules:
- if: $CI_COMMIT_BRANCH == "main" - 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}')
# - 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'
################ DEPLOY ########################## ################ DEPLOY ##########################
.deploy: .deploy:
stage: deploy stage: deploy
...@@ -343,6 +467,15 @@ build:prod:wiki: ...@@ -343,6 +467,15 @@ build:prod:wiki:
--set env="${ENV:-development}" --set env="${ENV:-development}"
--values deploy/df-wiki/values.yaml --values deploy/df-wiki/values.yaml
--values deploy/df-wiki/values.${ENV:-development}.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 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: deploy:dev:
extends: .deploy extends: .deploy
...@@ -410,3 +543,5 @@ delete-helm-release:prod: ...@@ -410,3 +543,5 @@ delete-helm-release:prod:
script: script:
- echo "Removing $CI_PROJECT_NAME-$CI_ENVIRONMENT_NAME" - echo "Removing $CI_PROJECT_NAME-$CI_ENVIRONMENT_NAME"
- helm delete -n ${NAMESPACE} $CI_PROJECT_NAME-$CI_ENVIRONMENT_NAME - helm delete -n ${NAMESPACE} $CI_PROJECT_NAME-$CI_ENVIRONMENT_NAME
...@@ -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,11 +70,16 @@ ENV NUXT_PUBLIC_MEILI_API_KEY=${MEILI_API_KEY} ...@@ -60,11 +70,16 @@ 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 ###
FROM nginxinc/nginx-unprivileged:1.25 FROM nginx:1.25-bookworm
# RUN rm -rf /usr/share/nginx/html/* # RUN rm -rf /usr/share/nginx/html/*
COPY nginx.conf /etc/nginx/conf.d/default.conf RUN apt update -y && apt install rsync -y
COPY --from=generate /usr/src/app/.output/public /etc/nginx/html COPY nginx.conf /etc/nginx/nginx.conf
RUN chown nginx:nginx /usr/share/nginx/html
COPY --chown=nginx:nginx --from=generate /usr/src/app/.output/public /usr/share/nginx/html
# RUN chmod -R nginx:nginx /usr/share/nginx/html/
USER nginx
CMD ["nginx", "-g", "daemon off;"] CMD ["nginx", "-g", "daemon off;"]
\ No newline at end of file
<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 || items.length - itemsToDisplay === 1" :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 && items.length - itemsToDisplay > 1">
<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>
......
<script lang="ts" setup> <script lang="ts" setup>
import { useDisplay } from 'vuetify'
const { mobile } = useDisplay()
export interface Props { export interface Props {
fluid?: boolean fluid?: boolean
toc?: boolean toc?: boolean
edit?: boolean edit?: boolean
navDrawer?: boolean
title?: string
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
fluid: false, fluid: false,
toc: true, toc: true,
edit: true edit: true,
navDrawer: true,
title: null
}); });
const drawer = ref(true); const drawer = ref(true);
...@@ -21,26 +29,32 @@ function onScroll() { ...@@ -21,26 +29,32 @@ function onScroll() {
} }
else { density.value = "prominent" } else { density.value = "prominent" }
} }
</script> </script>
<template> <template>
<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> --> <v-card flat color="transparent" :min-width="mobile ? undefined : 900" :max-width="fluid ? undefined : 1500">
<EditGitlab v-if="edit" /> <v-card-text>
<NavPrevNext v-if="edit" /> <slot />
</v-card-text>
<EditGitlab 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" :title="title !== null ? title : undefined" :density="density"
<slot name="drawer" :drawer="drawer"> :drawer-enabled="navDrawer" />
<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 ?? []" />
<nav-back-to-top /> <nav-back-to-top />
</VApp> </VApp>
</template> </template>
......
...@@ -10,13 +10,18 @@ const props = withDefaults(defineProps<Props>(), { ...@@ -10,13 +10,18 @@ const props = withDefaults(defineProps<Props>(), {
// import { useCustomTheme } from '~/composables/useCustomTheme' // import { useCustomTheme } from '~/composables/useCustomTheme'
import { useDisplay, useTheme } from "vuetify"; import { useDisplay, useTheme } from "vuetify";
const { navigation } = useContent(); const { navigation, page } = useContent();
// const drawer = ref(true); // const drawer = ref(true);
// const computedNavigation = computed(() => { // const computedNavigation = computed(() => {
// return navigation.value // return navigation.value
// .filter(({ _path }) => { // .filter((item: { layout: string }) => {
// return _path !== "/refseq"; // if (item?.layout === "db") {
// console.log(item)
// return false
// }
// return true
// // return item?.layout !== "db"
// }) // })
// }); // });
......
...@@ -5,17 +5,20 @@ import { useDisplay, useTheme } from "vuetify"; ...@@ -5,17 +5,20 @@ import { useDisplay, useTheme } from "vuetify";
export interface Props { export interface Props {
density: 'prominent' | 'compact' density: 'prominent' | 'compact'
drawer: boolean drawer: boolean
drawerEnabled: boolean
title?: string
} }
const runtimeConfig = useRuntimeConfig(); const runtimeConfig = useRuntimeConfig();
const { navigation } = useContent();
const { mobile } = useDisplay(); const { mobile } = useDisplay();
const theme = useTheme(); 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,
title: "Knowledge database of all known defense systems"
}); });
const emit = defineEmits(['update:drawer']) const emit = defineEmits(['update:drawer'])
function toggleTheme() { function toggleTheme() {
...@@ -34,17 +37,10 @@ const sections = ref([ ...@@ -34,17 +37,10 @@ const sections = ref([
}, },
{ id: "wiki", label: "Wiki", to: '/', }, { id: "wiki", label: "Wiki", to: '/', },
{ id: "refseq", label: "RefSeq DB", to: '/refseq/' }, { id: "refseq", label: "RefSeq DB", to: '/refseq/' },
{ id: "structure", label: "Structures DB", to: '/predicted-structure/' }, { id: "structure", label: "Structures DB", to: '/structure/' },
{ id: "help", label: "Help", to: '/help/' }, { id: "help", label: "Help", to: '/help/defensefinder' },
]); ]);
const computedNavigation = computed(() => {
return navigation.value
.filter(({ _path }) => {
return _path !== "/refseq";
})
});
function toggleDrawer() { function toggleDrawer() {
emit('update:drawer', !props.drawer) emit('update:drawer', !props.drawer)
...@@ -52,14 +48,13 @@ function toggleDrawer() { ...@@ -52,14 +48,13 @@ 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" class="d-flex align-self-center"></v-app-bar-nav-icon>
<!-- <Logo height="45px" /> --> <!-- <Logo height="45px" /> -->
</template> </template>
<v-app-bar-title> <v-app-bar-title class="d-flex align-self-center py-0">
<span class="d-flex align-center"> <span class="">
Knowledge database of all known anti-phage systems {{ title }} </span>
</span>
</v-app-bar-title> </v-app-bar-title>
<template #append> <template #append>
<template v-if="!mobile"> <template v-if="!mobile">
......
...@@ -16,7 +16,7 @@ const props = defineProps<{ ...@@ -16,7 +16,7 @@ const props = defineProps<{
<Navigation :navigation="navItem.children" /> <Navigation :navigation="navItem.children" />
</v-list-group> </v-list-group>
<template v-else> <template v-else>
<v-list-item :title="navItem.title" :value="navItem.title" :to="navItem._path" <v-list-item :title="navItem?.title ?? 'no title'" :value="navItem.title" :to="navItem._path"
:prepend-icon="navItem?.icon ? navItem.icon : null" color="primary" exact nav> :prepend-icon="navItem?.icon ? navItem.icon : null" color="primary" exact nav>
</v-list-item> </v-list-item>
</template> </template>
......
...@@ -3,21 +3,21 @@ import { usePfamStore } from "@/stores/pfam"; ...@@ -3,21 +3,21 @@ import { usePfamStore } from "@/stores/pfam";
const { pfam: pfamStore } = usePfamStore(); const { pfam: pfamStore } = usePfamStore();
export interface Props { export interface Props {
pfamString: string | null; pfams: Record<string, any>[];
itemsToDisplay?: number; itemsToDisplay?: number;
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
pfamString: null, pfams: [],
itemsToDisplay: 2, itemsToDisplay: 2,
}); });
const pfams = computed(() => { // const pfams = computed(() => {
if (props.pfamString === null) { // if (props.pfamString === null) {
return []; // return [];
} else { // } else {
return props.pfamString.split(",").map((pfam) => pfam.trim()); // return props.pfamString.split(",").map((pfam) => pfam.trim());
} // }
}); // });
const show = ref(false); const show = ref(false);
const pfamBaseUrl = ref(new URL("https://www.ebi.ac.uk/interpro/entry/pfam/")); const pfamBaseUrl = ref(new URL("https://www.ebi.ac.uk/interpro/entry/pfam/"));
...@@ -31,11 +31,11 @@ function constructPfamUrl(pfam: string) { ...@@ -31,11 +31,11 @@ function constructPfamUrl(pfam: string) {
<v-col> <v-col>
<v-card flat color="transparent" density="compact" rounded="false"> <v-card flat color="transparent" density="compact" rounded="false">
<template v-for="(pfam, index) in pfams" :key="pfam"> <template v-for="(pfam, index) in pfams" :key="pfam">
<v-chip v-if="index < itemsToDisplay || itemsToDisplay < 0" :href="constructPfamUrl(pfam)" target="_blank" <v-chip v-if="index < itemsToDisplay || itemsToDisplay < 0" :href="constructPfamUrl(pfam.AC)" target="_blank"
color="info" class="mr-1 mb-1"> color="info" class="mr-1 mb-1">
{{ pfam }} {{ pfam.DE }}
<v-tooltip activator="parent" location="top">{{ <v-tooltip activator="parent" location="top">{{
pfamStore.get(pfam)?.DE ?? "none" pfam.AC
}}</v-tooltip></v-chip> }}</v-tooltip></v-chip>
<template v-if="index === itemsToDisplay"> <template v-if="index === itemsToDisplay">
...@@ -51,11 +51,11 @@ function constructPfamUrl(pfam: string) { ...@@ -51,11 +51,11 @@ function constructPfamUrl(pfam: string) {
</template> </template>
<template v-if="pfams.length > itemsToDisplay && show"> <template v-if="pfams.length > itemsToDisplay && show">
<template v-for="(pfam, index) in pfams" :key="pfam"> <template v-for="(pfam, index) in pfams" :key="pfam">
<v-chip v-if="index >= itemsToDisplay" :href="constructPfamUrl(pfam)" target="_blank" color="info" <v-chip v-if="index >= itemsToDisplay" :href="constructPfamUrl(pfam.AC)" target="_blank" color="info"
class="mr-1 mb-1"> class="mr-1 mb-1">
{{ pfam }} {{ pfam.DE }}
<v-tooltip activator="parent" location="top">{{ <v-tooltip activator="parent" location="top">{{
pfamStore.get(pfam)?.DE ?? "none" pfam.AC
}}</v-tooltip></v-chip> }}</v-tooltip></v-chip>
</template> </template>
</template> </template>
......
...@@ -148,7 +148,7 @@ export default { ...@@ -148,7 +148,7 @@ export default {
const options = { const options = {
...(method === "plot" && { ...(method === "plot" && {
marks: this.mark == null ? [] : [this.mark], marks: this.mark == null ? [] : [this.mark],
width: 688 // better default for VitePress // width: 688 // better default for VitePress
}), }),
...this.options, ...this.options,
className: "plot" className: "plot"
......
<script setup lang="ts">
// import type { FacetDistribution } from "meilisearch";
import { useCsvDownload } from "@/composables/useCsvDownload"
import { useSlots } from 'vue'
import { useDisplay } from "vuetify";
import { useThrottleFn } from '@vueuse/core'
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 {
type: 'facet' | 'operator' | 'value' | 'text'
value: string
title: string
count?: number
deletable: boolean
props: Record<string, any>
}
const props = withDefaults(defineProps<Props>(), {
title: '',
db: 'refseq',
columnsToDownload: undefined,
sortBy: () => [{ key: "type", order: "asc" }],
numericalFilters: undefined
});
const slots = useSlots()
const sortByRef = toRef(props.sortBy)
const facetsRef = toRef(props.facets)
const emit = defineEmits(["refresh:search"])
const { search: msSearch, result: msResult } = useMeiliSearch(props.db)
const search: Ref<string> = ref("");
const filterOrSearch: Ref<FilterItem[] | null> = ref(null)
const hitsPerPage: Ref<number> = ref(25)
const itemsPerPage: Ref<number[]> = ref([25, 50, 100])
const filterError: Ref<string | null> = ref(null)
const msFilter: Ref<string | undefined> = ref(undefined)
const page = ref(1)
let loading = ref(false)
const expanded = ref([])
const { height, mobile } = useDisplay();
const minTableHeight = ref(400)
const computedTableHeight = computed(() => {
const computedHeight = height.value - 350
return computedHeight > minTableHeight.value ? computedHeight : minTableHeight.value
})
const pendingDownloadData = ref(false)
const filterInputValues = computed(() => {
if (filterOrSearch.value != null) {
return filterOrSearch.value.filter(({ props }) => props.type !== 'text')
} 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({
facets: ["*"],
filter: [],
sort: ["type:asc"],
})
const paginationParams = computed(() => {
return { ...reactiveParams, page: toValue(page), hitsPerPage: toValue(hitsPerPage), limit: 500 }
})
const notPaginatedParams = computed(() => {
return { ...reactiveParams, limit: 500000 }
})
watch([paginationParams, msSortBy, page], ([newParams, newSort, newPage]) => {
searchOrFilter()
})
onMounted(async () => {
searchOrFilter()
emitRefreshRes()
})
const computedFilter = computed(() => {
return [toValue(msFilter), props.numericalFilters].filter(f => f !== undefined).join(" AND ")
})
watch(computedFilter, () => {
searchOrFilter()
emitRefreshRes()
})
const msError = computed(() => {
if (filterError.value?.type && filterError.value?.message) {
return filterError.value?.message
} else { return false }
})
const throttleSearch = useThrottleFn(async () => {
searchOrFilter()
emitRefreshRes()
}, 300)
async function searchOrFilter() {
// do something, it will be called at most 1 time per second
try {
loading.value = true
// const q = queryInputValue.value === null ? "" : queryInputValue.value
const q = search.value
await msSearch(q, { ...paginationParams.value, filter: toValue(computedFilter), sort: msSortBy.value })
} catch (error: any) {
filterError.value = error
console.log(error)
}
finally {
loading.value = false
}
}
function emitRefreshRes() {
console.log("emit refresh:search")
const q = search.value
emit("refresh:search", {
index: props.db,
query: q,
params: { ...notPaginatedParams.value, filter: toValue(computedFilter), sort: msSortBy.value }
})
}
function clearFilterOrSearch() {
filterOrSearch.value = null
// searchOrFilter()
// emitRefreshRes()
}
// watch(msFilter, async (fos) => {
// searchOrFilter()
// emitRefreshRes()
// search.value = ''
// })
const totalHits = computed(() => {
return toValue(msResult)?.totalHits ?? toValue(msResult)?.estimatedTotalHits ?? 0
})
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("-").slice(0, -1).join("-")
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()
emitRefreshRes()
})
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 toValue(facetsRef).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 = msResult.value?.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
}
}
}) : []
}
}
})
function selectItem(item) {
filterOrSearch.value = Array.isArray(filterOrSearch.value) ? [...filterOrSearch.value, item] : [item]
}
function deleteOneFilter(index: number) {
if (isFilter.value) {
filterOrSearch.value?.splice(index - 2, 2)
}
}
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),
})
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>
<slot name="numerical-filters" :search="throttleSearch"></slot>
</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">
<template #top>
<template v-if="mobile">
<v-toolbar> <v-badge :content="totalHits" color="primary" class="mx-2">
<v-btn prepend-icon="md:download" :loading="pendingDownloadData" variant="text" color="primary"
@click="downloadData()">{{
props.title }}
</v-btn>
</v-badge></v-toolbar>
<v-toolbar><v-text-field v-model="search" label="Search..." hide-details="auto"
prepend-inner-icon="mdi-magnify" single-line clearable class="mx-2"></v-text-field></v-toolbar>
<v-toolbar><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" class="mx-2"
@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 === deleteOneFilter(index)"></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>
</v-autocomplete></v-toolbar>
</template>
<template v-else>
<v-toolbar>
<v-badge :content="totalHits" color="primary" class="mr-3">
<v-btn prepend-icon="md:download" :loading="pendingDownloadData" variant="text" color="primary"
@click="downloadData()">{{
props.title }}
</v-btn>
</v-badge>
<v-spacer></v-spacer>
<v-card variant="flat" color="transparent" :min-width="400" class="mx-2" :rounded="false">
<v-text-field v-model="search" label="Search..." hide-details="auto"
prepend-inner-icon="mdi-magnify" single-line clearable></v-text-field>
</v-card>
<v-card variant="flat" color="transparent" :min-width="500" class="mx-2" :rounded="false">
<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 === deleteOneFilter(index)"></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>
</v-autocomplete>
</v-card>
</v-toolbar>
</template>
</template>
<template v-for="(slot, index) of Object.keys(slots)" :key="index" v-slot:[slot]="data">
<slot :name="slot" v-bind="data"></slot>
</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;
...@@ -8,16 +9,21 @@ export interface Props { ...@@ -8,16 +9,21 @@ export interface Props {
enumerate?: boolean; enumerate?: boolean;
title?: string; title?: string;
abstract?: string; abstract?: string;
isRelevant?: boolean;
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
enumerate: true, enumerate: true,
divider: false, divider: false,
isRelevant: false,
}); });
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 articleTitle = computed(() => { const articleTitle = computed(() => {
return props?.title ?? article?.value?.title ?? props.doi; return props?.title ?? article?.value?.title ?? props.doi;
}); });
...@@ -26,42 +32,67 @@ const articleAbstract = computed(() => { ...@@ -26,42 +32,67 @@ 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="`ref-${props.doi}`" :target="article?.target" density="compact" color="transparent"
class="px-1"> class="px-1">
<template #prepend v-if="!mobile && enumerate"> <template v-if="!mobile" #prepend>
<v-avatar color="primary" size="small" density="compact" variant="tonal"> <v-icon icon="md:star" :color="props.isRelevant ? 'info' : 'transparent'"></v-icon>
{{ props?.index ?? "#" }}
</v-avatar>
</template> </template>
<!-- <template #append v-if="!mobile"> <!-- <template v-if="!mobile" #append>
<v-avatar> <v-btn v-if="articleAbstract" size="x-small" variant="plain"
<v-icon>{{ article?.prependIcon }}</v-icon> :append-icon="show ? 'mdi-chevron-up' : 'mdi-chevron-down'" class="px-0"
</v-avatar> @click.stop.prevent="show = !show">Abstract</v-btn>
</template> --> </template> -->
<v-card flat color="transparent" density="compact" class="my-0"> <v-card flat color="transparent" density="compact" class="my-0 article-ref">
<v-card-item density="compact" :class="mobile ? 'px-0 py-1' : 'py-1'"> <v-card-item density="compact" class="pa-0">
<v-card-title><span class="text-subtitle-1 font-weight-bold">{{ <v-toolbar class="py-0 d-flex align-start article-toolbar" color="transparent" :height="20">
articleTitle <v-toolbar-title class="font-weight-bold ml-0">{{ articleTitle }}</v-toolbar-title>
}}</span></v-card-title>
<v-card-subtitle> <v-btn v-if="articleAbstract" size="x-small" variant="plain" color="primary"
:append-icon="show ? 'mdi-chevron-up' : 'mdi-chevron-down'" class="px-1 align-center"
@click.stop.prevent="show = !show">Abstract</v-btn>
</v-toolbar>
<!-- <v-card-title class="py-0"><span class="font-weight-bold">{{
articleTitle
}}</span></v-card-title> -->
<v-card-subtitle class="py-0">
{{ article?.subtitle ?? "no authors" }}</v-card-subtitle> {{ article?.subtitle ?? "no authors" }}</v-card-subtitle>
<v-card-subtitle> <v-card-subtitle class="py-0">
{{ article?.containerTitle ?? "no containerTitle" }} ({{ {{ article?.containerTitle ?? "no containerTitle" }} ({{
article?.year article?.year
}})</v-card-subtitle> }})</v-card-subtitle>
</v-card-item> </v-card-item>
<v-card-item v-if="articleAbstract" density="compact" :class="mobile ? 'px-0' : 'py-1'"> <v-card-item class="pa-0">
<v-btn size="x-small" variant="outlined" :append-icon="show ? 'mdi-chevron-up' : 'mdi-chevron-down'" <v-expand-transition>
@click.stop.prevent="show = !show">Abstract</v-btn> <v-card v-show="show" flat color="transparent">
<v-card-text class="px-0">
{{ articleAbstract }}
</v-card-text>
</v-card>
</v-expand-transition>
</v-card-item> </v-card-item>
<v-expand-transition>
<v-card v-show="show" flat color="transparent">
<v-card-text>
{{ articleAbstract }}
</v-card-text>
</v-card>
</v-expand-transition>
</v-card> </v-card>
</v-list-item> </v-list-item>
<v-divider v-if="props.divider" inset></v-divider> <v-divider v-if="props.divider" inset></v-divider>
</template> </template>
\ No newline at end of file <style scoped>
.article-ref .v-card-item * {
line-height: 1rem !important;
}
.article-ref .v-card-item .v-card-subtitle,
.article-ref .v-card-item button.v-btn span {
font-size: 0.8rem !important;
}
.article-toolbar .v-toolbar-title {
font-size: 0.9rem !important;
}
.article-toolbar div.v-toolbar__content * {
align-items: flex-start;
}
</style>
\ No newline at end of file
<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
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
<script setup lang="ts"> <script setup lang="ts">
const slot = useSlots() const slot = useSlots()
const el = ref(null) const el: Ref<HTMLElement | null> = ref(null)
const rendered = ref(false) const rendered = ref(false)
async function render() { async function render() {
......
<script setup lang="ts"> <script setup lang="ts">
import { withTrailingSlash, withLeadingSlash, joinURL } from 'ufo' import { withTrailingSlash, withLeadingSlash, joinURL } from 'ufo'
import { useRuntimeConfig, computed } from '#imports' import { useRuntimeConfig, computed } from '#imports'
import * as d3 from "d3";
import * as Plot from "@observablehq/plot";
import PlotFigure from "~/components/PlotFigure";
import { useDisplay } from "vuetify";
export interface Props { export interface Props {
height?: number height?: number
dataUrls?: string[] dataUrls?: string[]
dataUrl?: string dataUrl?: string
uniq?: boolean
} }
const { mobile } = useDisplay()
// const selectedPdb = ref('') // const selectedPdb = ref('')
const refinedDataUrls = computed(() => { const refinedDataUrls = computed(() => {
...@@ -24,10 +34,15 @@ const refinedDataUrls = computed(() => { ...@@ -24,10 +34,15 @@ const refinedDataUrls = computed(() => {
if (props?.dataUrls && props?.dataUrls?.length > 0) { if (props?.dataUrls && props?.dataUrls?.length > 0) {
urls = [...props.dataUrls.map((dataUrl) => { urls = [...props.dataUrls.map((dataUrl) => {
return refinedUrl(dataUrl) return refinedUrl(dataUrl)
// return dataUrl
})] })]
} }
if (props?.dataUrl) { if (props?.dataUrl) {
urls = [...urls, refinedUrl(props.dataUrl)] urls = [
...urls,
// props.dataUrl
refinedUrl(props.dataUrl)
]
} }
return urls return urls
...@@ -37,29 +52,30 @@ const refinedDataUrls = computed(() => { ...@@ -37,29 +52,30 @@ const refinedDataUrls = computed(() => {
// const selectedPdb = ref(refinedDataUrls.value?.length > 0 ? refinedDataUrls.value[0] : null) // const selectedPdb = ref(refinedDataUrls.value?.length > 0 ? refinedDataUrls.value[0] : null)
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
height: 600, height: 600,
uniq: false
}) })
const { width, height } = useDisplay() const { width, height } = useDisplay()
const maxWidth = ref(1300) const maxWidth = ref(1500)
const dialog = ref(false) const dialog = ref(false)
// const show = ref(false) // const show = ref(false)
const computedWidth = computed(() => { const computedWidth = computed(() => {
if (width > maxWidth) return maxWidth // if (toValue(width) > toValue(maxWidth)) return toValue(maxWidth) / 1.5
return width return toValue(width) / 1.5
}) })
const computedHeight = computed(() => { const computedHeight = computed(() => {
return height.value - 250 return height.value - 250
}) })
const paeError: Ref<string | null> = ref(null)
function closeStructure() { function closeStructure() {
selectedPdb.value = null selectedPdb.value = null
dialog.value = false dialog.value = false
} }
...@@ -67,7 +83,7 @@ useHead({ ...@@ -67,7 +83,7 @@ useHead({
link: [ link: [
{ {
rel: 'stylesheet', rel: 'stylesheet',
href: 'https://www.ebi.ac.uk/pdbe/pdb-component-library/css/pdbe-molstar-3.1.2.css' href: 'https://www.ebi.ac.uk/pdbe/pdb-component-library/css/pdbe-molstar-3.1.3.css'
}, },
], ],
script: [ script: [
...@@ -80,7 +96,7 @@ useHead({ ...@@ -80,7 +96,7 @@ useHead({
}, },
{ {
type: "text/javascript", type: "text/javascript",
src: "https://www.ebi.ac.uk/pdbe/pdb-component-library/js/pdbe-molstar-component-3.1.2.js", src: "https://www.ebi.ac.uk/pdbe/pdb-component-library/js/pdbe-molstar-component-3.1.3.js",
// tagPosition: 'bodyClose' // tagPosition: 'bodyClose'
} }
] ]
...@@ -88,65 +104,172 @@ useHead({ ...@@ -88,65 +104,172 @@ useHead({
const pdbeMolstarComponent = ref(null) const pdbeMolstarComponent = ref(null)
// const selectedPdb = ref("/wiki/avs/AVAST_I,AVAST_I__Avs1A,0,V-plddts_85.07081.pdb") // const selectedPdb = ref("/wiki/avs/AVAST_I,AVAST_I__Avs1A,0,V-plddts_85.07081.pdb")
const selectedPdb = ref(null) 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
})
watch(selectedPdb, (newSelectedPdb, prevSelectPdb) => {
viewPdb(newSelectedPdb)
structureToDownload.value = newSelectedPdb
})
watch(selectedPdb, (selectedPdb, prevSelectPdb) => { onMounted(() => {
if (selectedPdb !== null) { 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 dialog.value = true
const format = toValue(pdbPath)?.split(".").slice(-1)[0]?.toLowerCase() ?? "pdb"
moleculeFormat.value = format
if (pdbeMolstarComponent.value?.viewerInstance) { if (pdbeMolstarComponent.value?.viewerInstance) {
const viewerInstance = pdbeMolstarComponent.value.viewerInstance const viewerInstance = pdbeMolstarComponent.value.viewerInstance
const customData = { url: pdbPath, format: format, binary: false }
viewerInstance.visual.update({ customData })
}
}
}
// show.value = true
const customData = { url: selectedPdb, format: "pdb", binary: false }
viewerInstance.visual.update({ customData })
}
function setSelectedPdbToFirst() {
const urls = toValue(refinedDataUrls)
if (urls.length >= 1) {
selectedPdb.value = urls[0]
} }
}) }
// const moleculeFormat = computed(() => {
// return toValue(selectedPdb)?.split(".")?.[-1]?.toLowerCase() ?? "pdb"
// })
const moleculeFormat: Ref<string> = ref("pdb")
</script> </script>
<template> <template>
<v-row><v-col><v-select v-model="selectedPdb" label="Select PDB" :items="refinedDataUrls" <template v-if="uniq">
hide-details="auto"></v-select></v-col></v-row> <v-row>
<v-btn size="x-small" variant="text" icon="md:visibility" @click="setSelectedPdbToFirst()"></v-btn>
<v-row justify="center"> <v-btn :disabled="!structureToDownload" size="x-small" variant="text" icon="md:download" class="ml-1"
<v-dialog v-model="dialog" transition="dialog-bottom-transition" fullscreen :scrim="false"> :href="structureToDownload"></v-btn>
<v-card flat :rounded="false"> </v-row>
<v-toolbar> </template>
<v-toolbar-title>Structures</v-toolbar-title> <v-row v-else>
<v-select v-model="selectedPdb" label="Select PDB" :items="refinedDataUrls" <v-col>
hide-details="auto"></v-select> <span class="d-flex flex-wrap align-center justify-center">
<v-spacer></v-spacer> <v-select v-model="selectedPdb" label="Select PDB" :items="refinedDataUrls" hide-details="auto">
</v-select>
<v-btn :disabled="!selectedPdb" icon="md:download" :href="selectedPdb"></v-btn> </span>
<v-btn icon @click="closeStructure"> </v-col>
<v-icon>mdi-close</v-icon>
</v-btn>
</v-toolbar>
<v-card-text>
<v-sheet v-if="selectedPdb"
class="d-flex align-center justify-center flex-wrap text-center mx-auto px-4 my-3"
:height="computedHeight" :max-width="1300" :width="computedWidth" position="relative">
<pdbe-molstar ref="pdbeMolstarComponent" hide-controls :custom-data-url="selectedPdb"
custom-data-format="pdb"></pdbe-molstar>
</v-sheet>
</v-card-text>
</v-card>
</v-dialog>
</v-row> </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="true" hide-controls="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
: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);">
&nbsp;</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);">
&nbsp;</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);">
&nbsp;</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);">
&nbsp;</div>
</template>
<v-list-item-title>
Very low (pLDDT &lt; 50) </v-list-item-title>
</v-list-item>
</v-list>
</v-card>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-dialog>
</template> </template>
<style scoped> <style>
.msp-plugin .msp-plugin-content { /* .msp-plugin .msp-plugin-content {
color: black !important; color: black !important;
} */
div.msp-plugin-content.msp-layout-expanded {
z-index: 5 !important
}
.legendColor {
height: 16px;
width: 16px;
} }
</style> </style>
\ No newline at end of file
...@@ -11,10 +11,8 @@ const dois = computed(() => { ...@@ -11,10 +11,8 @@ const dois = computed(() => {
</script> </script>
<template> <template>
( (<template v-for="doi, index in dois" :key="doi">
<template v-for="doi, index in dois" :key="doi">
<RefArticle :doi="doi"></RefArticle> <RefArticle :doi="doi"></RefArticle>
<span v-if="index < dois.length - 1">, </span> <span v-if="index < dois.length - 1">, </span>
</template> </template>)
)
</template> </template>
\ No newline at end of file
<script setup lang="ts"> <script setup lang="ts">
import { useTheme } from "vuetify";
const theme = useTheme();
export interface Props { export interface Props {
doi: string; doi: string;
} }
...@@ -7,7 +9,17 @@ const props = withDefaults(defineProps<Props>(), {}); ...@@ -7,7 +9,17 @@ const props = withDefaults(defineProps<Props>(), {});
const { article } = useFetchArticle(props.doi); const { article } = useFetchArticle(props.doi);
</script> </script>
<template> <template>
<v-chip v-if="article" variant="text" :href="`#ref-${props.doi}`" class="pa-0 text-caption font-italic">{{ <!-- <v-chip v-if="article" variant="text" :href="`#ref-${props.doi}`" class="pa-0 font-italic">{{
article?.author[0]?.family ?? "test" }} et al, article?.author[0]?.family ?? "test" }} et al,
{{ article?.year }}</v-chip> {{ article?.year }}</v-chip> -->
</template> <NuxtLink v-if="article" :href="`#ref-${props.doi}`"
\ No newline at end of file :class="theme.global.current.value.dark ? 'text-grey-lighten-1' : 'text-grey-darken-2'" class="pa-0 ">{{
article?.author[0]?.family ?? "test" }} et al,
{{ article?.year }}</NuxtLink>
</template>
<style scoped>
.ref-link {
/* color: rgba(var(--v-theme-tertiary)); */
color: grey-darken-3;
}
</style>
\ No newline at end of file
<script setup lang="ts">
import * as Plot from "@observablehq/plot";
import PlotFigure from "~/components/PlotFigure";
import { useDisplay } from "vuetify";
import type { SortItem } from "@/components/ServerDbTable.vue"
import { ServerDbTable } from "#components"
import { useSerialize } from "@/composables/useSerialize";
import { useRasterize } from "@/composables/useRasterize";
import { useDownloadBlob } from '@/composables/useDownloadBlob';
import type { ComponentPublicInstance } from 'vue'
const sortBy: Ref<SortItem[]> = ref([{ key: 'type', order: "asc" }])
const itemValue = ref("id");
const { width } = useDisplay();
const scaleTransform: Ref<string[]> = ref([])
const { serialize } = useSerialize()
const { rasterize } = useRasterize()
const { download } = useDownloadBlob()
const facets = ref([
"replicon",
"type",
"subtype",
"Superkingdom",
"phylum",
"order",
"family",
"genus",
"species",
])
const availableTaxo: Ref<string[]> = ref([
"species",
"genus",
"family",
"order",
"phylum",
"Superkingdom"
]);
const scaleTypes = ref<string[]>(['linear', 'sqrt', 'log', 'symlog'])
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",
sortable: false
}
])
const fullWidth = computed(() => {
return layoutPlot.value === 'fullwidth'
})
const computedHeaders = computed(() => {
return [...headers.value, ...availableTaxo.value.map(taxo => {
return {
title: capitalize(taxo),
key: taxo
}
})]
})
const { result: msResult } = useMeiliSearch('refseq')
const computedWidth = computed(() => {
const currentWidth = fullWidth.value ? width.value : width.value / 2
return Math.max(currentWidth, 550);
});
const allHits: Ref<Record<string, any> | undefined> = ref(undefined)
// onMounted(async () => {
// console.log("on mounted get all hits")
// const params = {
// facets: ["*"],
// filter: undefined,
// sort: ["type:asc"],
// limit: 500000
// }
// getAllHits({ index: "refseq", params, query: "" })
// })
const pendingAllHits = ref(false)
async function getAllHits(params: { index: string, params: Record<string, any>, query: string }) {
console.log(params.index)
if (params.index === 'refseq') {
console.log("get all hits in function ")
console.log(params)
pendingAllHits.value = true
try {
const { data, error } = await useAsyncMeiliSearch(params)
allHits.value = data.value
console.log(error.value)
} finally {
pendingAllHits.value = false
}
}
}
const plotHeight = computed(() => {
return computedWidth.value / 3;
// return 500
});
const defaultDataTableServerProps = ref({
showExpand: false
})
const dataTableServerProps = computed(() => {
return {
...defaultDataTableServerProps.value,
headers: computedHeaders.value,
itemValue: itemValue.value
}
})
const defaultBarPlotOptions = computed(() => {
return {
x: { label: null, tickRotate: 45 },
y: { grid: true, clamp: true },
// height: plotHeight.value + 100,
}
})
// system distri
const computedSystemDistribution = computed(() => {
if (toValue(msResult)?.facetDistribution?.type) {
return Object.entries(toValue(msResult).facetDistribution.type)
.map(([key, value]) => {
return {
type: key,
count: value
}
}).sort()
} else { return [] }
})
const computedDistriSystemOptions = computed(() => {
return {
...defaultBarPlotOptions.value,
marginBottom: 100,
y: { ...defaultBarPlotOptions.value.y, type: toValue(scaleType), label: "Count" },
x: { ...defaultBarPlotOptions.value.x, label: "Systems" },
width: computedWidth.value,
marks: [
// Plot.frame(),
Plot.barY(
toValue(computedSystemDistribution),
{
y: "count", x: 'type', tip: true,
sort: { x: "-y" },
},
),
],
};
});
// Taxo distri
const computedTaxonomyDistribution = computed(() => {
if (toValue(msResult)?.facetDistribution?.[selectedTaxoRank.value]) {
return Object.entries(toValue(msResult).facetDistribution[selectedTaxoRank.value]).map(([key, value]) => {
return {
[selectedTaxoRank.value]: key,
count: value
}
}).sort()
} else { return [] }
})
const computedDistriTaxoOptions = computed(() => {
return {
...defaultBarPlotOptions.value,
marginBottom: 100,
x: { ...defaultBarPlotOptions.value.x, label: selectedTaxoRank.value },
y: { ...defaultBarPlotOptions.value.y, type: toValue(scaleType), label: "Count" },
width: computedWidth.value,
marks: [
Plot.barY(
toValue(computedTaxonomyDistribution),
{
y: "count",
x: selectedTaxoRank.value,
tip: true,
// fill: "#6750a4",
sort: { x: "-y" },
}
),
],
};
});
function capitalize(name: string) {
const [first, ...rest] = name
return first.toUpperCase() + rest.join('').toLowerCase();
}
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()
}
})
}
const systemPanel: Ref<string[]> = ref([])
const layoutPlot: Ref<string> = ref("grid")
const binPlotOptions = ref({
marginLeft: 150,
marginBottom: 200,
padding: 0,
grid: true,
x: { tickRotate: 90, tip: true, label: "Systems" },
// y: { tickFormat: 's' },
color: { scheme: "turbo", legend: true },
})
const binPlotDataOptions = computed(() => {
const toValueAllHits = toValue(allHits)
return toValueAllHits?.hits?.length > 0 ? {
...binPlotOptions.value,
width: width.value,
color: {
...binPlotOptions.value.color,
type: scaleType.value,
tickFormat: '~s',
ticks: scaleType.value === 'symlog' ? 3 : 5,
// width: 350
},
// fy: { domain: groupSortDomain.value },
marks: [
Plot.cell(toValueAllHits?.hits ?? [], Plot.group({ fill: "count" }, { x: "type", y: selectedTaxoRank.value, tip: true, inset: 0.5, sort: { y: "fill" } })),
]
} : null
})
const scaleType = ref("linear")
const systemsDistributionPlot = ref<ComponentPublicInstance | null>(null)
const taxonomicDistributionPlot = ref<ComponentPublicInstance | null>(null)
const heatmapPlot = ref<ComponentPublicInstance | null>(null)
function downloadSvg(component: ComponentPublicInstance | null, filename: string) {
const blob = toValue(serialize(toValue(component)))
if (blob !== undefined) {
download(blob, filename)
}
}
async function downloadPng(component: ComponentPublicInstance | null, filename: string) {
const blob = await rasterize(toValue(component), filename)?.then((blob) => {
download(blob, filename)
})
}
</script>
<template>
<v-card flat class="mb-2" color="transparent">
<v-card color="transparent" flat>
<v-expansion-panels v-model="systemPanel" class="my-2" density="compact" multiple>
<v-expansion-panel elevation="3" value="barplot">
<v-expansion-panel-title color="grey-lighten-4"><v-icon color="primary"
class="mr-2">mdi-chart-bar</v-icon>Systems - Taxonomic</v-expansion-panel-title>
<v-expansion-panel-text>
<v-card flat color="transparent">
<v-toolbar flat color="transparent" density="compact">
<v-btn-toggle v-model="layoutPlot" density="compact" rounded="false" variant="text"
color="primary" mandatory class="mx-2">
<v-btn icon="md:grid_view" value="grid"></v-btn>
<v-btn icon="md:view_agenda" value="fullwidth"></v-btn>
</v-btn-toggle>
<v-select v-model="scaleType" class="mx-2" density="compact" :items="scaleTypes"
label="Scale Type" hide-details="auto"></v-select>
<v-select v-model="selectedTaxoRank" :items="availableTaxo" density="compact"
label="Select taxonomic rank" hide-details="auto" class="mx-2"></v-select>
</v-toolbar>
<v-row align="start">
<v-col :cols="fullWidth ? 12 : 6">
<v-card variant="flat">
<v-toolbar density="compact" color="transparent">
<v-spacer></v-spacer>
<v-menu>
<template v-slot:activator="{ props }">
<v-btn color="primary" prepend-icon="md:download" v-bind="props">
export
</v-btn>
</template>
<v-list>
<v-list-item value="svg">
<v-list-item-title
@click="downloadSvg(systemsDistributionPlot, 'df-systems-distribution.svg')">to
svg</v-list-item-title>
</v-list-item>
<v-list-item value="png">
<v-list-item-title
@click="downloadPng(systemsDistributionPlot, 'df-systems-distribution.png')">to
png</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-toolbar>
<PlotFigure ref="systemsDistributionPlot"
:options="unref(computedDistriSystemOptions)" defer></PlotFigure>
</v-card>
</v-col>
<v-col :cols="fullWidth ? 12 : 6">
<v-card variant="flat">
<v-toolbar density="compact" color="transparent">
<v-spacer></v-spacer>
<v-menu>
<template v-slot:activator="{ props }">
<v-btn color="primary" prepend-icon="md:download" v-bind="props">
export
</v-btn>
</template>
<v-list>
<v-list-item value="svg">
<v-list-item-title
@click="downloadSvg(taxonomicDistributionPlot, 'df-taxonomic-distribution.svg')">to
svg</v-list-item-title>
</v-list-item>
<v-list-item value="png">
<v-list-item-title
@click="downloadPng(taxonomicDistributionPlot, 'df-taxonomic-distribution.png')">to
png</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-toolbar>
<PlotFigure ref="taxonomicDistributionPlot" defer
:options="unref(computedDistriTaxoOptions)"></PlotFigure>
</v-card>
</v-col>
</v-row>
</v-card>
</v-expansion-panel-text>
</v-expansion-panel>
<v-expansion-panel elevation="3" value="matrix">
<v-expansion-panel-title color="grey-lighten-4">
<v-icon v-if="pendingAllHits === false" color="primary" class="mr-2">mdi-data-matrix
</v-icon>
<v-progress-circular v-else indeterminate color="primary" :size="22" class="mr-2"
:width="3"></v-progress-circular>
Heatmap </v-expansion-panel-title>
<v-expansion-panel-text>
<v-card v-if="pendingAllHits === false" flat color="transparent">
<v-toolbar flat color="transparent" density="compact">
<v-select v-model="scaleType" class="mx-2" density="compact" :items="scaleTypes"
label="Scale Type" hide-details="auto"></v-select>
<v-select v-model="selectedTaxoRank" :items="availableTaxo" density="compact"
label="Select taxonomic rank" hide-details="auto" class="mx-2"></v-select>
</v-toolbar>
<v-card v-if="toValue(binPlotDataOptions) !== null" variant="flat">
<v-toolbar density="compact" color="transparent">
<v-spacer></v-spacer>
<v-menu>
<template v-slot:activator="{ props }">
<v-btn color="primary" prepend-icon="md:download" v-bind="props">
export
</v-btn>
</template>
<v-list>
<v-list-item value="svg">
<v-list-item-title
@click="downloadSvg(heatmapPlot, 'df-heatmap-systems-taxonomy.svg')">to
svg</v-list-item-title>
</v-list-item>
<v-list-item value="png">
<v-list-item-title
@click="downloadPng(heatmapPlot, 'df-heatmap-systems-taxonomy.png')">to
png</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-toolbar>
<PlotFigure ref="heatmapPlot" :options="unref(binPlotDataOptions)" defer>
</PlotFigure>
</v-card>
</v-card>
<v-card v-else flat color="transparent">
<v-skeleton-loader type="card" :loading="pendingAllHits" height="400"></v-skeleton-loader>
</v-card>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
<ServerDbTable title="RefSeq" db="refseq" :sortBy="sortBy" :facets="facets"
:data-table-server-props="dataTableServerProps" @refresh:search="(params) => getAllHits(params)">
<template #[`item.accession_in_sys`]="{ item }">
<CollapsibleChips :items="namesToAccessionChips(item.accession_in_sys)">
</CollapsibleChips>
</template>
</ServerDbTable>
</v-card>
</v-card>
</template>
\ No newline at end of file
...@@ -15,11 +15,11 @@ const computedDois = computed(() => { ...@@ -15,11 +15,11 @@ const computedDois = computed(() => {
</script> </script>
<template> <template>
<div v-if="computedDois?.length > 0"> <div v-if="computedDois?.length > 0">
<ProseH2 id="relevant-abstracts">Relevant abstracts</ProseH2> <ProseH2 id="references">References</ProseH2>
<v-list density="compact" bg-color="transparent"> <v-list density="compact" bg-color="transparent">
<ArticleDoi v-for="(item, index) in computedDois" :key="item.doi" :index="index + 1" :doi="item.doi" <ArticleDoi v-for="(item, index) in computedDois" :key="item.doi" :index="index + 1" :doi="item.doi"
:title="item?.title" :divider="item.divider" :abstract="item?.abstract" /> :title="item?.title" :divider="item.divider" :abstract="item?.abstract" :is-relevant="item?.isRelevant ?? false" />
</v-list> </v-list>
</div> </div>
</template> </template>
\ No newline at end of file
<script setup lang="ts">
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");
const facets: Ref<string[]> = ref(["System", "subtype", "gene_name", "completed", "prediction_type",])
const headers: Ref<Object[]> = ref([
{ title: 'Structure', key: 'structure', sortable: false, removable: false },
{ title: "System", key: "System", removable: false },
{ title: "Gene name", key: "gene_name", removable: false },
{ title: "Subtype", key: "subtype", removable: false },
// { title: "pdb file", key: "pdb" },
// { title: "fasta", key: "fasta_file" },
{ title: "Proteins in structure", key: 'proteins_in_the_prediction', sortable: false, removable: true },
{ title: "System genes", key: "system_genes", sortable: false, removable: true },
{ title: "Sys id", key: "nb_sys", removable: true },
{ title: "Completed", key: "completed", removable: true },
{ title: "Prediction type", key: "prediction_type", removable: true },
{ title: "N genes in sys", key: "system_number_of_genes", removable: true },
{ title: "pLDDT", key: "plddts", removable: true },
{ title: "iptm+ptm", key: "iptm+ptm", removable: true },
{ title: "pDockQ", key: "pDockQ", removable: true },
// { 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
})
const dataTableServerProps = computed(() => {
return {
...toValue(defaultDataTableServerProps),
headers: toValue(headers),
itemValue: toValue(itemValue)
}
})
function namesToCollapsibleChips(names: string[], systemDir: string, file: string | null = null) {
if (file === null) {
return names.filter((it) => it !== "").map(it => ({ title: it.split("__")[1] }))
} else {
return names.filter((it) => it !== "").map(it => ({ title: it.split("__")[1], href: `/wiki/${systemDir}/${file}` }))
}
}
function pdbNameToCif(pdbPath: string) {
const cifPath = pdbPath.split(".").slice(0, -1).join(".")
return `${cifPath}.cif`
}
function toSystemName(rawName: string) {
// Does it work if it's a list of system genes ?
// split on __ for systeme_vgenes
return rawName.split("__")[0].toLocaleLowerCase()
}
const plddtDistribution = computed(() => {
if (toValue(msResult)?.facetDistribution?.plddts) {
return Object.entries(toValue(msResult).facetDistribution.plddts).map(([key, value]) => { })
}
})
</script>
<template>
<ServerDbTable title="Predicted Structures" db="structure" :sortBy="sortBy" :facets="facets"
: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 }">
<CollapsibleChips
:items="namesToCollapsibleChips(item.proteins_in_the_prediction, item.System_name_ok, item.fasta_file)">
</CollapsibleChips>
</template>
<template #[`item.system_genes`]="{ item }">
<CollapsibleChips :items="namesToCollapsibleChips(item.system_genes, item.System_name_ok)"></CollapsibleChips>
</template>
<template #[`item.structure`]="{ item }">
<MolstarPdbePlugin v-if="item?.pdb && item.pdb !== 'na'"
:data-urls="[`/${item.System_name_ok}/${pdbNameToCif(item.pdb)}`]" uniq>
</MolstarPdbePlugin>
<!-- <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>
<v-icon v-else color="warning" icon="md:dangerous"></v-icon>
</template>
</ServerDbTable>
</template>
\ No newline at end of file
<script setup lang="ts">
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", "PFAM.DE"])
const headers: Ref<Object[]> = ref([
{ title: "System", key: "title", removable: false },
{ title: "Article", key: "doi", removable: false },
{ title: "Sensor", key: "Sensor", removable: false },
{ title: "Activator", key: "Activator", removable: false },
{ title: "Effector", key: "Effector", removable: false },
{ title: "PFAM", key: "PFAM", removable: false },
{ title: "Contributors", key: "contributors", removable: false },
])
const { search: msSearch, result: msResult } = useMeiliSearch('systems')
const defaultDataTableServerProps = ref({
showExpand: false
})
const dataTableServerProps = computed(() => {
return {
...toValue(defaultDataTableServerProps),
headers: toValue(headers),
itemValue: toValue(itemValue)
}
})
const columnsToDownload = ref(['title', 'doi', 'Sensor', 'Activator', 'Effector', 'PFAM', 'contributors',])
</script>
<template>
<ServerDbTable title="List Systems" db="systems" :sortBy="sortBy" :facets="facets"
:data-table-server-props="dataTableServerProps" :columns-to-download="columnsToDownload">
<template #[`item.title`]="{ item }">
<v-chip color="info" link :to="`/defense-systems/${item.title.toLowerCase()}`">{{
item.title
}}</v-chip>
</template>
<template #[`item.doi`]="{ item }">
<ArticleDoi v-if="item?.doi" :doi="item.doi" :abstract="item?.abstract" :divider="false" :enumerate="false" />
</template>
<template #[`item.PFAM`]="{ item }">
<pfam-chips v-if="item?.PFAM" :pfams="item.PFAM"></pfam-chips>
</template>
</ServerDbTable>
</template>
\ No newline at end of file