diff --git a/src/api/mavedb/index.ts b/src/api/mavedb/index.ts index c097c748..3a92e5cf 100644 --- a/src/api/mavedb/index.ts +++ b/src/api/mavedb/index.ts @@ -1,18 +1,3 @@ -import {isAxiosError} from 'axios' - -export interface ErrorResponse { - status: number - data?: Record -} - -/** 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} - } - return {status: 500} -} - export * from './access-keys' export * from './calibrations' export * from './collections' diff --git a/src/api/mavedb/score-sets.ts b/src/api/mavedb/score-sets.ts index f2211a84..24ef5517 100644 --- a/src/api/mavedb/score-sets.ts +++ b/src/api/mavedb/score-sets.ts @@ -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 } @@ -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})) } // --------------------------------------------------------------------------- @@ -103,17 +97,32 @@ 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 { 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 { + 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 { const response = await axios.get(scoreSetVariantDataUrl(urn, params)) return response.data @@ -121,14 +130,14 @@ export async function downloadScoreSetVariantData(urn: string, params: URLSearch export async function getScoreSetScoresPreview(urn: string): Promise { 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 { 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 } @@ -138,7 +147,9 @@ export async function downloadMappedVariants(urn: string) { return response.data } -export async function getRecentlyPublishedScoreSets(signal?: AbortSignal): Promise { +export async function getRecentlyPublishedScoreSets( + signal?: AbortSignal +): Promise { const response = await axios.get(`${config.apiBaseUrl}/score-sets/recently-published`, { headers: {accept: 'application/json'}, signal diff --git a/src/api/mavedb/variants.ts b/src/api/mavedb/variants.ts index f0db95a4..24f5a31c 100644 --- a/src/api/mavedb/variants.ts +++ b/src/api/mavedb/variants.ts @@ -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[] @@ -47,3 +48,24 @@ export async function getScoreSet(urn: string): Promise { 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 { + 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 { + const response = await axios.get(variantCsvUrl(urn, namespaces)) + return response.data +} diff --git a/src/components/collection/CollectionDataSetEditor.vue b/src/components/collection/CollectionDataSetEditor.vue index 5e962885..9a4c997f 100644 --- a/src/components/collection/CollectionDataSetEditor.vue +++ b/src/components/collection/CollectionDataSetEditor.vue @@ -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' diff --git a/src/components/common/MvCsvColumnDialog.vue b/src/components/common/MvCsvColumnDialog.vue new file mode 100644 index 00000000..afc2e983 --- /dev/null +++ b/src/components/common/MvCsvColumnDialog.vue @@ -0,0 +1,129 @@ + + + diff --git a/src/components/common/MvVariantPreview.vue b/src/components/common/MvVariantPreview.vue index 85fd7762..e0d2c4b0 100644 --- a/src/components/common/MvVariantPreview.vue +++ b/src/components/common/MvVariantPreview.vue @@ -10,28 +10,32 @@
+ @click="downloadFile('scores')" /> + @click="downloadFile('counts')" /> + @click="customDialogVisible = true" /> + + + Preparing {{ fileDownloadLabel }}… +
@@ -82,8 +86,7 @@
+ class="flex items-center justify-between border-b border-border-light border-t bg-bg px-4 py-2 tablet:px-5"> Counts Showing {{ countsRows.length }} of {{ countsData.length.toLocaleString() }} @@ -109,34 +112,26 @@
+ class="px-5 py-8 text-center text-sm italic text-text-muted"> No data available.
- - -
- -
- -
+ + + diff --git a/src/components/screens/CollectionView.vue b/src/components/screens/CollectionView.vue index e5f9afa7..78bfd8fa 100644 --- a/src/components/screens/CollectionView.vue +++ b/src/components/screens/CollectionView.vue @@ -258,7 +258,7 @@ import {deleteCollection, removeCollectionEntity, updateCollection} from '@/api/ import {useDatasetPermissions} from '@/composables/use-dataset-permissions' import type {RowAction} from '@/components/common/MvRowActionMenu.vue' import {components} from '@/schema/openapi' -import {getErrorResponse} from '@/api/mavedb' +import {getErrorResponse} from '@/lib/errors' type Collection = components['schemas']['Collection'] type User = components['schemas']['User'] diff --git a/src/components/screens/ExperimentCreator.vue b/src/components/screens/ExperimentCreator.vue index 4d11926d..407d70a0 100644 --- a/src/components/screens/ExperimentCreator.vue +++ b/src/components/screens/ExperimentCreator.vue @@ -172,7 +172,8 @@ import useScopedId from '@/composables/scoped-id' import {useExperimentKeywords} from '@/composables/use-experiment-keywords' import {useJsonFileField} from '@/composables/use-json-file-field' import {usePublicationIdentifiers} from '@/composables/use-publication-identifiers' -import {createExperiment, getErrorResponse} from '@/api/mavedb' +import {createExperiment} from '@/api/mavedb' +import {getErrorResponse} from '@/lib/errors' import useAuth from '@/composition/auth' import {useKeywordOptions} from '@/composables/use-keyword-options' import {normalizeDoiArray, normalizeRawReadArray, normalizeContributorArray} from '@/lib/form-helpers' diff --git a/src/components/screens/ExperimentEditor.vue b/src/components/screens/ExperimentEditor.vue index 590165a9..940cb461 100644 --- a/src/components/screens/ExperimentEditor.vue +++ b/src/components/screens/ExperimentEditor.vue @@ -111,7 +111,8 @@ import {useExperimentKeywords} from '@/composables/use-experiment-keywords' import {useJsonFileField} from '@/composables/use-json-file-field' import {usePublicationIdentifiers, type PublicationIdentifier} from '@/composables/use-publication-identifiers' import useAuth from '@/composition/auth' -import {updateExperiment, getErrorResponse} from '@/api/mavedb' +import {updateExperiment} from '@/api/mavedb' +import {getErrorResponse} from '@/lib/errors' import useItem from '@/composition/item.ts' import {useKeywordOptions} from '@/composables/use-keyword-options' import {normalizeDoiArray, normalizeRawReadArray, normalizeContributorArray} from '@/lib/form-helpers' diff --git a/src/components/screens/ScoreSetCalibrationsView.vue b/src/components/screens/ScoreSetCalibrationsView.vue index 983695a3..373c2be7 100644 --- a/src/components/screens/ScoreSetCalibrationsView.vue +++ b/src/components/screens/ScoreSetCalibrationsView.vue @@ -232,7 +232,6 @@ import { getScoreSetCalibrations } from '@/api/mavedb' import {useDatasetPermissions} from '@/composables/use-dataset-permissions' -import axios from 'axios' import Button from 'primevue/button' import Column from 'primevue/column' import DataTable from 'primevue/datatable' @@ -242,6 +241,7 @@ import useAuth from '@/composition/auth' import {useCalibrationDialog} from '@/composables/use-calibration-dialog' import MvPageLoading from '@/components/common/MvPageLoading.vue' import CalibrationTable from '@/components/calibration/CalibrationTable.vue' +import {describeRequestError} from '@/lib/errors' import {getScoreSetShortName} from '@/lib/score-sets' import {useConfirm} from 'primevue/useconfirm' import CalibrationEditor from '@/components/calibration/CalibrationEditor.vue' @@ -258,17 +258,6 @@ type ScoreSet = components['schemas']['ScoreSet'] const CALIBRATION_ACTIONS = ['update', 'delete', 'publish', 'change_rank'] as const type CalibrationAuthorizations = Record<(typeof CALIBRATION_ACTIONS)[number], boolean> -function extractErrorDetail(error: unknown): string { - if ( - axios.isAxiosError(error) && - error.response?.data?.detail && - (typeof error.response.data.detail === 'string' || error.response.data.detail instanceof String) - ) { - return error.response.data.detail as string - } - return String(error) -} - export default { name: 'ScoreSetCalibrationsView', components: { @@ -510,7 +499,7 @@ export default { this.$toast.add({ severity: 'error', summary: 'Calibration Not Published', - detail: `An error occurred while publishing the calibration: ${extractErrorDetail(error)}. Please try again later.`, + detail: `An error occurred while publishing the calibration: ${describeRequestError(error)}. Please try again later.`, life: 4000 }) } @@ -534,7 +523,7 @@ export default { this.$toast.add({ severity: 'error', summary: 'Calibration Not Demoted', - detail: `An error occurred while demoting the calibration: ${extractErrorDetail(error)}. Please try again later.`, + detail: `An error occurred while demoting the calibration: ${describeRequestError(error)}. Please try again later.`, life: 4000 }) } @@ -555,7 +544,7 @@ export default { this.$toast.add({ severity: 'error', summary: 'Calibration Not Promoted', - detail: `An error occurred while promoting the calibration: ${extractErrorDetail(error)}. Please try again later.`, + detail: `An error occurred while promoting the calibration: ${describeRequestError(error)}. Please try again later.`, life: 4000 }) } @@ -583,7 +572,7 @@ export default { this.$toast.add({ severity: 'error', summary: 'Calibration Not Deleted', - detail: `An error occurred while deleting the calibration: ${extractErrorDetail(error)}. Please try again later.`, + detail: `An error occurred while deleting the calibration: ${describeRequestError(error)}. Please try again later.`, life: 4000 }) } @@ -610,7 +599,7 @@ export default { this.$toast.add({ severity: 'error', summary: 'Download Failed', - detail: `An error occurred while downloading the calibrations: ${extractErrorDetail(error)}. Please try again later.`, + detail: `An error occurred while downloading the calibrations: ${describeRequestError(error)}. Please try again later.`, life: 4000 }) } diff --git a/src/components/screens/ScoreSetCreator.vue b/src/components/screens/ScoreSetCreator.vue index 9d65a6cc..7a87d88e 100644 --- a/src/components/screens/ScoreSetCreator.vue +++ b/src/components/screens/ScoreSetCreator.vue @@ -405,7 +405,8 @@ import ToggleSwitch from 'primevue/toggleswitch' import {ref} from 'vue' import {useHead} from '@unhead/vue' -import {getExperiment, searchMyExperiments, createScoreSet, uploadVariantData, getErrorResponse} from '@/api/mavedb' +import {getExperiment, searchMyExperiments, createScoreSet, uploadVariantData} from '@/api/mavedb' +import {getErrorResponse} from '@/lib/errors' import CalibrationEditor from '@/components/calibration/CalibrationEditor.vue' import MvEmailPrompt from '@/components/common/MvEmailPrompt.vue' import ScoreSetContextFields from '@/components/forms/ScoreSetContextFields.vue' diff --git a/src/components/screens/ScoreSetEditor.vue b/src/components/screens/ScoreSetEditor.vue index 02762aee..49fbf953 100644 --- a/src/components/screens/ScoreSetEditor.vue +++ b/src/components/screens/ScoreSetEditor.vue @@ -257,7 +257,8 @@ import {useHead} from '@unhead/vue' import ToggleSwitch from 'primevue/toggleswitch' -import {searchMyExperiments, updateScoreSetWithVariants, getErrorResponse} from '@/api/mavedb' +import {searchMyExperiments, updateScoreSetWithVariants} from '@/api/mavedb' +import {getErrorResponse} from '@/lib/errors' import MvEmailPrompt from '@/components/common/MvEmailPrompt.vue' import MvEmptyState from '@/components/common/MvEmptyState.vue' import ScoreSetContextFields from '@/components/forms/ScoreSetContextFields.vue' diff --git a/src/components/screens/SearchVariantsScreen.vue b/src/components/screens/SearchVariantsScreen.vue index aba122a6..6678fc13 100644 --- a/src/components/screens/SearchVariantsScreen.vue +++ b/src/components/screens/SearchVariantsScreen.vue @@ -778,7 +778,8 @@ import { getAlleleByGnomad, getGeneBySymbol } from '@/api/clingen' -import {getCollection, getErrorResponse, lookupVariantsByClingenId} from '@/api/mavedb' +import {getCollection, lookupVariantsByClingenId} from '@/api/mavedb' +import {getErrorResponse} from '@/lib/errors' import {lookupVariantsByVrsDigest} from '@/api/mavedb/variants' import {useEntityCache} from '@/composables/entity-cache' import MvLoader from '@/components/common/MvLoader.vue' diff --git a/src/components/screens/SettingsScreen.vue b/src/components/screens/SettingsScreen.vue index ccd13d48..cade549b 100644 --- a/src/components/screens/SettingsScreen.vue +++ b/src/components/screens/SettingsScreen.vue @@ -153,15 +153,8 @@ import {defineComponent} from 'vue' import {useHead} from '@unhead/vue' import {useRouter} from 'vue-router' -import { - createAccessKey, - createRoleAccessKey, - deleteAccessKey, - getErrorResponse, - getMyCollections, - searchMyExperiments, - searchMyScoreSets -} from '@/api/mavedb' +import {createAccessKey, createRoleAccessKey, deleteAccessKey, getMyCollections, searchMyExperiments, searchMyScoreSets} from '@/api/mavedb' +import {getErrorResponse} from '@/lib/errors' import MvAccessKeyRow from '@/components/common/MvAccessKeyRow.vue' import MvOrcidLink from '@/components/common/MvOrcidLink.vue' import MvFloatField from '@/components/forms/MvFloatField.vue' diff --git a/src/components/screens/VariantScreen.vue b/src/components/screens/VariantScreen.vue index 14239880..8cae1126 100644 --- a/src/components/screens/VariantScreen.vue +++ b/src/components/screens/VariantScreen.vue @@ -3,21 +3,42 @@