mirror of
https://github.com/langgenius/dify.git
synced 2026-04-05 16:39:26 +08:00
feat: enhance ProgressBar and UsageInfo for storage mode (#31273)
Co-authored-by: CodingOnStar <hanxujiang@dify.ai>
This commit is contained in:
@@ -2,24 +2,61 @@ import { render, screen } from '@testing-library/react'
|
|||||||
import ProgressBar from './index'
|
import ProgressBar from './index'
|
||||||
|
|
||||||
describe('ProgressBar', () => {
|
describe('ProgressBar', () => {
|
||||||
it('renders with provided percent and color', () => {
|
describe('Normal Mode (determinate)', () => {
|
||||||
render(<ProgressBar percent={42} color="bg-test-color" />)
|
it('renders with provided percent and color', () => {
|
||||||
|
render(<ProgressBar percent={42} color="bg-test-color" />)
|
||||||
|
|
||||||
const bar = screen.getByTestId('billing-progress-bar')
|
const bar = screen.getByTestId('billing-progress-bar')
|
||||||
expect(bar).toHaveClass('bg-test-color')
|
expect(bar).toHaveClass('bg-test-color')
|
||||||
expect(bar.getAttribute('style')).toContain('width: 42%')
|
expect(bar.getAttribute('style')).toContain('width: 42%')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('caps width at 100% when percent exceeds max', () => {
|
||||||
|
render(<ProgressBar percent={150} color="bg-test-color" />)
|
||||||
|
|
||||||
|
const bar = screen.getByTestId('billing-progress-bar')
|
||||||
|
expect(bar.getAttribute('style')).toContain('width: 100%')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses the default color when no color prop is provided', () => {
|
||||||
|
render(<ProgressBar percent={20} color={undefined as unknown as string} />)
|
||||||
|
|
||||||
|
const bar = screen.getByTestId('billing-progress-bar')
|
||||||
|
expect(bar).toHaveClass('bg-components-progress-bar-progress-solid')
|
||||||
|
expect(bar.getAttribute('style')).toContain('width: 20%')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('caps width at 100% when percent exceeds max', () => {
|
describe('Indeterminate Mode', () => {
|
||||||
render(<ProgressBar percent={150} color="bg-test-color" />)
|
it('should render indeterminate progress bar when indeterminate is true', () => {
|
||||||
|
render(<ProgressBar percent={0} color="bg-test-color" indeterminate />)
|
||||||
|
|
||||||
const bar = screen.getByTestId('billing-progress-bar')
|
const bar = screen.getByTestId('billing-progress-bar-indeterminate')
|
||||||
expect(bar.getAttribute('style')).toContain('width: 100%')
|
expect(bar).toBeInTheDocument()
|
||||||
})
|
expect(bar).toHaveClass('bg-progress-bar-indeterminate-stripe')
|
||||||
|
})
|
||||||
|
|
||||||
it('uses the default color when no color prop is provided', () => {
|
it('should not render normal progress bar when indeterminate is true', () => {
|
||||||
render(<ProgressBar percent={20} color={undefined as unknown as string} />)
|
render(<ProgressBar percent={50} color="bg-test-color" indeterminate />)
|
||||||
|
|
||||||
expect(screen.getByTestId('billing-progress-bar')).toHaveClass('#2970FF')
|
expect(screen.queryByTestId('billing-progress-bar')).not.toBeInTheDocument()
|
||||||
|
expect(screen.getByTestId('billing-progress-bar-indeterminate')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render with default width (w-[30px]) when indeterminateFull is false', () => {
|
||||||
|
render(<ProgressBar percent={0} color="bg-test-color" indeterminate indeterminateFull={false} />)
|
||||||
|
|
||||||
|
const bar = screen.getByTestId('billing-progress-bar-indeterminate')
|
||||||
|
expect(bar).toHaveClass('w-[30px]')
|
||||||
|
expect(bar).not.toHaveClass('w-full')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render with full width (w-full) when indeterminateFull is true', () => {
|
||||||
|
render(<ProgressBar percent={0} color="bg-test-color" indeterminate indeterminateFull />)
|
||||||
|
|
||||||
|
const bar = screen.getByTestId('billing-progress-bar-indeterminate')
|
||||||
|
expect(bar).toHaveClass('w-full')
|
||||||
|
expect(bar).not.toHaveClass('w-[30px]')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,12 +3,27 @@ import { cn } from '@/utils/classnames'
|
|||||||
type ProgressBarProps = {
|
type ProgressBarProps = {
|
||||||
percent: number
|
percent: number
|
||||||
color: string
|
color: string
|
||||||
|
indeterminate?: boolean
|
||||||
|
indeterminateFull?: boolean // For Sandbox users: full width stripe
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProgressBar = ({
|
const ProgressBar = ({
|
||||||
percent = 0,
|
percent = 0,
|
||||||
color = '#2970FF',
|
color = 'bg-components-progress-bar-progress-solid',
|
||||||
|
indeterminate = false,
|
||||||
|
indeterminateFull = false,
|
||||||
}: ProgressBarProps) => {
|
}: ProgressBarProps) => {
|
||||||
|
if (indeterminate) {
|
||||||
|
return (
|
||||||
|
<div className="overflow-hidden rounded-[6px] bg-components-progress-bar-bg">
|
||||||
|
<div
|
||||||
|
data-testid="billing-progress-bar-indeterminate"
|
||||||
|
className={cn('h-1 rounded-[6px] bg-progress-bar-indeterminate-stripe', indeterminateFull ? 'w-full' : 'w-[30px]')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-hidden rounded-[6px] bg-components-progress-bar-bg">
|
<div className="overflow-hidden rounded-[6px] bg-components-progress-bar-bg">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -5,110 +5,310 @@ import UsageInfo from './index'
|
|||||||
const TestIcon = () => <span data-testid="usage-icon" />
|
const TestIcon = () => <span data-testid="usage-icon" />
|
||||||
|
|
||||||
describe('UsageInfo', () => {
|
describe('UsageInfo', () => {
|
||||||
it('renders the metric with a suffix unit and tooltip text', () => {
|
describe('Default Mode (non-storage)', () => {
|
||||||
render(
|
it('renders the metric with a suffix unit and tooltip text', () => {
|
||||||
<UsageInfo
|
render(
|
||||||
Icon={TestIcon}
|
<UsageInfo
|
||||||
name="Apps"
|
Icon={TestIcon}
|
||||||
usage={30}
|
name="Apps"
|
||||||
total={100}
|
usage={30}
|
||||||
unit="GB"
|
total={100}
|
||||||
tooltip="tooltip text"
|
unit="GB"
|
||||||
/>,
|
tooltip="tooltip text"
|
||||||
)
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
expect(screen.getByTestId('usage-icon')).toBeInTheDocument()
|
expect(screen.getByTestId('usage-icon')).toBeInTheDocument()
|
||||||
expect(screen.getByText('Apps')).toBeInTheDocument()
|
expect(screen.getByText('Apps')).toBeInTheDocument()
|
||||||
expect(screen.getByText('30')).toBeInTheDocument()
|
expect(screen.getByText('30')).toBeInTheDocument()
|
||||||
expect(screen.getByText('100')).toBeInTheDocument()
|
expect(screen.getByText('100')).toBeInTheDocument()
|
||||||
expect(screen.getByText('GB')).toBeInTheDocument()
|
expect(screen.getByText('GB')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders inline unit when unitPosition is inline', () => {
|
||||||
|
render(
|
||||||
|
<UsageInfo
|
||||||
|
Icon={TestIcon}
|
||||||
|
name="Storage"
|
||||||
|
usage={20}
|
||||||
|
total={100}
|
||||||
|
unit="GB"
|
||||||
|
unitPosition="inline"
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(screen.getByText('100GB')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows reset hint text instead of the unit when resetHint is provided', () => {
|
||||||
|
const resetHint = 'Resets in 3 days'
|
||||||
|
render(
|
||||||
|
<UsageInfo
|
||||||
|
Icon={TestIcon}
|
||||||
|
name="Storage"
|
||||||
|
usage={20}
|
||||||
|
total={100}
|
||||||
|
unit="GB"
|
||||||
|
resetHint={resetHint}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(screen.getByText(resetHint)).toBeInTheDocument()
|
||||||
|
expect(screen.queryByText('GB')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('displays unlimited text when total is infinite', () => {
|
||||||
|
render(
|
||||||
|
<UsageInfo
|
||||||
|
Icon={TestIcon}
|
||||||
|
name="Storage"
|
||||||
|
usage={10}
|
||||||
|
total={NUM_INFINITE}
|
||||||
|
unit="GB"
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(screen.getByText('billing.plansCommon.unlimited')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('applies warning color when usage is close to the limit', () => {
|
||||||
|
render(
|
||||||
|
<UsageInfo
|
||||||
|
Icon={TestIcon}
|
||||||
|
name="Storage"
|
||||||
|
usage={85}
|
||||||
|
total={100}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
const progressBar = screen.getByTestId('billing-progress-bar')
|
||||||
|
expect(progressBar).toHaveClass('bg-components-progress-warning-progress')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('applies error color when usage exceeds the limit', () => {
|
||||||
|
render(
|
||||||
|
<UsageInfo
|
||||||
|
Icon={TestIcon}
|
||||||
|
name="Storage"
|
||||||
|
usage={120}
|
||||||
|
total={100}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
const progressBar = screen.getByTestId('billing-progress-bar')
|
||||||
|
expect(progressBar).toHaveClass('bg-components-progress-error-progress')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not render the icon when hideIcon is true', () => {
|
||||||
|
render(
|
||||||
|
<UsageInfo
|
||||||
|
Icon={TestIcon}
|
||||||
|
name="Storage"
|
||||||
|
usage={5}
|
||||||
|
total={100}
|
||||||
|
hideIcon
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(screen.queryByTestId('usage-icon')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders inline unit when unitPosition is inline', () => {
|
describe('Storage Mode', () => {
|
||||||
render(
|
describe('Below Threshold', () => {
|
||||||
<UsageInfo
|
it('should render indeterminate progress bar when usage is below threshold', () => {
|
||||||
Icon={TestIcon}
|
render(
|
||||||
name="Storage"
|
<UsageInfo
|
||||||
usage={20}
|
Icon={TestIcon}
|
||||||
total={100}
|
name="Storage"
|
||||||
unit="GB"
|
usage={30}
|
||||||
unitPosition="inline"
|
total={5120}
|
||||||
/>,
|
unit="MB"
|
||||||
)
|
storageMode
|
||||||
|
storageThreshold={50}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
expect(screen.getByText('100GB')).toBeInTheDocument()
|
expect(screen.getByTestId('billing-progress-bar-indeterminate')).toBeInTheDocument()
|
||||||
})
|
expect(screen.queryByTestId('billing-progress-bar')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
it('shows reset hint text instead of the unit when resetHint is provided', () => {
|
it('should display "< threshold" format when usage is below threshold (non-sandbox)', () => {
|
||||||
const resetHint = 'Resets in 3 days'
|
render(
|
||||||
render(
|
<UsageInfo
|
||||||
<UsageInfo
|
Icon={TestIcon}
|
||||||
Icon={TestIcon}
|
name="Storage"
|
||||||
name="Storage"
|
usage={30}
|
||||||
usage={20}
|
total={5120}
|
||||||
total={100}
|
unit="MB"
|
||||||
unit="GB"
|
unitPosition="inline"
|
||||||
resetHint={resetHint}
|
storageMode
|
||||||
/>,
|
storageThreshold={50}
|
||||||
)
|
isSandboxPlan={false}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
expect(screen.getByText(resetHint)).toBeInTheDocument()
|
// Text "< 50" is rendered inside a single span
|
||||||
expect(screen.queryByText('GB')).not.toBeInTheDocument()
|
expect(screen.getByText(/< 50/)).toBeInTheDocument()
|
||||||
})
|
expect(screen.getByText('5120MB')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
it('displays unlimited text when total is infinite', () => {
|
it('should display "< threshold unit" format when usage is below threshold (sandbox)', () => {
|
||||||
render(
|
render(
|
||||||
<UsageInfo
|
<UsageInfo
|
||||||
Icon={TestIcon}
|
Icon={TestIcon}
|
||||||
name="Storage"
|
name="Storage"
|
||||||
usage={10}
|
usage={30}
|
||||||
total={NUM_INFINITE}
|
total={50}
|
||||||
unit="GB"
|
unit="MB"
|
||||||
/>,
|
storageMode
|
||||||
)
|
storageThreshold={50}
|
||||||
|
isSandboxPlan
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
expect(screen.getByText('billing.plansCommon.unlimited')).toBeInTheDocument()
|
// Text "< 50" is rendered inside a single span
|
||||||
})
|
expect(screen.getByText(/< 50/)).toBeInTheDocument()
|
||||||
|
// Unit "MB" appears in the display
|
||||||
|
expect(screen.getAllByText('MB').length).toBeGreaterThanOrEqual(1)
|
||||||
|
})
|
||||||
|
|
||||||
it('applies warning color when usage is close to the limit', () => {
|
it('should render full-width indeterminate bar for sandbox users below threshold', () => {
|
||||||
render(
|
render(
|
||||||
<UsageInfo
|
<UsageInfo
|
||||||
Icon={TestIcon}
|
Icon={TestIcon}
|
||||||
name="Storage"
|
name="Storage"
|
||||||
usage={85}
|
usage={30}
|
||||||
total={100}
|
total={50}
|
||||||
/>,
|
unit="MB"
|
||||||
)
|
storageMode
|
||||||
|
storageThreshold={50}
|
||||||
|
isSandboxPlan
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
const progressBar = screen.getByTestId('billing-progress-bar')
|
const bar = screen.getByTestId('billing-progress-bar-indeterminate')
|
||||||
expect(progressBar).toHaveClass('bg-components-progress-warning-progress')
|
expect(bar).toHaveClass('w-full')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('applies error color when usage exceeds the limit', () => {
|
it('should render narrow indeterminate bar for non-sandbox users below threshold', () => {
|
||||||
render(
|
render(
|
||||||
<UsageInfo
|
<UsageInfo
|
||||||
Icon={TestIcon}
|
Icon={TestIcon}
|
||||||
name="Storage"
|
name="Storage"
|
||||||
usage={120}
|
usage={30}
|
||||||
total={100}
|
total={5120}
|
||||||
/>,
|
unit="MB"
|
||||||
)
|
storageMode
|
||||||
|
storageThreshold={50}
|
||||||
|
isSandboxPlan={false}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
const progressBar = screen.getByTestId('billing-progress-bar')
|
const bar = screen.getByTestId('billing-progress-bar-indeterminate')
|
||||||
expect(progressBar).toHaveClass('bg-components-progress-error-progress')
|
expect(bar).toHaveClass('w-[30px]')
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('does not render the icon when hideIcon is true', () => {
|
describe('Sandbox Full Capacity', () => {
|
||||||
render(
|
it('should render error color progress bar when sandbox usage >= threshold', () => {
|
||||||
<UsageInfo
|
render(
|
||||||
Icon={TestIcon}
|
<UsageInfo
|
||||||
name="Storage"
|
Icon={TestIcon}
|
||||||
usage={5}
|
name="Storage"
|
||||||
total={100}
|
usage={50}
|
||||||
hideIcon
|
total={50}
|
||||||
/>,
|
unit="MB"
|
||||||
)
|
storageMode
|
||||||
|
storageThreshold={50}
|
||||||
|
isSandboxPlan
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
expect(screen.queryByTestId('usage-icon')).not.toBeInTheDocument()
|
const progressBar = screen.getByTestId('billing-progress-bar')
|
||||||
|
expect(progressBar).toHaveClass('bg-components-progress-error-progress')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should display "threshold / threshold unit" format when sandbox is at full capacity', () => {
|
||||||
|
render(
|
||||||
|
<UsageInfo
|
||||||
|
Icon={TestIcon}
|
||||||
|
name="Storage"
|
||||||
|
usage={50}
|
||||||
|
total={50}
|
||||||
|
unit="MB"
|
||||||
|
storageMode
|
||||||
|
storageThreshold={50}
|
||||||
|
isSandboxPlan
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
// First span: "50", Third span: "50 MB"
|
||||||
|
expect(screen.getByText('50')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText(/50 MB/)).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('/')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Pro/Team Users Above Threshold', () => {
|
||||||
|
it('should render normal progress bar when usage >= threshold', () => {
|
||||||
|
render(
|
||||||
|
<UsageInfo
|
||||||
|
Icon={TestIcon}
|
||||||
|
name="Storage"
|
||||||
|
usage={100}
|
||||||
|
total={5120}
|
||||||
|
unit="MB"
|
||||||
|
unitPosition="inline"
|
||||||
|
storageMode
|
||||||
|
storageThreshold={50}
|
||||||
|
isSandboxPlan={false}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(screen.getByTestId('billing-progress-bar')).toBeInTheDocument()
|
||||||
|
expect(screen.queryByTestId('billing-progress-bar-indeterminate')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should display actual usage when usage >= threshold', () => {
|
||||||
|
render(
|
||||||
|
<UsageInfo
|
||||||
|
Icon={TestIcon}
|
||||||
|
name="Storage"
|
||||||
|
usage={100}
|
||||||
|
total={5120}
|
||||||
|
unit="MB"
|
||||||
|
unitPosition="inline"
|
||||||
|
storageMode
|
||||||
|
storageThreshold={50}
|
||||||
|
isSandboxPlan={false}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(screen.getByText('100')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('5120MB')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Storage Tooltip', () => {
|
||||||
|
it('should render tooltip wrapper when storageTooltip is provided', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<UsageInfo
|
||||||
|
Icon={TestIcon}
|
||||||
|
name="Storage"
|
||||||
|
usage={30}
|
||||||
|
total={5120}
|
||||||
|
unit="MB"
|
||||||
|
storageMode
|
||||||
|
storageThreshold={50}
|
||||||
|
storageTooltip="This is a storage tooltip"
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tooltip wrapper should contain cursor-default class
|
||||||
|
const tooltipWrapper = container.querySelector('.cursor-default')
|
||||||
|
expect(tooltipWrapper).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import type { FC } from 'react'
|
import type { ComponentType, FC } from 'react'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import Tooltip from '@/app/components/base/tooltip'
|
import Tooltip from '@/app/components/base/tooltip'
|
||||||
@@ -9,7 +9,7 @@ import ProgressBar from '../progress-bar'
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
className?: string
|
className?: string
|
||||||
Icon: any
|
Icon: ComponentType<{ className?: string }>
|
||||||
name: string
|
name: string
|
||||||
tooltip?: string
|
tooltip?: string
|
||||||
usage: number
|
usage: number
|
||||||
@@ -19,6 +19,11 @@ type Props = {
|
|||||||
resetHint?: string
|
resetHint?: string
|
||||||
resetInDays?: number
|
resetInDays?: number
|
||||||
hideIcon?: boolean
|
hideIcon?: boolean
|
||||||
|
// Props for the 50MB threshold display logic
|
||||||
|
storageMode?: boolean
|
||||||
|
storageThreshold?: number
|
||||||
|
storageTooltip?: string
|
||||||
|
isSandboxPlan?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const WARNING_THRESHOLD = 80
|
const WARNING_THRESHOLD = 80
|
||||||
@@ -35,30 +40,141 @@ const UsageInfo: FC<Props> = ({
|
|||||||
resetHint,
|
resetHint,
|
||||||
resetInDays,
|
resetInDays,
|
||||||
hideIcon = false,
|
hideIcon = false,
|
||||||
|
storageMode = false,
|
||||||
|
storageThreshold = 50,
|
||||||
|
storageTooltip,
|
||||||
|
isSandboxPlan = false,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
// Special display logic for usage below threshold (only in storage mode)
|
||||||
|
const isBelowThreshold = storageMode && usage < storageThreshold
|
||||||
|
// Sandbox at full capacity (usage >= threshold and it's sandbox plan)
|
||||||
|
const isSandboxFull = storageMode && isSandboxPlan && usage >= storageThreshold
|
||||||
|
|
||||||
const percent = usage / total * 100
|
const percent = usage / total * 100
|
||||||
const color = percent >= 100
|
const getProgressColor = () => {
|
||||||
? 'bg-components-progress-error-progress'
|
if (percent >= 100)
|
||||||
: (percent >= WARNING_THRESHOLD ? 'bg-components-progress-warning-progress' : 'bg-components-progress-bar-progress-solid')
|
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
|
const isUnlimited = total === NUM_INFINITE
|
||||||
let totalDisplay: string | number = isUnlimited ? t('plansCommon.unlimited', { ns: 'billing' }) : total
|
let totalDisplay: string | number = isUnlimited ? t('plansCommon.unlimited', { ns: 'billing' }) : total
|
||||||
if (!isUnlimited && unit && unitPosition === 'inline')
|
if (!isUnlimited && unit && unitPosition === 'inline')
|
||||||
totalDisplay = `${total}${unit}`
|
totalDisplay = `${total}${unit}`
|
||||||
const showUnit = !!unit && !isUnlimited && unitPosition === 'suffix'
|
const showUnit = !!unit && !isUnlimited && unitPosition === 'suffix'
|
||||||
const resetText = resetHint ?? (typeof resetInDays === 'number' ? t('usagePage.resetsIn', { ns: 'billing', count: resetInDays }) : undefined)
|
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">
|
<div className="system-xs-regular ml-auto flex-1 text-right text-text-tertiary">
|
||||||
{resetText}
|
{resetText}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
: (showUnit && (
|
}
|
||||||
|
if (showUnit) {
|
||||||
|
return (
|
||||||
<div className="system-xs-medium ml-auto text-text-tertiary">
|
<div className="system-xs-medium ml-auto text-text-tertiary">
|
||||||
{unit}
|
{unit}
|
||||||
</div>
|
</div>
|
||||||
))
|
)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render usage display
|
||||||
|
const renderUsageDisplay = () => {
|
||||||
|
// Storage mode: special display logic
|
||||||
|
if (storageMode) {
|
||||||
|
// 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) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<span>
|
||||||
|
<
|
||||||
|
{' '}
|
||||||
|
{storageThreshold}
|
||||||
|
</span>
|
||||||
|
{!isSandboxPlan && (
|
||||||
|
<>
|
||||||
|
<span className="system-md-regular text-text-quaternary">/</span>
|
||||||
|
<span>{totalDisplay}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{isSandboxPlan && <span>{unit}</span>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// Pro/Team users with usage >= threshold - show actual usage
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<span>{usage}</span>
|
||||||
|
<span className="system-md-regular text-text-quaternary">/</span>
|
||||||
|
<span>{totalDisplay}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default display (storageMode = false)
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<span>{usage}</span>
|
||||||
|
<span className="system-md-regular text-text-quaternary">/</span>
|
||||||
|
<span>{totalDisplay}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderWithTooltip = (children: React.ReactNode) => {
|
||||||
|
if (storageMode && storageTooltip) {
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
popupContent={<div className="w-[200px]">{storageTooltip}</div>}
|
||||||
|
asChild={false}
|
||||||
|
>
|
||||||
|
<div className="cursor-default">{children}</div>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return children
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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}
|
||||||
|
indeterminateFull={isBelowThreshold && isSandboxPlan}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
return renderWithTooltip(progressBar)
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderUsageWithTooltip = () => {
|
||||||
|
return renderWithTooltip(renderUsageDisplay())
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('flex flex-col gap-2 rounded-xl bg-components-panel-bg p-4', className)}>
|
<div className={cn('flex flex-col gap-2 rounded-xl bg-components-panel-bg p-4', className)}>
|
||||||
@@ -78,17 +194,10 @@ const UsageInfo: FC<Props> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="system-md-semibold flex items-center gap-1 text-text-primary">
|
<div className="system-md-semibold flex items-center gap-1 text-text-primary">
|
||||||
<div className="flex items-center gap-1">
|
{renderUsageWithTooltip()}
|
||||||
{usage}
|
{renderRightInfo()}
|
||||||
<div className="system-md-regular text-text-quaternary">/</div>
|
|
||||||
<div>{totalDisplay}</div>
|
|
||||||
</div>
|
|
||||||
{rightInfo}
|
|
||||||
</div>
|
</div>
|
||||||
<ProgressBar
|
{renderProgressBar()}
|
||||||
percent={percent}
|
|
||||||
color={color}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
305
web/app/components/billing/usage-info/vector-space-info.spec.tsx
Normal file
305
web/app/components/billing/usage-info/vector-space-info.spec.tsx
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
import { render, screen } from '@testing-library/react'
|
||||||
|
import { defaultPlan } from '../config'
|
||||||
|
import { Plan } from '../type'
|
||||||
|
import VectorSpaceInfo from './vector-space-info'
|
||||||
|
|
||||||
|
// Mock provider context with configurable plan
|
||||||
|
let mockPlanType = Plan.sandbox
|
||||||
|
let mockVectorSpaceUsage = 30
|
||||||
|
let mockVectorSpaceTotal = 5120
|
||||||
|
|
||||||
|
vi.mock('@/context/provider-context', () => ({
|
||||||
|
useProviderContext: () => ({
|
||||||
|
plan: {
|
||||||
|
...defaultPlan,
|
||||||
|
type: mockPlanType,
|
||||||
|
usage: {
|
||||||
|
...defaultPlan.usage,
|
||||||
|
vectorSpace: mockVectorSpaceUsage,
|
||||||
|
},
|
||||||
|
total: {
|
||||||
|
...defaultPlan.total,
|
||||||
|
vectorSpace: mockVectorSpaceTotal,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('VectorSpaceInfo', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
// Reset to default values
|
||||||
|
mockPlanType = Plan.sandbox
|
||||||
|
mockVectorSpaceUsage = 30
|
||||||
|
mockVectorSpaceTotal = 5120
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Rendering', () => {
|
||||||
|
it('should render vector space info component', () => {
|
||||||
|
render(<VectorSpaceInfo />)
|
||||||
|
|
||||||
|
expect(screen.getByText('billing.usagePage.vectorSpace')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should apply custom className', () => {
|
||||||
|
render(<VectorSpaceInfo className="custom-class" />)
|
||||||
|
|
||||||
|
const container = screen.getByText('billing.usagePage.vectorSpace').closest('.custom-class')
|
||||||
|
expect(container).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Sandbox Plan', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockPlanType = Plan.sandbox
|
||||||
|
mockVectorSpaceUsage = 30
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render indeterminate progress bar when usage is below threshold', () => {
|
||||||
|
render(<VectorSpaceInfo />)
|
||||||
|
|
||||||
|
expect(screen.getByTestId('billing-progress-bar-indeterminate')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render full-width indeterminate bar for sandbox users', () => {
|
||||||
|
render(<VectorSpaceInfo />)
|
||||||
|
|
||||||
|
const bar = screen.getByTestId('billing-progress-bar-indeterminate')
|
||||||
|
expect(bar).toHaveClass('w-full')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should display "< 50" format for sandbox below threshold', () => {
|
||||||
|
render(<VectorSpaceInfo />)
|
||||||
|
|
||||||
|
expect(screen.getByText(/< 50/)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Sandbox Plan at Full Capacity', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockPlanType = Plan.sandbox
|
||||||
|
mockVectorSpaceUsage = 50
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render error color progress bar when at full capacity', () => {
|
||||||
|
render(<VectorSpaceInfo />)
|
||||||
|
|
||||||
|
const progressBar = screen.getByTestId('billing-progress-bar')
|
||||||
|
expect(progressBar).toHaveClass('bg-components-progress-error-progress')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should display "50 / 50 MB" format when at full capacity', () => {
|
||||||
|
render(<VectorSpaceInfo />)
|
||||||
|
|
||||||
|
expect(screen.getByText('50')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText(/50 MB/)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Professional Plan', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockPlanType = Plan.professional
|
||||||
|
mockVectorSpaceUsage = 30
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render indeterminate progress bar when usage is below threshold', () => {
|
||||||
|
render(<VectorSpaceInfo />)
|
||||||
|
|
||||||
|
expect(screen.getByTestId('billing-progress-bar-indeterminate')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render narrow indeterminate bar (not full width)', () => {
|
||||||
|
render(<VectorSpaceInfo />)
|
||||||
|
|
||||||
|
const bar = screen.getByTestId('billing-progress-bar-indeterminate')
|
||||||
|
expect(bar).toHaveClass('w-[30px]')
|
||||||
|
expect(bar).not.toHaveClass('w-full')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should display "< 50 / total" format when below threshold', () => {
|
||||||
|
render(<VectorSpaceInfo />)
|
||||||
|
|
||||||
|
expect(screen.getByText(/< 50/)).toBeInTheDocument()
|
||||||
|
// 5 GB = 5120 MB
|
||||||
|
expect(screen.getByText('5120MB')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Professional Plan Above Threshold', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockPlanType = Plan.professional
|
||||||
|
mockVectorSpaceUsage = 100
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render normal progress bar when usage >= threshold', () => {
|
||||||
|
render(<VectorSpaceInfo />)
|
||||||
|
|
||||||
|
expect(screen.getByTestId('billing-progress-bar')).toBeInTheDocument()
|
||||||
|
expect(screen.queryByTestId('billing-progress-bar-indeterminate')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should display actual usage when above threshold', () => {
|
||||||
|
render(<VectorSpaceInfo />)
|
||||||
|
|
||||||
|
expect(screen.getByText('100')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('5120MB')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Team Plan', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockPlanType = Plan.team
|
||||||
|
mockVectorSpaceUsage = 30
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render indeterminate progress bar when usage is below threshold', () => {
|
||||||
|
render(<VectorSpaceInfo />)
|
||||||
|
|
||||||
|
expect(screen.getByTestId('billing-progress-bar-indeterminate')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render narrow indeterminate bar (not full width)', () => {
|
||||||
|
render(<VectorSpaceInfo />)
|
||||||
|
|
||||||
|
const bar = screen.getByTestId('billing-progress-bar-indeterminate')
|
||||||
|
expect(bar).toHaveClass('w-[30px]')
|
||||||
|
expect(bar).not.toHaveClass('w-full')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should display "< 50 / total" format when below threshold', () => {
|
||||||
|
render(<VectorSpaceInfo />)
|
||||||
|
|
||||||
|
expect(screen.getByText(/< 50/)).toBeInTheDocument()
|
||||||
|
// 20 GB = 20480 MB
|
||||||
|
expect(screen.getByText('20480MB')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Team Plan Above Threshold', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockPlanType = Plan.team
|
||||||
|
mockVectorSpaceUsage = 100
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render normal progress bar when usage >= threshold', () => {
|
||||||
|
render(<VectorSpaceInfo />)
|
||||||
|
|
||||||
|
expect(screen.getByTestId('billing-progress-bar')).toBeInTheDocument()
|
||||||
|
expect(screen.queryByTestId('billing-progress-bar-indeterminate')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should display actual usage when above threshold', () => {
|
||||||
|
render(<VectorSpaceInfo />)
|
||||||
|
|
||||||
|
expect(screen.getByText('100')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('20480MB')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Pro/Team Plan Warning State', () => {
|
||||||
|
it('should show warning color when Professional plan usage approaches limit (80%+)', () => {
|
||||||
|
mockPlanType = Plan.professional
|
||||||
|
// 5120 MB * 80% = 4096 MB
|
||||||
|
mockVectorSpaceUsage = 4100
|
||||||
|
|
||||||
|
render(<VectorSpaceInfo />)
|
||||||
|
|
||||||
|
const progressBar = screen.getByTestId('billing-progress-bar')
|
||||||
|
expect(progressBar).toHaveClass('bg-components-progress-warning-progress')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show warning color when Team plan usage approaches limit (80%+)', () => {
|
||||||
|
mockPlanType = Plan.team
|
||||||
|
// 20480 MB * 80% = 16384 MB
|
||||||
|
mockVectorSpaceUsage = 16500
|
||||||
|
|
||||||
|
render(<VectorSpaceInfo />)
|
||||||
|
|
||||||
|
const progressBar = screen.getByTestId('billing-progress-bar')
|
||||||
|
expect(progressBar).toHaveClass('bg-components-progress-warning-progress')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Pro/Team Plan Error State', () => {
|
||||||
|
it('should show error color when Professional plan usage exceeds limit', () => {
|
||||||
|
mockPlanType = Plan.professional
|
||||||
|
// Exceeds 5120 MB
|
||||||
|
mockVectorSpaceUsage = 5200
|
||||||
|
|
||||||
|
render(<VectorSpaceInfo />)
|
||||||
|
|
||||||
|
const progressBar = screen.getByTestId('billing-progress-bar')
|
||||||
|
expect(progressBar).toHaveClass('bg-components-progress-error-progress')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show error color when Team plan usage exceeds limit', () => {
|
||||||
|
mockPlanType = Plan.team
|
||||||
|
// Exceeds 20480 MB
|
||||||
|
mockVectorSpaceUsage = 21000
|
||||||
|
|
||||||
|
render(<VectorSpaceInfo />)
|
||||||
|
|
||||||
|
const progressBar = screen.getByTestId('billing-progress-bar')
|
||||||
|
expect(progressBar).toHaveClass('bg-components-progress-error-progress')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Enterprise Plan (default case)', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockPlanType = Plan.enterprise
|
||||||
|
mockVectorSpaceUsage = 30
|
||||||
|
// Enterprise plan uses total.vectorSpace from context
|
||||||
|
mockVectorSpaceTotal = 102400 // 100 GB = 102400 MB
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should use total.vectorSpace from context for enterprise plan', () => {
|
||||||
|
render(<VectorSpaceInfo />)
|
||||||
|
|
||||||
|
// Enterprise plan should use the mockVectorSpaceTotal value (102400MB)
|
||||||
|
expect(screen.getByText('102400MB')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render indeterminate progress bar when usage is below threshold', () => {
|
||||||
|
render(<VectorSpaceInfo />)
|
||||||
|
|
||||||
|
expect(screen.getByTestId('billing-progress-bar-indeterminate')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render narrow indeterminate bar (not full width) for enterprise', () => {
|
||||||
|
render(<VectorSpaceInfo />)
|
||||||
|
|
||||||
|
const bar = screen.getByTestId('billing-progress-bar-indeterminate')
|
||||||
|
expect(bar).toHaveClass('w-[30px]')
|
||||||
|
expect(bar).not.toHaveClass('w-full')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should display "< 50 / total" format when below threshold', () => {
|
||||||
|
render(<VectorSpaceInfo />)
|
||||||
|
|
||||||
|
expect(screen.getByText(/< 50/)).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('102400MB')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Enterprise Plan Above Threshold', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockPlanType = Plan.enterprise
|
||||||
|
mockVectorSpaceUsage = 100
|
||||||
|
mockVectorSpaceTotal = 102400 // 100 GB
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render normal progress bar when usage >= threshold', () => {
|
||||||
|
render(<VectorSpaceInfo />)
|
||||||
|
|
||||||
|
expect(screen.getByTestId('billing-progress-bar')).toBeInTheDocument()
|
||||||
|
expect(screen.queryByTestId('billing-progress-bar-indeterminate')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should display actual usage when above threshold', () => {
|
||||||
|
render(<VectorSpaceInfo />)
|
||||||
|
|
||||||
|
expect(screen.getByText('100')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('102400MB')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,26 +1,44 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
|
import type { BasicPlan } from '../type'
|
||||||
import {
|
import {
|
||||||
RiHardDrive3Line,
|
RiHardDrive3Line,
|
||||||
} from '@remixicon/react'
|
} from '@remixicon/react'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useProviderContext } from '@/context/provider-context'
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
|
import { Plan } from '../type'
|
||||||
import UsageInfo from '../usage-info'
|
import UsageInfo from '../usage-info'
|
||||||
|
import { getPlanVectorSpaceLimitMB } from '../utils'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
className?: string
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Storage threshold in MB - usage below this shows as "< 50 MB"
|
||||||
|
const STORAGE_THRESHOLD_MB = getPlanVectorSpaceLimitMB(Plan.sandbox)
|
||||||
|
|
||||||
const VectorSpaceInfo: FC<Props> = ({
|
const VectorSpaceInfo: FC<Props> = ({
|
||||||
className,
|
className,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { plan } = useProviderContext()
|
const { plan } = useProviderContext()
|
||||||
const {
|
const {
|
||||||
|
type,
|
||||||
usage,
|
usage,
|
||||||
total,
|
total,
|
||||||
} = plan
|
} = plan
|
||||||
|
|
||||||
|
// Determine total based on plan type (in MB), derived from ALL_PLANS config
|
||||||
|
const getTotalInMB = () => {
|
||||||
|
const planLimit = getPlanVectorSpaceLimitMB(type as BasicPlan)
|
||||||
|
// For known plans, use the config value; otherwise fall back to API response
|
||||||
|
return planLimit > 0 ? planLimit : total.vectorSpace
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalInMB = getTotalInMB()
|
||||||
|
const isSandbox = type === Plan.sandbox
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UsageInfo
|
<UsageInfo
|
||||||
className={className}
|
className={className}
|
||||||
@@ -28,9 +46,13 @@ const VectorSpaceInfo: FC<Props> = ({
|
|||||||
name={t('usagePage.vectorSpace', { ns: 'billing' })}
|
name={t('usagePage.vectorSpace', { ns: 'billing' })}
|
||||||
tooltip={t('usagePage.vectorSpaceTooltip', { ns: 'billing' }) as string}
|
tooltip={t('usagePage.vectorSpaceTooltip', { ns: 'billing' }) as string}
|
||||||
usage={usage.vectorSpace}
|
usage={usage.vectorSpace}
|
||||||
total={total.vectorSpace}
|
total={totalInMB}
|
||||||
unit="MB"
|
unit="MB"
|
||||||
unitPosition="inline"
|
unitPosition="inline"
|
||||||
|
storageMode
|
||||||
|
storageThreshold={STORAGE_THRESHOLD_MB}
|
||||||
|
storageTooltip={t('usagePage.storageThresholdTooltip', { ns: 'billing' }) as string}
|
||||||
|
isSandboxPlan={isSandbox}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,33 @@
|
|||||||
import type { BillingQuota, CurrentPlanInfoBackend } from '../type'
|
import type { BasicPlan, BillingQuota, CurrentPlanInfoBackend } from '../type'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { ALL_PLANS, NUM_INFINITE } from '@/app/components/billing/config'
|
import { ALL_PLANS, NUM_INFINITE } from '@/app/components/billing/config'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse vectorSpace string from ALL_PLANS config and convert to MB
|
||||||
|
* @example "50MB" -> 50, "5GB" -> 5120, "20GB" -> 20480
|
||||||
|
*/
|
||||||
|
export const parseVectorSpaceToMB = (vectorSpace: string): number => {
|
||||||
|
const match = vectorSpace.match(/^(\d+)(MB|GB)$/i)
|
||||||
|
if (!match)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
const value = Number.parseInt(match[1], 10)
|
||||||
|
const unit = match[2].toUpperCase()
|
||||||
|
|
||||||
|
return unit === 'GB' ? value * 1024 : value
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the vector space limit in MB for a given plan type from ALL_PLANS config
|
||||||
|
*/
|
||||||
|
export const getPlanVectorSpaceLimitMB = (planType: BasicPlan): number => {
|
||||||
|
const planInfo = ALL_PLANS[planType]
|
||||||
|
if (!planInfo)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
return parseVectorSpaceToMB(planInfo.vectorSpace)
|
||||||
|
}
|
||||||
|
|
||||||
const parseLimit = (limit: number) => {
|
const parseLimit = (limit: number) => {
|
||||||
if (limit === 0)
|
if (limit === 0)
|
||||||
return NUM_INFINITE
|
return NUM_INFINITE
|
||||||
|
|||||||
@@ -21,6 +21,18 @@ vi.mock('../upgrade-btn', () => ({
|
|||||||
default: () => <button data-testid="vector-upgrade-btn" type="button">Upgrade</button>,
|
default: () => <button data-testid="vector-upgrade-btn" type="button">Upgrade</button>,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
// Mock utils to control threshold and plan limits
|
||||||
|
vi.mock('../utils', () => ({
|
||||||
|
getPlanVectorSpaceLimitMB: (planType: string) => {
|
||||||
|
// Return 5 for sandbox (threshold) and 100 for team
|
||||||
|
if (planType === 'sandbox')
|
||||||
|
return 5
|
||||||
|
if (planType === 'team')
|
||||||
|
return 100
|
||||||
|
return 0
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
describe('VectorSpaceFull', () => {
|
describe('VectorSpaceFull', () => {
|
||||||
const planMock = {
|
const planMock = {
|
||||||
type: 'team',
|
type: 'team',
|
||||||
@@ -52,6 +64,6 @@ describe('VectorSpaceFull', () => {
|
|||||||
render(<VectorSpaceFull />)
|
render(<VectorSpaceFull />)
|
||||||
|
|
||||||
expect(screen.getByText('8')).toBeInTheDocument()
|
expect(screen.getByText('8')).toBeInTheDocument()
|
||||||
expect(screen.getByText('10MB')).toBeInTheDocument()
|
expect(screen.getByText('100MB')).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1559,11 +1559,6 @@
|
|||||||
"count": 3
|
"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": {
|
"app/components/custom/custom-web-app-brand/index.spec.tsx": {
|
||||||
"ts/no-explicit-any": {
|
"ts/no-explicit-any": {
|
||||||
"count": 7
|
"count": 7
|
||||||
|
|||||||
@@ -172,6 +172,7 @@
|
|||||||
"usagePage.documentsUploadQuota": "Documents Upload Quota",
|
"usagePage.documentsUploadQuota": "Documents Upload Quota",
|
||||||
"usagePage.perMonth": "per month",
|
"usagePage.perMonth": "per month",
|
||||||
"usagePage.resetsIn": "Resets in {{count,number}} days",
|
"usagePage.resetsIn": "Resets in {{count,number}} days",
|
||||||
|
"usagePage.storageThresholdTooltip": "Detailed usage is shown once storage exceeds 50 MB.",
|
||||||
"usagePage.teamMembers": "Team Members",
|
"usagePage.teamMembers": "Team Members",
|
||||||
"usagePage.triggerEvents": "Trigger Events",
|
"usagePage.triggerEvents": "Trigger Events",
|
||||||
"usagePage.vectorSpace": "Knowledge Data Storage",
|
"usagePage.vectorSpace": "Knowledge Data Storage",
|
||||||
|
|||||||
@@ -172,6 +172,7 @@
|
|||||||
"usagePage.documentsUploadQuota": "ドキュメント・アップロード・クォータ",
|
"usagePage.documentsUploadQuota": "ドキュメント・アップロード・クォータ",
|
||||||
"usagePage.perMonth": "月あたり",
|
"usagePage.perMonth": "月あたり",
|
||||||
"usagePage.resetsIn": "{{count,number}}日後にリセット",
|
"usagePage.resetsIn": "{{count,number}}日後にリセット",
|
||||||
|
"usagePage.storageThresholdTooltip": "ストレージ使用量が 50 MB を超えると、詳細な使用状況が表示されます。",
|
||||||
"usagePage.teamMembers": "チームメンバー",
|
"usagePage.teamMembers": "チームメンバー",
|
||||||
"usagePage.triggerEvents": "トリガーイベント数",
|
"usagePage.triggerEvents": "トリガーイベント数",
|
||||||
"usagePage.vectorSpace": "ナレッジベースのデータストレージ",
|
"usagePage.vectorSpace": "ナレッジベースのデータストレージ",
|
||||||
|
|||||||
@@ -172,6 +172,7 @@
|
|||||||
"usagePage.documentsUploadQuota": "文档上传配额",
|
"usagePage.documentsUploadQuota": "文档上传配额",
|
||||||
"usagePage.perMonth": "每月",
|
"usagePage.perMonth": "每月",
|
||||||
"usagePage.resetsIn": "{{count,number}} 天后重置",
|
"usagePage.resetsIn": "{{count,number}} 天后重置",
|
||||||
|
"usagePage.storageThresholdTooltip": "存储空间超过 50 MB 后,将显示详细使用情况。",
|
||||||
"usagePage.teamMembers": "团队成员",
|
"usagePage.teamMembers": "团队成员",
|
||||||
"usagePage.triggerEvents": "触发器事件数",
|
"usagePage.triggerEvents": "触发器事件数",
|
||||||
"usagePage.vectorSpace": "知识库数据存储空间",
|
"usagePage.vectorSpace": "知识库数据存储空间",
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ const config = {
|
|||||||
'billing-plan-card-premium-bg': 'var(--color-billing-plan-card-premium-bg)',
|
'billing-plan-card-premium-bg': 'var(--color-billing-plan-card-premium-bg)',
|
||||||
'billing-plan-card-enterprise-bg': 'var(--color-billing-plan-card-enterprise-bg)',
|
'billing-plan-card-enterprise-bg': 'var(--color-billing-plan-card-enterprise-bg)',
|
||||||
'knowledge-pipeline-creation-footer-bg': 'var(--color-knowledge-pipeline-creation-footer-bg)',
|
'knowledge-pipeline-creation-footer-bg': 'var(--color-knowledge-pipeline-creation-footer-bg)',
|
||||||
|
'progress-bar-indeterminate-stripe': 'var(--color-progress-bar-indeterminate-stripe)',
|
||||||
},
|
},
|
||||||
animation: {
|
animation: {
|
||||||
'spin-slow': 'spin 2s linear infinite',
|
'spin-slow': 'spin 2s linear infinite',
|
||||||
|
|||||||
@@ -74,4 +74,5 @@ html[data-theme="dark"] {
|
|||||||
--color-billing-plan-card-premium-bg: linear-gradient(180deg, #F90 0%, rgba(255, 153, 0, 0.00) 100%);
|
--color-billing-plan-card-premium-bg: linear-gradient(180deg, #F90 0%, rgba(255, 153, 0, 0.00) 100%);
|
||||||
--color-billing-plan-card-enterprise-bg: linear-gradient(180deg, #03F 0%, rgba(0, 51, 255, 0.00) 100%);
|
--color-billing-plan-card-enterprise-bg: linear-gradient(180deg, #03F 0%, rgba(0, 51, 255, 0.00) 100%);
|
||||||
--color-knowledge-pipeline-creation-footer-bg: linear-gradient(90deg, rgba(34, 34, 37, 1) 4.89%, rgba(0, 0, 0, 0) 100%);
|
--color-knowledge-pipeline-creation-footer-bg: linear-gradient(90deg, rgba(34, 34, 37, 1) 4.89%, rgba(0, 0, 0, 0) 100%);
|
||||||
|
--color-progress-bar-indeterminate-stripe: repeating-linear-gradient(-55deg, #3A3A40, #3A3A40 2px, transparent 2px, transparent 5px);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,4 +74,5 @@ html[data-theme="light"] {
|
|||||||
--color-billing-plan-card-premium-bg: linear-gradient(180deg, #F90 0%, rgba(255, 153, 0, 0.00) 100%);
|
--color-billing-plan-card-premium-bg: linear-gradient(180deg, #F90 0%, rgba(255, 153, 0, 0.00) 100%);
|
||||||
--color-billing-plan-card-enterprise-bg: linear-gradient(180deg, #03F 0%, rgba(0, 51, 255, 0.00) 100%);
|
--color-billing-plan-card-enterprise-bg: linear-gradient(180deg, #03F 0%, rgba(0, 51, 255, 0.00) 100%);
|
||||||
--color-knowledge-pipeline-creation-footer-bg: linear-gradient(90deg, #FCFCFD 4.89%, rgba(255, 255, 255, 0.00) 100%);
|
--color-knowledge-pipeline-creation-footer-bg: linear-gradient(90deg, #FCFCFD 4.89%, rgba(255, 255, 255, 0.00) 100%);
|
||||||
|
--color-progress-bar-indeterminate-stripe: repeating-linear-gradient(-55deg, #D0D5DD, #D0D5DD 2px, transparent 2px, transparent 5px);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user