From 3cb10394a3a59b8b0d5ac7fc5e268a0d4db70f41 Mon Sep 17 00:00:00 2001 From: Simon Malesys <simon.malesys@pasteur.fr> Date: Thu, 27 Mar 2025 11:39:08 +0100 Subject: [PATCH] Add a waiting page for when the database is not ready yet --- src/client/api.ts | 3 + src/client/components/App.vue | 19 +++++-- src/client/components/AppTitle.vue | 20 +++++++ src/client/components/The404Page.vue | 22 +++++++- src/client/components/TheHomePage.vue | 14 +---- src/client/components/TheWaitingPage.vue | 72 ++++++++++++++++++++++++ src/client/router.ts | 5 ++ src/server/app.js | 13 ++++- src/server/routes/readyDatabaseRoute.js | 36 ++++++++++++ 9 files changed, 182 insertions(+), 22 deletions(-) create mode 100644 src/client/components/AppTitle.vue create mode 100644 src/client/components/TheWaitingPage.vue create mode 100644 src/server/routes/readyDatabaseRoute.js diff --git a/src/client/api.ts b/src/client/api.ts index 666d8989..51565650 100644 --- a/src/client/api.ts +++ b/src/client/api.ts @@ -69,5 +69,8 @@ export default Object.freeze({ }, archiveURL: (archiveName: string): string => { return `/api/downloads/${archiveName}` + }, + databaseIsReady: async (): Promise<AxiosResponse<any>> => { + return await base.get('/api/readyDatabase') } }) diff --git a/src/client/components/App.vue b/src/client/components/App.vue index 6503fb9a..79657acf 100644 --- a/src/client/components/App.vue +++ b/src/client/components/App.vue @@ -14,14 +14,23 @@ <script setup lang="ts"> import { onMounted } from 'vue' -import { useStore } from '../store' import AppFooter from './AppFooter.vue' import AppHeader from './AppHeader.vue' +import router from '../router' +import api from '../api' +import { AxiosError } from 'axios' -const store = useStore() - -onMounted(() => { - store.getStatistics() +onMounted(async () => { + await api.databaseIsReady() + .then() + .catch((err: AxiosError) => { + if (err.status === 503) { + router.replace({ name: 'WaitingPage' }) + } else { + alert(err.message) + console.log(err) + } + }) }) </script> diff --git a/src/client/components/AppTitle.vue b/src/client/components/AppTitle.vue new file mode 100644 index 00000000..8eaedf77 --- /dev/null +++ b/src/client/components/AppTitle.vue @@ -0,0 +1,20 @@ +<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> diff --git a/src/client/components/The404Page.vue b/src/client/components/The404Page.vue index b7e40624..144831c7 100644 --- a/src/client/components/The404Page.vue +++ b/src/client/components/The404Page.vue @@ -1,17 +1,33 @@ <template> - <h1>404 - Not Found</h1> - <p>The page or resource you requested doesn't seem to exist here...</p> + <AppTitle></AppTitle> + <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> <script setup lang="ts"> +import AppTitle from './AppTitle.vue' </script> <style scoped> +section { + display: flex; + flex-flow: column; + align-items: center; + gap: var(--spacing); +} + +h2, p { text-align: center; } -p { +a, p { font-size: 20px; + margin-top: 0; } </style> diff --git a/src/client/components/TheHomePage.vue b/src/client/components/TheHomePage.vue index d81ccf51..8f1d92d5 100644 --- a/src/client/components/TheHomePage.vue +++ b/src/client/components/TheHomePage.vue @@ -1,15 +1,5 @@ <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> + <AppTitle></AppTitle> <section class="page-description"> <p> @@ -141,6 +131,7 @@ <script setup lang="ts"> import { computed, onMounted } from 'vue' import { useStore } from '../store' +import AppTitle from './AppTitle.vue' import AntibodiesTable from './AntibodiesTable.vue' import NoResults from './NoResults.vue' import SearchBar from './SearchBar.vue' @@ -154,6 +145,7 @@ const versionDate = computed(() => { return store.antibodiesVersionDate }) * give the user a preview of what to expect after a search. */ onMounted((): void => { + store.getStatistics() store.fetchAntibodies(store.request.fetchParams()) store.countAntibodies(store.request.countParams()) }) diff --git a/src/client/components/TheWaitingPage.vue b/src/client/components/TheWaitingPage.vue new file mode 100644 index 00000000..eaf174bb --- /dev/null +++ b/src/client/components/TheWaitingPage.vue @@ -0,0 +1,72 @@ +<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> diff --git a/src/client/router.ts b/src/client/router.ts index 6636662a..0e6d020b 100644 --- a/src/client/router.ts +++ b/src/client/router.ts @@ -51,6 +51,11 @@ export default createRouter({ component: async () => await import('./components/TheAboutPage.vue'), name: 'AboutPage' }, + { + path: '/waiting', + component: async () => await import('./components/TheWaitingPage.vue'), + name: 'WaitingPage' + }, { path: '/:pathMatch(.*)*', component: async () => await import('./components/The404Page.vue'), diff --git a/src/server/app.js b/src/server/app.js index ac0adf58..c5cd4bf1 100644 --- a/src/server/app.js +++ b/src/server/app.js @@ -36,6 +36,7 @@ import downloadsFileRoute from './routes/downloadsFileRoute.js' import downloadsRoute from './routes/downloadsRoute.js' import healthCheckRoute from './routes/healthCheckRoute.js' import readyCheckRoute from './routes/readyCheckRoute.js' +import readyDatabaseRoute from './routes/readyDatabaseRoute.js' import statisticsRoute from './routes/statisticsRoute.js' /** @@ -86,9 +87,10 @@ fastify.register(fastifyMongo, { // Route controllers // ========================================================================= -// Decorate the server with a boolean marking when it is ready. -// Used by the readyCheck route. +// Decorate the server with booleans marking when resources are ready. +// Used by the readiness routes. fastify.decorate('serverIsReady', false) +fastify.decorate('databaseIsReady', true) // Decorate the server with an object // to store the workers while then run. @@ -101,7 +103,7 @@ fastify.addHook('onRequest', async (request) => { request.where = {} }) -// Register all controllers +// Register all API controllers fastify.route(antibodiesCountRoute) fastify.route(antibodiesDownloadArchiveFileRoute) fastify.route(antibodiesDownloadArchiveRoute) @@ -114,6 +116,9 @@ fastify.route(antibodiesVGeneSegmentsRoute) fastify.route(downloadsFileRoute) fastify.route(downloadsRoute) fastify.route(statisticsRoute) +fastify.route(readyDatabaseRoute) + +// Register health checks fastify.route(healthCheckRoute) fastify.route(readyCheckRoute) @@ -165,6 +170,7 @@ fastify.listen({ port: envConfig.ABSD_SERVER_PORT }).then(async () => { if (envConfig.NODE_ENV === 'production') { + fastify.databaseIsReady = false return await buildDatabase() } }).then((dbResults) => { @@ -173,6 +179,7 @@ fastify.listen({ } fastify.log.info({ envConfig }) fastify.serverIsReady = true + fastify.databaseIsReady = true }).catch(err => { fastify.log.error(err) process.exit(1) diff --git a/src/server/routes/readyDatabaseRoute.js b/src/server/routes/readyDatabaseRoute.js new file mode 100644 index 00000000..9998ae59 --- /dev/null +++ b/src/server/routes/readyDatabaseRoute.js @@ -0,0 +1,36 @@ +// 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 }) + } +} -- GitLab