Search
diff --git a/web/src/components/schemas/schemas-nav.tsx b/web/src/components/schemas/schemas-nav.tsx
index eff84fef..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 && (
@@ -76,16 +76,32 @@ export const SchemasNav = (props: Props) => {
+
);
diff --git a/web/src/components/spinners/loading-spinner.tsx b/web/src/components/spinners/loading-spinner.tsx
index 86b60506..f36df859 100644
--- a/web/src/components/spinners/loading-spinner.tsx
+++ b/web/src/components/spinners/loading-spinner.tsx
@@ -1,4 +1,4 @@
-import { Fragment } from 'react';
+import React from 'react';
type SpinnerProps = {
fillClassName?: string;
@@ -7,22 +7,25 @@ type SpinnerProps = {
export const LoadingSpinner = (props: SpinnerProps) => {
const { className, fillClassName } = props;
- const sClassName = className || '';
- const gClassName = fillClassName || '';
+ const sClassName = `${className || ''}`.trim();
+ const gClassName = `spinner-pulse ${fillClassName || ''}`.trim();
+
return (
-
+ <>
-
+ >
);
};
+
+export default LoadingSpinner;
\ No newline at end of file
diff --git a/web/src/components/tables/browse-table.tsx b/web/src/components/tables/browse-table.tsx
new file mode 100644
index 00000000..2c0ff569
--- /dev/null
+++ b/web/src/components/tables/browse-table.tsx
@@ -0,0 +1,69 @@
+import React from 'react';
+import { HotTable } from '@handsontable/react';
+import Handsontable from 'handsontable';
+import { formatToPercentage } from '../../utils/etc';
+import { useSampleTable } from '../../hooks/queries/useSampleTable';
+import { arraysToSampleList, sampleListToArrays } from '../../utils/sample-table';
+import { LoadingSpinner } from '../spinners/loading-spinner';
+
+type BrowseTableProps = {
+ namespace: string;
+ project: string;
+ tag: string;
+};
+
+export const BrowseTable = (props: BrowseTableProps) => {
+ const { namespace, project, tag } = props;
+ const { data: tabData, isFetching } = useSampleTable({
+ namespace,
+ project,
+ tag,
+ enabled: true
+ });
+ const tabSamples = tabData?.items || [];
+
+ return (
+ <>
+ {isFetching ? (
+
+
+
+ ) : tabSamples.length > 0 ? (
+
+
Sample Table:
+
+ {
+ const cellProperties: Handsontable.CellProperties = {
+ row: row,
+ col: col,
+ className: row === 0 ? 'fw-bold' : '',
+ instance: {} as Handsontable.Core,
+ visualRow: row,
+ visualCol: col,
+ prop: col
+ };
+ return cellProperties;
+ }}
+ licenseKey="non-commercial-and-evaluation"
+ className="custom-handsontable"
+ />
+
+
+ ) : (
+
+ This project has no samples.
+
+ )}
+ >
+ );
+};
diff --git a/web/src/components/tables/sample-table.tsx b/web/src/components/tables/sample-table.tsx
index d6d2d9df..d486141c 100644
--- a/web/src/components/tables/sample-table.tsx
+++ b/web/src/components/tables/sample-table.tsx
@@ -54,6 +54,11 @@ export const SampleTable = (props: Props) => {
});
}
+ // const columns = data[0].map((header, index) => ({
+ // data: index,
+ // readOnly: header === 'ph_id' || readOnly
+ // }));
+
const numColumns = data.length > 0 ? data[0].length : 0;
const ph_id_col = data[0].indexOf('ph_id');
@@ -90,11 +95,30 @@ export const SampleTable = (props: Props) => {
ref={hotRef}
data={data}
stretchH={stretchH || 'all'}
- height={height || tableHeight}
+ // height={height || tableHeight}
+ height={height || '100%'}
readOnly={readOnly}
colHeaders={true}
+ // columns={columns}
+ cells={(row, col, prop) => {
+ const cellProperties = {} as { isHeader?: boolean };
+ if (row === 0) {
+ cellProperties.isHeader = true;
+ } else {
+ cellProperties.isHeader = false;
+ }
+ // if (col === ph_id_col) {
+ // cellProperties.readOnly = true;
+ // }
+ return cellProperties;
+ }}
renderer={(instance, td, row, col, prop, value, cellProperties) => {
Handsontable.renderers.TextRenderer.apply(this, [instance, td, row, col, prop, value, cellProperties]);
+ if (cellProperties.isHeader) {
+ td.style.fontWeight = 'bold';
+ } else {
+ td.style.fontWeight = 'normal';
+ }
td.innerHTML = `
${value || ''}
`;
td.addEventListener('click', function (event) {
const innerDiv = td.querySelector('.truncated');
diff --git a/web/src/components/tables/standardizer-table.tsx b/web/src/components/tables/standardizer-table.tsx
new file mode 100644
index 00000000..5104c96d
--- /dev/null
+++ b/web/src/components/tables/standardizer-table.tsx
@@ -0,0 +1,152 @@
+import React from 'react';
+import { HotTable } from '@handsontable/react';
+import Handsontable from 'handsontable';
+
+import { formatToPercentage } from '../../utils/etc';
+
+type StandardizerTableProps = {
+ columnKey: string;
+ columnIndex: number;
+ standardizedData: Record
;
+ selectedValues: string;
+ whereDuplicates: number[] | null;
+ sampleTableIndex: string;
+ tabData: string[];
+ handleRadioChange: (key: string, value: string | null) => void;
+ tableData: string[][];
+};
+
+export const StandardizerTable = (props: StandardizerTableProps) => {
+ const {
+ columnKey,
+ columnIndex,
+ standardizedData,
+ selectedValues,
+ whereDuplicates,
+ sampleTableIndex,
+ tabData,
+ handleRadioChange,
+ tableData,
+ } = props;
+
+ return (
+ <>
+
+
+ {columnKey === sampleTableIndex ? (
+
+ SampleTableIndex must also be updated in project config!
+
+ ) : null}
+
+
+
+
+
+ >
+ );
+};
\ No newline at end of file
diff --git a/web/src/globals.css b/web/src/globals.css
index d28464d2..49b61f57 100644
--- a/web/src/globals.css
+++ b/web/src/globals.css
@@ -3,10 +3,20 @@ body {
overflow: auto;
}
-.namespace-nav {
- .nav-link.active {
- background-color: #0080ffc8 !important;
- }
+.namespace-nav .nav-link {
+ color: black;
+ transition: all 180ms;
+}
+
+.namespace-nav .nav-link:hover,
+.namespace-card:hover {
+ background-color: #052c6512;
+ color: #052c65;
+}
+
+.namespace-nav .nav-link.active {
+ background-color: #052c65cf !important;
+ color: white !important;
}
.top-z {
@@ -36,6 +46,11 @@ body {
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
+.btn-primary-emphasis {
+ background-color: #052c65;
+ color: white;
+}
+
/* tailwind pulse */
.pulse {
overflow: visible;
@@ -445,10 +460,10 @@ body {
@keyframes glow {
from {
- stroke: #7ab9fd;
+ stroke: #052c6598;
}
to {
- stroke: #007bff;
+ stroke: #052c65bf;
}
}
@@ -460,7 +475,7 @@ body {
}
.landing-icon-border {
- border: 1px solid;
+ border: 1px solid #052c65bf !important;
animation: border-glow 1s ease-in-out infinite alternate;
}
@@ -582,18 +597,6 @@ body {
display: block;
}
-.large-flex {
- @media (min-width: 1024px) {
- display: flex;
- }
-}
-
-.large-hidden {
- @media (min-width: 1024px) {
- display: none;
- }
-}
-
.list-none {
list-style: none;
}
@@ -688,3 +691,221 @@ 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 !important;
+ color: #28a745;
+}
+
+.namespace-card:hover {
+ background: #052c6512 !important;
+/* box-shadow: 0 0 8px #007bff80 !important;*/
+ transition: all 180ms;
+}
+
+.star-button {
+ background-color: white;
+}
+
+.starred-button {
+ background-color: #cfe2ff !important;
+ color: #052c65 !important;
+}
+
+.star-dropdown-button {
+ background-color: white;
+}
+
+.star-button:hover,
+.starred-button:hover,
+.star-dropdown-button:hover {
+ border: 1px solid black !important;
+ background-color: #cfe2ff80;
+ color: inherit;
+}
+
+.dropdown .star-button:hover,
+.dropdown .starred-button:hover {
+ box-shadow: inset -1px 0 0 0px black !important;
+}
+
+.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;
+}
+
+.modal-pill .nav-pills .nav-link.active {
+ background-color: white !important;
+ color: black !important;
+ box-shadow: .5px 1px 7px 1px #00000010 !important;
+ font-weight: 500 !important;
+}
+
+.modal-pill .nav-pills .nav-item {
+ margin-left: 1px;
+}
+
+.modal-pill .nav-pills .nav-item:first-child {
+ margin-left: 0;
+ margin-right: 0;
+}
+
+.modal-pill .nav-pills .nav-item:last-child {
+ margin-right: -1.5px;
+}
+
+.modal-pill .nav-pills .nav-link {
+ color: #000000aa;
+ padding-left: 0;
+ padding-right: 0;
+ width: calc(100% - 1.5px);
+}
+
+.modal-pill .nav-pills .nav-link:hover {
+ color: black;
+ transition: all 180ms;
+ background-color: #ffffff80;
+}
+
+.modal-pill .nav-tabs .nav-link.active {
+ color: black !important;
+ font-weight: 500 !important;
+}
+
+.modal-pill .nav-tabs .nav-link {
+ color: #000000aa;
+ padding-top: .4;
+ padding-bottom: .4;
+}
+
+.modal-pill .nav-tabs .nav-link:hover {
+ color: black;
+}
+
+.breadcrumb-item a,
+.dark-link {
+ color: #052c65cf !important;
+ text-decoration: none;
+ padding: 5px;
+ margin: -5px;
+ border-radius: .375rem;
+}
+
+@-moz-document url-prefix() {
+ .breadcrumb-item a,
+ .dark-link {
+ color: #052c65cf !important;
+ text-decoration: none;
+ padding: 5px;
+ margin: -5px 0;
+ border-radius: .375rem;
+ }
+}
+
+.breadcrumb-item a:hover,
+.dark-link:hover {
+ color: #052c65 !important;
+ background-color: #052c6512;
+}
+
+.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 {
+ transition: all 150ms;
+}
+
+.forked-link:hover {
+ border-color: black !important;
+ background-color: #cfe2ff80 !important;
+}
+
+.forked-link:hover a {
+ color: #007bff;
+}
+
+.forked-link a {
+ 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; }
+}
+
+.spinner-pulse circle {
+ animation: pulse-fast 1.15s cubic-bezier(0.1, 0, 0.1, 1) infinite;
+}
+
+.spinner-pulse circle:nth-child(1) { animation-delay: 0s; }
+.spinner-pulse circle:nth-child(8) { animation-delay: -0.14375s; }
+.spinner-pulse circle:nth-child(7) { animation-delay: -0.2875s; }
+.spinner-pulse circle:nth-child(6) { animation-delay: -0.43125s; }
+.spinner-pulse circle:nth-child(5) { animation-delay: -0.575s; }
+.spinner-pulse circle:nth-child(4) { animation-delay: -0.71875s; }
+.spinner-pulse circle:nth-child(3) { animation-delay: -0.8625s; }
+.spinner-pulse circle:nth-child(2) { animation-delay: -1.00625s; }
+
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/hooks/queries/useStandardize.ts b/web/src/hooks/queries/useStandardize.ts
new file mode 100644
index 00000000..d1931676
--- /dev/null
+++ b/web/src/hooks/queries/useStandardize.ts
@@ -0,0 +1,27 @@
+import { useQuery, UseQueryResult } from '@tanstack/react-query';
+import { getStandardizedCols, StandardizeColsResponse } from '../../api/project';
+import { useSession } from '../../contexts/session-context';
+
+export const useStandardize = (
+ namespace: string | undefined,
+ project: string | undefined,
+ tag: string | undefined,
+ schema: string | undefined
+): UseQueryResult => {
+ const session = useSession();
+
+ const query = useQuery({
+ queryKey: [namespace, project, tag],
+ queryFn: () =>
+ getStandardizedCols(
+ namespace || '',
+ project || '',
+ tag || '', // Assuming tag should not be undefined when called
+ session?.jwt || null, // Changed to null to match getStandardizedCols signature
+ schema || ''
+ ),
+ enabled: false, // This query should only run on demand (ie. when the user clicks the standardize button)
+ });
+
+ return query;
+};
\ No newline at end of file
diff --git a/web/src/hooks/queries/useStandardizerSchemas.ts b/web/src/hooks/queries/useStandardizerSchemas.ts
new file mode 100644
index 00000000..3af42ed2
--- /dev/null
+++ b/web/src/hooks/queries/useStandardizerSchemas.ts
@@ -0,0 +1,18 @@
+import { useQuery, UseQueryResult } from '@tanstack/react-query';
+import { getStandardizerSchemas } from '../../api/namespace';
+
+export const useStandardizerSchemas = (
+ namespace: string | undefined,
+): UseQueryResult => {
+
+ return useQuery({
+ queryKey: ['standardizerSchemas', namespace],
+ queryFn: () => {
+ if (!namespace) {
+ throw new Error('Namespace is required');
+ }
+ return getStandardizerSchemas(namespace);
+ },
+ enabled: !!namespace,
+ });
+};
\ No newline at end of file
diff --git a/web/src/hooks/stores/useStandardizeModalStore.ts b/web/src/hooks/stores/useStandardizeModalStore.ts
new file mode 100644
index 00000000..e85cc09f
--- /dev/null
+++ b/web/src/hooks/stores/useStandardizeModalStore.ts
@@ -0,0 +1,11 @@
+import { create } from 'zustand';
+
+type StandardizeModalStore = {
+ showStandardizeMetadataModal: boolean;
+ setShowStandardizeMetadataModal: (show: boolean) => void;
+};
+
+export const useStandardizeModalStore = create((set) => ({
+ showStandardizeMetadataModal: false,
+ setShowStandardizeMetadataModal: (show) => set({ showStandardizeMetadataModal: show }),
+}));
diff --git a/web/src/main.tsx b/web/src/main.tsx
index dde58be4..2bda3764 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';
@@ -26,7 +27,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';
@@ -71,9 +72,15 @@ const router = createBrowserRouter([
),
},
+ {
+ path: '/browse',
+ element: ,
+ },
{
path: '/schemas',
- element: ,
+ loader: async ({ params }) => {
+ return redirect(`/browse?view=schemas`);
+ },
},
{
path: '/schemas/:namespace',
diff --git a/web/src/pages/Browse.tsx b/web/src/pages/Browse.tsx
new file mode 100644
index 00000000..61ba38fd
--- /dev/null
+++ b/web/src/pages/Browse.tsx
@@ -0,0 +1,229 @@
+import { useRef, useState } from 'react';
+import Nav from 'react-bootstrap/Nav';
+import { NavLink, useParams, useSearchParams } from 'react-router-dom';
+
+import { NamespaceGrid } from '../components/browse/namespace-grid';
+import { NamespaceLongRow } from '../components/browse/namespace-long-row';
+import { ProjectAccordion } from '../components/browse/project-accordion';
+import { PageLayout } from '../components/layout/page-layout';
+import { Markdown } from '../components/markdown/render';
+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 { LoadingSpinner } from '../components/spinners/loading-spinner';
+import { useAllSchemas } from '../hooks/queries/useAllSchemas';
+import { useBiggestNamespace } from '../hooks/queries/useBiggestNamespace';
+import { useNamespaceProjects } from '../hooks/queries/useNamespaceProjects';
+import { useDebounce } from '../hooks/useDebounce';
+
+type View = 'peps' | 'schemas';
+
+const NoSchemas = () => {
+ return (
+
+ );
+};
+
+export function Browse() {
+ const [searchParams, setSearchParams] = useSearchParams();
+ const viewFromUrl = searchParams.get('view') as View;
+
+ const [view, setView] = useState(viewFromUrl || 'peps');
+
+ 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(undefined);
+
+ const searchDebounced = useDebounce(search, 500);
+
+ const namespaces = useBiggestNamespace(25);
+ const topNamespace = useNamespaceProjects(selectedNamespace, {
+ limit: 10,
+ offset: 0,
+ orderBy: 'stars',
+ order: 'asc',
+ search: '',
+ type: 'pep',
+ });
+
+ const handleSelectNamespace = (selectedNamespace: string) => {
+ setSelectedNamespace((prevSelectedNamespace: string | undefined) =>
+ prevSelectedNamespace === selectedNamespace ? undefined : 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)}
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+ {view === 'peps' ? (
+ <>
+
+ {selectedNamespace === undefined ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+
+
+ {topNamespace?.data?.results && !topNamespace?.isFetching ? (
+ <>
+
+
+
+ Want to see more? Visit the namespace to view remaining projects.
+
+ >
+ ) : selectedNamespace && (
+
+
+
+ )}
+
+
+ >
+ ) : (
+
+
+
+ {noSchemasInDatabase ? (
+
+ ) : (
+
+ {schemas?.results.map((s, i) => (
+
+ ))}
+
+ )}
+
+
+ )}
+
+ {
+ setShowCreateSchemaModal(false);
+ }}
+ />
+
+ );
+}
diff --git a/web/src/pages/Home.tsx b/web/src/pages/Home.tsx
index 6f3dc6db..696e8ed4 100644
--- a/web/src/pages/Home.tsx
+++ b/web/src/pages/Home.tsx
@@ -96,9 +96,9 @@ export function Home() {
largestNamespaces.results.map((namespace, index) => {
return (
@@ -140,7 +140,7 @@ export function Home() {
view detailed information about these metadata, and create projects.
-