Skip to content

Commit

Permalink
Merge pull request #1304 from jay-hodgson/SWC-7063
Browse files Browse the repository at this point in the history
  • Loading branch information
jay-hodgson authored Oct 22, 2024
2 parents ba2898c + aa2e4fd commit 6f28e96
Show file tree
Hide file tree
Showing 14 changed files with 314 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import React from 'react'
import { Box, SxProps, Tooltip, Typography } from '@mui/material'
import { useSynapseContext } from '../../utils'
import { useProjectStorageUsage } from '../../synapse-queries'
import { SYNAPSE_STORAGE_LOCATION_ID } from '../../synapse-client'
import HelpPopover from '../HelpPopover'
import { calculateFriendlyFileSize } from '../../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
if (maxAllowedFileBytes == 0) {
return <></>
}
const usageBarFilledPx = Math.min(
Math.round((sumFileBytes / maxAllowedFileBytes) * usageBarWidth),
usageBarWidth,
)
const friendlySumFileBytes = calculateFriendlyFileSize(sumFileBytes, 1)
const friendlyMaxAllowedFileBytes = calculateFriendlyFileSize(
maxAllowedFileBytes,
0,
)
return (
<Box
display="flex"
flexDirection="column"
width="210px"
fontFamily="DM Sans"
color="white"
px="10px"
sx={sx}
>
<Box display="flex" flexDirection="row" gap="5px">
<Typography
sx={{
// match current styles in Project metadata
fontWeight: 700,
fontSize: '16px',
}}
>
Data Availability{' '}
</Typography>{' '}
<HelpPopover
markdownText="Hosting Plan Options:
- Basic Plan: Free, for sharing small datasets (<100GB) with self-service setup. No direct support.
- Self-Managed Plan: Ideal for data longevity, FAIR principles, and NIH compliance. Includes consultation services and data access management tools.
- Data Coordination Plan: For large, multi-institutional projects, with personalized consulting, data curation, and a custom data portal."
helpUrl="https://help.synapse.org/docs/Sage-Offerings.2965078125.html"
/>
</Box>
{synapseStorageUsage.maxAllowedFileBytes && (
<Tooltip
title={`Using ${friendlySumFileBytes} out of ${friendlyMaxAllowedFileBytes}`}
>
<Box display="flex" flexDirection="row" gap="5px" alignItems="center">
<Typography variant="body1" fontSize="12px">
0
</Typography>
{/* Progress Bar */}
<Box
width={`${usageBarWidth}px`}
height="4px"
sx={{ backgroundColor: 'white', borderRadius: '50px' }}
>
<Box
width={`${usageBarFilledPx}px`}
height="4px"
sx={{ backgroundColor: '#EDC766', borderRadius: '50px' }}
></Box>
</Box>
<Typography variant="body1" fontSize="12px">
{friendlyMaxAllowedFileBytes}
</Typography>
</Box>
</Tooltip>
)}
</Box>
)
}

export default ProjectDataAvailability
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
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,
argTypes: {
isAuthenticated: {
type: 'boolean',
},
},
args: {
isAuthenticated: true,
},
} satisfies Meta
export default meta
type Story = StoryObj<typeof meta>

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),
],
},
},
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import ProjectDataAvailability from './ProjectDataAvailability'
import type { ProjectDataAvailabilityProps } from './ProjectDataAvailability'
export { ProjectDataAvailability, ProjectDataAvailabilityProps }
export default ProjectDataAvailability
1 change: 1 addition & 0 deletions packages/synapse-react-client/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
@@ -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))
},
),
]
Original file line number Diff line number Diff line change
@@ -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: false,
},
],
}
15 changes: 15 additions & 0 deletions packages/synapse-react-client/src/synapse-client/SynapseClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ import {
WIKI_PAGE_ID,
TERMS_OF_USE_INFO,
TERMS_OF_USE_STATUS,
PROJECT_STORAGE_USAGE,
} from '../utils/APIConstants'
import { dispatchDownloadListChangeEvent } from '../utils/functions/dispatchDownloadListChangeEvent'
import { BackendDestinationEnum, getEndpoint } from '../utils/functions'
Expand Down Expand Up @@ -333,6 +334,7 @@ import {
TermsOfServiceInfo,
TermsOfServiceStatus,
AccessToken,
ProjectStorageUsage,
} from '@sage-bionetworks/synapse-types'
import { calculateFriendlyFileSize } from '../utils/functions/calculateFriendlyFileSize'
import {
Expand Down Expand Up @@ -5593,3 +5595,16 @@ export const getTermsOfServiceStatus = (
{ signal },
)
}

