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] ) }