Skip to content

Commit

Permalink
Addressed all comments + adjusted blast UI
Browse files Browse the repository at this point in the history
  • Loading branch information
timurishmuratov7 committed Aug 9, 2024
1 parent f8e48df commit 0172f76
Show file tree
Hide file tree
Showing 6 changed files with 169 additions and 39 deletions.
32 changes: 32 additions & 0 deletions .github/workflows/blast-master.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: blast
run-name: blast

# Build and push nolabs image using GitHub Cache API
# Only if relevant files were changed

on:
push:
branches:
- master

jobs:
build:
permissions:
contents: read
packages: write

uses: ./.github/workflows/build-docker.yaml
with:
microservice_name: "blast"

push:
if: github.repository == 'BasedLabs/NoLabs'
needs: build

permissions:
contents: read
packages: write

uses: ./.github/workflows/push-docker.yaml
with:
microservice_name: "blast"
20 changes: 20 additions & 0 deletions .github/workflows/blast-pr.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: blast
run-name: blast

# Build and push nolabs image using GitHub Cache API
# Only if relevant files were changed

on:
pull_request:
branches:
- master

jobs:
build:
permissions:
contents: read
packages: write

uses: ./.github/workflows/build-docker.yaml
with:
microservice_name: "blast"
9 changes: 8 additions & 1 deletion compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,14 @@ services:
build:
context: microservices/reinvent
dockerfile: build/Dockerfile
command: --host=0.0.0.0 --port=5790 --workers=1
command: --host=0.0.0.0 --port=5790
blast:
image: 'ghcr.io/basedlabs/blast:1.0.0'
network_mode: host
build:
context: microservices/blast
dockerfile: build/Dockerfile
command: --host=0.0.0.0 --port=5740 --workers=1
nolabs:
image: 'ghcr.io/basedlabs/nolabs:2.0.4'
network_mode: host
Expand Down
86 changes: 78 additions & 8 deletions frontend/src/features/workflow/components/jobs/BlastJob.vue
Original file line number Diff line number Diff line change
Expand Up @@ -101,15 +101,46 @@
<div class="text-h6">Result</div>
</q-card-section>
<q-card-section>
<!-- Query Info -->
<div v-if="jobHasGeneratedData">
<div v-for="result in job?.result" :key="result.query_id">
<p>Program: {{ result.program }}</p>
<p>Database: {{ result.database }}</p>
<p>Query ID: {{ result.query_id }}</p>
<p>Query Definition: {{ result.query_def }}</p>
<p>Query Length: {{ result.query_len }}</p>
</div>
<!-- Hit Visualization -->
<div id="hit-visualization" class="hit-visualization full-width"></div>
<q-btn @click="downloadCSV" color="primary" label="Download Results as CSV" class="q-my-md" />
<!-- Table with HSPs including Hit Info -->
<q-table
:rows="visibleHits"
:columns="columns"
row-key="id"
class="q-mt-md"
/>
>
<template v-slot:body-cell="props">
<q-td :props="props" :style="{ maxWidth: '300px', overflowX: 'auto', whiteSpace: 'nowrap' }">
<div v-if="props.col.field === 'definition' || props.col.field === 'qseq' || props.col.field === 'hseq' || props.col.field === 'midline'" class="cell-content">
<div class="scrollable-cell-content">{{ props.row[props.col.field] }}</div>
<q-btn
flat
dense
round
icon="content_copy"
@click="copyToClipboard(props.row[props.col.field])"
v-ripple
class="copy-btn"
size="sm"
/>
</div>
<div v-else>
{{ props.row[props.col.field] }}
</div>
</q-td>
</template>
</q-table>
</div>
<div v-else>
No results available.
Expand All @@ -122,7 +153,7 @@

