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 (198)
Showing
with 1460 additions and 260 deletions
......@@ -4,6 +4,11 @@ workflow:
when: never
- when: always
# Functions that should be executed before the build script is run
variables:
......@@ -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:
......@@ -26,6 +33,7 @@ cache:
stages:
- delete-release
- build-df-cli
- lint
- get-data
- deploy-meilisearch
- update-meilisearch-indexes
......@@ -43,9 +51,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 +77,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 +102,14 @@ build:df-wiki-cli:
--values deploy/meilisearch/values.yaml
--values deploy/meilisearch/values.${ENV:-development}.yaml
# wait for it to start
- MEILI_POD=$(kubectl -n=${KUBE_NAMESPACE} get po -l app.kubernetes.io\/instance=${CI_PROJECT_NAME}-${CI_ENVIRONMENT_NAME}-meilisearch,app.kubernetes.io\/name=meilisearch --output jsonpath='{.items[0].metadata.name}')
- |
until kubectl -n=${KUBE_NAMESPACE} wait --for=condition=ready pod ${MEILI_POD} --timeout=1s
do
date
sleep 1
kubectl -n=${KUBE_NAMESPACE} get po
done
# - MEILI_POD=$(kubectl -n=${KUBE_NAMESPACE} get po -l app.kubernetes.io\/instance=${CI_PROJECT_NAME}-${CI_ENVIRONMENT_NAME}-meilisearch,app.kubernetes.io\/name=meilisearch --output jsonpath='{.items[0].metadata.name}')
# - |
# until kubectl -n=${KUBE_NAMESPACE} wait --for=condition=ready pod ${MEILI_POD} --timeout=1s
# do
# date
# sleep 1
# kubectl -n=${KUBE_NAMESPACE} get po
# done
deploy:meilisearch:dev:
......@@ -163,15 +177,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
......@@ -189,6 +211,8 @@ delete-meili-helm-release:prod:
update
--file data/all_predictions_statistics.csv
--document structure
allow_failure: false
update-meilisearch-index:dev:
rules:
......@@ -212,12 +236,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 +251,7 @@ update-meilisearch-index:prod:
artifacts:
reports:
dotenv: build.env
allow_failure: false
set-meili-env:dev:
......@@ -249,28 +272,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 +322,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,7 +337,7 @@ build:prod:wiki:
needs:
- set-meili-env:prod
- get-zotero
- get-pfam
# - get-pfam
variables:
BASE_URL: /wiki/
rules:
......
......@@ -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 anti-phage 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: "structure", label: "Structures DB", to: '/structure/' },
{ id: "help", label: "Help", to: '/help/' },
]);
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>
......
<script setup lang="ts">
// import type { FacetDistribution } from "meilisearch";
import { useSlots } from 'vue'
import { useDisplay } from "vuetify";
import * as Plot from "@observablehq/plot";
import PlotFigure from "~/components/PlotFigure";
import * as d3 from "d3";
import { useThrottleFn } from '@vueuse/core'
import { useMeiliSearch } from "#imports"
export interface SortItem {
key: string,
order: boolean | 'asc' | 'desc'
}
export interface Props {
title?: string
db?: string
sortBy?: SortItem[]
facets: MaybeRef<string[]>
dataTableServerProps: Record<string, any>
}
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',
sortBy: () => [{ key: "type", order: "asc" }],
});
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 } = useDisplay();
const minTableHeight = ref(400)
const computedTableHeight = computed(() => {
const computedHeight = height.value - 350
return computedHeight > minTableHeight.value ? computedHeight : minTableHeight.value
})
const plddtRange = ref([0, 100])
// const { pending: pendingDownloadData, downloadCsv } = useCsvDownload(props.db, `df-${props.db}`)
const filterInputValues = computed(() => {
// console.log("recompouted FILTER value")
if (filterOrSearch.value != null) {
return filterOrSearch.value.filter(({ props }) => props.type !== 'text')
} else {
return null
}
})
// const queryInputValue = computed(() => {
// if (filterOrSearch.value !== null) {
// const phrase = filterOrSearch.value
// .filter((f) => {
// return f.props.type === 'text'
// })
// .map((f) => {
// return f.value
// })
// if (phrase.length > 1) {
// return `${phrase.join(" ")}`
// }
// else { return phrase[0] }
// } else {
// return null
// }
// })
const isFilter = computed(() => {
return Array.isArray(filterOrSearch.value)
})
const msSortBy = computed(() => {
if (sortByRef.value.length > 0) {
return sortByRef.value.map((curr) => {
if (curr?.key && curr?.order) {
return `${curr.key}:${curr.order}`
}
else { return "" }
})
} else { return undefined }
})
const reactiveParams = reactive({
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()
})
const hasPlddt = computed(() => props.db === 'structure')
// Fetch results
const plddtFilter = computed(() => {
const plddtRangeValue = plddtRange.value
if (hasPlddt.value && plddtRangeValue?.length === 2) {
return `plddts ${plddtRangeValue[0]} TO ${plddtRangeValue[1]}`
} else {
return undefined
}
})
const computedFilter = computed(() => {
if (toValue(msFilter)) {
if (toValue(plddtFilter)) {
return `${toValue(msFilter)} AND ${toValue(plddtFilter)}`
}
else {
return toValue(msFilter)
}
} else {
if (toValue(plddtFilter)) {
return `${toValue(plddtFilter)}`
}
else {
return undefined
}
}
})
const msError = computed(() => {
if (filterError.value?.type && filterError.value?.message) {
return filterError.value?.message
} else { return false }
})
const throttleSearch = useThrottleFn(async () => { searchOrFilter() }, 300)
async function searchOrFilter(pagination = true) {
// 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
emit("refresh:search", {
index: props.db,
query: q,
params: { ...notPaginatedParams.value, filter: toValue(computedFilter), sort: msSortBy.value }
})
if (pagination) {
await msSearch(q, { ...paginationParams.value, filter: toValue(computedFilter), sort: msSortBy.value })
}
else {
await msSearch(q, { ...notPaginatedParams.value, filter: toValue(computedFilter), sort: msSortBy.value })
}
} catch (error: any) {
filterError.value = error
console.log(error)
}
finally {
loading.value = false
}
}
function clearFilterOrSearch() {
filterOrSearch.value = null
searchOrFilter()
}
watch(msFilter, async (fos) => {
searchOrFilter()
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() })
// watch(plddtRange, () => { searchOrFilter() })
const filterStep = computed(() => {
return filterInputValues.value !== null && filterInputValues.value.length > 0 ? filterInputValues.value?.length % 3 : null
})
const operatorItems = ref([
{
type: "operator", value: '=', title: "is", deletable: false, props: {
type: "operator", deletable: false
}
}, {
type: "operator", value: '!=', title: "is not", deletable: false, props: {
type: "operator",
deletable: false
}
},
])
const autocompleteItems = computed(() => {
const index = filterOrSearch.value?.length ?? 0
// console.log(index)
if (filterStep.value === null || filterStep.value === 0) {
return 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
}
}
}) : []
}
}
})
// const canAddTextSearch = computed(() => {
// if (filterOrSearch.value !== null && filterOrSearch.value.length > 0) {
// const lastItem = filterOrSearch.value.slice(-1)[0]
// return lastItem?.props.type === 'value' || lastItem?.props.type === "text"
// }
// return true
// })
function selectItem(item) {
filterOrSearch.value = Array.isArray(filterOrSearch.value) ? [...filterOrSearch.value, item] : [item]
}
function deleteOneFilter(index: number) {
if (isFilter.value) {
filterOrSearch.value?.splice(index - 2, 2)
}
}
function clearSearch() {
search.value = ""
}
// const groupSortDomain = computed(() => {
// console.log(msResult.value)
// return msResult.value ? d3.groupSort(msResult.value?.hits?.filter((d) => d.phylum), (g) => d3.median(g, (d) => d.phylum), (d) => d.type) : []
// })
</script>
<template>
<v-card flat color="transparent">
<v-card-text>
</v-card-text>
<v-card-text>
<v-row>
<v-col cols="5">
<v-text-field v-model="search" label="Search..." hide-details prepend-inner-icon="mdi-magnify"
single-line clearable></v-text-field>
</v-col>
<v-col>
<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-col>
</v-row>
<v-row v-if="props.db === 'structure'">
<v-col>
<v-range-slider v-model="plddtRange" density="compact" hide-details="auto" label="pLDDT" step="0.1"
@update:modelValue="throttleSearch()">
<template v-slot:prepend>
<span hide-details single-line type="number" variant="outlined" density="compact"
style="width: 70px">{{ plddtRange[0] }}</span>
</template>
<template v-slot:append>
<span hide-details single-line type="number" variant="outlined" style="width: 70px"
density="compact">{{ plddtRange[1] }}</span>
</template>
</v-range-slider>
</v-col>
</v-row>
</v-card-text>
<v-data-table-server v-if="!msError" v-model:page="page" color="primary" v-bind="dataTableServerProps"
v-model:items-per-page="hitsPerPage" v-model:sortBy="sortByRef" v-model:expanded="expanded" fixed-header
:loading="loading" :items="msResult?.hits ?? []" :items-length="totalHits" density="compact"
:items-per-page-options="itemsPerPage" :height="computedTableHeight" class="elevation-1 mt-2">
<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 { 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
}
......@@ -88,33 +104,109 @@ 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)
watch(selectedPdb, (selectedPdb, prevSelectPdb) => {
if (selectedPdb !== null) {
dialog.value = true
if (pdbeMolstarComponent.value?.viewerInstance) {
const viewerInstance = pdbeMolstarComponent.value.viewerInstance
const selectedPaePath = computed(() => {
return selectedPdb.value ? `${selectedPdb.value.split(".").slice(0, -1).join('.')}.png` : null
})
// show.value = true
// const paeData = ref([])
// watch(selectedPaePath, async (newPaePath) => {
// if (newPaePath !== null) {
// try {
// const data = await d3.tsv(newPaePath);
// if (data.length > 500) {
// paeError.value = `The PAE is too large to be displayed (${data.length} residus)`
// paeData.value = []
// }
// else if (data?.[0]?.[0] === undefined) {
// paeError.value = "The PAE cannot be downloaded"
// paeData.value = []
// }
// else {
// paeData.value = data
// paeError.value = null
// }
// }
// catch (error) {
// console.log(error)
// }
// } else {
// paeData.value = []
// }
// })
// const sanitizedPaeData = computed(() => {
// return paeData.value.reduce((acc, curr, index) => {
// const scoredResidue = index
// // let newAcc = [...acc]
// for (const [alignedResidue, value] of Object.entries(curr)) {
// // console.log(value)
// acc.push({ alignedResidue: parseInt(alignedResidue), scoredResidue: parseInt(scoredResidue), value: parseFloat(value) })
// // newAcc = [...newAcc, ...[{ alignedResidue: parseInt(alignedResidue), scoredResidue: parseInt(scoredResidue), value: parseFloat(value) }]]
// }
// return acc
// }, [])
// })
// const plotPaeOptions = computed(() => {
// return {
// width: 640,
// height: 640,
// color: { scheme: "Greens", legend: true, reverse: true, label: "Expected position error (Ångströms)" },
// y: { reverse: true },
// marks: [
// Plot.dot(sanitizedPaeData.value, { x: "scoredResidue", y: "alignedResidue", stroke: "value" })
// ]
// }
// })
watch(selectedPdb, (newSelectedPdb, prevSelectPdb) => {
viewPdb(newSelectedPdb)
})
const customData = { url: selectedPdb, format: "pdb", binary: false }
function viewPdb(pdbPath: string | null) {
if (pdbPath !== null) {
dialog.value = true
const format = toValue(pdbPath)?.split(".").slice(-1)[0]?.toLowerCase() ?? "pdb"
moleculeFormat.value = format
if (pdbeMolstarComponent.value?.viewerInstance) {
console.log(pdbeMolstarComponent.value)
const viewerInstance = pdbeMolstarComponent.value.viewerInstance
const customData = { url: pdbPath, format: format, binary: false }
viewerInstance.visual.update({ customData })
}
}
}
function setSelectedPdbToFirst() {
const urls = toValue(refinedDataUrls)
if (urls.length >= 1) {
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>
<span class="d-flex flex-wrap align-center justify-center">
<v-col>
<v-btn v-if="uniq" size="x-small" variant="tonal" icon="md:visibility" @click="setSelectedPdbToFirst()"></v-btn>
<v-select v-else v-model="selectedPdb" label="Select PDB" :items="refinedDataUrls" hide-details="auto">
</v-select>
</v-col>
<v-row justify="center">
<v-dialog v-model="dialog" transition="dialog-bottom-transition" fullscreen :scrim="false">
<v-card flat :rounded="false">
<v-toolbar>
......@@ -130,18 +222,77 @@ watch(selectedPdb, (selectedPdb, prevSelectPdb) => {
</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-row>
<v-col :cols="mobile ? 12 : 'auto'">
<v-sheet v-if="selectedPdb"
class="d-flex align-center justify-center flex-wrap text-center mx-auto px-4 my-3"
:height="computedHeight" :width="computedWidth" position="relative">
<pdbe-molstar ref="pdbeMolstarComponent" hide-controls="true" landscape="true"
:custom-data-url="selectedPdb" alphafold-view="true"
:custom-data-format="moleculeFormat"></pdbe-molstar>
</v-sheet>
</v-col>
<v-col :cols="mobile ? 12 : undefined">
<v-img :src="selectedPaePath"></v-img>
<!-- <PlotFigure v-if="sanitizedPaeData?.length > 0 && paeError === null" defer
: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>
</v-row>
</span>
</template>
......@@ -149,4 +300,9 @@ watch(selectedPdb, (selectedPdb, prevSelectPdb) => {
.msp-plugin .msp-plugin-content {
color: black !important;
}
.legendColor {
height: 16px;
width: 16px;
}
</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"
const sortBy: Ref<SortItem[]> = ref([{ key: 'type', order: "asc" }])
const itemValue = ref("id");
const { width } = useDisplay();
const scaleTransform: Ref<string[]> = ref([])
const facets = ref([
"replicon",
"type",
"subtype",
"Superkingdom",
"phylum",
"order",
"family",
"genus",
"species",
])
const availableTaxo: Ref<string[]> = ref([
"species",
"genus",
"family",
"order",
"phylum",
"Superkingdom"
]);
const selectedTaxoRank = ref("phylum");
const headers = ref([
{ title: "Replicon", key: "replicon" },
{
title: "Type",
key: "type",
},
{
title: "Subtype",
key: "subtype",
},
{
title: "Accessions",
key: "accession_in_sys",
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([])
onMounted(async () => {
const params = {
facets: ["*"],
filter: [],
sort: ["type:asc"],
limit: 500000
}
await getAllHits({ index: "refseq", params, query: "" })
})
async function getAllHits(params) {
console.log("refresh hits")
console.log(params)
console.log(params.index)
if (params.index === 'refseq') {
console.log(params.index)
const { data, error } = await useAsyncMeiliSearch(params)
console.log(error.value)
console.log(data.value)
allHits.value = data.value
}
}
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, ticks: 10 },
y: { grid: true, type: scaleType.value },
color: { legend: true },
width: computedWidth.value,
height: plotHeight.value + 100,
}
})
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,
marks: [
// Plot.frame(),
Plot.barY(
toValue(computedSystemDistribution),
{
y: "count", x: 'type', tip: true,
fill: "#6750a4",
sort: { x: "-y" },
},
),
],
};
});
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,
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<number> = ref(["table"])
const layoutPlot: Ref<string[]> = ref("grid")
const binPlotOptions = ref({
marginLeft: 150,
marginBottom: 200,
padding: 0,
width: 1920,
grid: true,
x: { tickRotate: 90, tip: true, },
color: { scheme: "turbo", legend: true },
})
const binPlotDataOptions = computed(() => {
return allHits.value?.hits?.length > 0 ? {
...binPlotOptions.value,
color: {
...binPlotOptions.value.color,
type: scaleType.value
},
// fy: { domain: groupSortDomain.value },
marks: [
Plot.cell(allHits.value?.hits ?? [], Plot.group({ fill: "count" }, { x: "type", y: selectedTaxoRank.value, tip: true, inset: 0.5, sort: { y: "fill" } })),
]
} : null
})
const scaleType = ref("linear")
</script>
<template>
<v-card flat class="mb-2" color="transparent">
<v-toolbar>
<!-- <v-toolbar-title>Plots</v-toolbar-title> -->
<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-spacer></v-spacer>
<v-select v-model="selectedTaxoRank" :items="availableTaxo" density="compact" label="Select taxonomic rank"
hide-details="auto" class="mx-2"></v-select>
<!-- <v-btn-toggle v-model="scaleTransform" density="compact" mandatory rounded="false" variant="text"
color="primary" class="mx-2">
<v-btn icon="mdi-math-log" value="linear">linear</v-btn>
<v-btn value="pow">pow</v-btn>
<v-btn icon="mdi-math-log" value="sqrt">sqrt</v-btn>
<v-btn icon="mdi-math-log" value="symlog">symlog</v-btn>
<v-btn icon="mdi-math-log" value="log">log</v-btn>
</v-btn-toggle> -->
<v-select v-model="scaleType" class="mx-2" density="compact" :items="['linear', 'sqrt', 'symlog']"
label="Scale Type" hide-details="auto"></v-select>
</v-toolbar>
<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-row align="start">
<v-col :cols="fullWidth ? 12 : 6">
<PlotFigure :options="unref(computedDistriSystemOptions)" defer></PlotFigure>
</v-col>
<v-col :cols="fullWidth ? 12 : 6">
<PlotFigure defer :options="unref(computedDistriTaxoOptions)"></PlotFigure>
</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 color="primary"
class="mr-2">mdi-data-matrix</v-icon>Heatmap</v-expansion-panel-title>
<v-expansion-panel-text>
<v-card flat color="transparent">
<PlotFigure v-if="toValue(binPlotDataOptions) !== null" :options="unref(binPlotDataOptions)"
defer>
</PlotFigure>
</v-card>
</v-expansion-panel-text>
</v-expansion-panel>
<v-expansion-panel elevation="3" value="table">
<v-expansion-panel-title color="grey-lighten-4"><v-icon color="primary"
class="mr-2">mdi-table</v-icon>Table</v-expansion-panel-title>
<v-expansion-panel-text>
<ServerDbTable title="RefSeq" db="refseq" :sortBy="sortBy" :facets="facets"
:data-table-server-props="dataTableServerProps" @refresh:search="getAllHits">
<template #top>
<v-toolbar><v-toolbar-title class="text-capitalize">
RefSeq
</v-toolbar-title><v-spacer></v-spacer>
</v-toolbar>
</template>
<template #[`item.accession_in_sys`]="{ item }">
<CollapsibleChips :items="namesToAccessionChips(item.accession_in_sys)">
</CollapsibleChips>
</template>
</ServerDbTable>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
</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 { ServerDbTable } from "#components"
const sortBy: Ref<SortItem[]> = ref([{ key: 'system', order: "asc" }])
const itemValue = ref("id");
const facets: Ref<string[]> = ref(["system", "completed"])
const headers: Ref<Object[]> = ref([
{ title: 'Structure', key: 'structure', sortable: false, removable: false },
{ title: "System", key: "system", 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 defaultDataTableServerProps = ref({
showExpand: false
})
const dataTableServerProps = computed(() => {
return {
...toValue(defaultDataTableServerProps),
headers: toValue(headers),
itemValue: toValue(itemValue)
}
})
function namesToCollapsibleChips(names: string[], file: string | null = null) {
if (file === null) {
return names.filter((it) => it !== "").map(it => ({ title: it.split("__").pop() }))
} else {
return names.filter((it) => it !== "").map(it => ({ title: it.split("__").pop(), href: `/wiki/${toSystemName(file)}/${file}` }))
}
}
function pdbNameToCif(pdbPath: string) {
const cifPath = pdbPath.split(".").slice(0, -1).join(".")
return `${cifPath}.cif`
}
function toSystemName(rawName: string) {
return rawName.split(/_|-0/)[0].toLocaleLowerCase()
}
const plddtDistribution = computed(() => {
if (toValue(msResult)?.facetDistribution?.plddts) {
return Object.entries(toValue(msResult).facetDistribution.plddts).map(([key, value]) => { })
}
})
function remove(key) {
headers.value = headers.value.filter(header => header.key !== key)
}
</script>
<template>
<ServerDbTable title="Predicted Structures" db="structure" :sortBy="sortBy" :facets="facets"
:data-table-server-props="dataTableServerProps">
<template #top>
<v-toolbar><v-toolbar-title class="text-capitalize">
Predicted Structures
</v-toolbar-title><v-spacer></v-spacer>
</v-toolbar>
</template>
<template #[`item.proteins_in_the_prediction`]="{ item }">
<CollapsibleChips :items="namesToCollapsibleChips(item.proteins_in_the_prediction, item.fasta_file)">
</CollapsibleChips>
</template>
<template #[`item.system_genes`]="{ item }">
<CollapsibleChips :items="namesToCollapsibleChips(item.system_genes)"></CollapsibleChips>
</template>
<template #[`item.structure`]="{ item }">
<MolstarPdbePlugin v-if="item?.pdb && item.pdb !== 'na'"
:data-urls="[`/${toSystemName(item.system)}/${pdbNameToCif(item.pdb)}`]" uniq>
</MolstarPdbePlugin>
<span v-else class="d-flex flex-wrap align-center justify-center">
<v-icon color="warning" icon="md:dangerous"></v-icon>
</span>
</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
import { ref } from 'vue';
import Papa from 'papaparse';
import { saveAs } from "file-saver";
export function useCsvDownload(index: MaybeRef<string>, baseName: MaybeRef<string> = "df"
) {
const pending = ref(false)
const { search: msSearch, result: msResult } = useMeiliSearch(toValue(index))
const filename = ref(`${toValue(baseName)}-data.csv`)
const downloadCsv = async (
query: MaybeRef<string>,
filter: MaybeRef<string>,
sortBy: MaybeRef<string[]>,
params: MaybeRef<Record<string, any>>,
) => {
filename.value = `${toValue(baseName)}-${toValue(filter)}.csv`
pending.value = true
try {
await msSearch(
toValue(query),
{
...toValue(params),
filter: toValue(filter),
sort: toValue(sortBy)
})
const csvContent = Papa.unparse(toValue(msResult).hits);
// console.log(csvContent)
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
saveAs(blob, `${toValue(filename)}`);
} finally {
pending.value = false
}
}
// watch(msResult, (newRes) => {
// console.log("save file !!!!!!")
// })
return {
pending, downloadCsv,
};
}
......@@ -2,17 +2,19 @@ import { useArticlesStore, type CslJson } from '../stores/articles'
import { ref, computed, watchEffect, toValue } from "vue"
// import { useFetch } from '#app';
// import { useFetch } from "nuxt"
import { useFetch } from '#imports'
import { type MaybeRef, useFetch } from '#imports'
import article from "@/public/articles.json"
export interface ArticleMessage {
export interface CrossrefArticle {
DOI: string;
issue: number;
title: string | string[];
type: string;
title: string[];
author: Array<{ family: string; given: string }>;
"container-title-short": string;
// "container-title-short": string;
"short-container-title": string;
"container-title": string;
abstract: string;
abstract?: string;
published: {
"date-parts": string[];
};
......@@ -23,7 +25,7 @@ export interface ArticleMessage {
export interface Article {
export interface WikiArticle {
DOI: string
title: string
subtitle: string
......@@ -36,115 +38,125 @@ export interface Article {
prependIcon: string
}
export interface RawArticle {
message: ArticleMessage
message: CrossrefArticle
}
type SrcArticle = ArticleMessage | CslJson
type SrcArticle = CrossrefArticle | CslJson
export function useFetchArticle(doi: string) {
export function useFetchArticle(doi: MaybeRef<string> = ref("")) {
// const article = ref<Article>()
// const rawArticle = ref<RawArticle>()
const srcArticle = ref<SrcArticle | null>(null)
const store = useArticlesStore()
const pending = ref(false)
const doiBaseUrl = ref(new URL("https://doi.org/"));
const url = ref(new URL(`/works/${doi}`, " https://api.crossref.org/").href);
const article = computed(() => {
if (srcArticle.value != undefined) {
const url = ref(new URL(`/works/${toValue(doi)}`, " https://api.crossref.org/").href);
const article = ref()
const zoteroArticles = ref()
function toAuthorsString(authors: Array<{ family: string; given: string }>) {
return authors
.map((curr) => {
return `${curr.family} ${curr.given}`;
})
.join(", ");
}
function getReferenceUrl(doi: string) {
return new URL(doi, doiBaseUrl.value).href;
}
function zoteroArticleToArticle(zoteroArticle: CslJson) {
if (zoteroArticle != undefined) {
const {
DOI,
title,
"container-title-short": cts,
"container-title": ct,
journalAbbreviation,
abstract,
published,
issued,
author,
...rest
} = srcArticle.value;
let sanitizedAbstract = abstract
if (sanitizedAbstract) {
sanitizedAbstract = /(?:\<jats\:p\>)?(.*)(?:\<\/jats\:p\>)?/.exec(sanitizedAbstract)?.[1] ?? ''
}
const sanitizedTitle = (Array.isArray(title)) ? title[0] : title
const sanitizedContainerTitle = (Array.isArray(ct)) ? cts?.length > 0 ? cts[0] : ct?.length > 0 ? ct[0] : "" : journalAbbreviation || ct
} = zoteroArticle;
return {
DOI,
title: sanitizedTitle,
title,
subtitle: toAuthorsString(author || []),
author,
containerTitle: sanitizedContainerTitle,
abstract: sanitizedAbstract,
year: published?.["date-parts"][0][0] ?? issued?.["date-parts"][0][0] ?? '',
containerTitle: ct,
abstract,
year: issued?.["date-parts"][0][0] ?? '',
href: getReferenceUrl(DOI),
target: "_blank",
prependIcon: "mdi-newspaper-variant-outline",
}
} else { return srcArticle.value }
})
const zoteroArticles = ref([])
// const config = useRuntimeConfig()
// console.log(config.value)
const fetchLocalArticles = () => {
useFetch<RawArticle[]>(
"/articles.json",
{ lazy: true, server: false }
).then(({ data }) => {
zoteroArticles.value = data.value
}) // localPending.value = articlesPending.value
if (zoteroArticles.value?.length > 0) {
for (const article of zoteroArticles.value) {
// console.log("article files : ", article.DOI)
store.add(article)
}
}
}
const fetchCrossRef = () => {
useFetch<RawArticle>(toValue(url), {
lazy: true, server: false,
}).then(({ data, pending: pendingUseFetch }) => {
if (data.value?.message) {
srcArticle.value = data.value.message
}
pending.value = pendingUseFetch.value
})
}
watchEffect(() => {
// no article in the store
if (store.articles.size === 0) {
fetchLocalArticles()
function crossrefToArticle(article: CrossrefArticle): WikiArticle {
const { title, DOI, type, "container-title": ct, "short-container-title": sct, abstract, author, issued } = article
// let sanitizedAbstract = abstract
const sanitizedAbstract = abstract ? /(?:\<jats\:p\>)?(.*)(?:\<\/jats\:p\>)?/.exec(abstract)?.[1] ?? '' : ''
const sanitizedContainerTitle = sct?.length > 0 ? sct[0] : ct?.length > 0 ? ct[0] : ""
return {
title: title?.length > 0 ? title[0] : "",
DOI,
abstract: sanitizedAbstract,
containerTitle: sanitizedContainerTitle,
subtitle: toAuthorsString(author || []),
author,
year: issued?.["date-parts"][0][0] ?? '',
href: getReferenceUrl(DOI),
target: "_blank",
prependIcon: "mdi-newspaper-variant-outline"
}
}
if (store.articles.has(doi)) {
srcArticle.value = store.articles.get(doi)
return
} else {
fetchCrossRef()
}
function crossrefToCsl(article: CrossrefArticle): CslJson {
const { title, DOI, type, "container-title": ct, "short-container-title": sct, abstract, author, issued } = article
// let sanitizedAbstract = abstract
const sanitizedAbstract = abstract ? /(?:\<jats\:p\>)?(.*)(?:\<\/jats\:p\>)?/.exec(abstract)?.[1] ?? '' : ''
const sanitizedContainerTitle = sct?.length > 0 ? sct[0] : ct?.length > 0 ? ct[0] : ""
return {
title: title?.length > 0 ? title[0] : "",
type,
DOI,
abstract: sanitizedAbstract,
author,
"container-title": sanitizedContainerTitle,
issued
})
function toAuthorsString(authors: Array<{ family: string; given: string }>) {
return authors
.map((curr) => {
return `${curr.family} ${curr.given}`;
})
.join(", ");
}
}
function getReferenceUrl(doi: string) {
return new URL(doi, doiBaseUrl.value).href;
if (store.articles.has(toValue(doi))) {
const cslArticle = store.articles.get(toValue(doi))
article.value = cslArticle ? zoteroArticleToArticle(cslArticle) : undefined
}
else {
useFetch<RawArticle>(toValue(url), {
lazy: true, server: false,
}).then(({ data, pending: pendingUseFetch }) => {
if (data.value?.message) {
article.value = crossrefToArticle(data.value.message)
store.add(crossrefToCsl(data.value.message))
}
pending.value = pendingUseFetch.value
})
}
// const fetchCrossRef = () => {
// useFetch<RawArticle>(toValue(url), {
// lazy: true, server: false,
// }).then(({ data, pending: pendingUseFetch }) => {
// if (data.value?.message) {
// srcArticle.value = data.value.message
// }
// pending.value = pendingUseFetch.value
// })
// }
return { article, pending }
}
......
import { MeiliSearch } from 'meilisearch'
import { useRuntimeConfig, watchEffect, type MaybeRef, ref, toValue } from '#imports'
import type { FacetDistribution, Hits } from 'meilisearch';
export function useFetchMsDocument(
import { useAsyncState } from '@vueuse/core'
import { errorMonitor } from 'events';
export async function useFetchMsDocument(
index: MaybeRef<string> = ref(""),
search: Ref<string> = ref(""),
filter: Ref<string> = ref(''),
......@@ -18,12 +21,11 @@ export function useFetchMsDocument(
apiKey: runtimeConfig.public.meiliApiKey
})
const pending = ref(false)
const filterError = ref(null)
const filterError: Ref<string | null> = ref(null)
const hits: Ref<Hits<Record<string, any>>> = ref([])
const totalHits = ref(0)
const totalPages = ref(0)
const facetDistribution: Ref<FacetDistribution | undefined> = ref({})
// reset page when filter and search change
watch(filter, () => {
page.value = 1
......@@ -32,34 +34,36 @@ export function useFetchMsDocument(
page.value = 1
})
watchEffect(async () => {
try {
pending.value = true
const res = await client
.index(toValue(index))
.search(toValue(search), {
limit: toValue(limit),
filter: toValue(filter),
hitsPerPage: toValue(hitsPerPage),
page: toValue(page),
facets: toValue(facets),
sort: toValue(sort),
})
filterError.value = null
const { hits: resHits, totalHits: resTotalHits, totalPages: resTotalPages, facetDistribution: facetD } = res
totalHits.value = resTotalHits
hits.value = resHits
totalPages.value = resTotalPages
facetDistribution.value = facetD
} catch ({ code, message }) {
if (code === 'invalid_search_filter') {
filterError.value = message
}
} finally {
pending.value = false
}
})
try {
pending.value = true
console.log(pending.value)
const res = await client
.index(toValue(index))
.search(toValue(search), {
limit: toValue(limit),
filter: toValue(filter),
hitsPerPage: toValue(hitsPerPage),
page: toValue(page),
facets: toValue(facets),
sort: toValue(sort),
})
filterError.value = null
const { hits: resHits, totalHits: resTotalHits, totalPages: resTotalPages, facetDistribution: facetD } = res
totalHits.value = resTotalHits
hits.value = resHits
totalPages.value = resTotalPages
facetDistribution.value = facetD
pending.value = false
} catch (e: any) {
filterError.value = e
}
finally {
pending.value = false
}
// })
// console.log(hits)
return { hits, totalHits, pending, filterError, totalPages, facetDistribution }
}
......@@ -10,7 +10,6 @@ export function useRefinedUrl(url: string | Ref<string>) {
const _base = withLeadingSlash(
withTrailingSlash(useRuntimeConfig().app.baseURL)
);
console.log(_base)
if (_base !== "/" && !sanitzedUrl.startsWith(_base)) {
return joinURL(_base, sanitzedUrl);
}
......
......@@ -4,36 +4,37 @@ layout: article
navigation:
icon: 'md:home'
relevantAbstracts:
- doi: 10.1126/science.1138140
- doi: 10.1038/nmicrobiol.2017.92
- doi: 10.1128/jb.64.4.557-569.1952
- doi: 10.1128/jb.65.2.113-121.1953
- doi: 10.1128/JB.05535-11
- doi: 10.1126/science.aar4120
- doi: 10.1038/s41579-023-00934-x
- doi: 10.1126/science.1138140
- doi: 10.1126/science.aba0372
- doi: 10.1038/s41586-019-1894-8
- doi: 10.1128/jb.64.4.557-569.1952
- doi: 10.1128/JB.05535-11
- doi: 10.1016/j.cell.2021.12.029
- doi: 10.1038/nmicrobiol.2017.92
- doi: 10.1038/s41586-019-1894-8
---
## Introduction
Bacteriophages, or phages for short, are viruses that infect bacteria and hijack bacterial cellular machinery to reproduce themselves. Phages are extremely abundant entities, and could be responsible for up to 20-40% of bacterial mortality daily :ref{doi=10.1038/s41586-019-1894-8}. Therefore, phage infection constitutes a very strong evolutionary pressure for bacteria.
Bacteria and their phages have co-existed for billions of years. The pressure of phage infection is thought to be a major driver of bacterial evolution and has favored the development of a diversity of anti-phage weapons. These weapons, namely anti-phage defense systems can be defined as single genes or groups of genes that partially or fully inhibit phage infection. For reviews on anti-phage systems, see : :ref{doi=10.1038/s41586-019-1894-8,10.1146/annurev-micro-020722-013730,10.1016/j.mib.2005.06.006,10.1038/s41579-023-00934-x}.
In response to this evolutionary pressure, bacteria have developed an arsenal of anti-phage defense systems. The term "defense system" here designates either a single gene or a set of genes, which expression provides the bacteria with some level of resistance against phage infection.
## A brief history of anti-phage systems
## History
The first discovered anti-phage system, a Restriction-Modification (RM) system, was described in the early 1950s :ref{doi=10.1128/jb.64.4.557-569.1952,10.1128/jb.65.2.113-121.1953}. In the following decades, a handful of other systems were discovered :ref{doi=10.1016/j.mib.2005.06.006}. In 2007, CRISPR-Cas systems were discovered to be anti-phage systems :ref{doi=10.1126/science.1138140}. As CRISPR-Cas systems and RM systems are extremely prevalent in bacteria, it was thought for some years that the antiviral immune system of bacteria had been mostly elucidated.
The first anti-phage defense system was discovered in the early 1950s by two separate teams of researchers :ref{doi=10.1128/jb.64.4.557-569.1952}, :ref{doi=10.1128/jb.65.2.113-121.1953}. Luria and Human reported a mysterious phenomenon, where one phage was only capable of infecting a specific bacterial strain once. The progeny phages produced by this first round of infection had lost their ability to infect the same strain again, yet remained able to infect other bacterial strains. For them, this could only mean that "the genotype of the host in which a virus reproduces affects the phenotype of the new virus" :ref{doi=10.1128/jb.64.4.557-569.1952}. A similar phenomenon was shortly after described by Bertani and Wiegle.
Following these two major breakthroughs, knowledge of anti-phage systems remained scarce for some years. Yet, in 2011, it was revealed that anti-phage systems tend to colocalize on the bacterial genome in defense-islands :ref{doi=10.1128/JB.05535-11}. This led to a guilt-by-association hypothesis: if a gene or a set of genes is frequently found in bacterial genomes in close proximity to known defense systems, such as RM or CRISPR-Cas systems, then it might constitute a new defense system. This hypothesis was tested systematically in a landarmark study in 2018 :ref{doi=10.1126/science.aar4120} leading to the discovery of 10 novel anti-phage systems. This started the uncovering of an impressive diversity of defense systems in a very short amount of time :ref{doi=10.1038/s41579-023-00934-x}.
Their work was in fact the first report of what would later be named Restriction-Modification ([RM](/defense-systems/rm)) system, which is considered to be the first anti-phage defense system discovered.
To date over 150 types of defense systems have been described, unveiling an unsuspected diversity of molecular mechanisms. The antiviral immune systems of bacteria therefore appear much more complex than previously envisioned, and new discoveries do not seem to be slowing down.
The sighting of a second defense system occured more than 40 years later, in the late 1980s, when several teams around the world observed arrays containing short, palindromic DNA repeats clustered together on the bacterial genome :ref{doi=10.1038/nmicrobiol.2017.92}. Yet, the biological function of these repeats was only elucidated in 2007, when a team of researchers demonstrated that these repeats were part of a new anti-phage defense systems :ref{doi=10.1126/science.1138140}, known as [CRISPR-Cas system](https://en.wikipedia.org/wiki/CRISPR).
## Introducing the defense finder wiki
Following these two major breakthroughs, knowledge of anti-phage systems remained scarce for some years. Yet, in 2011, Makarova and colleagues revealed that anti-phage systems tend to colocalize on the bacterial genome in defense-islands :ref{doi=10.1128/JB.05535-11}. This led to a guilt-by-association hypothesis : if a gene or a set of genes is frequently found in bacterial genomes in close proximity to known defense systems, such as RM or CRISPR-Cas systems, then it might constitute a new defense system. This concept had a large role in the discovery of an impressive diversity of defense systems in a very short amount of time.
The fast pace of discoveries in the field can be intimidating to newcomers and can make it difficult for all to keep track of new discoveries. For this reason, we decided to implement a collaborative knowledge base for the community. This wiki is divided in two sections:
1. A “general concepts” section, introducing key notions and ideas to understand anti-phage defense
2. A section introducing succinctly each of the defense systems currently known.
## List of known defense systems
This wiki is only a first version, and is intended to evolve based on the ideas and needs of the people using it. Whether it is to suggest new pages or to edit existing ones, all contributions are more than welcomed: please do not hesitate to contact us to participate!
To date, more than 150 anti-phage defense systems have been described. An exhaustive list of the systems with experimentally validated anti-phage activity can be found [here](/defense-systems).
---
title: General Concepts
toc: true
layout: article
layout: article-no-toc
---
# General concepts of defense systems
This section is empty. You can help by adding to it.
\ No newline at end of file
In the following pages are presented different general concepts that are useful to better comprehend the world of defense systems.
You'll find information on :
1. [Abortive infection](/general-concepts/abortive-infection/)
2. [Defense Islands ](/general-concepts/defense-islands/)
3. [Triggers of defense systems](/general-concepts/defense-systems_trigger/)
4. [Effectors of defense systems](/general-concepts/defense-systems_effector/)
5. [How defense systems were and are discovered](/general-concepts/defense-systems-discovery/)
6. [Defensive domains](/general-concepts/defensive-domains/)
7. [MGE and defense systems](/general-concepts/mge-defense-systems/)
8. [Anti defense systems](/general-concepts/anti-defense-systems/)
\ No newline at end of file
---
title: Abortive Infection
layout: article
toc: true
contributors:
- Lucas Paoli
relevantAbstracts:
- doi: 10.1128/jb.68.1.36-42.1954
- doi: 10.1016/0006-3002(61)90455-3
- doi: 10.1016/0022-2836(68)90078-8
- doi: 10.1128/jvi.4.2.162-168.1969
- doi: 10.1128/jvi.13.4.870-880.1974
- doi: 10.1128/mr.45.1.52-71.1981
- doi: 10.3168/jds.S0022-0302(90)78904-7
- doi: 10.1016/S0960-9822(00)00124-X
- doi: 10.1111/j.1365-2958.1995.tb02255.x
- doi: 10.1016/j.mib.2005.06.006
- doi: 10.1146/annurev-virology-011620-040628
- doi: 10.1016/j.mib.2023.102312
- doi: 10.1038/s41579-023-00934-x
---
This section is empty. You can help by adding to it.
The term abortive infection was coined in the 1950s :ref{doi=10.1128/jb.68.1.36-42.1954} to describe the observations that a fraction of the bacterial population did not support phage replication. This phenomenon, also called phage exclusion, was identified in multiple systems across the following decades :ref{doi=10.1016/0006-3002(61)90455-3,10.1016/0022-2836(68)90078-8,10.1128/jvi.4.2.162-168.1969,10.1128/jvi.13.4.870-880.1974} and reviewed extensively :ref{doi=10.1128/mr.45.1.52-71.1981,10.3168/jds.S0022-0302(90)78904-7,10.1111/j.1365-2958.1995.tb02255.x}. In the following years, and through the resolution of molecular mechanisms of key defense systems such as Rex or Lit, abortive infection became synonymous with infection-induced controlled cell-death. Controlled cell death upon detection of the phage infection stops the propagation of the phage and protects the rest of the bacterial population :ref{doi=10.1016/S0960-9822(00)00124-X,10.1016/j.mib.2005.06.006}. Abortive infection can thus be thought of as a form of bacterial altruism.
With the recent developments in phage-defense systems and microbial immunity (see :ref{doi=10.1038/s41579-023-00934-x} for a review), many newly identifed anti-phage defense systems are thought to function through abortive infection. Abortive defense systems often detect the phage infection at the later stage through protein sensing or the monitoring of host integrity but can also be based on nucleic acid sensing. Upon sensing, a diverse set of effectors can be used to reduce metabolism or induce cell-death (e.g., NAD+ depletion, translation interruption or membrane depolarisation). The diversity of and mechanisms of abortive infection were recently reviewd here :ref{doi=10.1146/annurev-virology-011620-040628}, while the evolutionary success of this paradoxical altruistic form of immunity has recently been discussed here :ref{doi=10.1016/j.mib.2023.102312}.
Although abortive infection is currently often understood as leading to cell-death, it should be noted that its original definition appeared to be broader and that some mechanisms currently included as abortive infection may only lead to metabolic stalling or dormancy.
## test article