From f0e36f69ad474e3c327fc3e6863a90b6d3fd18e4 Mon Sep 17 00:00:00 2001 From: Remi PLANEL <rplanel@pasteur.fr> Date: Wed, 20 Mar 2024 09:36:31 +0100 Subject: [PATCH] Error msg when galaxy instance not running --- backend/analysis/api.py | 26 +++++++++- backend/analysis/models.py | 6 +-- deploy/charts/nuxt/templates/deployment.yaml | 4 +- frontend/components/AnalysisList.vue | 50 ++++++++++---------- frontend/components/ErrorAlert.vue | 36 ++++++++++++++ frontend/components/Main.vue | 2 - frontend/components/SubmitAnalysis.vue | 8 ++++ frontend/components/UppyGenomeUpload.vue | 8 ++-- frontend/composables/isGalaxyOffline.ts | 18 +++++++ frontend/composables/useFetchAnalyses.ts | 7 +-- frontend/error.vue | 2 + frontend/middleware/maintenance.global.ts | 6 --- frontend/pages/analyses/index.vue | 14 +----- frontend/pages/index.vue | 13 +---- frontend/plugins/api.ts | 1 + frontend/server/api/probe.get.ts | 3 ++ 16 files changed, 132 insertions(+), 72 deletions(-) create mode 100644 frontend/components/ErrorAlert.vue create mode 100644 frontend/composables/isGalaxyOffline.ts create mode 100644 frontend/server/api/probe.get.ts diff --git a/backend/analysis/api.py b/backend/analysis/api.py index a481fde..984063d 100644 --- a/backend/analysis/api.py +++ b/backend/analysis/api.py @@ -4,6 +4,9 @@ from django.conf import settings from ninja import Router, File from ninja.files import UploadedFile + +from django_to_galaxy.models import GalaxyInstance + from analysis.models import ( Analysis, AnalysisWorkflow, @@ -38,6 +41,14 @@ def csrf(request): return JsonResponse({"token": token}) +@router.get("is-galaxy-online") +def is_galaxy_online(request): + + gi = GalaxyInstance.objects.get(name=settings.GALAXY_INSTANCE_NAME) + + return JsonResponse({"is_online": gi.is_online()}) + + @router.get("login") def login(request): if not request.session.session_key: @@ -49,7 +60,7 @@ def login(request): ) -@router.post("add", response=AnalysisOutSchema) +@router.post("add", response={200: AnalysisOutSchema, 503: Error}) def add( request, genomefile: UploadedFile = File(...), @@ -71,6 +82,12 @@ def add( session = Session.objects.get(session_key=session_key) aw = AnalysisWorkflow.objects.get(galaxy_id=settings.GALAXY_WORKFLOW_ID) + + # check if Galaxy online + + is_galaxy_online = aw.analysis_owner.galaxy_instance.is_online() + if not is_galaxy_online: + return 503, {"message": "The Galaxy instance is offline"} input_files = [ genome_path, ] @@ -80,11 +97,16 @@ def add( return analysis -@router.get("/", response=list[AnalysisOutSchema]) +@router.get("/", response={200: list[AnalysisOutSchema], 503: Error}) def list(request): session_key = request.session.session_key if session_key: session = Session.objects.get(session_key=session_key) + gi = GalaxyInstance.objects.get(name=settings.GALAXY_INSTANCE_NAME) + # check if Galaxy online + is_galaxy_online = gi.is_online() + if not is_galaxy_online: + return 503, {"message": "The Galaxy instance is offline"} return Analysis.objects.filter(analysis_history__session=session) else: return [] diff --git a/backend/analysis/models.py b/backend/analysis/models.py index 6418f16..3793948 100644 --- a/backend/analysis/models.py +++ b/backend/analysis/models.py @@ -2,16 +2,14 @@ import csv import tarfile from typing import Any from django.db import models -from django_to_galaxy.utils import enabled_cache from django.contrib.sessions.models import Session from tempfile import mkstemp from django.conf import settings from Bio import SeqIO from pathlib import Path from rich.console import Console -from typing import List, Dict +from typing import Dict -console = Console() # from galaxy.models import WorkflowInvocation @@ -31,6 +29,8 @@ from .schemas import GeneEntry, HmmerEntry, ProteinEntry, SystemEntry # Create your models here. +console = Console() + class AnalysisGalaxyUser(GalaxyUser): def create_history(self, session, name=None): diff --git a/deploy/charts/nuxt/templates/deployment.yaml b/deploy/charts/nuxt/templates/deployment.yaml index ca6d250..38819ab 100644 --- a/deploy/charts/nuxt/templates/deployment.yaml +++ b/deploy/charts/nuxt/templates/deployment.yaml @@ -48,11 +48,11 @@ spec: value: {{ .Values.dfApiPrefix }} livenessProbe: httpGet: - path: / + path: /api/probe port: 3000 readinessProbe: httpGet: - path: / + path: /api/probe port: 3000 resources: {{- toYaml .Values.resources | nindent 12 }} diff --git a/frontend/components/AnalysisList.vue b/frontend/components/AnalysisList.vue index 8180e81..8ec5158 100644 --- a/frontend/components/AnalysisList.vue +++ b/frontend/components/AnalysisList.vue @@ -3,19 +3,13 @@ import { computed } from 'vue' import { usePolling } from "../composables/useFetchAnalyses"; import type { Analysis } from '~/types'; import { joinURL } from "ufo"; -// import { useIsGalaxyOffline } from '~/composables/isGalaxyOffline'; const runtimeConfig = useRuntimeConfig() const sessionExpiryDate = useState('sessionExpiryDate') const toEdit: Ref<number | null> = ref(null) const newTitle: Ref<string | null> = ref(null) - - - const sanitizedAnalyses = computed(() => { return data !== null ? data.value?.map(a => { - const createTimeDate = new Date(a.create_time) - return { ...a, create_time: createTimeDate.toLocaleString() @@ -29,13 +23,17 @@ function getResultArchiveUrl(analysisId: number) { async function deleteAnalysis(analysisId: number) { // if (csrfToken.value) { - const { error } = await useAPI<null>(`/analysis/${analysisId}`, - { - method: "DELETE", - } - ) - if (error.value) throw createError(`Cannot delete analysis ${analysisId}`) - refresh() + try { + await useNuxtApp().$api<null>(`/analysis/${analysisId}`, + { + method: "DELETE", + } + ) + await refreshNuxtData() + } catch (error) { + throw createError(`Cannot delete analysis ${analysisId}`) + } + } function editionMode(analysisId: number, name: string) { @@ -44,14 +42,14 @@ function editionMode(analysisId: number, name: string) { } async function editAnalysis(analysisId: number) { // if (csrfToken.value) { - await useAPI<null>(`/analysis/${analysisId}`, + await useNuxtApp().$api<null>(`/analysis/${analysisId}`, { method: "PUT", body: { name: newTitle.value }, onResponse() { - refresh().finally(() => { + refreshNuxtData().finally(() => { toEdit.value = null newTitle.value = null }) @@ -76,27 +74,31 @@ const options = computed(() => { } }) - - - const { data, refresh, pending, error } = await useAPI<Analysis[]>('/analysis/', { key: "analyses-list", }); - usePolling(options) - - if (error.value) { + console.log(error.value) const toValError = toValue(error) - if (toValError !== null) { + + if (toValError?.data) { throw createError({ statusCode: toValError.statusCode, - message: "Error while getting the list of analysis" - + statusMessage: toValError.statusMessage, + message: toValError?.message, + data: toValError?.data ?? "" }) + } else { + console.log(error.value) + throw createError("Unable to get the analysis") + } + + + // } } diff --git a/frontend/components/ErrorAlert.vue b/frontend/components/ErrorAlert.vue new file mode 100644 index 0000000..582fd1d --- /dev/null +++ b/frontend/components/ErrorAlert.vue @@ -0,0 +1,36 @@ +<script setup lang="ts"> +import { useIsGalaxyOffline } from '~/composables/isGalaxyOffline'; +import type { FetchError } from "ofetch" + +defineEmits(['clearError']) + +export interface Props { + error?: MaybeRef<Error | FetchError | null | undefined> +} +const props = withDefaults(defineProps<Props>(), { + error: undefined, +}); + +const { message, isGalaxyOffline } = useIsGalaxyOffline({ error: props.error }) + + + +</script> + +<template> + <v-alert type="error" variant="tonal" prominent> + <v-card flat color="transparent"> + <v-card-text v-if="isGalaxyOffline"> + {{ message }} + </v-card-text> + <v-card-text v-else> + {{ error }} + </v-card-text> + <v-card-actions> + <v-btn color="currentColor" prepend-icon="md:home" variant="text" @click="$emit('clearError')"> + Back home + </v-btn> + </v-card-actions> + </v-card> + </v-alert> +</template> \ No newline at end of file diff --git a/frontend/components/Main.vue b/frontend/components/Main.vue index 47949bd..9d21eae 100644 --- a/frontend/components/Main.vue +++ b/frontend/components/Main.vue @@ -8,8 +8,6 @@ export interface Props { const props = withDefaults(defineProps<Props>(), { fluid: true }); -const sessionExpiryDate = useState('sessionExpiryDate', () => "") -// const { csrfToken } = useCsrfToken() const maxWidth = ref(1300) const minWidth = ref(900) const computedMinWidth = computed(() => { diff --git a/frontend/components/SubmitAnalysis.vue b/frontend/components/SubmitAnalysis.vue index f50afc0..b538d0f 100644 --- a/frontend/components/SubmitAnalysis.vue +++ b/frontend/components/SubmitAnalysis.vue @@ -14,6 +14,14 @@ const { meta, handleSubmit } = useForm({ const files = useField("files"); +const { data, error } = await useAPI<{ is_online: boolean }>("/analysis/is-galaxy-online", { + onRequest({ request }) { + } +}) + +if (data.value !== null && !data.value.is_online) { + throw createError("The galaxy instance is offline") +} </script> <template> diff --git a/frontend/components/UppyGenomeUpload.vue b/frontend/components/UppyGenomeUpload.vue index 76d380c..848b5b9 100644 --- a/frontend/components/UppyGenomeUpload.vue +++ b/frontend/components/UppyGenomeUpload.vue @@ -53,10 +53,10 @@ const uppyCovar = new Uppy({ const emitObject = (files.length === 0) ? undefined : files emit('update:modelValue', emitObject) }) - .on("error", (error) => { - console.log(error) - throw createError("uppy error") - }); + // .on("error", (error) => { + // console.log(error) + // throw createError("uppy error") + // }); const themeUppy = computed(() => { diff --git a/frontend/composables/isGalaxyOffline.ts b/frontend/composables/isGalaxyOffline.ts new file mode 100644 index 0000000..6dbb86a --- /dev/null +++ b/frontend/composables/isGalaxyOffline.ts @@ -0,0 +1,18 @@ +import type { FetchError } from "ofetch" + +export const useIsGalaxyOffline = ({ error }: { + error: MaybeRef<Error | FetchError | null | undefined> +}) => { + const message = ref<string>("The Galaxy instance used to run the analyses is currently offline. It is probably a maintenance ") + const isGalaxyOffline = ref<boolean>(false) + const toValError = toValue(error) + console.dir(toValError) + if (toValError && "data" in toValError) { + if (toValError?.statusCode === 503 && toValError.data?.message === "The Galaxy instance is offline") { + isGalaxyOffline.value = true + } + + } + + return { message, isGalaxyOffline } +} diff --git a/frontend/composables/useFetchAnalyses.ts b/frontend/composables/useFetchAnalyses.ts index ee4ee5c..bfac4b2 100644 --- a/frontend/composables/useFetchAnalyses.ts +++ b/frontend/composables/useFetchAnalyses.ts @@ -1,18 +1,13 @@ import { useIntervalFn } from "@vueuse/core"; import { NON_TERMINAL_STATE, type JobState, type AnalysesPollingOptions, type Analysis } from "~/types"; - - - - -export function usePolling( options: MaybeRef<AnalysesPollingOptions> = { interval: 5000, refresh: null, data: ref(null), pending: ref(false) }) { +export function usePolling(options: MaybeRef<AnalysesPollingOptions> = { interval: 5000, refresh: null, data: ref(null), pending: ref(false) }) { const nonTerminalState = computed(() => { return new Set<JobState>(NON_TERMINAL_STATE) }) const { interval, refresh, pending, data } = toValue(options) const { pause, resume, isActive } = useIntervalFn(() => { - console.log("je suis dans le polling") if (!pending.value) { console.log( `pending ${pending.value diff --git a/frontend/error.vue b/frontend/error.vue index a42e825..645b210 100644 --- a/frontend/error.vue +++ b/frontend/error.vue @@ -1,4 +1,5 @@ <script setup lang="ts"> + const error = useError() function handleError() { clearError({ redirect: '/' }) @@ -7,6 +8,7 @@ function handleError() { <template> <div> <NuxtLayout> + <!-- <div v-if="isGalaxyOffline">galaxy offfline</div> --> <v-alert type="error" variant="tonal" prominent> <v-card flat color="transparent"> <v-card-title v-if="error && error?.statusCode"> diff --git a/frontend/middleware/maintenance.global.ts b/frontend/middleware/maintenance.global.ts index 4e10edf..e1469c6 100644 --- a/frontend/middleware/maintenance.global.ts +++ b/frontend/middleware/maintenance.global.ts @@ -7,10 +7,4 @@ export default defineNuxtRouteMiddleware((to, from) => { if (isMaintenance && to.name !== "maintenance") { return navigateTo('/maintenance') } - // else { return } - - - - - }) diff --git a/frontend/pages/analyses/index.vue b/frontend/pages/analyses/index.vue index beda58b..dcf62b3 100644 --- a/frontend/pages/analyses/index.vue +++ b/frontend/pages/analyses/index.vue @@ -27,19 +27,9 @@ function resetError(error) { <v-card flat color="transparent"> <v-breadcrumbs :items="breadcrumbItems"></v-breadcrumbs> <NuxtErrorBoundary> - <analysis-list> </analysis-list> + <AnalysisList></AnalysisList> <template #error="{ error }"> - <v-alert type="error" variant="tonal" prominent> - <v-card flat color="transparent"> - <v-card-text> - {{ error }} - </v-card-text> - <v-card-actions> - <v-btn color="currentColor" prepend-icon="md:home" variant="outlined" - @click="resetError(error)">Back home</v-btn> - </v-card-actions> - </v-card> - </v-alert> + <ErrorAlert :error="error" @clear-error="resetError(error)"></ErrorAlert> </template> </NuxtErrorBoundary> </v-card> diff --git a/frontend/pages/index.vue b/frontend/pages/index.vue index b9ccb96..82f4e13 100644 --- a/frontend/pages/index.vue +++ b/frontend/pages/index.vue @@ -82,17 +82,8 @@ function resetError(error) { <NuxtErrorBoundary> <SubmitAnalysis></SubmitAnalysis> <template #error="{ error }"> - <v-alert type="error" variant="tonal" prominent> - <v-card flat color="transparent"> - <v-card-text> - {{ error }} - </v-card-text> - <v-card-actions> - <v-btn color="currentColor" prepend-icon="md:home" variant="outlined" @click="resetError(error)">Back - home</v-btn> - </v-card-actions> - </v-card> - </v-alert></template> + <ErrorAlert :error="error"></ErrorAlert> + </template> </NuxtErrorBoundary> </v-card> </v-col> diff --git a/frontend/plugins/api.ts b/frontend/plugins/api.ts index 8d66b65..57f7964 100644 --- a/frontend/plugins/api.ts +++ b/frontend/plugins/api.ts @@ -5,6 +5,7 @@ export default defineNuxtPlugin(() => { const $api = $fetch.create({ baseURL: "/dfapi", async onRequest({ request, options, error }) { + console.log("dans le on request du create") const { data } = await useFetch("/api/auth", { headers }) const toValToken = toValue(csrfToken) if (toValToken !== undefined) { diff --git a/frontend/server/api/probe.get.ts b/frontend/server/api/probe.get.ts new file mode 100644 index 0000000..a5cd75e --- /dev/null +++ b/frontend/server/api/probe.get.ts @@ -0,0 +1,3 @@ +export default defineEventHandler((event) => { + return 'ok' +}) -- GitLab