Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: new SearchBar with vuetify Toolbars (#17) #19

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# For more configuration details:
# https://docs.codecov.io/docs/codecov-yaml

# Check if this file is valid by running in bash:
# curl -X POST --data-binary @.codecov.yml https://codecov.io/validate

# Coverage configuration
# ----------------------
coverage:
status:
patch: false

range: 70..90 # First number represents red, and second represents green
# (default is 70..100)
round: down # up, down, or nearest
precision: 2 # Number of decimal places, between 0 and 5

# Ignoring Paths
# --------------
# which folders/files to ignore

# Pull request comments:
# ----------------------
# Diff is the Coverage Diff of the pull request.
# Files are the files impacted by the pull request
comment:
layout: diff, files # accepted in any order: reach, diff, flags, and/or files
2 changes: 1 addition & 1 deletion frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
<title>REEV</title>
</head>
<body>
<div id="app"></div>
Expand Down
Binary file modified frontend/public/favicon.ico
Binary file not shown.
14 changes: 10 additions & 4 deletions frontend/src/api/__tests__/common.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import { describe, it, expect } from 'vitest'

import { API_BASE_PREFIX } from '../common'
import { API_BASE_PREFIX_ANNONARS, API_BASE_PREFIX_MEHARI } from '../common'

describe('API_BASE_PREFIX constant', () => {
it('returns the correct API base prefix in production mode', () => {
describe('API_BASE_PREFIX constants', () => {
it('returns the correct API base prefix for annonars in production mode', () => {
const originalMode = import.meta.env.MODE
expect(API_BASE_PREFIX).toBe('/proxy/annonars')
expect(API_BASE_PREFIX_ANNONARS).toBe('/proxy/annonars')
import.meta.env.MODE = originalMode
})

it('returns the correct API base prefix for mehari in production mode', () => {
const originalMode = import.meta.env.MODE
expect(API_BASE_PREFIX_MEHARI).toBe('/proxy/mehari')
import.meta.env.MODE = originalMode
})
})
19 changes: 18 additions & 1 deletion frontend/src/api/__tests__/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, it, expect } from 'vitest'

import { roundIt } from '../utils'
import { roundIt, search } from '../utils'

describe('roundIt method', () => {
it('should round a positive value with default digits', () => {
Expand Down Expand Up @@ -33,3 +33,20 @@ describe('roundIt method', () => {
expect(result).toBe('<abbr title="-10.12345">-10.12</abbr>')
})
})

describe('search method', () => {
it('should return route location if match', () => {
const result = search('BRCA1')
expect(result).toEqual({
name: 'gene',
params: {
searchTerm: 'BRCA1'
}
})
})

it.skip('should return null if no match', () => {
const result = search('foo')
expect(result).toBe(null)
})
})
29 changes: 26 additions & 3 deletions frontend/src/api/annonars.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { API_BASE_PREFIX } from '@/api/common'
import { API_BASE_PREFIX_ANNONARS } from '@/api/common'

const API_BASE_URL = `${API_BASE_PREFIX}/`
const API_BASE_URL = `${API_BASE_PREFIX_ANNONARS}/`

export class AnnonarsClient {
private apiBaseUrl: string
Expand All @@ -12,7 +12,30 @@ export class AnnonarsClient {
}

async fetchGeneInfo(hgncId: string): Promise<any> {
const response = await fetch(`${this.apiBaseUrl}genes/info?hgnc-id=${hgncId}`, {
const response = await fetch(`${this.apiBaseUrl}genes/info?hgnc_id=${hgncId}`, {
method: 'GET'
})
return await response.json()
}

async fetchVariantInfo(
genomeRelease: string,
chromosome: string,
pos: number,
reference: string,
alternative: string
): Promise<any> {
let chrom = chromosome.replace('chr', '')
if (genomeRelease !== 'grch37') {
chrom = `chr${chrom}`
}

const url =
`${this.apiBaseUrl}annos/variant?genome_release=${genomeRelease}&` +
`chromosome=${chrom}&pos=${pos}&reference=${reference}&` +
`alternative=${alternative}`

const response = await fetch(url, {
method: 'GET'
})
return await response.json()
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/api/common.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
export const API_BASE_PREFIX =
export const API_BASE_PREFIX_ANNONARS =
import.meta.env.MODE == 'development' ? '//localhost:8080/proxy/annonars' : '/proxy/annonars'

export const API_BASE_PREFIX_MEHARI =
import.meta.env.MODE == 'development' ? '//localhost:8080/proxy/mehari' : '/proxy/mehari'
33 changes: 33 additions & 0 deletions frontend/src/api/mehari.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { API_BASE_PREFIX_MEHARI } from '@/api/common'

const API_BASE_URL = `${API_BASE_PREFIX_MEHARI}/`

export class MehariClient {
private apiBaseUrl: string
private csrfToken: string | null

constructor(apiBaseUrl?: string, csrfToken?: string) {
this.apiBaseUrl = apiBaseUrl ?? API_BASE_URL
this.csrfToken = csrfToken ?? null
}

async retrieveTxCsq(
genomeRelease: string,
chromosome: string,
pos: number,
reference: string,
alternative: string,
hgnc_id?: string
): Promise<any> {
const hgncSuffix = hgnc_id ? `&hgnc-id=${hgnc_id}` : ''
const url =
`${this.apiBaseUrl}tx/csq?genome-release=${genomeRelease}&` +
`chromosome=${chromosome}&position=${pos}&reference=${reference}&` +
`alternative=${alternative}${hgncSuffix}`

const response = await fetch(url, {
method: 'GET'
})
return await response.json()
}
}
128 changes: 128 additions & 0 deletions frontend/src/api/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,131 @@ export const roundIt = (value: number, digits: number = 2, label?: string): stri
const useLabel = label ? `${label}: ` : ''
return `<abbr title="${useLabel}${value}">${roundedValue}</abbr>`
}

/**
* Converts a number to a string with thousands separator.
*
* @param value The number to separate.
* @param separator The separator to use.
*/
export const separateIt = (value: number, separator: string = ' '): string => {
const asString = `${value}`
if (!asString.length) {
return '0'
}
const splitString = asString.split('.', 1)
const cardinal = splitString[0]
if (!cardinal?.length) {
splitString[0] = '0'
} else {
const offset = cardinal.length % 3
const arr = [cardinal.slice(0, offset)]
for (let i = 0; i <= cardinal.length; i += 3) {
arr.push(cardinal.slice(offset + i, offset + i + 3))
}
splitString[0] = arr.join(separator)
}
return splitString.join('.')
}

/**
* Returns whether the given variant looks mitochondrial.
*
* @param smallVar Small variant to check.
* @returns whether the position is on the mitochondrial genome
*/
export const isVariantMt = (smallVar: any): boolean => {
return ['MT', 'M', 'chrMT', 'chrM'].includes(smallVar?.chromosome)
}

/**
* Returns whether the given position is in a homopolymer on the mitochondrial chromosome.
*
* @param smallVar Small variant to check.
* @returns whether the position is in a mitochondrial homopolymer
*/
export const isVariantMtHomopolymer = (smallVar: any): any => {
if (!smallVar) {
return false
}
const { start, end } = smallVar
const positionCheck = (pos: number) => {
return (
(pos >= 66 && pos <= 71) ||
(pos >= 300 && pos <= 316) ||
(pos >= 513 && pos <= 525) ||
(pos >= 3106 && pos <= 3107) ||
(pos >= 12418 && pos <= 12425) ||
(pos >= 16182 && pos <= 16194)
)
}
if (isVariantMt(smallVar)) {
return positionCheck(start) || positionCheck(end)
}
}

/**
* Take a `searchTerm` and return a route location that can be used to navigate to
* the correct page.
*
* @param searchTerm The search term to use.
*/
export const search = (searchTerm: string, genomeRelease: string) => {
interface RouteLocationFragment {
name: string
params?: any
}

type RouteLoctionBuilder = () => RouteLocationFragment

// We iterate the regexps in the `Map` and will use the route from the
// first match.
const SEARCH_REGEXPS: [RegExp, RouteLoctionBuilder][] = [
[
/^chr\d+:\d+:[A-Z]:[A-Z]$/,
(): RouteLocationFragment => ({
name: 'variant',
params: {
searchTerm: searchTerm,
genomeRelease: genomeRelease
}
})
],
[
/^.*$/,
(): RouteLocationFragment => ({
name: 'gene',
params: {
searchTerm: searchTerm,
genomeRelease: genomeRelease
}
})
]
]

for (const [regexp, getRoute] of SEARCH_REGEXPS) {
if (regexp.test(searchTerm)) {
const routeLocation = getRoute()
console.log(`term ${searchTerm} matched ${regexp}, route is`, routeLocation)
return routeLocation
}
}
return null
}

/**
* Take a query string and return an object with the chromosome, pos, reference and
* alternative value.
*
* @param query Incoming query string
*/
export const infoFromQuery = (query: string): any => {
const [chromosome, pos, reference, alternative, hgnc_id] = query.split(':')
return {
chromosome: chromosome,
pos: pos,
reference: reference,
alternative: alternative,
hgnc_id: hgnc_id
}
}
4 changes: 2 additions & 2 deletions frontend/src/assets/__tests__/BRCA1GeneInfo.json
Git LFS file not shown
3 changes: 3 additions & 0 deletions frontend/src/assets/__tests__/TxCsq.json
Git LFS file not shown
3 changes: 3 additions & 0 deletions frontend/src/assets/__tests__/VariantInfo.json
Git LFS file not shown
41 changes: 35 additions & 6 deletions frontend/src/components/HeaderDetailPage.vue
Original file line number Diff line number Diff line change
@@ -1,23 +1,48 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useGeneInfoStore } from '@/stores/geneInfo'

import SearchBar from '@/components/SearchBar.vue'
import { search } from '@/api/utils'

export interface Props {
searchTerm?: string
genomeRelease?: string
}

const props = withDefaults(defineProps<Props>(), {
searchTerm: '',
genomeRelease: 'grch37'
})

const router = useRouter()

const geneInfoStore = useGeneInfoStore()
const searchTermRef = ref(props.searchTerm)
const genomeReleaseRef = ref(props.genomeRelease)

if (geneInfoStore.geneInfo === null) {
router.push({ name: 'home' })
const performSearch = async () => {
const routeLocation: any = search(searchTermRef.value, genomeReleaseRef.value)
if (routeLocation) {
router.push(routeLocation)
} else {
console.error(`no route found for ${searchTermRef.value}`)
}
}
</script>

<template>
<v-app-bar app class="top-bar">
<v-toolbar-title>
<router-link to="/">
<img src="@/assets/reev-logo.svg" id="logo" class="nav" alt="logo" width="100" />
<img src="@/assets/reev-logo.svg" id="logo" alt="logo" width="100" />
</router-link>
</v-toolbar-title>
<SearchBar
class="top-search-bar"
v-model:search-term="searchTermRef"
v-model:genome-release="genomeReleaseRef"
@click-search="performSearch"
/>
<v-spacer></v-spacer>
<v-toolbar-items class="topbar-links">
<v-btn id="about" to="/about"> About </v-btn>
Expand All @@ -32,14 +57,18 @@ if (geneInfoStore.geneInfo === null) {
border-bottom: 2px solid rgb(111, 100, 210);
}

.top-search-bar {
display: flex;
width: 50%;
}

.topbar-links {
display: flex;
margin: 0 10px;
}

#logo {
margin-left: 25px;
margin-right: 10px;
margin-top: 10px;
}
</style>
Loading
Loading