export const getProjectStorageUsage = (
projectId: string,
accessToken: string | undefined = undefined,
signal?: AbortSignal,
): Promise<ProjectStorageUsage> => {
return doGet<ProjectStorageUsage>(
PROJECT_STORAGE_USAGE(projectId),
accessToken,
BackendDestinationEnum.REPO_ENDPOINT,
{ signal },
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -881,6 +881,10 @@ export class KeyFactory {
public getTermsOfServiceStatus() {
return this.getKey('termsOfServiceStatus')
}

public getProjectStorageUsageKey(projectId: string) {
return this.getKey('projectstorageusage', projectId)
}
public getPaginatedDockerTagQueryKey(
id: string,
offset: string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export * from './useGetEntityForum'
export * from './useGetEntityHeaders'
export * from './useGetQueryResultBundle'
export * from './useSchema'
export * from './useProjectStorage'
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Hooks to access Project Storage in Synapse
*/
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
import SynapseClient from '../../synapse-client'
import { SynapseClientError, useSynapseContext } from '../../utils'
import { ProjectStorageUsage } from '@sage-bionetworks/synapse-types'

export function useProjectStorageUsage(
projectId: string,
options?: Partial<UseQueryOptions<ProjectStorageUsage, SynapseClientError>>,
) {
const { accessToken, keyFactory } = useSynapseContext()
return useQuery({
...options,
queryKey: keyFactory.getProjectStorageUsageKey(projectId),
queryFn: () => SynapseClient.getProjectStorageUsage(projectId, accessToken),
})
}
3 changes: 3 additions & 0 deletions packages/synapse-react-client/src/utils/APIConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@ export const REGISTER_ACCOUNT_STEP_2 = `${REPO}/account2`
export const TERMS_OF_USE = `${AUTH}/termsOfUse2`
export const TERMS_OF_USE_INFO = `${TERMS_OF_USE}/info`
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 VERIFICATION_SUBMISSION = `${REPO}/verificationSubmission`
export const VERIFICATION_SUBMISSION_STATE = (id: string) =>
`${VERIFICATION_SUBMISSION}/${id}/state`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ const sufixes: string[] = [
'YB',
]

export function calculateFriendlyFileSize(bytes: number) {
export function calculateFriendlyFileSize(
bytes: number,
fractionDigits?: number,
) {
if (!bytes) {
return ''
}
Expand All @@ -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]
)
}
17 changes: 17 additions & 0 deletions packages/synapse-types/src/ProjectStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export type ProjectStorageUsage = {
projectId: string // The ID of the project
locations: ProjectStorageLocationUsage[] // List of storage location usages
}

export type ProjectStorageLocationUsage = {
storageLocationId: string // The ID of the storage location
sumFileBytes: number // The total number of bytes of files currently associated with this project storage location
maxAllowedFileBytes?: number // The total number of allowed bytes for this project storage location (optional)
isOverLimit: boolean // Whether the project storage location is over its limit
}

export type ProjectStorageLocationLimit = {
projectId: string // The ID of the project
storageLocationId: string // The ID of the storage location
maxAllowedFileBytes: string // Sets the limit on the number of file bytes for this storage location
}
1 change: 1 addition & 0 deletions packages/synapse-types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export * from './PaginatedResults'
export * from './PartPresignedUrl'
export * from './Principal'
export * from './ProjectHeader'
export * from './ProjectStorage'
export * from './Provenance'
export * from './ReferenceList'
export * from './ResourceAccess'
Expand Down

0 comments on commit 6f28e96

Please sign in to comment.