mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-05 03:19:52 +08:00
feat(core): add cloud usage in sidebar avatar menu (#6400)
- Extract logic of getting cloud storage usage information into new hook - Move `<StorageProgress />`: `component` → `core` - Set minimum progress `0.5%` - Add cloud usage progress bar in sidebar user avatar's dropdown 
This commit is contained in:
@@ -1,6 +1,5 @@
|
|||||||
export { SettingHeader } from './setting-header';
|
export { SettingHeader } from './setting-header';
|
||||||
export { SettingRow } from './setting-row';
|
export { SettingRow } from './setting-row';
|
||||||
export * from './storage-progess';
|
|
||||||
export * from './workspace-detail-skeleton';
|
export * from './workspace-detail-skeleton';
|
||||||
export * from './workspace-list-skeleton';
|
export * from './workspace-list-skeleton';
|
||||||
export { SettingWrapper } from './wrapper';
|
export { SettingWrapper } from './wrapper';
|
||||||
|
|||||||
@@ -85,39 +85,3 @@ globalStyle(`${settingRow} .right-col`, {
|
|||||||
paddingLeft: '15px',
|
paddingLeft: '15px',
|
||||||
flexShrink: 0,
|
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',
|
|
||||||
});
|
|
||||||
|
|||||||
+3
-30
@@ -3,25 +3,20 @@ import { pushNotificationAtom } from '@affine/component/notification-center';
|
|||||||
import {
|
import {
|
||||||
SettingHeader,
|
SettingHeader,
|
||||||
SettingRow,
|
SettingRow,
|
||||||
StorageProgress,
|
|
||||||
} from '@affine/component/setting-components';
|
} from '@affine/component/setting-components';
|
||||||
import { Avatar } from '@affine/component/ui/avatar';
|
import { Avatar } from '@affine/component/ui/avatar';
|
||||||
import { Button } from '@affine/component/ui/button';
|
import { Button } from '@affine/component/ui/button';
|
||||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||||
import { useUserQuota } from '@affine/core/hooks/use-quota';
|
|
||||||
import {
|
import {
|
||||||
allBlobSizesQuery,
|
|
||||||
removeAvatarMutation,
|
removeAvatarMutation,
|
||||||
SubscriptionPlan,
|
|
||||||
updateUserProfileMutation,
|
updateUserProfileMutation,
|
||||||
uploadAvatarMutation,
|
uploadAvatarMutation,
|
||||||
} from '@affine/graphql';
|
} from '@affine/graphql';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { ArrowRightSmallIcon, CameraIcon } from '@blocksuite/icons';
|
import { ArrowRightSmallIcon, CameraIcon } from '@blocksuite/icons';
|
||||||
import bytes from 'bytes';
|
|
||||||
import { useSetAtom } from 'jotai';
|
import { useSetAtom } from 'jotai';
|
||||||
import type { FC, MouseEvent } from 'react';
|
import type { FC, MouseEvent } from 'react';
|
||||||
import { Suspense, useCallback, useMemo, useState } from 'react';
|
import { Suspense, useCallback, useState } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
authAtom,
|
authAtom,
|
||||||
@@ -31,11 +26,10 @@ import {
|
|||||||
import { useCurrentUser } from '../../../../hooks/affine/use-current-user';
|
import { useCurrentUser } from '../../../../hooks/affine/use-current-user';
|
||||||
import { useServerFeatures } from '../../../../hooks/affine/use-server-config';
|
import { useServerFeatures } from '../../../../hooks/affine/use-server-config';
|
||||||
import { useMutation } from '../../../../hooks/use-mutation';
|
import { useMutation } from '../../../../hooks/use-mutation';
|
||||||
import { useQuery } from '../../../../hooks/use-query';
|
|
||||||
import { useUserSubscription } from '../../../../hooks/use-subscription';
|
|
||||||
import { mixpanel } from '../../../../utils';
|
import { mixpanel } from '../../../../utils';
|
||||||
import { validateAndReduceImage } from '../../../../utils/reduce-image';
|
import { validateAndReduceImage } from '../../../../utils/reduce-image';
|
||||||
import { Upload } from '../../../pure/file-upload';
|
import { Upload } from '../../../pure/file-upload';
|
||||||
|
import { StorageProgress } from './storage-progress';
|
||||||
import * as styles from './style.css';
|
import * as styles from './style.css';
|
||||||
|
|
||||||
export const UserAvatar = () => {
|
export const UserAvatar = () => {
|
||||||
@@ -190,21 +184,6 @@ const StoragePanel = () => {
|
|||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const { payment: hasPaymentFeature } = useServerFeatures();
|
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 setSettingModalAtom = useSetAtom(openSettingModalAtom);
|
||||||
const onUpgrade = useCallback(() => {
|
const onUpgrade = useCallback(() => {
|
||||||
mixpanel.track('Button', {
|
mixpanel.track('Button', {
|
||||||
@@ -222,13 +201,7 @@ const StoragePanel = () => {
|
|||||||
desc=""
|
desc=""
|
||||||
spreadCol={false}
|
spreadCol={false}
|
||||||
>
|
>
|
||||||
<StorageProgress
|
<StorageProgress onUpgrade={onUpgrade} upgradable={hasPaymentFeature} />
|
||||||
max={maxLimit}
|
|
||||||
plan={plan}
|
|
||||||
value={data.collectAllBlobSizes.size}
|
|
||||||
onUpgrade={onUpgrade}
|
|
||||||
upgradable={hasPaymentFeature}
|
|
||||||
/>
|
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+33
@@ -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',
|
||||||
|
});
|
||||||
+8
-23
@@ -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 { SubscriptionPlan } from '@affine/graphql';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import bytes from 'bytes';
|
|
||||||
import clsx from 'clsx';
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { Button } from '../../ui/button';
|
import * as styles from './storage-progress.css';
|
||||||
import { Tooltip } from '../../ui/tooltip';
|
|
||||||
import * as styles from './share.css';
|
|
||||||
|
|
||||||
export interface StorageProgressProgress {
|
export interface StorageProgressProgress {
|
||||||
max: number;
|
|
||||||
value: number;
|
|
||||||
upgradable?: boolean;
|
upgradable?: boolean;
|
||||||
onUpgrade: () => void;
|
onUpgrade: () => void;
|
||||||
plan: SubscriptionPlan;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ButtonType {
|
enum ButtonType {
|
||||||
@@ -22,20 +17,12 @@ enum ButtonType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const StorageProgress = ({
|
export const StorageProgress = ({
|
||||||
max: upperLimit,
|
|
||||||
value,
|
|
||||||
upgradable = true,
|
upgradable = true,
|
||||||
onUpgrade,
|
onUpgrade,
|
||||||
plan,
|
|
||||||
}: StorageProgressProgress) => {
|
}: StorageProgressProgress) => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const percent = useMemo(
|
const { plan, usedText, color, percent, maxLimitText } =
|
||||||
() => Math.round((value / upperLimit) * 100),
|
useCloudStorageUsage();
|
||||||
[upperLimit, value]
|
|
||||||
);
|
|
||||||
|
|
||||||
const used = useMemo(() => bytes.format(value), [value]);
|
|
||||||
const max = useMemo(() => bytes.format(upperLimit), [upperLimit]);
|
|
||||||
|
|
||||||
const buttonType = useMemo(() => {
|
const buttonType = useMemo(() => {
|
||||||
if (plan === SubscriptionPlan.Free) {
|
if (plan === SubscriptionPlan.Free) {
|
||||||
@@ -50,17 +37,15 @@ export const StorageProgress = ({
|
|||||||
<div className="storage-progress-desc">
|
<div className="storage-progress-desc">
|
||||||
<span>{t['com.affine.storage.used.hint']()}</span>
|
<span>{t['com.affine.storage.used.hint']()}</span>
|
||||||
<span>
|
<span>
|
||||||
{used}/{max}
|
{usedText}/{maxLimitText}
|
||||||
{` (${plan} ${t['com.affine.storage.plan']()})`}
|
{` (${plan} ${t['com.affine.storage.plan']()})`}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="storage-progress-bar-wrapper">
|
<div className="storage-progress-bar-wrapper">
|
||||||
<div
|
<div
|
||||||
className={clsx(styles.storageProgressBar, {
|
className={styles.storageProgressBar}
|
||||||
danger: percent > 80,
|
style={{ width: `${percent}%`, backgroundColor: color }}
|
||||||
})}
|
|
||||||
style={{ width: `${percent > 100 ? '100' : percent}%` }}
|
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -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({
|
export const workspaceAndUserWrapper = style({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -23,3 +26,46 @@ export const userInfoWrapper = style({
|
|||||||
globalStyle(`button.${userInfoWrapper} > span`, {
|
globalStyle(`button.${userInfoWrapper} > span`, {
|
||||||
lineHeight: 0,
|
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,
|
||||||
|
});
|
||||||
|
|||||||
@@ -12,12 +12,18 @@ import {
|
|||||||
openSettingModalAtom,
|
openSettingModalAtom,
|
||||||
openSignOutModalAtom,
|
openSignOutModalAtom,
|
||||||
} from '@affine/core/atoms';
|
} from '@affine/core/atoms';
|
||||||
|
import { useCloudStorageUsage } from '@affine/core/hooks/affine/use-cloud-storage-usage';
|
||||||
import {
|
import {
|
||||||
useCurrentUser,
|
useCurrentUser,
|
||||||
useSession,
|
useSession,
|
||||||
} from '@affine/core/hooks/affine/use-current-user';
|
} from '@affine/core/hooks/affine/use-current-user';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
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 { useSetAtom } from 'jotai';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
@@ -92,6 +98,11 @@ const AccountMenu = () => {
|
|||||||
<AccountIcon />
|
<AccountIcon />
|
||||||
</MenuIcon>
|
</MenuIcon>
|
||||||
}
|
}
|
||||||
|
endFix={
|
||||||
|
<MenuIcon position="end">
|
||||||
|
<ArrowRightSmallIcon />
|
||||||
|
</MenuIcon>
|
||||||
|
}
|
||||||
data-testid="workspace-modal-account-settings-option"
|
data-testid="workspace-modal-account-settings-option"
|
||||||
onClick={onOpenAccountSetting}
|
onClick={onOpenAccountSetting}
|
||||||
>
|
>
|
||||||
@@ -104,6 +115,11 @@ const AccountMenu = () => {
|
|||||||
<SignOutIcon />
|
<SignOutIcon />
|
||||||
</MenuIcon>
|
</MenuIcon>
|
||||||
}
|
}
|
||||||
|
endFix={
|
||||||
|
<MenuIcon position="end">
|
||||||
|
<ArrowRightSmallIcon />
|
||||||
|
</MenuIcon>
|
||||||
|
}
|
||||||
data-testid="workspace-modal-sign-out-option"
|
data-testid="workspace-modal-sign-out-option"
|
||||||
onClick={onOpenSignOutModal}
|
onClick={onOpenSignOutModal}
|
||||||
>
|
>
|
||||||
@@ -113,13 +129,37 @@ const AccountMenu = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const OperationMenu = () => {
|
const CloudUsage = () => {
|
||||||
// TODO: display usage progress bar
|
const { color, usedText, maxLimitText, percent } = useCloudStorageUsage();
|
||||||
const StorageUsage = null;
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles.cloudUsage}
|
||||||
|
style={assignInlineVars({
|
||||||
|
[styles.progressColorVar]: color,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div className={styles.cloudUsageLabel}>
|
||||||
|
<span className={styles.cloudUsageLabelUsed}>{usedText}</span>
|
||||||
|
<span> / </span>
|
||||||
|
<span>{maxLimitText}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.cloudUsageBar}>
|
||||||
|
<div
|
||||||
|
className={styles.cloudUsageBarInner}
|
||||||
|
style={{ width: `${percent}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const OperationMenu = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{StorageUsage}
|
<CloudUsage />
|
||||||
|
<Divider />
|
||||||
<AccountMenu />
|
<AccountMenu />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user