Skip to content

Commit 234d7fb

Browse files
authored
Merge pull request #1729 from microbiomedata/issue-1707-embed-krona-plots
Make Krona plots viewable in data portal
2 parents 23958a3 + 89976a6 commit 234d7fb

File tree

4 files changed

+163
-3
lines changed

4 files changed

+163
-3
lines changed

nmdc_server/api.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,25 @@ async def download_data_object(
616616
}
617617

618618

619+
@router.get("/data_object/{data_object_id}/get_html_content_url")
620+
async def get_data_object_html_content(data_object_id: str, db: Session = Depends(get_db)):
621+
data_object = crud.get_data_object(db, data_object_id)
622+
if data_object is None:
623+
raise HTTPException(status_code=404, detail="DataObject not found")
624+
url = data_object.url
625+
if url is None:
626+
raise HTTPException(status_code=404, detail="DataObject has no url reference")
627+
if data_object.file_type in [
628+
"Kraken2 Krona Plot",
629+
"GOTTCHA2 Krona Plot",
630+
"Centrifuge Krona Plot",
631+
]:
632+
return {
633+
"url": url,
634+
}
635+
raise HTTPException(status_code=400, detail="DataObject has no relevant HTML content")
636+
637+
619638
@router.post(
620639
"/data_object/workflow_summary",
621640
response_model=schemas.DataObjectAggregation,

tests/test_download.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import pytest
12
from fastapi.testclient import TestClient
23
from sqlalchemy.orm.session import Session
34

@@ -130,3 +131,42 @@ def test_generate_bulk_download_filtered(
130131
# Verify that the bulk download cannot be accessed a second time
131132
resp = client.get(f"/api/bulk_download/{id_}")
132133
assert resp.status_code == 410
134+
135+
136+
@pytest.mark.parametrize(
137+
("data_object_type", "expected_status_code"), [("Kraken2 Krona Plot", 200), ("foo", 400)]
138+
)
139+
def test_get_url_for_html_content_unauthenticated(
140+
db: Session,
141+
client: TestClient,
142+
data_object_type: str,
143+
expected_status_code: int,
144+
):
145+
data_object = fakes.DataObjectFactory(
146+
url="https://data.microbiomedata.org/data/dob",
147+
workflow_type=WorkflowActivityTypeEnum.metagenome_assembly.value,
148+
file_type=data_object_type,
149+
)
150+
db.commit()
151+
resp = client.get(f"/api/data_object/{data_object.id}/get_html_content_url")
152+
assert resp.status_code == expected_status_code
153+
154+
155+
@pytest.mark.parametrize(
156+
("data_object_type", "expected_status_code"), [("Kraken2 Krona Plot", 200), ("foo", 400)]
157+
)
158+
def test_get_url_for_html_content_authenticated(
159+
db: Session,
160+
client: TestClient,
161+
logged_in_user,
162+
data_object_type: str,
163+
expected_status_code: int,
164+
):
165+
data_object = fakes.DataObjectFactory(
166+
url="https://data.microbiomedata.org/data/dob",
167+
workflow_type=WorkflowActivityTypeEnum.metagenome_assembly.value,
168+
file_type=data_object_type,
169+
)
170+
db.commit()
171+
resp = client.get(f"/api/data_object/{data_object.id}/get_html_content_url")
172+
assert resp.status_code == expected_status_code

web/src/components/DataObjectTable.vue

Lines changed: 98 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,18 @@
22
// @ts-ignore
33
import NmdcSchema from 'nmdc-schema/nmdc_schema/nmdc_materialized_patterns.yaml';
44
import {
5-
computed, defineComponent, PropType, reactive,
5+
computed, defineComponent, PropType, reactive, ref, Ref, watch,
66
} from '@vue/composition-api';
77
import { flattenDeep } from 'lodash';
88
99
import { DataTableHeader } from 'vuetify';
1010
import { humanFileSize } from '@/data/utils';
11-
import { client, BiosampleSearchResult, OmicsProcessingResult } from '@/data/api';
11+
import {
12+
client,
13+
BiosampleSearchResult,
14+
OmicsProcessingResult,
15+
api,
16+
} from '@/data/api';
1217
import { stateRefs, acceptTerms } from '@/store';
1318
import { metaproteomicCategoryEnumToDisplay } from '@/encoding';
1419
@@ -87,6 +92,33 @@ export default defineComponent({
8792
},
8893
];
8994
95+
const selectedHtmlDataObject: Ref<any | null> = ref(null);
96+
const dataModal = ref(false);
97+
const iframeDataSource = ref('');
98+
const iframeLoading = ref(false);
99+
function hasHtmlData(fileType: string) {
100+
return [
101+
'Kraken2 Krona Plot',
102+
'GOTTCHA2 Krona Plot',
103+
'Centrifuge Krona Plot',
104+
].includes(fileType);
105+
}
106+
async function openHtmlDataModal(item: any) {
107+
iframeLoading.value = true;
108+
dataModal.value = true;
109+
iframeDataSource.value = await api.getDataObjectHtmlContentUrl(item.id);
110+
selectedHtmlDataObject.value = item;
111+
}
112+
watch(dataModal, () => {
113+
if (!dataModal.value) {
114+
selectedHtmlDataObject.value = null;
115+
iframeDataSource.value = '';
116+
}
117+
});
118+
function onIframeLoaded() {
119+
iframeLoading.value = false;
120+
}
121+
90122
const termsDialog = reactive({
91123
item: null as null | OmicsProcessingResult,
92124
value: false,
@@ -186,6 +218,13 @@ export default defineComponent({
186218
}
187219
188220
return {
221+
dataModal,
222+
iframeLoading,
223+
hasHtmlData,
224+
openHtmlDataModal,
225+
selectedHtmlDataObject,
226+
iframeDataSource,
227+
onIframeLoaded,
189228
onAcceptTerms,
190229
handleDownload,
191230
descriptionMap,
@@ -212,6 +251,52 @@ export default defineComponent({
212251
@clicked="onAcceptTerms"
213252
/>
214253
</v-dialog>
254+
<v-dialog
255+
v-if="selectedHtmlDataObject !== null"
256+
v-model="dataModal"
257+
scroll-strategy="block"
258+
width="80vw"
259+
scrollable
260+
>
261+
<v-card
262+
class="d-flex flex-column"
263+
width="100%"
264+
height="80vh"
265+
>
266+
<v-card-text class="d-flex flex-grow-1">
267+
<span
268+
v-if="iframeLoading"
269+
class="d-flex align-center justify-center flex-grow-1"
270+
>
271+
<v-progress-circular
272+
class="mr-2"
273+
color="primary"
274+
size="24"
275+
indeterminate
276+
/>
277+
Loading...
278+
</span>
279+
<iframe
280+
:title="selectedHtmlDataObject.description || 'Data object HTML content'"
281+
:src="iframeDataSource"
282+
:style="{border: 'none' }"
283+
:width="iframeLoading ? '0%' : '100%'"
284+
class="pa-4"
285+
loading="lazy"
286+
@load="onIframeLoaded"
287+
/>
288+
</v-card-text>
289+
<v-card-actions class="flex-row">
290+
<v-spacer />
291+
<v-btn
292+
text
293+
@click="dataModal = false"
294+
>
295+
Close
296+
</v-btn>
297+
</v-card-actions>
298+
</v-card>
299+
</v-dialog>
215300
<v-data-table
216301
:headers="headers"
217302
:items="items"
@@ -282,7 +367,17 @@ export default defineComponent({
282367
<span>This file is included in the currently selected bulk download</span>
283368
</v-tooltip>
284369
</td>
285-
<td>{{ item.file_type }}</td>
370+
<td>
371+
{{ item.file_type }}
372+
<v-btn
373+
v-if="hasHtmlData(item.file_type)"
374+
color="primary"
375+
icon
376+
@click="openHtmlDataModal(item)"
377+
>
378+
<v-icon>mdi-magnify</v-icon>
379+
</v-btn>
380+
</td>
286381
<td>{{ item.file_type_description }}</td>
287382
<td>{{ humanFileSize(item.file_size_bytes) }}</td>
288383
<td>{{ item.downloads }}</td>

web/src/data/api.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -641,6 +641,11 @@ async function createBulkDownload(conditions: Condition[], dataObjectFilter: Dat
641641
};
642642
}
643643

644+
async function getDataObjectHtmlContentUrl(dataObjectId: string): Promise<string> {
645+
const { data } = await client.get<{ url: string }>(`data_object/${dataObjectId}/get_html_content_url`);
646+
return data.url;
647+
}
648+
644649
export interface KeggTermSearchResponse {
645650
term: string;
646651
text: string;
@@ -846,6 +851,7 @@ client.interceptors.response.use(undefined, async (error: AxiosError) => {
846851

847852
const api = {
848853
createBulkDownload,
854+
getDataObjectHtmlContentUrl,
849855
getBinnedFacet,
850856
getBulkDownloadSummary,
851857
getBulkDownloadAggregateSummary,

0 commit comments

Comments
 (0)