Skip to content

Commit

Permalink
Add a "my sequences page" displaying released sequences (#1037)
Browse files Browse the repository at this point in the history
* wip

* still types to fix

* add link

* fix types and more

* remove download dialog

* remove redundancies

* Update my_sequences.astro

* Update website/src/routes.ts

Co-authored-by: Fabian Engelniederhammer <[email protected]>

* Update website/src/pages/[organism]/my_sequences/[group].astro

Co-authored-by: Fabian Engelniederhammer <[email protected]>

* Update website/src/pages/[organism]/my_sequences/[group].astro

Co-authored-by: Fabian Engelniederhammer <[email protected]>

* refactor(website): use GroupManagementClient

* refactor to reduce code duplication

* improve error message

* add back group functionality with better exclude option

* lots of fixes

* add underline to link

* get user groups not all groups

---------

Co-authored-by: Fabian Engelniederhammer <[email protected]>
Co-authored-by: Fabian Engelniederhammer <[email protected]>
  • Loading branch information
3 people authored Feb 20, 2024
1 parent 88dc4fe commit 6163414
Show file tree
Hide file tree
Showing 14 changed files with 380 additions and 71 deletions.
6 changes: 5 additions & 1 deletion website/src/components/DataUploadForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,11 @@ const InnerDataUploadForm = ({
<p className='text-gray-500'>Loading groups...</p>
) : (
<p className='text-red-500'>
No group found. Please join or <a href={routes.userOverviewPage()}>create a group</a>.
No group found. Please join or{' '}
<a href={routes.userOverviewPage()} className='underline'>
create a group
</a>
.
</p>
)
) : (
Expand Down
27 changes: 27 additions & 0 deletions website/src/components/MySequencesPage/GroupSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { type FC } from 'react';

import { routes } from '../../routes';

type GroupSelectorProps = {
groupNames: string[];
selectedGroupName: string;
organism: string;
};
export const GroupSelector: FC<GroupSelectorProps> = ({ groupNames, selectedGroupName, organism }) => {
return (
<select
className='mt-4 select select-bordered'
onChange={(event) => {
const newGroup = event.target.value;
const page = routes.mySequencesPage(organism, newGroup);
location.href = page;
}}
>
{groupNames.map((groupName: string) => (
<option selected={groupName === selectedGroupName} value={groupName} key={groupName}>
{groupName}
</option>
))}
</select>
);
};
3 changes: 2 additions & 1 deletion website/src/components/SearchPage/SearchForm.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { beforeEach, describe, expect, test, vi } from 'vitest';

import { SearchForm } from './SearchForm';
import { testConfig, testOrganism } from '../../../vitest.setup.ts';
import { routes } from '../../routes.ts';
import { routes, SEARCH } from '../../routes.ts';
import type { MetadataFilter } from '../../types/config.ts';
import type { ReferenceGenomesSequenceNames } from '../../types/referencesGenomes.ts';
import type { ClientConfig } from '../../types/runtimeConfig.ts';
Expand Down Expand Up @@ -40,6 +40,7 @@ function renderSearchForm(
filters={searchFormFilters}
initialMutationFilter={{}}
clientConfig={clientConfig}
classOfSearchPage={SEARCH}
referenceGenomesSequenceNames={referenceGenomesSequenceNames}
/>
</QueryClientProvider>,
Expand Down
8 changes: 6 additions & 2 deletions website/src/components/SearchPage/SearchForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { PangoLineageField } from './fields/PangoLineageField';
import { getClientLogger } from '../../clientLogger.ts';
import { getLapisUrl } from '../../config.ts';
import { useOffCanvas } from '../../hooks/useOffCanvas';
import { routes, navigateToSearchPage } from '../../routes.ts';
import { routes, navigateToSearchLikePage, type ClassOfSearchPageType } from '../../routes.ts';
import type { MetadataFilter, MutationFilter } from '../../types/config.ts';
import type { ReferenceGenomesSequenceNames } from '../../types/referencesGenomes.ts';
import type { ClientConfig } from '../../types/runtimeConfig.ts';
Expand All @@ -29,6 +29,8 @@ interface SearchFormProps {
initialMutationFilter: MutationFilter;
clientConfig: ClientConfig;
referenceGenomesSequenceNames: ReferenceGenomesSequenceNames;
classOfSearchPage: ClassOfSearchPageType;
group?: string;
}

const clientLogger = getClientLogger('SearchForm');
Expand All @@ -39,6 +41,8 @@ export const SearchForm: FC<SearchFormProps> = ({
initialMutationFilter,
clientConfig,
referenceGenomesSequenceNames,
classOfSearchPage,
group,
}) => {
const [fieldValues, setFieldValues] = useState<(MetadataFilter & { label: string })[]>(
filters.map((filter) => ({
Expand Down Expand Up @@ -66,7 +70,7 @@ export const SearchForm: FC<SearchFormProps> = ({
event.preventDefault();
setIsLoading(true);
const searchableFieldValues = fieldValues.filter((field) => !(field.notSearchable ?? false));
navigateToSearchPage(organism, searchableFieldValues, mutationFilter);
navigateToSearchLikePage(organism, classOfSearchPage, group, searchableFieldValues, mutationFilter);
};

const resetSearch = async () => {
Expand Down
16 changes: 14 additions & 2 deletions website/src/components/SearchPage/SearchPagination.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Pagination as MUIPagination } from '@mui/material';
import type { FC } from 'react';

import { navigateToSearchPage } from '../../routes';
import { navigateToSearchLikePage, type ClassOfSearchPageType } from '../../routes';
import type { MetadataFilter, MutationFilter } from '../../types/config.ts';
import type { OrderBy } from '../../types/lapis.ts';

Expand All @@ -12,6 +12,8 @@ type SearchPaginationProps = {
orderBy: OrderBy;
organism: string;
page: number;
classOfSearchPage: ClassOfSearchPageType;
group?: string;
};

export const SearchPagination: FC<SearchPaginationProps> = ({
Expand All @@ -21,13 +23,23 @@ export const SearchPagination: FC<SearchPaginationProps> = ({
orderBy,
organism,
page,
classOfSearchPage,
group,
}) => {
return (
<MUIPagination
count={count}
page={page}
onChange={(_, newPage) => {
navigateToSearchPage(organism, metadataFilter, mutationFilter, newPage, orderBy);
navigateToSearchLikePage(
organism,
classOfSearchPage,
group,
metadataFilter,
mutationFilter,
newPage,
orderBy,
);
}}
color='primary'
variant='outlined'
Expand Down
23 changes: 17 additions & 6 deletions website/src/components/SearchPage/Table.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { capitalCase } from 'change-case';
import type { FC, ReactElement } from 'react';

import { routes, navigateToSearchPage } from '../../routes.ts';
import { routes, navigateToSearchLikePage, type ClassOfSearchPageType } from '../../routes.ts';
import type { MetadataFilter, MutationFilter, Schema } from '../../types/config.ts';
import type { OrderBy } from '../../types/lapis.ts';
import MdiTriangle from '~icons/mdi/triangle';
import MdiTriangleDown from '~icons/mdi/triangle-down';

export type TableSequenceData = {
[key: string]: string | number | null;
};
Expand All @@ -19,9 +18,21 @@ type TableProps = {
mutationFilter: MutationFilter;
page: number;
orderBy: OrderBy;
classOfSearchPage: ClassOfSearchPageType;
group?: string;
};

export const Table: FC<TableProps> = ({ organism, data, schema, metadataFilter, mutationFilter, page, orderBy }) => {
export const Table: FC<TableProps> = ({
organism,
data,
schema,
metadataFilter,
mutationFilter,
page,
orderBy,
classOfSearchPage,
group,
}) => {
const primaryKey = schema.primaryKey;

const columns = schema.tableColumns.map((field) => ({
Expand All @@ -32,18 +43,18 @@ export const Table: FC<TableProps> = ({ organism, data, schema, metadataFilter,
const handleSort = (field: string) => {
if (orderBy.field === field) {
if (orderBy.type === 'ascending') {
navigateToSearchPage(organism, metadataFilter, mutationFilter, page, {
navigateToSearchLikePage(organism, classOfSearchPage, group, metadataFilter, mutationFilter, page, {
field,
type: 'descending',
});
} else {
navigateToSearchPage(organism, metadataFilter, mutationFilter, page, {
navigateToSearchLikePage(organism, classOfSearchPage, group, metadataFilter, mutationFilter, page, {
field,
type: 'ascending',
});
}
} else {
navigateToSearchPage(organism, metadataFilter, mutationFilter, page, {
navigateToSearchLikePage(organism, classOfSearchPage, group, metadataFilter, mutationFilter, page, {
field,
type: 'ascending',
});
Expand Down
3 changes: 3 additions & 0 deletions website/src/components/User/UserPage.astro
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ const keycloakLogoutUrl = (await getKeycloakClient()).endSessionUrl({
<div class='my-2 mx-4'>
<a href={routes.userSequenceReviewPage(organism)}>Sequence Review</a>
</div>
<div class='my-2 mx-4'>
<a href={routes.mySequencesWithoutGroup(organism)}>My sequences</a>
</div>
</>
)
}
Expand Down
39 changes: 39 additions & 0 deletions website/src/pages/[organism]/my_sequences.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import { routes } from '../../routes';
import { GroupManagementClient } from '../../services/groupManagementClient';
import { getAccessToken } from '../../utils/getAccessToken';
const accessToken = getAccessToken(Astro.locals.session)!;
const groupsResult = await GroupManagementClient.create().getGroupsOfUser(accessToken);
const organism = Astro.params.organism;
let noGroups = false;
let errorMessage = '';
if (groupsResult.isOk()) {
if (groupsResult.value.length > 0) {
return Astro.redirect(routes.mySequencesPage(organism, groupsResult.value[0].groupName));
}
noGroups = true;
} else {
errorMessage = groupsResult.error.detail;
}
---

<BaseLayout title='My Sequences'>
<div>
<p>
{
noGroups && (
<div>
To access sequence-management pages your account needs to be part of a group. Please{' '}
<a href={routes.whereYouCreateAGroup()}>create a group</a>, or ask an existing group admin to
add you to their group.
</div>
)
}
{errorMessage && <div class='bg-red-500'>{errorMessage}</div>}
</p>
</div>
</BaseLayout>
111 changes: 111 additions & 0 deletions website/src/pages/[organism]/my_sequences/[group].astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
---
import { GroupSelector } from '../../../components/MySequencesPage/GroupSelector';
import { SearchForm } from '../../../components/SearchPage/SearchForm';
import { SearchPagination } from '../../../components/SearchPage/SearchPagination';
import { Table } from '../../../components/SearchPage/Table';
import ErrorBox from '../../../components/common/ErrorBox.astro';
import BaseLayout from '../../../layouts/BaseLayout.astro';
import { MY_SEQUENCES } from '../../../routes';
import { GroupManagementClient } from '../../../services/groupManagementClient';
import { pageSize } from '../../../settings';
import { getAccessToken } from '../../../utils/getAccessToken';
import { processParametersAndFetchSearch } from '../../../utils/processParametersAndFetchSearch';
const group = Astro.params.group!;
const accessToken = getAccessToken(Astro.locals.session)!;
const groupsResult = await GroupManagementClient.create().getGroupsOfUser(accessToken);
if (groupsResult.isOk() === false) {
return new Response(undefined, {
status: 500,
message: 'Failed to get groups',
});
}
const groups = groupsResult.value;
const groupNames = groups.map((group: { groupName: string }) => group.groupName);
const groupExists = groupNames.includes(group);
if (groupExists === false) {
return new Response(undefined, {
status: 404,
});
}
const {
cleanedOrganism,
organism,
data,
page,
metadataFilter,
mutationFilter,
referenceGenomesSequenceNames,
schema,
clientConfig,
orderBy,
} = await processParametersAndFetchSearch(Astro, group);
---

<BaseLayout title={`${cleanedOrganism.displayName} - My sequences`}>
<h1 class='title'>Viewing sequences for group {group}</h1>
{
groupNames.length > 1 && (
<>
Choose group:
<GroupSelector groupNames={groupNames} selectedGroupName={group} organism={organism} client:load />
</>
)
}

<div class='flex flex-col md:flex-row gap-8 md:gap-4'>
<div class='md:w-72'>
<SearchForm
organism={organism}
filters={metadataFilter}
initialMutationFilter={mutationFilter}
clientConfig={clientConfig}
referenceGenomesSequenceNames={referenceGenomesSequenceNames}
classOfSearchPage={MY_SEQUENCES}
group={group}
client:only='react'
/>
</div>
{
data.match(
(data) => (
<div class='flex-1'>
<div class='mt-4 mb-1'>
Search returned {data.totalCount.toLocaleString()}
sequence{data.totalCount === 1 ? '' : 's'}
</div>
<Table
organism={organism}
data={data.data}
schema={schema}
metadataFilter={metadataFilter}
mutationFilter={mutationFilter}
page={page}
orderBy={orderBy}
classOfSearchPage={MY_SEQUENCES}
group={group}
client:load
/>

<div class='mt-4 flex justify-center'>
<SearchPagination
client:only='react'
count={Math.ceil(data.totalCount / pageSize)}
page={page}
metadataFilter={metadataFilter}
mutationFilter={mutationFilter}
orderBy={orderBy}
organism={organism}
classOfSearchPage={MY_SEQUENCES}
group={group}
/>
</div>
</div>
),
(error) => <ErrorBox title='Failed searching sequences' message={error.detail} />,
)
}
</div>
</BaseLayout>
Loading

0 comments on commit 6163414

Please sign in to comment.