diff --git a/packages/synapse-react-client/src/components/ProjectStorage/ProjectDataAvailability.tsx b/packages/synapse-react-client/src/components/ProjectStorage/ProjectDataAvailability.tsx
new file mode 100644
index 0000000000..d44d3ad9c5
--- /dev/null
+++ b/packages/synapse-react-client/src/components/ProjectStorage/ProjectDataAvailability.tsx
@@ -0,0 +1,101 @@
+import React from 'react'
+import { Box, SxProps, Tooltip, Typography } from '@mui/material'
+import { useTheme } from '@mui/material'
+import { useSynapseContext } from '../../utils'
+import { useProjectStorageUsage } from '../../synapse-queries'
+import { SYNAPSE_STORAGE_LOCATION_ID } from 'src/synapse-client'
+import HelpPopover from '../HelpPopover'
+import { calculateFriendlyFileSize } from 'src/utils/functions/calculateFriendlyFileSize'
+
+export type ProjectDataAvailabilityProps = {
+ projectId?: string
+ sx?: SxProps
+}
+const usageBarWidth = 142 //px
+
+export const ProjectDataAvailability: React.FunctionComponent<
+ ProjectDataAvailabilityProps
+> = ({ projectId, sx }) => {
+ const { accessToken } = useSynapseContext()
+ const isLoggedIn = !!accessToken
+ const { data } = useProjectStorageUsage(projectId!, {
+ enabled: !!projectId && isLoggedIn,
+ })
+
+ const projectDataUsageArray = data?.locations.filter(
+ v => parseInt(v.storageLocationId) == SYNAPSE_STORAGE_LOCATION_ID,
+ )
+ const synapseStorageUsage =
+ projectDataUsageArray?.length == 1 ? projectDataUsageArray[0] : undefined
+ if (!synapseStorageUsage) {
+ return <>>
+ }
+ const { sumFileBytes = 0, maxAllowedFileBytes = 1 } = synapseStorageUsage
+ const usageBarFilledPx = Math.min(
+ Math.round((sumFileBytes / maxAllowedFileBytes) * usageBarWidth),
+ usageBarWidth,
+ )
+ const friendlySumFileBytes = calculateFriendlyFileSize(sumFileBytes, 1)
+ const friendlyMaxAllowedFileBytes = calculateFriendlyFileSize(
+ maxAllowedFileBytes,
+ 0,
+ )
+ return (
+
+
+
+ Data Availability{' '}
+ {' '}
+
+
+ {synapseStorageUsage.maxAllowedFileBytes && (
+
+
+
+ 0
+
+ {/* Progress Bar */}
+
+
+
+
+ {friendlyMaxAllowedFileBytes}
+
+
+
+ )}
+
+ )
+}
+
+export default ProjectDataAvailability
diff --git a/packages/synapse-react-client/src/components/ProjectStorage/ProjectStorage.stories.ts b/packages/synapse-react-client/src/components/ProjectStorage/ProjectStorage.stories.ts
new file mode 100644
index 0000000000..fbc0ad8853
--- /dev/null
+++ b/packages/synapse-react-client/src/components/ProjectStorage/ProjectStorage.stories.ts
@@ -0,0 +1,77 @@
+import { Meta, StoryObj } from '@storybook/react'
+import ProjectDataAvailability from './ProjectDataAvailability'
+import { MOCK_REPO_ORIGIN } from 'src/utils/functions/getEndpoint'
+import { getUserProfileHandlers } from 'src/mocks/msw/handlers/userProfileHandlers'
+import { getEntityHandlers } from 'src/mocks/msw/handlers/entityHandlers'
+import { getProjectStorageHandlers } from 'src/mocks/msw/handlers/projectStorageHandlers'
+import {
+ OVER_LIMIT_PROJECT_ID,
+ UNDER_LIMIT_PROJECT_ID,
+} from 'src/mocks/projectStorage/mockProjectStorageLimits'
+
+const meta = {
+ title: 'Synapse/ProjectStorage',
+ component: ProjectDataAvailability,
+ parameters: {
+ chromatic: { viewports: [600, 1200] },
+ },
+ argTypes: {
+ isAuthenticated: {
+ type: 'boolean',
+ },
+ },
+ args: {
+ isAuthenticated: true,
+ },
+} satisfies Meta
+export default meta
+type Story = StoryObj
+
+export const ProjectDataUnderLimit: Story = {
+ args: {
+ projectId: UNDER_LIMIT_PROJECT_ID,
+ sx: { backgroundColor: '#375574' },
+ },
+ parameters: {
+ stack: 'mock',
+ msw: {
+ handlers: [
+ ...getUserProfileHandlers(MOCK_REPO_ORIGIN),
+ ...getEntityHandlers(MOCK_REPO_ORIGIN),
+ ...getProjectStorageHandlers(MOCK_REPO_ORIGIN),
+ ],
+ },
+ },
+}
+
+export const ProjectDataOverLimit: Story = {
+ args: {
+ projectId: OVER_LIMIT_PROJECT_ID,
+ sx: { backgroundColor: '#375574' },
+ },
+
+ parameters: {
+ stack: 'mock',
+ msw: {
+ handlers: [
+ ...getUserProfileHandlers(MOCK_REPO_ORIGIN),
+ ...getEntityHandlers(MOCK_REPO_ORIGIN),
+ ...getProjectStorageHandlers(MOCK_REPO_ORIGIN),
+ ],
+ },
+ },
+}
+
+export const ProjectDataStorageNotSet: Story = {
+ args: { projectId: 'syn31415123' },
+ parameters: {
+ stack: 'mock',
+ msw: {
+ handlers: [
+ ...getUserProfileHandlers(MOCK_REPO_ORIGIN),
+ ...getEntityHandlers(MOCK_REPO_ORIGIN),
+ ...getProjectStorageHandlers(MOCK_REPO_ORIGIN),
+ ],
+ },
+ },
+}
diff --git a/packages/synapse-react-client/src/components/ProjectStorage/index.ts b/packages/synapse-react-client/src/components/ProjectStorage/index.ts
new file mode 100644
index 0000000000..46b8e38a1d
--- /dev/null
+++ b/packages/synapse-react-client/src/components/ProjectStorage/index.ts
@@ -0,0 +1,4 @@
+import ProjectDataAvailability from './ProjectDataAvailability'
+import type { ProjectDataAvailabilityProps } from './ProjectDataAvailability'
+export { ProjectDataAvailability, ProjectDataAvailabilityProps }
+export default ProjectDataAvailability
diff --git a/packages/synapse-react-client/src/components/index.ts b/packages/synapse-react-client/src/components/index.ts
index 5720db8de6..5d1caf1df6 100644
--- a/packages/synapse-react-client/src/components/index.ts
+++ b/packages/synapse-react-client/src/components/index.ts
@@ -43,6 +43,7 @@ export * from './ProjectViewCarousel'
export * from './Programs'
export * from './ProgrammaticTableDownload'
export * from './ProvenanceGraph'
+export * from './ProjectStorage'
export * from './QueryContext'
export * from './QueryCount'
export * from './QueryWrapper'
diff --git a/packages/synapse-react-client/src/mocks/msw/handlers/projectStorageHandlers.ts b/packages/synapse-react-client/src/mocks/msw/handlers/projectStorageHandlers.ts
new file mode 100644
index 0000000000..84f7d779fc
--- /dev/null
+++ b/packages/synapse-react-client/src/mocks/msw/handlers/projectStorageHandlers.ts
@@ -0,0 +1,26 @@
+import { rest } from 'msw'
+import { PROJECT_STORAGE_USAGE } from '../../../utils/APIConstants'
+import { BackendDestinationEnum, getEndpoint } from '../../../utils/functions'
+import {
+ mockProjectStorageUsageOverLimit,
+ mockProjectStorageUsageUnderLimit,
+ OVER_LIMIT_PROJECT_ID,
+ UNDER_LIMIT_PROJECT_ID,
+} from '../../../mocks/projectStorage/mockProjectStorageLimits'
+
+export const getProjectStorageHandlers = (
+ backendOrigin = getEndpoint(BackendDestinationEnum.REPO_ENDPOINT),
+) => [
+ rest.get(
+ `${backendOrigin}${PROJECT_STORAGE_USAGE(OVER_LIMIT_PROJECT_ID)}`,
+ async (req, res, ctx) => {
+ return res(ctx.status(201), ctx.json(mockProjectStorageUsageOverLimit))
+ },
+ ),
+ rest.get(
+ `${backendOrigin}${PROJECT_STORAGE_USAGE(UNDER_LIMIT_PROJECT_ID)}`,
+ async (req, res, ctx) => {
+ return res(ctx.status(201), ctx.json(mockProjectStorageUsageUnderLimit))
+ },
+ ),
+]
diff --git a/packages/synapse-react-client/src/mocks/projectStorage/mockProjectStorageLimits.ts b/packages/synapse-react-client/src/mocks/projectStorage/mockProjectStorageLimits.ts
new file mode 100644
index 0000000000..f990f8d928
--- /dev/null
+++ b/packages/synapse-react-client/src/mocks/projectStorage/mockProjectStorageLimits.ts
@@ -0,0 +1,41 @@
+import { ProjectStorageUsage } from '@sage-bionetworks/synapse-types'
+import { SYNAPSE_STORAGE_LOCATION_ID } from '../../synapse-client'
+
+export const OVER_LIMIT_PROJECT_ID = 'syn54321'
+export const UNDER_LIMIT_PROJECT_ID = 'syn12345'
+
+export const mockProjectStorageUsageOverLimit: ProjectStorageUsage = {
+ projectId: OVER_LIMIT_PROJECT_ID,
+ locations: [
+ {
+ storageLocationId: `${SYNAPSE_STORAGE_LOCATION_ID}`,
+ sumFileBytes: 1200000000, // 1.2 GB
+ maxAllowedFileBytes: 1073741824, // 1 GB limit
+ isOverLimit: true, // Over the limit
+ },
+ {
+ storageLocationId: 'location-2',
+ sumFileBytes: 100000000,
+ maxAllowedFileBytes: 1073741824,
+ isOverLimit: false,
+ },
+ ],
+}
+
+export const mockProjectStorageUsageUnderLimit: ProjectStorageUsage = {
+ projectId: UNDER_LIMIT_PROJECT_ID,
+ locations: [
+ {
+ storageLocationId: `${SYNAPSE_STORAGE_LOCATION_ID}`,
+ sumFileBytes: 500000000,
+ maxAllowedFileBytes: 1073741824,
+ isOverLimit: false, // Under the limit
+ },
+ {
+ storageLocationId: 'location-2',
+ sumFileBytes: 1000,
+ maxAllowedFileBytes: 1073741824,
+ isOverLimit: true,
+ },
+ ],
+}
diff --git a/packages/synapse-react-client/src/synapse-client/SynapseClient.ts b/packages/synapse-react-client/src/synapse-client/SynapseClient.ts
index 371aed706d..6ddda2e908 100644
--- a/packages/synapse-react-client/src/synapse-client/SynapseClient.ts
+++ b/packages/synapse-react-client/src/synapse-client/SynapseClient.ts
@@ -99,7 +99,6 @@ import {
TERMS_OF_USE_INFO,
TERMS_OF_USE_STATUS,
PROJECT_STORAGE_USAGE,
- PROJECT_STORAGE_LIMIT,
} from '../utils/APIConstants'
import { dispatchDownloadListChangeEvent } from '../utils/functions/dispatchDownloadListChangeEvent'
import { BackendDestinationEnum, getEndpoint } from '../utils/functions'
@@ -336,7 +335,6 @@ import {
TermsOfServiceStatus,
AccessToken,
ProjectStorageUsage,
- ProjectStorageLocationLimit,
} from '@sage-bionetworks/synapse-types'
import { calculateFriendlyFileSize } from '../utils/functions/calculateFriendlyFileSize'
import {
@@ -5610,17 +5608,3 @@ export const getProjectStorageUsage = (
{ signal },
)
}
-
-export const getProjectStorageLocationLimit = (
- request: ProjectStorageLocationLimit,
- accessToken: string | undefined = undefined,
- signal?: AbortSignal,
-): Promise => {
- return doPost(
- PROJECT_STORAGE_LIMIT(request.projectId),
- request,
- accessToken,
- BackendDestinationEnum.REPO_ENDPOINT,
- { signal },
- )
-}
diff --git a/packages/synapse-react-client/src/utils/APIConstants.ts b/packages/synapse-react-client/src/utils/APIConstants.ts
index 80fb9a1943..cdbf8bbcfb 100644
--- a/packages/synapse-react-client/src/utils/APIConstants.ts
+++ b/packages/synapse-react-client/src/utils/APIConstants.ts
@@ -100,8 +100,6 @@ export const TERMS_OF_USE_STATUS = `${TERMS_OF_USE}/status`
const PROJECT = (projectId: string) => `${REPO}/project/${projectId}`
export const PROJECT_STORAGE_USAGE = (projectId: string) =>
`${PROJECT(projectId)}/storage/usage`
-export const PROJECT_STORAGE_LIMIT = (projectId: string) =>
- `${PROJECT(projectId)}/storage/limit`
export const VERIFICATION_SUBMISSION = `${REPO}/verificationSubmission`
export const VERIFICATION_SUBMISSION_STATE = (id: string) =>
`${VERIFICATION_SUBMISSION}/${id}/state`
diff --git a/packages/synapse-react-client/src/utils/functions/calculateFriendlyFileSize.ts b/packages/synapse-react-client/src/utils/functions/calculateFriendlyFileSize.ts
index 67cdc01bc7..9778554527 100644
--- a/packages/synapse-react-client/src/utils/functions/calculateFriendlyFileSize.ts
+++ b/packages/synapse-react-client/src/utils/functions/calculateFriendlyFileSize.ts
@@ -10,7 +10,10 @@ const sufixes: string[] = [
'YB',
]
-export function calculateFriendlyFileSize(bytes: number) {
+export function calculateFriendlyFileSize(
+ bytes: number,
+ fractionDigits?: number,
+) {
if (!bytes) {
return ''
}
@@ -19,6 +22,6 @@ export function calculateFriendlyFileSize(bytes: number) {
// tslint:disable-next-line
return (
(!bytes && '0 Bytes') ||
- (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + sufixes[i]
+ (bytes / Math.pow(1024, i)).toFixed(fractionDigits ?? 2) + ' ' + sufixes[i]
)
}