<script lang="ts">
import { defineComponent } from 'vue';
import { QSpinner, QInput, QBtn, QTable } from 'quasar';
import { QSpinner, QInput, QBtn, QTable, QTooltip, QIcon, QTd } from 'quasar';
import * as d3 from 'd3';
import {
nolabs__application__use_cases__blast__api_models__JobResponse,
Expand Down Expand Up @@ -172,6 +203,10 @@ export default defineComponent({
},
columns() {
return [
{ name: 'hit_id', label: 'Hit ID', field: 'hit_id', align: 'left' },
{ name: 'definition', label: 'Definition', field: 'definition', align: 'left', sortable: true },
{ name: 'accession', label: 'Accession', field: 'accession', align: 'left' },
{ name: 'length', label: 'Length', field: 'length', align: 'left' },
{ name: 'num', label: 'HSP Number', field: 'num', align: 'left' },
{ name: 'bit_score', label: 'Bit Score', field: 'bit_score', align: 'left' },
{ name: 'score', label: 'Score', field: 'score', align: 'left' },
Expand All @@ -186,18 +221,32 @@ export default defineComponent({
{ name: 'positive', label: 'Positive', field: 'positive', align: 'left' },
{ name: 'gaps', label: 'Gaps', field: 'gaps', align: 'left' },
{ name: 'align_len', label: 'Alignment Length', field: 'align_len', align: 'left' },
{ name: 'qseq', label: 'Query Sequence', field: 'qseq', align: 'left' },
{ name: 'hseq', label: 'Hit Sequence', field: 'hseq', align: 'left' },
{ name: 'midline', label: 'Midline', field: 'midline', align: 'left' }
{ name: 'qseq', label: 'Query Sequence', field: 'qseq', align: 'left', sortable: true },
{ name: 'hseq', label: 'Hit Sequence', field: 'hseq', align: 'left', sortable: true },
{ name: 'midline', label: 'Midline', field: 'midline', align: 'left', sortable: true }
];
},
visibleHits() {
if (this.visibleHitId) {
return this.job?.result.flatMap(result => result.hits)
.filter(hit => hit.id === this.visibleHitId)
.flatMap(hit => hit.hsps) || [];
.flatMap(hit => hit.hsps.map(hsp => ({
...hsp,
hit_id: hit.id,
definition: hit.definition,
accession: hit.accession,
length: hit.length
}))) || [];
}
return this.job?.result.flatMap(result => result.hits).flatMap(hit => hit.hsps) || [];
return this.job?.result.flatMap(result => result.hits).flatMap(hit =>
hit.hsps.map(hsp => ({
...hsp,
hit_id: hit.id,
definition: hit.definition,
accession: hit.accession,
length: hit.length
}))
) || [];
}
},
async mounted() {
Expand Down Expand Up @@ -401,11 +450,20 @@ export default defineComponent({
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
},
copyToClipboard(text) {
navigator.clipboard.writeText(text).then(() => {
this.$q.notify({
type: 'positive',
message: 'Copied to clipboard!'
});
});
}
},
components: {
QBtn, // Ensure QBtn component is registered
QTable
QTable,
QTd
},
});
</script>
Expand Down Expand Up @@ -436,4 +494,16 @@ export default defineComponent({
.full-width {
width: 100%;
}
.scrollable-cell-content {
max-width: 250px;
overflow-x: auto;
white-space: nowrap;
display: inline-block;
vertical-align: middle;
}
.copy-btn {
margin-left: 8px;
}
</style>
2 changes: 1 addition & 1 deletion frontend/src/features/workflow/components/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,8 +190,8 @@ export const useWorkflowStore = defineStore('workflowStore', {
});
}
}
this.sendWorkflowUpdate();
}
this.sendWorkflowUpdate();
},
async deleteProteinFromExperiment(nodeId: string, proteinId: string) {
try {
Expand Down
59 changes: 30 additions & 29 deletions nolabs/domain/models/blast.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,41 +14,41 @@


class Hsp(EmbeddedDocument):
num = IntField(required=True)
bit_score = FloatField(required=True)
score = IntField(required=True)
evalue = FloatField(required=True)
query_from = IntField(required=True)
query_to = IntField(required=True)
hit_from = IntField(required=True)
hit_to = IntField(required=True)
query_frame = IntField(required=True)
hit_frame = IntField(required=True)
identity = IntField(required=True)
positive = IntField(required=True)
gaps = IntField(required=True)
align_len = IntField(required=True)
qseq = StringField(required=True)
hseq = StringField(required=True)
midline = StringField(required=True)
num: int = IntField(required=True)
bit_score: float = FloatField(required=True)
score: int = IntField(required=True)
evalue: float = FloatField(required=True)
query_from: int = IntField(required=True)
query_to: int = IntField(required=True)
hit_from: int = IntField(required=True)
hit_to: int = IntField(required=True)
query_frame: int = IntField(required=True)
hit_frame: int = IntField(required=True)
identity: int = IntField(required=True)
positive: int = IntField(required=True)
gaps: int = IntField(required=True)
align_len: int = IntField(required=True)
qseq: str = StringField(required=True)
hseq: str = StringField(required=True)
midline: str = StringField(required=True)


class Hit(EmbeddedDocument):
num = IntField(required=True)
id = StringField(required=True)
definition = StringField(required=True)
accession = StringField(required=True)
length = IntField(required=True)
num: int = IntField(required=True)
id: str = StringField(required=True)
definition: str = StringField(required=True)
accession: str = StringField(required=True)
length: int = IntField(required=True)
hsps = EmbeddedDocumentListField(Hsp, required=True)


class BlastJobResult(EmbeddedDocument):
protein_id = UUIDField(required=True)
program = StringField(required=True)
database = StringField(required=True)
query_id = StringField(required=True)
query_def = StringField(required=True)
query_len = IntField(required=True)
protein_id: UUID = UUIDField(required=True)
program: str = StringField(required=True)
database: str = StringField(required=True)
query_id: str = StringField(required=True)
query_def: str = StringField(required=True)
query_len: int = IntField(required=True)
hits = EmbeddedDocumentListField(Hit, required=True)

@staticmethod
Expand Down Expand Up @@ -103,7 +103,8 @@ def result_valid(self) -> bool:
return bool(self.result)

def input_valid(self) -> bool:
return bool(self.protein and self.job_type)
#TODO: add more blast jobs types when DNA is introduced, move to enums then
return bool(self.protein and self.job_type and (self.job_type in ["blastp"]))

def set_result(self, result: List[BlastJobResult]):
if not self.protein:
Expand Down

0 comments on commit 0172f76

Please sign in to comment.