Skip to content

Commit

Permalink
Merge pull request #238 from wtsi-npg/devel
Browse files Browse the repository at this point in the history
Release 2.3.0
  • Loading branch information
nerdstrike authored Jul 30, 2024
2 parents 828ce4d + 72312f2 commit 797e5ff
Show file tree
Hide file tree
Showing 33 changed files with 1,143 additions and 244 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]

## [2.3.0] - 2024-07-30

### Added

* New endpoint for fetching statistics about a library pool
* Pool composition table added to the QC View for any well. Folded away by default
* Barcode names from MLWH are now available to front and back end code

### Fixed

* Well detail fetching errors now produce a visible warning that was previously lost

## [2.2.0] - 2024-06-11

### Added
Expand Down
2 changes: 1 addition & 1 deletion frontend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,4 @@ COPY ./vite.config.js /code/longue_vue/vite.config.js
COPY ./index.html /code/longue_vue/index.html
COPY ./.env /code/longue_vue/.env

CMD ["npm", "exec", "vite", "--", "--host", "--port", "80", "--base", "/ui/"]
CMD ["npm", "run", "dev", "--", "--host", "--port", "80", "--base", "/ui/"]
6 changes: 3 additions & 3 deletions frontend/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "npg-longue-vue",
"version": "2.2.0",
"version": "2.3.0",
"description": "UI for LangQC",
"author": "Kieron Taylor <[email protected]>",
"license": "GPL-3.0-or-later",
Expand All @@ -10,9 +10,9 @@
},
"type": "module",
"scripts": {
"dev": "vite --port 3000",
"dev": "vite --port 80 --mode development",
"build": "vite build",
"preview": "vite preview --port 3000",
"preview": "vite preview --port 80",
"test": "vitest run",
"coverage": "vitest run --coverage",
"test:e2e:ci": "start-server-and-test preview http://localhost:3000/ 'cypress run --e2e'",
Expand Down
18 changes: 17 additions & 1 deletion frontend/src/App.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script setup>
import { RouterView, useRoute } from 'vue-router';
import { onMounted, provide, ref } from "vue";
import { ElMessage } from "element-plus";
import { ElMessage, ElTooltip } from "element-plus";
import { Search } from '@element-plus/icons-vue';
import router from "@/router/index.js";
Expand Down Expand Up @@ -173,4 +173,20 @@ nav {
align-items: center;
text-align: left;
}
.MetricOrange {
background-color: #F8CBAD;
}
.MetricBlue {
background-color: #BDD6EE;
}
.MetricGreen {
background-color: #C6E0B4;
}
.MetricYellow {
background-color: #FFE698;
}
</style>
45 changes: 45 additions & 0 deletions frontend/src/components/PoolStats.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<script setup>
import { ref } from "vue";
import { ElTooltip, ElCollapse, ElCollapseItem } from "element-plus";
const props = defineProps({
pool: Object,
})
const active = ref([])
</script>

<template>
<el-collapse v-model="active" accordion>
<el-collapse-item name="1">
<template #title><slot>Pool composition</slot></template>
<el-tooltip content="Caution, this value is of low signficance when the pool size is small">
<p>Coefficient of Variance: {{ props.pool.pool_coeff_of_variance }}</p>
</el-tooltip>
<table>
<tr>
<th>Sample</th>
<th>Tag 1</th>
<th>Tag 2</th>
<th>Deplexing barcode ID</th>
<th>HiFi bases (Gb)</th>
<th>HiFi reads</th>
<th>HiFi mean read length</th>
<th>Percentage of HiFi bases</th>
<th>Percentage of total reads</th>
</tr>
<tr :key="library.id_product" v-for="library in props.pool.products">
<td>{{ library.sample_name }}</td>
<td>{{ library.tag1_name }}</td>
<td>{{ library.tag2_name }}</td>
<td>{{ library.deplexing_barcode }}</td>
<td>{{ library.hifi_read_bases }}</td>
<td>{{ library.hifi_num_reads }}</td>
<td>{{ library.hifi_read_length_mean }}</td>
<td>{{ library.hifi_bases_percent }}</td>
<td class="MetricYellow">{{ library.percentage_total_reads }}</td>
</tr>
</table>
</el-collapse-item>
</el-collapse>
</template>
33 changes: 31 additions & 2 deletions frontend/src/components/QcView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,20 @@
* An information view containing run data and metrics useful for QC assessment
*/
import { computed } from "vue";
import { computed, ref, watch } from "vue";
import groupMetrics from "../utils/metrics.js";
import { combineLabelWithPlate } from "../utils/text.js"
import PoolStats from "./PoolStats.vue";
import LangQc from "../utils/langqc";
const dataClient = new LangQc()
const props = defineProps({
// Well object representing one prepared input for the instrument
// Expects content in the form of lang_qc/models/pacbio/well.py:PacBioWellFull
well: Object,
});
})
const slURL = computed(() => {
let hostname = props.well.metrics.smrt_link.hostname
Expand Down Expand Up @@ -98,6 +103,23 @@
return ''
})
const poolStats = ref(null)
watch(() => props.well, () => {
poolStats.value = null // empty in case next well doesn't have a pool
if (ssLimsNumSamples.value > 0) {
dataClient.getPoolMetrics(props.well.id_product).then(
(response) => { poolStats.value = response }
).catch((error) => {
if (error.message.match("Conflict")) {
// Nothing to do
} else {
console.log(error)
// make a banner show this error?
}
})
}
}, { immediate: true }
)
</script>
<template>
Expand Down Expand Up @@ -165,6 +187,8 @@
</table>
</div>
<PoolStats v-if="poolStats" :pool="poolStats">Pool composition</PoolStats>
<div id="Metrics">
<table>
<tr>
Expand Down Expand Up @@ -197,6 +221,11 @@
td {
padding-left: 5px;
padding-right: 5px;
align-self: flex-start;
}
tr>td:first-child {
vertical-align: top;
/* Pin first text element to the top of the td */
}
table.summary {
border: 0px;
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/WellTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ defineEmits(['wellSelected'])
<th>Well time complete</th>
</tr>
<tr :key="wellObj.id_product" v-for="wellObj in wellCollection">
<td>{{ wellObj.run_name }}</td>
<td :id="wellObj.run_name">{{ wellObj.run_name }}</td>
<td class="well_selector">
<el-tooltip placement="top" effect="light" :show-after="tooltipDelay"
:content="'<span>'.concat(listStudiesForTooltip(wellObj.study_names)).concat('</span>')"
Expand Down
66 changes: 66 additions & 0 deletions frontend/src/components/__tests__/PoolStats.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { describe, expect, test } from 'vitest'
import { mount } from '@vue/test-utils'
import ElementPlus from 'element-plus'

import PoolStats from '../PoolStats.vue'

const wrapper = mount(PoolStats, {
global: {
plugins: [ElementPlus],
},
props: {
pool: {
pool_coeff_of_variance: 47.2,
products: [{
id_product: 'A'.repeat(64),
sample_name: 'allthets',
tag1_name: 'TTTTTTTT',
tag2_name: null,
deplexing_barcode: 'bc10--bc10',
hifi_read_bases: 900,
hifi_num_reads: 20,
hifi_read_length_mean: 45,
hifi_bases_percent: 90.001,
percentage_total_reads: 66.6
},{
id_product: 'B'.repeat(64),
sample_name: null,
tag1_name: 'GGGGGGGG',
tag2_name: null,
deplexing_barcode: 'bc11--bc11',
hifi_read_bases: 100,
hifi_num_reads: 10,
hifi_read_length_mean: 10,
hifi_bases_percent: 100,
percentage_total_reads: 33.3
}]
}
}
})

describe('Create poolstats table with good data', () => {
test('Component is "folded" by default', () => {
expect(wrapper.getComponent('transition-stub').attributes()['appear']).toEqual('false')
})

test('Coefficient of variance showing', async () => {
let topStat = wrapper.find('p')
await topStat.trigger('focus')
expect(topStat.classes('el-tooltip__trigger')).toBeTruthy()

expect(topStat.text()).toEqual('Coefficient of Variance: 47.2')
})

test('Table looks about right', () => {
let rows = wrapper.findAll('tr')
expect(rows.length).toEqual(3)

// Check tag 1 has been set
expect(rows[1].findAll('td')[1].text()).toEqual('TTTTTTTT')
expect(rows[2].findAll('td')[1].text()).toEqual('GGGGGGGG')

// Sample name column set appropriately
expect(rows[1].find('td').text()).toEqual('allthets')
expect(rows[2].find('td').text()).toEqual('')
})
})
5 changes: 5 additions & 0 deletions frontend/src/components/__tests__/QcView.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ import QcView from '../QcView.vue';

describe('Component renders', () => {

// Supply PoolStats element with some data
fetch.mockResponse(
JSON.stringify({})
)

afterEach(async () => {
cleanup()
});
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/stores/focusWell.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export const useWellStore = defineStore('focusWell', {
(error) => {
ElMessage({
message: error.message,
type: error,
type: "error",
duration: 5000
})
}
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/utils/__tests__/langqc.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ describe('Example fake remote api call', () => {

client.getWellsForRunPromise('blah')
expect(fetch.mock.calls[6][0]).toEqual('/api/pacbio/run/blah?page_size=100&page=1')

client.getPoolMetrics('A12345');
expect(fetch.mock.calls[7][0]).toEqual('/api/pacbio/products/A12345/seq_level/pool')
});
});

Expand Down
6 changes: 6 additions & 0 deletions frontend/src/utils/langqc.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,4 +118,10 @@ export default class LangQc {
}
)
}

getPoolMetrics(id_product) {
// Use the product metrics endpoint to get additional metrics
// for a well.
return this.fetchWrapper(this.buildUrl(['products', id_product, 'seq_level', 'pool']));
}
}
5 changes: 4 additions & 1 deletion frontend/src/views/WellsByRun.vue
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ function flatten_data(values) {
getUserName((email) => { user.value = email }).then();
watch(() => route.query, (after, before) => {
if (qcQueryChanged(before, after)) {
if (after.idProduct === undefined) {
focusWell.setFocusWell(null)
} else if (qcQueryChanged(before, after)) {
focusWell.loadWellDetail(after.idProduct)
}
},
Expand All @@ -51,6 +53,7 @@ watch(() => props.runName, () => {
Promise.all(promises).then(
(values) => (wellCollection.value = flatten_data(values))
).catch(error => {
console.log(error.message + " when resolving promises in props.runName watcher")
ElMessage({
message: error.message,
type: "warning",
Expand Down
24 changes: 12 additions & 12 deletions frontend/src/views/__tests__/WellsByRun.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,8 @@ describe('Does it work?', async () => {

test('Click a well selection, QC View appears (because URL alters)', async () => {
// Not providing precisely the right data, but serves for the component
fetch.mockResponseOnce(
JSON.stringify(secondaryRun)
fetch.mockResponses(
[JSON.stringify(secondaryRun)], // well QC data loading
)

let buttons = wrapper.findAll('button')
Expand All @@ -164,20 +164,20 @@ describe('Does it work?', async () => {
page_number: 1,
total_number_of_items: 1,
wells: [secondaryRun]
})
]
}),
{ status: 200 }
],
)
await wrapper.setProps({runName: ['TRACTION-RUN-211', 'TRACTION-RUN-210']})
await flushPromises()

test('Table now contains wells from both runs', () => {
const table = wrapper.get('table')
expect(table.exists()).toBe(true)
const table = wrapper.get('table')
expect(table.exists()).toBe(true)

expect(table.find('TRACTION-RUN-211').exists()).toBe(true)
expect(table.find('TRACTION-RUN-210').exists()).toBe(true)
expect(table.find("td#TRACTION-RUN-211").exists()).toBe(true)
expect(table.find("td#TRACTION-RUN-210").exists()).toBe(true)

const rows = table.findAll('tr')
expect(rows.length).toEqual(4)
})
const rows = table.findAll('tr')
expect(rows.length).toEqual(4)
})
})
2 changes: 1 addition & 1 deletion lang_qc/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "2.2.0"
__version__ = "2.3.0"
5 changes: 3 additions & 2 deletions lang_qc/db/helper/wells.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright (c) 2022, 2023 Genome Research Ltd.
# Copyright (c) 2022, 2023, 2024 Genome Research Ltd.
#
# Authors:
# Marina Gourtovaia <[email protected]>
Expand Down Expand Up @@ -38,6 +38,7 @@
from lang_qc.models.qc_flow_status import QcFlowStatusEnum
from lang_qc.models.qc_state import QcState as QcStateModel
from lang_qc.util.errors import EmptyListOfRunNamesError, RunNotFoundError
from lang_qc.util.type_checksum import PacBioWellSHA256

"""
This package is using an undocumented feature of Pydantic, type
Expand All @@ -64,7 +65,7 @@ class WellWh(BaseModel):
# The TestClient seems to be keeping these instances alive and changing them.

def get_mlwh_well_by_product_id(
self, id_product: str
self, id_product: PacBioWellSHA256
) -> PacBioRunWellMetrics | None:
"""
Returns a well row record from the well metrics table or
Expand Down
Loading

0 comments on commit 797e5ff

Please sign in to comment.