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 (380)
Showing
with 1673 additions and 245 deletions
data/ filter=lfs diff=lfs merge=lfs -text
data/**/*.csv filter=lfs diff=lfs merge=lfs -text
data/**/*.tsv filter=lfs diff=lfs merge=lfs -text
......@@ -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,9 +79,7 @@ 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
when: manual
allow_failure: true
################ 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,16 +179,39 @@ delete-meili-helm-release:prod:
- helm delete -n ${NAMESPACE} ${CI_PROJECT_NAME}-${CI_ENVIRONMENT_NAME}-meilisearch
# Update Meili search indexes
# 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 + generate some file that goes to meilisearch
.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:
# - rm data/list-systems.json
- >
df-wiki-cli
content systems
--dir content/3.defense-systems/
--pfam public/pfam-a-hmm.csv
--output data/list-systems.json
- df-wiki-cli meilisearch delete-all-documents refseq
- >
df-wiki-cli
meilisearch
--host ${MEILI_HOST}
--key ${MEILI_MASTER_KEY}
index-update
refseq
sys_id
- >
df-wiki-cli
meilisearch
......@@ -187,8 +226,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 +261,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 +276,7 @@ update-meilisearch-index:prod:
artifacts:
reports:
dotenv: build.env
allow_failure: false
set-meili-env:dev:
......@@ -249,28 +297,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 +347,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 +362,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,11 +482,20 @@ 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
rules:
- if: $CI_COMMIT_BRANCH != "main"
- if: $CI_COMMIT_BRANCH == "dev" || $CI_COMMIT_BRANCH == "refactor-facet-autocomplete"
needs:
- "build:dev:wiki"
when: manual
......@@ -410,3 +558,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">
import { filter } from '@observablehq/plot'
export interface FilterItem {
type: 'facet' | 'innerOperator' | 'outerOperator' | 'value'
value: string
title: string
count?: number
deletable: boolean
props: Record<string, any>
}
export interface FacetItem {
title: string
value: string
type: "facet"
icon?: string
count?: number
}
export interface OperatorItem {
title: string,
type: 'operator'
}
export interface FacetCategory {
title: string
type: "subheader"
}
export interface FacetDivider {
type: "divider"
}
export type FacetInputItem = FacetItem | FacetCategory | FacetDivider
export interface Props {
db: string
modelValue: FilterItem[] | undefined
facets: MaybeRef<FacetInputItem[] | undefined>
facetDistribution: MaybeRef<Record<string, Record<string, number>> | undefined>
isValidFilters?: MaybeRef<boolean>
autocompleteProps?: Record<string, any>
}
const emit = defineEmits(['update:modelValue', "meiliFilters"])
const filterId = ref<number>(0)
const props = withDefaults(defineProps<Props>(), {
modelValue: undefined,
autocompleteProps: () => {
return {
chips: true,
clearable: true,
multiple: true,
"auto-select-first": true,
"return-object": true,
"prepend-inner-icon": "md:filter_alt",
"hide-details": "auto",
"item-value": "value", "item-title": "title",
label: "Filter results...",
"single-line": true,
}
},
isValidFilters: false
});
// const { result: msResult } = useMeiliSearch(props.db)
const isAutocompleteFocused = ref<boolean>(false)
// const facetDistribution: Ref<Record<string, Record<string, number>>> = useState('facetDistribution')
const autocompleteProps = computed(() => {
return {
...props.autocompleteProps,
items: toValue(autocompleteItems)
}
})
const filterStep = computed(() => {
const toValFilterItems = toValue(props.modelValue)
if (toValFilterItems !== undefined) {
return toValFilterItems.length % 4
}
})
const innerOperatorItems = ref<FilterItem[]>([
{
type: "innerOperator", value: '=', title: "is", deletable: false, props: {
type: "innerOperator", deletable: false
}
}, {
type: "innerOperator", value: '!=', title: "is not", deletable: false, props: {
type: "innerOperator",
deletable: false
}
},
])
const outerOperatorItems = ref<FilterItem[]>([
{
type: "outerOperator", value: 'AND', title: "AND", deletable: false, props: {
type: "outerOperator", deletable: false
}
}, {
type: "outerOperator", value: 'OR', title: "OR", deletable: false, props: {
type: "outerOperator",
deletable: false
}
},
])
const autocompleteItems = computed(() => {
const toValFilterItems = toValue(props.modelValue)
// const index = toValFilterItems?.length ?? 0
if (filterStep.value === undefined || filterStep.value === 0) {
filterId.value++
return toValue(props.facets)?.map(facetItem => {
switch (facetItem.type) {
case "facet":
return {
type: "facet",
value: `${facetItem.value}-${filterId.value}`,
title: facetItem.title,
deletable: false,
icon: facetItem?.icon,
count: facetItem?.count,
props: {
deletable: false,
type: "facet"
}
}
case "subheader":
return {
type: "subheader",
title: facetItem.title,
deletable: false,
props: {
type: "subheader"
}
}
case "divider":
return { type: "divider" }
default:
break;
}
})
}
if (filterStep.value === 1) {
filterId.value++
return innerOperatorItems.value.map(it => { return { ...it, value: `${it.value}-${filterId.value}`, } })
}
if (filterStep.value === 2) {
filterId.value++
// get the facet value
if (Array.isArray(toValFilterItems)) {
const { type, value } = toValFilterItems?.slice(-2, -1)[0]
const sanitizedValue = value.split("-")[0]
const facetDistri = toValue(props.facetDistribution)
return facetDistri?.[sanitizedValue] ? Object.entries(facetDistri[sanitizedValue]).map(([key, val]) => {
return {
type: "value", value: `${key}-${filterId.value}`, title: key, count: val, deletable: true, props: {
type: "value", count: val, deletable: true
}
}
}) : []
}
}
if (filterStep.value === 3) {
filterId.value++
return outerOperatorItems.value.map(it => { return { ...it, value: `${it.value}-${filterId.value}`, } })
}
})
const hasFacetDistribution = computed(() => {
const toValFacetDistribution = toValue(props.facetDistribution)
return toValFacetDistribution !== undefined && Object.keys(toValFacetDistribution).length > 0
})
function updateAutocompleteFocused(isFocused: boolean) {
isAutocompleteFocused.value = isFocused
}
function emitUpdateModelValue(filters: MaybeRef<FilterItem[] | undefined>) {
emit('update:modelValue', toValue(filters))
}
function clearFilters() {
emitUpdateModelValue(undefined)
}
function deleteOneFilter(index: number) {
const toValFilterItems = toValue(props.modelValue)
// check if the next item is an outeroperator
const nextFilterItem = toValFilterItems?.slice(index + 1, index + 2)
if (index + 1 === toValFilterItems?.length && toValFilterItems?.length >= 7) {
// need to remove the previous outer operator
toValFilterItems?.splice(index - 3, 4)
}
else if (nextFilterItem?.length === 1 && nextFilterItem[0].type === 'outerOperator') {
toValFilterItems?.splice(index - 2, 4)
}
else {
toValFilterItems?.splice(index - 2, 3)
}
emitUpdateModelValue(toValFilterItems)
}
function isItemFilter(type: string | undefined) {
return type === "facet" || type === "innerOperator" || type === "outerOperator" || type === "value"
}
const hint = ref<string>('All <span class="font-weight-bold">OR</span> in a row are grouped together. Example: <span class="font-weight-bold">brex OR avs AND Archea</span> &rArr; <span class="font-weight-bold">(brex OR avs) AND Archea</span>')
</script>
<template>
<v-autocomplete :model-value="props.modelValue" @click:clear="clearFilters" v-bind="autocompleteProps" :hint="hint"
persistent-hint @update:focused="updateAutocompleteFocused" @update:modelValue="emitUpdateModelValue"
:loading="!hasFacetDistribution" :disabled="!hasFacetDistribution">
<template #message="{ message }">
<span v-html="message"></span>
</template>
<template #item="{ props, item }">
<v-list-item v-if="isItemFilter(item?.raw?.type)" v-bind="{ ...props, active: false }" :title="item.title"
:prepend-icon="item?.raw?.icon ? item.raw.icon : undefined"
:subtitle="item.raw?.count ? item.raw.count : ''" :value="props.value">
</v-list-item>
<v-divider v-if="item.raw.type === 'divider'"></v-divider>
<v-list-subheader v-if="item.raw.type === 'subheader'" :title="item.raw.title"></v-list-subheader>
</template>
<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 #append>
<v-btn variant="text" icon="md:filter_alt" @click="emitUpdateModelValue(props.modelValue)"
:disabled="!isValidFilters"></v-btn>
</template> -->
</v-autocomplete>
</template>
\ 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() {
......@@ -29,22 +32,15 @@ watchEffect(() => {
const sections = ref([
{
id: "webservice",
label: "webservice",
label: "Webservice",
href: runtimeConfig.public.defenseFinderWebservice,
},
{ 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 { useMeiliFilters } from "@/composables/useMeiliFilters"
import { useSlots } from 'vue'
import { useDisplay } from "vuetify";
import { useThrottleFn } from '@vueuse/core'
import type { FacetInputItem, FilterItem } from '@/components/AutocompleteMeiliFacets.vue'
import { useMeiliSearch } from "#imports"
import type { Filter } from "meilisearch";
// import { saveAs } from "file-saver";
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 AutocompleteMeiliFacetProps {
db: string
facets: FacetInputItem[] | undefined
facetDistribution: Record<string, Record<string, number>> | undefined
}
export interface Props {
title?: string
sortBy?: SortItem[]
numericalFilters?: Ref<string[] | undefined>
dataTableServerProps: Record<string, any>
columnsToDownload?: MaybeRef<string[] | undefined>
autocompleteMeiliFacetsProps: AutocompleteMeiliFacetProps
}
const props = withDefaults(defineProps<Props>(), {
title: '',
columnsToDownload: undefined,
sortBy: () => [{ key: "type", order: "asc" }],
numericalFilters: () => ref(undefined),
autocompleteMeiliFacetsProps: () => {
return {
db: 'refseq',
facetDistribution: undefined,
facets: undefined
}
}
});
// const facetDistribution: Ref<Record<string, Record<string, number>> | undefined> = useState(`refseqFacetDistribution`)
const slots = useSlots()
const sortByRef = toRef(props.sortBy)
const emit = defineEmits(["refresh:search"])
const { search: msSearch, result: msResult } = useMeiliSearch(props.autocompleteMeiliFacetsProps.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 toRefNumericalFilters = toRef(props.numericalFilters)
// const meiliFilters = ref<string | undefined>(undefined)
const filterInputValues = computed(() => {
if (filterOrSearch.value != null) {
return filterOrSearch.value.filter(({ props }) => props.type !== 'text')
} else {
return null
}
})
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]) => {
if (toValue(isValidFilters)) {
searchOrFilter()
}
})
onBeforeMount(async () => {
searchOrFilter()
emitRefreshRes()
})
const msFilterCompo = ref<FilterItem[] | undefined>(undefined)
const computedFilterStr = computed(() => {
const toValFilters = toValue(msFilterCompo)
let filtersStr: string | undefined = undefined
if (toValFilters !== undefined && toValFilters.length > 0) {
const tmpFilterItems = [...toValFilters]
if (tmpFilterItems.length % 4 === 0) {
tmpFilterItems.splice(-1)
}
filtersStr = "(" + tmpFilterItems.map((it, index) => {
const sanitizedValue = it.value.split("-").slice(0, -1).join("-")
if ((index + 1) % 4 === 3) {
return `"${sanitizedValue}"`
} else if ((index + 1) % 4 === 0) {
return ` ${sanitizedValue} `
}
else {
return `${sanitizedValue}`
}
}).join("") + ")"
}
return [filtersStr, props.numericalFilters].filter(f => f !== undefined && f !== null).join(" AND ")
})
const computedF = computed(() => toValue(props.numericalFilters))
const { arrayFilters: computedFilter } = useMeiliFilters(msFilterCompo, computedF)
// const computedFilter = computed(() => {
// const toValFilters = toValue(msFilterCompo)
// if (toValFilters !== undefined && toValFilters.length > 0) {
// meiliFilterAsArray
// }
// })
watch(computedFilter, () => {
console.log(toValue(computedFilter))
if (toValue(isValidFilters) && (toValue(computedFilter) !== undefined || toValue(filterInputValues) === null)) {
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)
const lastFilterItem = computed(() => {
const toValFilterItems = toValue(msFilterCompo)
if (toValFilterItems !== undefined && Array.isArray(toValFilterItems)) {
return toValFilterItems.slice(-1)[0]
}
})
const isValidFilters = computed(() => {
const toValFilterItems = toValue(msFilterCompo)
if (toValFilterItems === undefined || Array.isArray(toValFilterItems) && toValFilterItems?.length === 0) {
return true
}
else {
const toValLastFilterItem = toValue(lastFilterItem)
if (toValLastFilterItem !== undefined) {
console.log(toValLastFilterItem.type)
console.log(toValLastFilterItem.type === 'value')
return toValLastFilterItem.type === 'value'
// && isAutocompleteFocused.value === false
// || (toValFilterStep === 0 && toValLastFilterItem.type === "outerOperator" && toValLastFilterItem.value.split("-")[0] === "AND")
}
}
return false
})
async function searchOrFilter() {
if (toValue(isValidFilters)) {
// 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() {
const q = search.value
emit("refresh:search", {
index: props.autocompleteMeiliFacetsProps.db,
query: q,
params: { ...notPaginatedParams.value, filter: toValue(computedFilter), sort: msSortBy.value }
})
}
const totalHits = computed(() => {
return toValue(msResult)?.totalHits ?? toValue(msResult)?.estimatedTotalHits ?? 0
})
watch(search, () => {
searchOrFilter()
// emitRefreshRes()
})
// watch(msFilterCompo, () => {
// searchOrFilter()
// })
async function downloadData() {
pendingDownloadData.value = true
try {
const { data } = await useAsyncMeiliSearch({
index: props.autocompleteMeiliFacetsProps.db,
params: { ...toValue(notPaginatedParams), filter: toValue(computedFilter), sort: toValue(msSortBy) },
query: toValue(search),
})
useCsvDownload(data, props.columnsToDownload, props.title)
} finally {
pendingDownloadData.value = false
}
}
function focusedOrBlur(isFocused: boolean) {
if (!isFocused) {
emitRefreshRes()
}
}
</script>
<template>
<v-card flat color="transparent">
<slot name="numerical-filters" :search="throttleSearch"></slot>
<v-data-table-server v-if="!msError" v-model:page="page" color="primary" v-bind="dataTableServerProps"
v-model:items-per-page="hitsPerPage" v-model:sortBy="sortByRef" v-model:expanded="expanded" fixed-header
:loading="loading" :items="msResult?.hits ?? []" :items-length="totalHits" density="compact"
:items-per-page-options="itemsPerPage" :height="computedTableHeight" class="elevation-1 mt-2">
<template #top>
<v-card variant="flat" color="transparent">
<v-card-title>
<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-card-title>
<v-card-title>
<v-text-field v-model="search" label="Search..." hide-details="auto" :disabled="pendingDownloadData"
prepend-inner-icon="mdi-magnify" single-line clearable
@update:focused="focusedOrBlur"></v-text-field>
</v-card-title>
<v-card-title>
<AutocompleteMeiliFacets v-model="msFilterCompo" v-bind="props.autocompleteMeiliFacetsProps"
:is-valid-filters="isValidFilters">
</AutocompleteMeiliFacets>
</v-card-title>
</v-card>
</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,171 @@ 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` : undefined
})
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" style="position:relative;">
<pdbe-molstar ref="pdbeMolstarComponent" :custom-data-url="selectedPdb" alphafold-view
sequence-panel="true" landscape="false" :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
</style>
......@@ -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
This diff is collapsed.
......@@ -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