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

![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)
This commit is contained in:
CatsJuice
2024-04-01 07:41:19 +00:00
parent af2158cb0c
commit f4e1e23120
8 changed files with 192 additions and 96 deletions
@@ -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',
});
@@ -0,0 +1,77 @@
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 { useMemo } from 'react';
import * as styles from './storage-progress.css';
export interface StorageProgressProgress {
upgradable?: boolean;
onUpgrade: () => void;
}
enum ButtonType {
Primary = 'primary',
Default = 'default',
}
export const StorageProgress = ({
upgradable = true,
onUpgrade,
}: StorageProgressProgress) => {
const t = useAFFiNEI18N();
const { plan, usedText, color, percent, maxLimitText } =
useCloudStorageUsage();
const buttonType = useMemo(() => {
if (plan === SubscriptionPlan.Free) {
return ButtonType.Primary;
}
return ButtonType.Default;
}, [plan]);
return (
<div className={styles.storageProgressContainer}>
<div className={styles.storageProgressWrapper}>
<div className="storage-progress-desc">
<span>{t['com.affine.storage.used.hint']()}</span>
<span>
{usedText}/{maxLimitText}
{` (${plan} ${t['com.affine.storage.plan']()})`}
</span>
</div>
<div className="storage-progress-bar-wrapper">
<div
className={styles.storageProgressBar}
style={{ width: `${percent}%`, backgroundColor: color }}
></div>
</div>
</div>
{upgradable ? (
<Tooltip
options={{ hidden: percent < 100 }}
content={
plan === 'Free'
? t['com.affine.storage.maximum-tips']()
: t['com.affine.storage.maximum-tips.pro']()
}
>
<span tabIndex={0}>
<Button
type={buttonType}
onClick={onUpgrade}
className={styles.storageButton}
>
{plan === 'Free'
? t['com.affine.storage.upgrade']()
: t['com.affine.storage.change-plan']()}
</Button>
</span>
</Tooltip>
) : null}
</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>&nbsp;/&nbsp;</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 />
</>
);