Skip to content
Merged
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
15 changes: 0 additions & 15 deletions src/api/mavedb/index.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,3 @@
import {isAxiosError} from 'axios'

export interface ErrorResponse {
status: number
data?: Record<string, unknown>
}

/** Extract a normalized response from a caught Axios error. */
export function getErrorResponse(e: unknown): ErrorResponse {
if (isAxiosError(e) && e.response) {
return {status: e.response.status, data: e.response.data as Record<string, unknown>}
}
return {status: 500}
}

export * from './access-keys'
export * from './calibrations'
export * from './collections'
Expand Down
39 changes: 25 additions & 14 deletions src/api/mavedb/score-sets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,10 @@ type ScoreSetSearch = components['schemas']['ScoreSetsSearch']
type ScoreSetsSearchResponse = components['schemas']['ScoreSetsSearchResponse']
export type ScoreSetsSearchFilterOptionsResponse = components['schemas']['ScoreSetsSearchFilterOptionsResponse']

const HISTOGRAM_VARIANT_DATA_NAMESPACES = ['vep', 'scores', 'clingen']
const HISTOGRAM_VARIANT_DATA_NAMESPACES = ['vep', 'scores', 'clingen', 'mavedb']

function scoreSetVariantDataParams(
options: {includePostMappedHgvs?: boolean; namespaces?: string[]} = {}
): URLSearchParams {
function scoreSetVariantDataParams(options: {namespaces?: string[]} = {}): URLSearchParams {
const params = new URLSearchParams()
if (options.includePostMappedHgvs) params.append('include_post_mapped_hgvs', 'true')
for (const namespace of options.namespaces ?? []) params.append('namespaces', namespace)
return params
}
Expand All @@ -25,10 +22,7 @@ function scoreSetVariantDataUrl(urn: string, params: URLSearchParams = new URLSe
}

export function histogramScoreSetVariantDataUrl(urn: string): string {
return scoreSetVariantDataUrl(
urn,
scoreSetVariantDataParams({includePostMappedHgvs: true, namespaces: HISTOGRAM_VARIANT_DATA_NAMESPACES})
)
return scoreSetVariantDataUrl(urn, scoreSetVariantDataParams({namespaces: HISTOGRAM_VARIANT_DATA_NAMESPACES}))
}

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -103,32 +97,47 @@ export async function publishScoreSet(urn: string) {
}

export async function getScoreSetClinicalControlOptions(urn: string) {
const response = await axios.get(`${config.apiBaseUrl}/score-sets/${encodeURIComponent(urn)}/clinical-controls/options`)
const response = await axios.get(
`${config.apiBaseUrl}/score-sets/${encodeURIComponent(urn)}/clinical-controls/options`
)
return response.data
}

export async function downloadScoreSetFile(urn: string, type: 'scores' | 'counts'): Promise<string> {
const response = await axios.get(
`${config.apiBaseUrl}/score-sets/${encodeURIComponent(urn)}/${type}?drop_na_columns=true`
`${config.apiBaseUrl}/score-sets/${encodeURIComponent(urn)}/${type}?drop_unused_hgvs_columns=true`
)
return response.data
}

/**
* Fetch the CSV column namespaces this score set has data for.
*/
export async function getScoreSetCsvNamespaces(
urn: string,
signal?: AbortSignal
): Promise<components['schemas']['AvailableCsvNamespace'][]> {
const response = await axios.get(`${config.apiBaseUrl}/score-sets/${encodeURIComponent(urn)}/csv-namespaces`, {
signal
})
return response.data
}

export async function downloadScoreSetVariantData(urn: string, params: URLSearchParams): Promise<string> {
const response = await axios.get(scoreSetVariantDataUrl(urn, params))
return response.data
}

export async function getScoreSetScoresPreview(urn: string): Promise<string> {
const response = await axios.get(
`${config.apiBaseUrl}/score-sets/${encodeURIComponent(urn)}/scores?drop_na_columns=true`
`${config.apiBaseUrl}/score-sets/${encodeURIComponent(urn)}/scores?drop_unused_hgvs_columns=true`
)
return response.data
}

export async function getScoreSetCountsPreview(urn: string): Promise<string> {
const response = await axios.get(
`${config.apiBaseUrl}/score-sets/${encodeURIComponent(urn)}/counts?drop_na_columns=true`
`${config.apiBaseUrl}/score-sets/${encodeURIComponent(urn)}/counts?drop_unused_hgvs_columns=true`
)
return response.data
}
Expand All @@ -138,7 +147,9 @@ export async function downloadMappedVariants(urn: string) {
return response.data
}

export async function getRecentlyPublishedScoreSets(signal?: AbortSignal): Promise<components['schemas']['ScoreSet'][]> {
export async function getRecentlyPublishedScoreSets(
signal?: AbortSignal
): Promise<components['schemas']['ScoreSet'][]> {
const response = await axios.get(`${config.apiBaseUrl}/score-sets/recently-published`, {
headers: {accept: 'application/json'},
signal
Expand Down
22 changes: 22 additions & 0 deletions src/api/mavedb/variants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ type ScoreSet = components['schemas']['ScoreSet']
type VariantEffectMeasurementWithScoreSet = components['schemas']['VariantEffectMeasurementWithScoreSet']
type ClingenAlleleIdVariantLookupResponse = components['schemas']['ClingenAlleleIdVariantLookupResponse']
type MappedVariant = components['schemas']['MappedVariant']
type AvailableCsvNamespace = components['schemas']['AvailableCsvNamespace']

export async function lookupVariantsByClingenId(
clingenAlleleIds: string[]
Expand Down Expand Up @@ -47,3 +48,24 @@ export async function getScoreSet(urn: string): Promise<ScoreSet> {
const response = await axios.get(`${config.apiBaseUrl}/score-sets/${encodeURIComponent(urn)}`)
return response.data
}

/**
* Fetch the CSV column namespaces this variant has data for.
*/
export async function getVariantCsvNamespaces(urn: string, signal?: AbortSignal): Promise<AvailableCsvNamespace[]> {
const response = await axios.get(`${config.apiBaseUrl}/variants/${encodeURIComponent(urn)}/csv-namespaces`, {signal})
return response.data
}

export function variantCsvUrl(urn: string, namespaces?: string[]): string {
const params = new URLSearchParams()
for (const namespace of namespaces ?? []) params.append('namespaces', namespace)
const query = params.toString()
const baseUrl = `${config.apiBaseUrl}/variants/${encodeURIComponent(urn)}/csv`
return query ? `${baseUrl}?${query}` : baseUrl
}

export async function downloadVariantCsv(urn: string, namespaces?: string[]): Promise<string> {
const response = await axios.get(variantCsvUrl(urn, namespaces))
return response.data
}
2 changes: 1 addition & 1 deletion src/components/collection/CollectionDataSetEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ import useItem from '@/composition/item.ts'
import {addCollectionScoreSet, addCollectionExperiment} from '@/api/mavedb/collections'
import {getScoreSet} from '@/api/mavedb/variants'
import {getExperiment} from '@/api/mavedb/experiments'
import {getErrorResponse} from '@/api/mavedb'
import {getErrorResponse} from '@/lib/errors'
import {type DataSetType, DATA_SET_TYPE_LABELS} from '@/lib/collections'
import MvEmailPrompt from '@/components/common/MvEmailPrompt.vue'
import {components} from '@/schema/openapi'
Expand Down
129 changes: 129 additions & 0 deletions src/components/common/MvCsvColumnDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
<template>
<PDialog
:base-z-index="901"
:header="header"
modal
:style="{width: '30rem'}"
:visible="visible"
@show="load()"
@update:visible="$emit('update:visible', $event)">
<MvLoader v-if="loading" />
<MvErrorState
v-else-if="error"
description="The list of available columns could not be loaded."
title="Could not load download options"
@retry="load()" />
<div v-else class="flex flex-col gap-4 py-2">
<div class="flex items-center justify-between border-b border-border-light pb-2">
<span class="text-sm text-text-muted">{{ selectionSummary }}</span>
<button
class="cursor-pointer border-none bg-transparent text-xs-plus font-semibold text-sage hover:underline"
type="button"
@click="toggleAll()">
{{ allSelected ? 'Select none' : 'Select all' }}
</button>
</div>

<div v-for="section in sections" :key="section.group" class="flex flex-col gap-2">
<span class="text-xs font-bold uppercase tracking-wide text-text-muted">{{ section.title }}</span>
<template v-for="subsection in section.subsections" :key="subsection.label ?? '_'">
<!-- Only present when the section spans more than one score set, e.g. a variant measured in
several assays, each with its own calibrations. -->
<span v-if="subsection.label" class="text-xs text-text-muted" :title="subsection.urn ?? undefined">{{
subsection.label
}}</span>
<label
v-for="entry in subsection.namespaces"
:key="entry.namespace"
class="flex cursor-pointer items-center gap-2 text-sm"
:class="{'pl-3': subsection.label}">
<Checkbox v-model="selected" :value="entry.namespace" />
{{ entry.label }}
</label>
</template>
</div>

<div v-if="formattingExtraOptions.length > 0" class="flex flex-col gap-2 border-t border-border-light pt-3">
<span class="text-xs font-bold uppercase tracking-wide text-text-muted">Options</span>
<label
v-for="option in formattingExtraOptions"
:key="option.value"
class="flex cursor-pointer items-center gap-2 text-sm">
<Checkbox v-model="selectedExtras" :value="option.value" />
{{ option.label }}
</label>
</div>
</div>

<template #footer>
<PButton label="Cancel" severity="secondary" size="small" @click="$emit('update:visible', false)" />
<PButton
:disabled="loading || !!error || selectedColumnGroups === 0"
icon="pi pi-download"
label="Download"
size="small"
@click="confirm" />
</template>
</PDialog>
</template>

<script lang="ts">
import Button from 'primevue/button'
import Checkbox from 'primevue/checkbox'
import PDialog from 'primevue/dialog'
import {computed, defineComponent, type PropType} from 'vue'

import MvErrorState from '@/components/common/MvErrorState.vue'
import MvLoader from '@/components/common/MvLoader.vue'
import {useCsvNamespaces, type CsvExtraOption} from '@/composables/use-csv-namespaces'

/**
* Column picker for the CSV exports, shared by the score set and variant pages.
*
* Namespace checkboxes come from the API, so neither page maintains its own list or labels. Non-namespace
* query flags are passed in as `extraOptions` and returned separately as `extras`.
*
* Download stays disabled until a namespace is checked, so the emitted selection always names its
* columns — there is no "empty means all" rule for callers to know.
*/
export default defineComponent({
name: 'MvCsvColumnDialog',

components: {Checkbox, MvErrorState, MvLoader, PButton: Button, PDialog},

props: {
visible: {type: Boolean, required: true},
/** The score set or variant whose columns to offer. */
urn: {type: String as PropType<string | null>, default: null},
kind: {type: String as PropType<'scoreSet' | 'variant'>, required: true},
header: {type: String, default: 'Choose columns'},
/** Formatting flags, e.g. omitting unused HGVS columns. Returned as `extras` on confirm. */
extraOptions: {type: Array as PropType<CsvExtraOption[]>, default: () => []}
},

emits: ['update:visible', 'confirm'],

setup(props) {
// The whole selection model lives in the composable, where it is unit-testable without a DOM —
// including the extras, so the count and "Select all" cover every checkbox on screen. Spread so the
// template reads the refs directly rather than reaching through a wrapper object.
return {
...useCsvNamespaces({
urn: computed(() => props.urn),
kind: props.kind,
extraOptions: computed(() => props.extraOptions)
})
}
},

methods: {
confirm() {
this.$emit('confirm', {
namespaces: [...this.selected],
extras: [...this.selectedExtras]
})
this.$emit('update:visible', false)
}
}
})
</script>
Loading
Loading