Skip to content

Commit

Permalink
feat(website): search reactoring, fixes, and new features (#1890)
Browse files Browse the repository at this point in the history
  • Loading branch information
theosanderson authored May 21, 2024
1 parent de129ec commit 3b19450
Show file tree
Hide file tree
Showing 35 changed files with 1,134 additions and 1,541 deletions.
3 changes: 3 additions & 0 deletions kubernetes/loculus/templates/_common-metadata.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,9 @@ fields:
{{- if .initiallyVisible }}
initiallyVisible: {{ .initiallyVisible }}
{{- end }}
{{- if or (or (eq .type "timestamp") (eq .type "date")) ( .rangeSearch) }}
rangeSearch: true
{{- end }}
{{- if .hideOnSequenceDetailsPage }}
hideOnSequenceDetailsPage: {{ .hideOnSequenceDetailsPage }}
{{- end }}
Expand Down
41 changes: 17 additions & 24 deletions website/src/components/SearchPage/CustomizeModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,31 +22,26 @@ const CheckboxField: React.FC<CheckboxFieldProps> = ({ label, checked, onChange,
</div>
);

interface FieldValue {
name: string;
label: string;
isVisible?: boolean;
notSearchable?: boolean;
}

interface CustomizeModalProps {
isCustomizeModalOpen: boolean;
toggleCustomizeModal: () => void;
alwaysPresentFieldNames: string[];
fieldValues: FieldValue[];
handleFieldVisibilityChange: (fieldName: string, isVisible: boolean) => void;
visibilities: Map<string, boolean>;
setAVisibility: (fieldName: string, isVisible: boolean) => void;
nameToLabelMap: Record<string, string>;
}

export const CustomizeModal: React.FC<CustomizeModalProps> = ({
isCustomizeModalOpen,
toggleCustomizeModal,
alwaysPresentFieldNames,
fieldValues,
handleFieldVisibilityChange,
visibilities,
setAVisibility,
nameToLabelMap,
}) => {
return (
<Transition appear show={isCustomizeModalOpen}>
<Dialog as='div' className='fixed inset-0 z-10 overflow-y-auto' onClose={toggleCustomizeModal}>
<Dialog as='div' className='fixed inset-0 z-50 overflow-y-auto' onClose={toggleCustomizeModal}>
<div className='min-h-screen px-4 text-center'>
<Dialog.Overlay className='fixed inset-0 bg-black opacity-30' />

Expand All @@ -66,18 +61,16 @@ export const CustomizeModal: React.FC<CustomizeModalProps> = ({
<CheckboxField key={fieldName} label={fieldName} checked disabled />
))}

{fieldValues
.filter((field) => field.notSearchable !== true)
.map((field) => (
<CheckboxField
key={field.name}
label={field.label}
checked={field.isVisible !== false}
onChange={(e) => {
handleFieldVisibilityChange(field.name, e.target.checked);
}}
/>
))}
{Array.from(visibilities).map(([fieldName, visible]) => (
<CheckboxField
key={fieldName}
label={nameToLabelMap[fieldName]}
checked={visible}
onChange={(e) => {
setAVisibility(fieldName, e.target.checked);
}}
/>
))}
</div>

<div className='mt-6'>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,22 @@
import type { FC } from 'react';

import type { FilterValue, MutationFilter } from '../../../types/config.ts';
import type { FieldValues } from '../../../types/config';

type ActiveDownloadFiltersProps = {
metadataFilter: FilterValue[];
mutationFilter: MutationFilter;
lapisSearchParameters: Record<string, any>;
hiddenFieldValues: FieldValues;
};

export const ActiveDownloadFilters: FC<ActiveDownloadFiltersProps> = ({ metadataFilter, mutationFilter }) => {
const filterValues: FilterValue[] = metadataFilter.filter((f) => f.filterValue.length > 0);
[
{ name: 'nucleotideMutations', value: mutationFilter.nucleotideMutationQueries },
{ name: 'aminoAcidMutations', value: mutationFilter.aminoAcidMutationQueries },
{ name: 'nucleotideInsertion', value: mutationFilter.nucleotideInsertionQueries },
{ name: 'aminoAcidInsertions', value: mutationFilter.aminoAcidInsertionQueries },
].forEach(({ name, value }) => {
if (value !== undefined && value.length > 0) {
filterValues.push({ name, filterValue: value.join(', ') });
}
});
export const ActiveDownloadFilters: FC<ActiveDownloadFiltersProps> = ({ lapisSearchParameters, hiddenFieldValues }) => {
let filterValues = Object.entries(lapisSearchParameters)
.filter((vals) => vals[1] !== undefined && vals[1] !== '')
.filter(([name, val]) => !(Object.keys(hiddenFieldValues).includes(name) && hiddenFieldValues[name] === val))
.map(([name, filterValue]) => ({ name, filterValue }));

filterValues = filterValues.filter(({ filterValue }) => filterValue.length > 0);

if (filterValues.length === 0) {
return undefined;
return null;
}

return (
Expand All @@ -30,7 +25,7 @@ export const ActiveDownloadFilters: FC<ActiveDownloadFiltersProps> = ({ metadata
<div className='flex flex-row flex-wrap gap-4'>
{filterValues.map(({ name, filterValue }) => (
<div key={name} className='border-black border rounded-full px-2 py-1 text-sm'>
{name}: {filterValue}
{name}: {typeof filterValue === 'object' ? filterValue.join(', ') : filterValue}
</div>
))}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,19 @@ import { type FC, type MouseEventHandler, useMemo } from 'react';

import { type DownloadOption, generateDownloadUrl } from './generateDownloadUrl.ts';
import { approxMaxAcceptableUrlLength } from '../../../routes/routes.ts';
import type { AccessionFilter, FilterValue, MutationFilter } from '../../../types/config.ts';

type DownloadButtonProps = {
downloadOption: DownloadOption | undefined;
lapisUrl: string;
accessionFilter: AccessionFilter;
metadataFilter: FilterValue[];
mutationFilter: MutationFilter;
lapisSearchParameters: Record<string, any>;
disabled?: boolean;
onClick?: () => void;
};

export const DownloadButton: FC<DownloadButtonProps> = ({
downloadOption,
lapisUrl,
accessionFilter,
metadataFilter,
mutationFilter,
lapisSearchParameters,
disabled = false,
onClick,
}) => {
Expand All @@ -37,13 +32,7 @@ export const DownloadButton: FC<DownloadButtonProps> = ({
};
}

const { url, baseUrl, params } = generateDownloadUrl(
accessionFilter,
metadataFilter,
mutationFilter,
downloadOption,
lapisUrl,
);
const { url, baseUrl, params } = generateDownloadUrl(lapisSearchParameters, downloadOption, lapisUrl);
const useGet = url.length <= approxMaxAcceptableUrlLength;
if (useGet) {
return {
Expand All @@ -62,7 +51,7 @@ export const DownloadButton: FC<DownloadButtonProps> = ({
}
},
};
}, [downloadOption, disabled, accessionFilter, metadataFilter, mutationFilter, lapisUrl, onClick]);
}, [downloadOption, disabled, lapisSearchParameters, lapisUrl, onClick]);

return (
<a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import userEvent from '@testing-library/user-event';
import { beforeAll, describe, expect, test, vi } from 'vitest';

import { DownloadDialog } from './DownloadDialog.tsx';
import type { AccessionFilter, FilterValue, MutationFilter } from '../../../types/config.ts';
import type { ReferenceGenomesSequenceNames } from '../../../types/referencesGenomes.ts';

const defaultReferenceGenome: ReferenceGenomesSequenceNames = {
Expand All @@ -13,20 +12,13 @@ const defaultReferenceGenome: ReferenceGenomesSequenceNames = {

const defaultLapisUrl = 'https://lapis';

async function renderDialog(
accessionFilter: AccessionFilter = {},
metadataFilter: FilterValue[] = [],
mutationFilter: MutationFilter = {},
referenceGenomesSequenceNames: ReferenceGenomesSequenceNames = defaultReferenceGenome,
lapisUrl: string = defaultLapisUrl,
) {
async function renderDialog(lapisSearchParameters: any = {}) {
render(
<DownloadDialog
accessionFilter={accessionFilter}
metadataFilter={metadataFilter}
mutationFilter={mutationFilter}
referenceGenomesSequenceNames={referenceGenomesSequenceNames}
lapisUrl={lapisUrl}
lapisSearchParameters={lapisSearchParameters}
referenceGenomesSequenceNames={defaultReferenceGenome}
lapisUrl={defaultLapisUrl}
hiddenFieldValues={{}}
/>,
);

Expand All @@ -49,12 +41,11 @@ describe('DownloadDialog', () => {
});

test('should display active filters if there are some', async () => {
await renderDialog({}, [{ name: 'field1', filterValue: 'value1' }], {
nucleotideMutationQueries: ['A123T', 'G234C'],
});
await renderDialog({ field1: 'value1', nucleotideMutations: 'A123T,G234C' });
expect(screen.queryByText(/Active filters/)).toBeInTheDocument();
expect(screen.queryByText('field1: value1')).toBeInTheDocument();
expect(screen.queryByText(/A123T, G234C/)).toBeInTheDocument();

expect(screen.queryByText(/A123T,G234C/)).toBeInTheDocument();
});

test('should not display active filters if there are none', async () => {
Expand All @@ -77,7 +68,7 @@ describe('DownloadDialog', () => {
});

test('should generate the right download link', async () => {
await renderDialog({ accession: ['accession1', 'accession2'] }, [{ name: 'field1', filterValue: 'value1' }]);
await renderDialog({ accession: ['accession1', 'accession2'], field1: 'value1' });
await checkAgreement();

expect(getDownloadHref()).toBe(
Expand Down
21 changes: 10 additions & 11 deletions website/src/components/SearchPage/DownloadDialog/DownloadDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,21 @@ import { DownloadButton } from './DownloadButton.tsx';
import { DownloadForm } from './DownloadForm.tsx';
import { type DownloadOption } from './generateDownloadUrl.ts';
import { routes } from '../../../routes/routes.ts';
import type { AccessionFilter, FilterValue, MutationFilter } from '../../../types/config.ts';
import type { FieldValues } from '../../../types/config.ts';
import type { ReferenceGenomesSequenceNames } from '../../../types/referencesGenomes.ts';

type DownloadDialogProps = {
accessionFilter: AccessionFilter;
metadataFilter: FilterValue[];
mutationFilter: MutationFilter;
lapisSearchParameters: Record<string, any>;
referenceGenomesSequenceNames: ReferenceGenomesSequenceNames;
lapisUrl: string;
hiddenFieldValues: FieldValues;
};

export const DownloadDialog: FC<DownloadDialogProps> = ({
accessionFilter,
metadataFilter,
mutationFilter,
lapisSearchParameters,
referenceGenomesSequenceNames,
lapisUrl,
hiddenFieldValues,
}) => {
const dialogRef = useRef<HTMLDialogElement>(null);
const [downloadOption, setDownloadOption] = useState<DownloadOption | undefined>();
Expand Down Expand Up @@ -53,7 +51,10 @@ export const DownloadDialog: FC<DownloadDialogProps> = ({

<h3 className='font-bold text-2xl mb-4'>Download</h3>

<ActiveDownloadFilters metadataFilter={metadataFilter} mutationFilter={mutationFilter} />
<ActiveDownloadFilters
lapisSearchParameters={lapisSearchParameters}
hiddenFieldValues={hiddenFieldValues}
/>
<DownloadForm
referenceGenomesSequenceNames={referenceGenomesSequenceNames}
onChange={setDownloadOption}
Expand Down Expand Up @@ -82,9 +83,7 @@ export const DownloadDialog: FC<DownloadDialogProps> = ({
disabled={!agreedToDataUseTerms}
lapisUrl={lapisUrl}
downloadOption={downloadOption}
accessionFilter={accessionFilter}
metadataFilter={metadataFilter}
mutationFilter={mutationFilter}
lapisSearchParameters={lapisSearchParameters}
onClick={closeDialog}
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { IS_REVOCATION_FIELD, metadataDefaultDownloadDataFormat, VERSION_STATUS_FIELD } from '../../../settings.ts';
import type { AccessionFilter, FilterValue, MutationFilter } from '../../../types/config.ts';
import { siloVersionStatuses } from '../../../types/lapis.ts';

export type DownloadDataType =
Expand All @@ -18,14 +17,13 @@ export type DownloadOption = {
};

export const generateDownloadUrl = (
accessionFilter: AccessionFilter,
metadataFilter: FilterValue[],
mutationFilter: MutationFilter,
lapisSearchParameters: Record<string, any>,
option: DownloadOption,
lapisUrl: string,
) => {
const baseUrl = `${lapisUrl}${getEndpoint(option.dataType)}`;
const params = new URLSearchParams();

params.set('downloadAsFile', 'true');
if (!option.includeOldData) {
params.set(VERSION_STATUS_FIELD, siloVersionStatuses.latestVersion);
Expand All @@ -40,31 +38,31 @@ export const generateDownloadUrl = (
if (option.compression !== undefined) {
params.set('compression', option.compression);
}
if (accessionFilter.accession !== undefined) {
for (const accession of accessionFilter.accession) {
if (lapisSearchParameters.accession !== undefined) {
for (const accession of lapisSearchParameters.accession) {
params.append('accession', accession);
}
}
for (const { name, filterValue } of metadataFilter) {
if (filterValue.trim().length > 0) {
params.set(name, filterValue);

const mutationKeys = ['nucleotideMutations', 'aminoAcidMutations', 'nucleotideInsertions', 'aminoAcidInsertions'];

for (const [key, value] of Object.entries(lapisSearchParameters)) {
// Skip accession and mutations
if (key === 'accession' || mutationKeys.includes(key)) {
continue;
}
const trimmedValue = value.trim();
if (trimmedValue.length > 0) {
params.set(key, trimmedValue);
}
}
if (mutationFilter.nucleotideMutationQueries !== undefined && mutationFilter.nucleotideMutationQueries.length > 0) {
params.set('nucleotideMutations', mutationFilter.nucleotideMutationQueries.join(','));
}
if (mutationFilter.aminoAcidMutationQueries !== undefined && mutationFilter.aminoAcidMutationQueries.length > 0) {
params.set('aminoAcidMutations', mutationFilter.aminoAcidMutationQueries.join(','));
}
if (
mutationFilter.nucleotideInsertionQueries !== undefined &&
mutationFilter.nucleotideInsertionQueries.length > 0
) {
params.set('nucleotideInsertions', mutationFilter.nucleotideInsertionQueries.join(','));
}
if (mutationFilter.aminoAcidInsertionQueries !== undefined && mutationFilter.aminoAcidInsertionQueries.length > 0) {
params.set('aminoAcidInsertions', mutationFilter.aminoAcidInsertionQueries.join(','));
}

mutationKeys.forEach((key) => {
if (lapisSearchParameters[key] !== undefined) {
params.set(key, lapisSearchParameters[key].join(','));
}
});

return {
url: `${baseUrl}?${params}`,
baseUrl,
Expand Down
Loading

0 comments on commit 3b19450

Please sign in to comment.