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: add opt-in rich fasta headers with metadata #3448

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
3 changes: 3 additions & 0 deletions kubernetes/loculus/templates/_common-metadata.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,9 @@ organisms:
{{ if .description }}
description: {{ quote .description }}
{{ end }}
{{ if .richFastaHeaderFields}}
richFastaHeaderFields: {{ toJson .richFastaHeaderFields }}
{{ end }}
primaryKey: accessionVersion
inputFields: {{- include "loculus.inputFields" . | nindent 8 }}
- name: versionComment
Expand Down
1 change: 1 addition & 0 deletions kubernetes/loculus/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ defaultOrganismConfig: &defaultOrganismConfig
enabled: true
externalFields:
- ncbiReleaseDate
richFastaHeaderFields: ["displayName"]
### Field list
## General fields
# name: Key used across app to refer to this field (required)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
export type DownloadDataType =
| { type: 'metadata' }
| { type: 'unalignedNucleotideSequences'; segment?: string }
| {
type: 'unalignedNucleotideSequences';
segment?: string;
includeRichFastaHeaders?: boolean;
}
| { type: 'alignedNucleotideSequences'; segment?: string }
| { type: 'alignedAminoAcidSequences'; gene: string };

Expand All @@ -20,21 +24,3 @@ export const dataTypeForFilename = (dataType: DownloadDataType): string => {
return `aligned-aa-${dataType.gene}`;
}
};

