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:
when: never
- when: always
# 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'
......@@ -15,6 +20,8 @@ variables:
# prod
HOST_PROD: 'defense-finder.pasteur.cloud'
MEILI_HOST_PROD: 'defense-finder-meilisearch.pasteur.cloud'
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
cache:
......@@ -24,15 +31,18 @@ cache:
stages:
- delete-release
- build-df-cli
- lint
- get-data
- deploy-meilisearch
- update-meilisearch-indexes
- get-meili-key
- build
# - 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
......@@ -43,9 +53,17 @@ stages:
# 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:
stage: build-df-cli
image: python:3.11-bullseye
stage: build-df-cli
before_script:
- cd packages/df-wiki-cli/
- pip install poetry
......@@ -61,10 +79,8 @@ build:df-wiki-cli:
- echo "Build done ..."
- poetry publish --repository gitlab --skip-existing
- echo "Publishing done!"
rules:
- changes:
- packages/df-wiki-cli/**/*.{py, toml} # ... or whatever your file extension is
allow_failure: false
when: manual
allow_failure: true
################ DEPLOY MEILISEARCH #################
.deploy:meilisearch:
......@@ -88,14 +104,14 @@ build:df-wiki-cli:
--values deploy/meilisearch/values.yaml
--values deploy/meilisearch/values.${ENV:-development}.yaml
# wait for it to start
- MEILI_POD=$(kubectl -n=${KUBE_NAMESPACE} get po -l app.kubernetes.io\/instance=${CI_PROJECT_NAME}-${CI_ENVIRONMENT_NAME}-meilisearch,app.kubernetes.io\/name=meilisearch --output jsonpath='{.items[0].metadata.name}')
- |
until kubectl -n=${KUBE_NAMESPACE} wait --for=condition=ready pod ${MEILI_POD} --timeout=1s
do
date
sleep 1
kubectl -n=${KUBE_NAMESPACE} get po
done
# - MEILI_POD=$(kubectl -n=${KUBE_NAMESPACE} get po -l app.kubernetes.io\/instance=${CI_PROJECT_NAME}-${CI_ENVIRONMENT_NAME}-meilisearch,app.kubernetes.io\/name=meilisearch --output jsonpath='{.items[0].metadata.name}')
# - |
# until kubectl -n=${KUBE_NAMESPACE} wait --for=condition=ready pod ${MEILI_POD} --timeout=1s
# do
# date
# sleep 1
# kubectl -n=${KUBE_NAMESPACE} get po
# done
deploy:meilisearch:dev:
......@@ -163,15 +179,23 @@ delete-meili-helm-release:prod:
- 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-meilisearch-index:
extends: .df-wiki-cli-run
stage: update-meilisearch-indexes
image: python:3.11-bullseye
variables:
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:
- >
df-wiki-cli
......@@ -187,8 +211,18 @@ delete-meili-helm-release:prod:
--host ${MEILI_HOST}
--key ${MEILI_MASTER_KEY}
update
--file data/all_predictions_statistics.csv
--file data/all_predictions_statistics_clean.csv
--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:
rules:
......@@ -212,12 +246,10 @@ update-meilisearch-index:prod:
############# get-meili-key ###############
.set-meili-env:
image: python:3.11-bullseye
extends: .df-wiki-cli-run
stage: get-meili-key
variables:
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:
- >
df-wiki-cli
......@@ -229,6 +261,7 @@ update-meilisearch-index:prod:
artifacts:
reports:
dotenv: build.env
allow_failure: false
set-meili-env:dev:
......@@ -249,28 +282,24 @@ set-meili-env:prod:
##############################
get-zotero:
image: python:3.11-bullseye
extends: .df-wiki-cli-run
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:
- df-wiki-cli articles --key ${ZOTERO_API_KEY} --output public/articles.json
- df-wiki-cli articles --key ${ZOTERO_API_KEY} --output content/_data/_articles.json
artifacts:
paths:
- public/articles.json
- content/_data/_articles.json
rules:
- if: $CI_COMMIT_BRANCH == "main"
get-pfam:
image: python:3.11-bullseye
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:
- df-wiki-cli pfam --output public/pfam-a-hmm.csv
artifacts:
paths:
- public/pfam-a-hmm.csv
# get-pfam:
# extends: .df-wiki-cli-run
# stage: get-data
# script:
# - df-wiki-cli pfam --output public/pfam-a-hmm.csv
# artifacts:
# paths:
# - public/pfam-a-hmm.csv
# rules:
# - if: $CI_COMMIT_BRANCH == "main"
......@@ -303,9 +332,12 @@ build:dev:wiki:
extends: .build
needs:
- set-meili-env:dev
- get-pfam
# - get-pfam
variables:
BASE_URL: /wiki/
before_script:
- *docker-login
# - "sed -i 's/MEILISEARCH_API_KEY/${$MEILI_API_KEY}/g' nuxt.config.ts"
rules:
- if: $CI_COMMIT_BRANCH != "main"
......@@ -315,13 +347,105 @@ build:prod:wiki:
needs:
- set-meili-env:prod
- get-zotero
- get-pfam
# - get-pfam
variables:
BASE_URL: /wiki/
rules:
- 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:
stage: deploy
......@@ -343,6 +467,15 @@ build:prod: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 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
......@@ -410,3 +543,5 @@ delete-helm-release:prod:
script:
- echo "Removing $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
ARG MEILI_API_KEY=api_key
ENV NUXT_APP_BASE_URL=${BASE_URL}
# nuxt module
ENV NUXT_PUBLIC_MEILISEARCH_CLIENT_HOST_URL=${MEILI_HOST}
ENV NUXT_PUBLIC_MEILISEARCH_CLIENT_SEARCH_API_KEY=${MEILI_API_KEY}
ENV NUXT_PUBLIC_MEILI_HOST=${MEILI_HOST}
ENV NUXT_PUBLIC_MEILI_API_KEY=${MEILI_API_KEY}
......@@ -24,6 +28,7 @@ ENV NUXT_PUBLIC_MEILI_API_KEY=${MEILI_API_KEY}
WORKDIR /usr/src/app
COPY --from=install /usr/src/app ./
COPY . /usr/src/app
EXPOSE 3000 24678 4000
CMD ["npm", "run", "dev"]
......@@ -51,8 +56,13 @@ ARG BASE_URL=/
ARG MEILI_HOST=http://localhost:7700
ARG MEILI_API_KEY
ENV NODE_OPTIONS=--max_old_space_size=8192
ENV NODE_OPTIONS=--max_old_space_size=12288
ENV NUXT_APP_BASE_URL=${BASE_URL}
# nuxt module
ENV NUXT_PUBLIC_MEILISEARCH_CLIENT_HOST_URL=${MEILI_HOST}
ENV NUXT_PUBLIC_MEILISEARCH_CLIENT_SEARCH_API_KEY=${MEILI_API_KEY}
ENV NUXT_PUBLIC_MEILI_HOST=${MEILI_HOST}
ENV NUXT_PUBLIC_MEILI_API_KEY=${MEILI_API_KEY}
......@@ -60,11 +70,16 @@ ENV NUXT_PUBLIC_MEILI_API_KEY=${MEILI_API_KEY}
WORKDIR /usr/src/app
COPY --from=install /usr/src/app ./
COPY . /usr/src/app
RUN npm run generate
### STAGE: NGINX ###
FROM nginxinc/nginx-unprivileged:1.25
FROM nginx:1.25-bookworm
# RUN rm -rf /usr/share/nginx/html/*
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=generate /usr/src/app/.output/public /etc/nginx/html
RUN apt update -y && apt install rsync -y
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;"]
\ No newline at end of file
<script setup lang="ts">
interface item {
title: string;
href?: string | undefined
}
export interface Props {
accessions: string[];
items: item[];
itemsToDisplay?: number;
baseUrl: string;
}
const props = withDefaults(defineProps<Props>(), {
pfamString: null,
itemsToDisplay: 2,
items: () => [],
itemsToDisplay: 1,
});
// const accessions = computed(() => {
// if (props.accessionString === null) {
// return [];
// } else {
// return props.accessionString.split(",").map((acc) => acc.trim());
// }
// });
const show = ref(false);
function constructUrl(accession: string) {
return new URL(accession, props.baseUrl).toString();
}
</script>
<template>
<!-- class="d-inline-flex justify-start align-center" -->
<span v-if="show" class="d-flex flex-wrap align-center justify-start">
<template v-if="accessions.length > itemsToDisplay">
<template v-for="(acc) in accessions" :key="acc">
<v-chip :href="constructUrl(acc)" target="_blank" color="info" class="mr-1 my-1 align-self-center"
size="small">
{{ acc }}
<template v-if="items.length > itemsToDisplay">
<template v-for="item in items" :key="item.title">
<v-chip :href="item?.href" :target="item?.href === undefined ? item?.href : '_blank'" color="info"
class="mr-1 my-1 align-self-center" size="small">
{{ item.title }}
</v-chip>
</template>
</template>
<v-btn v-if="itemsToDisplay < accessions.length" variant="text" :icon="'mdi-chevron-up'"
@click="show = !show"></v-btn>
<v-btn v-if="itemsToDisplay < items.length" variant="text" :icon="'mdi-chevron-up'" @click="show = !show"></v-btn>
</span>
<span v-else class="d-flex flex-wrap align-center justify-start">
<template v-for="(acc, index) in accessions" :key="acc">
<v-chip v-if="index < itemsToDisplay || itemsToDisplay < 0" :href="constructUrl(acc)" target="_blank"
color="info" class="mr-1 my-1 align-self-center" size="small">
{{ acc }}
<template v-for="(item, index) in items" :key="item.title">
<v-chip v-if="index < itemsToDisplay || itemsToDisplay < 0 || items.length - itemsToDisplay === 1" :href="item?.href"
:target="item?.href === undefined ? item?.href : '_blank'" color="info" class="mr-1 my-1 align-self-center"
size="small">
{{ item.title }}
</v-chip>
<template v-if="index === itemsToDisplay">
<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"
@click="show = !show">
(+{{ accessions.length - itemsToDisplay }} others)
(+{{ items.length - itemsToDisplay }} others)
</v-chip>
<v-btn v-if="itemsToDisplay < accessions.length && !show" variant="text" :icon="'mdi-chevron-down'"
<v-btn v-if="itemsToDisplay < items.length && !show" variant="text" :icon="'mdi-chevron-down'"
@click="show = !show"></v-btn>
</template>
</template>
......
<script lang="ts" setup>
import { useDisplay } from 'vuetify'
const { mobile } = useDisplay()
export interface Props {
fluid?: boolean
toc?: boolean
edit?: boolean
navDrawer?: boolean
title?: string
}
const props = withDefaults(defineProps<Props>(), {
fluid: false,
toc: true,
edit: true
edit: true,
navDrawer: true,
title: null
});
const drawer = ref(true);
......@@ -21,26 +29,32 @@ function onScroll() {
}
else { density.value = "prominent" }
}
</script>
<template>
<VApp>
<v-main style="min-height: 300px">
<v-container v-scroll="onScroll" :fluid="fluid">
<slot />
<!-- </v-card-text>
</v-card> -->
<EditGitlab v-if="edit" />
<NavPrevNext v-if="edit" />
<v-row justify="center">
<v-col cols="auto">
<v-card flat color="transparent" :min-width="mobile ? undefined : 900" :max-width="fluid ? undefined : 1500">
<v-card-text>
<slot />
</v-card-text>
<EditGitlab v-if="edit" />
<NavPrevNext v-if="edit" />
</v-card>
</v-col>
</v-row>
</v-container>
<!-- <Footer></Footer> -->
</v-main>
<NavNavbar v-model:drawer="drawer" :density="density" />
<slot name="drawer" :drawer="drawer">
<NavNavbar v-model:drawer="drawer" :title="title !== null ? title : undefined" :density="density"
:drawer-enabled="navDrawer" />
<slot v-if="navDrawer" name="drawer" :drawer="drawer">
<NavDrawer :drawer="drawer" />
</slot>
<NavTableOfContent v-if="toc" :links="page.body.toc.links" />
<NavTableOfContent v-if="toc" :links="page.body.toc.links ?? []" />
<nav-back-to-top />
</VApp>
</template>
......
......@@ -10,13 +10,18 @@ const props = withDefaults(defineProps<Props>(), {
// import { useCustomTheme } from '~/composables/useCustomTheme'
import { useDisplay, useTheme } from "vuetify";
const { navigation } = useContent();
const { navigation, page } = useContent();
// const drawer = ref(true);
// const computedNavigation = computed(() => {
// return navigation.value
// .filter(({ _path }) => {
// return _path !== "/refseq";
// .filter((item: { layout: string }) => {
// if (item?.layout === "db") {
// console.log(item)
// return false
// }
// return true
// // return item?.layout !== "db"
// })
// });
......
......@@ -5,17 +5,20 @@ import { useDisplay, useTheme } from "vuetify";
export interface Props {
density: 'prominent' | 'compact'
drawer: boolean
drawerEnabled: boolean
title?: string
}
const runtimeConfig = useRuntimeConfig();
const { navigation } = useContent();
const { mobile } = useDisplay();
const theme = useTheme();
const switchTheme = ref(false)
const props = withDefaults(defineProps<Props>(), {
density: "prominent",
drawer: true
drawer: true,
drawerEnabled: true,
title: "Knowledge database of all known defense systems"
});
const emit = defineEmits(['update:drawer'])
function toggleTheme() {
......@@ -34,17 +37,10 @@ const sections = ref([
},
{ id: "wiki", label: "Wiki", to: '/', },
{ id: "refseq", label: "RefSeq DB", to: '/refseq/' },
{ id: "structure", label: "Structures DB", to: '/predicted-structure/' },
{ id: "help", label: "Help", to: '/help/' },
{ id: "structure", label: "Structures DB", to: '/structure/' },
{ id: "help", label: "Help", to: '/help/defensefinder' },
]);
const computedNavigation = computed(() => {
return navigation.value
.filter(({ _path }) => {
return _path !== "/refseq";
})
});
function toggleDrawer() {
emit('update:drawer', !props.drawer)
......@@ -52,14 +48,13 @@ function toggleDrawer() {
</script>
<template>
<v-app-bar :elevation="0" border name="app-bar" :density="density" color="background">
<template #prepend>
<v-app-bar-nav-icon @click.stop="toggleDrawer"></v-app-bar-nav-icon>
<template v-if="drawerEnabled" #prepend>
<v-app-bar-nav-icon @click.stop="toggleDrawer" class="d-flex align-self-center"></v-app-bar-nav-icon>
<!-- <Logo height="45px" /> -->
</template>
<v-app-bar-title>
<span class="d-flex align-center">
Knowledge database of all known anti-phage systems
</span>
<v-app-bar-title class="d-flex align-self-center py-0">
<span class="">
{{ title }} </span>
</v-app-bar-title>
<template #append>
<template v-if="!mobile">
......
......@@ -16,7 +16,7 @@ const props = defineProps<{
<Navigation :navigation="navItem.children" />
</v-list-group>
<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>
</v-list-item>
</template>
......
......@@ -3,21 +3,21 @@ import { usePfamStore } from "@/stores/pfam";
const { pfam: pfamStore } = usePfamStore();
export interface Props {
pfamString: string | null;
pfams: Record<string, any>[];
itemsToDisplay?: number;
}
const props = withDefaults(defineProps<Props>(), {
pfamString: null,
pfams: [],
itemsToDisplay: 2,
});
const pfams = computed(() => {
if (props.pfamString === null) {
return [];
} else {
return props.pfamString.split(",").map((pfam) => pfam.trim());
}
});
// const pfams = computed(() => {
// if (props.pfamString === null) {
// return [];
// } else {
// return props.pfamString.split(",").map((pfam) => pfam.trim());
// }
// });
const show = ref(false);
const pfamBaseUrl = ref(new URL("https://www.ebi.ac.uk/interpro/entry/pfam/"));
......@@ -31,11 +31,11 @@ function constructPfamUrl(pfam: string) {
<v-col>
<v-card flat color="transparent" density="compact" rounded="false">
<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">
{{ pfam }}
{{ pfam.DE }}
<v-tooltip activator="parent" location="top">{{
pfamStore.get(pfam)?.DE ?? "none"
pfam.AC
}}</v-tooltip></v-chip>
<template v-if="index === itemsToDisplay">
......@@ -51,11 +51,11 @@ function constructPfamUrl(pfam: string) {
</template>
<template v-if="pfams.length > itemsToDisplay && show">
<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">
{{ pfam }}
{{ pfam.DE }}
<v-tooltip activator="parent" location="top">{{
pfamStore.get(pfam)?.DE ?? "none"
pfam.AC
}}</v-tooltip></v-chip>
</template>
</template>
......
......@@ -148,7 +148,7 @@ export default {
const options = {
...(method === "plot" && {
marks: this.mark == null ? [] : [this.mark],
width: 688 // better default for VitePress
// width: 688 // better default for VitePress
}),
...this.options,
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">
import { useDisplay } from "vuetify";
import { useArticlesStore } from '@/stores/articles'
export interface Props {
index?: number;
......@@ -8,16 +9,21 @@ export interface Props {
enumerate?: boolean;
title?: string;
abstract?: string;
isRelevant?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
enumerate: true,
divider: false,
isRelevant: false,
});
const { article } = useFetchArticle(props.doi);
const { mobile } = useDisplay();
const show = ref(false);
const articleTitle = computed(() => {
return props?.title ?? article?.value?.title ?? props.doi;
});
......@@ -26,42 +32,67 @@ const articleAbstract = computed(() => {
});
</script>
<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">
<template #prepend v-if="!mobile && enumerate">
<v-avatar color="primary" size="small" density="compact" variant="tonal">
{{ props?.index ?? "#" }}
</v-avatar>
<template v-if="!mobile" #prepend>
<v-icon icon="md:star" :color="props.isRelevant ? 'info' : 'transparent'"></v-icon>
</template>
<!-- <template #append v-if="!mobile">
<v-avatar>
<v-icon>{{ article?.prependIcon }}</v-icon>
</v-avatar>
</template> -->
<v-card flat color="transparent" density="compact" class="my-0">
<v-card-item density="compact" :class="mobile ? 'px-0 py-1' : 'py-1'">
<v-card-title><span class="text-subtitle-1 font-weight-bold">{{
articleTitle
}}</span></v-card-title>
<v-card-subtitle>
<!-- <template v-if="!mobile" #append>
<v-btn v-if="articleAbstract" size="x-small" variant="plain"
:append-icon="show ? 'mdi-chevron-up' : 'mdi-chevron-down'" class="px-0"
@click.stop.prevent="show = !show">Abstract</v-btn>
</template> -->
<v-card flat color="transparent" density="compact" class="my-0 article-ref">
<v-card-item density="compact" class="pa-0">
<v-toolbar class="py-0 d-flex align-start article-toolbar" color="transparent" :height="20">
<v-toolbar-title class="font-weight-bold ml-0">{{ articleTitle }}</v-toolbar-title>
<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>
<v-card-subtitle>
<v-card-subtitle class="py-0">
{{ article?.containerTitle ?? "no containerTitle" }} ({{
article?.year
}})</v-card-subtitle>
</v-card-item>
<v-card-item v-if="articleAbstract" density="compact" :class="mobile ? 'px-0' : 'py-1'">
<v-btn size="x-small" variant="outlined" :append-icon="show ? 'mdi-chevron-up' : 'mdi-chevron-down'"
@click.stop.prevent="show = !show">Abstract</v-btn>
<v-card-item class="pa-0">
<v-expand-transition>
<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-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-list-item>
<v-divider v-if="props.divider" inset></v-divider>
</template>
\ No newline at end of file
</template>
<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 @@
<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() {
......
<script setup lang="ts">
import { withTrailingSlash, withLeadingSlash, joinURL } from 'ufo'
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 {
height?: number
dataUrls?: string[]
dataUrl?: string
uniq?: boolean
}
const { mobile } = useDisplay()
// const selectedPdb = ref('')
const refinedDataUrls = computed(() => {
......@@ -24,10 +34,15 @@ const refinedDataUrls = computed(() => {
if (props?.dataUrls && props?.dataUrls?.length > 0) {
urls = [...props.dataUrls.map((dataUrl) => {
return refinedUrl(dataUrl)
// return dataUrl
})]
}
if (props?.dataUrl) {
urls = [...urls, refinedUrl(props.dataUrl)]
urls = [
...urls,
// props.dataUrl
refinedUrl(props.dataUrl)
]
}
return urls
......@@ -37,29 +52,30 @@ const refinedDataUrls = computed(() => {
// const selectedPdb = ref(refinedDataUrls.value?.length > 0 ? refinedDataUrls.value[0] : null)
const props = withDefaults(defineProps<Props>(), {
height: 600,
uniq: false
})
const { width, height } = useDisplay()
const maxWidth = ref(1300)
const maxWidth = ref(1500)
const dialog = ref(false)
// const show = ref(false)
const computedWidth = computed(() => {
if (width > maxWidth) return maxWidth
return width
// if (toValue(width) > toValue(maxWidth)) return toValue(maxWidth) / 1.5
return toValue(width) / 1.5
})
const computedHeight = computed(() => {
return height.value - 250
})
const paeError: Ref<string | null> = ref(null)
function closeStructure() {
selectedPdb.value = null
dialog.value = false
}
......@@ -67,7 +83,7 @@ useHead({
link: [
{
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: [
......@@ -80,7 +96,7 @@ useHead({
},
{
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'
}
]
......@@ -88,65 +104,172 @@ useHead({
const pdbeMolstarComponent = ref(null)
// 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) => {
if (selectedPdb !== null) {
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) {
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>
<template>
<v-row><v-col><v-select v-model="selectedPdb" label="Select PDB" :items="refinedDataUrls"
hide-details="auto"></v-select></v-col></v-row>
<v-row justify="center">
<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-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>
<template v-if="uniq">
<v-row>
<v-btn size="x-small" variant="text" icon="md:visibility" @click="setSelectedPdbToFirst()"></v-btn>
<v-btn :disabled="!structureToDownload" size="x-small" variant="text" icon="md:download" class="ml-1"
:href="structureToDownload"></v-btn>
</v-row>
</template>
<v-row v-else>
<v-col>
<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-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>
<style scoped>
.msp-plugin .msp-plugin-content {
<style>
/* .msp-plugin .msp-plugin-content {
color: black !important;
} */
div.msp-plugin-content.msp-layout-expanded {
z-index: 5 !important
}
.legendColor {
height: 16px;
width: 16px;
}
</style>
\ No newline at end of file
......@@ -11,10 +11,8 @@ const dois = computed(() => {
</script>
<template>
(
<template v-for="doi, index in dois" :key="doi">
(<template v-for="doi, index in dois" :key="doi">
<RefArticle :doi="doi"></RefArticle>
<span v-if="index < dois.length - 1">, </span>
</template>
)
</template>)
</template>
\ No newline at end of file
<script setup lang="ts">
import { useTheme } from "vuetify";
const theme = useTheme();
export interface Props {
doi: string;
}
......@@ -7,7 +9,17 @@ const props = withDefaults(defineProps<Props>(), {});
const { article } = useFetchArticle(props.doi);
</script>
<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?.year }}</v-chip>
</template>
\ No newline at end of file
{{ article?.year }}</v-chip> -->
<NuxtLink v-if="article" :href="`#ref-${props.doi}`"
: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(() => {
</script>
<template>
<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">
<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>
</div>
</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