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(website): SSR on initial load of search page #2290

Merged
merged 35 commits into from
Jul 22, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
094b5b3
refactor query parsing
theosanderson Jul 11, 2024
4d381ac
wip
theosanderson Jul 11, 2024
4eba0bd
comment out
theosanderson Jul 11, 2024
052caa9
type fixes
theosanderson Jul 11, 2024
0bb409a
[skip CI]
theosanderson Jul 11, 2024
c198cc9
update
theosanderson Jul 11, 2024
6e2bbf7
clean up
theosanderson Jul 11, 2024
2194f41
move parseMutationString
theosanderson Jul 11, 2024
dc3ffd9
update
theosanderson Jul 11, 2024
1dc60c1
Update KeycloakClientManager.ts
theosanderson Jul 11, 2024
9b36fa8
wip - need to sort out: pagination, ordering, flash reload
theosanderson Jul 12, 2024
19056fd
fix not actually searching
theosanderson Jul 12, 2024
ebcd89f
fix longstanding issue where adding random query parameters would bor…
theosanderson Jul 12, 2024
b925128
bring page into url client side
theosanderson Jul 12, 2024
c38e0b7
provide initial query dict
theosanderson Jul 12, 2024
135dbd8
Merge branch 'main' into search-ssr
theosanderson Jul 12, 2024
2eaebbc
working quite nicely
theosanderson Jul 12, 2024
843cb96
format
theosanderson Jul 12, 2024
2cfdb54
linted
theosanderson Jul 12, 2024
2f6f882
delete file
theosanderson Jul 12, 2024
6f2274a
types are fixed
theosanderson Jul 12, 2024
47cc221
update
theosanderson Jul 12, 2024
eaa8eca
oops
theosanderson Jul 12, 2024
f0771cc
fix
theosanderson Jul 12, 2024
24605ec
format
theosanderson Jul 12, 2024
9a152ac
don't set page to "1" ever in URL
theosanderson Jul 12, 2024
a09a4b3
format
theosanderson Jul 12, 2024
7b986f7
locale
theosanderson Jul 12, 2024
34a2d2f
Update SearchFullUI.spec.tsx
theosanderson Jul 12, 2024
0e38724
Merge branch 'main' into search-ssr
theosanderson Jul 16, 2024
90a5ed2
Fix missing accession, mutation fields
theosanderson Jul 18, 2024
5170130
Merge branch 'main' into search-ssr
theosanderson Jul 18, 2024
06e13a6
Merge branch 'main' into search-ssr
theosanderson Jul 21, 2024
05832eb
Automated code formatting
Jul 21, 2024
999815b
empty
theosanderson Jul 21, 2024
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
4 changes: 4 additions & 0 deletions website/src/components/SearchPage/SearchFullUI.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ function renderSearchFullUI({
tableColumns: ['field1', 'field3'],
primaryKey: 'accession',
} as Schema,
initialData: [],

initialCount: 0,
initialQueryDict: {},
};

render(
Expand Down
70 changes: 38 additions & 32 deletions website/src/components/SearchPage/SearchFullUI.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
VISIBILITY_PREFIX,
COLUMN_VISIBILITY_PREFIX,
getLapisSearchParameters,
getMetadataSchemaWithExpandedRanges,
} from '../../utils/search.ts';
import ErrorBox from '../common/ErrorBox.tsx';