/**
* Get the LAPIS endpoint where to download this data type from.
*/
export const getEndpoint = (dataType: DownloadDataType) => {
const segmentPath = (segment?: string) => (segment !== undefined ? `/${segment}` : '');

switch (dataType.type) {
case 'metadata':
return '/sample/details';
case 'unalignedNucleotideSequences':
return '/sample/unalignedNucleotideSequences' + segmentPath(dataType.segment);
case 'alignedNucleotideSequences':
return '/sample/alignedNucleotideSequences' + segmentPath(dataType.segment);
case 'alignedAminoAcidSequences':
return `/sample/alignedAminoAcidSequences/${dataType.gene}`;
}
};
33 changes: 24 additions & 9 deletions website/src/components/SearchPage/DownloadDialog/DownloadForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const DownloadForm: FC<DownloadFormProps> = ({ referenceGenomesSequenceNa
const [unalignedNucleotideSequence, setUnalignedNucleotideSequence] = useState(0);
const [alignedNucleotideSequence, setAlignedNucleotideSequence] = useState(0);
const [alignedAminoAcidSequence, setAlignedAminoAcidSequence] = useState(0);
const [includeRichFastaHeaders, setIncludeRichFastaHeaders] = useState(0);

const isMultiSegmented = referenceGenomesSequenceNames.nucleotideSequences.length > 1;

Expand All @@ -34,6 +35,7 @@ export const DownloadForm: FC<DownloadFormProps> = ({ referenceGenomesSequenceNa
segment: isMultiSegmented
? referenceGenomesSequenceNames.nucleotideSequences[unalignedNucleotideSequence]
: undefined,
includeRichFastaHeaders: includeRichFastaHeaders === 1,
};
break;
case 2:
Expand Down Expand Up @@ -68,6 +70,7 @@ export const DownloadForm: FC<DownloadFormProps> = ({ referenceGenomesSequenceNa
unalignedNucleotideSequence,
alignedNucleotideSequence,
alignedAminoAcidSequence,
includeRichFastaHeaders,
isMultiSegmented,
referenceGenomesSequenceNames.nucleotideSequences,
referenceGenomesSequenceNames.genes,
Expand Down Expand Up @@ -114,19 +117,30 @@ export const DownloadForm: FC<DownloadFormProps> = ({ referenceGenomesSequenceNa
{ label: <>Metadata</> },
{
label: <>Raw nucleotide sequences</>,
subOptions: isMultiSegmented ? (
subOptions: (
<div className='px-8'>
<DropdownOptionBlock
name='unalignedNucleotideSequences'
options={referenceGenomesSequenceNames.nucleotideSequences.map((segment) => ({
label: <>{segment}</>,
}))}
selected={unalignedNucleotideSequence}
onSelect={setUnalignedNucleotideSequence}
{isMultiSegmented ? (
<DropdownOptionBlock
name='unalignedNucleotideSequences'
options={referenceGenomesSequenceNames.nucleotideSequences.map((segment) => ({
label: <>{segment}</>,
}))}
selected={unalignedNucleotideSequence}
onSelect={setUnalignedNucleotideSequence}
disabled={dataType !== 1}
/>
) : undefined}
<RadioOptionBlock
name='richFastaHeaders'
title='FASTA header style'
options={[{ label: <>Accession only</> }, { label: <>Accession and metadata</> }]}
selected={includeRichFastaHeaders}
onSelect={setIncludeRichFastaHeaders}
disabled={dataType !== 1}
variant='nested'
/>
</div>
) : undefined,
),
},
{
label: <>Aligned nucleotide sequences</>,
Expand Down Expand Up @@ -170,6 +184,7 @@ export const DownloadForm: FC<DownloadFormProps> = ({ referenceGenomesSequenceNa
options={[{ label: <>None</> }, { label: <>Zstandard</> }, { label: <>Gzip</> }]}
selected={compression}
onSelect={setCompression}
disabled={dataType === 1 && includeRichFastaHeaders === 1} // Rich headers don't support compression
/>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import kebabCase from 'just-kebab-case';

import { getEndpoint, dataTypeForFilename, type DownloadDataType } from './DownloadDataType.ts';
import { dataTypeForFilename, type DownloadDataType } from './DownloadDataType.ts';
import type { SequenceFilter } from './SequenceFilters.tsx';
import { IS_REVOCATION_FIELD, metadataDefaultDownloadDataFormat, VERSION_STATUS_FIELD } from '../../../settings.ts';
import { versionStatuses } from '../../../types/lapis.ts';
Expand All @@ -21,19 +21,21 @@ export type DownloadOption = {
export class DownloadUrlGenerator {
private readonly organism: string;
private readonly lapisUrl: string;
private readonly richFastaHeaderFields: string[];

/**
* Create new DownloadUrlGenerator with the given properties.
* @param organism The organism, will be part of the filename.
* @param lapisUrl The lapis API URL for downloading.
*/
constructor(organism: string, lapisUrl: string) {
constructor(organism: string, lapisUrl: string, richFastaHeaderFields: string[] = ['accessionVersion']) {
this.organism = organism;
this.lapisUrl = lapisUrl;
this.richFastaHeaderFields = richFastaHeaderFields;
}

public generateDownloadUrl(downloadParameters: SequenceFilter, option: DownloadOption) {
const baseUrl = `${this.lapisUrl}${getEndpoint(option.dataType)}`;
const baseUrl = this.downloadEndpoint(option.dataType);
const params = new URLSearchParams();

params.set('downloadAsFile', 'true');
Expand All @@ -52,8 +54,20 @@ export class DownloadUrlGenerator {
params.set('compression', option.compression);
}

if (
option.dataType.type === 'unalignedNucleotideSequences' &&
option.dataType.includeRichFastaHeaders === true
) {
// get from config
params.set('headerFields', this.richFastaHeaderFields.join(','));
}

downloadParameters.toUrlSearchParams().forEach(([name, value]) => {
params.append(name, value);
// Empty values are not allowed for e.g. aminoAcidInsertion filters
// Hence, filter out empty values
if (value !== '') {
params.append(name, value);
}
});

return {
Expand All @@ -69,4 +83,21 @@ export class DownloadUrlGenerator {
const timestamp = new Date().toISOString().slice(0, 16).replace(':', '');
return `${organism}_${dataType}_${timestamp}`;
}

private readonly downloadEndpoint = (dataType: DownloadDataType) => {
const segmentPath = (segment?: string) => (segment !== undefined ? '/' + segment : '');

switch (dataType.type) {
case 'metadata':
return this.lapisUrl + '/sample/details';
case 'unalignedNucleotideSequences':
return dataType.includeRichFastaHeaders === true
? '/' + this.organism + '/api/sequences' + segmentPath(dataType.segment)
: this.lapisUrl + '/sample/unalignedNucleotideSequences' + segmentPath(dataType.segment);
case 'alignedNucleotideSequences':
return this.lapisUrl + '/sample/alignedNucleotideSequences' + segmentPath(dataType.segment);
case 'alignedAminoAcidSequences':
return this.lapisUrl + '/sample/alignedAminoAcidSequences/' + dataType.gene;
}
};
}
19 changes: 14 additions & 5 deletions website/src/components/SearchPage/DownloadDialog/OptionBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export type OptionBlockProps = {
selected: number;
onSelect: (index: number) => void;
disabled?: boolean;
variant?: 'default' | 'nested'; // New prop
};

export const RadioOptionBlock: FC<OptionBlockProps> = ({
Expand All @@ -19,22 +20,30 @@ export const RadioOptionBlock: FC<OptionBlockProps> = ({
selected,
onSelect,
disabled = false,
variant = 'default',
}) => {
return (
<div className='basis-1/2 justify-start'>
{title !== undefined && <h4 className='font-bold'>{title}</h4>}
// <div className='basis-1/2 justify-start'>
<div className={(variant === 'nested' ? 'px-4 mr-10' : '') + ' basis-1/2 justify-start'}>
{title !== undefined && (
<h4 className={variant === 'nested' ? 'text-sm font-normal' : 'font-bold'}>{title}</h4>
)}
{options.map((option, index) => (
<div key={index}>
<div key={index} className={disabled ? 'bg-gray-100' : ''}>
<label className='label justify-start py-1 items-baseline'>
<input
type='radio'
name={name}
className='mr-4 text-primary-600 focus:ring-primary-600 relative bottom-[-0.2rem]'
className='mr-4 text-primary-600 focus:ring-primary-600 relative bottom-[-0.2rem] disabled:opacity-50'
checked={index === selected}
onChange={() => onSelect(index)}
disabled={disabled}
/>
<span className='label-text'>{option.label}</span>
<span
className={`label-text ${disabled ? 'text-gray-500' : ''} ${variant === 'nested' ? 'text-sm' : ''}`}
>
{option.label}
</span>
</label>
{option.subOptions}
</div>
Expand Down
2 changes: 1 addition & 1 deletion website/src/components/SearchPage/SearchFullUI.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ export const InnerSearchFullUI = ({
};

const lapisUrl = getLapisUrl(clientConfig, organism);
const downloadUrlGenerator = new DownloadUrlGenerator(organism, lapisUrl);
const downloadUrlGenerator = new DownloadUrlGenerator(organism, lapisUrl, schema.richFastaHeaderFields);

const hooks = lapisClientHooks(lapisUrl).zodiosHooks;
const aggregatedHook = hooks.useAggregated({}, {});
Expand Down
Loading
Loading