From db93de281dc45049d8e4bb9ecdfea18b66270f86 Mon Sep 17 00:00:00 2001 From: Sam Park Date: Mon, 5 Aug 2024 13:56:06 -0400 Subject: [PATCH 001/101] deleting view that is selected now unselects the view selector --- web/src/components/modals/add-view-options.tsx | 5 ++++- web/src/components/project/view-selector.tsx | 10 ++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/web/src/components/modals/add-view-options.tsx b/web/src/components/modals/add-view-options.tsx index ba6fee3c..c00ab6d6 100644 --- a/web/src/components/modals/add-view-options.tsx +++ b/web/src/components/modals/add-view-options.tsx @@ -15,6 +15,7 @@ type Props = { show: boolean; onHide: () => void; filteredSamples: string[]; + deleteView: (deletedView: string) => void; }; type FormValues = { @@ -30,7 +31,7 @@ type ViewOption = { }; export const ViewOptionsModal = (props: Props) => { - const { show, onHide, filteredSamples } = props; + const { show, onHide, filteredSamples, deleteView } = props; const { namespace, projectName, tag } = useProjectPage(); @@ -66,7 +67,9 @@ export const ViewOptionsModal = (props: Props) => { return; } viewMutations.removeViewMutation.mutate(selectedViewDelete.value); + console.log(selectedViewDelete) setSelectedViewDelete(null); + deleteView(selectedViewDelete.value) }; const onSubmit = () => { diff --git a/web/src/components/project/view-selector.tsx b/web/src/components/project/view-selector.tsx index d9008b7a..a7fe6631 100644 --- a/web/src/components/project/view-selector.tsx +++ b/web/src/components/project/view-selector.tsx @@ -41,6 +41,15 @@ export const ViewSelector = (props: ViewSelectorProps) => { const { user } = useSession(); const { data: projectInfo } = useProjectAnnotation(namespace, projectName, tag); + const deleteView = (deletedView: string) => { + if (selectRef.current.getValue()[0].value === deletedView) { + setView(undefined); + searchParams.delete('view'); + setSearchParams(searchParams); + selectRef.current.clearValue(); + }; + }; + const userHasOwnership = user && projectInfo && canEdit(user, projectInfo); const selectorRadius = userHasOwnership ? '0 .25em .25em 0' : '.25em'; @@ -121,6 +130,7 @@ export const ViewSelector = (props: ViewSelectorProps) => { show={showViewOptionsModal} onHide={() => setShowViewOptionsModal(false)} filteredSamples={filteredSamples} + deleteView={deleteView} /> ); From 057803ac72e07547f15f0751e8c730919a7ab404 Mon Sep 17 00:00:00 2001 From: Sam Park Date: Mon, 5 Aug 2024 14:33:29 -0400 Subject: [PATCH 002/101] rounded corner in schema select dropdown --- .../forms/components/schemas-databio-dropdown.tsx | 6 ++++++ web/src/components/modals/add-view-options.tsx | 1 - 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/web/src/components/forms/components/schemas-databio-dropdown.tsx b/web/src/components/forms/components/schemas-databio-dropdown.tsx index 64bc24c7..ea8553bc 100644 --- a/web/src/components/forms/components/schemas-databio-dropdown.tsx +++ b/web/src/components/forms/components/schemas-databio-dropdown.tsx @@ -32,6 +32,12 @@ const SchemaDropdown: FC = ({ value, onChange, showDownload = true }) => isClearable menuPlacement="top" className="w-100" + styles={{ + control: (provided) => ({ + ...provided, + borderRadius: '.33333em', + }) + }} /> {showDownload && ( { return; } viewMutations.removeViewMutation.mutate(selectedViewDelete.value); - console.log(selectedViewDelete) setSelectedViewDelete(null); deleteView(selectedViewDelete.value) }; From 7905ce6304d100bf7cb494ae13b94975ce008dbd Mon Sep 17 00:00:00 2001 From: Sam Park Date: Mon, 5 Aug 2024 17:07:55 -0400 Subject: [PATCH 003/101] added modal for no schema yellow button and prevented clearing schemas from dropdown because it doesn --- .../components/schemas-databio-dropdown.tsx | 2 +- .../components/modals/validation-result.tsx | 72 ++++++++++++------- .../project-validation-and-edit-buttons.tsx | 36 ++-------- .../project/validation/validation-result.tsx | 16 +++++ 4 files changed, 72 insertions(+), 54 deletions(-) diff --git a/web/src/components/forms/components/schemas-databio-dropdown.tsx b/web/src/components/forms/components/schemas-databio-dropdown.tsx index ea8553bc..027e9e66 100644 --- a/web/src/components/forms/components/schemas-databio-dropdown.tsx +++ b/web/src/components/forms/components/schemas-databio-dropdown.tsx @@ -29,7 +29,7 @@ const SchemaDropdown: FC = ({ value, onChange, showDownload = true }) => onChange(newValue?.value || ''); }} placeholder={isLoading ? 'Fetching schemas...' : 'Assign a schema...'} - isClearable + // isClearable menuPlacement="top" className="w-100" styles={{ diff --git a/web/src/components/modals/validation-result.tsx b/web/src/components/modals/validation-result.tsx index 681cb5ed..10f5494d 100644 --- a/web/src/components/modals/validation-result.tsx +++ b/web/src/components/modals/validation-result.tsx @@ -19,7 +19,7 @@ type FormProps = { }; export const ValidationResultModal = (props: Props) => { - const { show, onHide, validationResult } = props; + const { show, onHide, validationResult, currentSchema } = props; const { namespace, projectName, tag } = useProjectPage(); @@ -33,9 +33,15 @@ export const ValidationResultModal = (props: Props) => { const newSchema = updateForm.watch('schema'); const handleSubmit = () => { - submit({ - newSchema, - }); + if (newSchema === '') { + submit({ + newSchema: null, + }); + } else { + submit({ + newSchema: newSchema, + }); + } }; return ( @@ -49,33 +55,51 @@ export const ValidationResultModal = (props: Props) => { >

- {validationResult?.valid ? ( - - - Validation Passed - + {currentSchema ? ( + <> + {validationResult?.valid ? ( + + + Validation Passed + + ) : ( + + + Validation Failed + + )} + ) : ( - - - Validation Failed + + Select a Schema )}

- {validationResult?.valid ? ( -

Your PEP is valid against the schema.

- ) : ( - -

You PEP is invalid against the schema.

-

Validation result:

-
-              {JSON.stringify(validationResult, null, 2)}
-            
-
+ {currentSchema ? ( + <> + {validationResult?.valid ? ( +

Your PEP is valid against the schema.

+ ) : ( + +

You PEP is invalid against the schema.

+

Validation result:

+
+                {JSON.stringify(validationResult, null, 2)}
+              
+
+ )} + + ) : ( null )} -
- + + + {currentSchema ? ( + + ) : ( + + )}
- {projectInfo?.pep_schema ? ( - - ) : ( -
- <> - - As you edit your project below, it will be validated against the schema currently selected for - it. - - } - delay={{ show: 250, hide: 500 }} - trigger={['hover']} - > -
- - No schema -
-
- -
- )} -
+
+ ) : ( + + )} + Date: Tue, 6 Aug 2024 10:54:01 -0400 Subject: [PATCH 004/101] missed closing div --- .../components/project/project-validation-and-edit-buttons.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/web/src/components/project/project-validation-and-edit-buttons.tsx b/web/src/components/project/project-validation-and-edit-buttons.tsx index 924c03a7..beeb5ece 100644 --- a/web/src/components/project/project-validation-and-edit-buttons.tsx +++ b/web/src/components/project/project-validation-and-edit-buttons.tsx @@ -49,6 +49,7 @@ export const ProjectValidationAndEditButtons = (props: ProjectValidationAndEditB isValidating={projectValidationQuery.isLoading} validationResult={validationResult} /> +
+ + + + + ); +}; diff --git a/web/src/components/project/project-header.tsx b/web/src/components/project/project-header.tsx index b052e4b1..5a94bdb6 100644 --- a/web/src/components/project/project-header.tsx +++ b/web/src/components/project/project-header.tsx @@ -2,10 +2,18 @@ import { ProjectInfoFooter } from './project-info-footer'; import { ProjectDescription } from './project-page-description'; import { ProjectHeaderBar } from './project-page-header-bar'; -export const ProjectHeader = () => { + +type Props = { + sampleTable: ReturnType['data']; + sampleTableIndex: string; +}; + +export const ProjectHeader = (props: Props) => { + const { sampleTable, sampleTableIndex } = props; + return (
- +
diff --git a/web/src/components/project/project-page-header-bar.tsx b/web/src/components/project/project-page-header-bar.tsx index 020c0b65..ed2b381c 100644 --- a/web/src/components/project/project-page-header-bar.tsx +++ b/web/src/components/project/project-page-header-bar.tsx @@ -18,10 +18,18 @@ import { canEdit } from '../../utils/permissions'; import { downloadZip } from '../../utils/project'; import { ProjectHistoryModal } from '../modals/project-history'; import { ProjectHeaderBarPlaceholder } from './placeholders/project-header-bar-placeholder'; +import { StandardizeMetadataModal } from '../modals/standardize-metadata'; -type ProjectPageHeaderBarProps = {}; -export const ProjectHeaderBar = (props: ProjectPageHeaderBarProps) => { + +type Props = { + sampleTable: ReturnType['data']; + sampleTableIndex: string; +}; + +export const ProjectHeaderBar = (props: Props) => { + const { sampleTable, sampleTableIndex } = props; + const { user, login, jwt } = useSession(); const [searchParams] = useSearchParams(); @@ -45,6 +53,7 @@ export const ProjectHeaderBar = (props: ProjectPageHeaderBarProps) => { const [showEditMetaMetadataModal, setShowEditMetaMetadataModal] = useState(false); const [showAddToPOPModal, setShowAddToPOPModal] = useState(false); const [showProjectHistoryModal, setShowProjectHistoryModal] = useState(false); + const [showStandardizeMetadataModal, setShowStandardizeMetadataModal] = useState(false); // queries const projectAnnotationQuery = useProjectAnnotation(namespace, projectName, tag); @@ -179,13 +188,17 @@ export const ProjectHeaderBar = (props: ProjectPageHeaderBarProps) => { {user && projectInfo && canEdit(user, projectInfo) && ( + setShowEditMetaMetadataModal(true)}> + + Edit + setShowProjectHistoryModal(true)}> History - setShowEditMetaMetadataModal(true)}> - - Edit + setShowStandardizeMetadataModal(true)}> + + Standardize setShowDeletePEPModal(true)}> @@ -274,9 +287,7 @@ export const ProjectHeaderBar = (props: ProjectPageHeaderBarProps) => { /> { - setShowAddToPOPModal(false); - }} + onHide={() => setShowAddToPOPModal(false)} namespace={namespace!} project={projectName} tag={tag} @@ -288,6 +299,15 @@ export const ProjectHeaderBar = (props: ProjectPageHeaderBarProps) => { project={projectName} tag={tag} /> + setShowStandardizeMetadataModal(false)} + namespace={namespace} + project={projectName} + tag={tag} + sampleTable={sampleTable} + sampleTableIndex={sampleTableIndex} + />
); }; diff --git a/web/src/globals.css b/web/src/globals.css index d28464d2..a3b90159 100644 --- a/web/src/globals.css +++ b/web/src/globals.css @@ -688,3 +688,14 @@ body { .btn-dark.glow-button { --box-shadow-color: #343a4080; } + +.btn-group-vertical .btn-check:checked + .btn-outline-secondary{ + box-shadow: inset 0 0 10px 2.5px #28a74524 !important; + outline: none; + border: 1px solid #28a745; + + background-color: #28a74516; + color: #28a745; +} + + diff --git a/web/src/pages/Project.tsx b/web/src/pages/Project.tsx index 98a68fc3..c9138d74 100644 --- a/web/src/pages/Project.tsx +++ b/web/src/pages/Project.tsx @@ -115,7 +115,7 @@ export const ProjectPage = () => { )} - + {projectInfo?.pop && !forceTraditionalInterface ? ( ) : ( From 9ecb06ee5e59e13c2457462115d8671acf56675a Mon Sep 17 00:00:00 2001 From: Sam Park Date: Tue, 13 Aug 2024 11:43:47 -0400 Subject: [PATCH 006/101] standardizer ui kinda works --- .../modals/standardize-metadata.tsx | 94 ++++++++++++++----- .../components/project/project-interface.tsx | 20 ++++ .../project/project-page-header-bar.tsx | 14 +-- web/src/globals.css | 2 +- .../hooks/stores/useStandardizeModalStore.ts | 11 +++ 5 files changed, 108 insertions(+), 33 deletions(-) create mode 100644 web/src/hooks/stores/useStandardizeModalStore.ts diff --git a/web/src/components/modals/standardize-metadata.tsx b/web/src/components/modals/standardize-metadata.tsx index 41259a07..967232dd 100644 --- a/web/src/components/modals/standardize-metadata.tsx +++ b/web/src/components/modals/standardize-metadata.tsx @@ -14,18 +14,23 @@ type Props = { onHide: () => void; sampleTable: ReturnType['data']; sampleTableIndex: string; + newSamples: any; + setNewSamples: (samples: any[][]) => void; }; export const StandardizeMetadataModal = (props: Props) => { - const { namespace, project, tag, show, onHide, sampleTable, sampleTableIndex } = props; + const { namespace, project, tag, show, onHide, sampleTable, sampleTableIndex, newSamples, setNewSamples } = props; + + // const tabDataRaw = sampleListToArrays(sampleTable?.items || []) + const tabDataRaw = newSamples; - const tabDataRaw = sampleListToArrays(sampleTable?.items || []) const tabData = tabDataRaw[0].map((_, colIndex) => tabDataRaw.map(row => row[colIndex])).reduce((obj, row) => { const [key, ...values] = row; obj[key] = values; return obj; }, {}); + // console.log(tabDataRaw) // console.log(tabData) // console.log(sampleTableIndex) @@ -38,25 +43,13 @@ export const StandardizeMetadataModal = (props: Props) => { return `${(value * 100).toFixed(2)}%`; }; - const [selectedValues, setSelectedValues] = useState({}); - const [hasDuplicates, setHasDuplicates] = useState(false); - const handleRadioChange = (key, value) => { setSelectedValues(prev => { - let newValues; - if (value === null) { - // Create a new object without the specified key - const { [key]: _, ...rest } = prev; - newValues = rest; - } else { - // Add or update the key-value pair - newValues = { - ...prev, - [key]: value - }; - } + const newValues = { + ...prev, + [key]: value === null ? key : value + }; - // Check for duplicates setHasDuplicates(checkForDuplicates(newValues)); return newValues; @@ -82,6 +75,47 @@ export const StandardizeMetadataModal = (props: Props) => { return false; // No duplicates found }; + const getDefaultSelections = () => { + const defaultSelections = {}; + Object.keys(data).forEach(key => { + if (data[key]['Not Predictable'] === 0) { + defaultSelections[key] = key; // Use the key itself as the default value + } else { + const options = Object.entries(data[key]); + const [bestOption] = options.reduce((best, current) => + current[1] > best[1] && current[0] !== 'Not Predictable' ? current : best + ); + defaultSelections[key] = bestOption; + } + }); + return defaultSelections; + }; + + type TabDataRow = string[]; + type TabData = TabDataRow[]; + type SelectedValues = Record; + + const updateTabDataRaw = (tabDataRaw: TabData, selectedValues: SelectedValues): TabData => { + if (tabDataRaw.length === 0) return tabDataRaw; + + // Create a new array to avoid mutating the original + const updatedTabDataRaw: TabData = [tabDataRaw[0].slice(), ...tabDataRaw.slice(1)]; + + // Update only the column names (first row) based on selectedValues + Object.entries(selectedValues).forEach(([key, value]) => { + const columnIndex = updatedTabDataRaw[0].indexOf(key); + if (columnIndex !== -1 && key !== value) { + updatedTabDataRaw[0][columnIndex] = value; + } + }); + + return updatedTabDataRaw; + }; + + + const [selectedValues, setSelectedValues] = useState(getDefaultSelections()); + const [hasDuplicates, setHasDuplicates] = useState(false); + return ( @@ -131,7 +165,7 @@ export const StandardizeMetadataModal = (props: Props) => { name={key} id={`${key}-suggested-${subKey}`} value={subKey} - defaultChecked={(value !== 0) && (index === 0)} + defaultChecked={selectedValues[key] === subKey} disabled={data[key]['Not Predictable'] === 0} onChange={() => handleRadioChange(key, subKey)} /> @@ -146,8 +180,8 @@ export const StandardizeMetadataModal = (props: Props) => { type="radio" name={key} id={`${key}-original`} - value="original" - defaultChecked={data[key]['Not Predictable'] === 0} + value={key} + defaultChecked={selectedValues[key] === key} // Check if the selected value is the same as the key onChange={() => handleRadioChange(key, null)} />
-
-
-

{key}

- - {tabData[key] && ( -

[{tabData[key].slice(0, 3).join(', ')}]

- )} +
+
+ +
+ [item]) || []) + ]} + colHeaders={false} + rowHeaders={true} + width='100%' + height='90%' + colWidths="100%" // Set all columns to 100% width + stretchH="all" // Stretch all columns + autoColumnSize={false} // Disable auto column sizing + columns={[ + { + data: 0, + type: typeof tabData[key] === 'number' ? 'numeric' : 'text', + renderer: function(instance, td, row, col, prop, value, cellProperties) { + Handsontable.renderers.TextRenderer.apply(this, arguments); + if (row === 0) { + td.style.fontWeight = 'bold'; + } + } + } + ]} + licenseKey="non-commercial-and-evaluation" + /> +
-
+
{Object.entries(data[key]).map(([subKey, value], index, array) => ( @@ -169,7 +199,7 @@ export const StandardizeMetadataModal = (props: Props) => { disabled={data[key]['Not Predictable'] === 0} onChange={() => handleRadioChange(key, subKey)} /> - @@ -184,7 +214,7 @@ export const StandardizeMetadataModal = (props: Props) => { defaultChecked={selectedValues[key] === key} // Check if the selected value is the same as the key onChange={() => handleRadioChange(key, null)} /> -
@@ -198,7 +228,7 @@ export const StandardizeMetadataModal = (props: Props) => { - {hasDuplicates && ( + {whereDuplicates !== null && (
Warning: ensure no duplicates between original and predicted columns have been selected.
@@ -215,7 +245,7 @@ export const StandardizeMetadataModal = (props: Props) => {
-
+
+ + +
+
+
Standardizer Schema
+
+
+ + ({ + ...provided, + borderRadius: '.333333em', // Left radii set to 0, right radii kept at 4px + }), + }} + options={ + [ + { value: 'ENCODE', label: 'ENCODE' }, + { value: 'Fairtracks', label: 'Fairtracks' }, + { value: 'vanilla', label: 'Vanilla' } + ] + } + /> +
+
+ +
+
+
+ +
+
Original Column
-
Predicted Standardized Column
+
Predicted Column Header
+ {Object.keys(data).map((key, index) => (
-
+
+
-
+
[item]) || []) - ]} + data={prepareHandsontableData(key, selectedValues, tabData)} colHeaders={false} rowHeaders={true} width='100%' - height='90%' - colWidths="100%" // Set all columns to 100% width - stretchH="all" // Stretch all columns - autoColumnSize={false} // Disable auto column sizing + height='100%' + colWidths="100%" + stretchH="all" + autoColumnSize={false} columns={[ { data: 0, @@ -175,48 +223,55 @@ export const StandardizeMetadataModal = (props: Props) => { Handsontable.renderers.TextRenderer.apply(this, arguments); if (row === 0) { td.style.fontWeight = 'bold'; + if (whereDuplicates?.includes(index)) { + td.style.color= 'red'; + } } + } } ]} licenseKey="non-commercial-and-evaluation" + className='custom-handsontable' />
-
-
- {Object.entries(data[key]).map(([subKey, value], index, array) => ( - - - handleRadioChange(key, subKey)} - /> - - - ))} - - handleRadioChange(key, null)} - /> - +
+
+
+ handleRadioChange(key, null)} + /> + + + {Object.entries(data[key]).map(([subKey, value], index, array) => ( + + + handleRadioChange(key, subKey)} + /> + + + ))} +

@@ -230,7 +285,7 @@ export const StandardizeMetadataModal = (props: Props) => { {whereDuplicates !== null && (
- Warning: ensure no duplicates between original and predicted columns have been selected. + Warning: ensure no duplicate column names have been selected.
)} diff --git a/web/src/globals.css b/web/src/globals.css index c2902bd9..907300f0 100644 --- a/web/src/globals.css +++ b/web/src/globals.css @@ -699,3 +699,39 @@ body { } +.custom-handsontable .handsontable { +/* border-radius: 8px;*/ + overflow: hidden; +} + +.custom-handsontable .handsontable th, +.custom-handsontable .handsontable td { +/* border-color: #ccc;*/ +} + +.custom-handsontable .handsontable th:first-child, +.custom-handsontable .handsontable td:first-child { + border-left: none !important; +} + +.custom-handsontable .handsontable th:last-child, +.custom-handsontable .handsontable td:last-child { + border-right: none !important; +} + +.custom-handsontable .handsontable tr:first-child th, +.custom-handsontable .handsontable tr:first-child td { + border-top: none !important; +} + +.custom-handsontable .handsontable tr:last-child th, +.custom-handsontable .handsontable tr:last-child td { + border-bottom: none !important; +} + +.btn-group-vertical { + bottom: -1px; +} + + + From 862f891e642fa450f96cbfb797c7233005b77de8 Mon Sep 17 00:00:00 2001 From: Sam Park Date: Fri, 16 Aug 2024 12:00:07 -0400 Subject: [PATCH 009/101] progress on standardizer api, trouble using hook to refetch it --- pephub/routers/api/v1/project.py | 37 +++ web/src/api/project.ts | 21 ++ .../modals/standardize-metadata.tsx | 250 ++++++++++-------- web/src/globals.css | 5 - web/src/hooks/queries/useStandardize.ts | 14 + 5 files changed, 213 insertions(+), 114 deletions(-) create mode 100644 web/src/hooks/queries/useStandardize.ts diff --git a/pephub/routers/api/v1/project.py b/pephub/routers/api/v1/project.py index 01718e64..847510fd 100644 --- a/pephub/routers/api/v1/project.py +++ b/pephub/routers/api/v1/project.py @@ -55,6 +55,7 @@ ConfigResponseModel, ) from .helpers import verify_updated_project +from attribute_standardizer.attr_standardizer_class import AttrStandardizer _LOGGER = logging.getLogger(__name__) @@ -1138,3 +1139,39 @@ def delete_full_history( status_code=400, detail="Could not delete history. Server error.", ) + + +@project.get( + "/standardize", + summary="Standardize PEP metadata column headers", +) +async def get_standardized_cols( + namespace: str, + project: str, + tag: Optional[str] = DEFAULT_TAG, + schema: str = '' +): + """ + Standardize PEP metadata column headers using BEDmess. + + Args: + - pep (str): PEP string to be standardized + - schema (str): Schema for AttrStandardizer + + Returns: + - dict: Standardized results + """ + + if schema == '': + return {} + + path = namespace + '/' + project + ':' + tag + print(path) + + model = AttrStandardizer(schema) + + results = model.standardize(pep=path) + return {"results": results} + + + diff --git a/web/src/api/project.ts b/web/src/api/project.ts index 18927cda..f14003db 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`; + return axios + .get(url, { headers: { Authorization: `Bearer ${jwt}` } }) + .then((res) => res.data); +}; diff --git a/web/src/components/modals/standardize-metadata.tsx b/web/src/components/modals/standardize-metadata.tsx index 8de9095d..1f1ba62d 100644 --- a/web/src/components/modals/standardize-metadata.tsx +++ b/web/src/components/modals/standardize-metadata.tsx @@ -5,6 +5,7 @@ import { useProjectAnnotation } from '../../hooks/queries/useProjectAnnotation'; import { useSampleTable } from '../../hooks/queries/useSampleTable'; import { ProjectMetaEditForm } from '../forms/edit-project-meta'; import { arraysToSampleList, sampleListToArrays } from '../../utils/sample-table'; +import { useStandardize } from '../../hooks/queries/useStandardize'; import { HotTable } from '@handsontable/react'; import Handsontable from 'handsontable'; @@ -24,6 +25,10 @@ type Props = { setNewSamples: (samples: any[][]) => void; }; +type TabDataRow = string[]; +type TabData = TabDataRow[]; +type SelectedValues = Record; + export const StandardizeMetadataModal = (props: Props) => { const { namespace, project, tag, show, onHide, sampleTable, sampleTableIndex, newSamples, setNewSamples } = props; @@ -39,10 +44,21 @@ export const StandardizeMetadataModal = (props: Props) => { // console.log(tabData) // console.log(sampleTableIndex) - const data: DynamicData = { - 'sample_type': { 'Not Predictable': 0.0 }, - 'genome': { 'biospecimen_class_term_label': 0.9097071290016174, 'prediction_2': 0.321412348, 'sample_type': 0.12534} - }; + // const [selectedOption, setSelectedOption] = useState({ value: 'ENCODE', label: 'ENCODE' }); + const [selectedOption, setSelectedOption] = useState(null); + const selectRef = React.useRef(null); + + const { isLoading, isError, error, data, refetch } = useStandardize( + namespace, + project, + tag, + selectedOption?.value, + ); + + const standardizedData = data?.results + + console.log(selectedOption) + console.log(standardizedData) const formatToPercentage = (value: number): string => { return `${(value * 100).toFixed(2)}%`; @@ -83,26 +99,16 @@ export const StandardizeMetadataModal = (props: Props) => { }; const getDefaultSelections = () => { + if (!standardizedData) { + return {}; + } const defaultSelections = {}; - Object.keys(data).forEach(key => { - // if (data[key]['Not Predictable'] === 0) { - // defaultSelections[key] = key; // Use the key itself as the default value - // } else { - // const options = Object.entries(data[key]); - // const [bestOption] = options.reduce((best, current) => - // current[1] > best[1] && current[0] !== 'Not Predictable' ? current : best - // ); - // defaultSelections[key] = bestOption; - // } + Object.keys(standardizedData).forEach(key => { defaultSelections[key] = key; }); return defaultSelections; }; - type TabDataRow = string[]; - type TabData = TabDataRow[]; - type SelectedValues = Record; - const updateTabDataRaw = (tabDataRaw: TabData, selectedValues: SelectedValues): TabData => { if (tabDataRaw.length === 0) return tabDataRaw; @@ -120,7 +126,7 @@ export const StandardizeMetadataModal = (props: Props) => { return updatedTabDataRaw; }; - function prepareHandsontableData(key, selectedValues, tabData) { + const prepareHandsontableData = (key, selectedValues, tabData) => { const selectedValue = selectedValues[key] || ''; const topValues = tabData[key]?.slice(0, 6).map(item => [item]) || []; const emptyRows = Array(Math.max(0, 6 - topValues.length)).fill(['']); @@ -132,6 +138,16 @@ export const StandardizeMetadataModal = (props: Props) => { ]; } + const handleSubmit = async (event) => { + event.preventDefault(); + if (selectRef.current) { + const currentValue = selectRef.current.getValue()[0]; + setSelectedOption(currentValue); + console.log('Form submitted with:', currentValue); + // Refetch data with new selectedOption + await refetch(); + } + }; const [selectedValues, setSelectedValues] = useState(getDefaultSelections()); const [whereDuplicates, setWhereDuplicates] = useState(null) @@ -156,106 +172,118 @@ export const StandardizeMetadataModal = (props: Props) => {
Standardizer Schema
-
-
- + + +
+
({ - ...provided, - borderRadius: '.333333em', // Left radii set to 0, right radii kept at 4px - }), - }} - options={ - [ - { value: 'ENCODE', label: 'ENCODE' }, - { value: 'Fairtracks', label: 'Fairtracks' }, - { value: 'vanilla', label: 'Vanilla' } - ] - } - /> + className="top-z w-100 ms-1" + ref={selectRef} + styles={{ + control: (provided) => ({ + ...provided, + borderRadius: '.333333em', // Left radii set to 0, right radii kept at 4px + }), + }} + options={ + [ + { value: 'ENCODE', label: 'ENCODE' }, + { value: 'FAIRTRACKS', label: 'Fairtracks' } + ] + } + defaultValue={selectedOption} + />
- + + +
+
-
+
-
+ {standardizedData ? -
-
-
Original Column
-
-
-
Predicted Column Header
+ <> +
+ +
+
+
Original Column
+
+
+
Predicted Column Header
+
-
-
- {Object.keys(data).map((key, index) => ( -
- -
- -
- -
- + {Object.keys(standardizedData).map((key, index) => ( +
+ +
+ +
+ +
+ -
- -
-
-
-
- handleRadioChange(key, null)} + ]} + licenseKey="non-commercial-and-evaluation" + className='custom-handsontable' /> - +
- {Object.entries(data[key]).map(([subKey, value], index, array) => ( - - +
+
+
+
+ handleRadioChange(key, null)} + /> + + + {Object.entries(standardizedData[key]).map(([subKey, value], index, array) => ( + { id={`${key}-suggested-${subKey}`} value={subKey} defaultChecked={selectedValues[key] === subKey} - disabled={data[key]['Not Predictable'] === 0} + disabled={standardizedData[key]['Not Predictable'] === 0} onChange={() => handleRadioChange(key, subKey)} /> - - ))} + + ))} + +
+
-
-
- ))} - + ))} + + + : null + } diff --git a/web/src/globals.css b/web/src/globals.css index 907300f0..868f7dc3 100644 --- a/web/src/globals.css +++ b/web/src/globals.css @@ -729,9 +729,4 @@ body { border-bottom: none !important; } -.btn-group-vertical { - bottom: -1px; -} - - diff --git a/web/src/hooks/queries/useStandardize.ts b/web/src/hooks/queries/useStandardize.ts new file mode 100644 index 00000000..1e31d150 --- /dev/null +++ b/web/src/hooks/queries/useStandardize.ts @@ -0,0 +1,14 @@ +import { useQuery } from '@tanstack/react-query'; + +import { getStandardizedCols } from '../../api/project'; +import { useSession } from '../../contexts/session-context'; + +export const useStandardize = (namespace: string | undefined, project: string | undefined, tag: string | undefined, schema: string | undefined) => { + const session = useSession(); + const query = useQuery({ + queryKey: [namespace, project, tag], + queryFn: () => getStandardizedCols(namespace || '', project || '', tag, session?.jwt || '', schema || ''), + enabled: namespace !== undefined || (project !== undefined && session?.jwt !== null) || schema !== null, + }); + return query; +}; From a69d512654f15847dc19bc8bd87e7b4d0242b6ea Mon Sep 17 00:00:00 2001 From: Nathan LeRoy Date: Fri, 16 Aug 2024 12:23:23 -0400 Subject: [PATCH 010/101] small tweaks --- .../modals/standardize-metadata.tsx | 363 +++++++++--------- web/src/hooks/queries/useStandardize.ts | 9 +- 2 files changed, 183 insertions(+), 189 deletions(-) diff --git a/web/src/components/modals/standardize-metadata.tsx b/web/src/components/modals/standardize-metadata.tsx index 1f1ba62d..7fd19bd0 100644 --- a/web/src/components/modals/standardize-metadata.tsx +++ b/web/src/components/modals/standardize-metadata.tsx @@ -1,17 +1,16 @@ +import { HotTable } from '@handsontable/react'; +import Handsontable from 'handsontable'; +import 'handsontable/dist/handsontable.full.css'; +import React, { FormEvent, useState } from 'react'; import { Modal } from 'react-bootstrap'; -import React, { useState } from 'react'; +import ReactSelect from 'react-select'; + import { useEditProjectMetaMutation } from '../../hooks/mutations/useEditProjectMetaMutation'; import { useProjectAnnotation } from '../../hooks/queries/useProjectAnnotation'; import { useSampleTable } from '../../hooks/queries/useSampleTable'; -import { ProjectMetaEditForm } from '../forms/edit-project-meta'; -import { arraysToSampleList, sampleListToArrays } from '../../utils/sample-table'; import { useStandardize } from '../../hooks/queries/useStandardize'; - -import { HotTable } from '@handsontable/react'; -import Handsontable from 'handsontable'; -import 'handsontable/dist/handsontable.full.css'; - -import ReactSelect from 'react-select'; +import { arraysToSampleList, sampleListToArrays } from '../../utils/sample-table'; +import { ProjectMetaEditForm } from '../forms/edit-project-meta'; type Props = { namespace: string; @@ -28,51 +27,54 @@ type Props = { type TabDataRow = string[]; type TabData = TabDataRow[]; type SelectedValues = Record; +type AvailableSchemas = 'ENCODE' | 'FAIRTRACKS'; export const StandardizeMetadataModal = (props: Props) => { const { namespace, project, tag, show, onHide, sampleTable, sampleTableIndex, newSamples, setNewSamples } = props; // const tabDataRaw = sampleListToArrays(sampleTable?.items || []) const tabDataRaw = newSamples; - const tabData = tabDataRaw[0].map((_, colIndex) => tabDataRaw.map(row => row[colIndex])).reduce((obj, row) => { - const [key, ...values] = row; - obj[key] = values; - return obj; - }, {}); + const tabData = tabDataRaw[0] + .map((_, colIndex) => tabDataRaw.map((row) => row[colIndex])) + .reduce((obj, row) => { + const [key, ...values] = row; + obj[key] = values; + return obj; + }, {}); // console.log(tabDataRaw) // console.log(tabData) // console.log(sampleTableIndex) // const [selectedOption, setSelectedOption] = useState({ value: 'ENCODE', label: 'ENCODE' }); - const [selectedOption, setSelectedOption] = useState(null); - const selectRef = React.useRef(null); - - const { isLoading, isError, error, data, refetch } = useStandardize( - namespace, - project, - tag, - selectedOption?.value, - ); + const [selectedOption, setSelectedOption] = useState('ENCODE'); + + const { + isLoading, + isError, + error, + data, + refetch: standardize, + } = useStandardize(namespace, project, tag, selectedOption?.value); - const standardizedData = data?.results + const standardizedData = data?.results; - console.log(selectedOption) - console.log(standardizedData) + console.log(selectedOption); + console.log(standardizedData); const formatToPercentage = (value: number): string => { return `${(value * 100).toFixed(2)}%`; }; const handleRadioChange = (key, value) => { - setSelectedValues(prev => { + setSelectedValues((prev) => { const newValues = { ...prev, - [key]: value === null ? key : value + [key]: value === null ? key : value, }; - setWhereDuplicates(checkForDuplicates(newValues)) - + setWhereDuplicates(checkForDuplicates(newValues)); + return newValues; }); }; @@ -103,7 +105,7 @@ export const StandardizeMetadataModal = (props: Props) => { return {}; } const defaultSelections = {}; - Object.keys(standardizedData).forEach(key => { + Object.keys(standardizedData).forEach((key) => { defaultSelections[key] = key; }); return defaultSelections; @@ -128,29 +130,22 @@ export const StandardizeMetadataModal = (props: Props) => { const prepareHandsontableData = (key, selectedValues, tabData) => { const selectedValue = selectedValues[key] || ''; - const topValues = tabData[key]?.slice(0, 6).map(item => [item]) || []; + const topValues = tabData[key]?.slice(0, 6).map((item) => [item]) || []; const emptyRows = Array(Math.max(0, 6 - topValues.length)).fill(['']); - return [ - [selectedValue], - ...topValues, - ...emptyRows - ]; - } + return [[selectedValue], ...topValues, ...emptyRows]; + }; - const handleSubmit = async (event) => { + const handleSubmit = async (event: FormEvent) => { event.preventDefault(); - if (selectRef.current) { - const currentValue = selectRef.current.getValue()[0]; - setSelectedOption(currentValue); - console.log('Form submitted with:', currentValue); - // Refetch data with new selectedOption - await refetch(); - } + console.log('Form submitted with:', selectedOption); + + // Refetch data with new selectedOption + await standardize(); }; const [selectedValues, setSelectedValues] = useState(getDefaultSelections()); - const [whereDuplicates, setWhereDuplicates] = useState(null) + const [whereDuplicates, setWhereDuplicates] = useState(null); return ( @@ -158,192 +153,189 @@ export const StandardizeMetadataModal = (props: Props) => { Metadata Standardizer -
-
-

Use the metadata standardizer powered by BEDmess to bring consistency across metadata columns in all of your projects. - Compare predicted suggestions below (accuracy indicated in parenthesis) and choose whether to keep or discard them. - Column contents [previewed in brackets] are not changed by the standardizer. - After accepting the changes, save your project for them to take effect.

+
+
+

+ Use the metadata standardizer powered by BEDmess to bring consistency across metadata columns in all of + your projects. Compare predicted suggestions below (accuracy indicated in parenthesis) and choose whether + to keep or discard them. Column contents [previewed in brackets] are not changed by the standardizer. + After accepting the changes, save your project for them to take effect. +

-
+
+
+
+
Standardizer Schema
-
-
-
Standardizer Schema
-
-
-
+
+
({ ...provided, borderRadius: '.333333em', // Left radii set to 0, right radii kept at 4px }), }} - options={ - [ - { value: 'ENCODE', label: 'ENCODE' }, - { value: 'FAIRTRACKS', label: 'Fairtracks' } - ] - } + options={[ + // @ts-ignore + { value: 'ENCODE', label: 'ENCODE' }, + // @ts-ignore + { value: 'FAIRTRACKS', label: 'Fairtracks' }, + ]} defaultValue={selectedOption} + value={selectedOption} + onChange={(selectedOption) => { + if (selectedOption === null) { + return; + } + setSelectedOption(selectedOption); + }} /> -
-
- - -
- +
+ +
- {standardizedData ? - - <> -
+ {standardizedData ? ( + <> +
-
-
-
Original Column
-
-
-
Predicted Column Header
+
+
+
Original Column
+
+
+
Predicted Column Header
+
-
- -
- {Object.keys(standardizedData).map((key, index) => ( -
- -
- -
- -
- + {Object.keys(standardizedData).map((key, index) => ( +
+
+
+
+ -
- -
-
-
-
- handleRadioChange(key, null)} + }, + }, + ]} + licenseKey="non-commercial-and-evaluation" + className="custom-handsontable" /> - - - {Object.entries(standardizedData[key]).map(([subKey, value], index, array) => ( - - handleRadioChange(key, subKey)} - /> - - - ))} -
+
+
+
+ handleRadioChange(key, null)} + /> + + + {Object.entries(standardizedData[key]).map(([subKey, value], index, array) => ( + + handleRadioChange(key, subKey)} + /> + + + ))} +
+
+
+
-
-
- ))} - - - : null - } - + ))} + + + ) : null} - {whereDuplicates !== null && ( -
- Warning: ensure no duplicate column names have been selected. -
+
Warning: ensure no duplicate column names have been selected.
)} - - -
- ); }; diff --git a/web/src/hooks/queries/useStandardize.ts b/web/src/hooks/queries/useStandardize.ts index 1e31d150..e041f88c 100644 --- a/web/src/hooks/queries/useStandardize.ts +++ b/web/src/hooks/queries/useStandardize.ts @@ -3,12 +3,17 @@ import { useQuery } from '@tanstack/react-query'; import { getStandardizedCols } from '../../api/project'; import { useSession } from '../../contexts/session-context'; -export const useStandardize = (namespace: string | undefined, project: string | undefined, tag: string | undefined, schema: string | undefined) => { +export const useStandardize = ( + namespace: string | undefined, + project: string | undefined, + tag: string | undefined, + schema: string | undefined, +) => { const session = useSession(); const query = useQuery({ queryKey: [namespace, project, tag], queryFn: () => getStandardizedCols(namespace || '', project || '', tag, session?.jwt || '', schema || ''), - enabled: namespace !== undefined || (project !== undefined && session?.jwt !== null) || schema !== null, + enabled: false, // This query should only run on demand (ie. when the user clicks the standardize button) }); return query; }; From 3030166694e20cdba09512f9cb3a53a31cfada50 Mon Sep 17 00:00:00 2001 From: Sam Park Date: Fri, 16 Aug 2024 16:49:27 -0400 Subject: [PATCH 011/101] metadata standardizer api integration --- web/src/api/project.ts | 4 +- .../modals/standardize-metadata.tsx | 93 +++++++++---------- .../components/spinners/loading-spinner.tsx | 23 +++-- web/src/globals.css | 17 ++++ 4 files changed, 75 insertions(+), 62 deletions(-) diff --git a/web/src/api/project.ts b/web/src/api/project.ts index f14003db..67533a5d 100644 --- a/web/src/api/project.ts +++ b/web/src/api/project.ts @@ -418,9 +418,9 @@ export const getStandardizedCols = ( name: string, tag: string, jwt: string | null, - schema: string + schema: string, ) => { - const url = `${API_BASE}/projects/${namespace}/${name}/standardize`; + const url = `${API_BASE}/projects/${namespace}/${name}/standardize?schema=${schema}&tag=${tag}`; return axios .get(url, { headers: { Authorization: `Bearer ${jwt}` } }) .then((res) => res.data); diff --git a/web/src/components/modals/standardize-metadata.tsx b/web/src/components/modals/standardize-metadata.tsx index 7fd19bd0..99c8ba5a 100644 --- a/web/src/components/modals/standardize-metadata.tsx +++ b/web/src/components/modals/standardize-metadata.tsx @@ -1,7 +1,7 @@ import { HotTable } from '@handsontable/react'; import Handsontable from 'handsontable'; import 'handsontable/dist/handsontable.full.css'; -import React, { FormEvent, useState } from 'react'; +import React, { FormEvent, useState, useEffect, useCallback } from 'react'; import { Modal } from 'react-bootstrap'; import ReactSelect from 'react-select'; @@ -11,6 +11,7 @@ import { useSampleTable } from '../../hooks/queries/useSampleTable'; import { useStandardize } from '../../hooks/queries/useStandardize'; import { arraysToSampleList, sampleListToArrays } from '../../utils/sample-table'; import { ProjectMetaEditForm } from '../forms/edit-project-meta'; +import { LoadingSpinner } from '../spinners/loading-spinner' type Props = { namespace: string; @@ -42,15 +43,12 @@ export const StandardizeMetadataModal = (props: Props) => { return obj; }, {}); - // console.log(tabDataRaw) - // console.log(tabData) - // console.log(sampleTableIndex) - - // const [selectedOption, setSelectedOption] = useState({ value: 'ENCODE', label: 'ENCODE' }); - const [selectedOption, setSelectedOption] = useState('ENCODE'); + const [selectedOption, setSelectedOption] = useState(''); + const [selectedValues, setSelectedValues] = useState({}); + const [whereDuplicates, setWhereDuplicates] = useState(null); const { - isLoading, + isFetching, isError, error, data, @@ -59,9 +57,6 @@ export const StandardizeMetadataModal = (props: Props) => { const standardizedData = data?.results; - console.log(selectedOption); - console.log(standardizedData); - const formatToPercentage = (value: number): string => { return `${(value * 100).toFixed(2)}%`; }; @@ -100,10 +95,7 @@ export const StandardizeMetadataModal = (props: Props) => { return result.length > 0 ? result : null; }; - const getDefaultSelections = () => { - if (!standardizedData) { - return {}; - } + const getDefaultSelections = (standardizedData) => { const defaultSelections = {}; Object.keys(standardizedData).forEach((key) => { defaultSelections[key] = key; @@ -128,24 +120,28 @@ export const StandardizeMetadataModal = (props: Props) => { return updatedTabDataRaw; }; - const prepareHandsontableData = (key, selectedValues, tabData) => { - const selectedValue = selectedValues[key] || ''; - const topValues = tabData[key]?.slice(0, 6).map((item) => [item]) || []; - const emptyRows = Array(Math.max(0, 6 - topValues.length)).fill(['']); - - return [[selectedValue], ...topValues, ...emptyRows]; - }; - const handleSubmit = async (event: FormEvent) => { event.preventDefault(); - console.log('Form submitted with:', selectedOption); - // Refetch data with new selectedOption await standardize(); }; - const [selectedValues, setSelectedValues] = useState(getDefaultSelections()); - const [whereDuplicates, setWhereDuplicates] = useState(null); + const prepareHandsontableData = useCallback((key: string) => { + const selectedValue = selectedValues[key] || ''; + const topValues = tabData[key]?.slice(0, 6).map((item) => [item]) || []; + const emptyRows = Array(Math.max(0, 6 - topValues.length)).fill(['']); + + return [[selectedValue], ...topValues, ...emptyRows]; + }, [selectedValues, tabData]); + + useEffect(() => { + if (data?.results) { + const defaultSelections = getDefaultSelections(data.results); + setWhereDuplicates(checkForDuplicates(defaultSelections)); + console.log('Setting default selections:', defaultSelections); + setSelectedValues(defaultSelections); + } + }, [data]); return ( @@ -157,7 +153,7 @@ export const StandardizeMetadataModal = (props: Props) => {

Use the metadata standardizer powered by BEDmess to bring consistency across metadata columns in all of - your projects. Compare predicted suggestions below (accuracy indicated in parenthesis) and choose whether + your projects. After choosing a standardizer schema below, compare predicted suggestions (accuracy indicated in parenthesis) and choose whether to keep or discard them. Column contents [previewed in brackets] are not changed by the standardizer. After accepting the changes, save your project for them to take effect.

@@ -206,7 +202,7 @@ export const StandardizeMetadataModal = (props: Props) => {
- {standardizedData ? ( + {standardizedData && !isFetching ? ( <>
@@ -223,13 +219,14 @@ export const StandardizeMetadataModal = (props: Props) => { {Object.keys(standardizedData).map((key, index) => (
+ {key === sampleTableIndex ?

SampleTableIndex must also be updated in project config!

: null}
{ td.style.color = 'red'; } } - }, - }, + } + } ]} licenseKey="non-commercial-and-evaluation" className="custom-handsontable" @@ -270,7 +267,7 @@ export const StandardizeMetadataModal = (props: Props) => { name={key} id={`${key}-original`} value={key} - defaultChecked={selectedValues[key] === key} // Check if the selected value is the same as the key + checked={selectedValues[key] === key} // Check if the selected value is the same as the key onChange={() => handleRadioChange(key, null)} />
*/} -
- - +
+ +
@@ -106,21 +106,14 @@ export const CreateSchemaForm = (props: Props) => { />
-

- * Namespace and Schema Name are required. -

-
- - - + {/* Add a dropdown here */}
= ({ onHide, defaultNamespace }) => { })}
) : null} +

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

+ +
+ +
- - - - ); }; From 1e1f8e63e0f7102e3294450bc0bba413d46ed65b Mon Sep 17 00:00:00 2001 From: Sam Park Date: Wed, 4 Sep 2024 00:37:36 -0400 Subject: [PATCH 046/101] ui polish --- .../project-cards/project-card-dropdown.tsx | 4 +- .../namespace/project-cards/project-card.tsx | 16 ++--- .../components/namespace/view-selector.tsx | 8 +-- .../schemas/schema-card-dropdown.tsx | 2 +- web/src/globals.css | 70 ++++++++++++++++--- web/src/pages/Namespace.tsx | 12 ++-- 6 files changed, 82 insertions(+), 30 deletions(-) diff --git a/web/src/components/namespace/project-cards/project-card-dropdown.tsx b/web/src/components/namespace/project-cards/project-card-dropdown.tsx index 4b2f3990..46831de2 100644 --- a/web/src/components/namespace/project-cards/project-card-dropdown.tsx +++ b/web/src/components/namespace/project-cards/project-card-dropdown.tsx @@ -33,7 +33,7 @@ export const ProjectCardDropdown: FC = (props) => {
) : ( From 04f61b935ef3001512abe10957a3f14013564851 Mon Sep 17 00:00:00 2001 From: Sam Park Date: Wed, 4 Sep 2024 02:08:41 -0400 Subject: [PATCH 051/101] class changes --- .../namespace/project-cards/project-card-dropdown.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/components/namespace/project-cards/project-card-dropdown.tsx b/web/src/components/namespace/project-cards/project-card-dropdown.tsx index 46831de2..05548197 100644 --- a/web/src/components/namespace/project-cards/project-card-dropdown.tsx +++ b/web/src/components/namespace/project-cards/project-card-dropdown.tsx @@ -32,7 +32,7 @@ export const ProjectCardDropdown: FC = (props) => { Date: Wed, 4 Sep 2024 10:49:54 -0400 Subject: [PATCH 052/101] edit metadata modal update --- pephub/routers/api/v1/project.py | 3 -- .../components/forms/edit-project-meta.tsx | 30 ++++++---------- .../components/modals/edit-meta-metadata.tsx | 34 +++++++++++++------ .../components/namespace/view-selector.tsx | 2 +- web/src/globals.css | 2 +- 5 files changed, 36 insertions(+), 35 deletions(-) diff --git a/pephub/routers/api/v1/project.py b/pephub/routers/api/v1/project.py index 34532e76..09d572ca 100644 --- a/pephub/routers/api/v1/project.py +++ b/pephub/routers/api/v1/project.py @@ -1171,11 +1171,8 @@ async def get_standardized_cols( prj = peppy.Project.from_dict(pep) model = AttrStandardizer(schema) - print(prj.sample_table) - print(pep) try: results = model.standardize(pep=prj) - print(results) except Exception: raise HTTPException( code=400, diff --git a/web/src/components/forms/edit-project-meta.tsx b/web/src/components/forms/edit-project-meta.tsx index 2dca7132..f3fa5ec1 100644 --- a/web/src/components/forms/edit-project-meta.tsx +++ b/web/src/components/forms/edit-project-meta.tsx @@ -68,7 +68,7 @@ export const ProjectMetaEditForm = (props: Props) => { return (
-
+
@@ -80,16 +80,14 @@ export const ProjectMetaEditForm = (props: Props) => { id="is-private-toggle" />
-
+
-
- +
+ { render={({ message }) => (message ?

{message}

: null)} />
-
- +
+
{ />
-
- +
+ { render={({ message }) => (message ?

{message}

: null)} />
-
- +
+ { id="metadata-save-btn" disabled={(!isDirty && isValid) || !!errors.name?.message || !!errors.tag?.message || isSubmitting} type="button" - className="btn btn-success me-1" + className="btn btn-success" > {isSubmitting ? 'Saving...' : 'Save'} diff --git a/web/src/components/modals/edit-meta-metadata.tsx b/web/src/components/modals/edit-meta-metadata.tsx index 2d7395cb..80d1b12a 100644 --- a/web/src/components/modals/edit-meta-metadata.tsx +++ b/web/src/components/modals/edit-meta-metadata.tsx @@ -35,18 +35,30 @@ export const EditMetaMetadataModal = (props: Props) => { return ( - - Edit Metadata - - +
+

Edit Metadata

+ +

+
+ + +
); diff --git a/web/src/components/namespace/view-selector.tsx b/web/src/components/namespace/view-selector.tsx index 4c20cbc9..6a09bec9 100644 --- a/web/src/components/namespace/view-selector.tsx +++ b/web/src/components/namespace/view-selector.tsx @@ -27,7 +27,7 @@ export const NamespaceViewSelector: FC = (props) => { }; return ( -
+
- +
- -

{message}

} /> + + {uploadFile ? ( -
+
diff --git a/web/src/components/modals/add-view-options.tsx b/web/src/components/modals/add-view-options.tsx index 52ffad47..900efc17 100644 --- a/web/src/components/modals/add-view-options.tsx +++ b/web/src/components/modals/add-view-options.tsx @@ -240,7 +240,8 @@ export const ViewOptionsModal = (props: Props) => { styles={{ control: (provided) => ({ ...provided, - borderRadius: '0.333333em', + borderRadius: '0.375em', + borderColor: '#dee2e6' }), }} className="top-z w-100 ms-auto" diff --git a/web/src/components/modals/standardize-metadata.tsx b/web/src/components/modals/standardize-metadata.tsx index d3c8998c..695d2c6a 100644 --- a/web/src/components/modals/standardize-metadata.tsx +++ b/web/src/components/modals/standardize-metadata.tsx @@ -25,7 +25,7 @@ type Props = { newSamples: any[][]; setNewSamples: (samples: any[][]) => void; resetStandardizedData: boolean; - setResetStandardizedData: (boolean) => void; + setResetStandardizedData: (value: boolean) => void; }; type TabDataRow = string[]; From c1da23caa1adf35bee79f35733391d2b77525e13 Mon Sep 17 00:00:00 2001 From: Khoroshevskyi Date: Fri, 6 Sep 2024 13:16:08 -0400 Subject: [PATCH 060/101] Added archive metadata endpoint --- pephub/const.py | 2 ++ pephub/routers/api/v1/namespace.py | 18 ++++++++++++++++++ requirements/requirements-all.txt | 2 +- 3 files changed, 21 insertions(+), 1 deletion(-) 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/routers/api/v1/namespace.py b/pephub/routers/api/v1/namespace.py index 408f2147..6f8069be 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, @@ -436,3 +439,18 @@ 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 diff --git a/requirements/requirements-all.txt b/requirements/requirements-all.txt index 378ad8a7..d82fe6c3 100644 --- a/requirements/requirements-all.txt +++ b/requirements/requirements-all.txt @@ -1,6 +1,6 @@ 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 From c816fc7f445f0dcc86adbb5cbca7d322e492134b Mon Sep 17 00:00:00 2001 From: Sam Park Date: Fri, 6 Sep 2024 13:47:48 -0400 Subject: [PATCH 061/101] schema api endpoints modal --- web/src/components/modals/delete-schema.tsx | 2 +- .../modals/schema-api-endpoints.tsx | 32 ++++++++ web/src/components/schemas/schema-header.tsx | 80 ++++++++++++++----- 3 files changed, 91 insertions(+), 23 deletions(-) create mode 100644 web/src/components/modals/schema-api-endpoints.tsx diff --git a/web/src/components/modals/delete-schema.tsx b/web/src/components/modals/delete-schema.tsx index cc635ea7..aed71fad 100644 --- a/web/src/components/modals/delete-schema.tsx +++ b/web/src/components/modals/delete-schema.tsx @@ -34,7 +34,7 @@ export const DeleteSchemaModal = (props: Props) => { }} > -

Delete PEP?

+

Delete Schema?

Are you sure you want to delete this schema? This action cannot be undone.

diff --git a/web/src/components/modals/schema-api-endpoints.tsx b/web/src/components/modals/schema-api-endpoints.tsx new file mode 100644 index 00000000..7d5ead11 --- /dev/null +++ b/web/src/components/modals/schema-api-endpoints.tsx @@ -0,0 +1,32 @@ +import { FC } from 'react'; +import { Modal } from 'react-bootstrap'; + +const API_HOST = import.meta.env.VITE_API_HOST || ''; + +interface Props { + namespace: string; + name: string; + show: boolean; + onHide: () => void; +} + +export const SchemaAPIEndpointsModal: FC = ({ namespace, name, show, onHide }) => { + return ( + + +

API Endpoints

+
+ +

+ GET + Schema: + + + /api/v1/schemas/{namespace}/{name} + + +

+
+
+ ); +}; diff --git a/web/src/components/schemas/schema-header.tsx b/web/src/components/schemas/schema-header.tsx index 78fca214..66fcb86b 100644 --- a/web/src/components/schemas/schema-header.tsx +++ b/web/src/components/schemas/schema-header.tsx @@ -1,5 +1,5 @@ import { Fragment, useRef, useState } from 'react'; -import { Breadcrumb } from 'react-bootstrap'; +import { Breadcrumb, Dropdown } from 'react-bootstrap'; import { useParams } from 'react-router-dom'; import YAML from 'yaml'; @@ -8,6 +8,7 @@ import { useEditSchemaMutation } from '../../hooks/mutations/useEditSchemaMutati import { useSchema } from '../../hooks/queries/useSchema'; import { copyToClipboard } from '../../utils/etc'; import { DeleteSchemaModal } from '../modals/delete-schema'; +import { SchemaAPIEndpointsModal } from '../modals/schema-api-endpoints'; const API_HOST = import.meta.env.VITE_API_HOST || ''; @@ -27,6 +28,8 @@ export const SchemaHeader = (props: Props) => { const [copied, setCopied] = useState(false); const [editingDescription, setEditingDescription] = useState(false); const [showSchemaDeleteModal, setShowSchemaDeleteModal] = useState(false); + const [showSchemaAPIModal, setShowSchemaAPIModal] = useState(false); + const [newDescription, setNewDescription] = useState(props.description); const { data: schemaData } = useSchema(namespace, schema); @@ -43,14 +46,18 @@ export const SchemaHeader = (props: Props) => { {schema}
{editingDescription ? ( @@ -132,6 +160,14 @@ export const SchemaHeader = (props: Props) => { onHide={() => setShowSchemaDeleteModal(false)} redirect={`/${namespace}?view=schemas`} /> + setShowSchemaAPIModal(false)} + redirect={`/${namespace}?view=schemas`} + />
+ ); }; From c68f807a9f51a980ac0b579f487e492eb53cfa77 Mon Sep 17 00:00:00 2001 From: Sam Park Date: Fri, 6 Sep 2024 13:49:20 -0400 Subject: [PATCH 062/101] remove redirect for schema api modal --- web/src/components/schemas/schema-header.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/web/src/components/schemas/schema-header.tsx b/web/src/components/schemas/schema-header.tsx index 66fcb86b..5a070370 100644 --- a/web/src/components/schemas/schema-header.tsx +++ b/web/src/components/schemas/schema-header.tsx @@ -165,7 +165,6 @@ export const SchemaHeader = (props: Props) => { name={schema!} show={showSchemaAPIModal} onHide={() => setShowSchemaAPIModal(false)} - redirect={`/${namespace}?view=schemas`} />
From 8d241c4dd2cce65faca48034e8c6a9e98f6dd601 Mon Sep 17 00:00:00 2001 From: Khoroshevskyi Date: Mon, 9 Sep 2024 13:57:44 -0400 Subject: [PATCH 063/101] Added bedbase schema for bedboss --- web/src/components/layout/nav/nav-desktop.tsx | 18 ++- .../modals/standardize-metadata.tsx | 136 ++++++++++++------ 2 files changed, 105 insertions(+), 49 deletions(-) diff --git a/web/src/components/layout/nav/nav-desktop.tsx b/web/src/components/layout/nav/nav-desktop.tsx index 295e2b06..e62ff501 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 (
    -
  • +
  • {user ? ( {
  • - Browse + Schemas
  • @@ -231,11 +231,21 @@ export const NavDesktop = () => { )} - + {/**/} Give Feedback - + {/**/} Report a Bug diff --git a/web/src/components/modals/standardize-metadata.tsx b/web/src/components/modals/standardize-metadata.tsx index 695d2c6a..4e35e07e 100644 --- a/web/src/components/modals/standardize-metadata.tsx +++ b/web/src/components/modals/standardize-metadata.tsx @@ -1,7 +1,7 @@ import { HotTable } from '@handsontable/react'; import Handsontable from 'handsontable'; import 'handsontable/dist/handsontable.full.css'; -import React, { FormEvent, useState, useEffect, useCallback, useMemo } from 'react'; +import React, { FormEvent, useCallback, useEffect, useMemo, useState } from 'react'; import { Modal } from 'react-bootstrap'; import ReactSelect from 'react-select'; @@ -9,10 +9,10 @@ import { useEditProjectMetaMutation } from '../../hooks/mutations/useEditProject import { useProjectAnnotation } from '../../hooks/queries/useProjectAnnotation'; import { useSampleTable } from '../../hooks/queries/useSampleTable'; import { useStandardize } from '../../hooks/queries/useStandardize'; +import { formatToPercentage } from '../../utils/etc'; import { arraysToSampleList, sampleListToArrays } from '../../utils/sample-table'; import { ProjectMetaEditForm } from '../forms/edit-project-meta'; -import { LoadingSpinner } from '../spinners/loading-spinner' -import { formatToPercentage } from '../../utils/etc' +import { LoadingSpinner } from '../spinners/loading-spinner'; type Props = { namespace: string; @@ -36,7 +36,19 @@ type AvailableSchemas = 'ENCODE' | 'FAIRTRACKS'; type StandardizedData = Record>; export const StandardizeMetadataModal = (props: Props) => { - const { namespace, project, tag, show, onHide, sampleTable, sampleTableIndex, newSamples, setNewSamples, resetStandardizedData, setResetStandardizedData } = props; + const { + namespace, + project, + tag, + show, + onHide, + sampleTable, + sampleTableIndex, + newSamples, + setNewSamples, + resetStandardizedData, + setResetStandardizedData, + } = props; const tabDataRaw = newSamples; const tabData = tabDataRaw[0] @@ -121,9 +133,9 @@ export const StandardizeMetadataModal = (props: Props) => { Object.entries(selectedValues).forEach(([key, value]) => { let columnIndex = updatedTabDataRaw[0].indexOf(key); if (columnIndex === -1) { - const originalValue = getOriginalColValues(key) + const originalValue = getOriginalColValues(key); if (originalValue) { - columnIndex = updatedTabDataRaw[0].indexOf(originalValue) + columnIndex = updatedTabDataRaw[0].indexOf(originalValue); } } if (columnIndex !== -1) { @@ -140,17 +152,23 @@ export const StandardizeMetadataModal = (props: Props) => { await standardize(); }; - const prepareHandsontableData = useCallback((key: string) => { - const selectedValue = selectedValues[key] || ''; - const originalValue = getOriginalColValues(key) - let topValues: string[][] = [] - if (originalValue) { - topValues = tabData[key]?.slice(0, 6).map((item) => [item]) || tabData[originalValue]?.slice(0, 6).map((item) => [item]) || []; - } - const emptyRows = Array(Math.max(0, 6 - topValues.length)).fill(['']); + const prepareHandsontableData = useCallback( + (key: string) => { + const selectedValue = selectedValues[key] || ''; + const originalValue = getOriginalColValues(key); + let topValues: string[][] = []; + if (originalValue) { + topValues = + tabData[key]?.slice(0, 6).map((item) => [item]) || + tabData[originalValue]?.slice(0, 6).map((item) => [item]) || + []; + } + const emptyRows = Array(Math.max(0, 6 - topValues.length)).fill(['']); - return [[selectedValue], ...topValues, ...emptyRows]; - }, [selectedValues, tabData]); + return [[selectedValue], ...topValues, ...emptyRows]; + }, + [selectedValues, tabData], + ); useEffect(() => { if (standardizedData) { @@ -168,26 +186,25 @@ export const StandardizeMetadataModal = (props: Props) => { } }, [resetStandardizedData]); - return ( -
    +

    Metadata Standardizer

    -

    - Use the metadata standardizer powered by BEDmess to bring consistency across metadata columns in all of - your projects. After choosing a standardizer schema below, compare predicted suggestions (confidence indicated in parenthesis) and choose whether - to keep or discard them. Column contents are not modified by the standardizer. - After accepting the changes, save your project for them to take effect. +

    + Use the metadata standardizer powered by BEDmess to bring consistency across metadata columns in all of your + projects. After choosing a standardizer schema below, compare predicted suggestions (confidence indicated in + parenthesis) and choose whether to keep or discard them. Column contents are not modified by the + standardizer. After accepting the changes, save your project for them to take effect.

    @@ -211,6 +228,8 @@ export const StandardizeMetadataModal = (props: Props) => { { value: 'ENCODE', label: 'ENCODE' }, // @ts-ignore { value: 'FAIRTRACKS', label: 'Fairtracks' }, + // @ts-ignore + { value: 'BEDBASE', label: 'BEDBASE' }, ]} defaultValue={selectedOption} value={selectedOption} @@ -223,7 +242,11 @@ export const StandardizeMetadataModal = (props: Props) => { />
    -
    @@ -238,10 +261,10 @@ export const StandardizeMetadataModal = (props: Props) => {
    -

    Original Column

    +

    Original Column

    -

    Predicted Column Header

    +

    Predicted Column Header

    @@ -249,13 +272,29 @@ export const StandardizeMetadataModal = (props: Props) => { {Object.keys(standardizedData).map((key, index) => (
    - {key === sampleTableIndex ?

    SampleTableIndex must also be updated in project config!

    : null} + {key === sampleTableIndex ? ( +

    + SampleTableIndex must also be updated in project config! +

    + ) : null}
    -
    +
    { { data: 0, type: typeof tabData[key] === 'number' ? 'numeric' : 'text', - renderer: function( + renderer: function ( instance: Handsontable.Core, td: HTMLTableCellElement, row: number, col: number, prop: string | number, value: any, - cellProperties: Handsontable.CellProperties + cellProperties: Handsontable.CellProperties, ) { - Handsontable.renderers.TextRenderer.apply(this, [instance, td, row, col, prop, value, cellProperties]); + Handsontable.renderers.TextRenderer.apply(this, [ + instance, + td, + row, + col, + prop, + value, + cellProperties, + ]); if (row === 0) { td.style.fontWeight = 'bold'; if (whereDuplicates?.includes(index)) { td.style.color = 'red'; } } - } - } + }, + }, ]} licenseKey="non-commercial-and-evaluation" className="custom-handsontable" @@ -310,7 +357,7 @@ export const StandardizeMetadataModal = (props: Props) => { className="btn btn-outline-secondary selected-outline shadow-sm bg-white" htmlFor={`${key}-original`} > - {key} (original value) + {key} (original value) {Object.entries(standardizedData[key]).map(([subKey, value], index, array) => ( @@ -342,19 +389,18 @@ export const StandardizeMetadataModal = (props: Props) => { ))} - ) : isFetching ? - -
    + ) : isFetching ? ( +
    - : - null - } + ) : null}
    {whereDuplicates !== null && ( -
    Warning: ensure no duplicate column names have been selected.
    +
    + Warning: ensure no duplicate column names have been selected. +
    )} - - - - - ); -}; diff --git a/web/src/components/namespace/archive/download-geo-count.tsx b/web/src/components/namespace/archive/download-geo-count.tsx new file mode 100644 index 00000000..32750e3d --- /dev/null +++ b/web/src/components/namespace/archive/download-geo-count.tsx @@ -0,0 +1,109 @@ +import { FC, useState, useMemo } from 'react'; + +import { useNamespaceArchive } from '../../../hooks/queries/useNamespaceArchive' + +interface Props { + namespace: string | undefined; +} + +export const DownloadGeoCount = (props: Props) => { + const namespace = props.namespace; + + const { + isFetching, + isError, + error, + data, + } = useNamespaceArchive(namespace); + + // let testData = { + // count: 12, + // results: [ + // { + // identifier: 8, + // namespace: "geo", + // file_path: "https://cloud2.databio.org/pephub/geo/geo_2024_09_06.tar", + // creation_date: "2024-09-06T17:00:18.3913772", + // number_of_projects: 228300 + // }, + // { + // identifier: 9, + // namespace: "geo", + // file_path: "https://cloud2.databio.org/pephub/geo/geo_2024_09_07.tar", + // creation_date: "2024-09-07T09:30:45.1234567", + // number_of_projects: 33000 + // }, + // { + // identifier: 1, + // namespace: "geo", + // file_path: "https://cloud2.databio.org/pephub/geo/geo_2024_09_06.tar", + // creation_date: "2024-09-06T17:00:18.3913772", + // number_of_projects: 228300 + // }, + // { + // identifier: 53, + // namespace: "geo", + // file_path: "https://cloud2.databio.org/pephub/geo/geo_2024_09_07.tar", + // creation_date: "2024-09-07T09:30:45.1234567", + // number_of_projects: 33000 + // },{ + // identifier: 74, + // namespace: "geo", + // file_path: "https://cloud2.databio.org/pephub/geo/geo_2024_09_06.tar", + // creation_date: "2024-09-06T17:00:18.3913772", + // number_of_projects: 228300 + // }, + // { + // identifier: 45, + // namespace: "geo", + // file_path: "https://cloud2.databio.org/pephub/geo/geo_2024_09_07.tar", + // creation_date: "2024-09-07T09:30:45.1234567", + // number_of_projects: 33000 + // },{ + // identifier: 12, + // namespace: "geo", + // file_path: "https://cloud2.databio.org/pephub/geo/geo_2024_09_06.tar", + // creation_date: "2024-09-06T17:00:18.3913772", + // number_of_projects: 228300 + // }, + // { + // identifier: 60, + // namespace: "geo", + // file_path: "https://cloud2.databio.org/pephub/geo/geo_2024_09_07.tar", + // creation_date: "2024-09-07T09:30:45.1234567", + // number_of_projects: 33000 + // }, + // { + // identifier: 7, + // namespace: "geo", + // file_path: "https://cloud2.databio.org/pephub/geo/geo_2024_09_06.tar", + // creation_date: "2024-09-06T17:00:18.3913772", + // number_of_projects: 228300 + // }, + // { + // identifier: 14, + // namespace: "geo", + // file_path: "https://cloud2.databio.org/pephub/geo/geo_2024_09_07.tar", + // creation_date: "2024-09-07T09:30:45.1234567", + // number_of_projects: 33000 + // },{ + // identifier: 5, + // namespace: "geo", + // file_path: "https://cloud2.databio.org/pephub/geo/geo_2024_09_06.tar", + // creation_date: "2024-09-06T17:00:18.3913772", + // number_of_projects: 228300 + // }, + // { + // identifier: 3, + // namespace: "geo", + // file_path: "https://cloud2.databio.org/pephub/geo/geo_2024_09_07.tar", + // creation_date: "2024-09-07T09:30:45.1234567", + // number_of_projects: 33000 + // } + // ] + // }; + + return ( + {data?.count} + ); +}; diff --git a/web/src/components/namespace/archive/download-geo.tsx b/web/src/components/namespace/archive/download-geo.tsx new file mode 100644 index 00000000..ea87b02f --- /dev/null +++ b/web/src/components/namespace/archive/download-geo.tsx @@ -0,0 +1,135 @@ +import { FC, useState, useMemo } from 'react'; + +import { useNamespaceArchive } from '../../../hooks/queries/useNamespaceArchive' +import { NamespaceArchiveTable } from '../archive/namespace-archive-table' +import { ArchiveItem, ArchiveResponse } from '../../../api/namespace' + +interface Props { + namespace: string | undefined; +} + +export const DownloadGeo = (props: Props) => { + const namespace = props.namespace; + + const { + isFetching, + isError, + error, + data, + } = useNamespaceArchive(namespace); + + // console.log(data) + + // let testData = { + // count: 12, + // results: [ + // { + // identifier: 8, + // namespace: "geo", + // file_path: "https://cloud2.databio.org/pephub/geo/geo_2024_09_06.tar", + // creation_date: "2024-09-06T17:00:18.3913772", + // number_of_projects: 228300, + // file_size: 626581571 + // }, + // { + // identifier: 9, + // namespace: "geo", + // file_path: "https://cloud2.databio.org/pephub/geo/geo_2024_09_07.tar", + // creation_date: "2024-09-07T09:30:45.1234567", + // number_of_projects: 33000, + // file_size: 626581571 + // }, + // { + // identifier: 1, + // namespace: "geo", + // file_path: "https://cloud2.databio.org/pephub/geo/geo_2024_09_06.tar", + // creation_date: "2024-09-06T17:00:18.3913772", + // number_of_projects: 228300, + // file_size: 626581571 + // }, + // { + // identifier: 53, + // namespace: "geo", + // file_path: "https://cloud2.databio.org/pephub/geo/geo_2024_09_07.tar", + // creation_date: "2024-09-07T09:30:45.1234567", + // number_of_projects: 33000, + // file_size: 626581571 + // },{ + // identifier: 74, + // namespace: "geo", + // file_path: "https://cloud2.databio.org/pephub/geo/geo_2024_09_06.tar", + // creation_date: "2024-09-06T17:00:18.3913772", + // number_of_projects: 228300, + // file_size: 626581571 + // }, + // { + // identifier: 45, + // namespace: "geo", + // file_path: "https://cloud2.databio.org/pephub/geo/geo_2024_09_07.tar", + // creation_date: "2024-09-07T09:30:45.1234567", + // number_of_projects: 33000, + // file_size: 626581571 + // },{ + // identifier: 12, + // namespace: "geo", + // file_path: "https://cloud2.databio.org/pephub/geo/geo_2024_09_06.tar", + // creation_date: "2024-09-06T17:00:18.3913772", + // number_of_projects: 228300, + // file_size: 626581571 + // }, + // { + // identifier: 60, + // namespace: "geo", + // file_path: "https://cloud2.databio.org/pephub/geo/geo_2024_09_07.tar", + // creation_date: "2024-09-07T09:30:45.1234567", + // number_of_projects: 33000, + // file_size: 626581571 + // }, + // { + // identifier: 7, + // namespace: "geo", + // file_path: "https://cloud2.databio.org/pephub/geo/geo_2024_09_06.tar", + // creation_date: "2024-09-06T17:00:18.3913772", + // number_of_projects: 228300, + // file_size: 626581571 + // }, + // { + // identifier: 14, + // namespace: "geo", + // file_path: "https://cloud2.databio.org/pephub/geo/geo_2024_09_07.tar", + // creation_date: "2024-09-07T09:30:45.1234567", + // number_of_projects: 33000, + // file_size: 626581571 + // },{ + // identifier: 5, + // namespace: "geo", + // file_path: "https://cloud2.databio.org/pephub/geo/geo_2024_09_06.tar", + // creation_date: "2024-09-06T17:00:18.3913772", + // number_of_projects: 228300, + // file_size: 626581571 + // }, + // { + // identifier: 3, + // namespace: "geo", + // file_path: "https://cloud2.databio.org/pephub/geo/geo_2024_09_07.tar", + // creation_date: "2024-09-07T09:30:45.1234567", + // number_of_projects: 33000, + // file_size: 626581571 + // } + // ] + // }; + + // console.log(testData) + + return ( +
    +

    Download All GEO Metadata

    +

    + Download all available PEPs for the Gene Expresion Omnibus (GEO). + All GEO PEPs are archived quarterly into a single tar file. + Each archive is slightly over 1 gb in size. +

    + {data ? : null} +
    + ); +}; diff --git a/web/src/components/namespace/archive/namespace-archive-table.tsx b/web/src/components/namespace/archive/namespace-archive-table.tsx new file mode 100644 index 00000000..4c0f21ab --- /dev/null +++ b/web/src/components/namespace/archive/namespace-archive-table.tsx @@ -0,0 +1,134 @@ +import { FC, useState, useMemo } from 'react'; + +import { ArchiveItem, ArchiveResponse } from '../../../api/namespace' + +interface Props { + data: ArchiveResponse; +} + +const ITEMS_PER_PAGE = 10; + +const formatDate = (dateString: string): string => { + const date = new Date(dateString); + return date.toLocaleString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: 'numeric', + minute: '2-digit', + hour12: true + }); +}; + +export const NamespaceArchiveTable: React.FC = ({ data }) => { + const [currentPage, setCurrentPage] = useState(1); + const [sortConfig, setSortConfig] = useState<{ key: keyof ArchiveItem; direction: 'ascending' | 'descending' }>({ + key: 'creation_date', + direction: 'descending' + }); + + const sortedData = useMemo(() => { + let sortableItems = [...data.results]; + if (sortConfig !== null) { + sortableItems.sort((a, b) => { + if (a[sortConfig.key] < b[sortConfig.key]) { + return sortConfig.direction === 'ascending' ? -1 : 1; + } + if (a[sortConfig.key] > b[sortConfig.key]) { + return sortConfig.direction === 'ascending' ? 1 : -1; + } + return 0; + }); + } + return sortableItems; + }, [data.results, sortConfig]); + + const currentTableData = useMemo(() => { + const firstPageIndex = (currentPage - 1) * ITEMS_PER_PAGE; + const lastPageIndex = firstPageIndex + ITEMS_PER_PAGE; + return sortedData.slice(firstPageIndex, lastPageIndex); + }, [currentPage, sortedData]); + + const totalPages = Math.ceil(sortedData.length / ITEMS_PER_PAGE); + + const requestSort = (key: keyof ArchiveItem) => { + let direction: 'ascending' | 'descending' = 'ascending'; + if (sortConfig && sortConfig.key === key && sortConfig.direction === 'ascending') { + direction = 'descending'; + } + setSortConfig({ key, direction }); + setCurrentPage(1); // Reset to first page when sorting + }; + + const getSortDirection = (name: keyof ArchiveItem) => { + if (!sortConfig || sortConfig.key !== name) { + return ; + } + return sortConfig.direction === 'ascending' ? : ; + }; + + return ( +
    + +
    + + + + + + + + + + + + {currentTableData.map((result) => ( + + + + + + + + ))} + +
    requestSort('namespace')} style={{ cursor: 'pointer' }}> + Namespace {getSortDirection('namespace')} + requestSort('number_of_projects')} style={{ cursor: 'pointer' }}> + Number of Projects {getSortDirection('number_of_projects')} + requestSort('creation_date')} style={{ cursor: 'pointer' }}> + Archive Date {getSortDirection('creation_date')} + requestSort('file_size')} style={{ cursor: 'pointer' }}> + File Size {getSortDirection('file_size')} + Download Link
    {result.namespace}{result.number_of_projects}{formatDate(result.creation_date)}{(result.file_size / 1024 / 1024 / 1024).toFixed(2)} gb + + {result.file_path.split('/').pop()} + +
    +
    + {totalPages > 1 && ( + + )} +
    + ); +}; diff --git a/web/src/components/namespace/view-selector.tsx b/web/src/components/namespace/view-selector.tsx index 6a09bec9..bd34a8cd 100644 --- a/web/src/components/namespace/view-selector.tsx +++ b/web/src/components/namespace/view-selector.tsx @@ -2,8 +2,9 @@ import { FC } from 'react'; import { OverlayTrigger, Tooltip } from 'react-bootstrap'; import Nav from 'react-bootstrap/Nav'; import { NavLink, useSearchParams } from 'react-router-dom'; +import { DownloadGeoCount } from '../namespace/archive/download-geo-count'; -type View = 'peps' | 'pops' | 'schemas' | 'stars'; +type View = 'peps' | 'pops' | 'schemas' | 'stars' | 'archive'; type Props = { view: View; @@ -13,6 +14,8 @@ type Props = { numSchemas: number; setView: (view: View) => void; enableStars?: boolean; + isGEO?: boolean; + namespace?: string; }; export const NamespaceViewSelector: FC = (props) => { @@ -91,6 +94,23 @@ export const NamespaceViewSelector: FC = (props) => { )} + {props.isGEO && ( + + + + Archive + {props.view === 'archive' ? ( + + + + ) : ( + + + + )} + + + )}
    ); diff --git a/web/src/globals.css b/web/src/globals.css index dc5e0408..f7c01ba6 100644 --- a/web/src/globals.css +++ b/web/src/globals.css @@ -47,6 +47,7 @@ body { .btn-primary-emphasis { background-color: #052c65; + color: white; } /* tailwind pulse */ @@ -839,6 +840,17 @@ body { .dark-button { background-color: #052c65 !important; color: white !important; + border-radius: .375em; +} + +.page-item:not(.disabled) .page-link { + color: #052c65cf; +} + +.page-item.active .page-link { + color: white !important; + border-color: #052c65cf !important; + background-color: #052c65cf !important; } .forked-link { diff --git a/web/src/hooks/queries/useNamespaceArchive.ts b/web/src/hooks/queries/useNamespaceArchive.ts new file mode 100644 index 00000000..f9d123e9 --- /dev/null +++ b/web/src/hooks/queries/useNamespaceArchive.ts @@ -0,0 +1,10 @@ +import { useQuery } from '@tanstack/react-query'; + +import { getNamespaceArchive } from '../../api/namespace'; + +export const useNamespaceArchive = (namespace: string | undefined) => { + return useQuery({ + queryKey: [namespace], + queryFn: () => getNamespaceArchive(namespace || ''), + }); +}; diff --git a/web/src/pages/Namespace.tsx b/web/src/pages/Namespace.tsx index 54c2bbc7..b645a60e 100644 --- a/web/src/pages/Namespace.tsx +++ b/web/src/pages/Namespace.tsx @@ -7,7 +7,7 @@ import { PageLayout } from '../components/layout/page-layout'; import { Pagination } from '../components/layout/pagination'; import { AddPEPModal } from '../components/modals/add-pep'; import { DeveloperSettingsModal } from '../components/modals/developer-settings-modal'; -import { DownloadGeo } from '../components/modals/download-geo'; +import { DownloadGeo } from '../components/namespace/archive/download-geo'; import { NamespaceAPIEndpointsModal } from '../components/modals/namespace-api-endpoints'; import { NamespaceBadge } from '../components/namespace/namespace-badge'; import { NamespacePagePlaceholder } from '../components/namespace/namespace-page-placeholder'; @@ -25,7 +25,7 @@ import { useNamespaceStars } from '../hooks/queries/useNamespaceStars'; import { useDebounce } from '../hooks/useDebounce'; import { numberWithCommas } from '../utils/etc'; -type View = 'peps' | 'pops' | 'schemas' | 'stars'; +type View = 'peps' | 'pops' | 'schemas' | 'stars' | 'archive'; export const NamespacePage = () => { const [searchParams] = useSearchParams(); @@ -147,7 +147,7 @@ export const NamespacePage = () => { API - {namespace === 'geo' && ( + {namespace === 'geos' && (
    @@ -309,7 +311,7 @@ export const NamespacePage = () => { ) : null}
    - ) : ( + ) : view === 'stars' ? ( // render stars in namespace {stars?.length === 0 ? ( @@ -332,14 +334,21 @@ export const NamespacePage = () => {
    )} - )} + ) : (view === 'archive' && namespace === 'geo') ? ( + +
    + +
    +
    + ) : null + } setShowAddPEPModal(false)} /> setShowEndpointsModal(false)} /> - setShowGeoDownloadModal(false)} /> + setShowSettingsModal(false)} /> ); From 8a8308b6617b8188cb54fdadc265325766e254ff Mon Sep 17 00:00:00 2001 From: Sam Park Date: Mon, 9 Sep 2024 15:02:46 -0400 Subject: [PATCH 065/101] bedms --- web/src/components/modals/standardize-metadata.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/modals/standardize-metadata.tsx b/web/src/components/modals/standardize-metadata.tsx index 4e35e07e..2047e5ed 100644 --- a/web/src/components/modals/standardize-metadata.tsx +++ b/web/src/components/modals/standardize-metadata.tsx @@ -201,7 +201,7 @@ export const StandardizeMetadataModal = (props: Props) => {

    - Use the metadata standardizer powered by BEDmess to bring consistency across metadata columns in all of your + Use the metadata standardizer powered by BEDms to bring consistency across metadata columns in all of your projects. After choosing a standardizer schema below, compare predicted suggestions (confidence indicated in parenthesis) and choose whether to keep or discard them. Column contents are not modified by the standardizer. After accepting the changes, save your project for them to take effect. From d9b79e477744c9059bfe5eaf37a3cd40f04177be Mon Sep 17 00:00:00 2001 From: Sam Park Date: Mon, 9 Sep 2024 15:16:19 -0400 Subject: [PATCH 066/101] add null archive case --- web/src/components/namespace/archive/download-geo-count.tsx | 2 +- web/src/components/namespace/archive/download-geo.tsx | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/web/src/components/namespace/archive/download-geo-count.tsx b/web/src/components/namespace/archive/download-geo-count.tsx index 32750e3d..c0fc4779 100644 --- a/web/src/components/namespace/archive/download-geo-count.tsx +++ b/web/src/components/namespace/archive/download-geo-count.tsx @@ -104,6 +104,6 @@ export const DownloadGeoCount = (props: Props) => { // }; return ( - {data?.count} + {data ? data.count : 0} ); }; diff --git a/web/src/components/namespace/archive/download-geo.tsx b/web/src/components/namespace/archive/download-geo.tsx index ea87b02f..e37d6fb1 100644 --- a/web/src/components/namespace/archive/download-geo.tsx +++ b/web/src/components/namespace/archive/download-geo.tsx @@ -129,7 +129,9 @@ export const DownloadGeo = (props: Props) => { All GEO PEPs are archived quarterly into a single tar file. Each archive is slightly over 1 gb in size.

    - {data ? : null} + {data ? : +

    No archives currently exist for this namespace.

    + }
    ); }; From a430ebdf62fa1b39ddef6cf68de49d6b0621501c Mon Sep 17 00:00:00 2001 From: Sam Park Date: Mon, 9 Sep 2024 18:58:20 -0400 Subject: [PATCH 067/101] fix star buttons --- .../namespace/archive/download-geo-count.tsx | 87 --------------- .../namespace/archive/download-geo.tsx | 105 +----------------- .../project-cards/project-card-dropdown.tsx | 8 +- .../project/project-info-footer.tsx | 14 +-- .../project/project-page-header-bar.tsx | 55 ++------- web/src/components/project/project-stars.tsx | 72 ++++++++++++ web/src/globals.css | 4 +- 7 files changed, 95 insertions(+), 250 deletions(-) create mode 100644 web/src/components/project/project-stars.tsx diff --git a/web/src/components/namespace/archive/download-geo-count.tsx b/web/src/components/namespace/archive/download-geo-count.tsx index c0fc4779..857f204d 100644 --- a/web/src/components/namespace/archive/download-geo-count.tsx +++ b/web/src/components/namespace/archive/download-geo-count.tsx @@ -16,93 +16,6 @@ export const DownloadGeoCount = (props: Props) => { data, } = useNamespaceArchive(namespace); - // let testData = { - // count: 12, - // results: [ - // { - // identifier: 8, - // namespace: "geo", - // file_path: "https://cloud2.databio.org/pephub/geo/geo_2024_09_06.tar", - // creation_date: "2024-09-06T17:00:18.3913772", - // number_of_projects: 228300 - // }, - // { - // identifier: 9, - // namespace: "geo", - // file_path: "https://cloud2.databio.org/pephub/geo/geo_2024_09_07.tar", - // creation_date: "2024-09-07T09:30:45.1234567", - // number_of_projects: 33000 - // }, - // { - // identifier: 1, - // namespace: "geo", - // file_path: "https://cloud2.databio.org/pephub/geo/geo_2024_09_06.tar", - // creation_date: "2024-09-06T17:00:18.3913772", - // number_of_projects: 228300 - // }, - // { - // identifier: 53, - // namespace: "geo", - // file_path: "https://cloud2.databio.org/pephub/geo/geo_2024_09_07.tar", - // creation_date: "2024-09-07T09:30:45.1234567", - // number_of_projects: 33000 - // },{ - // identifier: 74, - // namespace: "geo", - // file_path: "https://cloud2.databio.org/pephub/geo/geo_2024_09_06.tar", - // creation_date: "2024-09-06T17:00:18.3913772", - // number_of_projects: 228300 - // }, - // { - // identifier: 45, - // namespace: "geo", - // file_path: "https://cloud2.databio.org/pephub/geo/geo_2024_09_07.tar", - // creation_date: "2024-09-07T09:30:45.1234567", - // number_of_projects: 33000 - // },{ - // identifier: 12, - // namespace: "geo", - // file_path: "https://cloud2.databio.org/pephub/geo/geo_2024_09_06.tar", - // creation_date: "2024-09-06T17:00:18.3913772", - // number_of_projects: 228300 - // }, - // { - // identifier: 60, - // namespace: "geo", - // file_path: "https://cloud2.databio.org/pephub/geo/geo_2024_09_07.tar", - // creation_date: "2024-09-07T09:30:45.1234567", - // number_of_projects: 33000 - // }, - // { - // identifier: 7, - // namespace: "geo", - // file_path: "https://cloud2.databio.org/pephub/geo/geo_2024_09_06.tar", - // creation_date: "2024-09-06T17:00:18.3913772", - // number_of_projects: 228300 - // }, - // { - // identifier: 14, - // namespace: "geo", - // file_path: "https://cloud2.databio.org/pephub/geo/geo_2024_09_07.tar", - // creation_date: "2024-09-07T09:30:45.1234567", - // number_of_projects: 33000 - // },{ - // identifier: 5, - // namespace: "geo", - // file_path: "https://cloud2.databio.org/pephub/geo/geo_2024_09_06.tar", - // creation_date: "2024-09-06T17:00:18.3913772", - // number_of_projects: 228300 - // }, - // { - // identifier: 3, - // namespace: "geo", - // file_path: "https://cloud2.databio.org/pephub/geo/geo_2024_09_07.tar", - // creation_date: "2024-09-07T09:30:45.1234567", - // number_of_projects: 33000 - // } - // ] - // }; - return ( {data ? data.count : 0} ); diff --git a/web/src/components/namespace/archive/download-geo.tsx b/web/src/components/namespace/archive/download-geo.tsx index e37d6fb1..4b1c9ac0 100644 --- a/web/src/components/namespace/archive/download-geo.tsx +++ b/web/src/components/namespace/archive/download-geo.tsx @@ -18,109 +18,6 @@ export const DownloadGeo = (props: Props) => { data, } = useNamespaceArchive(namespace); - // console.log(data) - - // let testData = { - // count: 12, - // results: [ - // { - // identifier: 8, - // namespace: "geo", - // file_path: "https://cloud2.databio.org/pephub/geo/geo_2024_09_06.tar", - // creation_date: "2024-09-06T17:00:18.3913772", - // number_of_projects: 228300, - // file_size: 626581571 - // }, - // { - // identifier: 9, - // namespace: "geo", - // file_path: "https://cloud2.databio.org/pephub/geo/geo_2024_09_07.tar", - // creation_date: "2024-09-07T09:30:45.1234567", - // number_of_projects: 33000, - // file_size: 626581571 - // }, - // { - // identifier: 1, - // namespace: "geo", - // file_path: "https://cloud2.databio.org/pephub/geo/geo_2024_09_06.tar", - // creation_date: "2024-09-06T17:00:18.3913772", - // number_of_projects: 228300, - // file_size: 626581571 - // }, - // { - // identifier: 53, - // namespace: "geo", - // file_path: "https://cloud2.databio.org/pephub/geo/geo_2024_09_07.tar", - // creation_date: "2024-09-07T09:30:45.1234567", - // number_of_projects: 33000, - // file_size: 626581571 - // },{ - // identifier: 74, - // namespace: "geo", - // file_path: "https://cloud2.databio.org/pephub/geo/geo_2024_09_06.tar", - // creation_date: "2024-09-06T17:00:18.3913772", - // number_of_projects: 228300, - // file_size: 626581571 - // }, - // { - // identifier: 45, - // namespace: "geo", - // file_path: "https://cloud2.databio.org/pephub/geo/geo_2024_09_07.tar", - // creation_date: "2024-09-07T09:30:45.1234567", - // number_of_projects: 33000, - // file_size: 626581571 - // },{ - // identifier: 12, - // namespace: "geo", - // file_path: "https://cloud2.databio.org/pephub/geo/geo_2024_09_06.tar", - // creation_date: "2024-09-06T17:00:18.3913772", - // number_of_projects: 228300, - // file_size: 626581571 - // }, - // { - // identifier: 60, - // namespace: "geo", - // file_path: "https://cloud2.databio.org/pephub/geo/geo_2024_09_07.tar", - // creation_date: "2024-09-07T09:30:45.1234567", - // number_of_projects: 33000, - // file_size: 626581571 - // }, - // { - // identifier: 7, - // namespace: "geo", - // file_path: "https://cloud2.databio.org/pephub/geo/geo_2024_09_06.tar", - // creation_date: "2024-09-06T17:00:18.3913772", - // number_of_projects: 228300, - // file_size: 626581571 - // }, - // { - // identifier: 14, - // namespace: "geo", - // file_path: "https://cloud2.databio.org/pephub/geo/geo_2024_09_07.tar", - // creation_date: "2024-09-07T09:30:45.1234567", - // number_of_projects: 33000, - // file_size: 626581571 - // },{ - // identifier: 5, - // namespace: "geo", - // file_path: "https://cloud2.databio.org/pephub/geo/geo_2024_09_06.tar", - // creation_date: "2024-09-06T17:00:18.3913772", - // number_of_projects: 228300, - // file_size: 626581571 - // }, - // { - // identifier: 3, - // namespace: "geo", - // file_path: "https://cloud2.databio.org/pephub/geo/geo_2024_09_07.tar", - // creation_date: "2024-09-07T09:30:45.1234567", - // number_of_projects: 33000, - // file_size: 626581571 - // } - // ] - // }; - - // console.log(testData) - return (

    Download All GEO Metadata

    @@ -130,7 +27,7 @@ export const DownloadGeo = (props: Props) => { Each archive is slightly over 1 gb in size.

    {data ? : -

    No archives currently exist for this namespace.

    +

    No archives currently exist for this namespace.

    }
    ); diff --git a/web/src/components/namespace/project-cards/project-card-dropdown.tsx b/web/src/components/namespace/project-cards/project-card-dropdown.tsx index 3306c723..3b791c79 100644 --- a/web/src/components/namespace/project-cards/project-card-dropdown.tsx +++ b/web/src/components/namespace/project-cards/project-card-dropdown.tsx @@ -8,6 +8,7 @@ import { useAddStar } from '../../../hooks/mutations/useAddStar'; import { useRemoveStar } from '../../../hooks/mutations/useRemoveStar'; import { copyToClipboard } from '../../../utils/etc'; import { LoadingSpinner } from '../../spinners/loading-spinner'; +import { numberWithCommas } from '../../../utils/etc'; interface Props { project: ProjectAnnotation; @@ -26,7 +27,7 @@ export const ProjectCardDropdown: FC = (props) => { const { isPending: isAddingStar, addStar } = useAddStar(user?.login); const { isPending: isRemovingStar, removeStar } = useRemoveStar(user?.login); - const [localStarred, setLocalStarred] = useState(false); + const [localStarred, setLocalStarred] = useState(isStarred); return ( @@ -51,7 +52,6 @@ export const ProjectCardDropdown: FC = (props) => { projectNameToStar: project.name, projectTagToStar: project.tag, }); - setLocalStarred(true) } }} > @@ -60,7 +60,7 @@ export const ProjectCardDropdown: FC = (props) => { - {copied ? 'Copied!' : (localStarred ? (starNumber + 1) : starNumber)} + {copied ? 'Copied!' : (localStarred ? numberWithCommas(starNumber) : numberWithCommas(starNumber + 1))}
    @@ -69,7 +69,7 @@ export const ProjectCardDropdown: FC = (props) => { - {copied ? 'Copied!' : starNumber} + {copied ? 'Copied!' : (localStarred ? numberWithCommas(starNumber - 1) : numberWithCommas(starNumber))}
    diff --git a/web/src/components/project/project-info-footer.tsx b/web/src/components/project/project-info-footer.tsx index 56f70c54..2a395767 100644 --- a/web/src/components/project/project-info-footer.tsx +++ b/web/src/components/project/project-info-footer.tsx @@ -56,14 +56,12 @@ export const ProjectInfoFooter = () => {
    {projectInfo?.forked_from && ( - - - - Forked from - + + Forked from + { const isStarred = stars?.find((star) => star.namespace === projectInfo?.namespace && star.name === projectInfo?.name) !== undefined; + const [localStarred, setLocalStarred] = useState(isStarred); + + console.log('localStarred ' + localStarred) + console.log('isStarred ' + isStarred) + // watch for the fork query param to open the fork modal useEffect(() => { if (fork) { @@ -147,6 +153,10 @@ export const ProjectHeaderBar = (props: Props) => {
+ {projectInfo ? + + : null + } @@ -211,51 +221,6 @@ export const ProjectHeaderBar = (props: Props) => { -
= (props) => { + const { project, isStarred, starNumber } = props; + const { user } = useSession(); + + const { isPending: isAddingStar, addStar } = useAddStar(user?.login); + const { isPending: isRemovingStar, removeStar } = useRemoveStar(user?.login); + + const [localStarred, setLocalStarred] = useState(isStarred); + + return ( + + ); +}; diff --git a/web/src/globals.css b/web/src/globals.css index f7c01ba6..4a5ecd11 100644 --- a/web/src/globals.css +++ b/web/src/globals.css @@ -739,8 +739,8 @@ body { color: inherit; } -.star-button:hover, -.starred-button:hover { +.dropdown .star-button:hover, +.dropdown .starred-button:hover { box-shadow: inset -1px 0 0 0px black !important; } From ebe6e845b2fbd651288e45beaa3d3c5102408a80 Mon Sep 17 00:00:00 2001 From: Sam Park Date: Mon, 9 Sep 2024 19:02:39 -0400 Subject: [PATCH 068/101] add shadows to project star button --- web/src/components/project/project-stars.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/project/project-stars.tsx b/web/src/components/project/project-stars.tsx index 39e36e84..a73a796b 100644 --- a/web/src/components/project/project-stars.tsx +++ b/web/src/components/project/project-stars.tsx @@ -27,7 +27,7 @@ export const ProjectStars: FC = (props) => {
{project.description ? ( diff --git a/web/src/components/project/project-page-header-bar.tsx b/web/src/components/project/project-page-header-bar.tsx index cb774742..e58bf901 100644 --- a/web/src/components/project/project-page-header-bar.tsx +++ b/web/src/components/project/project-page-header-bar.tsx @@ -43,7 +43,7 @@ export const ProjectHeaderBar = (props: Props) => { const { namespace, projectName, tag, forceTraditionalInterface, setForceTraditionalInterface } = useProjectPage(); // add star and remove star mutations - const { data: stars } = useNamespaceStars(user?.login, {}, true); + const { data: stars, isLoading } = useNamespaceStars(user?.login, {}, true); const { isPending: isAddingStar, addStar } = useAddStar(user?.login); const { isPending: isRemovingStar, removeStar } = useRemoveStar(user?.login); @@ -66,11 +66,6 @@ export const ProjectHeaderBar = (props: Props) => { const isStarred = stars?.find((star) => star.namespace === projectInfo?.namespace && star.name === projectInfo?.name) !== undefined; - const [localStarred, setLocalStarred] = useState(isStarred); - - console.log('localStarred ' + localStarred) - console.log('isStarred ' + isStarred) - // watch for the fork query param to open the fork modal useEffect(() => { if (fork) { @@ -153,7 +148,7 @@ export const ProjectHeaderBar = (props: Props) => {
- {projectInfo ? + {projectInfo && !isLoading ? : null } diff --git a/web/src/components/project/project-stars.tsx b/web/src/components/project/project-stars.tsx index a73a796b..647cc561 100644 --- a/web/src/components/project/project-stars.tsx +++ b/web/src/components/project/project-stars.tsx @@ -23,6 +23,9 @@ export const ProjectStars: FC = (props) => { const [localStarred, setLocalStarred] = useState(isStarred); + console.log('isStarred ' + isStarred) + console.log('localStarred ' + localStarred) + return (
@@ -62,7 +65,7 @@ export const ProjectStars: FC = (props) => { - {(localStarred ? numberWithCommas(starNumber - 1) : numberWithCommas(starNumber))} + {(localStarred ? numberWithCommas(starNumber - 1) : numberWithCommas(starNumber))} {((localStarred && starNumber - 1 === 1) || (!localStarred && starNumber === 1)) ? 'Star' : 'Stars'}
From 676c4ab89d15aed9bf0f22c84a03c8ed1adccc7a Mon Sep 17 00:00:00 2001 From: Sam Park Date: Tue, 10 Sep 2024 00:04:22 -0400 Subject: [PATCH 070/101] style changes --- web/src/components/layout/nav/nav-desktop.tsx | 2 +- web/src/components/namespace/archive/download-geo.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/components/layout/nav/nav-desktop.tsx b/web/src/components/layout/nav/nav-desktop.tsx index e62ff501..a9b774c4 100644 --- a/web/src/components/layout/nav/nav-desktop.tsx +++ b/web/src/components/layout/nav/nav-desktop.tsx @@ -135,7 +135,7 @@ export const NavDesktop = () => {
  • - Schemas + Browse
  • diff --git a/web/src/components/namespace/archive/download-geo.tsx b/web/src/components/namespace/archive/download-geo.tsx index 4b1c9ac0..5ed43a3a 100644 --- a/web/src/components/namespace/archive/download-geo.tsx +++ b/web/src/components/namespace/archive/download-geo.tsx @@ -27,7 +27,7 @@ export const DownloadGeo = (props: Props) => { Each archive is slightly over 1 gb in size.

    {data ? : -

    No archives currently exist for this namespace.

    +

    No archives currently exist for this namespace.

    }
  • ); From 7b605bc5c97789f5e106ee9ddbda8c62269ec090 Mon Sep 17 00:00:00 2001 From: Sam Park Date: Tue, 10 Sep 2024 00:07:02 -0400 Subject: [PATCH 071/101] style again --- web/src/components/namespace/archive/download-geo.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/namespace/archive/download-geo.tsx b/web/src/components/namespace/archive/download-geo.tsx index 5ed43a3a..a4c69f58 100644 --- a/web/src/components/namespace/archive/download-geo.tsx +++ b/web/src/components/namespace/archive/download-geo.tsx @@ -27,7 +27,7 @@ export const DownloadGeo = (props: Props) => { Each archive is slightly over 1 gb in size.

    {data ? : -

    No archives currently exist for this namespace.

    +

    No archives currently exist for this namespace.

    }
    ); From a7da1938a5cfef8110de2a45a75c1a2e9dfea6aa Mon Sep 17 00:00:00 2001 From: Khoroshevskyi Date: Tue, 10 Sep 2024 12:53:12 -0400 Subject: [PATCH 072/101] Added schema validation before saving pep --- pephub/routers/api/v1/helpers.py | 26 ++++++++++++++++++++++---- requirements/requirements-all.txt | 4 ++-- 2 files changed, 24 insertions(+), 6 deletions(-) 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/requirements/requirements-all.txt b/requirements/requirements-all.txt index d82fe6c3..84b83daa 100644 --- a/requirements/requirements-all.txt +++ b/requirements/requirements-all.txt @@ -2,8 +2,8 @@ fastapi>=0.108.0 psycopg>=3.1.15 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 From 291bb8d4f0dd4c1c8a82f7acfdced99a9f46f759 Mon Sep 17 00:00:00 2001 From: Sam Park Date: Tue, 10 Sep 2024 23:07:55 -0400 Subject: [PATCH 073/101] browse page progress --- .../components/browse/project-accordion.tsx | 31 ++ web/src/components/project/view-selector.tsx | 4 +- web/src/components/schemas/schemas-nav.tsx | 2 +- web/src/globals.css | 7 +- web/src/main.tsx | 4 +- web/src/pages/Browse.tsx | 325 ++++++++++++++++++ web/src/pages/Namespace.tsx | 6 +- .../pages/{Schemas.tsx => Schemas copy.tsx} | 0 8 files changed, 368 insertions(+), 11 deletions(-) create mode 100644 web/src/components/browse/project-accordion.tsx create mode 100644 web/src/pages/Browse.tsx rename web/src/pages/{Schemas.tsx => Schemas copy.tsx} (100%) diff --git a/web/src/components/browse/project-accordion.tsx b/web/src/components/browse/project-accordion.tsx new file mode 100644 index 00000000..e54a72c5 --- /dev/null +++ b/web/src/components/browse/project-accordion.tsx @@ -0,0 +1,31 @@ +import { Editor } from '@monaco-editor/react'; +import { useForm } from 'react-hook-form'; +import { useParams } from 'react-router-dom'; + +import { PageLayout } from '../components/layout/page-layout'; +import { SchemaHeader } from '../components/schemas/schema-header'; +import { SchemaInterface } from '../components/schemas/schema-interface'; +import { useSession } from '../contexts/session-context'; +import { useSchema } from '../hooks/queries/useSchema'; + +export function Schema() { + const { user } = useSession(); + const { namespace, schema } = useParams(); + const { data: schemaData, isFetching: isLoading } = useSchema(namespace, schema); + + const {} = useForm(); + + const canEdit = (user && (user.login === namespace || user.orgs.includes(namespace || 'NONE'))) || false; + + return ( + + + + ); +} diff --git a/web/src/components/project/view-selector.tsx b/web/src/components/project/view-selector.tsx index 9ddb6131..b6807f18 100644 --- a/web/src/components/project/view-selector.tsx +++ b/web/src/components/project/view-selector.tsx @@ -80,7 +80,7 @@ export const ViewSelector = (props: ViewSelectorProps) => { }} className="top-z w-100" options={[ - { value: null, label: "Default view" }, + { value: null, label: "Default View" }, ...(projectViews?.views.map((view) => ({ view: view.name, description: view.description || 'No description', @@ -115,7 +115,7 @@ export const ViewSelector = (props: ViewSelectorProps) => { } value={ view === undefined - ? { value: null, label: "Default view" } + ? { value: null, label: "Default View" } : { view: view, description: view, value: view, label: view } } /> diff --git a/web/src/components/schemas/schemas-nav.tsx b/web/src/components/schemas/schemas-nav.tsx index b1bd0ab2..a3b23993 100644 --- a/web/src/components/schemas/schemas-nav.tsx +++ b/web/src/components/schemas/schemas-nav.tsx @@ -33,7 +33,7 @@ export const SchemasNav = (props: Props) => {
    -

    PEPhub schemas

    +

    PEPhub schemas

    {user && (
    diff --git a/web/src/globals.css b/web/src/globals.css index 4a5ecd11..c1436049 100644 --- a/web/src/globals.css +++ b/web/src/globals.css @@ -8,7 +8,8 @@ body { transition: all 180ms; } -.namespace-nav .nav-link:hover { +.namespace-nav .nav-link:hover, +.namespace-card:hover { background-color: #052c6512; color: #052c65; } @@ -778,7 +779,7 @@ body { background-color: white !important; color: black !important; box-shadow: .5px 1px 7px 1px #00000010 !important; - font-weight: 600 !important; + font-weight: 500 !important; } .modal-pill .nav-pills .nav-item { @@ -809,7 +810,7 @@ body { .modal-pill .nav-tabs .nav-link.active { color: black !important; - font-weight: 600 !important; + font-weight: 500 !important; } .modal-pill .nav-tabs .nav-link { diff --git a/web/src/main.tsx b/web/src/main.tsx index dde58be4..dc518a08 100644 --- a/web/src/main.tsx +++ b/web/src/main.tsx @@ -26,7 +26,7 @@ import { LoginSuccessPage } from './pages/LoginSuccess'; import { NamespacePage } from './pages/Namespace'; import { ProjectPage } from './pages/Project'; import { Schema } from './pages/Schema'; -import { Schemas } from './pages/Schemas'; +import { Browse } from './pages/Browse'; import { SearchPage } from './pages/Search'; import { EidoValidator } from './pages/Validator'; @@ -73,7 +73,7 @@ const router = createBrowserRouter([ }, { path: '/schemas', - element: , + element: , }, { path: '/schemas/:namespace', diff --git a/web/src/pages/Browse.tsx b/web/src/pages/Browse.tsx new file mode 100644 index 00000000..3764540d --- /dev/null +++ b/web/src/pages/Browse.tsx @@ -0,0 +1,325 @@ +import { useState } from 'react'; +import Nav from 'react-bootstrap/Nav'; +import { NavLink, useParams, useSearchParams } from 'react-router-dom'; + +import { PageLayout } from '../components/layout/page-layout'; +import { CreateSchemaModal } from '../components/modals/create-schema-modal'; +import { SchemasPagePlaceholder } from '../components/schemas/placeholders/schemas-page-placeholder'; +import { SchemaCard } from '../components/schemas/schema-card'; +import { SchemasNav } from '../components/schemas/schemas-nav'; +import { useAllSchemas } from '../hooks/queries/useAllSchemas'; +import { useDebounce } from '../hooks/useDebounce'; +import { useBiggestNamespace } from '../hooks/queries/useBiggestNamespace'; +import { useNamespaceProjects } from '../hooks/queries/useNamespaceProjects'; +import { LoadingSpinner } from '../components/spinners/loading-spinner'; +import { Markdown } from '../components/markdown/render'; + +import 'bootstrap/dist/css/bootstrap.min.css' +import "bootstrap/dist/js/bootstrap.bundle.min.js" + +type View = 'namespaces' | 'schemas'; + +const ProjectAccordion = ({ projects }) => { + const [openIndex, setOpenIndex] = useState(null); + + // Filter out the 'length' property + const projectItems = Object.entries(projects).filter(([key]) => key !== 'length'); + + const formatDate = (dateString) => { + const date = new Date(dateString); + return date.toLocaleString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: 'numeric', + minute: '2-digit', + hour12: true + }); + }; + + return ( +
    + {projectItems.map(([key, project], index) => ( +
    +

    + +

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

    No description

    } +

    Sample Count: {project.number_of_samples}

    +

    Created: {formatDate(project.submission_date)}

    +

    Updated: {formatDate(project.last_update_date)}

    + +
    +
    +
    + ))} +
    + ); +}; + + +const NoSchemas = () => { + return ( +
    +

    No schemas found

    + +
    + ); +}; + +export function Browse() { + const [searchParams, setSearchParams] = useSearchParams(); + const viewFromUrl = searchParams.get('view') as View; + + const [view, setView] = useState(viewFromUrl || 'namespaces'); + + + const [limit, setLimit] = useState(25); + const [offset, setOffset] = useState(0); + const [search, setSearch] = useState(''); + const [orderBy, setOrderBy] = useState('update_date'); + const [order, setOrder] = useState<'asc' | 'desc'>('desc'); + const [showCreateSchemaModal, setShowCreateSchemaModal] = useState(false); + const [selectedNamespace, setSelectedNamespace] = useState(null) + + const searchDebounced = useDebounce(search, 500); + + const namespaces = useBiggestNamespace(10); + const topNamespace = useNamespaceProjects(selectedNamespace, { + limit: 10, + offset: 0, + orderBy: 'update_date', + // @ts-ignore - just for now, I know this will work fine + order: 'asc', + search: '', + type: view === 'pep', + }); + + console.log(namespaces?.data?.results[0].namespace) + + const handleSelectNamespace = (selectedNamespace) => { + setSelectedNamespace(prevSelectedNamespace => + prevSelectedNamespace === selectedNamespace ? null : selectedNamespace + ); + } + + const handleNavSelect = (eventKey: string | null) => { + if (eventKey === null) { + return; + } + searchParams.set('view', eventKey); + setSearchParams(searchParams); + setView(eventKey as View); + }; + + const { + data: schemas, + isFetching: isLoading, + error, + } = useAllSchemas({ + limit, + offset, + search: searchDebounced, + order, + orderBy, + }); + + const noSchemasInDatabase = schemas?.count === 0; + + if (isLoading) { + return ( + +
    + +
    +
    + ); + } + + if (error) { + return ( + +
    +
    + Error fetching schemas +
    +
    +
    {JSON.stringify(error, null, 2)}
    +
    +
    +
    + ); + } + + const renderRow = (startIndex, endIndex) => ( +
    +
    + {namespaces?.data?.results ? ( + Object.values(namespaces.data.results) + .slice(startIndex, endIndex) + .map((item, index) => ( + + )) + ) : null} +
    +
    + ); + + const renderLongRow = () => ( +
    +
    + {namespaces?.data?.results ? ( + Object.values(namespaces.data.results).map((item, index) => ( +
    +
    + +
    +
    + )) + ) : null} +
    +
    + ) + + return ( + +
    + + +
    + +
    + + {view === 'namespaces' ? + <> +
    +
    + {selectedNamespace === null ? +
    + {renderRow(0, 5)} + {renderRow(5, 10)} +
    + : +
    + {renderLongRow()} + + +
    + } +
    +
    + +
    + {topNamespace?.data?.results ? + : selectedNamespace ? +
    + +
    + : null + } +
    + + : +
    + +
    + {noSchemasInDatabase ? ( + + ) : ( +
    + {schemas?.results.map((s, i) => ( + + ))} +
    + )} +
    +
    + } + +
    + { + setShowCreateSchemaModal(false); + }} + /> +
    + ); +} diff --git a/web/src/pages/Namespace.tsx b/web/src/pages/Namespace.tsx index b645a60e..58adb690 100644 --- a/web/src/pages/Namespace.tsx +++ b/web/src/pages/Namespace.tsx @@ -147,7 +147,7 @@ export const NamespacePage = () => { API - {namespace === 'geos' && ( + {namespace === 'geo' && ( + +
    +
    + + {project.description ? {project.description} :

    No description

    } +

    Sample Count: {project.number_of_samples}

    +

    Created: {formatDate(project.submission_date)}

    +

    Updated: {formatDate(project.last_update_date)}

    + +
    +
    +
    + ))} +
    ); -} +}; \ No newline at end of file diff --git a/web/src/pages/Browse.tsx b/web/src/pages/Browse.tsx index 3764540d..ca5d4032 100644 --- a/web/src/pages/Browse.tsx +++ b/web/src/pages/Browse.tsx @@ -13,70 +13,10 @@ import { useBiggestNamespace } from '../hooks/queries/useBiggestNamespace'; import { useNamespaceProjects } from '../hooks/queries/useNamespaceProjects'; import { LoadingSpinner } from '../components/spinners/loading-spinner'; import { Markdown } from '../components/markdown/render'; - -import 'bootstrap/dist/css/bootstrap.min.css' -import "bootstrap/dist/js/bootstrap.bundle.min.js" +import { ProjectAccordion } from '../components/browse/project-accordion' type View = 'namespaces' | 'schemas'; -const ProjectAccordion = ({ projects }) => { - const [openIndex, setOpenIndex] = useState(null); - - // Filter out the 'length' property - const projectItems = Object.entries(projects).filter(([key]) => key !== 'length'); - - const formatDate = (dateString) => { - const date = new Date(dateString); - return date.toLocaleString('en-US', { - year: 'numeric', - month: 'long', - day: 'numeric', - hour: 'numeric', - minute: '2-digit', - hour12: true - }); - }; - - return ( -
    - {projectItems.map(([key, project], index) => ( -
    -

    - -

    -
    -
    - - {project.description ? {project.description} :

    No description

    } -

    Sample Count: {project.number_of_samples}

    -

    Created: {formatDate(project.submission_date)}

    -

    Updated: {formatDate(project.last_update_date)}

    - -
    -
    -
    - ))} -
    - ); -}; - - const NoSchemas = () => { return (
    Date: Wed, 11 Sep 2024 11:05:24 -0400 Subject: [PATCH 075/101] browse page progress --- .../components/browse/namespace-long-row.tsx | 75 +++++++++++++++++++ .../components/browse/project-accordion.tsx | 24 ++++-- web/src/pages/Browse.tsx | 43 +++++++---- 3 files changed, 121 insertions(+), 21 deletions(-) create mode 100644 web/src/components/browse/namespace-long-row.tsx 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..8c4021a1 --- /dev/null +++ b/web/src/components/browse/namespace-long-row.tsx @@ -0,0 +1,75 @@ +import React, { useRef, useEffect, useCallback } from 'react'; + +export const NamespaceLongRow = ({ namespaces, selectedNamespace, handleSelectNamespace }) => { + const containerRef = useRef(null); + const itemRefs = useRef({}); + + const isInViewport = (element) => { + const rect = element.getBoundingClientRect(); + return ( + rect.top >= 0 && + rect.left >= 0 && + rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && + rect.right <= (window.innerWidth || document.documentElement.clientWidth) + ); + }; + + const scrollToItem = useCallback((namespace) => { + if (itemRefs.current[namespace]) { + const element = itemRefs.current[namespace]; + if (!isInViewport(element)) { + element.scrollIntoView({ + behavior: 'smooth', + block: 'nearest', + inline: 'start' + }); + } + } + }, []); + + useEffect(() => { + if (selectedNamespace) { + scrollToItem(selectedNamespace); + } + }, [selectedNamespace, scrollToItem]); + + const renderLongRow = () => ( +
    + {namespaces?.data?.results ? ( + Object.values(namespaces.data.results).map((item, index) => ( +
    itemRefs.current[item.namespace] = el} + className="col-2 flex-shrink-0" + style={{ scrollSnapAlign: 'start' }} + > +
    + +
    +
    + )) + ) : null} +
    + ); + + return ( +
    + {renderLongRow()} + + +
    + ); +}; diff --git a/web/src/components/browse/project-accordion.tsx b/web/src/components/browse/project-accordion.tsx index 2ed08a06..4f2d2e4b 100644 --- a/web/src/components/browse/project-accordion.tsx +++ b/web/src/components/browse/project-accordion.tsx @@ -48,12 +48,24 @@ export const ProjectAccordion = ({ projects }) => { data-bs-parent="#projectAccordion" >
    - - {project.description ? {project.description} :

    No description

    } -

    Sample Count: {project.number_of_samples}

    -

    Created: {formatDate(project.submission_date)}

    -

    Updated: {formatDate(project.last_update_date)}

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

    No description

    } +

    Sample Count: {project.number_of_samples}

    +

    Created: {formatDate(project.submission_date)}

    +

    Updated: {formatDate(project.last_update_date)}

    +
    + +
    diff --git a/web/src/pages/Browse.tsx b/web/src/pages/Browse.tsx index ca5d4032..d6f14429 100644 --- a/web/src/pages/Browse.tsx +++ b/web/src/pages/Browse.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, useRef } from 'react'; import Nav from 'react-bootstrap/Nav'; import { NavLink, useParams, useSearchParams } from 'react-router-dom'; @@ -14,6 +14,7 @@ import { useNamespaceProjects } from '../hooks/queries/useNamespaceProjects'; import { LoadingSpinner } from '../components/spinners/loading-spinner'; import { Markdown } from '../components/markdown/render'; import { ProjectAccordion } from '../components/browse/project-accordion' +import { NamespaceLongRow } from '../components/browse/namespace-long-row' type View = 'namespaces' | 'schemas'; @@ -48,7 +49,7 @@ export function Browse() { const searchDebounced = useDebounce(search, 500); - const namespaces = useBiggestNamespace(10); + const namespaces = useBiggestNamespace(25); const topNamespace = useNamespaceProjects(selectedNamespace, { limit: 10, offset: 0, @@ -58,13 +59,11 @@ export function Browse() { search: '', type: view === 'pep', }); - - console.log(namespaces?.data?.results[0].namespace) const handleSelectNamespace = (selectedNamespace) => { setSelectedNamespace(prevSelectedNamespace => - prevSelectedNamespace === selectedNamespace ? null : selectedNamespace - ); + prevSelectedNamespace === selectedNamespace ? null : selectedNamespace + ); } const handleNavSelect = (eventKey: string | null) => { @@ -116,7 +115,7 @@ export function Browse() { } const renderRow = (startIndex, endIndex) => ( -
    +
    {namespaces?.data?.results ? ( Object.values(namespaces.data.results) @@ -144,12 +143,21 @@ export function Browse() { const renderLongRow = () => (
    -
    +
    {namespaces?.data?.results ? ( Object.values(namespaces.data.results).map((item, index) => ( -
    +
    itemRefs.current[item.namespace] = el} + className="col-2 flex-shrink-0" + style={{ scrollSnapAlign: 'start' }} + >
    -
    +

    handleSelectNamespace(item?.namespace)}> {index + 1}. {item?.namespace} @@ -200,15 +208,20 @@ export function Browse() {

    {selectedNamespace === null ? -
    +
    {renderRow(0, 5)} {renderRow(5, 10)} + {renderRow(10, 15)} + {renderRow(15, 20)} + {renderRow(20, 25)}
    :
    - {renderLongRow()} - - +
    }
    @@ -225,7 +238,7 @@ export function Browse() {
    : -
    +
    Date: Wed, 11 Sep 2024 12:22:29 -0400 Subject: [PATCH 076/101] fix type errors --- .../components/browse/namespace-long-row.tsx | 56 ++++++++++++------- .../components/browse/project-accordion.tsx | 10 +++- web/src/pages/Browse.tsx | 18 +++--- 3 files changed, 52 insertions(+), 32 deletions(-) diff --git a/web/src/components/browse/namespace-long-row.tsx b/web/src/components/browse/namespace-long-row.tsx index 8c4021a1..fa1850b0 100644 --- a/web/src/components/browse/namespace-long-row.tsx +++ b/web/src/components/browse/namespace-long-row.tsx @@ -1,29 +1,43 @@ import React, { useRef, useEffect, useCallback } from 'react'; -export const NamespaceLongRow = ({ namespaces, selectedNamespace, handleSelectNamespace }) => { - const containerRef = useRef(null); - const itemRefs = useRef({}); +import { BiggestNamespaceResults } from '../../../types'; - const isInViewport = (element) => { +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 && - rect.left >= 0 && - rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && - rect.right <= (window.innerWidth || document.documentElement.clientWidth) + 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) => { - if (itemRefs.current[namespace]) { - const element = itemRefs.current[namespace]; - if (!isInViewport(element)) { - element.scrollIntoView({ - behavior: 'smooth', - block: 'nearest', - inline: 'start' - }); - } + const scrollToItem = useCallback((namespace: string) => { + const element = itemRefs.current[namespace]; + if (element && !isInViewport(element)) { + element.scrollIntoView({ + behavior: 'smooth', + block: 'nearest', + inline: 'start' + }); } }, []); @@ -39,11 +53,11 @@ export const NamespaceLongRow = ({ namespaces, selectedNamespace, handleSelectNa className="row flex-nowrap overflow-auto py-1" style={{ scrollSnapType: 'x mandatory' }} > - {namespaces?.data?.results ? ( - Object.values(namespaces.data.results).map((item, index) => ( + {namespaces ? ( + Object.values(namespaces).map((item, index) => (
    itemRefs.current[item.namespace] = el} + ref={(el) => { itemRefs.current[item.namespace] = el; }} className="col-2 flex-shrink-0" style={{ scrollSnapAlign: 'start' }} > diff --git a/web/src/components/browse/project-accordion.tsx b/web/src/components/browse/project-accordion.tsx index 4f2d2e4b..a68d398a 100644 --- a/web/src/components/browse/project-accordion.tsx +++ b/web/src/components/browse/project-accordion.tsx @@ -1,17 +1,23 @@ import { useState } from 'react'; import { Markdown } from '../markdown/render'; +import { ProjectAnnotation } from '../../../types'; import 'bootstrap/dist/css/bootstrap.min.css' import "bootstrap/dist/js/bootstrap.bundle.min.js" -export const ProjectAccordion = ({ projects }) => { +type Props = { + projects: ProjectAnnotation[]; +}; + +export const ProjectAccordion = (props: Props) => { + const { projects } = props; const [openIndex, setOpenIndex] = useState(null); // Filter out the 'length' property const projectItems = Object.entries(projects).filter(([key]) => key !== 'length'); - const formatDate = (dateString) => { + const formatDate = (dateString: string) => { const date = new Date(dateString); return date.toLocaleString('en-US', { year: 'numeric', diff --git a/web/src/pages/Browse.tsx b/web/src/pages/Browse.tsx index d6f14429..3abbf0b7 100644 --- a/web/src/pages/Browse.tsx +++ b/web/src/pages/Browse.tsx @@ -45,7 +45,7 @@ export function Browse() { const [orderBy, setOrderBy] = useState('update_date'); const [order, setOrder] = useState<'asc' | 'desc'>('desc'); const [showCreateSchemaModal, setShowCreateSchemaModal] = useState(false); - const [selectedNamespace, setSelectedNamespace] = useState(null) + const [selectedNamespace, setSelectedNamespace] = useState(undefined); const searchDebounced = useDebounce(search, 500); @@ -57,12 +57,12 @@ export function Browse() { // @ts-ignore - just for now, I know this will work fine order: 'asc', search: '', - type: view === 'pep', + type: 'pep', }); - const handleSelectNamespace = (selectedNamespace) => { - setSelectedNamespace(prevSelectedNamespace => - prevSelectedNamespace === selectedNamespace ? null : selectedNamespace + const handleSelectNamespace = (selectedNamespace: string) => { + setSelectedNamespace((prevSelectedNamespace: string | undefined) => + prevSelectedNamespace === selectedNamespace ? undefined : selectedNamespace ); } @@ -114,13 +114,13 @@ export function Browse() { ); } - const renderRow = (startIndex, endIndex) => ( + const renderRow = (startIndex: number, endIndex: number) => (
    {namespaces?.data?.results ? ( Object.values(namespaces.data.results) .slice(startIndex, endIndex) - .map((item, index) => ( + .map((item, index: number) => (
    @@ -207,7 +207,7 @@ export function Browse() { <>
    - {selectedNamespace === null ? + {selectedNamespace === undefined ?
    {renderRow(0, 5)} {renderRow(5, 10)} @@ -218,7 +218,7 @@ export function Browse() { :
    From b7e38645751f6898f9deeca3d06835037efda515 Mon Sep 17 00:00:00 2001 From: Sam Park Date: Wed, 11 Sep 2024 14:26:05 -0400 Subject: [PATCH 077/101] address comments --- .../components/browse/namespace-long-row.tsx | 64 +++++++++---------- .../components/browse/project-accordion.tsx | 21 +----- .../archive/namespace-archive-table.tsx | 15 +---- web/src/globals.css | 21 ++++++ web/src/main.tsx | 1 + 5 files changed, 56 insertions(+), 66 deletions(-) diff --git a/web/src/components/browse/namespace-long-row.tsx b/web/src/components/browse/namespace-long-row.tsx index fa1850b0..878914de 100644 --- a/web/src/components/browse/namespace-long-row.tsx +++ b/web/src/components/browse/namespace-long-row.tsx @@ -47,43 +47,37 @@ export const NamespaceLongRow = (props: Props) => { } }, [selectedNamespace, scrollToItem]); - const renderLongRow = () => ( -
    - {namespaces ? ( - Object.values(namespaces).map((item, index) => ( -
    { itemRefs.current[item.namespace] = el; }} - className="col-2 flex-shrink-0" - style={{ scrollSnapAlign: 'start' }} - > -
    - -
    -
    - )) - ) : null} -
    - ); - return (
    ); }; diff --git a/web/src/components/browse/project-accordion.tsx b/web/src/components/browse/project-accordion.tsx index a68d398a..300b7ba4 100644 --- a/web/src/components/browse/project-accordion.tsx +++ b/web/src/components/browse/project-accordion.tsx @@ -2,9 +2,7 @@ import { useState } from 'react'; import { Markdown } from '../markdown/render'; import { ProjectAnnotation } from '../../../types'; - -import 'bootstrap/dist/css/bootstrap.min.css' -import "bootstrap/dist/js/bootstrap.bundle.min.js" +import { dateStringToDateTime } from '../../utils/dates' type Props = { projects: ProjectAnnotation[]; @@ -12,23 +10,10 @@ type Props = { export const ProjectAccordion = (props: Props) => { const { projects } = props; - const [openIndex, setOpenIndex] = useState(null); // Filter out the 'length' property const projectItems = Object.entries(projects).filter(([key]) => key !== 'length'); - const formatDate = (dateString: string) => { - const date = new Date(dateString); - return date.toLocaleString('en-US', { - year: 'numeric', - month: 'long', - day: 'numeric', - hour: 'numeric', - minute: '2-digit', - hour12: true - }); - }; - return (
    {projectItems.map(([key, project], index) => ( @@ -58,8 +43,8 @@ export const ProjectAccordion = (props: Props) => {
    {project.description ? {project.description} :

    No description

    }

    Sample Count: {project.number_of_samples}

    -

    Created: {formatDate(project.submission_date)}

    -

    Updated: {formatDate(project.last_update_date)}

    +

    Created: {dateStringToDateTime(project.submission_date)}

    +

    Updated: {dateStringToDateTime(project.last_update_date)}

    { - const date = new Date(dateString); - return date.toLocaleString('en-US', { - year: 'numeric', - month: 'long', - day: 'numeric', - hour: 'numeric', - minute: '2-digit', - hour12: true - }); -}; - export const NamespaceArchiveTable: React.FC = ({ data }) => { const [currentPage, setCurrentPage] = useState(1); const [sortConfig, setSortConfig] = useState<{ key: keyof ArchiveItem; direction: 'ascending' | 'descending' }>({ @@ -94,7 +83,7 @@ export const NamespaceArchiveTable: React.FC = ({ data }) => { {result.namespace} {result.number_of_projects} - {formatDate(result.creation_date)} + {dateStringToDateTime(result.creation_date)} {(result.file_size / 1024 / 1024 / 1024).toFixed(2)} gb diff --git a/web/src/globals.css b/web/src/globals.css index c1436049..7f66f1da 100644 --- a/web/src/globals.css +++ b/web/src/globals.css @@ -871,6 +871,27 @@ body { color: #052c65cf; } +.scroll-track-none::-webkit-scrollbar { + width: 3px; + height: 6px; +} + +.scroll-track-none::-webkit-scrollbar-button { + width: 0; + height: 0; + display: none; +} + +.scroll-track-none::-webkit-scrollbar-corner { + background-color: transparent; +} + +.scroll-track-none::-webkit-scrollbar-thumb { + background-color: rgba(0, 0, 0, 0.2); + -webkit-box-shadow: inset 1px 1px 0 rgba(0, 0, 0, 0.10), inset 0 -1px 0 rgba(0, 0, 0, 0.07); + border-radius: .375em; +} + @keyframes pulse-fast { 0%, 88%, 100% { opacity: 1; } 10% { opacity: 0.5; } diff --git a/web/src/main.tsx b/web/src/main.tsx index dc518a08..cd366212 100644 --- a/web/src/main.tsx +++ b/web/src/main.tsx @@ -5,6 +5,7 @@ import 'bootstrap-icons/font/bootstrap-icons.css'; // css import 'bootstrap/dist/css/bootstrap.min.css'; import 'handsontable/dist/handsontable.full.min.css'; +import "bootstrap/dist/js/bootstrap.bundle.min.js" // Language // handsontable stuff import { registerAllModules } from 'handsontable/registry'; From 621a8a486f746c606ccd00a591ac780a64839451 Mon Sep 17 00:00:00 2001 From: Sam Park Date: Wed, 11 Sep 2024 15:18:04 -0400 Subject: [PATCH 078/101] link to namespace added to browse --- .../components/browse/project-accordion.tsx | 2 +- web/src/pages/Browse.tsx | 32 ++++++++++++++----- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/web/src/components/browse/project-accordion.tsx b/web/src/components/browse/project-accordion.tsx index 300b7ba4..ab5fdc6c 100644 --- a/web/src/components/browse/project-accordion.tsx +++ b/web/src/components/browse/project-accordion.tsx @@ -15,7 +15,7 @@ export const ProjectAccordion = (props: Props) => { const projectItems = Object.entries(projects).filter(([key]) => key !== 'length'); return ( -
    +
    {projectItems.map(([key, project], index) => (

    diff --git a/web/src/pages/Browse.tsx b/web/src/pages/Browse.tsx index 3abbf0b7..fb11f776 100644 --- a/web/src/pages/Browse.tsx +++ b/web/src/pages/Browse.tsx @@ -205,7 +205,7 @@ export function Browse() { {view === 'namespaces' ? <> -
    +
    {selectedNamespace === undefined ?
    @@ -216,7 +216,7 @@ export function Browse() { {renderRow(20, 25)}
    : -
    +
    - {topNamespace?.data?.results ? - : selectedNamespace ? -
    - +
    + {topNamespace?.data?.results ? + <> + + +

    Want to see more? Visit the namespace to view remaining projects.

    + + : selectedNamespace ? +
    + +
    + : null + }
    - : null - }
    : From 53107a876d3a92b217edca61ed4634056fdd03d2 Mon Sep 17 00:00:00 2001 From: Sam Park Date: Wed, 11 Sep 2024 16:54:05 -0400 Subject: [PATCH 079/101] reroute schemas to browse --- web/src/components/layout/nav/nav-desktop.tsx | 2 +- web/src/components/schemas/schema-header.tsx | 2 +- web/src/main.tsx | 2 +- web/src/pages/Browse.tsx | 12 +++++------- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/web/src/components/layout/nav/nav-desktop.tsx b/web/src/components/layout/nav/nav-desktop.tsx index a9b774c4..8b105d7c 100644 --- a/web/src/components/layout/nav/nav-desktop.tsx +++ b/web/src/components/layout/nav/nav-desktop.tsx @@ -134,7 +134,7 @@ export const NavDesktop = () => {
  • - + Browse
  • diff --git a/web/src/components/schemas/schema-header.tsx b/web/src/components/schemas/schema-header.tsx index 5a070370..4d5beced 100644 --- a/web/src/components/schemas/schema-header.tsx +++ b/web/src/components/schemas/schema-header.tsx @@ -41,7 +41,7 @@ export const SchemaHeader = (props: Props) => {
    - schemas + schemas {namespace} {schema} diff --git a/web/src/main.tsx b/web/src/main.tsx index cd366212..a64bf09a 100644 --- a/web/src/main.tsx +++ b/web/src/main.tsx @@ -73,7 +73,7 @@ const router = createBrowserRouter([ ), }, { - path: '/schemas', + path: '/browse', element: , }, { diff --git a/web/src/pages/Browse.tsx b/web/src/pages/Browse.tsx index fb11f776..c2dd85ec 100644 --- a/web/src/pages/Browse.tsx +++ b/web/src/pages/Browse.tsx @@ -16,7 +16,7 @@ import { Markdown } from '../components/markdown/render'; import { ProjectAccordion } from '../components/browse/project-accordion' import { NamespaceLongRow } from '../components/browse/namespace-long-row' -type View = 'namespaces' | 'schemas'; +type View = 'peps' | 'schemas'; const NoSchemas = () => { return ( @@ -36,8 +36,7 @@ export function Browse() { const [searchParams, setSearchParams] = useSearchParams(); const viewFromUrl = searchParams.get('view') as View; - const [view, setView] = useState(viewFromUrl || 'namespaces'); - + const [view, setView] = useState(viewFromUrl || 'peps'); const [limit, setLimit] = useState(25); const [offset, setOffset] = useState(0); @@ -54,7 +53,6 @@ export function Browse() { limit: 10, offset: 0, orderBy: 'update_date', - // @ts-ignore - just for now, I know this will work fine order: 'asc', search: '', type: 'pep', @@ -189,9 +187,9 @@ export function Browse() { className='border border-2 border-light-subtle rounded rounded-3 bg-body-secondary mt-3 w-50 mx-auto' > - + - Popular Namespaces + Popular PEPs @@ -203,7 +201,7 @@ export function Browse() {
    - {view === 'namespaces' ? + {view === 'peps' ? <>
    From 84c7e9c332572dc5f06e6adec31e11a2c54d613d Mon Sep 17 00:00:00 2001 From: Sam Park Date: Wed, 11 Sep 2024 17:05:01 -0400 Subject: [PATCH 080/101] remove individual geo download button --- web/src/pages/Namespace.tsx | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/web/src/pages/Namespace.tsx b/web/src/pages/Namespace.tsx index 58adb690..ac7ae83c 100644 --- a/web/src/pages/Namespace.tsx +++ b/web/src/pages/Namespace.tsx @@ -59,7 +59,6 @@ export const NamespacePage = () => { // state const [showAddPEPModal, setShowAddPEPModal] = useState(false); const [showEndpointsModal, setShowEndpointsModal] = useState(false); - const [showGeoDownloadModal, setShowGeoDownloadModal] = useState(false); const [showSettingsModal, setShowSettingsModal] = useState(false); const [view, setView] = useState(viewFromUrl || 'peps'); const [starSearch, setStarSearch] = useState(searchParams.get('starSearch') || ''); @@ -147,17 +146,6 @@ export const NamespacePage = () => { API - {namespace === 'geo' && ( - - )} {user?.login === namespace && ( ) : null}
    From f4732dccb8924e39fe3bde20da0b5916c55faaa7 Mon Sep 17 00:00:00 2001 From: Sam Park Date: Fri, 13 Sep 2024 13:04:35 -0400 Subject: [PATCH 082/101] non hardcoded standardizer schemas --- pephub/routers/api/v1/namespace.py | 14 ++++++++++++++ pephub/routers/api/v1/project.py | 19 +++++++++++++++++++ web/src/api/namespace.ts | 6 ++++++ .../modals/standardize-metadata.tsx | 19 +++++++------------ .../project-cards/project-card-dropdown.tsx | 3 --- web/src/globals.css | 11 +++++++++++ .../hooks/queries/useStandardizerSchemas.ts | 18 ++++++++++++++++++ 7 files changed, 75 insertions(+), 15 deletions(-) create mode 100644 web/src/hooks/queries/useStandardizerSchemas.ts diff --git a/pephub/routers/api/v1/namespace.py b/pephub/routers/api/v1/namespace.py index 6f8069be..29c8991c 100644 --- a/pephub/routers/api/v1/namespace.py +++ b/pephub/routers/api/v1/namespace.py @@ -42,6 +42,8 @@ from ....helpers import parse_user_file_upload, split_upload_files_on_init_file from ...models import FavoriteRequest, ProjectJsonRequest, ProjectRawModel +from attribute_standardizer import AttrStandardizer + load_dotenv() namespaces = APIRouter(prefix="/api/v1/namespaces", tags=["namespace"]) @@ -454,3 +456,15 @@ async def get_archive(namespace: str, agent: PEPDatabaseAgent = Depends(get_db)) item.file_path = os.path.join(ARCHIVE_URL_PATH, item.file_path) return result + + +@namespace.get( + "/standardizer_schemas", + summary="Get metadata of all archived files of all projects in the namespace", +) +async def get_schemas(namespace: str, agent: PEPDatabaseAgent = Depends(get_db)): + + model = AttrStandardizer("ENCODE") + schemas = model.get_available_schemas() + + return schemas diff --git a/pephub/routers/api/v1/project.py b/pephub/routers/api/v1/project.py index 6137a550..ea6e53e3 100644 --- a/pephub/routers/api/v1/project.py +++ b/pephub/routers/api/v1/project.py @@ -1182,3 +1182,22 @@ async def get_standardized_cols( ) return StandardizerResponse(results=results) + + +@project.get( + "/history/{history_id}/zip", + summary="Zip a project history by id", + response_class=FileResponse, +) +def get_zip_snapshot( + namespace: str, + project: str, + history_id: int, + tag: str = DEFAULT_TAG, +): + """ + Get a project dict from history by id + """ + + schemas = model.show_available_schemas() + return schemas diff --git a/web/src/api/namespace.ts b/web/src/api/namespace.ts index 06bd6fe3..4f2def76 100644 --- a/web/src/api/namespace.ts +++ b/web/src/api/namespace.ts @@ -363,3 +363,9 @@ 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/components/modals/standardize-metadata.tsx b/web/src/components/modals/standardize-metadata.tsx index 2047e5ed..0d799664 100644 --- a/web/src/components/modals/standardize-metadata.tsx +++ b/web/src/components/modals/standardize-metadata.tsx @@ -9,6 +9,7 @@ import { useEditProjectMetaMutation } from '../../hooks/mutations/useEditProject import { useProjectAnnotation } from '../../hooks/queries/useProjectAnnotation'; import { useSampleTable } from '../../hooks/queries/useSampleTable'; import { useStandardize } from '../../hooks/queries/useStandardize'; +import { useStandardizerSchemas } from '../../hooks/queries/useStandardizerSchemas'; import { formatToPercentage } from '../../utils/etc'; import { arraysToSampleList, sampleListToArrays } from '../../utils/sample-table'; import { ProjectMetaEditForm } from '../forms/edit-project-meta'; @@ -31,8 +32,6 @@ type Props = { type TabDataRow = string[]; type TabData = TabDataRow[]; type SelectedValues = Record; -type AvailableSchemas = 'ENCODE' | 'FAIRTRACKS'; - type StandardizedData = Record>; export const StandardizeMetadataModal = (props: Props) => { @@ -50,6 +49,8 @@ export const StandardizeMetadataModal = (props: Props) => { setResetStandardizedData, } = props; + const { data: schemaOptions} = useStandardizerSchemas(namespace); + const tabDataRaw = newSamples; const tabData = tabDataRaw[0] .map((_, colIndex) => tabDataRaw.map((row) => row[colIndex])) @@ -62,7 +63,7 @@ export const StandardizeMetadataModal = (props: Props) => { const originalCols: string[] = useMemo(() => Object.keys(tabData), []); const newCols: string[] = Object.keys(tabData); - const [selectedOption, setSelectedOption] = useState<{ value: AvailableSchemas; label: string } | null>(null); + const [selectedOption, setSelectedOption] = useState<{ value: string; label: string } | null>(null); const [selectedValues, setSelectedValues] = useState({}); const [whereDuplicates, setWhereDuplicates] = useState(null); @@ -220,17 +221,11 @@ export const StandardizeMetadataModal = (props: Props) => { styles={{ control: (provided) => ({ ...provided, - borderRadius: '.333333em', // Left radii set to 0, right radii kept at 4px + borderRadius: '.375em', // Left radii set to 0, right radii kept at 4px }), }} - options={[ - // @ts-ignore - { value: 'ENCODE', label: 'ENCODE' }, - // @ts-ignore - { value: 'FAIRTRACKS', label: 'Fairtracks' }, - // @ts-ignore - { value: 'BEDBASE', label: 'BEDBASE' }, - ]} + // @ts-ignore + options={schemaOptions?.map(option => ({ value: option, label: option }))} defaultValue={selectedOption} value={selectedOption} onChange={(selectedOption) => { diff --git a/web/src/components/namespace/project-cards/project-card-dropdown.tsx b/web/src/components/namespace/project-cards/project-card-dropdown.tsx index 4a4f18e9..cb9fe9c5 100644 --- a/web/src/components/namespace/project-cards/project-card-dropdown.tsx +++ b/web/src/components/namespace/project-cards/project-card-dropdown.tsx @@ -29,9 +29,6 @@ export const ProjectCardDropdown: FC = (props) => { const [localStarred, setLocalStarred] = useState(isStarred); - console.log('isStarred ' + project?.name + ' ' + isStarred) - console.log('localStarred ' + project?.name + ' ' + localStarred) - return (

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

    No description

    } -

    Sample Count: {project.number_of_samples}

    -

    Created: {dateStringToDateTime(project.submission_date)}

    -

    Updated: {dateStringToDateTime(project.last_update_date)}

    + Created: {dateStringToDateTime(project.submission_date)} + Updated: {dateStringToDateTime(project.last_update_date)}
    -
    +
    { } style={{ transitionDuration: '250ms' }} > - + ) : null}
    diff --git a/web/src/pages/Browse.tsx b/web/src/pages/Browse.tsx index c2dd85ec..247b83eb 100644 --- a/web/src/pages/Browse.tsx +++ b/web/src/pages/Browse.tsx @@ -231,7 +231,7 @@ export function Browse() { <>
    { return date.toLocaleString('default', { month: 'long', year: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric', second:'numeric'}); }; +export const dateStringToDateTimeShort = (dateString: string) => { + const date = new Date(dateString); + return date.toLocaleString('default', { month: 'short', year: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric', second:'numeric'}); +}; + export const dateStringToDate = (dateString: string) => { const date = new Date(dateString); // give full month name From 9a4cc949a040d6a1b337bfad4d185e8a960b175d Mon Sep 17 00:00:00 2001 From: Sam Park Date: Mon, 16 Sep 2024 00:04:46 -0400 Subject: [PATCH 084/101] nav mobile --- web/src/components/layout/nav/nav-mobile.tsx | 22 ++++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/web/src/components/layout/nav/nav-mobile.tsx b/web/src/components/layout/nav/nav-mobile.tsx index af020760..dfe60c51 100644 --- a/web/src/components/layout/nav/nav-mobile.tsx +++ b/web/src/components/layout/nav/nav-mobile.tsx @@ -85,26 +85,26 @@ export const MobileNav = () => { )} - - - GitHub + + + Search - - - Schemas + + + Browse Validation - - - Search - - + Docs + + + GitHub + {/**/} From ed1e6ef34738f1ba7aebea8091348f2b1f307755 Mon Sep 17 00:00:00 2001 From: Khoroshevskyi Date: Mon, 16 Sep 2024 10:25:52 -0400 Subject: [PATCH 085/101] added stars to sort by to namespace endpoint --- README.md | 2 +- pephub/_version.py | 2 +- pephub/routers/api/v1/namespace.py | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) 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/routers/api/v1/namespace.py b/pephub/routers/api/v1/namespace.py index 6f8069be..0d08089c 100644 --- a/pephub/routers/api/v1/namespace.py +++ b/pephub/routers/api/v1/namespace.py @@ -80,7 +80,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"]], From eee525ec953fbe5e3ee833226d15d9c04cc05e75 Mon Sep 17 00:00:00 2001 From: Sam Park Date: Mon, 16 Sep 2024 12:59:37 -0400 Subject: [PATCH 086/101] order browse peps by stars and improve browse page mobile compatibility --- .../components/browse/namespace-long-row.tsx | 2 +- web/src/pages/Browse.tsx | 79 ++++++++----------- 2 files changed, 35 insertions(+), 46 deletions(-) diff --git a/web/src/components/browse/namespace-long-row.tsx b/web/src/components/browse/namespace-long-row.tsx index 878914de..0dcae945 100644 --- a/web/src/components/browse/namespace-long-row.tsx +++ b/web/src/components/browse/namespace-long-row.tsx @@ -59,7 +59,7 @@ export const NamespaceLongRow = (props: Props) => {
    { itemRefs.current[item.namespace] = el; }} - className="col-2 flex-shrink-0" + className="col-xl-2 col-md-4 flex-shrink-0" style={{ scrollSnapAlign: 'start' }} >
    diff --git a/web/src/pages/Browse.tsx b/web/src/pages/Browse.tsx index 247b83eb..bd28bdfc 100644 --- a/web/src/pages/Browse.tsx +++ b/web/src/pages/Browse.tsx @@ -52,8 +52,9 @@ export function Browse() { const topNamespace = useNamespaceProjects(selectedNamespace, { limit: 10, offset: 0, - orderBy: 'update_date', - order: 'asc', + orderBy: 'stars', + // orderBy: 'update_date', + order: 'desc', search: '', type: 'pep', }); @@ -139,52 +140,43 @@ export function Browse() {
    ); - const renderLongRow = () => ( -
    -
    - {namespaces?.data?.results ? ( - Object.values(namespaces.data.results).map((item, index) => ( -
    itemRefs.current[item.namespace] = el} - className="col-2 flex-shrink-0" - style={{ scrollSnapAlign: 'start' }} - > -
    -
    -

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

    -

    - {item?.number_of_projects} Projects -

    + const renderNamespaceGrid = () => ( +
    +
    +
    + {namespaces?.data?.results ? + Object.values(namespaces.data.results).map((item, index: number) => ( +
    +
    +
    -
    - )) - ) : null} + )) + : null} +
    - ) + ); return (
    - - -
    +