From f4e1e231206eb26951eef0f3c0b5ee156faa1b9a Mon Sep 17 00:00:00 2001 From: CatsJuice Date: Mon, 1 Apr 2024 07:41:19 +0000 Subject: [PATCH] feat(core): add cloud usage in sidebar avatar menu (#6400) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Extract logic of getting cloud storage usage information into new hook - Move ``: `component` → `core` - Set minimum progress `0.5%` - Add cloud usage progress bar in sidebar user avatar's dropdown ![CleanShot 2024-03-29 at 17.10.04@2x.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/LakojjjzZNf6ogjOVwKE/1fe9371a-a870-49a1-b4bb-b923c2fa4fe6.png) --- .../components/setting-components/index.tsx | 1 - .../setting-components/share.css.ts | 36 ------------ .../setting-modal/account-setting/index.tsx | 33 +---------- .../account-setting/storage-progress.css.ts | 33 +++++++++++ .../account-setting/storage-progress.tsx} | 31 +++------- .../components/root-app-sidebar/index.css.ts | 48 +++++++++++++++- .../components/root-app-sidebar/user-info.tsx | 50 +++++++++++++++-- .../hooks/affine/use-cloud-storage-usage.ts | 56 +++++++++++++++++++ 8 files changed, 192 insertions(+), 96 deletions(-) create mode 100644 packages/frontend/core/src/components/affine/setting-modal/account-setting/storage-progress.css.ts rename packages/frontend/{component/src/components/setting-components/storage-progess.tsx => core/src/components/affine/setting-modal/account-setting/storage-progress.tsx} (70%) create mode 100644 packages/frontend/core/src/hooks/affine/use-cloud-storage-usage.ts diff --git a/packages/frontend/component/src/components/setting-components/index.tsx b/packages/frontend/component/src/components/setting-components/index.tsx index 3cf81c0fbe..d26bc5d939 100644 --- a/packages/frontend/component/src/components/setting-components/index.tsx +++ b/packages/frontend/component/src/components/setting-components/index.tsx @@ -1,6 +1,5 @@ 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/frontend/component/src/components/setting-components/share.css.ts b/packages/frontend/component/src/components/setting-components/share.css.ts index 2b06a4361f..223d5b7933 100644 --- a/packages/frontend/component/src/components/setting-components/share.css.ts +++ b/packages/frontend/component/src/components/setting-components/share.css.ts @@ -85,39 +85,3 @@ 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: cssVar('fontXs'), - color: cssVar('textSecondaryColor'), - height: '20px', - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: 2, -}); -globalStyle(`${storageProgressWrapper} .storage-progress-bar-wrapper`, { - height: '8px', - borderRadius: '4px', - backgroundColor: cssVar('black10'), - overflow: 'hidden', -}); -export const storageProgressBar = style({ - height: '100%', - backgroundColor: cssVar('processingColor'), - selectors: { - '&.danger': { - backgroundColor: cssVar('errorColor'), - }, - }, -}); -export const storageButton = style({ - padding: '4px 12px', -}); diff --git a/packages/frontend/core/src/components/affine/setting-modal/account-setting/index.tsx b/packages/frontend/core/src/components/affine/setting-modal/account-setting/index.tsx index 1f95dcec7b..5276e5b0af 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/account-setting/index.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/account-setting/index.tsx @@ -3,25 +3,20 @@ import { pushNotificationAtom } from '@affine/component/notification-center'; import { SettingHeader, SettingRow, - StorageProgress, } from '@affine/component/setting-components'; import { Avatar } from '@affine/component/ui/avatar'; import { Button } from '@affine/component/ui/button'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; -import { useUserQuota } from '@affine/core/hooks/use-quota'; import { - allBlobSizesQuery, removeAvatarMutation, - SubscriptionPlan, updateUserProfileMutation, uploadAvatarMutation, } from '@affine/graphql'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { ArrowRightSmallIcon, CameraIcon } from '@blocksuite/icons'; -import bytes from 'bytes'; import { useSetAtom } from 'jotai'; import type { FC, MouseEvent } from 'react'; -import { Suspense, useCallback, useMemo, useState } from 'react'; +import { Suspense, useCallback, useState } from 'react'; import { authAtom, @@ -31,11 +26,10 @@ import { import { useCurrentUser } from '../../../../hooks/affine/use-current-user'; import { useServerFeatures } from '../../../../hooks/affine/use-server-config'; import { useMutation } from '../../../../hooks/use-mutation'; -import { useQuery } from '../../../../hooks/use-query'; -import { useUserSubscription } from '../../../../hooks/use-subscription'; import { mixpanel } from '../../../../utils'; import { validateAndReduceImage } from '../../../../utils/reduce-image'; import { Upload } from '../../../pure/file-upload'; +import { StorageProgress } from './storage-progress'; import * as styles from './style.css'; export const UserAvatar = () => { @@ -190,21 +184,6 @@ const StoragePanel = () => { const t = useAFFiNEI18N(); const { payment: hasPaymentFeature } = useServerFeatures(); - const { data } = useQuery({ - query: allBlobSizesQuery, - }); - - const [subscription] = useUserSubscription(); - const plan = subscription?.plan ?? SubscriptionPlan.Free; - - const quota = useUserQuota(); - const maxLimit = useMemo(() => { - if (quota) { - return quota.storageQuota; - } - return bytes.parse(plan === SubscriptionPlan.Free ? '10GB' : '100GB'); - }, [plan, quota]); - const setSettingModalAtom = useSetAtom(openSettingModalAtom); const onUpgrade = useCallback(() => { mixpanel.track('Button', { @@ -222,13 +201,7 @@ const StoragePanel = () => { desc="" spreadCol={false} > - + ); }; diff --git a/packages/frontend/core/src/components/affine/setting-modal/account-setting/storage-progress.css.ts b/packages/frontend/core/src/components/affine/setting-modal/account-setting/storage-progress.css.ts new file mode 100644 index 0000000000..a6eb95b317 --- /dev/null +++ b/packages/frontend/core/src/components/affine/setting-modal/account-setting/storage-progress.css.ts @@ -0,0 +1,33 @@ +import { cssVar } from '@toeverything/theme'; +import { globalStyle, style } from '@vanilla-extract/css'; + +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: cssVar('fontXs'), + color: cssVar('textSecondaryColor'), + height: '20px', + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 2, +}); +globalStyle(`${storageProgressWrapper} .storage-progress-bar-wrapper`, { + height: '8px', + borderRadius: '4px', + backgroundColor: cssVar('black10'), + overflow: 'hidden', +}); +export const storageProgressBar = style({ + height: '100%', +}); +export const storageButton = style({ + padding: '4px 12px', +}); diff --git a/packages/frontend/component/src/components/setting-components/storage-progess.tsx b/packages/frontend/core/src/components/affine/setting-modal/account-setting/storage-progress.tsx similarity index 70% rename from packages/frontend/component/src/components/setting-components/storage-progess.tsx rename to packages/frontend/core/src/components/affine/setting-modal/account-setting/storage-progress.tsx index e5cb1c7053..2594e96335 100644 --- a/packages/frontend/component/src/components/setting-components/storage-progess.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/account-setting/storage-progress.tsx @@ -1,19 +1,14 @@ +import { Button, Tooltip } from '@affine/component'; +import { useCloudStorageUsage } from '@affine/core/hooks/affine/use-cloud-storage-usage'; import { SubscriptionPlan } from '@affine/graphql'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; -import bytes from 'bytes'; -import clsx from 'clsx'; import { useMemo } from 'react'; -import { Button } from '../../ui/button'; -import { Tooltip } from '../../ui/tooltip'; -import * as styles from './share.css'; +import * as styles from './storage-progress.css'; export interface StorageProgressProgress { - max: number; - value: number; upgradable?: boolean; onUpgrade: () => void; - plan: SubscriptionPlan; } enum ButtonType { @@ -22,20 +17,12 @@ enum ButtonType { } export const StorageProgress = ({ - max: upperLimit, - value, upgradable = true, onUpgrade, - plan, }: StorageProgressProgress) => { const t = useAFFiNEI18N(); - const percent = useMemo( - () => Math.round((value / upperLimit) * 100), - [upperLimit, value] - ); - - const used = useMemo(() => bytes.format(value), [value]); - const max = useMemo(() => bytes.format(upperLimit), [upperLimit]); + const { plan, usedText, color, percent, maxLimitText } = + useCloudStorageUsage(); const buttonType = useMemo(() => { if (plan === SubscriptionPlan.Free) { @@ -50,17 +37,15 @@ export const StorageProgress = ({
{t['com.affine.storage.used.hint']()} - {used}/{max} + {usedText}/{maxLimitText} {` (${plan} ${t['com.affine.storage.plan']()})`}
80, - })} - style={{ width: `${percent > 100 ? '100' : percent}%` }} + className={styles.storageProgressBar} + style={{ width: `${percent}%`, backgroundColor: color }} >
diff --git a/packages/frontend/core/src/components/root-app-sidebar/index.css.ts b/packages/frontend/core/src/components/root-app-sidebar/index.css.ts index 684f679db5..3c1d6d782a 100644 --- a/packages/frontend/core/src/components/root-app-sidebar/index.css.ts +++ b/packages/frontend/core/src/components/root-app-sidebar/index.css.ts @@ -1,4 +1,7 @@ -import { globalStyle, style } from '@vanilla-extract/css'; +import { cssVar } from '@toeverything/theme'; +import { createVar, globalStyle, style } from '@vanilla-extract/css'; + +export const progressColorVar = createVar(); export const workspaceAndUserWrapper = style({ display: 'flex', @@ -23,3 +26,46 @@ export const userInfoWrapper = style({ globalStyle(`button.${userInfoWrapper} > span`, { lineHeight: 0, }); + +export const operationMenu = style({ + display: 'flex', + flexDirection: 'column', + gap: 4, +}); + +export const cloudUsage = style({ + display: 'flex', + flexDirection: 'column', + gap: 4, + padding: '4px 12px', +}); +export const cloudUsageLabel = style({ + fontWeight: 500, + lineHeight: '20px', + fontSize: cssVar('fontXs'), + color: cssVar('textSecondaryColor'), +}); +export const cloudUsageLabelUsed = style({ + color: progressColorVar, +}); + +export const cloudUsageBar = style({ + height: 10, + borderRadius: 5, + overflow: 'hidden', + position: 'relative', + minWidth: 160, + + '::before': { + position: 'absolute', + inset: 0, + content: '""', + backgroundColor: cssVar('black'), + opacity: 0.04, + }, +}); +export const cloudUsageBarInner = style({ + height: '100%', + borderRadius: 'inherit', + backgroundColor: progressColorVar, +}); diff --git a/packages/frontend/core/src/components/root-app-sidebar/user-info.tsx b/packages/frontend/core/src/components/root-app-sidebar/user-info.tsx index f1b8b127a6..54c53eb45b 100644 --- a/packages/frontend/core/src/components/root-app-sidebar/user-info.tsx +++ b/packages/frontend/core/src/components/root-app-sidebar/user-info.tsx @@ -12,12 +12,18 @@ import { openSettingModalAtom, openSignOutModalAtom, } from '@affine/core/atoms'; +import { useCloudStorageUsage } from '@affine/core/hooks/affine/use-cloud-storage-usage'; import { useCurrentUser, useSession, } from '@affine/core/hooks/affine/use-current-user'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; -import { AccountIcon, SignOutIcon } from '@blocksuite/icons'; +import { + AccountIcon, + ArrowRightSmallIcon, + SignOutIcon, +} from '@blocksuite/icons'; +import { assignInlineVars } from '@vanilla-extract/dynamic'; import { useSetAtom } from 'jotai'; import { useCallback } from 'react'; @@ -92,6 +98,11 @@ const AccountMenu = () => { } + endFix={ + + + + } data-testid="workspace-modal-account-settings-option" onClick={onOpenAccountSetting} > @@ -104,6 +115,11 @@ const AccountMenu = () => { } + endFix={ + + + + } data-testid="workspace-modal-sign-out-option" onClick={onOpenSignOutModal} > @@ -113,13 +129,37 @@ const AccountMenu = () => { ); }; -const OperationMenu = () => { - // TODO: display usage progress bar - const StorageUsage = null; +const CloudUsage = () => { + const { color, usedText, maxLimitText, percent } = useCloudStorageUsage(); + return ( +
+
+ {usedText} +  /  + {maxLimitText} +
+ +
+
+
+
+ ); +}; + +const OperationMenu = () => { return ( <> - {StorageUsage} + + ); diff --git a/packages/frontend/core/src/hooks/affine/use-cloud-storage-usage.ts b/packages/frontend/core/src/hooks/affine/use-cloud-storage-usage.ts new file mode 100644 index 0000000000..f0439c0830 --- /dev/null +++ b/packages/frontend/core/src/hooks/affine/use-cloud-storage-usage.ts @@ -0,0 +1,56 @@ +import { allBlobSizesQuery, SubscriptionPlan } from '@affine/graphql'; +import { cssVar } from '@toeverything/theme'; +import bytes from 'bytes'; +import { useMemo } from 'react'; + +import { useQuery } from '../use-query'; +import { useUserQuota } from '../use-quota'; +import { useUserSubscription } from '../use-subscription'; + +/** + * Hook to get currentUser's cloud storage usage information. + */ +export const useCloudStorageUsage = () => { + const { data } = useQuery({ + query: allBlobSizesQuery, + }); + + const quota = useUserQuota(); + const [subscription] = useUserSubscription(); + + const plan = subscription?.plan ?? SubscriptionPlan.Free; + + const maxLimit = useMemo(() => { + if (quota) { + return quota.storageQuota; + } + return bytes.parse(plan === SubscriptionPlan.Free ? '10GB' : '100GB'); + }, [plan, quota]); + + const used = data?.collectAllBlobSizes?.size ?? 0; + const percent = Math.min( + 100, + Math.max(0.5, Number(((used / maxLimit) * 100).toFixed(4))) + ); + const color = percent > 80 ? cssVar('errorColor') : cssVar('processingColor'); + + const usedText = bytes.format(used); + const maxLimitText = bytes.format(maxLimit); + + return { + /** Current subscription plan of logged in user */ + plan, + /** Used storage in bytes */ + used, + /** Formatted used storage */ + usedText, + /** CSS variable name for progress bar color */ + color, + /** Percentage of storage used */ + percent, + /** Maximum storage limit in bytes */ + maxLimit, + /** Formatted maximum storage limit */ + maxLimitText, + }; +};