Skip to content

Commit

Permalink
Merge pull request #299 from pepkit/dev
Browse files Browse the repository at this point in the history
Integrate views into the user interface
  • Loading branch information
nleroy917 authored Feb 22, 2024
2 parents 3980978 + 9aa827f commit 7a6a64c
Show file tree
Hide file tree
Showing 20 changed files with 412 additions and 53 deletions.
5 changes: 5 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

## [0.11.6] - 02-08-2024

- Added interface for selecting and viewing project views
- optimized loading of very large sample tables

## [0.11.6] - 02-08-2024

### Fixed

- Docs and docs links
Expand Down
2 changes: 1 addition & 1 deletion pephub/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.11.6"
__version__ = "0.11.7"
4 changes: 2 additions & 2 deletions pephub/routers/api/v1/namespace.py
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,7 @@ async def get_namespace_information(
summary="Get statistics about each namespace",
response_model=NamespaceStats,
)
async def get_namespace_information(
async def get_namespace_stats(
agent: PEPDatabaseAgent = Depends(get_db),
namespace: Optional[str] = None,
):
Expand All @@ -467,5 +467,5 @@ async def get_namespace_information(
except Exception:
raise HTTPException(
status_code=500,
detail=f"Internal server error. Unexpected return value. Error: 500",
detail="Internal server error. Unexpected return value. Error: 500",
)
4 changes: 2 additions & 2 deletions pephub/routers/api/v1/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -573,10 +573,10 @@ async def upload_sample(
detail="Sample already exists in project. Use 'overwrite' parameter to overwrite.",
)

except Exception as e:
except Exception as _:
raise HTTPException(
status_code=400,
detail=f"Could not upload sample. Server error!",
detail="Could not upload sample. Server error!",
)


Expand Down
42 changes: 41 additions & 1 deletion web/src/api/project.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import axios from 'axios';
import { offset } from 'handsontable/helpers/dom';

import { Project, ProjectAnnotation, ProjectConfigResponse, Sample } from '../../types';
import { Project, ProjectAnnotation, ProjectConfigResponse, ProjectViewAnnotation, Sample } from '../../types';

const API_HOST = import.meta.env.VITE_API_HOST || '';
const API_BASE = `${API_HOST}/api/v1`;
Expand Down Expand Up @@ -37,6 +37,13 @@ export interface MultiProjectResponse {
limit: number;
}

export interface ProjectViewsResponse {
namespace: string;
project: string;
tag: string;
views: ProjectViewAnnotation[];
}

export const getProject = (
namespace: string,
projectName: string,
Expand Down Expand Up @@ -254,3 +261,36 @@ export const editTotalProject = (

return axios.patch(url, requestBody, { headers: { Authorization: `Bearer ${token}` } });
};

export const getProjectViews = (
namespace: string,
projectName: string,
tag: string = 'default',
token: string | null = null,
) => {
const url = `${API_BASE}/projects/${namespace}/${projectName}/views?tag=${tag}`;
if (!token) {
return axios.get<ProjectViewsResponse>(url).then((res) => res.data);
} else {
return axios
.get<ProjectViewsResponse>(url, { headers: { Authorization: `Bearer ${token}` } })
.then((res) => res.data);
}
};

export const getView = (
namespace: string,
projectName: string,
tag: string = 'default',
viewName: string,
token: string | null = null,
) => {
const url = `${API_BASE}/projects/${namespace}/${projectName}/views/${viewName}?tag=${tag}&raw=false`;
if (!token) {
return axios.get<ProjectViewAnnotation>(url).then((res) => res.data);
} else {
return axios
.get<ProjectViewAnnotation>(url, { headers: { Authorization: `Bearer ${token}` } })
.then((res) => res.data);
}
};
6 changes: 5 additions & 1 deletion web/src/components/forms/edit-project-meta.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,11 @@ export const ProjectMetaEditForm: FC<Props> = ({

// grab the sample table to warn the user if they wont be able to swap
// to a POP
const { data: sampleTable } = useSampleTable(namespace, name, tag);
const { data: sampleTable } = useSampleTable({
namespace,
project: name,
tag,
});

const mutation = useEditProjectMetaMutation(namespace, name, tag, onSubmit, onFailedSubmit, metadata);

Expand Down
50 changes: 34 additions & 16 deletions web/src/components/layout/project-data-nav.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,34 @@
import { FC } from 'react';

type ProjectView = 'samples' | 'subsamples' | 'config';
import { ProjectViewAnnotation } from '../../../types';
import { ProjectViewsResponse } from '../../api/project';
import { ViewSelector } from '../project/view-selector';

type PageView = 'samples' | 'subsamples' | 'config';

interface NavProps {
projectView: string;
setProjectView: (view: ProjectView) => void;
pageView: string;
setPageView: (view: PageView) => void;
samplesIsDirty: boolean;
subsamplesIsDirty: boolean;
configIsDirty: boolean;
projectViewIsLoading: boolean;
projectViews: ProjectViewsResponse | undefined;
projectView: string | undefined;
setProjectView: (view: string | undefined) => void;
}

interface ViewButtonProps {
view: ProjectView;
setProjectView: (view: ProjectView) => void;
view: PageView;
setPageView: (view: PageView) => void;
icon: string;
text: string;
isDirty: boolean;
}

const ViewButton: FC<ViewButtonProps> = ({ view, setProjectView, icon, text, isDirty }) => (
const ViewButton: FC<ViewButtonProps> = ({ view, setPageView, icon, text, isDirty }) => (
<div>
<button onClick={() => setProjectView(view)} className="border-0 bg-transparent mr-4">
<button onClick={() => setPageView(view)} className="border-0 bg-transparent mr-4">
<span className="text-xs">
<i className="bi bi-circle-fill ms-1 text-transparent"></i>
</span>
Expand All @@ -41,59 +49,69 @@ const ViewButton: FC<ViewButtonProps> = ({ view, setProjectView, icon, text, isD
);

export const ProjectDataNav: FC<NavProps> = ({
projectView,
setProjectView,
pageView,
setPageView,
samplesIsDirty,
subsamplesIsDirty,
configIsDirty,
projectViewIsLoading,
projectViews,
projectView,
setProjectView,
}) => {
return (
<div className="d-flex flex-row align-items-center">
<div className="w-100 d-flex flex-row align-items-center">
<div
className={
projectView === 'samples'
pageView === 'samples'
? 'border border-grey border-bottom-0 rounded-top shadow-sm bg-solid px-1 py-2 text-muted'
: 'px-2 py-1'
}
>
<ViewButton
view="samples"
setProjectView={setProjectView}
setPageView={setPageView}
icon="bi bi-table me-2"
text="Samples"
isDirty={samplesIsDirty}
/>
</div>
<div
className={
projectView === 'subsamples'
pageView === 'subsamples'
? 'border border-grey border-bottom-0 rounded-top shadow-sm bg-solid px-1 py-2'
: 'px-2 py-1'
}
>
<ViewButton
view="subsamples"
setProjectView={setProjectView}
setPageView={setPageView}
icon="bi bi-grid-3x3-gap-fill me-2"
text="Subsamples"
isDirty={subsamplesIsDirty}
/>
</div>
<div
className={
projectView === 'config'
pageView === 'config'
? 'border border-grey border-bottom-0 rounded-top shadow-sm bg-solid px-1 py-2'
: 'px-2 py-1'
}
>
<ViewButton
view="config"
setProjectView={setProjectView}
setPageView={setPageView}
icon="bi bi-filetype-yml me-2"
text="Config"
isDirty={configIsDirty}
/>
</div>
<ViewSelector
projectViewsIsLoading={projectViewIsLoading}
projectViews={projectViews}
view={projectView}
setView={setProjectView}
/>
</div>
);
};
8 changes: 5 additions & 3 deletions web/src/components/modals/add-to-pop.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import { AxiosError } from 'axios';
import { FC, Fragment, useEffect, useState } from 'react';
import { Modal } from 'react-bootstrap';
import { set } from 'react-hook-form';
import toast from 'react-hot-toast';

import { useSampleTableMutation } from '../../hooks/mutations/useSampleTableMutation';
import { useSampleTable } from '../../hooks/queries/useSampleTable';
import { useSession } from '../../hooks/useSession';
import { extractErrorMessage } from '../../utils/etc';
import { NamespaceSearchDropdown } from '../forms/components/namespace-search-dropdown';
import { PepSearchDropdown } from '../forms/components/pep-search-dropdown';
import { LoadingSpinner } from '../spinners/loading-spinner';

Expand All @@ -35,7 +33,11 @@ export const AddToPOPModal: FC<Props> = (props) => {
const [projectName, tag] = project?.split('/')[1].split(':') || [undefined, undefined];

// I run data validation in the actual button click, so im not doing it here
const { data: currentSampleTable } = useSampleTable(namespace, projectName!, tag);
const { data: currentSampleTable } = useSampleTable({
namespace,
project: projectName!,
tag: tag,
});
const sampleTableMutation = useSampleTableMutation(namespace, projectName!, tag!);

const onCancel = () => {
Expand Down
64 changes: 64 additions & 0 deletions web/src/components/modals/sample-table-too-large.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { FC } from 'react';
import { FormCheck, Modal } from 'react-bootstrap';

import { useLocalStorage } from '../../hooks/useLocalStorage';

interface Props {
show: boolean;
onHide: () => void;
namespace?: string;
}

export const LargeSampleTableModal: FC<Props> = ({ show, onHide, namespace }) => {
const [_, setHideLargeSampleTableModal] = useLocalStorage('hideLargeSampleTableModal', 'false');
return (
<Modal
centered
animation={false}
show={show}
onHide={() => {
onHide();
}}
>
<Modal.Header closeButton className="bg-warning bg-opacity-25">
<h1 className="modal-title fs-5">Very large sample table detected!</h1>
</Modal.Header>
<Modal.Body>
<p>
You've stumbled upon a very large sample table. Because of this, we've disabled viewing the entire table in
the browser for performance reasons. You can still view slices of the sample table by leveraging PEP{' '}
<a href="https://pephub-api.databio.org/api/v1/docs#/project/get_views_api_v1_projects__namespace___project__views_get">
views
</a>
.
</p>
<p>
Use the dropdown in the top right to select a view, or use the API directly to fetch slices of the sample
table.
</p>
<form>
<input
type="checkbox"
id="dont-show-again"
className="form-check-input"
onChange={(e) => {
if (e.target.checked) {
setHideLargeSampleTableModal('true');
} else {
setHideLargeSampleTableModal('false');
}
}}
/>
<label htmlFor="dont-show-again" className="ms-1 form-check-label">
Don't show this message again
</label>
</form>
</Modal.Body>
<Modal.Footer>
<button onClick={onHide} type="button" className="btn btn-dark">
Dismiss
</button>
</Modal.Footer>
</Modal>
);
};
6 changes: 5 additions & 1 deletion web/src/components/pop/pop-interface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ interface Props {
export const PopInterface = ({ project }: Props) => {
const { namespace, name, tag } = project;
const { user } = useSession();
const { data: peps, isFetching: gettingProjectList } = useSampleTable(namespace, name, tag);
const { data: peps, isFetching: gettingProjectList } = useSampleTable({
namespace,
project: name,
tag,
});
const { data: allProjectsInfo, isFetching: isLoading } = useMultiProjectAnnotation(
peps?.items.map((p) => `${p.namespace}/${p.name}:${p.tag}`),
);
Expand Down
Loading

0 comments on commit 7a6a64c

Please sign in to comment.