diff --git a/README.md b/README.md index 96e9fbcc..2b674424 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,6 @@ **Source Code**: https://github.com/pepkit/pephub ---- +-- - diff --git a/pephub/_version.py b/pephub/_version.py index f23a6b39..9e78220f 100644 --- a/pephub/_version.py +++ b/pephub/_version.py @@ -1 +1 @@ -__version__ = "0.13.0" +__version__ = "0.14.0" diff --git a/pephub/const.py b/pephub/const.py index 5927a90d..5b9498d1 100644 --- a/pephub/const.py +++ b/pephub/const.py @@ -138,3 +138,5 @@ DEFAULT_QDRANT_SCORE_THRESHOLD = ( 0.72 # empirical value, highly dependent on the model used ) + +ARCHIVE_URL_PATH = "https://cloud2.databio.org/pephub/" diff --git a/pephub/dependencies.py b/pephub/dependencies.py index f5ead0f2..08cbcc7c 100644 --- a/pephub/dependencies.py +++ b/pephub/dependencies.py @@ -149,8 +149,10 @@ def read_authorization_header(authorization: str = Header(None)) -> Union[dict, except jwt.exceptions.DecodeError as e: _LOGGER_PEPHUB.error(e) return None - except jwt.exceptions.ExpiredSignatureError: - raise HTTPException(status_code=401, detail="JWT has expired") + except jwt.exceptions.ExpiredSignatureError as e: + # raise HTTPException(status_code=401, detail="JWT has expired") + _LOGGER_PEPHUB.error(e) + return None return session_info diff --git a/pephub/helpers.py b/pephub/helpers.py index 8cc08c65..ea1553d4 100644 --- a/pephub/helpers.py +++ b/pephub/helpers.py @@ -96,6 +96,29 @@ def zip_conv_result(conv_result: dict, filename: str = "project.zip") -> Respons return resp +def download_yaml(content: dict, file_name: str = "unnamed.yaml") -> Response: + """ + Convert json/dict to downloading io format + + :param content: content of the file + :param file_name: name of the file + return Response: response object + """ + + yaml_string = yaml.dump(content) + + yaml_bytes = io.BytesIO() + yaml_bytes.write(yaml_string.encode("utf-8")) + yaml_bytes.seek(0) # Move the pointer to the start of the stream + + # Create a streaming response with the YAML data + return Response( + yaml_bytes.getvalue(), + media_type="application/x-yaml", + headers={"Content-Disposition": f"attachment; filename={file_name}"}, + ) + + def build_authorization_url( client_id: str, redirect_uri: str, diff --git a/pephub/routers/api/v1/helpers.py b/pephub/routers/api/v1/helpers.py index e3ec152c..1318ede8 100644 --- a/pephub/routers/api/v1/helpers.py +++ b/pephub/routers/api/v1/helpers.py @@ -1,30 +1,48 @@ import logging import eido +from eido.validation import validate_config +from eido.exceptions import EidoValidationError import peppy import yaml from fastapi.exceptions import HTTPException from peppy import Project from peppy.const import ( CONFIG_KEY, - SAMPLE_NAME_ATTR, SAMPLE_RAW_DICT_KEY, - SAMPLE_TABLE_INDEX_KEY, SUBSAMPLE_RAW_LIST_KEY, ) +from ....dependencies import ( + get_db, +) _LOGGER = logging.getLogger(__name__) +DEFAULT_SCHEMA_NAMESPACE = "databio" +DEFAULT_SCHEMA_NAME = "pep-2.1.0" async def verify_updated_project(updated_project) -> peppy.Project: new_raw_project = {} + agent = get_db() + default_schema = agent.schema.get( + namespace=DEFAULT_SCHEMA_NAMESPACE, name=DEFAULT_SCHEMA_NAME + ) + if not updated_project.sample_table or not updated_project.project_config_yaml: raise HTTPException( status_code=400, detail="Please provide a sample table and project config yaml to update project", ) - + try: + validate_config( + yaml.safe_load(updated_project.project_config_yaml), default_schema + ) + except EidoValidationError as e: + raise HTTPException( + status_code=400, + detail=f"Config structure error: {', '.join(list(e.errors_by_type.keys()))}. Please check schema definition and try again.", + ) # sample table update new_raw_project[SAMPLE_RAW_DICT_KEY] = updated_project.sample_table @@ -64,7 +82,7 @@ async def verify_updated_project(updated_project) -> peppy.Project: try: # validate project (it will also validate samples) - eido.validate_project(new_project, "http://schema.databio.org/pep/2.1.0.yaml") + eido.validate_project(new_project, default_schema) except Exception as _: raise HTTPException( status_code=400, diff --git a/pephub/routers/api/v1/namespace.py b/pephub/routers/api/v1/namespace.py index 408f2147..f6a489ae 100644 --- a/pephub/routers/api/v1/namespace.py +++ b/pephub/routers/api/v1/namespace.py @@ -1,6 +1,7 @@ import shutil import tempfile from typing import List, Literal, Optional, Union +import os import peppy from dotenv import load_dotenv @@ -20,6 +21,7 @@ ListOfNamespaceInfo, Namespace, NamespaceStats, + TarNamespaceModelReturn, ) from peppy import Project from peppy.const import DESC_KEY, NAME_KEY @@ -27,6 +29,7 @@ from ....const import ( DEFAULT_TAG, + ARCHIVE_URL_PATH, ) from ....dependencies import ( get_db, @@ -39,6 +42,8 @@ from ....helpers import parse_user_file_upload, split_upload_files_on_init_file from ...models import FavoriteRequest, ProjectJsonRequest, ProjectRawModel +from bedms.const import AVAILABLE_SCHEMAS + load_dotenv() namespaces = APIRouter(prefix="/api/v1/namespaces", tags=["namespace"]) @@ -77,7 +82,9 @@ async def get_namespace_projects( offset: int = 0, query: str = None, admin_list: List[str] = Depends(get_namespace_access_list), - order_by: str = "update_date", + order_by: Optional[ + Literal["update_date", "name", "submission_date", "stars"] + ] = "update_date", order_desc: bool = False, filter_by: Annotated[ Optional[Literal["submission_date", "last_update_date"]], @@ -175,7 +182,6 @@ async def create_pep( p = Project(f"{dirpath}/{init_file.filename}") p.name = name p.description = description - p.pep_schema = pep_schema try: agent.project.create( p, @@ -184,7 +190,7 @@ async def create_pep( tag=tag, description=description, is_private=is_private, - pep_schema=pep_schema, + pep_schema=pep_schema or "databio/pep-2.1.0", ) except ProjectUniqueNameError: raise HTTPException( @@ -228,7 +234,7 @@ async def upload_raw_pep( is_private = project_from_json.is_private tag = project_from_json.tag overwrite = project_from_json.overwrite - pep_schema = project_from_json.pep_schema + pep_schema = project_from_json.pep_schema or "databio/pep-2.1.0" pop = project_from_json.pop or False if hasattr(project_from_json, NAME_KEY): if project_from_json.name: @@ -436,3 +442,27 @@ def remove_user( }, status_code=202, ) + + +@namespace.get( + "/archive", + summary="Get metadata of all archived files of all projects in the namespace", + response_model=TarNamespaceModelReturn, +) +async def get_archive(namespace: str, agent: PEPDatabaseAgent = Depends(get_db)): + + result = agent.namespace.get_tar_info(namespace) + + for item in result.results: + item.file_path = os.path.join(ARCHIVE_URL_PATH, item.file_path) + + return result + + +@namespace.get( + "/standardizer-schemas", + summary="Get all available schemas from BEDMS", +) +async def get_schemas(namespace: str, agent: PEPDatabaseAgent = Depends(get_db)): + + return AVAILABLE_SCHEMAS diff --git a/pephub/routers/api/v1/project.py b/pephub/routers/api/v1/project.py index 01718e64..26dd6fa3 100644 --- a/pephub/routers/api/v1/project.py +++ b/pephub/routers/api/v1/project.py @@ -53,9 +53,12 @@ ProjectHistoryResponse, SamplesResponseModel, ConfigResponseModel, + StandardizerResponse, ) from .helpers import verify_updated_project +from bedms import AttrStandardizer + _LOGGER = logging.getLogger(__name__) load_dotenv() @@ -159,7 +162,10 @@ async def update_pep( ) if new_project: update_dict.update(project=new_project) - new_name = new_project.name or project + try: + new_name = new_project.name or project + except NotImplementedError: + new_name = new_project.name = project else: new_name = project agent.project.update( @@ -171,7 +177,6 @@ async def update_pep( ) # fetch latest name and tag - new_name = updated_project.name or new_name tag = updated_project.tag or tag return JSONResponse( @@ -1138,3 +1143,42 @@ def delete_full_history( status_code=400, detail="Could not delete history. Server error.", ) + + +@project.post( + "/standardize", + summary="Standardize PEP metadata column headers", + response_model=StandardizerResponse, +) +async def get_standardized_cols( + pep: peppy.Project = Depends(get_project), + schema: str = "", +): + """ + Standardize PEP metadata column headers using BEDmess. + + :param namespace: pep: PEP string to be standardized + :param schema: Schema for AttrStandardizer + + :return dict: Standardized results + """ + + if schema == "": + raise HTTPException( + code=500, + detail="Schema is required! Available schemas are ENCODE and Fairtracks", + ) + return {} + + prj = peppy.Project.from_dict(pep) + model = AttrStandardizer(schema) + + try: + results = model.standardize(pep=prj) + except Exception: + raise HTTPException( + code=400, + detail=f"Error standardizing PEP.", + ) + + return StandardizerResponse(results=results) diff --git a/pephub/routers/api/v1/schemas.py b/pephub/routers/api/v1/schemas.py index 84bd8082..a8bb3dea 100644 --- a/pephub/routers/api/v1/schemas.py +++ b/pephub/routers/api/v1/schemas.py @@ -1,4 +1,5 @@ -from typing import Optional +from typing import Optional, Union, Literal +from starlette.responses import Response import yaml from dotenv import load_dotenv @@ -27,6 +28,7 @@ SchemaGroupAssignRequest, SchemaGetResponse, ) +from ....helpers import download_yaml from ....dependencies import ( get_db, get_namespace_access_list, @@ -168,14 +170,21 @@ async def create_schema_for_namespace_by_file( ) -@schemas.get("/{namespace}/{schema}", response_model=SchemaGetResponse) +@schemas.get("/{namespace}/{schema}", response_model=Union[SchemaGetResponse, dict]) async def get_schema( namespace: str, schema: str, agent: PEPDatabaseAgent = Depends(get_db), + return_type: Optional[Literal["yaml", "json"]] = "json", ): try: schema_dict = agent.schema.get(namespace=namespace, name=schema) + except SchemaDoesNotExistError: + raise HTTPException( + status_code=404, detail=f"Schema {schema}/{namespace} not found." + ) + if return_type == "yaml": + info = agent.schema.info(namespace=namespace, name=schema) return SchemaGetResponse( schema=yaml.dump(schema_dict), @@ -183,11 +192,23 @@ async def get_schema( last_update_date=info.last_update_date, submission_date=info.submission_date, ) + else: + return schema_dict + +@schemas.get("/{namespace}/{schema}/file") +async def download_schema( + namespace: str, + schema: str, + agent: PEPDatabaseAgent = Depends(get_db), +) -> Response: + try: + schema_dict = agent.schema.get(namespace=namespace, name=schema) except SchemaDoesNotExistError: raise HTTPException( status_code=404, detail=f"Schema {schema}/{namespace} not found." ) + return download_yaml(schema_dict, file_name=f"{namespace}/{schema}.yaml") @schemas.delete("/{namespace}/{schema}") diff --git a/pephub/routers/models.py b/pephub/routers/models.py index d9907abe..12939b5b 100644 --- a/pephub/routers/models.py +++ b/pephub/routers/models.py @@ -159,3 +159,7 @@ class SchemaGetResponse(BaseModel): description: Optional[str] = None last_update_date: str = "" submission_date: str = "" + + +class StandardizerResponse(BaseModel): + results: dict = {} diff --git a/requirements/requirements-all.txt b/requirements/requirements-all.txt index 378ad8a7..84b83daa 100644 --- a/requirements/requirements-all.txt +++ b/requirements/requirements-all.txt @@ -1,9 +1,9 @@ fastapi>=0.108.0 psycopg>=3.1.15 -pepdbagent>=0.11.0 +pepdbagent>=0.11.1 # pepdbagent @ git+https://github.com/pepkit/pepdbagent.git@schams2.0#egg=pepdbagent -peppy>=0.40.5 -eido>=0.2.2 +peppy>=0.40.6 +eido>=0.2.3 jinja2>=3.1.2 python-multipart>=0.0.5 uvicorn diff --git a/web/package.json b/web/package.json index 9dbf595d..30c06791 100644 --- a/web/package.json +++ b/web/package.json @@ -22,7 +22,7 @@ "@typescript-eslint/eslint-plugin": "^5.56.0", "@typescript-eslint/parser": "^5.56.0", "axios": "^1.3.4", - "bootstrap": "^5.2.3", + "bootstrap": "^5.3.3", "bootstrap-icons": "^1.10.3", "eslint": "^8.36.0", "eslint-config-prettier": "^8.8.0", @@ -57,6 +57,7 @@ "devDependencies": { "@mdx-js/rollup": "^3.0.0", "@trivago/prettier-plugin-sort-imports": "^4.1.1", + "@types/js-yaml": "^4.0.9", "@types/react": "^18.0.28", "@types/react-dom": "^18.0.11", "@vitejs/plugin-react-swc": "^3.0.0", diff --git a/web/public/github-branch-primary.svg b/web/public/github-branch-primary.svg index 934c80c8..f405bc23 100644 --- a/web/public/github-branch-primary.svg +++ b/web/public/github-branch-primary.svg @@ -10,7 +10,7 @@ version="1.1" id="svg6" sodipodi:docname="github-branch-primary.svg" - inkscape:version="1.2.2 (b0a84865, 2022-12-01)" + inkscape:version="1.3.2 (091e20e, 2023-11-25)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" @@ -28,21 +28,21 @@ inkscape:deskcolor="#d1d1d1" showgrid="false" inkscape:zoom="2.0566406" - inkscape:cx="175.52896" + inkscape:cx="175.28585" inkscape:cy="256" - inkscape:window-width="1390" - inkscape:window-height="1246" - inkscape:window-x="0" - inkscape:window-y="819" + inkscape:window-width="1392" + inkscape:window-height="916" + inkscape:window-x="1730" + inkscape:window-y="145" inkscape:window-maximized="0" inkscape:current-layer="svg6" /> + style="fill:#052c65;fill-opacity:1"> + style="fill:#052c65;fill-opacity:1" /> diff --git a/web/src/api/namespace.ts b/web/src/api/namespace.ts index 769738fd..99ef85e6 100644 --- a/web/src/api/namespace.ts +++ b/web/src/api/namespace.ts @@ -74,6 +74,20 @@ type DeleteAllPepsResponse = { message: string; }; +export interface ArchiveItem { + identifier: number; + namespace: string; + file_path: string; + creation_date: string; // ISO 8601 date string + number_of_projects: number; + file_size: number; +} + +export interface ArchiveResponse { + count: number; + results: ArchiveItem[]; +} + export const getNamespaceInfo = (namespace: string, token: string | null = null) => { const url = `${API_BASE}/namespaces/${namespace}/`; // note the trailing slash if (!token) { @@ -344,3 +358,14 @@ export const deleteAllPepsFromNamespace = (namespace: string, token: string | nu }, }); }; + +export const getNamespaceArchive = (namespace: string) => { + const url = `${API_BASE}/namespaces/${namespace}/archive`; + return axios.get(url).then((res) => res.data); +}; + +export const getStandardizerSchemas = (namespace: string) => { + const url = `${API_BASE}/namespaces/${namespace}/standardizer-schemas`; + return axios.get(url).then((res) => res.data); +}; + diff --git a/web/src/api/project.ts b/web/src/api/project.ts index 18927cda..540c1dde 100644 --- a/web/src/api/project.ts +++ b/web/src/api/project.ts @@ -85,6 +85,14 @@ export type RestoreProjectFromHistoryResponse = { registry: string; }; +export type StandardizeColsResponse = { + results: { + [key: string]: { + [key: string]: number; + }; + }; +}; + export const getProject = ( namespace: string, projectName: string, @@ -404,3 +412,16 @@ export const restoreProjectFromHistory = ( const url = `${API_BASE}/projects/${namespace}/${name}/history/${historyId}/restore?tag=${tag}`; return axios.post(url, {}, { headers: { Authorization: `Bearer ${jwt}` } }); }; + +export const getStandardizedCols = ( + namespace: string, + name: string, + tag: string, + jwt: string | null, + schema: string, +) => { + const url = `${API_BASE}/projects/${namespace}/${name}/standardize?schema=${schema}&tag=${tag}`; + return axios + .post(url, { headers: { Authorization: `Bearer ${jwt || 'NO_AUTHORIZATION'}` } }) + .then((res) => res.data); +}; diff --git a/web/src/api/schemas.ts b/web/src/api/schemas.ts index 64bbd10b..a6772567 100644 --- a/web/src/api/schemas.ts +++ b/web/src/api/schemas.ts @@ -62,7 +62,7 @@ export const getSchemas = async (params: PaginationParams) => { }; export const getSchema = async (namespace: string, name: string) => { - const url = `${API_BASE}/schemas/${namespace}/${name}`; + const url = `${API_BASE}/schemas/${namespace}/${name}?return_type=yaml`; const { data } = await axios.get(url); return data; }; diff --git a/web/src/components/browse/namespace-grid.tsx b/web/src/components/browse/namespace-grid.tsx new file mode 100644 index 00000000..0ca136ec --- /dev/null +++ b/web/src/components/browse/namespace-grid.tsx @@ -0,0 +1,44 @@ +import React, { useRef, useEffect, useCallback } from 'react'; + +import { BiggestNamespaceResults } from '../../../types'; + +type Props = { + namespaces: BiggestNamespaceResults[] | undefined; + selectedNamespace: string | undefined; + handleSelectNamespace: (selectedNamespace: string) => void; +}; + +export const NamespaceGrid = (props: Props) => { + const { + namespaces, + selectedNamespace, + handleSelectNamespace + } = props; + + return ( +
+
+
+ {namespaces && ( + Object.values(namespaces).map((item, index: number) => ( +
+
+
+

+ handleSelectNamespace(item?.namespace)}> + {index + 1}. {item?.namespace} + +

+

+ {item?.number_of_projects} {item?.number_of_projects === 1 ? 'Project' : 'Projects'} +

+
+
+
+ )) + )} +
+
+
+ ); +}; diff --git a/web/src/components/browse/namespace-long-row.tsx b/web/src/components/browse/namespace-long-row.tsx new file mode 100644 index 00000000..e1933e8f --- /dev/null +++ b/web/src/components/browse/namespace-long-row.tsx @@ -0,0 +1,83 @@ +import React, { useRef, useEffect, useCallback } from 'react'; + +import { BiggestNamespaceResults } from '../../../types'; + +type Props = { + namespaces: BiggestNamespaceResults[] | undefined; + selectedNamespace: string | undefined; + handleSelectNamespace: (selectedNamespace: string) => void; +}; + +export const NamespaceLongRow = (props: Props) => { + const { + namespaces, + selectedNamespace, + handleSelectNamespace + } = props; + + const containerRef = useRef(null); + const itemRefs = useRef<{ [key: string]: HTMLDivElement | null }>({}); + + const isInViewport = (element: HTMLElement): boolean => { + const rect = element.getBoundingClientRect(); + const padding = 12; // Adjust this value to increase or decrease the padding + + return ( + rect.top >= 0 - padding && + rect.left >= 0 - padding && + rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) + padding && + rect.right <= (window.innerWidth || document.documentElement.clientWidth) + padding + ); + }; + + const scrollToItem = useCallback((namespace: string) => { + const element = itemRefs.current[namespace]; + if (element && !isInViewport(element)) { + element.scrollIntoView({ + behavior: 'smooth', + block: 'nearest', + inline: 'start' + }); + } + }, []); + + useEffect(() => { + if (selectedNamespace) { + scrollToItem(selectedNamespace); + } + }, [selectedNamespace, scrollToItem]); + + return ( +
+
+ {namespaces && ( + Object.values(namespaces).map((item, index) => ( +
{ itemRefs.current[item.namespace] = el; }} + className="col-xxl-2 col-lg-3 col-md-4 col-sm-6 flex-shrink-0" + style={{ scrollSnapAlign: 'start' }} + > +
+
+

+ handleSelectNamespace(item?.namespace)}> + {index + 1}. {item?.namespace} + +

+

+ {item?.number_of_projects} {item?.number_of_projects === 1 ? 'Project' : 'Projects'} +

+
+
+
+ )) + )} +
+
+ ); +}; diff --git a/web/src/components/browse/project-accordion.tsx b/web/src/components/browse/project-accordion.tsx new file mode 100644 index 00000000..d6493e77 --- /dev/null +++ b/web/src/components/browse/project-accordion.tsx @@ -0,0 +1,91 @@ +import React, { useState } from 'react'; +import { Markdown } from '../markdown/render'; +import { ProjectAnnotation } from '../../../types'; +import { dateStringToDateTime, dateStringToDateTimeShort } from '../../utils/dates' +import { BrowseTable } from '../tables/browse-table'; + +type Props = { + projects: ProjectAnnotation[]; +}; + +export const ProjectAccordion = (props: Props) => { + const { projects } = props; + + // Filter out the 'length' property + const projectItems = Object.entries(projects).filter(([key]) => key !== 'length'); + + // Set the initial open item to the first item's key, or null if there are no items + const initialOpenItem = projectItems.length > 0 ? projectItems[0][0] : null; + const [openItem, setOpenItem] = useState(initialOpenItem); + + const handleAccordionToggle = (key: string) => { + setOpenItem(prevOpenItem => prevOpenItem === key ? null : key); + }; + + return ( +
+ {projectItems.map(([key, project], index) => ( +
+

+ +

+
+
+
+
+ {project.description ? {project.description} :

No description

} +
+ +
+ {openItem === key && ( +
+ +
+ )} +
+
+

+ Sample Count: {project.number_of_samples} + Stars: {project.stars_number} +

+

+ Created: {dateStringToDateTime(project.submission_date)} + Updated: {dateStringToDateTime(project.last_update_date)} +

+
+
+
+
+
+ ))} +
+ ); +}; \ No newline at end of file diff --git a/web/src/components/forms/blank-project-form.tsx b/web/src/components/forms/blank-project-form.tsx index b72f437f..82cccb94 100644 --- a/web/src/components/forms/blank-project-form.tsx +++ b/web/src/components/forms/blank-project-form.tsx @@ -10,6 +10,7 @@ import { arraysToSampleList, sampleListToArrays } from '../../utils/sample-table import { ProjectConfigEditor } from '../project/project-config'; import { SampleTable } from '../tables/sample-table'; import { SchemaDropdown } from './components/schemas-databio-dropdown'; +import { CombinedErrorMessage } from './components/combined-error-message' interface BlankProjectInputs { is_private: boolean; @@ -27,35 +28,6 @@ interface Props { defaultNamespace?: string; } -type CombinedErrorMessageProps = { - errors: FieldErrors; -}; - -const CombinedErrorMessage = (props: CombinedErrorMessageProps) => { - const { errors } = props; - const nameError = errors.project_name?.message; - const tagError = errors.tag?.message; - let msg = null; - - if (nameError == 'empty' && !tagError) { - msg = 'Project Name must not be empty.'; - } else if (nameError == 'invalid' && !tagError) { - msg = "Project Name must contain only alphanumeric characters, '-', or '_'."; - } else if (nameError == 'empty' && tagError == 'invalid') { - msg = "Project Name must not be empty and Tag must contain only alphanumeric characters, '-', or '_'."; - } else if (nameError == 'invalid' && tagError == 'invalid') { - msg = "Project Name and Tag must contain only alphanumeric characters, '-', or '_'."; - } else if (!nameError && tagError == 'invalid') { - msg = "Project Tag must contain only alphanumeric characters, '-', or '_'."; - } - - if (nameError || tagError) { - return

