Skip to content

Commit

Permalink
Merge branch 'dev' into schemas
Browse files Browse the repository at this point in the history
  • Loading branch information
nleroy917 authored Jul 24, 2024
2 parents 30ba839 + 8e29d35 commit 430423f
Show file tree
Hide file tree
Showing 13 changed files with 528 additions and 117 deletions.
70 changes: 58 additions & 12 deletions web/src/api/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,43 +13,60 @@ import {
const API_HOST = import.meta.env.VITE_API_HOST || '';
const API_BASE = `${API_HOST}/api/v1`;

interface ProjectUpdateItems {
type ProjectUpdateItems = {
project_value?: Project | null;
tag?: string | null;
is_private?: boolean | null;
name?: string | null;
pep_schema?: string | null;
}
};

interface ProjectUpdateMetadata extends ProjectUpdateItems {
type ProjectUpdateMetadata = ProjectUpdateItems & {
sample_table?: Sample[] | null;
project_config_yaml?: string | null;
description?: string | null;
subsample_list?: string[] | null;
}
export interface SampleTableResponse {
};
export type SampleTableResponse = {
count: number;
items: Sample[];
}
};

export interface DeleteProjectResponse {
export type DeleteProjectResponse = {
message: string;
registry: string;
}
};

export interface MultiProjectResponse {
export type MultiProjectResponse = {
count: number;
results: ProjectAnnotation[];
offset: number;
limit: number;
}
};

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

export type CreateProjectViewRequest = {
description?: string;
viewName: string;
sampleNames: string[];
noFail?: boolean;
};

export type CreateProjectViewResponse = {
message: string;
registry: string;
};

export type DeleteProjectViewResponse = {
message: string;
registry: string;
};

export type ProjectAllHistoryResponse = {
namespace: string;
Expand Down Expand Up @@ -319,6 +336,35 @@ export const getView = (
}
};

export const addProjectView = (
namespace: string,
projectName: string,
tag: string = 'default',
token: string | null,
params: CreateProjectViewRequest,
) => {
const url = `${API_BASE}/projects/${namespace}/${projectName}/views/${params.viewName}?description=${params.description}&tag=${tag}`;
return axios.post<CreateProjectViewResponse>(
url,
params.sampleNames,
{ headers: { Authorization: `Bearer ${token}` } }
);
};

export const deleteProjectView = (
namespace: string,
projectName: string,
tag: string = 'default',
viewName: string,
token: string | null,
) => {
const url = `${API_BASE}/projects/${namespace}/${projectName}/views/${viewName}?tag=${tag}`;
return axios.delete<DeleteProjectViewResponse>(
url,
{ headers: { Authorization: `Bearer ${token}` } }
);
};

export const getProjectAllHistory = (namespace: string, name: string, tag: string, jwt: string | null) => {
const url = `${API_BASE}/projects/${namespace}/${name}/history?tag=${tag}`;
return axios
Expand Down
6 changes: 3 additions & 3 deletions web/src/components/layout/nav/nav-desktop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -236,9 +236,9 @@ export const NavDesktop = () => {
</Dropdown>
</div>
) : (
<div className="my-0 nav-item h5 pt-1">
<button className="btn btn-sm btn-dark px-3 mb-1" onClick={() => login()}>
<i className="fa fa-github"></i>Log in
<div className="my-0 me-3 nav-item h5 pt-1">
<button className="btn btn-sm btn-dark px-2 mb-1" onClick={() => login()}>
<i className="bi bi-github pe-1"></i>Sign In
</button>
</div>
)}
Expand Down
35 changes: 17 additions & 18 deletions web/src/components/layout/project-data-nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import { OverlayTrigger, Tooltip } from 'react-bootstrap';
import { useProjectPageView } from '../../hooks/stores/useProjectPageView';
import { ViewSelector } from '../project/view-selector';

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

type NavProps = {};
type NavProps = {
filteredSamples: string[];
};

type ViewButtonProps = {
view: PageView;
Expand Down Expand Up @@ -43,26 +45,12 @@ const ViewButton = (props: ViewButtonProps) => {
};

export const ProjectDataNav = (props: NavProps) => {
const {} = props;
const { filteredSamples } = props;

const { pageView, setPageView } = useProjectPageView();

return (
<div className="h-100 w-100 d-flex flex-row align-items-center">
<div className="mx-2">
<OverlayTrigger
placement="right"
delay={{ show: 100, hide: 600 }}
overlay={
<Tooltip id="project-nav-tabs-tooltip">
A project consists of samples, subsamples, and a configuration file. For a detailed explanation of each
you can refer to the <a href="https://pep.databio.org/spec/specification/">PEP specification</a>.
</Tooltip>
}
>
<i className="bi bi-info-circle text-muted"></i>
</OverlayTrigger>
</div>
<div
className={
pageView === 'samples' ? 'border-0 px-1 h-100 text-muted bg-white shadow-sm align-middle' : 'px-1 h-100'
Expand Down Expand Up @@ -104,7 +92,18 @@ export const ProjectDataNav = (props: NavProps) => {
color={pageView === 'config' ? ' text-dark' : ' text-muted'}
/>
</div>
<ViewSelector />
<div className={pageView === 'help' ? 'border-0 px-1 h-100 text-muted bg-white shadow-sm' : 'px-1 h-100'}>
<ViewButton
view="help"
setPageView={setPageView}
icon="bi bi-question-circle-fill me-2"
text="Help"
isDirty={false}
bold={pageView === 'help' ? ' fw-normal' : ' fw-light'}
color={pageView === 'help' ? ' text-dark' : ' text-muted'}
/>
</div>
<ViewSelector filteredSamples={filteredSamples} />
</div>
);
};
184 changes: 184 additions & 0 deletions web/src/components/modals/add-view-options.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import { FC, useState } from 'react';
import { Modal, Tab, Tabs } from 'react-bootstrap';
import ReactSelect from 'react-select';
import { Controller, FieldErrors, useForm } from 'react-hook-form';
import { ErrorMessage } from '@hookform/error-message';


import { useViewMutations } from '../../hooks/mutations/useViewMutations';
import { useProjectPage } from '../../contexts/project-page-context';
import { CreateProjectViewRequest, addProjectView, deleteProjectView } from '../../api/project';
import { useProjectViews } from '../../hooks/queries/useProjectViews';
import { useProjectSelectedView } from '../../hooks/stores/useProjectSelectedViewStore';

type Props = {
show: boolean;
onHide: () => void;
filteredSamples: string[];
};

type FormValues = {
name: string;
description: string;
};

export const ViewOptionsModal = (props: Props) => {
const { show, onHide, filteredSamples } = props;

const { namespace, projectName, tag } = useProjectPage();
const { view, setView } = useProjectSelectedView();

const projectViewsQuery = useProjectViews(namespace, projectName, tag);

const projectViewsIsLoading = projectViewsQuery.isLoading;
const projectViews = projectViewsQuery.data;

const viewMutations = useViewMutations(namespace, projectName, tag);

const [selectedViewDelete, setSelectedViewDelete] = useState(null);
const [deleteState, setDeleteState] = useState(true);

const {
register,
reset: resetForm,
formState: { isValid, errors },
} = useForm<FormValues>({
mode: 'onChange',
defaultValues: {
name: null,
description: null,
},
});

const handleDeleteView = async() => {
viewMutations.removeViewMutation.mutate(selectedViewDelete.value);
setSelectedViewDelete(null)
};

const runValidation = () => {
projectViewsQuery.refetch();
};

const onSubmit = (e) => {
e.preventDefault();

const createViewRequest: CreateProjectViewRequest = {
viewName: e.target[0].value,
sampleNames: filteredSamples, // You might want to update this based on your requirements
description: e.target[1].value,
noFail: false
};

viewMutations.addViewMutation.mutate(createViewRequest);

e.target.reset()
resetForm({}, { keepValues: false })
};

return (
<Modal size="lg" centered animation={false} show={show} onHide={onHide} style={{zIndex: 99999}}>
<Modal.Header closeButton>
<h1 className="modal-title fs-5">Manage Views</h1>
</Modal.Header>
<Modal.Body>
{filteredSamples ? (
<div className="">
<h6 className="mb-1">Save View</h6>
<p className="mb-3 text-xs">Save the current filtered sample table state as a view by providing a name (required) and description (optional) for the view.</p>
<form onSubmit={onSubmit}>
<div className="input-group mb-2">
<span className="input-group-text text-xs">Name</span>
<input
{...register('name', {
required: {
value: true,
message: 'View Name must not be empty.',
},
pattern: {
value: /^[a-zA-Z0-9_-]+$/,
message: "View Name must contain only alphanumeric characters, '-', or '_'.",
},
})}
placeholder="..."
type="text"
className="form-control text-xs"
id="view-name"
aria-describedby="view-name-help"
/>
</div>
<div className="input-group">
<span className="input-group-text text-xs">Description</span>
<input
{...register('desc')}
placeholder="..."
type="text"
className="form-control text-xs"
id="view-description"
aria-describedby="view-description-help"
/>
</div>
<ErrorMessage
errors={errors}
name="name"
render={({ message }) => message ? <p className="text-danger text-xs pt-1 mb-0">{message}</p> : null}
/>
<button
disabled={!isValid || !!errors.name?.message}
type='submit'
className="btn btn-success px-2 mt-3 text-xs">
<i className="bi bi-plus-lg"></i> Save New View
</button>
</form>
<hr />
</div>
) : null }
<div className="">
<h6 className="mb-1">Remove View</h6>
<p className="mb-3 text-xs">Remove an existing view by selecting it from the dropdown menu.</p>
<ReactSelect
styles={{
control: (provided) => ({
...provided,
borderRadius: '0.333333em', // Left radii set to 0, right radii kept at 4px
}),
}}
className="top-z w-100 ms-auto"
options={
projectViews?.views.map((view) => ({
view: view.name,
description: view.description || 'No description',
value: view.name,
label: `${view.name} | ${view.description || 'No description'}`,
})) || []
}
onChange={(selectedOption) => {
if ((selectedOption === null) || (projectViews?.views.length === 0)) {
setSelectedViewDelete(null);
setDeleteState(true);
} else {
setSelectedViewDelete(selectedOption);
setDeleteState(false);
}
}}
isDisabled={projectViews?.views.length === 0 || projectViewsIsLoading}
isClearable
placeholder={
projectViewsIsLoading
? 'Loading views...'
: projectViews?.views.length === 0
? 'No views available'
: 'Select a view'
}
value={selectedViewDelete === null ? null : { view: selectedViewDelete.view, description: selectedViewDelete.description, value: selectedViewDelete.value, label: selectedViewDelete.label }}
/>
<button
disabled={deleteState}
onClick={handleDeleteView}
className="btn btn-danger px-2 mt-3 text-xs">
<i className="bi bi-trash"></i> Remove View
</button>
</div>
</Modal.Body>
</Modal>
);
};
Loading

0 comments on commit 430423f

Please sign in to comment.