mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 12:28:42 +00: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 { SettingRow } from './setting-row';
|
||||
export * from './storage-progess';
|
||||
export * from './workspace-detail-skeleton';
|
||||
export * from './workspace-list-skeleton';
|
||||
export { SettingWrapper } from './wrapper';
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
<StorageProgress
|
||||
max={maxLimit}
|
||||
plan={plan}
|
||||
value={data.collectAllBlobSizes.size}
|
||||
onUpgrade={onUpgrade}
|
||||
upgradable={hasPaymentFeature}
|
||||
/>
|
||||
<StorageProgress onUpgrade={onUpgrade} upgradable={hasPaymentFeature} />
|
||||
</SettingRow>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
@@ -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 = ({
|
||||
<div className="storage-progress-desc">
|
||||
<span>{t['com.affine.storage.used.hint']()}</span>
|
||||
<span>
|
||||
{used}/{max}
|
||||
{usedText}/{maxLimitText}
|
||||
{` (${plan} ${t['com.affine.storage.plan']()})`}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="storage-progress-bar-wrapper">
|
||||
<div
|
||||
className={clsx(styles.storageProgressBar, {
|
||||
danger: percent > 80,
|
||||
})}
|
||||
style={{ width: `${percent > 100 ? '100' : percent}%` }}
|
||||
className={styles.storageProgressBar}
|
||||
style={{ width: `${percent}%`, backgroundColor: color }}
|
||||
></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({
|
||||
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,
|
||||
});
|
||||
|
||||
@@ -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 = () => {
|
||||
<AccountIcon />
|
||||
</MenuIcon>
|
||||
}
|
||||
endFix={
|
||||
<MenuIcon position="end">
|
||||
<ArrowRightSmallIcon />
|
||||
</MenuIcon>
|
||||
}
|
||||
data-testid="workspace-modal-account-settings-option"
|
||||
onClick={onOpenAccountSetting}
|
||||
>
|
||||
@@ -104,6 +115,11 @@ const AccountMenu = () => {
|
||||
<SignOutIcon />
|
||||
</MenuIcon>
|
||||
}
|
||||
endFix={
|
||||
<MenuIcon position="end">
|
||||
<ArrowRightSmallIcon />
|
||||
</MenuIcon>
|
||||
}
|
||||
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 (
|
||||
<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 (
|
||||
<>
|
||||
{StorageUsage}
|
||||
<CloudUsage />
|
||||
<Divider />
|
||||
<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