Skip to content
Snippets Groups Projects
Commit 1a4a5472 authored by Simon Malesys's avatar Simon Malesys
Browse files

Merge branch 'dev'

parents f6fd5aad 6b697d8a
No related branches found
No related tags found
No related merge requests found
Pipeline #153328 passed with stages
in 7 minutes and 51 seconds
...@@ -63,9 +63,9 @@ spec: ...@@ -63,9 +63,9 @@ spec:
httpGet: httpGet:
path: /ready path: /ready
port: 3000 port: 3000
initialDelaySeconds: 30 initialDelaySeconds: 0
periodSeconds: 30 periodSeconds: 5
timeoutSeconds: 5 timeoutSeconds: 2
failureThreshold: 5 failureThreshold: 5
resources: resources:
requests: requests:
......
...@@ -69,5 +69,8 @@ export default Object.freeze({ ...@@ -69,5 +69,8 @@ export default Object.freeze({
}, },
archiveURL: (archiveName: string): string => { archiveURL: (archiveName: string): string => {
return `/api/downloads/${archiveName}` return `/api/downloads/${archiveName}`
},
databaseIsReady: async (): Promise<AxiosResponse<any>> => {
return await base.get('/api/readyDatabase')
} }
}) })
...@@ -14,14 +14,23 @@ ...@@ -14,14 +14,23 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted } from 'vue' import { onMounted } from 'vue'
import { useStore } from '../store'
import AppFooter from './AppFooter.vue' import AppFooter from './AppFooter.vue'
import AppHeader from './AppHeader.vue' import AppHeader from './AppHeader.vue'
import router from '../router'
import api from '../api'
import { AxiosError } from 'axios'
const store = useStore() onMounted(async () => {
await api.databaseIsReady()
onMounted(() => { .then()
store.getStatistics() .catch((err: AxiosError) => {
if (err.status === 503) {
router.replace({ name: 'WaitingPage' })
} else {
alert(err.message)
console.log(err)
}
})
}) })
</script> </script>
......
<template>
<h1>
<img
src="/absd-title.png"
alt="ABSD logo with title"
height="90"
width="325">
<span class="title-for-a11y">
ABSD - AntiBody Sequence database
</span>
</h1>
</template>
<script setup lang="ts"></script>
<style scoped>
.title-for-a11y {
display: none;
}
</style>
<template> <template>
<h1>404 - Not Found</h1> <AppTitle></AppTitle>
<p>The page or resource you requested doesn't seem to exist here...</p> <section>
<h2>404 - Not Found</h2>
<p>The page or resource you requested doesn't seem to exist here...</p>
<RouterLink :to="{ name: 'HomePage' }">
Go back home
</RouterLink>
</section>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import AppTitle from './AppTitle.vue'
</script> </script>
<style scoped> <style scoped>
section {
display: flex;
flex-flow: column;
align-items: center;
gap: var(--spacing);
}
h2,
p { p {
text-align: center; text-align: center;
} }
p { a, p {
font-size: 20px; font-size: 20px;
margin-top: 0;
} }
</style> </style>
<template> <template>
<h1> <AppTitle></AppTitle>
<img
src="/absd-title.png"
alt="ABSD logo with title"
height="90"
width="325"
>
<span class="title-for-a11y">
ABSD - AntiBody Sequence database
</span>
</h1>
<section class="page-description"> <section class="page-description">
<p> <p>
...@@ -141,6 +131,7 @@ ...@@ -141,6 +131,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted } from 'vue' import { computed, onMounted } from 'vue'
import { useStore } from '../store' import { useStore } from '../store'
import AppTitle from './AppTitle.vue'
import AntibodiesTable from './AntibodiesTable.vue' import AntibodiesTable from './AntibodiesTable.vue'
import NoResults from './NoResults.vue' import NoResults from './NoResults.vue'
import SearchBar from './SearchBar.vue' import SearchBar from './SearchBar.vue'
...@@ -154,8 +145,10 @@ const versionDate = computed(() => { return store.antibodiesVersionDate }) ...@@ -154,8 +145,10 @@ const versionDate = computed(() => { return store.antibodiesVersionDate })
* give the user a preview of what to expect after a search. * give the user a preview of what to expect after a search.
*/ */
onMounted((): void => { onMounted((): void => {
store.fetchAntibodies(store.request.fetchParams()) store.getStatistics().then(() => {
store.countAntibodies(store.request.countParams()) store.fetchAntibodies(store.request.fetchParams())
store.countAntibodies(store.request.countParams())
})
}) })
</script> </script>
......
<template>
<AppTitle></AppTitle>
<section>
<p>
The Database is currently being updated,
please come back in a few minutes.
</p>
<div class="spinner"></div>
</section>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue'
import AppTitle from './AppTitle.vue'
import api from '../api'
import { AxiosError } from 'axios'
import router from '../router'
/** ID of the interval timer started on page load */
let intervalID: number
/**
* After page load, check for database readiness immediately
* and every 5 seconds.
*/
onMounted(() => {
checkDatabaseIsReady()
intervalID = setInterval(checkDatabaseIsReady, 5000)
})
/**
* Remove the timer when leaving the page to ensure
* memory safety.
*/
onUnmounted(() => {
clearInterval(intervalID)
})
/**
* Check is the database is ready and reroute to
* the home page in that case.
*/
function checkDatabaseIsReady() {
api.databaseIsReady()
.then(() => {
router.replace({ name: 'HomePage' })
})
.catch((err: AxiosError) => {
if (err.status === 503) return
else {
alert(err.message)
console.log(err)
}
})
}
</script>
<style scoped>
section {
align-items: center;
display: flex;
flex-flow: column wrap;
gap: var(--spacing);
}
.spinner {
height: 50px;
width: 50px;
}
</style>
...@@ -51,6 +51,11 @@ export default createRouter({ ...@@ -51,6 +51,11 @@ export default createRouter({
component: async () => await import('./components/TheAboutPage.vue'), component: async () => await import('./components/TheAboutPage.vue'),
name: 'AboutPage' name: 'AboutPage'
}, },
{
path: '/waiting',
component: async () => await import('./components/TheWaitingPage.vue'),
name: 'WaitingPage'
},
{ {
path: '/:pathMatch(.*)*', path: '/:pathMatch(.*)*',
component: async () => await import('./components/The404Page.vue'), component: async () => await import('./components/The404Page.vue'),
......
...@@ -170,8 +170,8 @@ export const useStore = defineStore('store', { ...@@ -170,8 +170,8 @@ export const useStore = defineStore('store', {
* Get the database statistics from the server and stores it in the * Get the database statistics from the server and stores it in the
* corresponding variable in the store. * corresponding variable in the store.
*/ */
getStatistics(): void { getStatistics(): Promise<void> {
api.getStatistics().then(response => { return api.getStatistics().then(response => {
this.statistics = Object.freeze(response.data) this.statistics = Object.freeze(response.data)
}).catch((err: AxiosError) => { }).catch((err: AxiosError) => {
alert(err.message) alert(err.message)
......
...@@ -36,6 +36,7 @@ import downloadsFileRoute from './routes/downloadsFileRoute.js' ...@@ -36,6 +36,7 @@ import downloadsFileRoute from './routes/downloadsFileRoute.js'
import downloadsRoute from './routes/downloadsRoute.js' import downloadsRoute from './routes/downloadsRoute.js'
import healthCheckRoute from './routes/healthCheckRoute.js' import healthCheckRoute from './routes/healthCheckRoute.js'
import readyCheckRoute from './routes/readyCheckRoute.js' import readyCheckRoute from './routes/readyCheckRoute.js'
import readyDatabaseRoute from './routes/readyDatabaseRoute.js'
import statisticsRoute from './routes/statisticsRoute.js' import statisticsRoute from './routes/statisticsRoute.js'
/** /**
...@@ -86,9 +87,10 @@ fastify.register(fastifyMongo, { ...@@ -86,9 +87,10 @@ fastify.register(fastifyMongo, {
// Route controllers // Route controllers
// ========================================================================= // =========================================================================
// Decorate the server with a boolean marking when it is ready. // Decorate the server with booleans marking when resources are ready.
// Used by the readyCheck route. // Used by the readiness routes.
fastify.decorate('serverIsReady', false) fastify.decorate('serverIsReady', false)
fastify.decorate('databaseIsReady', true)
// Decorate the server with an object // Decorate the server with an object
// to store the workers while then run. // to store the workers while then run.
...@@ -101,7 +103,7 @@ fastify.addHook('onRequest', async (request) => { ...@@ -101,7 +103,7 @@ fastify.addHook('onRequest', async (request) => {
request.where = {} request.where = {}
}) })
// Register all controllers // Register all API controllers
fastify.route(antibodiesCountRoute) fastify.route(antibodiesCountRoute)
fastify.route(antibodiesDownloadArchiveFileRoute) fastify.route(antibodiesDownloadArchiveFileRoute)
fastify.route(antibodiesDownloadArchiveRoute) fastify.route(antibodiesDownloadArchiveRoute)
...@@ -114,6 +116,9 @@ fastify.route(antibodiesVGeneSegmentsRoute) ...@@ -114,6 +116,9 @@ fastify.route(antibodiesVGeneSegmentsRoute)
fastify.route(downloadsFileRoute) fastify.route(downloadsFileRoute)
fastify.route(downloadsRoute) fastify.route(downloadsRoute)
fastify.route(statisticsRoute) fastify.route(statisticsRoute)
fastify.route(readyDatabaseRoute)
// Register health checks
fastify.route(healthCheckRoute) fastify.route(healthCheckRoute)
fastify.route(readyCheckRoute) fastify.route(readyCheckRoute)
...@@ -164,15 +169,19 @@ fastify.listen({ ...@@ -164,15 +169,19 @@ fastify.listen({
host: envConfig.ABSD_SERVER_HOST, host: envConfig.ABSD_SERVER_HOST,
port: envConfig.ABSD_SERVER_PORT port: envConfig.ABSD_SERVER_PORT
}).then(async () => { }).then(async () => {
fastify.serverIsReady = true
fastify.log.info({ envConfig })
if (envConfig.NODE_ENV === 'production') { if (envConfig.NODE_ENV === 'production') {
fastify.databaseIsReady = false
return await buildDatabase() return await buildDatabase()
} }
}).then((dbResults) => { }).then((dbResults) => {
if (dbResults) { if (dbResults) {
fastify.log.info(`${envConfig.ABSD_DB_NAME} database created`) fastify.log.info(`${envConfig.ABSD_DB_NAME} database created`)
} }
fastify.log.info({ envConfig })
fastify.serverIsReady = true fastify.databaseIsReady = true
}).catch(err => { }).catch(err => {
fastify.log.error(err) fastify.log.error(err)
process.exit(1) process.exit(1)
......
// ABSD
// Copyright (C) 2025 Institut Pasteur
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
/**
* Route returning if the database is available or not.
* False when the database is inserting data.
*/
export default {
method: 'GET',
url: '/api/readyDatabase',
logLevel: 'warn',
handler: function (request, reply) {
if (this.databaseIsReady) {
return reply
.code(200)
.send({ ready: true })
}
return reply
.code(503)
.send({ ready: false })
}
}
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment