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

Add/remove view UI #359

Merged
merged 16 commits into from
Jul 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
71 changes: 58 additions & 13 deletions web/src/api/project.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import axios from 'axios';
import { offset } from 'handsontable/helpers/dom';

import {
Project,
Expand All @@ -14,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 @@ -320,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 @@ -227,9 +227,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
25 changes: 19 additions & 6 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';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we think we are going to remove this?


type NavProps = {};
type NavProps = {
filteredSamples: string[];
sanghoonio marked this conversation as resolved.
Show resolved Hide resolved
};

type ViewButtonProps = {
view: PageView;
Expand Down Expand Up @@ -43,13 +45,13 @@ 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">
{/*<div className="mx-2">
sanghoonio marked this conversation as resolved.
Show resolved Hide resolved
<OverlayTrigger
placement="right"
delay={{ show: 100, hide: 600 }}
Expand All @@ -62,7 +64,7 @@ export const ProjectDataNav = (props: NavProps) => {
>
<i className="bi bi-info-circle text-muted"></i>
</OverlayTrigger>
</div>
</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 +106,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>
Comment on lines +109 to +119
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we think we will still keep this?

<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';

interface Props {
sanghoonio marked this conversation as resolved.
Show resolved Hide resolved
show: boolean;
onHide: () => void;
filteredSamples: string[];
}

export const ViewOptionsModal: FC<Props> = ({ show, onHide, filteredSamples}) => {
sanghoonio marked this conversation as resolved.
Show resolved Hide resolved

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);

type FormValues = {
name: string;
description: string;
};
sanghoonio marked this conversation as resolved.
Show resolved Hide resolved

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) => {
debugger;
sanghoonio marked this conversation as resolved.
Show resolved Hide resolved
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
Loading