{msg}

; - } - - return null; -}; - export const BlankProjectForm = (props: Props) => { const { onHide, defaultNamespace } = props; // get user innfo @@ -106,8 +78,8 @@ sample_table: samples.csv const { isPending: isSubmitting, submit } = useBlankProjectFormMutation(namespace); return ( -
-
+ +
-
-
- - - +
+ + +
-
+
:
- +
-

{message}

} /> + + - +
{ setValue('pep_schema', schema); }} + showDownload={false} /> )} />
- + -
+
-
+
{ setValue('config', data); }} - height={300} + height={295} />
+

+ * Namespace and Project Name are required. A tag value of "default" will be supplied if the Tag input is left empty. +

+
+ ), { + duration: 16000, + position: 'top-right', + }); return; } }} @@ -241,7 +242,7 @@ sample_table: samples.csv
diff --git a/web/src/components/layout/landing-paths.tsx b/web/src/components/layout/landing-paths.tsx index 940d2f6d..58d698a4 100644 --- a/web/src/components/layout/landing-paths.tsx +++ b/web/src/components/layout/landing-paths.tsx @@ -21,14 +21,14 @@ export const LandingPaths: FC = () => { @@ -38,7 +38,7 @@ export const LandingPaths: FC = () => { x1="300" y2="10" x2="400" - stroke="#3b82f6" + stroke="#052c65" strokeWidth="3" fill="none" /> @@ -48,7 +48,7 @@ export const LandingPaths: FC = () => { x1="300" y2="640" x2="400" - stroke="#3b82f6" + stroke="#052c65" strokeWidth="3" fill="none" /> @@ -58,7 +58,7 @@ export const LandingPaths: FC = () => { x1="100" y2="10" x2="0" - stroke="#3b82f6" + stroke="#052c65" strokeWidth="3" fill="none" /> @@ -68,7 +68,7 @@ export const LandingPaths: FC = () => { x1="100" y2="640" x2="0" - stroke="#3b82f6" + stroke="#052c65" strokeWidth="3" fill="none" /> @@ -81,7 +81,7 @@ export const LandingPaths: FC = () => { > @@ -90,7 +90,7 @@ export const LandingPaths: FC = () => { @@ -99,7 +99,7 @@ export const LandingPaths: FC = () => { @@ -109,7 +109,7 @@ export const LandingPaths: FC = () => { diff --git a/web/src/components/layout/nav/nav-desktop.tsx b/web/src/components/layout/nav/nav-desktop.tsx index bb1af12d..8b105d7c 100644 --- a/web/src/components/layout/nav/nav-desktop.tsx +++ b/web/src/components/layout/nav/nav-desktop.tsx @@ -58,7 +58,7 @@ export const NavDesktop = () => { return (