Expand All @@ -42,6 +43,9 @@ interface InnerSearchFullUIProps {
clientConfig: ClientConfig;
schema: Schema;
hiddenFieldValues?: FieldValues;
initialData: TableSequenceData[];
initialCount: number;
initialQueryDict: QueryState;
}
interface QueryState {
[key: string]: string;
Expand All @@ -55,45 +59,25 @@ export const InnerSearchFullUI = ({
clientConfig,
schema,
hiddenFieldValues,
initialData,
initialCount,
initialQueryDict,
}: InnerSearchFullUIProps) => {
if (!hiddenFieldValues) {
hiddenFieldValues = {};
}

const metadataSchema = schema.metadata;

const [isColumnModalOpen, setIsColumnModalOpen] = useState(false);

const metadataSchemaWithExpandedRanges = useMemo(() => {
const result = [];
for (const field of metadataSchema) {
if (field.rangeSearch === true) {
const fromField = {
...field,
name: `${field.name}From`,
label: `From`,
fieldGroup: field.name,
fieldGroupDisplayName: field.displayName ?? sentenceCase(field.name),
};
const toField = {
...field,
name: `${field.name}To`,
label: `To`,
fieldGroup: field.name,
fieldGroupDisplayName: field.displayName ?? sentenceCase(field.name),
};
result.push(fromField);
result.push(toField);
} else {
result.push(field);
}
}
return result;
return getMetadataSchemaWithExpandedRanges(metadataSchema);
}, [metadataSchema]);

const [previewedSeqId, setPreviewedSeqId] = useState<string | null>(null);
const [previewHalfScreen, setPreviewHalfScreen] = useState(false);
const [state, setState] = useQueryAsState({});
const [page, setPage] = useState(1);
const [state, setState] = useQueryAsState(initialQueryDict);

const searchVisibilities = useMemo(() => {
return getFieldVisibilitiesFromQuery(schema, state);
Expand All @@ -116,6 +100,15 @@ export const InnerSearchFullUI = ({

const orderDirection = state.order ?? schema.defaultOrder ?? 'ascending';

const page = parseInt(state.page ?? '1', 10);

const setPage = (newPage: number) => {
setState((prev: QueryState) => ({
...prev,
page: newPage.toString(),
}));
};

const setOrderByField = (field: string) => {
setState((prev: QueryState) => ({
...prev,
Expand All @@ -130,8 +123,8 @@ export const InnerSearchFullUI = ({
};

const fieldValues = useMemo(() => {
return getFieldValuesFromQuery(state, hiddenFieldValues);
}, [state, hiddenFieldValues]);
return getFieldValuesFromQuery(state, hiddenFieldValues, schema);
}, [state, hiddenFieldValues, schema]);

const setAFieldValue: SetAFieldValue = (fieldName, value) => {
setState((prev: any) => {
Expand Down Expand Up @@ -203,16 +196,20 @@ export const InnerSearchFullUI = ({

const [oldData, setOldData] = useState<TableSequenceData[] | null>(null);
const [oldCount, setOldCount] = useState<number | null>(null);
const [firstClientSideLoadOfDataCompleted, setFirstClientSideLoadOfDataCompleted] = useState(false);
const [firstClientSideLoadOfCountCompleted, setFirstClientSideLoadOfCountCompleted] = useState(false);

useEffect(() => {
if (detailsHook.data?.data && oldData !== detailsHook.data.data) {
setOldData(detailsHook.data.data);
setFirstClientSideLoadOfDataCompleted(true);
}
}, [detailsHook.data?.data, oldData]);

useEffect(() => {
if (aggregatedHook.data?.data && oldCount !== aggregatedHook.data.data[0].count) {
setOldCount(aggregatedHook.data.data[0].count);
setFirstClientSideLoadOfCountCompleted(true);
}
}, [aggregatedHook.data?.data, oldCount]);

Expand Down Expand Up @@ -284,7 +281,13 @@ export const InnerSearchFullUI = ({
{!(totalSequences === undefined && oldCount === null) && (
<div
className={`
${detailsHook.isLoading || aggregatedHook.isLoading ? 'opacity-50 pointer-events-none' : ''}
${
!(firstClientSideLoadOfCountCompleted && firstClientSideLoadOfDataCompleted)
? 'cursor-wait pointer-events-none'
: detailsHook.isLoading || aggregatedHook.isLoading
? 'opacity-50 pointer-events-none'
: ''
}
`}
>
<div className='text-sm text-gray-800 mb-6 justify-between flex md:px-6 items-baseline'>
Expand All @@ -294,10 +297,13 @@ export const InnerSearchFullUI = ({
? totalSequences.toLocaleString()
: oldCount !== null
? oldCount.toLocaleString()
: ''}{' '}
: initialCount}{' '}
sequence
{totalSequences === 1 ? '' : 's'}
{detailsHook.isLoading || aggregatedHook.isLoading ? (
{detailsHook.isLoading ||
aggregatedHook.isLoading ||
!firstClientSideLoadOfCountCompleted ||
!firstClientSideLoadOfDataCompleted ? (
<span className='loading loading-spinner loading-xs ml-3 appearSlowly'></span>
) : null}
</div>
Expand All @@ -323,7 +329,7 @@ export const InnerSearchFullUI = ({
data={
detailsHook.data?.data !== undefined
? (detailsHook.data.data as TableSequenceData[])
: oldData ?? []
: oldData ?? initialData
}
setPreviewedSeqId={setPreviewedSeqId}
previewedSeqId={previewedSeqId}
Expand Down
6 changes: 5 additions & 1 deletion website/src/components/SearchPage/useQueryAsState.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ export default function useQueryAsState(defaultDict) {
for (const [key, value] of urlParams) {
newDict[key] = value;
}
setValueDict(newDict);

setValueDict( // only change if actually different
(prev) =>
JSON.stringify(prev) === JSON.stringify(newDict) ? prev : newDict
);
}, []);

useEffect(() => {
Expand Down
14 changes: 14 additions & 0 deletions website/src/pages/[organism]/search/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { siloVersionStatuses } from '../../../types/lapis';
import { getAccessToken } from '../../../utils/getAccessToken';
import { getMyGroups } from '../../../utils/getMyGroups';
import { getReferenceGenomesSequenceNames } from '../../../utils/search';
import { performLapisSearchQueries } from '../../../utils/serversideSearch';

const hiddenFieldValues = {
[VERSION_STATUS_FIELD]: siloVersionStatuses.latestVersion,
[IS_REVOCATION_FIELD]: 'false',
Expand All @@ -29,6 +31,15 @@ const accessToken = getAccessToken(Astro.locals.session);
const myGroups = accessToken !== undefined ? await getMyGroups(accessToken) : [];

const referenceGenomeSequenceNames = getReferenceGenomesSequenceNames(cleanedOrganism.key);

const initialQueryDict = Object.fromEntries(Astro.url.searchParams.entries());
const { data, totalCount } = await performLapisSearchQueries(
initialQueryDict,
schema,
referenceGenomeSequenceNames,
hiddenFieldValues,
cleanedOrganism.key,
);
---

<BaseLayout title={`${cleanedOrganism.displayName} - Browse`} noHorizontalPadding>
Expand All @@ -43,5 +54,8 @@ const referenceGenomeSequenceNames = getReferenceGenomesSequenceNames(cleanedOrg
accessToken={accessToken}
referenceGenomesSequenceNames={referenceGenomeSequenceNames}
hiddenFieldValues={hiddenFieldValues}
initialData={data}
initialCount={totalCount}
initialQueryDict={initialQueryDict}
/>
</BaseLayout>
16 changes: 16 additions & 0 deletions website/src/pages/[organism]/submission/[groupId]/released.astro
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import { GROUP_ID_FIELD, VERSION_STATUS_FIELD } from '../../../../settings';
import { siloVersionStatuses } from '../../../../types/lapis';
import { getAccessToken } from '../../../../utils/getAccessToken';
import { getReferenceGenomesSequenceNames } from '../../../../utils/search';
import { performLapisSearchQueries } from '../../../../utils/serversideSearch';
import { getGroupsAndCurrentGroup } from '../../../../utils/submissionPages';

const groupsResult = await getGroupsAndCurrentGroup(Astro.params, Astro.locals.session);
if (groupsResult.isErr()) {
return new Response(undefined, { status: groupsResult.error.status });
Expand All @@ -34,9 +36,20 @@ const hiddenFieldValues = {
[VERSION_STATUS_FIELD]: siloVersionStatuses.latestVersion,
[GROUP_ID_FIELD]: group.groupId,
};

const initialQueryDict = Object.fromEntries(Astro.url.searchParams.entries());
const { data, totalCount } = await performLapisSearchQueries(
initialQueryDict,
schema,
referenceGenomeSequenceNames,
hiddenFieldValues,
cleanedOrganism.key,
);
---

<SubmissionPageWrapper groupsResult={groupsResult} title='Released sequences'>
<h1 class='title px-3 py-2 ml-1'>Search</h1>

<SearchFullUI
client:load
clientConfig={clientConfig}
Expand All @@ -46,5 +59,8 @@ const hiddenFieldValues = {
accessToken={accessToken}
referenceGenomesSequenceNames={referenceGenomeSequenceNames}
hiddenFieldValues={hiddenFieldValues}
initialData={data}
initialCount={totalCount}
initialQueryDict={initialQueryDict}
/>
</SubmissionPageWrapper>
49 changes: 40 additions & 9 deletions website/src/utils/search.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { sentenceCase } from 'change-case';

import { type BaseType } from './sequenceTypeHelpers';
import type { TableSequenceData } from '../components/SearchPage/Table';
import { getReferenceGenomes } from '../config';
import type { MetadataFilter, Schema } from '../types/config';
import type { Metadata, MetadataFilter, Schema } from '../types/config';
import type { ReferenceGenomesSequenceNames, ReferenceAccession, NamedSequence } from '../types/referencesGenomes';

export const VISIBILITY_PREFIX = 'visibility_';

export type MutationQuery = {
Expand All @@ -13,8 +16,9 @@ export type MutationQuery = {

export const COLUMN_VISIBILITY_PREFIX = 'column_';

const ORDER_KEY = 'orderBy';
const ORDER_DIRECTION_KEY = 'order';
export const ORDER_KEY = 'orderBy';
export const ORDER_DIRECTION_KEY = 'order';
export const PAGE_KEY = 'page';

export type SearchResponse = {
data: TableSequenceData[];
Expand Down Expand Up @@ -79,19 +83,46 @@ export const getColumnVisibilitiesFromQuery = (schema: Schema, state: Record<str
const initiallyVisibleAccessor: VisibilityAccessor = (field) => schema.tableColumns.includes(field.name);
return getFieldOrColumnVisibilitiesFromQuery(schema, state, COLUMN_VISIBILITY_PREFIX, initiallyVisibleAccessor);
};
export const getMetadataSchemaWithExpandedRanges = (metadataSchema: Metadata[]) => {
theosanderson marked this conversation as resolved.
Show resolved Hide resolved
const result = [];
for (const field of metadataSchema) {
if (field.rangeSearch === true) {
const fromField = {
...field,
name: `${field.name}From`,
label: `From`,
fieldGroup: field.name,
fieldGroupDisplayName: field.displayName ?? sentenceCase(field.name),
};
const toField = {
...field,
name: `${field.name}To`,
label: `To`,
fieldGroup: field.name,
fieldGroupDisplayName: field.displayName ?? sentenceCase(field.name),
};
result.push(fromField);
result.push(toField);
} else {
result.push(field);
}
}
return result;
};

export const getFieldValuesFromQuery = (
state: Record<string, string>,
hiddenFieldValues: Record<string, any>,
schema: Schema,
): Record<string, any> => {
const fieldKeys = Object.keys(state)
.filter((key) => !key.startsWith(VISIBILITY_PREFIX) && !key.startsWith(COLUMN_VISIBILITY_PREFIX))
.filter((key) => key !== ORDER_KEY && key !== ORDER_DIRECTION_KEY);

const values: Record<string, any> = { ...hiddenFieldValues };
for (const key of fieldKeys) {
values[key] = state[key];
const expandedSchema = getMetadataSchemaWithExpandedRanges(schema.metadata);
for (const field of expandedSchema) {
if (field.name in state) {
values[field.name] = state[field.name];
}
}

return values;
};

Expand Down
62 changes: 62 additions & 0 deletions website/src/utils/serversideSearch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import {
getFieldValuesFromQuery,
getLapisSearchParameters,
ORDER_DIRECTION_KEY,
ORDER_KEY,
PAGE_KEY,
getColumnVisibilitiesFromQuery,
type SearchResponse,
} from './search';
import type { TableSequenceData } from '../components/SearchPage/Table';
import { LapisClient } from '../services/lapisClient';
import { pageSize } from '../settings';
import type { Schema } from '../types/config';
import type { ReferenceGenomesSequenceNames } from '../types/referencesGenomes';

export const performLapisSearchQueries = async (
state: Record<string, string>,
schema: Schema,
referenceGenomesSequenceNames: ReferenceGenomesSequenceNames,
hiddenFieldValues: Record<string, any>,
organism: string,
): Promise<SearchResponse> => {
const fieldValues = getFieldValuesFromQuery(state, hiddenFieldValues, schema);
const lapisSearchParameters = getLapisSearchParameters(fieldValues, referenceGenomesSequenceNames);

const orderByField = ORDER_KEY in state ? state[ORDER_KEY] : schema.defaultOrderBy;
const orderDirection = state[ORDER_DIRECTION_KEY] ?? schema.defaultOrder;
const page = state[PAGE_KEY] ? parseInt(state[PAGE_KEY], 10) : 1;

const columnVisibilities = getColumnVisibilitiesFromQuery(schema, state);

const columnsToShow = schema.metadata
.filter((field) => columnVisibilities.get(field.name) === true)
.map((field) => field.name);

const client = LapisClient.createForOrganism(organism);

const [detailsResult, aggregatedResult] = await Promise.all([
// @ts-expect-error because OrderBy typing does not accept this for unknown reasons
client.call('details', {
...lapisSearchParameters,
fields: [...columnsToShow, schema.primaryKey],
limit: pageSize,
offset: (page - 1) * pageSize,
orderBy: [
{
field: orderByField,
type: orderDirection === 'ascending' ? 'ascending' : 'descending',
},
],
}),
client.call('aggregated', {
...lapisSearchParameters,
fields: [],
}),
]);

return {
data: detailsResult.unwrapOr({ data: [] }).data as TableSequenceData[],
totalCount: aggregatedResult.unwrapOr({ data: [{ count: 0 }] }).data[0].count,
};
};
Loading