diff --git a/apps/core/src/components/affine/setting-modal/account-setting/index.tsx b/apps/core/src/components/affine/setting-modal/account-setting/index.tsx index c2dc20c763..54286a7753 100644 --- a/apps/core/src/components/affine/setting-modal/account-setting/index.tsx +++ b/apps/core/src/components/affine/setting-modal/account-setting/index.tsx @@ -2,15 +2,16 @@ import { FlexWrapper, Input } from '@affine/component'; import { SettingHeader, SettingRow, + StorageProgress, } from '@affine/component/setting-components'; import { UserAvatar } from '@affine/component/user-avatar'; -import { uploadAvatarMutation } from '@affine/graphql'; +import { allBlobSizesQuery, uploadAvatarMutation } from '@affine/graphql'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; -import { useMutation } from '@affine/workspace/affine/gql'; +import { useMutation, useQuery } from '@affine/workspace/affine/gql'; import { ArrowRightSmallIcon, CameraIcon, DoneIcon } from '@blocksuite/icons'; import { Button, IconButton } from '@toeverything/components/button'; -import { useAtom } from 'jotai/index'; -import { type FC, useCallback, useState } from 'react'; +import { useAtom } from 'jotai'; +import { type FC, Suspense, useCallback, useState } from 'react'; import { authAtom } from '../../../../atoms'; import { useCurrentUser } from '../../../../hooks/affine/use-current-user'; @@ -109,6 +110,30 @@ export const AvatarAndName = () => { ); }; +const StoragePanel = () => { + const t = useAFFiNEI18N(); + + const { data } = useQuery({ + query: allBlobSizesQuery, + }); + + const onUpgrade = useCallback(() => {}, []); + + return ( + + + + ); +}; + export const AccountSetting: FC = () => { const t = useAFFiNEI18N(); const user = useCurrentUser(); @@ -154,7 +179,9 @@ export const AccountSetting: FC = () => { : t['com.affine.settings.password.action.set']()} - + + + ({ size })); } + @Query(() => WorkspaceBlobSizes) + async collectAllBlobSizes(@CurrentUser() user: User) { + const workspaces = await this.workspaces(user); + + const size = ( + await Promise.all(workspaces.map(({ id }) => this.storage.blobsSize(id))) + ).reduce((prev, curr) => prev + curr, 0); + + return { size }; + } + @Mutation(() => String) async setBlob( @CurrentUser() user: UserType, diff --git a/apps/server/src/schema.gql b/apps/server/src/schema.gql index b03b160aea..09dda6bfdc 100644 --- a/apps/server/src/schema.gql +++ b/apps/server/src/schema.gql @@ -154,6 +154,7 @@ type Query { """List blobs of workspace""" listBlobs(workspaceId: String!): [String!]! collectBlobSizes(workspaceId: String!): WorkspaceBlobSizes! + collectAllBlobSizes: WorkspaceBlobSizes! """Get current user""" currentUser: UserType! diff --git a/packages/component/src/components/setting-components/index.tsx b/packages/component/src/components/setting-components/index.tsx index c444472a8c..ab57d4bbef 100644 --- a/packages/component/src/components/setting-components/index.tsx +++ b/packages/component/src/components/setting-components/index.tsx @@ -1,6 +1,7 @@ export { SettingModal, type SettingModalProps } from './modal'; export { SettingHeader } from './setting-header'; export { SettingRow } from './setting-row'; +export * from './storage-progess'; export * from './workspace-detail-skeleton'; export * from './workspace-list-skeleton'; export { SettingWrapper } from './wrapper'; diff --git a/packages/component/src/components/setting-components/share.css.ts b/packages/component/src/components/setting-components/share.css.ts index 51041d48b9..5c7f68804d 100644 --- a/packages/component/src/components/setting-components/share.css.ts +++ b/packages/component/src/components/setting-components/share.css.ts @@ -92,3 +92,55 @@ globalStyle(`${settingRow} .right-col`, { paddingLeft: '15px', flexShrink: 0, }); + +export const storageProgressContainer = style({ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', +}); + +export const storageProgressWrapper = style({ + flexGrow: 1, + marginRight: '20px', +}); + +globalStyle(`${storageProgressWrapper} .storage-progress-desc`, { + fontSize: 'var(--affine-font-xs)', + color: 'var(--affine-text-secondary-color)', + height: '20px', + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 2, +}); +globalStyle(`${storageProgressWrapper} .storage-progress-bar-wrapper`, { + height: '8px', + borderRadius: '4px', + backgroundColor: 'var(--affine-pure-black-10)', + overflow: 'hidden', +}); +export const storageProgressBar = style({ + height: '100%', + backgroundColor: 'var(--affine-processing-color)', + selectors: { + '&.warning': { + // Wait for design + backgroundColor: '#FF7C09', + }, + '&.danger': { + backgroundColor: 'var(--affine-error-color)', + }, + }, +}); +export const storageExtendHint = style({ + borderRadius: '4px', + padding: '4px 8px', + backgroundColor: 'var(--affine-background-secondary-color)', + color: 'var(--affine-text-secondary-color)', + fontSize: 'var(--affine-font-xs)', + lineHeight: '20px', + marginTop: 8, +}); +globalStyle(`${storageExtendHint} a`, { + color: 'var(--affine-link-color)', +}); diff --git a/packages/component/src/components/setting-components/storage-progess.tsx b/packages/component/src/components/setting-components/storage-progess.tsx new file mode 100644 index 0000000000..05dc50b50a --- /dev/null +++ b/packages/component/src/components/setting-components/storage-progess.tsx @@ -0,0 +1,87 @@ +import { useAFFiNEI18N } from '@affine/i18n/hooks'; +import { Button } from '@toeverything/components/button'; +import { Tooltip } from '@toeverything/components/tooltip'; +import clsx from 'clsx'; +import { useMemo, useRef } from 'react'; + +import * as styles from './share.css'; + +export interface StorageProgressProgress { + max: number; + value: number; + onUpgrade: () => void; +} + +const transformBytesToMB = (bytes: number) => { + return (bytes / 1024 / 1024).toFixed(2); +}; + +const transformBytesToGB = (bytes: number) => { + return (bytes / 1024 / 1024 / 1024).toFixed(2); +}; + +export const StorageProgress = ({ + max: upperLimit, + value, + onUpgrade, +}: StorageProgressProgress) => { + const t = useAFFiNEI18N(); + const ref = useRef(null); + const percent = useMemo( + () => Math.round((value / upperLimit) * 100), + [upperLimit, value] + ); + + const used = useMemo(() => transformBytesToMB(value), [value]); + const max = useMemo(() => transformBytesToGB(upperLimit), [upperLimit]); + + return ( + <> +
+
+
+ {t['com.affine.storage.used.hint']()} + + {used}MB/{max}GB + +
+ +
+
80, + danger: percent > 99, + })} + style={{ width: `${percent}%` }} + >
+
+
+ + +
+ +
+
+
+ {percent > 80 ? ( +
+ {t['com.affine.storage.extend.hint']()} + + {t['com.affine.storage.extend.link']()} + +
+ ) : null} + + ); +}; diff --git a/packages/graphql/src/graphql/blobs-size.gql b/packages/graphql/src/graphql/blobs-size.gql new file mode 100644 index 0000000000..8d5e90b3eb --- /dev/null +++ b/packages/graphql/src/graphql/blobs-size.gql @@ -0,0 +1,5 @@ +query allBlobSizes { + collectAllBlobSizes { + size + } +} diff --git a/packages/graphql/src/graphql/index.ts b/packages/graphql/src/graphql/index.ts index 66efd62d64..c1ee7d0b45 100644 --- a/packages/graphql/src/graphql/index.ts +++ b/packages/graphql/src/graphql/index.ts @@ -53,6 +53,19 @@ query blobSizes($workspaceId: String!) { }`, }; +export const allBlobSizesQuery = { + id: 'allBlobSizesQuery' as const, + operationName: 'allBlobSizes', + definitionName: 'collectAllBlobSizes', + containsFile: false, + query: ` +query allBlobSizes { + collectAllBlobSizes { + size + } +}`, +}; + export const changeEmailMutation = { id: 'changeEmailMutation' as const, operationName: 'changeEmail', diff --git a/packages/graphql/src/schema.ts b/packages/graphql/src/schema.ts index d132539e31..f8177becea 100644 --- a/packages/graphql/src/schema.ts +++ b/packages/graphql/src/schema.ts @@ -82,6 +82,13 @@ export type BlobSizesQuery = { collectBlobSizes: { __typename?: 'WorkspaceBlobSizes'; size: number }; }; +export type AllBlobSizesQueryVariables = Exact<{ [key: string]: never }>; + +export type AllBlobSizesQuery = { + __typename?: 'Query'; + collectAllBlobSizes: { __typename?: 'WorkspaceBlobSizes'; size: number }; +}; + export type ChangeEmailMutationVariables = Exact<{ id: Scalars['String']['input']; newEmail: Scalars['String']['input']; @@ -448,6 +455,11 @@ export type Queries = variables: BlobSizesQueryVariables; response: BlobSizesQuery; } + | { + name: 'allBlobSizesQuery'; + variables: AllBlobSizesQueryVariables; + response: AllBlobSizesQuery; + } | { name: 'getCurrentUserQuery'; variables: GetCurrentUserQueryVariables; diff --git a/packages/i18n/src/resources/en.json b/packages/i18n/src/resources/en.json index a1c8c45d8a..fffc637ca0 100644 --- a/packages/i18n/src/resources/en.json +++ b/packages/i18n/src/resources/en.json @@ -530,5 +530,11 @@ "com.affine.share-menu.ShareViaExportDescription": "Download a static copy of your page to share with others.", "com.affine.share-menu.ShareWithLink": "Share with link", "com.affine.share-menu.ShareWithLinkDescription": "Create a link you can easily share with anyone. The visitors will open your page in the form od a document", - "com.affine.share-menu.ShareMode": "Share mode" + "com.affine.share-menu.ShareMode": "Share mode", + "com.affine.storage.title": "AFFiNE Cloud Storage", + "com.affine.storage.upgrade": "Upgrade to Pro", + "com.affine.storage.disabled.hint": "AFFiNE Cloud is currently in early access phase and is not supported for upgrading, please be patient and wait for our pricing plan.", + "com.affine.storage.used.hint": "Space used", + "com.affine.storage.extend.hint": "The usage has reached its maximum capacity, AFFiNE Cloud is currently in early access phase and is not supported for upgrading, please be patient and wait for our pricing plan. ", + "com.affine.storage.extend.link": "To get more information click here." }