diff --git a/frontend/composables/useDownloadBlob.ts b/frontend/composables/useDownloadBlob.ts new file mode 100644 index 0000000000000000000000000000000000000000..7ae7b07beab9547b5e2e9d6ae992271f03a01a1e --- /dev/null +++ b/frontend/composables/useDownloadBlob.ts @@ -0,0 +1,14 @@ +export function useDownloadBlob() { + function download(blob: MaybeRef<Blob>, filename: MaybeRef<string>) { + const toValueBlob = toValue(blob) + const toValueFilename = toValue(filename) + var a = document.createElement("a"); + a.href = URL.createObjectURL(toValueBlob); + a.download = toValueFilename; + a.click(); + URL.revokeObjectURL(a.href); + } + + return { download } + +} \ No newline at end of file diff --git a/frontend/composables/useRasterize.ts b/frontend/composables/useRasterize.ts new file mode 100644 index 0000000000000000000000000000000000000000..02d9ac80115e764f0cdb3813cfdde489ff71117d --- /dev/null +++ b/frontend/composables/useRasterize.ts @@ -0,0 +1,44 @@ +import { useSerialize } from './useSerialize'; +import { useSvgPlot } from './useSvgPlot'; +const { serialize } = useSerialize() + +export function useRasterize() { + + function rasterize(component: MaybeRef<ComponentPublicInstance | null>, filename: MaybeRef<string>) { + const toValueCompo = toValue(component) + + if (toValueCompo !== null) { + const { svg } = useSvgPlot(toValueCompo) + const toValueSvg = toValue(svg) + console.log(toValueSvg) + if (toValueSvg !== null) { + let resolve, reject; + const promise: Promise<Blob> = new Promise((y, n) => (resolve = y, reject = n)); + const image = new Image; + image.onerror = reject; + + image.onload = () => { + const rect = toValueSvg.getBoundingClientRect(); + const canvas = document.createElement("canvas"); + canvas.width = rect.width + canvas.height = rect.height + const ctx = canvas.getContext("2d") + + + if (ctx !== null) { + ctx.drawImage(image, 0, 0, rect.width, rect.height); + ctx.canvas.toBlob(resolve); + } + } + const blob = toValue(serialize(component)) + if (blob !== undefined) { + image.src = URL.createObjectURL(blob); + console.log(image.src) + } + return promise; + } + } + } + return { rasterize } + +} \ No newline at end of file diff --git a/frontend/composables/useSerialize.ts b/frontend/composables/useSerialize.ts new file mode 100644 index 0000000000000000000000000000000000000000..47bcbc451f39d721fb4e6527bb2f77303c66bbad --- /dev/null +++ b/frontend/composables/useSerialize.ts @@ -0,0 +1,37 @@ +import { useSvgPlot } from './useSvgPlot'; + + +export function useSerialize() { + const xmlns = ref("http://www.w3.org/2000/xmlns/"); + const xlinkns = ref("http://www.w3.org/1999/xlink"); + const svgns = ref("http://www.w3.org/2000/svg"); + const blob = ref<Blob>() + + function serialize(compo: MaybeRef<ComponentPublicInstance | null>) { + const toValueCompo = toValue(compo) + if (toValueCompo !== null) { + const { svg } = useSvgPlot(toValueCompo) + const toValueSvg = toValue(svg) + if (toValueSvg !== null) { + const clonedSvg = toValueSvg.cloneNode(true); + const fragment = window.location.href + "#"; + const walker = document.createTreeWalker(toValueSvg, NodeFilter.SHOW_ELEMENT); + while (walker.nextNode()) { + for (const attr of walker.currentNode.attributes) { + if (attr.value.includes(fragment)) { + attr.value = attr.value.replace(fragment, "#"); + } + } + } + clonedSvg.setAttributeNS(xmlns.value, "xmlns", svgns.value); + clonedSvg.setAttributeNS(xmlns.value, "xmlns:xlink", xlinkns.value); + const serializer = new window.XMLSerializer; + const string = serializer.serializeToString(clonedSvg); + blob.value = new Blob([string], { type: "image/svg+xml" }); + return blob + } + else { return undefined } + } + } + return { serialize } +} \ No newline at end of file diff --git a/frontend/composables/useSvgPlot.ts b/frontend/composables/useSvgPlot.ts new file mode 100644 index 0000000000000000000000000000000000000000..b2375d3a89bacfd8b7cc66451c2653fbdee38a91 --- /dev/null +++ b/frontend/composables/useSvgPlot.ts @@ -0,0 +1,9 @@ +export function useSvgPlot(component: MaybeRef<ComponentPublicInstance>) { + const svg = ref<SVGElement | null>(null) + + const toValueCompo = toValue(component) + const rootElem = toValueCompo.$el + svg.value = rootElem.querySelector("svg") + console.log(svg.value) + return { svg } +} \ No newline at end of file diff --git a/frontend/nuxt.config.ts b/frontend/nuxt.config.ts index 84546c275c099b2506e6bd0a207fd5444f8be64a..46bcdc729132107c84a60172f4db4c6bea74c71b 100644 --- a/frontend/nuxt.config.ts +++ b/frontend/nuxt.config.ts @@ -40,11 +40,11 @@ export default defineNuxtConfig({ defaultLocale: 'en', // not needed if you have @nuxtjs/i18n installed }, security: { - // csrf: { - // // https: false, - // addCsrfTokenToEventCtx: true, - // cookieKey: 'csrftoken', - // } + headers: { + contentSecurityPolicy: { + "img-src": ["'self'", "data:", "blob:"] + } + } }, vuetify: { vuetifyOptions: { diff --git a/frontend/pages/analyses/[analysisId].vue b/frontend/pages/analyses/[analysisId].vue index bf017bb4fa8e175e1cf2f277e1c1749ac29ca06a..43bcdb5225dbd3ed494924104ebeda752a04ddae 100644 --- a/frontend/pages/analyses/[analysisId].vue +++ b/frontend/pages/analyses/[analysisId].vue @@ -7,12 +7,18 @@ import { useRoute, computed } from "#imports"; import * as d3 from "d3"; import { useElementSize } from '@vueuse/core' import { joinURL } from "ufo"; +import type { ComponentPublicInstance } from 'vue' +import { useSerialize } from '~/composables/useSerialize'; +import { useRasterize } from '~/composables/useRasterize'; +import { useDownloadBlob } from '~/composables/useDownloadBlob'; const route = useRoute(); const { selectedProtein } = useSelectedProtein() const runtimeConfig = useRuntimeConfig() - +const { serialize } = useSerialize() +const { rasterize } = useRasterize() +const { download } = useDownloadBlob() const analysisId = computed(() => { if (Array.isArray(route.params.analysisId)) return null @@ -164,7 +170,18 @@ const computedWidth = computed(() => { +function downloadSvg(component: ComponentPublicInstance | null, filename: string) { + const blob = toValue(serialize(toValue(component))) + if (blob !== undefined) { + download(blob, filename) + } +} +async function downloadPng(component: ComponentPublicInstance | null, filename: string) { + const blob = await rasterize(toValue(component), filename)?.then((blob) => { + download(blob, filename) + }) +} watch(selectedProtein, () => { const toValHitId = toValue(selectedProtein) @@ -191,7 +208,8 @@ const minRange = ref(0) const maxRange = ref(innerWidth.value) -const svgRef = ref(null) +const svgRef = ref<ComponentPublicInstance | null>(null) +const figureRef = ref<ComponentPublicInstance | null>(null) const domain = ref([0, 10000]) // const range = ref() @@ -443,8 +461,31 @@ useHead({ <v-card> <v-toolbar density="compact" class="pr-2"> <v-toolbar-title>{{ analysis.name }}</v-toolbar-title> - <v-btn color="primary" prepend-icon="mdi-download" :href="getResultArchiveUrl(analysis.id)">Download - all results</v-btn> + <v-menu> + <template v-slot:activator="{ props }"> + <v-btn color="primary" prepend-icon="md:download" class="mr-2" v-bind="props"> + Export + </v-btn> + </template> + <v-list> + <v-list-item v-if="analysis !== null" prepend-icon="mdi-archive" value="archive"> + <v-list-item-title @click="getResultArchiveUrl(analysis.id)">Download all results + </v-list-item-title> + </v-list-item> + <v-divider></v-divider> + <v-list-subheader>Images</v-list-subheader> + <v-list-item prepend-icon="mdi-svg" value="svg"> + <v-list-item-title @click="downloadSvg(figureRef, `df-results-${analysis.name}.svg`)">to + svg</v-list-item-title> + + </v-list-item> + <v-list-item prepend-icon="mdi-image" value="png"> + + <v-list-item-title @click="downloadPng(figureRef, `df-results-${analysis.name}.png`)">to + png</v-list-item-title> + </v-list-item> + </v-list> + </v-menu> <v-chip color="primary" rounded>{{ new Date(analysis.create_time).toLocaleString() }}</v-chip> <template v-if="analysis.percentage_done !== 100 && analysis.stderr === ''" #extension> <v-row> @@ -471,7 +512,7 @@ useHead({ <template v-else> <v-card-text> <div ref="gbContainer"> - <v-card flat color="transparent"> + <v-card ref="figureRef" flat color="transparent"> <v-toolbar variant="flat" density="compact" color="transparent"> <v-spacer></v-spacer><v-toolbar-items> <v-switch v-model="displayHmmerHits" color="primary" label="Display HMM-only hits diff --git a/frontend/pages/analyses/[analysisId]/genes.vue b/frontend/pages/analyses/[analysisId]/genes.vue index 1841eddbbc4135bbd7821b5f62d145f2b861bf5e..49f4567e3472fb718090380532a768616e2fd9a2 100644 --- a/frontend/pages/analyses/[analysisId]/genes.vue +++ b/frontend/pages/analyses/[analysisId]/genes.vue @@ -50,11 +50,11 @@ const sanitizedGenes = computed(() => { </script> <template> <AnalysisResultDataTable v-if="sanitizedGenes.length > 0" :items="sanitizedGenes" :headers="headers" /> - <v-card v-else flat color="transparent"> - <v-card-text> - <v-alert type="info" variant="tonal"> - No gene found - </v-alert> - </v-card-text> - </v-card> + <v-card v-else flat color="transparent"> + <v-card-text> + <v-alert type="info" variant="tonal"> + No gene found + </v-alert> + </v-card-text> + </v-card> </template> diff --git a/frontend/pages/analyses/[analysisId]/systems.vue b/frontend/pages/analyses/[analysisId]/systems.vue index e8e45cc0b1bfdf79f2285c25c622fa9c1f24c8e7..8f3d8fc018f199d6a06e637dc4a897558dc8dc69 100644 --- a/frontend/pages/analyses/[analysisId]/systems.vue +++ b/frontend/pages/analyses/[analysisId]/systems.vue @@ -8,9 +8,6 @@ const { data: systems, error } = await useAPI<SystemsOut>( `/analysis/${route.params.analysisId}/systems` ); - - - if (error.value) { throw createError({ message: `Error while getting the list systems for analysis ${route.params.analysisId}` }) }