From bad3812057d091b67df1a68db54ed610d5673c05 Mon Sep 17 00:00:00 2001 From: matthewpeterkort Date: Thu, 25 Jun 2026 15:51:01 -0700 Subject: [PATCH 01/22] add storage summary page --- packages/core/src/features/gecko/geckoApi.ts | 104 +- packages/core/src/features/syfon/syfonApi.ts | 25 + packages/core/src/features/syfon/types.ts | 6 + .../src/components/Modals/LoginModal.tsx | 2 +- .../components/Protected/ProtectedContent.tsx | 4 +- .../Navigation/ProjectWorkspaceTabs.tsx | 199 +- .../src/features/Navigation/Sidebar.tsx | 69 +- .../presentationPage.unit.test.tsx | 51 +- packages/frontend/src/lib/session/session.tsx | 50 + .../src/pages/FileSummary/FileSummary.tsx | 1865 +++++++++++++++-- .../frontend/src/pages/FileSummary/hooks.tsx | 1672 +++++++++++++-- .../src/pages/FileSummary/hooks.unit.test.ts | 114 + .../src/pages/FileSummary/storageUtils.ts | 81 + .../frontend/src/pages/FileSummary/types.ts | 31 +- .../src/pages/GitExplorer/GitProject.tsx | 3 + .../GitExplorer/GitProject.unit.test.tsx | 27 +- .../OrganizationProject.tsx | 330 +-- .../OrganizationProject.unit.test.tsx | 57 +- .../frontend/src/types/gen3-core-gecko.d.ts | 34 + .../frontend/src/types/gen3-core-syfon.d.ts | 11 + .../config/datacommons/navigation.json | 6 - .../sampleCommons/config/gen3/navigation.json | 6 - .../org/[org]/project/[project]/storage.tsx | 7 + 23 files changed, 4030 insertions(+), 724 deletions(-) create mode 100644 packages/frontend/src/pages/FileSummary/hooks.unit.test.ts create mode 100644 packages/frontend/src/pages/FileSummary/storageUtils.ts create mode 100644 packages/sampleCommons/src/pages/org/[org]/project/[project]/storage.tsx diff --git a/packages/core/src/features/gecko/geckoApi.ts b/packages/core/src/features/gecko/geckoApi.ts index c632bd72..2d2a08ca 100644 --- a/packages/core/src/features/gecko/geckoApi.ts +++ b/packages/core/src/features/gecko/geckoApi.ts @@ -272,6 +272,18 @@ export interface GeckoGitTreeResponse { readonly project_id: string; readonly ref: string; readonly path: string; + readonly entry_count: number; + readonly truncated?: boolean; + readonly entries: Array; +} + +export interface GeckoGitManifestResponse { + readonly project_id: string; + readonly ref: string; + readonly path: string; + readonly entry_count: number; + readonly has_more: boolean; + readonly next_cursor?: string; readonly entries: Array; } @@ -993,11 +1005,47 @@ export const geckoApi = geckoTaggedApi.injectEndpoints({ project: string; path?: string; ref?: string; + include_size?: boolean; + include_last_modified?: boolean; + include_lfs_pointer?: boolean; + view?: 'manifest'; + limit?: number; } >({ - query: ({ organization, project, path, ref }) => { + query: ({ + organization, + project, + path, + ref, + include_size, + include_last_modified, + include_lfs_pointer, + view, + limit, + }) => { const normalizedPath = path?.trim().replace(/^\/+|\/+$/g, ''); - const query = ref ? `?ref=${encodeURIComponent(ref)}` : ''; + const queryParams = new URLSearchParams(); + if (ref) { + queryParams.set('ref', ref); + } + if (typeof include_size === 'boolean') { + queryParams.set('include_size', String(include_size)); + } + if (typeof include_last_modified === 'boolean') { + queryParams.set('include_last_modified', String(include_last_modified)); + } + if (typeof include_lfs_pointer === 'boolean') { + queryParams.set('include_lfs_pointer', String(include_lfs_pointer)); + } + if (view) { + queryParams.set('view', view); + } + if (typeof limit === 'number') { + queryParams.set('limit', String(limit)); + } + const query = queryParams.toString() + ? `?${queryParams.toString()}` + : ''; const suffix = normalizedPath ? `/tree/${normalizedPath .split('/') @@ -1053,6 +1101,58 @@ export const geckoApi = geckoTaggedApi.injectEndpoints({ credentials: 'include', }), }), + getGeckoGitProjectManifest: builder.query< + GeckoGitManifestResponse, + { + organization: string; + project: string; + path?: string; + ref?: string; + cursor?: string; + files_only?: boolean; + limit?: number; + } + >({ + query: ({ + organization, + project, + path, + ref, + cursor, + files_only, + limit, + }) => { + const normalizedPath = path?.trim().replace(/^\/+|\/+$/g, ''); + const queryParams = new URLSearchParams(); + if (ref) { + queryParams.set('ref', ref); + } + if (cursor) { + queryParams.set('cursor', cursor); + } + if (typeof files_only === 'boolean') { + queryParams.set('files_only', String(files_only)); + } + if (typeof limit === 'number') { + queryParams.set('limit', String(limit)); + } + const query = queryParams.toString() + ? `?${queryParams.toString()}` + : ''; + const suffix = normalizedPath + ? `/manifest/${normalizedPath + .split('/') + .map((segment) => encodeURIComponent(segment)) + .join('/')}${query}` + : `/manifest${query}`; + + return { + url: buildGitProjectApiPath(organization, project, suffix), + method: 'GET', + credentials: 'include', + }; + }, + }), getGeckoGitUploadSession: builder.query< GeckoGitUploadSessionResponse, { diff --git a/packages/core/src/features/syfon/syfonApi.ts b/packages/core/src/features/syfon/syfonApi.ts index cbcbd30d..5f2dda64 100644 --- a/packages/core/src/features/syfon/syfonApi.ts +++ b/packages/core/src/features/syfon/syfonApi.ts @@ -3,6 +3,7 @@ import { SYFON_API, SYFON_DRS_API } from '../../constants'; import { SyfonCompleteMultipartUploadArgs, SyfonBucketsResponse, + SyfonBulkDeleteDrsObjectsArgs, SyfonCreateUploadUrlArgs, SyfonDeleteBucketScopeArgs, SyfonDeleteProjectArgs, @@ -209,6 +210,29 @@ export const syfonApi = syfonTags.injectEndpoints({ url: `${SYFON_DRS_API}/objects/${objectId}`, }), }), + bulkDeleteSyfonDrsObjects: builder.mutation< + void, + SyfonBulkDeleteDrsObjectsArgs + >({ + invalidatesTags: ['SyfonDrsObject'], + query: ({ + bulk_object_ids, + delete_object_metadata, + delete_storage_data, + }) => ({ + body: { + bulk_object_ids, + delete_object_metadata, + delete_storage_data, + }, + method: 'PUT', + responseHandler: async (response: Response) => { + await response.text(); + return null; + }, + url: `${SYFON_DRS_API}/objects/delete`, + }), + }), getSyfonDownloadUrl: builder.query({ providesTags: (_result, _error, objectId) => [ { id: objectId, type: 'SyfonDrsObject' }, @@ -531,6 +555,7 @@ export const { useLazyGetSyfonObjectsByChecksumQuery, useRegisterSyfonDrsObjectsMutation, useDeleteSyfonDrsObjectMutation, + useBulkDeleteSyfonDrsObjectsMutation, useGetSyfonDownloadUrlQuery, useLazyGetSyfonDownloadUrlQuery, useCreateSyfonUploadUrlMutation, diff --git a/packages/core/src/features/syfon/types.ts b/packages/core/src/features/syfon/types.ts index d67ad8d9..632c9929 100644 --- a/packages/core/src/features/syfon/types.ts +++ b/packages/core/src/features/syfon/types.ts @@ -75,6 +75,12 @@ export interface SyfonDrsRegisterResponse { objects: Array; } +export interface SyfonBulkDeleteDrsObjectsArgs { + bulk_object_ids: Array; + delete_object_metadata?: boolean; + delete_storage_data?: boolean; +} + export interface SyfonSignedUrlResponse { url?: string; } diff --git a/packages/frontend/src/components/Modals/LoginModal.tsx b/packages/frontend/src/components/Modals/LoginModal.tsx index 1ac19e6f..8846e1d0 100644 --- a/packages/frontend/src/components/Modals/LoginModal.tsx +++ b/packages/frontend/src/components/Modals/LoginModal.tsx @@ -19,7 +19,7 @@ export const LoginView = ({ }: { redirectPath?: string; }): JSX.Element => ( -
+
{ } return ( -
-
+
+
Protected Content diff --git a/packages/frontend/src/features/Navigation/ProjectWorkspaceTabs.tsx b/packages/frontend/src/features/Navigation/ProjectWorkspaceTabs.tsx index 1296c282..cdeb0e57 100644 --- a/packages/frontend/src/features/Navigation/ProjectWorkspaceTabs.tsx +++ b/packages/frontend/src/features/Navigation/ProjectWorkspaceTabs.tsx @@ -1,8 +1,8 @@ -import React, { useEffect, useMemo, useState } from 'react'; -import { Tabs } from '@mantine/core'; +import React, { useMemo } from 'react'; +import Link from 'next/link'; import { useGetAuthzMappingsQuery, userHasMethodForServiceOnResource } from '@gen3/core'; -type ProjectWorkspaceTabKey = 'git' | 'presentation' | 'explorer'; +type ProjectWorkspaceTabKey = 'git' | 'presentation' | 'explorer' | 'storage'; interface ProjectWorkspaceTabsProps { readonly activeTab: ProjectWorkspaceTabKey; @@ -13,91 +13,9 @@ interface ProjectWorkspaceTabsProps { readonly gitHref?: string; readonly presentationHref?: string; readonly explorerHref?: string; + readonly storageHref?: string; } -interface EmbeddedRoutePanelProps { - readonly active: boolean; - readonly src: string; - readonly title: string; -} - -const buildEmbeddedHref = (href: string) => - `${href}${href.includes('?') ? '&' : '?'}embed=1`; - -const EmbeddedRoutePanel = ({ - active, - src, - title, -}: EmbeddedRoutePanelProps) => { - const iframeRef = React.useRef(null); - const resizeObserverRef = React.useRef(null); - const [iframeHeight, setIframeHeight] = useState(0); - - const measureIframe = React.useCallback(() => { - const iframe = iframeRef.current; - const doc = iframe?.contentDocument; - if (!doc) { - return; - } - - const bodyHeight = doc.body?.scrollHeight ?? 0; - const documentHeight = doc.documentElement?.scrollHeight ?? 0; - const nextHeight = Math.ceil(Math.max(bodyHeight, documentHeight)); - - setIframeHeight((currentHeight) => - currentHeight !== nextHeight ? nextHeight : currentHeight, - ); - }, []); - - const attachIframeMeasurement = React.useCallback(() => { - const iframe = iframeRef.current; - const doc = iframe?.contentDocument; - if (!doc) { - return; - } - - doc.defaultView?.removeEventListener('resize', measureIframe); - resizeObserverRef.current?.disconnect(); - measureIframe(); - - if (typeof ResizeObserver !== 'undefined') { - const observer = new ResizeObserver(measureIframe); - if (doc.body) { - observer.observe(doc.body); - } - if (doc.documentElement) { - observer.observe(doc.documentElement); - } - resizeObserverRef.current = observer; - } - - doc.defaultView?.addEventListener('resize', measureIframe); - void doc.fonts?.ready.then(measureIframe); - }, [measureIframe]); - - useEffect( - () => () => { - const iframe = iframeRef.current; - const doc = iframe?.contentDocument; - doc?.defaultView?.removeEventListener('resize', measureIframe); - resizeObserverRef.current?.disconnect(); - }, - [measureIframe], - ); - - return ( -