mirror of
https://github.com/langgenius/dify.git
synced 2026-03-19 05:37:42 +08:00
feat(billing): enhance usage info with storage threshold display
- Add storageThreshold, storageTooltip, storageTotalDisplay props to UsageInfo - Implement indeterminate state in ProgressBar for usage below threshold - Update VectorSpaceInfo to calculate total based on plan type - Add i18n for storage threshold tooltip (en-US, ja-JP, zh-Hans)
This commit is contained in:
@ -3,12 +3,23 @@ import { cn } from '@/utils/classnames'
|
||||
type ProgressBarProps = {
|
||||
percent: number
|
||||
color: string
|
||||
indeterminate?: boolean
|
||||
}
|
||||
|
||||
const ProgressBar = ({
|
||||
percent = 0,
|
||||
color = '#2970FF',
|
||||
indeterminate = false,
|
||||
}: ProgressBarProps) => {
|
||||
if (indeterminate) {
|
||||
return (
|
||||
<div
|
||||
data-testid="billing-progress-bar-indeterminate"
|
||||
className="h-1 overflow-hidden rounded-[6px] bg-components-progress-bar-bg"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden rounded-[6px] bg-components-progress-bar-bg">
|
||||
<div
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { ComponentType, FC } from 'react'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
@ -9,7 +9,7 @@ import ProgressBar from '../progress-bar'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
Icon: any
|
||||
Icon: ComponentType<{ className?: string }>
|
||||
name: string
|
||||
tooltip?: string
|
||||
usage: number
|
||||
@ -19,6 +19,11 @@ type Props = {
|
||||
resetHint?: string
|
||||
resetInDays?: number
|
||||
hideIcon?: boolean
|
||||
// Props for the 50MB threshold display logic
|
||||
storageThreshold?: number
|
||||
storageTooltip?: string
|
||||
storageTotalDisplay?: string // e.g., "5GB" or "50MB" for formatted display
|
||||
isSandboxPlan?: boolean
|
||||
}
|
||||
|
||||
const WARNING_THRESHOLD = 80
|
||||
@ -35,30 +40,150 @@ const UsageInfo: FC<Props> = ({
|
||||
resetHint,
|
||||
resetInDays,
|
||||
hideIcon = false,
|
||||
storageThreshold = 50,
|
||||
storageTooltip,
|
||||
storageTotalDisplay,
|
||||
isSandboxPlan = false,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
// Special display logic for usage below threshold
|
||||
const isBelowThreshold = usage < storageThreshold
|
||||
// Sandbox at full capacity (usage >= threshold and it's sandbox plan)
|
||||
const isSandboxFull = isSandboxPlan && usage >= storageThreshold
|
||||
|
||||
const percent = usage / total * 100
|
||||
const color = percent >= 100
|
||||
? 'bg-components-progress-error-progress'
|
||||
: (percent >= WARNING_THRESHOLD ? 'bg-components-progress-warning-progress' : 'bg-components-progress-bar-progress-solid')
|
||||
const getProgressColor = () => {
|
||||
if (percent >= 100)
|
||||
return 'bg-components-progress-error-progress'
|
||||
if (percent >= WARNING_THRESHOLD)
|
||||
return 'bg-components-progress-warning-progress'
|
||||
return 'bg-components-progress-bar-progress-solid'
|
||||
}
|
||||
const color = getProgressColor()
|
||||
const isUnlimited = total === NUM_INFINITE
|
||||
let totalDisplay: string | number = isUnlimited ? t('plansCommon.unlimited', { ns: 'billing' }) : total
|
||||
if (!isUnlimited && unit && unitPosition === 'inline')
|
||||
totalDisplay = `${total}${unit}`
|
||||
const showUnit = !!unit && !isUnlimited && unitPosition === 'suffix'
|
||||
const resetText = resetHint ?? (typeof resetInDays === 'number' ? t('usagePage.resetsIn', { ns: 'billing', count: resetInDays }) : undefined)
|
||||
const rightInfo = resetText
|
||||
? (
|
||||
|
||||
const renderRightInfo = () => {
|
||||
if (resetText) {
|
||||
return (
|
||||
<div className="system-xs-regular ml-auto flex-1 text-right text-text-tertiary">
|
||||
{resetText}
|
||||
</div>
|
||||
)
|
||||
: (showUnit && (
|
||||
}
|
||||
if (showUnit) {
|
||||
return (
|
||||
<div className="system-xs-medium ml-auto text-text-tertiary">
|
||||
{unit}
|
||||
</div>
|
||||
))
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// Render usage display
|
||||
const renderUsageDisplay = () => {
|
||||
// Sandbox user at full capacity
|
||||
if (isSandboxFull) {
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
<span>
|
||||
{storageThreshold}
|
||||
</span>
|
||||
<span className="system-md-regular text-text-quaternary">/</span>
|
||||
<span>
|
||||
{storageThreshold}
|
||||
{' '}
|
||||
{unit}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
// Usage below threshold - show "< 50 MB" or "< 50 / 5GB"
|
||||
if (isBelowThreshold) {
|
||||
const totalText = storageTotalDisplay || totalDisplay
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
<span>
|
||||
<
|
||||
{' '}
|
||||
{storageThreshold}
|
||||
</span>
|
||||
{!isSandboxPlan && (
|
||||
<>
|
||||
<span className="system-md-regular text-text-quaternary">/</span>
|
||||
<span>{totalText}</span>
|
||||
</>
|
||||
)}
|
||||
{isSandboxPlan && <span>{unit}</span>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
// Pro/Team users with usage >= threshold - show actual usage
|
||||
const totalText = storageTotalDisplay || totalDisplay
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
<span>{usage}</span>
|
||||
<span className="system-md-regular text-text-quaternary">/</span>
|
||||
<span>{totalText}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Render progress bar with optional tooltip wrapper
|
||||
const renderProgressBar = () => {
|
||||
const progressBar = (
|
||||
<ProgressBar
|
||||
percent={isBelowThreshold ? 0 : percent}
|
||||
color={isSandboxFull ? 'bg-components-progress-error-progress' : color}
|
||||
indeterminate={isBelowThreshold}
|
||||
/>
|
||||
)
|
||||
|
||||
if (storageTooltip) {
|
||||
return (
|
||||
<Tooltip
|
||||
popupContent={(
|
||||
<div className="w-[200px]">
|
||||
{storageTooltip}
|
||||
</div>
|
||||
)}
|
||||
asChild={false}
|
||||
>
|
||||
<div className="cursor-default">{progressBar}</div>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
return progressBar
|
||||
}
|
||||
|
||||
// Render usage text with optional tooltip wrapper
|
||||
const renderUsageWithTooltip = () => {
|
||||
const usageDisplay = renderUsageDisplay()
|
||||
|
||||
if (storageTooltip) {
|
||||
return (
|
||||
<Tooltip
|
||||
popupContent={(
|
||||
<div className="w-[200px]">
|
||||
{storageTooltip}
|
||||
</div>
|
||||
)}
|
||||
asChild={false}
|
||||
>
|
||||
<div className="cursor-default">{usageDisplay}</div>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
return usageDisplay
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('flex flex-col gap-2 rounded-xl bg-components-panel-bg p-4', className)}>
|
||||
@ -78,17 +203,10 @@ const UsageInfo: FC<Props> = ({
|
||||
)}
|
||||
</div>
|
||||
<div className="system-md-semibold flex items-center gap-1 text-text-primary">
|
||||
<div className="flex items-center gap-1">
|
||||
{usage}
|
||||
<div className="system-md-regular text-text-quaternary">/</div>
|
||||
<div>{totalDisplay}</div>
|
||||
</div>
|
||||
{rightInfo}
|
||||
{renderUsageWithTooltip()}
|
||||
{renderRightInfo()}
|
||||
</div>
|
||||
<ProgressBar
|
||||
percent={percent}
|
||||
color={color}
|
||||
/>
|
||||
{renderProgressBar()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -6,21 +6,44 @@ import {
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { Plan } from '../type'
|
||||
import UsageInfo from '../usage-info'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
}
|
||||
|
||||
// Storage threshold in MB - usage below this shows as "< 50 MB"
|
||||
const STORAGE_THRESHOLD_MB = 50
|
||||
|
||||
const VectorSpaceInfo: FC<Props> = ({
|
||||
className,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { plan } = useProviderContext()
|
||||
const {
|
||||
type,
|
||||
usage,
|
||||
total,
|
||||
} = plan
|
||||
|
||||
// Determine total based on plan type (in MB)
|
||||
const getTotalInMB = () => {
|
||||
switch (type) {
|
||||
case Plan.sandbox:
|
||||
return STORAGE_THRESHOLD_MB // 50 MB
|
||||
case Plan.professional:
|
||||
return 5 * 1024 // 5 GB = 5120 MB
|
||||
case Plan.team:
|
||||
return 20 * 1024 // 20 GB = 20480 MB
|
||||
default:
|
||||
return total.vectorSpace
|
||||
}
|
||||
}
|
||||
|
||||
const totalInMB = getTotalInMB()
|
||||
const isSandbox = type === Plan.sandbox
|
||||
|
||||
return (
|
||||
<UsageInfo
|
||||
className={className}
|
||||
@ -28,9 +51,13 @@ const VectorSpaceInfo: FC<Props> = ({
|
||||
name={t('usagePage.vectorSpace', { ns: 'billing' })}
|
||||
tooltip={t('usagePage.vectorSpaceTooltip', { ns: 'billing' }) as string}
|
||||
usage={usage.vectorSpace}
|
||||
total={total.vectorSpace}
|
||||
total={totalInMB}
|
||||
unit="MB"
|
||||
unitPosition="inline"
|
||||
storageThreshold={STORAGE_THRESHOLD_MB}
|
||||
storageTooltip={t('usagePage.storageThresholdTooltip', { ns: 'billing' }) as string}
|
||||
storageTotalDisplay={`${totalInMB}MB`}
|
||||
isSandboxPlan={isSandbox}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1691,11 +1691,6 @@
|
||||
"count": 3
|
||||
}
|
||||
},
|
||||
"app/components/billing/usage-info/index.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"app/components/custom/custom-web-app-brand/index.spec.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 7
|
||||
|
||||
@ -172,6 +172,7 @@
|
||||
"usagePage.documentsUploadQuota": "Documents Upload Quota",
|
||||
"usagePage.perMonth": "per month",
|
||||
"usagePage.resetsIn": "Resets in {{count,number}} days",
|
||||
"usagePage.storageThresholdTooltip": "Detailed usage is shown once storage exceeds 50 MB.",
|
||||
"usagePage.teamMembers": "Team Members",
|
||||
"usagePage.triggerEvents": "Trigger Events",
|
||||
"usagePage.vectorSpace": "Knowledge Data Storage",
|
||||
|
||||
@ -172,6 +172,7 @@
|
||||
"usagePage.documentsUploadQuota": "ドキュメント・アップロード・クォータ",
|
||||
"usagePage.perMonth": "月あたり",
|
||||
"usagePage.resetsIn": "{{count,number}}日後にリセット",
|
||||
"usagePage.storageThresholdTooltip": "ストレージ使用量が 50 MB を超えると、詳細な使用状況が表示されます。",
|
||||
"usagePage.teamMembers": "チームメンバー",
|
||||
"usagePage.triggerEvents": "トリガーイベント数",
|
||||
"usagePage.vectorSpace": "ナレッジベースのデータストレージ",
|
||||
|
||||
@ -172,6 +172,7 @@
|
||||
"usagePage.documentsUploadQuota": "文档上传配额",
|
||||
"usagePage.perMonth": "每月",
|
||||
"usagePage.resetsIn": "{{count,number}} 天后重置",
|
||||
"usagePage.storageThresholdTooltip": "存储空间超过 50 MB 后,将显示详细使用情况。",
|
||||
"usagePage.teamMembers": "团队成员",
|
||||
"usagePage.triggerEvents": "触发器事件数",
|
||||
"usagePage.vectorSpace": "知识库数据存储空间",
|
||||
|
||||
Reference in New Issue
Block a user