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

View File

@@ -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';

View File

@@ -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',
});

View File

@@ -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>
);
};

View File

@@ -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',
});

View File

@@ -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>

View File

@@ -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,
});

View File

@@ -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 />
</>
);

View File

@@ -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,
};
};