mirror of
https://github.com/langgenius/dify.git
synced 2026-06-07 16:32:01 +08:00
feat(dify-ui): add status and progress primitives (#36615)
This commit is contained in:
parent
9ddd98a265
commit
23539c5bcc
@ -2534,7 +2534,7 @@
|
||||
},
|
||||
"web/app/components/header/account-setting/model-provider-page/model-auth/switch-credential-in-load-balancing.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 3
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/header/account-setting/model-provider-page/model-modal/Input.tsx": {
|
||||
@ -2653,11 +2653,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/plugins/plugin-auth/authorized-in-node.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/plugins/plugin-auth/authorized/index.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
@ -2684,11 +2679,6 @@
|
||||
"count": 3
|
||||
}
|
||||
},
|
||||
"web/app/components/plugins/plugin-auth/plugin-auth-in-agent.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/plugins/plugin-auth/types.ts": {
|
||||
"erasable-syntax-only/enums": {
|
||||
"count": 2
|
||||
|
||||
@ -73,6 +73,10 @@
|
||||
"types": "./src/meter/index.tsx",
|
||||
"import": "./src/meter/index.tsx"
|
||||
},
|
||||
"./progress": {
|
||||
"types": "./src/progress/index.tsx",
|
||||
"import": "./src/progress/index.tsx"
|
||||
},
|
||||
"./number-field": {
|
||||
"types": "./src/number-field/index.tsx",
|
||||
"import": "./src/number-field/index.tsx"
|
||||
@ -105,6 +109,10 @@
|
||||
"types": "./src/select/index.tsx",
|
||||
"import": "./src/select/index.tsx"
|
||||
},
|
||||
"./status-dot": {
|
||||
"types": "./src/status-dot/index.tsx",
|
||||
"import": "./src/status-dot/index.tsx"
|
||||
},
|
||||
"./slider": {
|
||||
"types": "./src/slider/index.tsx",
|
||||
"import": "./src/slider/index.tsx"
|
||||
|
||||
80
packages/dify-ui/src/progress/__tests__/index.spec.tsx
Normal file
80
packages/dify-ui/src/progress/__tests__/index.spec.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
import { render } from 'vitest-browser-react'
|
||||
import { ProgressCircle } from '../index'
|
||||
|
||||
describe('ProgressCircle', () => {
|
||||
it('exposes progressbar semantics through Base UI Progress', async () => {
|
||||
const screen = await render(<ProgressCircle value={40} aria-label="Uploading" />)
|
||||
|
||||
const progress = screen.getByLabelText('Uploading')
|
||||
|
||||
await expect.element(progress).toHaveAttribute('role', 'progressbar')
|
||||
await expect.element(progress).toHaveAttribute('aria-valuemin', '0')
|
||||
await expect.element(progress).toHaveAttribute('aria-valuemax', '100')
|
||||
await expect.element(progress).toHaveAttribute('aria-valuenow', '40')
|
||||
})
|
||||
|
||||
it('supports custom min and max', async () => {
|
||||
const screen = await render(<ProgressCircle value={3} min={1} max={5} aria-label="Installing" />)
|
||||
|
||||
const progress = screen.getByLabelText('Installing')
|
||||
|
||||
await expect.element(progress).toHaveAttribute('aria-valuemin', '1')
|
||||
await expect.element(progress).toHaveAttribute('aria-valuemax', '5')
|
||||
await expect.element(progress).toHaveAttribute('aria-valuenow', '3')
|
||||
})
|
||||
|
||||
it('renders indeterminate state when value is null', async () => {
|
||||
const screen = await render(<ProgressCircle value={null} aria-label="Processing" data-testid="progress" />)
|
||||
|
||||
await expect.element(screen.getByTestId('progress')).toHaveAttribute('data-indeterminate')
|
||||
await expect.element(screen.getByTestId('progress')).not.toHaveAttribute('aria-valuenow')
|
||||
expect(screen.getByTestId('progress').element().querySelector('path')).toBeNull()
|
||||
})
|
||||
|
||||
it('does not render a progress sector for zero progress', async () => {
|
||||
const screen = await render(<ProgressCircle value={0} aria-label="Uploading" data-testid="progress" />)
|
||||
|
||||
expect(screen.getByTestId('progress').element().querySelector('path')).toBeNull()
|
||||
})
|
||||
|
||||
it('applies design kit size variants', async () => {
|
||||
const screen = await render(<ProgressCircle value={50} size="large" aria-label="Uploading" data-testid="progress" />)
|
||||
|
||||
const root = screen.getByTestId('progress').element() as HTMLElement
|
||||
const svg = root.querySelector('svg')!
|
||||
|
||||
expect(root.className).toContain('size-5')
|
||||
expect(svg.getAttribute('width')).toBe('21')
|
||||
expect(svg.getAttribute('height')).toBe('21')
|
||||
})
|
||||
|
||||
it('applies color tokens to circle and sector', async () => {
|
||||
const screen = await render(<ProgressCircle value={50} color="error" aria-label="Uploading" data-testid="progress" />)
|
||||
|
||||
const root = screen.getByTestId('progress').element() as HTMLElement
|
||||
const circle = root.querySelector('circle')!
|
||||
const path = root.querySelector('path')!
|
||||
|
||||
expect(circle.getAttribute('class')).toContain('fill-components-progress-error-bg')
|
||||
expect(circle.getAttribute('class')).toContain('stroke-components-progress-error-border')
|
||||
expect(path.getAttribute('class')).toContain('fill-components-progress-error-progress')
|
||||
})
|
||||
|
||||
it('renders a deterministic progress sector', async () => {
|
||||
const screen = await render(<ProgressCircle value={75} aria-label="Uploading" data-testid="progress" />)
|
||||
|
||||
const path = screen.getByTestId('progress').element().querySelector('path')!
|
||||
|
||||
expect(path.getAttribute('d')).toContain('A 6,6 0 1 1')
|
||||
})
|
||||
|
||||
it('renders a closed circle sector for complete progress', async () => {
|
||||
const screen = await render(<ProgressCircle value={100} aria-label="Uploading" data-testid="progress" />)
|
||||
|
||||
const path = screen.getByTestId('progress').element().querySelector('path')!
|
||||
const pathData = path.getAttribute('d')!
|
||||
|
||||
expect(pathData).toContain('A 6,6 0 1 1 6,12')
|
||||
expect(pathData).toContain('A 6,6 0 1 1 6,0')
|
||||
})
|
||||
})
|
||||
77
packages/dify-ui/src/progress/index.stories.tsx
Normal file
77
packages/dify-ui/src/progress/index.stories.tsx
Normal file
@ -0,0 +1,77 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite'
|
||||
import type { ProgressCircleColor, ProgressCircleSize } from '.'
|
||||
import { Fragment } from 'react'
|
||||
import { ProgressCircle } from '.'
|
||||
|
||||
const colors: ProgressCircleColor[] = ['gray', 'white', 'blue', 'warning', 'error']
|
||||
const sizes: ProgressCircleSize[] = ['small', 'medium', 'large']
|
||||
|
||||
const meta = {
|
||||
title: 'Base/UI/Progress',
|
||||
component: ProgressCircle,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
'Task progress primitives. ProgressCircle matches the Dify Design Kit circular Progress component and uses Base UI Progress semantics.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
} satisfies Meta<typeof ProgressCircle>
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Circle: Story = {
|
||||
args: {
|
||||
'value': 42,
|
||||
'color': 'blue',
|
||||
'size': 'small',
|
||||
'aria-label': 'Uploading',
|
||||
},
|
||||
}
|
||||
|
||||
export const CircleMatrix: Story = {
|
||||
args: {
|
||||
'value': 62,
|
||||
'aria-label': 'Progress',
|
||||
},
|
||||
render: () => (
|
||||
<div className="grid grid-cols-[auto_auto_auto_auto] items-center gap-4 rounded-lg bg-components-panel-bg p-4">
|
||||
<div />
|
||||
{sizes.map(size => (
|
||||
<div key={size} className="system-xs-medium text-text-tertiary">
|
||||
{size}
|
||||
</div>
|
||||
))}
|
||||
{colors.map(color => (
|
||||
<Fragment key={color}>
|
||||
<div className="system-xs-semibold-uppercase text-text-secondary">
|
||||
{color}
|
||||
</div>
|
||||
{sizes.map(size => (
|
||||
<ProgressCircle
|
||||
key={`${color}-${size}`}
|
||||
value={62}
|
||||
color={color}
|
||||
size={size}
|
||||
aria-label={`${color} ${size} progress`}
|
||||
/>
|
||||
))}
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
export const Indeterminate: Story = {
|
||||
args: {
|
||||
'value': null,
|
||||
'color': 'gray',
|
||||
'size': 'medium',
|
||||
'aria-label': 'Processing',
|
||||
},
|
||||
}
|
||||
167
packages/dify-ui/src/progress/index.tsx
Normal file
167
packages/dify-ui/src/progress/index.tsx
Normal file
@ -0,0 +1,167 @@
|
||||
'use client'
|
||||
|
||||
import type { VariantProps } from 'class-variance-authority'
|
||||
import { Progress as BaseProgress } from '@base-ui/react/progress'
|
||||
import { cva } from 'class-variance-authority'
|
||||
import { cn } from '../cn'
|
||||
|
||||
const progressCircleRootVariants = cva(
|
||||
'inline-flex shrink-0 items-center justify-center',
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
small: 'size-3',
|
||||
medium: 'size-4',
|
||||
large: 'size-5',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: 'small',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
const progressCircleColorClasses = {
|
||||
gray: {
|
||||
stroke: 'stroke-components-progress-gray-border',
|
||||
fill: 'fill-components-progress-gray-bg',
|
||||
sector: 'fill-components-progress-gray-progress',
|
||||
},
|
||||
white: {
|
||||
stroke: 'stroke-components-progress-white-border',
|
||||
fill: 'fill-components-progress-white-bg',
|
||||
sector: 'fill-components-progress-white-progress',
|
||||
},
|
||||
blue: {
|
||||
stroke: 'stroke-components-progress-brand-border',
|
||||
fill: 'fill-components-progress-brand-bg',
|
||||
sector: 'fill-components-progress-brand-progress',
|
||||
},
|
||||
warning: {
|
||||
stroke: 'stroke-components-progress-warning-border',
|
||||
fill: 'fill-components-progress-warning-bg',
|
||||
sector: 'fill-components-progress-warning-progress',
|
||||
},
|
||||
error: {
|
||||
stroke: 'stroke-components-progress-error-border',
|
||||
fill: 'fill-components-progress-error-bg',
|
||||
sector: 'fill-components-progress-error-progress',
|
||||
},
|
||||
} as const
|
||||
|
||||
export type ProgressCircleSize = NonNullable<VariantProps<typeof progressCircleRootVariants>['size']>
|
||||
export type ProgressCircleColor = keyof typeof progressCircleColorClasses
|
||||
|
||||
const progressCircleSizeValues = {
|
||||
small: 12,
|
||||
medium: 16,
|
||||
large: 20,
|
||||
} as const satisfies Record<ProgressCircleSize, number>
|
||||
|
||||
type ProgressCircleAccessibleNameProps
|
||||
= | {
|
||||
'aria-label': string
|
||||
'aria-labelledby'?: never
|
||||
}
|
||||
| {
|
||||
'aria-label'?: never
|
||||
'aria-labelledby': string
|
||||
}
|
||||
|
||||
export type ProgressCircleProps
|
||||
= Omit<BaseProgress.Root.Props, 'children' | 'className' | 'aria-label' | 'aria-labelledby'>
|
||||
& ProgressCircleAccessibleNameProps
|
||||
& {
|
||||
className?: string
|
||||
color?: ProgressCircleColor
|
||||
size?: ProgressCircleSize
|
||||
circleStrokeWidth?: number
|
||||
}
|
||||
|
||||
function getProgressPercentage(value: number | null, min: number, max: number) {
|
||||
if (value === null || !Number.isFinite(value) || max <= min)
|
||||
return null
|
||||
|
||||
return Math.min(100, Math.max(0, ((value - min) / (max - min)) * 100))
|
||||
}
|
||||
|
||||
function getSectorPath(size: number, percentage: number | null) {
|
||||
if (percentage === null || percentage <= 0)
|
||||
return ''
|
||||
|
||||
const radius = size / 2
|
||||
const center = size / 2
|
||||
|
||||
if (percentage >= 100) {
|
||||
return `
|
||||
M ${center},${center - radius}
|
||||
A ${radius},${radius} 0 1 1 ${center},${center + radius}
|
||||
A ${radius},${radius} 0 1 1 ${center},${center - radius}
|
||||
Z
|
||||
`
|
||||
}
|
||||
|
||||
const angle = (percentage / 100) * 360
|
||||
const radians = (angle * Math.PI) / 180
|
||||
const x = center + radius * Math.cos(radians - Math.PI / 2)
|
||||
const y = center + radius * Math.sin(radians - Math.PI / 2)
|
||||
const largeArcFlag = percentage > 50 ? 1 : 0
|
||||
|
||||
return `
|
||||
M ${center},${center}
|
||||
L ${center},${center - radius}
|
||||
A ${radius},${radius} 0 ${largeArcFlag} 1 ${x},${y}
|
||||
Z
|
||||
`
|
||||
}
|
||||
|
||||
export function ProgressCircle({
|
||||
className,
|
||||
color = 'blue',
|
||||
size = 'small',
|
||||
circleStrokeWidth = 1,
|
||||
value,
|
||||
min = 0,
|
||||
max = 100,
|
||||
...props
|
||||
}: ProgressCircleProps) {
|
||||
const numericSize = progressCircleSizeValues[size]
|
||||
const percentage = getProgressPercentage(value, min, max)
|
||||
const radius = numericSize / 2
|
||||
const center = numericSize / 2
|
||||
const pathData = getSectorPath(numericSize, percentage)
|
||||
const colors = progressCircleColorClasses[color]
|
||||
|
||||
return (
|
||||
<BaseProgress.Root
|
||||
className={cn(progressCircleRootVariants({ size }), className)}
|
||||
value={value}
|
||||
min={min}
|
||||
max={max}
|
||||
{...props}
|
||||
>
|
||||
<svg
|
||||
width={numericSize + circleStrokeWidth}
|
||||
height={numericSize + circleStrokeWidth}
|
||||
viewBox={`0 0 ${numericSize + circleStrokeWidth} ${numericSize + circleStrokeWidth}`}
|
||||
aria-hidden="true"
|
||||
className="block"
|
||||
>
|
||||
<circle
|
||||
className={cn(colors.fill, colors.stroke)}
|
||||
cx={center + circleStrokeWidth / 2}
|
||||
cy={center + circleStrokeWidth / 2}
|
||||
r={radius}
|
||||
strokeWidth={circleStrokeWidth}
|
||||
/>
|
||||
{pathData && (
|
||||
<path
|
||||
className={colors.sector}
|
||||
d={pathData}
|
||||
transform={`translate(${circleStrokeWidth / 2}, ${circleStrokeWidth / 2})`}
|
||||
/>
|
||||
)}
|
||||
</svg>
|
||||
</BaseProgress.Root>
|
||||
)
|
||||
}
|
||||
57
packages/dify-ui/src/status-dot/__tests__/index.spec.tsx
Normal file
57
packages/dify-ui/src/status-dot/__tests__/index.spec.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import { render } from 'vitest-browser-react'
|
||||
import { StatusDot, StatusDotSkeleton } from '../index'
|
||||
|
||||
describe('StatusDot', () => {
|
||||
it('renders a medium success dot by default', async () => {
|
||||
const screen = await render(<StatusDot data-testid="dot" />)
|
||||
|
||||
const root = screen.getByTestId('dot').element() as HTMLElement
|
||||
|
||||
await expect.element(screen.getByTestId('dot')).toHaveAttribute('aria-hidden', 'true')
|
||||
expect(root.className).toContain('size-2')
|
||||
expect(root.className).toContain('bg-components-badge-status-light-success-bg')
|
||||
expect(root.className).toContain('border-components-badge-status-light-success-border-inner')
|
||||
expect(root.className).toContain('shadow-status-indicator-green-shadow')
|
||||
})
|
||||
|
||||
it('uses small dot geometry', async () => {
|
||||
const screen = await render(<StatusDot size="small" data-testid="dot" />)
|
||||
|
||||
const root = screen.getByTestId('dot').element() as HTMLElement
|
||||
|
||||
expect(root.className).toContain('size-1.5')
|
||||
expect(root.className).toContain('rounded-xs')
|
||||
})
|
||||
|
||||
it.each([
|
||||
['warning', 'bg-components-badge-status-light-warning-bg', 'border-components-badge-status-light-warning-border-inner'],
|
||||
['error', 'bg-components-badge-status-light-error-bg', 'border-components-badge-status-light-error-border-inner'],
|
||||
['normal', 'bg-components-badge-status-light-normal-bg', 'border-components-badge-status-light-normal-border-inner'],
|
||||
['disabled', 'bg-components-badge-status-light-disabled-bg', 'border-components-badge-status-light-disabled-border-inner'],
|
||||
] as const)('applies %s status tokens', async (status, backgroundClass, borderClass) => {
|
||||
const screen = await render(<StatusDot status={status} data-testid="dot" />)
|
||||
|
||||
const dot = screen.getByTestId('dot').element() as HTMLElement
|
||||
|
||||
expect(dot.className).toContain(backgroundClass)
|
||||
expect(dot.className).toContain(borderClass)
|
||||
})
|
||||
|
||||
it('keeps an explicit accessible label visible to assistive tech', async () => {
|
||||
const screen = await render(<StatusDot aria-label="Active" data-testid="dot" />)
|
||||
|
||||
await expect.element(screen.getByTestId('dot')).toHaveAttribute('aria-label', 'Active')
|
||||
await expect.element(screen.getByTestId('dot')).not.toHaveAttribute('aria-hidden')
|
||||
})
|
||||
|
||||
it('renders skeleton styling without status color', async () => {
|
||||
const screen = await render(<StatusDotSkeleton data-testid="dot" />)
|
||||
|
||||
const dot = screen.getByTestId('dot').element() as HTMLElement
|
||||
|
||||
expect(dot.className).toContain('bg-text-primary')
|
||||
expect(dot.className).toContain('opacity-30')
|
||||
expect(dot.className).not.toContain('bg-components-badge-status-light-success-bg')
|
||||
expect(dot.className).not.toContain('border-components-badge-status-light-success-border-inner')
|
||||
})
|
||||
})
|
||||
62
packages/dify-ui/src/status-dot/index.stories.tsx
Normal file
62
packages/dify-ui/src/status-dot/index.stories.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite'
|
||||
import type { StatusDotSize, StatusDotStatus } from '.'
|
||||
import { Fragment } from 'react'
|
||||
import { StatusDot, StatusDotSkeleton } from '.'
|
||||
|
||||
const statuses: StatusDotStatus[] = ['success', 'warning', 'error', 'normal', 'disabled']
|
||||
const sizes: StatusDotSize[] = ['small', 'medium']
|
||||
|
||||
const meta = {
|
||||
title: 'Base/UI/StatusDot',
|
||||
component: StatusDot,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
'Status Dot primitive from the Dify Design Kit. Use it for compact visual status indicators; provide an accessible label only when the dot is the sole status representation.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
} satisfies Meta<typeof StatusDot>
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
status: 'success',
|
||||
size: 'medium',
|
||||
},
|
||||
}
|
||||
|
||||
export const Matrix: Story = {
|
||||
render: () => (
|
||||
<div className="grid grid-cols-[auto_auto_auto] items-center gap-4">
|
||||
<div />
|
||||
<div className="system-xs-medium text-text-tertiary">Small</div>
|
||||
<div className="system-xs-medium text-text-tertiary">Medium</div>
|
||||
{statuses.map(status => (
|
||||
<Fragment key={status}>
|
||||
<div className="system-xs-semibold-uppercase text-text-secondary">
|
||||
{status}
|
||||
</div>
|
||||
{sizes.map(size => (
|
||||
<StatusDot key={`${status}-${size}`} status={status} size={size} />
|
||||
))}
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
export const Skeleton: Story = {
|
||||
render: () => (
|
||||
<div className="flex items-center gap-4">
|
||||
<StatusDotSkeleton size="small" />
|
||||
<StatusDotSkeleton size="medium" />
|
||||
</div>
|
||||
),
|
||||
}
|
||||
108
packages/dify-ui/src/status-dot/index.tsx
Normal file
108
packages/dify-ui/src/status-dot/index.tsx
Normal file
@ -0,0 +1,108 @@
|
||||
'use client'
|
||||
|
||||
import type { VariantProps } from 'class-variance-authority'
|
||||
import type { ComponentProps } from 'react'
|
||||
import { cva } from 'class-variance-authority'
|
||||
import { cn } from '../cn'
|
||||
|
||||
const statusDotVariants = cva(
|
||||
'block shrink-0 border border-solid',
|
||||
{
|
||||
variants: {
|
||||
status: {
|
||||
success: 'border-components-badge-status-light-success-border-inner bg-components-badge-status-light-success-bg shadow-status-indicator-green-shadow',
|
||||
warning: 'border-components-badge-status-light-warning-border-inner bg-components-badge-status-light-warning-bg shadow-status-indicator-warning-shadow',
|
||||
error: 'border-components-badge-status-light-error-border-inner bg-components-badge-status-light-error-bg shadow-status-indicator-red-shadow',
|
||||
normal: 'border-components-badge-status-light-normal-border-inner bg-components-badge-status-light-normal-bg shadow-status-indicator-blue-shadow',
|
||||
disabled: 'border-components-badge-status-light-disabled-border-inner bg-components-badge-status-light-disabled-bg shadow-status-indicator-gray-shadow',
|
||||
},
|
||||
size: {
|
||||
small: 'size-1.5 rounded-xs',
|
||||
medium: 'size-2 rounded-[3px]',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
status: 'success',
|
||||
size: 'medium',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
const statusDotSkeletonVariants = cva(
|
||||
'block shrink-0 border border-transparent bg-text-primary opacity-30',
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
small: 'size-1.5 rounded-xs',
|
||||
medium: 'size-2 rounded-[3px]',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: 'medium',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
type StatusDotVariants = VariantProps<typeof statusDotVariants>
|
||||
|
||||
export type StatusDotStatus = NonNullable<StatusDotVariants['status']>
|
||||
export type StatusDotSize = NonNullable<StatusDotVariants['size']>
|
||||
|
||||
export type StatusDotProps
|
||||
= Omit<ComponentProps<'span'>, 'children'>
|
||||
& {
|
||||
status?: StatusDotStatus
|
||||
size?: StatusDotSize
|
||||
}
|
||||
|
||||
export type StatusDotSkeletonProps
|
||||
= Omit<ComponentProps<'span'>, 'children'>
|
||||
& {
|
||||
size?: StatusDotSize
|
||||
}
|
||||
|
||||
export function StatusDot({
|
||||
className,
|
||||
status = 'success',
|
||||
size = 'medium',
|
||||
'aria-hidden': ariaHidden,
|
||||
'aria-label': ariaLabel,
|
||||
'aria-labelledby': ariaLabelledBy,
|
||||
...props
|
||||
}: StatusDotProps) {
|
||||
const hidden = ariaHidden ?? (ariaLabel || ariaLabelledBy ? undefined : true)
|
||||
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
statusDotVariants({ status, size }),
|
||||
className,
|
||||
)}
|
||||
aria-hidden={hidden}
|
||||
aria-label={ariaLabel}
|
||||
aria-labelledby={ariaLabelledBy}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function StatusDotSkeleton({
|
||||
className,
|
||||
size = 'medium',
|
||||
'aria-hidden': ariaHidden,
|
||||
'aria-label': ariaLabel,
|
||||
'aria-labelledby': ariaLabelledBy,
|
||||
...props
|
||||
}: StatusDotSkeletonProps) {
|
||||
const hidden = ariaHidden ?? (ariaLabel || ariaLabelledBy ? undefined : true)
|
||||
|
||||
return (
|
||||
<span
|
||||
className={cn(statusDotSkeletonVariants({ size }), className)}
|
||||
aria-hidden={hidden}
|
||||
aria-label={ariaLabel}
|
||||
aria-labelledby={ariaLabelledBy}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -143,10 +143,6 @@ vi.mock('@/app/components/header/account-setting/model-provider-page/declaration
|
||||
ConfigurationMethodEnum: { predefinedModel: 'predefined-model' },
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/header/indicator', () => ({
|
||||
default: ({ color }: { color: string }) => <span data-testid={`indicator-${color}`} />,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/plugins/card/base/card-icon', () => ({
|
||||
default: ({ src }: { src: string }) => <div data-testid="card-icon" data-src={typeof src === 'string' ? src : 'emoji'} />,
|
||||
}))
|
||||
@ -282,7 +278,7 @@ describe('Tool Provider Detail Flow Integration', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Authorized')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('indicator-green')).toBeInTheDocument()
|
||||
expect(document.querySelector('.shadow-status-indicator-green-shadow')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
import type { FC, JSX } from 'react'
|
||||
import type { AliyunConfig, ArizeConfig, DatabricksConfig, LangFuseConfig, LangSmithConfig, MLflowConfig, OpikConfig, PhoenixConfig, TencentConfig, WeaveConfig } from './type'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
||||
import { Switch } from '@langgenius/dify-ui/switch'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import { useBoolean } from 'ahooks'
|
||||
@ -9,7 +10,6 @@ import * as React from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import ProviderConfigModal from './provider-config-modal'
|
||||
import ProviderPanel from './provider-panel'
|
||||
import TracingIcon from './tracing-icon'
|
||||
@ -330,7 +330,7 @@ const ConfigPopup: FC<PopupProps> = ({
|
||||
<div className="title-2xl-semi-bold text-text-primary">{t(`${I18N_PREFIX}.tracing`, { ns: 'app' })}</div>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Indicator color={enabled ? 'green' : 'gray'} />
|
||||
<StatusDot status={enabled ? 'success' : 'disabled'} />
|
||||
<div className={cn('ml-1 system-xs-semibold-uppercase text-text-tertiary', enabled && 'text-util-colors-green-green-600')}>
|
||||
{t(`${I18N_PREFIX}.${enabled ? 'enabled' : 'disabled'}`, { ns: 'app' })}
|
||||
</div>
|
||||
|
||||
@ -3,6 +3,7 @@ import type { FC } from 'react'
|
||||
import type { AliyunConfig, ArizeConfig, DatabricksConfig, LangFuseConfig, LangSmithConfig, MLflowConfig, OpikConfig, PhoenixConfig, TencentConfig, WeaveConfig } from './type'
|
||||
import type { TracingStatus } from '@/models/app'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import {
|
||||
RiArrowDownDoubleLine,
|
||||
@ -15,7 +16,6 @@ import { useTranslation } from 'react-i18next'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import { AliyunIcon, ArizeIcon, DatabricksIcon, LangfuseIcon, LangsmithIcon, MlflowIcon, OpikIcon, PhoenixIcon, TencentIcon, WeaveIcon } from '@/app/components/base/icons/src/public/tracing'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { usePathname } from '@/next/navigation'
|
||||
import { fetchTracingConfig as doFetchTracingConfig, fetchTracingStatus, updateTracingStatus } from '@/service/apps'
|
||||
@ -290,7 +290,7 @@ const Panel: FC = () => {
|
||||
)}
|
||||
>
|
||||
<div className="mr-1 ml-4 flex items-center">
|
||||
<Indicator color={enabled ? 'green' : 'gray'} />
|
||||
<StatusDot status={enabled ? 'success' : 'disabled'} />
|
||||
<div className="ml-1.5 system-xs-semibold-uppercase text-text-tertiary">
|
||||
{t(`${I18N_PREFIX}.${enabled ? 'enabled' : 'disabled'}`, { ns: 'app' })}
|
||||
</div>
|
||||
|
||||
@ -7,6 +7,7 @@ import type { AgentTool } from '@/types/app'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
||||
import { Switch } from '@langgenius/dify-ui/switch'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import {
|
||||
@ -24,7 +25,6 @@ import AppIcon from '@/app/components/base/app-icon'
|
||||
import { DefaultToolIcon } from '@/app/components/base/icons/src/public/other'
|
||||
import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
|
||||
import { Infotip } from '@/app/components/base/infotip'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import { addDefaultValue, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
|
||||
import ToolPicker from '@/app/components/workflow/block-selector/tool-picker'
|
||||
@ -340,7 +340,7 @@ const AgentTools: FC = () => {
|
||||
}}
|
||||
>
|
||||
{t('notAuthorized', { ns: 'tools' })}
|
||||
<Indicator className="ml-2" color="orange" />
|
||||
<StatusDot className="ml-2" status="warning" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -17,6 +17,7 @@ import {
|
||||
DrawerPortal,
|
||||
DrawerViewport,
|
||||
} from '@langgenius/dify-ui/drawer'
|
||||
import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import { RiCloseLine, RiEditFill } from '@remixicon/react'
|
||||
@ -47,7 +48,6 @@ import { AppSourceType } from '@/service/share'
|
||||
import { useChatConversationDetail, useCompletionConversationDetail } from '@/service/use-log'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import PromptLogModal from '../../base/prompt-log-modal'
|
||||
import Indicator from '../../header/indicator'
|
||||
import {
|
||||
applyAnnotationAdded,
|
||||
applyAnnotationEdited,
|
||||
@ -114,7 +114,7 @@ const statusTdRender = (statusCount: StatusCount) => {
|
||||
if (statusCount.paused > 0) {
|
||||
return (
|
||||
<div className="inline-flex items-center gap-1 system-xs-semibold-uppercase">
|
||||
<Indicator color="yellow" />
|
||||
<StatusDot status="warning" />
|
||||
<span className="text-util-colors-warning-warning-600">Pending</span>
|
||||
</div>
|
||||
)
|
||||
@ -122,7 +122,7 @@ const statusTdRender = (statusCount: StatusCount) => {
|
||||
else if (statusCount.partial_success + statusCount.failed === 0) {
|
||||
return (
|
||||
<div className="inline-flex items-center gap-1 system-xs-semibold-uppercase">
|
||||
<Indicator color="green" />
|
||||
<StatusDot status="success" />
|
||||
<span className="text-util-colors-green-green-600">Success</span>
|
||||
</div>
|
||||
)
|
||||
@ -130,7 +130,7 @@ const statusTdRender = (statusCount: StatusCount) => {
|
||||
else if (statusCount.failed === 0) {
|
||||
return (
|
||||
<div className="inline-flex items-center gap-1 system-xs-semibold-uppercase">
|
||||
<Indicator color="green" />
|
||||
<StatusDot status="success" />
|
||||
<span className="text-util-colors-green-green-600">Partial Success</span>
|
||||
</div>
|
||||
)
|
||||
@ -138,7 +138,7 @@ const statusTdRender = (statusCount: StatusCount) => {
|
||||
else {
|
||||
return (
|
||||
<div className="inline-flex items-center gap-1 system-xs-semibold-uppercase">
|
||||
<Indicator color="red" />
|
||||
<StatusDot status="error" />
|
||||
<span className="text-util-colors-red-red-600">
|
||||
{statusCount.failed}
|
||||
{' '}
|
||||
|
||||
@ -4,6 +4,7 @@ import type { ConfigParams } from './settings'
|
||||
import type { AppDetailResponse } from '@/models/app'
|
||||
import type { AppSSO } from '@/types/app'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
||||
import { Switch } from '@langgenius/dify-ui/switch'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import * as React from 'react'
|
||||
@ -12,7 +13,6 @@ import { useTranslation } from 'react-i18next'
|
||||
import AppBasic from '@/app/components/app-sidebar/basic'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import SecretKeyButton from '@/app/components/develop/secret-key/secret-key-button'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import { AccessMode } from '@/models/access-control'
|
||||
@ -296,7 +296,7 @@ function AppCard({
|
||||
}
|
||||
/>
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
<Indicator color={cardState.runningStatus ? 'green' : 'yellow'} />
|
||||
<StatusDot status={cardState.runningStatus ? 'success' : 'warning'} />
|
||||
<div className={`${cardState.runningStatus ? 'text-text-success' : 'text-text-warning'} system-xs-semibold-uppercase`}>
|
||||
{cardState.runningStatus
|
||||
? t('overview.status.running', { ns: 'appOverview' })
|
||||
|
||||
@ -3,6 +3,7 @@ import type { AppDetailResponse } from '@/models/app'
|
||||
import type { AppTrigger } from '@/service/use-tools'
|
||||
import type { AppSSO } from '@/types/app'
|
||||
import type { I18nKeysByPrefix } from '@/types/i18n'
|
||||
import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
||||
import { Switch } from '@langgenius/dify-ui/switch'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -30,20 +31,6 @@ type ITriggerCardProps = {
|
||||
const getTriggerIcon = (trigger: AppTrigger, triggerPlugins: any[]) => {
|
||||
const { trigger_type, status, provider_name } = trigger
|
||||
|
||||
// Status dot styling based on trigger status
|
||||
const getStatusDot = () => {
|
||||
if (status === 'enabled') {
|
||||
return (
|
||||
<div className="absolute -top-0.5 -left-0.5 size-1.5 rounded-xs border border-black/15 bg-green-500" />
|
||||
)
|
||||
}
|
||||
else {
|
||||
return (
|
||||
<div className="absolute -top-0.5 -left-0.5 size-1.5 rounded-xs border border-components-badge-status-light-disabled-border-inner bg-components-badge-status-light-disabled-bg shadow-status-indicator-gray-shadow" />
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Get BlockEnum type from trigger_type
|
||||
let blockType: BlockEnum
|
||||
switch (trigger_type) {
|
||||
@ -78,7 +65,11 @@ const getTriggerIcon = (trigger: AppTrigger, triggerPlugins: any[]) => {
|
||||
size="md"
|
||||
toolIcon={triggerIcon}
|
||||
/>
|
||||
{getStatusDot()}
|
||||
<StatusDot
|
||||
className="absolute -top-0.5 -left-0.5"
|
||||
size="small"
|
||||
status={status === 'enabled' ? 'success' : 'disabled'}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -12,11 +12,11 @@ import {
|
||||
DrawerPortal,
|
||||
DrawerViewport,
|
||||
} from '@langgenius/dify-ui/drawer'
|
||||
import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
||||
import * as React from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import useTimestamp from '@/hooks/use-timestamp'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
@ -67,7 +67,7 @@ const WorkflowAppLogList: FC<ILogs> = ({ logs, appDetail, onRefresh }) => {
|
||||
if (status === 'succeeded') {
|
||||
return (
|
||||
<div className="inline-flex items-center gap-1 system-xs-semibold-uppercase">
|
||||
<Indicator color="green" />
|
||||
<StatusDot status="success" />
|
||||
<span className="text-util-colors-green-green-600">Success</span>
|
||||
</div>
|
||||
)
|
||||
@ -75,7 +75,7 @@ const WorkflowAppLogList: FC<ILogs> = ({ logs, appDetail, onRefresh }) => {
|
||||
if (status === 'failed') {
|
||||
return (
|
||||
<div className="inline-flex items-center gap-1 system-xs-semibold-uppercase">
|
||||
<Indicator color="red" />
|
||||
<StatusDot status="error" />
|
||||
<span className="text-util-colors-red-red-600">Failure</span>
|
||||
</div>
|
||||
)
|
||||
@ -83,7 +83,7 @@ const WorkflowAppLogList: FC<ILogs> = ({ logs, appDetail, onRefresh }) => {
|
||||
if (status === 'stopped') {
|
||||
return (
|
||||
<div className="inline-flex items-center gap-1 system-xs-semibold-uppercase">
|
||||
<Indicator color="yellow" />
|
||||
<StatusDot status="warning" />
|
||||
<span className="text-util-colors-warning-warning-600">Stop</span>
|
||||
</div>
|
||||
)
|
||||
@ -91,7 +91,7 @@ const WorkflowAppLogList: FC<ILogs> = ({ logs, appDetail, onRefresh }) => {
|
||||
if (status === 'paused') {
|
||||
return (
|
||||
<div className="inline-flex items-center gap-1 system-xs-semibold-uppercase">
|
||||
<Indicator color="yellow" />
|
||||
<StatusDot status="warning" />
|
||||
<span className="text-util-colors-warning-warning-600">Pending</span>
|
||||
</div>
|
||||
)
|
||||
@ -99,7 +99,7 @@ const WorkflowAppLogList: FC<ILogs> = ({ logs, appDetail, onRefresh }) => {
|
||||
if (status === 'running') {
|
||||
return (
|
||||
<div className="inline-flex items-center gap-1 system-xs-semibold-uppercase">
|
||||
<Indicator color="blue" />
|
||||
<StatusDot status="normal" />
|
||||
<span className="text-util-colors-blue-light-blue-light-600">Running</span>
|
||||
</div>
|
||||
)
|
||||
@ -107,7 +107,7 @@ const WorkflowAppLogList: FC<ILogs> = ({ logs, appDetail, onRefresh }) => {
|
||||
if (status === 'partial-succeeded') {
|
||||
return (
|
||||
<div className="inline-flex items-center gap-1 system-xs-semibold-uppercase">
|
||||
<Indicator color="green" />
|
||||
<StatusDot status="success" />
|
||||
<span className="text-util-colors-green-green-600">Partial Success</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { FileEntity } from '../types'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { ProgressCircle } from '@langgenius/dify-ui/progress'
|
||||
import {
|
||||
RiDeleteBinLine,
|
||||
RiDownloadLine,
|
||||
@ -9,11 +10,11 @@ import {
|
||||
memo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import { PreviewMode } from '@/app/components/base/features/types'
|
||||
import { ReplayLine } from '@/app/components/base/icons/src/vender/other'
|
||||
import ImagePreview from '@/app/components/base/image-uploader/image-preview'
|
||||
import ProgressCircle from '@/app/components/base/progress-bar/progress-circle'
|
||||
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
|
||||
import { downloadUrl } from '@/utils/download'
|
||||
import { formatFileSize } from '@/utils/format'
|
||||
@ -43,6 +44,7 @@ const FileInAttachmentItem = ({
|
||||
canPreview,
|
||||
previewMode = PreviewMode.CurrentPage,
|
||||
}: FileInAttachmentItemProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { id, name, type, progress, supportFileType, base64Url, url, isRemote } = file
|
||||
const ext = getFileExtension(name, type, isRemote)
|
||||
const isImageFile = supportFileType === SupportUploadFileTypes.image
|
||||
@ -108,7 +110,8 @@ const FileInAttachmentItem = ({
|
||||
progress >= 0 && !fileIsUploaded(file) && (
|
||||
<ProgressCircle
|
||||
className="mr-2.5"
|
||||
percentage={progress}
|
||||
value={progress}
|
||||
aria-label={t('uploading', { ns: 'custom' })}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { FileEntity } from '../types'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { ProgressCircle } from '@langgenius/dify-ui/progress'
|
||||
import {
|
||||
RiCloseLine,
|
||||
RiDownloadLine,
|
||||
@ -8,7 +9,6 @@ import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ReplayLine } from '@/app/components/base/icons/src/vender/other'
|
||||
import ImagePreview from '@/app/components/base/image-uploader/image-preview'
|
||||
import ProgressCircle from '@/app/components/base/progress-bar/progress-circle'
|
||||
import { downloadUrl } from '@/utils/download'
|
||||
import FileImageRender from '../file-image-render'
|
||||
import {
|
||||
@ -65,11 +65,9 @@ const FileImageItem = ({
|
||||
progress >= 0 && !fileIsUploaded(file) && (
|
||||
<div className="absolute inset-0 z-10 flex items-center justify-center border-2 border-effects-image-frame bg-background-overlay-alt">
|
||||
<ProgressCircle
|
||||
percentage={progress}
|
||||
size={12}
|
||||
circleStrokeColor="stroke-components-progress-white-border"
|
||||
circleFillColor="fill-transparent"
|
||||
sectorFillColor="fill-components-progress-white-progress"
|
||||
value={progress}
|
||||
color="white"
|
||||
aria-label={t('uploading', { ns: 'custom' })}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
import type { FileEntity } from '../types'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { ProgressCircle } from '@langgenius/dify-ui/progress'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import AudioPreview from '@/app/components/base/file-uploader/audio-preview'
|
||||
import PdfPreview from '@/app/components/base/file-uploader/dynamic-pdf-preview'
|
||||
import VideoPreview from '@/app/components/base/file-uploader/video-preview'
|
||||
import ProgressCircle from '@/app/components/base/progress-bar/progress-circle'
|
||||
import { downloadUrl } from '@/utils/download'
|
||||
import { formatFileSize } from '@/utils/format'
|
||||
import FileTypeIcon from '../file-type-icon'
|
||||
@ -110,9 +110,9 @@ const FileItem = ({
|
||||
{
|
||||
progress >= 0 && !fileIsUploaded(file) && (
|
||||
<ProgressCircle
|
||||
percentage={progress}
|
||||
size={12}
|
||||
value={progress}
|
||||
className="shrink-0"
|
||||
aria-label={t('uploading', { ns: 'custom' })}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,89 +0,0 @@
|
||||
import { render } from '@testing-library/react'
|
||||
import ProgressCircle from '../progress-circle'
|
||||
|
||||
const extractLargeArcFlag = (pathData: string): string => {
|
||||
const afterA = pathData.slice(pathData.indexOf('A') + 1)
|
||||
const tokens = afterA.replace(/,/g, ' ').trim().split(/\s+/)
|
||||
// Arc syntax: A rx ry x-axis-rotation large-arc-flag sweep-flag x y
|
||||
return tokens[3]!
|
||||
}
|
||||
|
||||
describe('ProgressCircle', () => {
|
||||
describe('Render', () => {
|
||||
it('renders an SVG with default props', () => {
|
||||
const { container } = render(<ProgressCircle />)
|
||||
|
||||
const svg = container.querySelector('svg')
|
||||
const circle = container.querySelector('circle')
|
||||
const path = container.querySelector('path')
|
||||
|
||||
expect(svg)!.toBeInTheDocument()
|
||||
expect(circle)!.toBeInTheDocument()
|
||||
expect(path)!.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Props', () => {
|
||||
it('applies correct size and viewBox when size is provided', () => {
|
||||
const size = 24
|
||||
const strokeWidth = 2
|
||||
|
||||
const { container } = render(
|
||||
<ProgressCircle size={size} circleStrokeWidth={strokeWidth} />,
|
||||
)
|
||||
|
||||
const svg = container.querySelector('svg') as SVGElement
|
||||
|
||||
expect(svg)!.toHaveAttribute('width', String(size + strokeWidth))
|
||||
expect(svg)!.toHaveAttribute('height', String(size + strokeWidth))
|
||||
expect(svg)!.toHaveAttribute(
|
||||
'viewBox',
|
||||
`0 0 ${size + strokeWidth} ${size + strokeWidth}`,
|
||||
)
|
||||
})
|
||||
|
||||
it('applies custom stroke and fill classes to the circle', () => {
|
||||
const { container } = render(
|
||||
<ProgressCircle
|
||||
circleStrokeColor="stroke-red-500"
|
||||
circleFillColor="fill-red-100"
|
||||
/>,
|
||||
)
|
||||
const circle = container.querySelector('circle')!
|
||||
expect(circle!)!.toHaveClass('stroke-red-500')
|
||||
expect(circle!)!.toHaveClass('fill-red-100')
|
||||
})
|
||||
|
||||
it('applies custom sector fill color to the path', () => {
|
||||
const { container } = render(
|
||||
<ProgressCircle sectorFillColor="fill-blue-500" />,
|
||||
)
|
||||
const path = container.querySelector('path')!
|
||||
expect(path!)!.toHaveClass('fill-blue-500')
|
||||
})
|
||||
|
||||
it('uses large arc flag when percentage is greater than 50', () => {
|
||||
const { container } = render(<ProgressCircle percentage={75} />)
|
||||
const path = container.querySelector('path')!
|
||||
const d = path.getAttribute('d') || ''
|
||||
expect(d).toContain('A')
|
||||
expect(extractLargeArcFlag(d)).toBe('1')
|
||||
})
|
||||
|
||||
it('uses small arc flag when percentage is 50 or less', () => {
|
||||
const { container } = render(<ProgressCircle percentage={25} />)
|
||||
const path = container.querySelector('path')!
|
||||
const d = path.getAttribute('d') || ''
|
||||
expect(d).toContain('A')
|
||||
expect(extractLargeArcFlag(d)).toBe('0')
|
||||
})
|
||||
|
||||
it('uses small arc flag when percentage is exactly 50', () => {
|
||||
const { container } = render(<ProgressCircle percentage={50} />)
|
||||
const path = container.querySelector('path')!
|
||||
const d = path.getAttribute('d') || ''
|
||||
expect(d).toContain('A')
|
||||
expect(extractLargeArcFlag(d)).toBe('0')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,90 +0,0 @@
|
||||
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { useState } from 'react'
|
||||
import ProgressCircle from './progress-circle'
|
||||
|
||||
const ProgressCircleDemo = ({
|
||||
initialPercentage = 42,
|
||||
size = 24,
|
||||
}: {
|
||||
initialPercentage?: number
|
||||
size?: number
|
||||
}) => {
|
||||
const [percentage, setPercentage] = useState(initialPercentage)
|
||||
|
||||
return (
|
||||
<div className="flex w-full max-w-md flex-col gap-4 rounded-2xl border border-divider-subtle bg-components-panel-bg p-6">
|
||||
<div className="flex items-center justify-between text-xs tracking-[0.18em] text-text-tertiary uppercase">
|
||||
<span>Upload progress</span>
|
||||
<span className="rounded-md border border-divider-subtle bg-background-default px-2 py-1 text-[11px] text-text-secondary">
|
||||
{percentage}
|
||||
%
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<ProgressCircle percentage={percentage} size={size} className="shrink-0" />
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={percentage}
|
||||
onChange={event => setPercentage(Number.parseInt(event.target.value, 10))}
|
||||
className="h-2 w-full cursor-pointer appearance-none rounded-full bg-divider-subtle accent-primary-600"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-3 text-xs text-text-tertiary">
|
||||
<label className="flex items-center gap-1">
|
||||
Size
|
||||
<input
|
||||
type="number"
|
||||
min={12}
|
||||
max={48}
|
||||
value={size}
|
||||
disabled
|
||||
className="h-7 w-16 rounded-md border border-divider-subtle bg-background-default px-2 text-xs"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="rounded-lg border border-divider-subtle bg-background-default-subtle p-3 text-[11px] leading-relaxed text-text-tertiary">
|
||||
ProgressCircle renders a deterministic SVG slice. Advance the slider to preview how the arc grows for upload indicators.
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const meta = {
|
||||
title: 'Base/Feedback/ProgressCircle',
|
||||
component: ProgressCircleDemo,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Compact radial progress indicator wired to upload flows. The story provides a slider to scrub through percentages.',
|
||||
},
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
initialPercentage: {
|
||||
control: { type: 'range', min: 0, max: 100, step: 1 },
|
||||
},
|
||||
size: {
|
||||
control: { type: 'number', min: 12, max: 48, step: 2 },
|
||||
},
|
||||
},
|
||||
args: {
|
||||
initialPercentage: 42,
|
||||
size: 24,
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
} satisfies Meta<typeof ProgressCircleDemo>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Playground: Story = {}
|
||||
|
||||
export const NearComplete: Story = {
|
||||
args: {
|
||||
initialPercentage: 92,
|
||||
},
|
||||
}
|
||||
@ -1,64 +0,0 @@
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { memo } from 'react'
|
||||
|
||||
type ProgressCircleProps = {
|
||||
className?: string
|
||||
percentage?: number
|
||||
size?: number
|
||||
circleStrokeWidth?: number
|
||||
circleStrokeColor?: string
|
||||
circleFillColor?: string
|
||||
sectorFillColor?: string
|
||||
}
|
||||
|
||||
const ProgressCircle: React.FC<ProgressCircleProps> = ({
|
||||
className,
|
||||
percentage = 0,
|
||||
size = 12,
|
||||
circleStrokeWidth = 1,
|
||||
circleStrokeColor = 'stroke-components-progress-brand-border',
|
||||
circleFillColor = 'fill-components-progress-brand-bg',
|
||||
sectorFillColor = 'fill-components-progress-brand-progress',
|
||||
}) => {
|
||||
const radius = size / 2
|
||||
const center = size / 2
|
||||
const angle = (percentage / 101) * 360
|
||||
const radians = (angle * Math.PI) / 180
|
||||
const x = center + radius * Math.cos(radians - Math.PI / 2)
|
||||
const y = center + radius * Math.sin(radians - Math.PI / 2)
|
||||
const largeArcFlag = percentage > 50 ? 1 : 0
|
||||
|
||||
const pathData = `
|
||||
M ${center},${center}
|
||||
L ${center},${center - radius}
|
||||
A ${radius},${radius} 0 ${largeArcFlag} 1 ${x},${y}
|
||||
Z
|
||||
`
|
||||
|
||||
return (
|
||||
<svg
|
||||
width={size + circleStrokeWidth}
|
||||
height={size + circleStrokeWidth}
|
||||
viewBox={`0 0 ${size + circleStrokeWidth} ${size + circleStrokeWidth}`}
|
||||
className={className}
|
||||
>
|
||||
<circle
|
||||
className={cn(
|
||||
circleFillColor,
|
||||
circleStrokeColor,
|
||||
)}
|
||||
cx={center + circleStrokeWidth / 2}
|
||||
cy={center + circleStrokeWidth / 2}
|
||||
r={radius}
|
||||
strokeWidth={circleStrokeWidth}
|
||||
/>
|
||||
<path
|
||||
className={cn(sectorFillColor)}
|
||||
d={pathData}
|
||||
transform={`translate(${circleStrokeWidth / 2}, ${circleStrokeWidth / 2})`}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(ProgressCircle)
|
||||
@ -1,5 +1,6 @@
|
||||
import type { FileEntity } from '../types'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { ProgressCircle } from '@langgenius/dify-ui/progress'
|
||||
import {
|
||||
RiCloseLine,
|
||||
} from '@remixicon/react'
|
||||
@ -7,9 +8,9 @@ import {
|
||||
memo,
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import FileImageRender from '@/app/components/base/file-uploader/file-image-render'
|
||||
import { ReplayLine } from '@/app/components/base/icons/src/vender/other'
|
||||
import ProgressCircle from '@/app/components/base/progress-bar/progress-circle'
|
||||
import { fileIsUploaded } from '../utils'
|
||||
|
||||
type ImageItemProps = {
|
||||
@ -26,6 +27,7 @@ const ImageItem = ({
|
||||
onReUpload,
|
||||
onPreview,
|
||||
}: ImageItemProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { id, progress, base64Url, sourceUrl } = file
|
||||
|
||||
const handlePreview = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
|
||||
@ -69,11 +71,9 @@ const ImageItem = ({
|
||||
progress >= 0 && !fileIsUploaded(file) && (
|
||||
<div className="absolute inset-0 z-10 flex items-center justify-center border-2 border-effects-image-frame bg-background-overlay-alt">
|
||||
<ProgressCircle
|
||||
percentage={progress}
|
||||
size={12}
|
||||
circleStrokeColor="stroke-components-progress-white-border"
|
||||
circleFillColor="fill-transparent"
|
||||
sectorFillColor="fill-components-progress-white-progress"
|
||||
value={progress}
|
||||
color="white"
|
||||
aria-label={t('uploading', { ns: 'custom' })}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { FileEntity } from '../types'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { ProgressCircle } from '@langgenius/dify-ui/progress'
|
||||
import {
|
||||
RiCloseLine,
|
||||
} from '@remixicon/react'
|
||||
@ -7,9 +8,9 @@ import {
|
||||
memo,
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import FileImageRender from '@/app/components/base/file-uploader/file-image-render'
|
||||
import { ReplayLine } from '@/app/components/base/icons/src/vender/other'
|
||||
import ProgressCircle from '@/app/components/base/progress-bar/progress-circle'
|
||||
import { fileIsUploaded } from '../utils'
|
||||
|
||||
type ImageItemProps = {
|
||||
@ -26,6 +27,7 @@ const ImageItem = ({
|
||||
onReUpload,
|
||||
onPreview,
|
||||
}: ImageItemProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { id, progress, base64Url, sourceUrl } = file
|
||||
|
||||
const handlePreview = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
|
||||
@ -69,11 +71,9 @@ const ImageItem = ({
|
||||
progress >= 0 && !fileIsUploaded(file) && (
|
||||
<div className="absolute inset-0 z-10 flex items-center justify-center border-2 border-effects-image-frame bg-background-overlay-alt">
|
||||
<ProgressCircle
|
||||
percentage={progress}
|
||||
size={12}
|
||||
circleStrokeColor="stroke-components-progress-white-border"
|
||||
circleFillColor="fill-transparent"
|
||||
sectorFillColor="fill-components-progress-white-progress"
|
||||
value={progress}
|
||||
color="white"
|
||||
aria-label={t('uploading', { ns: 'custom' })}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -19,46 +19,45 @@ describe('useIndexStatus', () => {
|
||||
expect(keys).toEqual(expect.arrayContaining(expectedKeys))
|
||||
})
|
||||
|
||||
// Verify each status entry has the correct color
|
||||
describe('colors', () => {
|
||||
it('should return orange color for queuing', () => {
|
||||
describe('status variants', () => {
|
||||
it('should return warning status for queuing', () => {
|
||||
const { result } = renderHook(() => useIndexStatus())
|
||||
expect(result.current.queuing.color).toBe('orange')
|
||||
expect(result.current.queuing.status).toBe('warning')
|
||||
})
|
||||
|
||||
it('should return blue color for indexing', () => {
|
||||
it('should return normal status for indexing', () => {
|
||||
const { result } = renderHook(() => useIndexStatus())
|
||||
expect(result.current.indexing.color).toBe('blue')
|
||||
expect(result.current.indexing.status).toBe('normal')
|
||||
})
|
||||
|
||||
it('should return orange color for paused', () => {
|
||||
it('should return warning status for paused', () => {
|
||||
const { result } = renderHook(() => useIndexStatus())
|
||||
expect(result.current.paused.color).toBe('orange')
|
||||
expect(result.current.paused.status).toBe('warning')
|
||||
})
|
||||
|
||||
it('should return red color for error', () => {
|
||||
it('should return error status for error', () => {
|
||||
const { result } = renderHook(() => useIndexStatus())
|
||||
expect(result.current.error.color).toBe('red')
|
||||
expect(result.current.error.status).toBe('error')
|
||||
})
|
||||
|
||||
it('should return green color for available', () => {
|
||||
it('should return success status for available', () => {
|
||||
const { result } = renderHook(() => useIndexStatus())
|
||||
expect(result.current.available.color).toBe('green')
|
||||
expect(result.current.available.status).toBe('success')
|
||||
})
|
||||
|
||||
it('should return green color for enabled', () => {
|
||||
it('should return success status for enabled', () => {
|
||||
const { result } = renderHook(() => useIndexStatus())
|
||||
expect(result.current.enabled.color).toBe('green')
|
||||
expect(result.current.enabled.status).toBe('success')
|
||||
})
|
||||
|
||||
it('should return gray color for disabled', () => {
|
||||
it('should return disabled status for disabled', () => {
|
||||
const { result } = renderHook(() => useIndexStatus())
|
||||
expect(result.current.disabled.color).toBe('gray')
|
||||
expect(result.current.disabled.status).toBe('disabled')
|
||||
})
|
||||
|
||||
it('should return gray color for archived', () => {
|
||||
it('should return disabled status for archived', () => {
|
||||
const { result } = renderHook(() => useIndexStatus())
|
||||
expect(result.current.archived.color).toBe('gray')
|
||||
expect(result.current.archived.status).toBe('disabled')
|
||||
})
|
||||
})
|
||||
|
||||
@ -105,14 +104,13 @@ describe('useIndexStatus', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// Verify each entry has both color and text properties
|
||||
it('should return objects with color and text properties for every status', () => {
|
||||
it('should return objects with status and text properties for every status', () => {
|
||||
const { result } = renderHook(() => useIndexStatus())
|
||||
|
||||
for (const key of Object.keys(result.current) as Array<keyof typeof result.current>) {
|
||||
expect(result.current[key]).toHaveProperty('color')
|
||||
expect(result.current[key]).toHaveProperty('status')
|
||||
expect(result.current[key]).toHaveProperty('text')
|
||||
expect(typeof result.current[key].color).toBe('string')
|
||||
expect(typeof result.current[key].status).toBe('string')
|
||||
expect(typeof result.current[key].text).toBe('string')
|
||||
}
|
||||
})
|
||||
|
||||
@ -34,14 +34,14 @@ vi.mock('@langgenius/dify-ui/toast', () => ({
|
||||
// Mock useIndexStatus hook
|
||||
vi.mock('../hooks', () => ({
|
||||
useIndexStatus: () => ({
|
||||
queuing: { text: 'Queuing', color: 'orange' },
|
||||
indexing: { text: 'Indexing', color: 'blue' },
|
||||
paused: { text: 'Paused', color: 'yellow' },
|
||||
error: { text: 'Error', color: 'red' },
|
||||
available: { text: 'Available', color: 'green' },
|
||||
enabled: { text: 'Enabled', color: 'green' },
|
||||
disabled: { text: 'Disabled', color: 'gray' },
|
||||
archived: { text: 'Archived', color: 'gray' },
|
||||
queuing: { text: 'Queuing', status: 'warning' },
|
||||
indexing: { text: 'Indexing', status: 'normal' },
|
||||
paused: { text: 'Paused', status: 'warning' },
|
||||
error: { text: 'Error', status: 'error' },
|
||||
available: { text: 'Available', status: 'success' },
|
||||
enabled: { text: 'Enabled', status: 'success' },
|
||||
disabled: { text: 'Disabled', status: 'disabled' },
|
||||
archived: { text: 'Archived', status: 'disabled' },
|
||||
}),
|
||||
}))
|
||||
|
||||
|
||||
@ -1,15 +1,16 @@
|
||||
import type { StatusDotStatus } from '@langgenius/dify-ui/status-dot'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export const useIndexStatus = () => {
|
||||
const { t } = useTranslation()
|
||||
return {
|
||||
queuing: { color: 'orange', text: t('list.status.queuing', { ns: 'datasetDocuments' }) }, // waiting
|
||||
indexing: { color: 'blue', text: t('list.status.indexing', { ns: 'datasetDocuments' }) }, // indexing splitting parsing cleaning
|
||||
paused: { color: 'orange', text: t('list.status.paused', { ns: 'datasetDocuments' }) }, // paused
|
||||
error: { color: 'red', text: t('list.status.error', { ns: 'datasetDocuments' }) }, // error
|
||||
available: { color: 'green', text: t('list.status.available', { ns: 'datasetDocuments' }) }, // completed,archived = false,enabled = true
|
||||
enabled: { color: 'green', text: t('list.status.enabled', { ns: 'datasetDocuments' }) }, // completed,archived = false,enabled = true
|
||||
disabled: { color: 'gray', text: t('list.status.disabled', { ns: 'datasetDocuments' }) }, // completed,archived = false,enabled = false
|
||||
archived: { color: 'gray', text: t('list.status.archived', { ns: 'datasetDocuments' }) }, // completed,archived = true
|
||||
}
|
||||
queuing: { status: 'warning', text: t('list.status.queuing', { ns: 'datasetDocuments' }) },
|
||||
indexing: { status: 'normal', text: t('list.status.indexing', { ns: 'datasetDocuments' }) },
|
||||
paused: { status: 'warning', text: t('list.status.paused', { ns: 'datasetDocuments' }) },
|
||||
error: { status: 'error', text: t('list.status.error', { ns: 'datasetDocuments' }) },
|
||||
available: { status: 'success', text: t('list.status.available', { ns: 'datasetDocuments' }) },
|
||||
enabled: { status: 'success', text: t('list.status.enabled', { ns: 'datasetDocuments' }) },
|
||||
disabled: { status: 'disabled', text: t('list.status.disabled', { ns: 'datasetDocuments' }) },
|
||||
archived: { status: 'disabled', text: t('list.status.archived', { ns: 'datasetDocuments' }) },
|
||||
} satisfies Record<string, { status: StatusDotStatus, text: string }>
|
||||
}
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import type { StatusDotStatus } from '@langgenius/dify-ui/status-dot'
|
||||
import type { OperationName } from '../types'
|
||||
import type { ColorMap, IndicatorProps } from '@/app/components/header/indicator'
|
||||
import type { CommonResponse } from '@/models/common'
|
||||
import type { DocumentDisplayStatus } from '@/models/datasets'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
||||
import { Switch } from '@langgenius/dify-ui/switch'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
@ -11,19 +12,17 @@ import * as React from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Infotip } from '@/app/components/base/infotip'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import { useDocumentDelete, useDocumentDisable, useDocumentEnable } from '@/service/knowledge/use-document'
|
||||
import { asyncRunSafe } from '@/utils'
|
||||
import s from '../style.module.css'
|
||||
import { useIndexStatus } from './hooks'
|
||||
|
||||
const STATUS_TEXT_COLOR_MAP: ColorMap = {
|
||||
green: 'text-util-colors-green-green-600',
|
||||
orange: 'text-util-colors-warning-warning-600',
|
||||
red: 'text-util-colors-red-red-600',
|
||||
blue: 'text-util-colors-blue-light-blue-light-600',
|
||||
yellow: 'text-util-colors-warning-warning-600',
|
||||
gray: 'text-text-tertiary',
|
||||
const STATUS_TEXT_COLOR_MAP: Record<StatusDotStatus, string> = {
|
||||
success: 'text-util-colors-green-green-600',
|
||||
warning: 'text-util-colors-warning-warning-600',
|
||||
error: 'text-util-colors-red-red-600',
|
||||
normal: 'text-util-colors-blue-light-blue-light-600',
|
||||
disabled: 'text-text-tertiary',
|
||||
}
|
||||
type StatusItemProps = {
|
||||
status: DocumentDisplayStatus
|
||||
@ -43,6 +42,7 @@ const StatusItem = ({ status, reverse = false, scene = 'list', textCls = '', err
|
||||
const { t } = useTranslation()
|
||||
const DOC_INDEX_STATUS_MAP = useIndexStatus()
|
||||
const localStatus = status.toLowerCase() as keyof typeof DOC_INDEX_STATUS_MAP
|
||||
const statusItem = DOC_INDEX_STATUS_MAP[localStatus]
|
||||
const { enabled = false, archived = false, id = '' } = detail || {}
|
||||
const { mutateAsync: enableDocument } = useDocumentEnable()
|
||||
const { mutateAsync: disableDocument } = useDocumentDisable()
|
||||
@ -78,9 +78,9 @@ const StatusItem = ({ status, reverse = false, scene = 'list', textCls = '', err
|
||||
}, [localStatus])
|
||||
return (
|
||||
<div className={cn('flex items-center', reverse ? 'flex-row-reverse' : '', scene === 'detail' ? s.statusItemDetail : '')}>
|
||||
<Indicator color={DOC_INDEX_STATUS_MAP[localStatus]?.color as IndicatorProps['color']} className={reverse ? 'ml-2' : 'mr-2'} />
|
||||
<span className={cn(`${STATUS_TEXT_COLOR_MAP[DOC_INDEX_STATUS_MAP[localStatus].color as keyof typeof STATUS_TEXT_COLOR_MAP]} text-sm`, textCls)}>
|
||||
{DOC_INDEX_STATUS_MAP[localStatus]?.text}
|
||||
<StatusDot status={statusItem.status} className={reverse ? 'ml-2' : 'mr-2'} />
|
||||
<span className={cn(`${STATUS_TEXT_COLOR_MAP[statusItem.status]} text-sm`, textCls)}>
|
||||
{statusItem.text}
|
||||
</span>
|
||||
{errorMessage && (
|
||||
<Infotip
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
||||
import { Switch } from '@langgenius/dify-ui/switch'
|
||||
import { RiArrowRightUpLine, RiBookOpenLine } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import { useSelector as useAppContextSelector } from '@/context/app-context'
|
||||
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
|
||||
import { useDatasetApiAccessUrl } from '@/hooks/use-api-access-url'
|
||||
@ -44,9 +44,9 @@ const Card = ({
|
||||
<div className="p-2">
|
||||
<div className="mb-1.5 flex justify-between">
|
||||
<div className="flex items-center gap-1">
|
||||
<Indicator
|
||||
<StatusDot
|
||||
className="shrink-0"
|
||||
color={apiEnabled ? 'green' : 'yellow'}
|
||||
status={apiEnabled ? 'success' : 'warning'}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
||||
import * as React from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ApiAggregate } from '@/app/components/base/icons/src/vender/knowledge'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import Card from './card'
|
||||
|
||||
type ApiAccessProps = {
|
||||
@ -36,9 +36,9 @@ const ApiAccess = ({
|
||||
>
|
||||
<ApiAggregate className="size-4 shrink-0 text-text-secondary" />
|
||||
{expand && <div className="grow system-sm-medium text-text-secondary">{t('appMenus.apiAccess', { ns: 'common' })}</div>}
|
||||
<Indicator
|
||||
<StatusDot
|
||||
className={cn('shrink-0', !expand && 'absolute -top-px -right-px')}
|
||||
color={apiEnabled ? 'green' : 'yellow'}
|
||||
status={apiEnabled ? 'success' : 'warning'}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { PopoverClose } from '@langgenius/dify-ui/popover'
|
||||
import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
||||
import { RiBookOpenLine, RiKey2Line } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import CopyFeedback from '@/app/components/base/copy-feedback'
|
||||
import { ApiAggregate } from '@/app/components/base/icons/src/vender/knowledge'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import { useDatasetApiAccessUrl } from '@/hooks/use-api-access-url'
|
||||
import Link from '@/next/link'
|
||||
|
||||
@ -35,10 +35,10 @@ const Card = ({
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-x-1">
|
||||
<Indicator
|
||||
<StatusDot
|
||||
className="shrink-0"
|
||||
color={
|
||||
apiBaseUrl ? 'green' : 'yellow'
|
||||
status={
|
||||
apiBaseUrl ? 'success' : 'warning'
|
||||
}
|
||||
/>
|
||||
<div
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import SecretKeyModal from '@/app/components/develop/secret-key/secret-key-modal'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import Card from './card'
|
||||
|
||||
type ServiceApiProps = {
|
||||
@ -40,10 +40,10 @@ const ServiceApi = ({
|
||||
open ? 'bg-components-button-secondary-bg-hover' : 'hover:bg-components-button-secondary-bg-hover',
|
||||
)}
|
||||
>
|
||||
<Indicator
|
||||
<StatusDot
|
||||
className={cn('shrink-0')}
|
||||
color={
|
||||
apiBaseUrl ? 'green' : 'yellow'
|
||||
status={
|
||||
apiBaseUrl ? 'success' : 'warning'
|
||||
}
|
||||
/>
|
||||
<div className="grow system-sm-medium text-text-secondary">{t('serviceApi.title', { ns: 'dataset' })}</div>
|
||||
|
||||
@ -353,8 +353,7 @@ describe('AccountDropdown', () => {
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
|
||||
// Assert
|
||||
const indicator = screen.getByTestId('status-indicator')
|
||||
expect(indicator).toHaveClass('bg-components-badge-status-light-warning-bg')
|
||||
expect(document.querySelector('.bg-components-badge-status-light-warning-bg')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show green indicator when version is latest', () => {
|
||||
@ -374,8 +373,7 @@ describe('AccountDropdown', () => {
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
|
||||
// Assert
|
||||
const indicator = screen.getByTestId('status-indicator')
|
||||
expect(indicator).toHaveClass('bg-components-badge-status-light-success-bg')
|
||||
expect(document.querySelector('.bg-components-badge-status-light-success-bg')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
import type { MouseEventHandler, ReactNode } from 'react'
|
||||
import { Avatar } from '@langgenius/dify-ui/avatar'
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLinkItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@langgenius/dify-ui/dropdown-menu'
|
||||
import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -22,7 +23,6 @@ import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||
import { useLogout } from '@/service/use-common'
|
||||
import AccountAbout from '../account-about'
|
||||
import GithubStar from '../github-star'
|
||||
import Indicator from '../indicator'
|
||||
import Compliance from './compliance'
|
||||
import { ExternalLinkIndicator, MenuItemContent } from './menu-item-content'
|
||||
import Support from './support'
|
||||
@ -216,7 +216,7 @@ export default function AppSelector() {
|
||||
trailing={(
|
||||
<div className="flex shrink-0 items-center">
|
||||
<div className="mr-2 system-xs-regular text-text-tertiary">{langGeniusVersionInfo.current_version}</div>
|
||||
<Indicator color={langGeniusVersionInfo.current_version === langGeniusVersionInfo.latest_version ? 'green' : 'orange'} />
|
||||
<StatusDot status={langGeniusVersionInfo.current_version === langGeniusVersionInfo.latest_version ? 'success' : 'warning'} />
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
@ -2,13 +2,13 @@ import type {
|
||||
DataSourceCredential,
|
||||
} from './types'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
||||
import {
|
||||
memo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import Operator from './operator'
|
||||
|
||||
type ItemProps = {
|
||||
@ -76,7 +76,7 @@ const Item = ({
|
||||
}
|
||||
<div className="flex shrink-0 items-center">
|
||||
<div className="mr-1 flex size-3 items-center justify-center">
|
||||
<Indicator color="green" />
|
||||
<StatusDot status="success" />
|
||||
</div>
|
||||
<div className="system-xs-semibold-uppercase text-util-colors-green-green-600">
|
||||
connected
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { Status } from './declarations'
|
||||
import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Indicator from '../../indicator'
|
||||
|
||||
type OperateProps = {
|
||||
isOpen: boolean
|
||||
@ -71,13 +71,13 @@ const Operate = ({
|
||||
status === 'fail' && (
|
||||
<div className="mr-4 flex items-center">
|
||||
<div className="text-xs text-[#D92D20]">{t('provider.invalidApiKey', { ns: 'common' })}</div>
|
||||
<Indicator color="red" className="ml-2" />
|
||||
<StatusDot status="error" className="ml-2" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
status === 'success' && (
|
||||
<Indicator color="green" className="mr-4" />
|
||||
<StatusDot status="success" className="mr-4" />
|
||||
)
|
||||
}
|
||||
<div
|
||||
|
||||
@ -8,8 +8,8 @@ vi.mock('@remixicon/react', () => ({
|
||||
}))
|
||||
|
||||
// Mock Indicator
|
||||
vi.mock('@/app/components/header/indicator', () => ({
|
||||
default: ({ color }: { color: string }) => <div data-testid={`indicator-${color}`} />,
|
||||
vi.mock('@langgenius/dify-ui/status-dot', () => ({
|
||||
StatusDot: ({ status }: { status: string }) => <div data-testid={`indicator-${status}`} />,
|
||||
}))
|
||||
|
||||
describe('ConfigModel', () => {
|
||||
@ -19,7 +19,7 @@ describe('ConfigModel', () => {
|
||||
|
||||
expect(screen.getByText(/modelProvider.auth.authorizationError/)).toBeInTheDocument()
|
||||
expect(screen.getByTestId('scales-icon')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('indicator-orange')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('indicator-warning')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByText(/modelProvider.auth.authorizationError/))
|
||||
expect(onClick).toHaveBeenCalled()
|
||||
@ -29,7 +29,7 @@ describe('ConfigModel', () => {
|
||||
render(<ConfigModel credentialRemoved />)
|
||||
|
||||
expect(screen.getByText(/modelProvider.auth.credentialRemoved/)).toBeInTheDocument()
|
||||
expect(screen.getByTestId('indicator-red')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('indicator-error')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render standard config message when no flags enabled', () => {
|
||||
|
||||
@ -10,8 +10,8 @@ vi.mock('../authorized/credential-item', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/header/indicator', () => ({
|
||||
default: () => <div data-testid="indicator" />,
|
||||
vi.mock('@langgenius/dify-ui/status-dot', () => ({
|
||||
StatusDot: () => <div data-testid="indicator" />,
|
||||
}))
|
||||
|
||||
vi.mock('@remixicon/react', () => ({
|
||||
|
||||
@ -15,8 +15,8 @@ vi.mock('../authorized', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/header/indicator', () => ({
|
||||
default: ({ color }: { color: string }) => <div data-testid={`indicator-${color}`} />,
|
||||
vi.mock('@langgenius/dify-ui/status-dot', () => ({
|
||||
StatusDot: ({ status }: { status: string }) => <div data-testid={`indicator-${status}`} />,
|
||||
}))
|
||||
|
||||
vi.mock('@remixicon/react', () => ({
|
||||
@ -58,7 +58,7 @@ describe('SwitchCredentialInLoadBalancing', () => {
|
||||
)
|
||||
|
||||
expect(screen.getByText('Key 1'))!.toBeInTheDocument()
|
||||
expect(screen.getByTestId('indicator-green'))!.toBeInTheDocument()
|
||||
expect(screen.getByTestId('indicator-success'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render auth removed status when selected credential is not in list', () => {
|
||||
@ -73,7 +73,7 @@ describe('SwitchCredentialInLoadBalancing', () => {
|
||||
)
|
||||
|
||||
expect(screen.getByText(/modelProvider.auth.authRemoved/))!.toBeInTheDocument()
|
||||
expect(screen.getByTestId('indicator-red'))!.toBeInTheDocument()
|
||||
expect(screen.getByTestId('indicator-error'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render unavailable status when credentials list is empty', () => {
|
||||
@ -156,7 +156,7 @@ describe('SwitchCredentialInLoadBalancing', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('indicator-red'))!.toBeInTheDocument()
|
||||
expect(screen.getByTestId('indicator-error'))!.toBeInTheDocument()
|
||||
expect(screen.getByText(/auth.credentialUnavailableInButton/))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
@ -244,9 +244,9 @@ describe('SwitchCredentialInLoadBalancing', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// indicator-green shown (not authRemoved, not unavailable, not empty)
|
||||
// indicator-green shown (not authRemoved, not unavailable, not empty)
|
||||
expect(screen.getByTestId('indicator-green'))!.toBeInTheDocument()
|
||||
// indicator-success shown (not authRemoved, not unavailable, not empty)
|
||||
// indicator-success shown (not authRemoved, not unavailable, not empty)
|
||||
expect(screen.getByTestId('indicator-success'))!.toBeInTheDocument()
|
||||
// credential_name is empty so nothing printed for name
|
||||
// credential_name is empty so nothing printed for name
|
||||
// credential_name is empty so nothing printed for name
|
||||
|
||||
@ -2,8 +2,8 @@ import type { Credential } from '../../../declarations'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import CredentialItem from '../credential-item'
|
||||
|
||||
vi.mock('@/app/components/header/indicator', () => ({
|
||||
default: () => <div data-testid="indicator" />,
|
||||
vi.mock('@langgenius/dify-ui/status-dot', () => ({
|
||||
StatusDot: () => <div data-testid="indicator" />,
|
||||
}))
|
||||
|
||||
describe('CredentialItem', () => {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { Credential } from '../../declarations'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import {
|
||||
memo,
|
||||
@ -8,7 +9,6 @@ import {
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
|
||||
type CredentialItemProps = {
|
||||
credential: Credential
|
||||
@ -71,7 +71,7 @@ const CredentialItem = ({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<Indicator className="mr-1.5 ml-2 shrink-0" />
|
||||
<StatusDot className="mr-1.5 ml-2 shrink-0" />
|
||||
<div
|
||||
className="truncate system-md-regular text-text-secondary"
|
||||
title={credential.credential_name}
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
||||
import {
|
||||
RiEqualizer2Line,
|
||||
RiScales3Line,
|
||||
} from '@remixicon/react'
|
||||
import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
|
||||
type ConfigModelProps = {
|
||||
onClick?: () => void
|
||||
@ -30,7 +30,7 @@ const ConfigModel = ({
|
||||
>
|
||||
<RiScales3Line className="mr-0.5 size-3" />
|
||||
{t('modelProvider.auth.authorizationError', { ns: 'common' })}
|
||||
<Indicator color="orange" className="absolute -top-px -right-px size-1.5" />
|
||||
<StatusDot status="warning" className="absolute -top-px -right-px size-1.5" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -49,7 +49,7 @@ const ConfigModel = ({
|
||||
credentialRemoved && (
|
||||
<>
|
||||
{t('modelProvider.auth.credentialRemoved', { ns: 'common' })}
|
||||
<Indicator color="red" className="ml-2" />
|
||||
<StatusDot status="error" className="ml-2" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import {
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@langgenius/dify-ui/popover'
|
||||
import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
||||
import {
|
||||
RiAddLine,
|
||||
RiArrowDownSLine,
|
||||
@ -15,7 +16,6 @@ import {
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import CredentialItem from './authorized/credential-item'
|
||||
|
||||
type CredentialSelectorProps = {
|
||||
@ -60,7 +60,7 @@ const CredentialSelector = ({
|
||||
selectedCredential && (
|
||||
<div className="flex items-center">
|
||||
{
|
||||
!selectedCredential.addNewCredential && <Indicator className="mr-2 ml-1 shrink-0" />
|
||||
!selectedCredential.addNewCredential && <StatusDot className="mr-2 ml-1 shrink-0" />
|
||||
}
|
||||
<div className="truncate system-sm-regular text-components-input-text-filled" title={selectedCredential.credential_name}>{selectedCredential.credential_name}</div>
|
||||
{
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import type { StatusDotStatus } from '@langgenius/dify-ui/status-dot'
|
||||
import type { Dispatch, SetStateAction } from 'react'
|
||||
import type {
|
||||
Credential,
|
||||
@ -6,6 +7,7 @@ import type {
|
||||
} from '../declarations'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import { RiArrowDownSLine } from '@remixicon/react'
|
||||
import {
|
||||
@ -15,7 +17,6 @@ import {
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
import { ConfigurationMethodEnum, ModelModalModeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import Authorized from './authorized'
|
||||
|
||||
type SwitchCredentialInLoadBalancingProps = {
|
||||
@ -49,9 +50,9 @@ const SwitchCredentialInLoadBalancing = ({
|
||||
const authRemoved = selectedCredentialId && !currentCredential && !empty
|
||||
const unavailable = currentCredential?.not_allowed_to_use
|
||||
|
||||
let color = 'green'
|
||||
let color: StatusDotStatus = 'success'
|
||||
if (authRemoved || unavailable)
|
||||
color = 'red'
|
||||
color = 'error'
|
||||
|
||||
const Item = (
|
||||
<Button
|
||||
@ -64,9 +65,9 @@ const SwitchCredentialInLoadBalancing = ({
|
||||
>
|
||||
{
|
||||
!empty && (
|
||||
<Indicator
|
||||
<StatusDot
|
||||
className="mr-2"
|
||||
color={color as any}
|
||||
status={color}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ConfigurationMethodEnum } from '../declarations'
|
||||
|
||||
@ -22,9 +23,7 @@ const ConfigurationButton = ({ modelProvider, handleOpenModal }: ConfigurationBu
|
||||
{t('nodes.agent.notAuthorized', { ns: 'workflow' })}
|
||||
</div>
|
||||
<div className="flex h-[14px] w-[14px] items-center justify-center">
|
||||
<div className="h-2 w-2 shrink-0 rounded-[3px] border border-components-badge-status-light-warning-border-inner
|
||||
bg-components-badge-status-light-warning-bg shadow-components-badge-status-light-warning-halo"
|
||||
/>
|
||||
<StatusDot status="warning" />
|
||||
</div>
|
||||
</Button>
|
||||
)
|
||||
|
||||
@ -4,6 +4,7 @@ import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { ComboboxGroup, ComboboxItem, ComboboxItemIndicator } from '@langgenius/dify-ui/combobox'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
import { PreviewCardTrigger } from '@langgenius/dify-ui/preview-card'
|
||||
import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { CreditsCoin } from '@/app/components/base/icons/src/vender/line/financeAndECommerce'
|
||||
@ -122,13 +123,13 @@ function PopupItem({
|
||||
: credentialName
|
||||
? (
|
||||
<>
|
||||
<span className={cn('size-1.5 shrink-0 rounded-xs border', isApiKeyActive ? 'border-components-badge-status-light-success-border-inner bg-components-badge-status-light-success-bg' : 'border-components-badge-status-light-error-border-inner bg-components-badge-status-light-error-bg')} />
|
||||
<StatusDot size="small" status={isApiKeyActive ? 'success' : 'error'} />
|
||||
<span className="ml-1 truncate text-text-tertiary">{credentialName}</span>
|
||||
</>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
<span className="size-1.5 shrink-0 rounded-xs border border-components-badge-status-light-disabled-border-inner bg-components-badge-status-light-disabled-bg" />
|
||||
<StatusDot size="small" status="disabled" />
|
||||
<span className="ml-1 truncate text-text-tertiary">{t('modelProvider.selector.configureRequired', { ns: 'common' })}</span>
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -85,8 +85,8 @@ vi.mock('../model-auth-dropdown', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/header/indicator', () => ({
|
||||
default: ({ color }: { color: string }) => <div data-testid="indicator" data-color={color} />,
|
||||
vi.mock('@langgenius/dify-ui/status-dot', () => ({
|
||||
StatusDot: ({ status }: { status: string }) => <div data-testid="indicator" data-status={status} />,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/icons/src/vender/line/alertsAndFeedback/Warning', () => ({
|
||||
@ -192,7 +192,7 @@ describe('CredentialPanel', () => {
|
||||
it('should show green indicator and credential name for api-fallback (exhausted + authorized key)', () => {
|
||||
mockTrialCredits.isExhausted = true
|
||||
renderWithQueryClient(createProvider())
|
||||
expect(screen.getByTestId('indicator')).toHaveAttribute('data-color', 'green')
|
||||
expect(screen.getByTestId('indicator')).toHaveAttribute('data-status', 'success')
|
||||
expect(screen.getByText('test-credential')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
@ -206,7 +206,7 @@ describe('CredentialPanel', () => {
|
||||
renderWithQueryClient(createProvider({
|
||||
preferred_provider_type: PreferredProviderTypeEnum.custom,
|
||||
}))
|
||||
expect(screen.getByTestId('indicator')).toHaveAttribute('data-color', 'green')
|
||||
expect(screen.getByTestId('indicator')).toHaveAttribute('data-status', 'success')
|
||||
expect(screen.getByText('test-credential')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
@ -228,7 +228,7 @@ describe('CredentialPanel', () => {
|
||||
available_credentials: [{ credential_id: 'cred-1', credential_name: 'Bad Key' }],
|
||||
},
|
||||
}))
|
||||
expect(screen.getByTestId('indicator')).toHaveAttribute('data-color', 'red')
|
||||
expect(screen.getByTestId('indicator')).toHaveAttribute('data-status', 'error')
|
||||
expect(screen.getByText('Bad Key')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import type { ModelProvider } from '../declarations'
|
||||
import type { CardVariant } from './use-credential-panel-state'
|
||||
import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
||||
import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Warning from '@/app/components/base/icons/src/vender/line/alertsAndFeedback/Warning'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import ModelAuthDropdown from './model-auth-dropdown'
|
||||
import SystemQuotaCard from './system-quota-card'
|
||||
import { useChangeProviderPriority } from './use-change-provider-priority'
|
||||
@ -38,7 +38,7 @@ const CredentialPanel = ({
|
||||
<SystemQuotaCard.Label className={needsGap ? 'gap-1' : undefined}>
|
||||
{isTextLabel
|
||||
? <TextLabel variant={variant} />
|
||||
: <StatusLabel variant={variant} credentialName={credentialName} />}
|
||||
: <CredentialStatus variant={variant} credentialName={credentialName} />}
|
||||
</SystemQuotaCard.Label>
|
||||
<SystemQuotaCard.Actions>
|
||||
<ModelAuthDropdown
|
||||
@ -78,17 +78,17 @@ function TextLabel({ variant }: { variant: CardVariant }) {
|
||||
)
|
||||
}
|
||||
|
||||
function StatusLabel({ variant, credentialName }: {
|
||||
function CredentialStatus({ variant, credentialName }: {
|
||||
variant: CardVariant
|
||||
credentialName: string | undefined
|
||||
}) {
|
||||
const isDestructive = isDestructiveVariant(variant)
|
||||
const dotColor = isDestructive ? 'red' : 'green'
|
||||
const dotColor = isDestructive ? 'error' : 'success'
|
||||
const showWarning = variant === 'api-fallback'
|
||||
|
||||
return (
|
||||
<>
|
||||
<Indicator className="shrink-0" color={dotColor} />
|
||||
<StatusDot className="shrink-0" status={dotColor} />
|
||||
<span
|
||||
className={`truncate ${isDestructive ? 'text-text-destructive' : 'text-text-secondary'}`}
|
||||
title={credentialName}
|
||||
|
||||
@ -9,6 +9,7 @@ import type {
|
||||
ModelProvider,
|
||||
} from '../declarations'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
||||
import { Switch } from '@langgenius/dify-ui/switch'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
@ -21,7 +22,6 @@ import s from '@/app/components/custom/style.module.css'
|
||||
import { AddCredentialInLoadBalancing } from '@/app/components/header/account-setting/model-provider-page/model-auth'
|
||||
import { IS_CE_EDITION } from '@/config'
|
||||
import { useProviderContextSelector } from '@/context/provider-context'
|
||||
import Indicator from '../../../indicator'
|
||||
import { ConfigurationMethodEnum } from '../declarations'
|
||||
import CooldownTimer from './cooldown-timer'
|
||||
|
||||
@ -194,7 +194,7 @@ const ModelLoadBalancingConfigs = ({
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<Indicator color={credential?.not_allowed_to_use ? 'gray' : 'green'} />
|
||||
<StatusDot status={credential?.not_allowed_to_use ? 'disabled' : 'success'} />
|
||||
)}
|
||||
/>
|
||||
<TooltipContent>
|
||||
|
||||
@ -1,79 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import Indicator from '../index'
|
||||
|
||||
describe('Indicator', () => {
|
||||
it('should render with default props', () => {
|
||||
render(<Indicator />)
|
||||
const indicator = screen.getByTestId('status-indicator')
|
||||
expect(indicator).toBeInTheDocument()
|
||||
expect(indicator).toHaveClass(
|
||||
'bg-components-badge-status-light-success-bg',
|
||||
)
|
||||
expect(indicator).toHaveClass(
|
||||
'border-components-badge-status-light-success-border-inner',
|
||||
)
|
||||
expect(indicator).toHaveClass('shadow-status-indicator-green-shadow')
|
||||
})
|
||||
|
||||
it('should render with orange color', () => {
|
||||
render(<Indicator color="orange" />)
|
||||
const indicator = screen.getByTestId('status-indicator')
|
||||
expect(indicator).toHaveClass(
|
||||
'bg-components-badge-status-light-warning-bg',
|
||||
)
|
||||
expect(indicator).toHaveClass(
|
||||
'border-components-badge-status-light-warning-border-inner',
|
||||
)
|
||||
expect(indicator).toHaveClass('shadow-status-indicator-warning-shadow')
|
||||
})
|
||||
|
||||
it('should render with red color', () => {
|
||||
render(<Indicator color="red" />)
|
||||
const indicator = screen.getByTestId('status-indicator')
|
||||
expect(indicator).toHaveClass('bg-components-badge-status-light-error-bg')
|
||||
expect(indicator).toHaveClass(
|
||||
'border-components-badge-status-light-error-border-inner',
|
||||
)
|
||||
expect(indicator).toHaveClass('shadow-status-indicator-red-shadow')
|
||||
})
|
||||
|
||||
it('should render with blue color', () => {
|
||||
render(<Indicator color="blue" />)
|
||||
const indicator = screen.getByTestId('status-indicator')
|
||||
expect(indicator).toHaveClass('bg-components-badge-status-light-normal-bg')
|
||||
expect(indicator).toHaveClass(
|
||||
'border-components-badge-status-light-normal-border-inner',
|
||||
)
|
||||
expect(indicator).toHaveClass('shadow-status-indicator-blue-shadow')
|
||||
})
|
||||
|
||||
it('should render with yellow color', () => {
|
||||
render(<Indicator color="yellow" />)
|
||||
const indicator = screen.getByTestId('status-indicator')
|
||||
expect(indicator).toHaveClass(
|
||||
'bg-components-badge-status-light-warning-bg',
|
||||
)
|
||||
expect(indicator).toHaveClass(
|
||||
'border-components-badge-status-light-warning-border-inner',
|
||||
)
|
||||
expect(indicator).toHaveClass('shadow-status-indicator-warning-shadow')
|
||||
})
|
||||
|
||||
it('should render with gray color', () => {
|
||||
render(<Indicator color="gray" />)
|
||||
const indicator = screen.getByTestId('status-indicator')
|
||||
expect(indicator).toHaveClass(
|
||||
'bg-components-badge-status-light-disabled-bg',
|
||||
)
|
||||
expect(indicator).toHaveClass(
|
||||
'border-components-badge-status-light-disabled-border-inner',
|
||||
)
|
||||
expect(indicator).toHaveClass('shadow-status-indicator-gray-shadow')
|
||||
})
|
||||
|
||||
it('should apply custom className', () => {
|
||||
render(<Indicator className="custom-class" />)
|
||||
const indicator = screen.getByTestId('status-indicator')
|
||||
expect(indicator).toHaveClass('custom-class')
|
||||
})
|
||||
})
|
||||
@ -1,54 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
|
||||
export type IndicatorProps = {
|
||||
color?: 'green' | 'orange' | 'red' | 'blue' | 'yellow' | 'gray'
|
||||
className?: string
|
||||
}
|
||||
|
||||
export type ColorMap = {
|
||||
green: string
|
||||
orange: string
|
||||
red: string
|
||||
blue: string
|
||||
yellow: string
|
||||
gray: string
|
||||
}
|
||||
|
||||
const BACKGROUND_MAP: ColorMap = {
|
||||
green: 'bg-components-badge-status-light-success-bg',
|
||||
orange: 'bg-components-badge-status-light-warning-bg',
|
||||
red: 'bg-components-badge-status-light-error-bg',
|
||||
blue: 'bg-components-badge-status-light-normal-bg',
|
||||
yellow: 'bg-components-badge-status-light-warning-bg',
|
||||
gray: 'bg-components-badge-status-light-disabled-bg',
|
||||
}
|
||||
const BORDER_MAP: ColorMap = {
|
||||
green: 'border-components-badge-status-light-success-border-inner',
|
||||
orange: 'border-components-badge-status-light-warning-border-inner',
|
||||
red: 'border-components-badge-status-light-error-border-inner',
|
||||
blue: 'border-components-badge-status-light-normal-border-inner',
|
||||
yellow: 'border-components-badge-status-light-warning-border-inner',
|
||||
gray: 'border-components-badge-status-light-disabled-border-inner',
|
||||
}
|
||||
const SHADOW_MAP: ColorMap = {
|
||||
green: 'shadow-status-indicator-green-shadow',
|
||||
orange: 'shadow-status-indicator-warning-shadow',
|
||||
red: 'shadow-status-indicator-red-shadow',
|
||||
blue: 'shadow-status-indicator-blue-shadow',
|
||||
yellow: 'shadow-status-indicator-warning-shadow',
|
||||
gray: 'shadow-status-indicator-gray-shadow',
|
||||
}
|
||||
|
||||
export default function Indicator({
|
||||
color = 'green',
|
||||
className = '',
|
||||
}: IndicatorProps) {
|
||||
return (
|
||||
<div
|
||||
data-testid="status-indicator"
|
||||
className={cn('h-2 w-2 rounded-[3px] border border-solid', BACKGROUND_MAP[color], BORDER_MAP[color], SHADOW_MAP[color], className)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -5,6 +5,9 @@ import { useSelectedLayoutSegment } from '@/next/navigation'
|
||||
|
||||
import PluginsNav from '../index'
|
||||
|
||||
const queryErrorStatusDot = (container: HTMLElement) =>
|
||||
container.querySelector('.shadow-status-indicator-red-shadow')
|
||||
|
||||
vi.mock('@/next/navigation', () => ({
|
||||
useSelectedLayoutSegment: vi.fn(),
|
||||
}))
|
||||
@ -38,7 +41,7 @@ describe('PluginsNav', () => {
|
||||
const svg = linkElement.querySelector('svg')
|
||||
expect(svg).toBeInTheDocument()
|
||||
|
||||
expect(screen.queryByTestId('status-indicator')).not.toBeInTheDocument()
|
||||
expect(queryErrorStatusDot(linkElement)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
describe('Active State', () => {
|
||||
@ -70,7 +73,7 @@ describe('PluginsNav', () => {
|
||||
expect(svgs.length).toBe(1)
|
||||
expect(svgs[0]).toHaveClass('install-icon')
|
||||
|
||||
expect(screen.queryByTestId('status-indicator')).not.toBeInTheDocument()
|
||||
expect(queryErrorStatusDot(container)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders Installing With Error state (Inactive)', () => {
|
||||
@ -81,7 +84,7 @@ describe('PluginsNav', () => {
|
||||
const downloadingIcon = container.querySelector('.install-icon')
|
||||
expect(downloadingIcon).toBeInTheDocument()
|
||||
|
||||
expect(screen.getByTestId('status-indicator')).toBeInTheDocument()
|
||||
expect(queryErrorStatusDot(container)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders Failed state (Inactive)', () => {
|
||||
@ -93,7 +96,7 @@ describe('PluginsNav', () => {
|
||||
expect(svg).toBeInTheDocument()
|
||||
expect(svg).not.toHaveClass('install-icon')
|
||||
|
||||
expect(screen.getByTestId('status-indicator')).toBeInTheDocument()
|
||||
expect(queryErrorStatusDot(container)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders Default icon when Active even if installing', () => {
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
'use client'
|
||||
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Group } from '@/app/components/base/icons/src/vender/other'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import { usePluginTaskStatus } from '@/app/components/plugins/plugin-page/plugin-tasks/hooks'
|
||||
import Link from '@/next/link'
|
||||
import { useSelectedLayoutSegment } from '@/next/navigation'
|
||||
@ -37,8 +37,8 @@ const PluginsNav = ({
|
||||
>
|
||||
{
|
||||
(isFailed || isInstallingWithError) && !activated && (
|
||||
<Indicator
|
||||
color="red"
|
||||
<StatusDot
|
||||
status="error"
|
||||
className="absolute -top-px -left-px"
|
||||
/>
|
||||
)
|
||||
|
||||
@ -2,8 +2,8 @@ import { cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import AuthorizedInDataSourceNode from '../authorized-in-data-source-node'
|
||||
|
||||
vi.mock('@/app/components/header/indicator', () => ({
|
||||
default: ({ color }: { color: string }) => <span data-testid="indicator" data-color={color} />,
|
||||
vi.mock('@langgenius/dify-ui/status-dot', () => ({
|
||||
StatusDot: ({ status }: { status: string }) => <span data-testid="indicator" data-status={status} />,
|
||||
}))
|
||||
|
||||
describe('AuthorizedInDataSourceNode', () => {
|
||||
@ -19,7 +19,7 @@ describe('AuthorizedInDataSourceNode', () => {
|
||||
|
||||
it('renders with green indicator', () => {
|
||||
render(<AuthorizedInDataSourceNode authorizationsNum={1} onJumpToDataSourcePage={mockOnJump} />)
|
||||
expect(screen.getByTestId('indicator')).toHaveAttribute('data-color', 'green')
|
||||
expect(screen.getByTestId('indicator')).toHaveAttribute('data-status', 'success')
|
||||
})
|
||||
|
||||
it('renders singular text for 1 authorization', () => {
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
||||
import { RiEqualizer2Line } from '@remixicon/react'
|
||||
import {
|
||||
memo,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
|
||||
type AuthorizedInDataSourceNodeProps = {
|
||||
authorizationsNum: number
|
||||
@ -22,9 +22,9 @@ const AuthorizedInDataSourceNode = ({
|
||||
size="small"
|
||||
onClick={onJumpToDataSourcePage}
|
||||
>
|
||||
<Indicator
|
||||
<StatusDot
|
||||
className="mr-1.5"
|
||||
color="green"
|
||||
status="success"
|
||||
/>
|
||||
{
|
||||
authorizationsNum > 1
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import type { StatusDotStatus } from '@langgenius/dify-ui/status-dot'
|
||||
import type {
|
||||
Credential,
|
||||
PluginPayload,
|
||||
} from './types'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
||||
import { RiArrowDownSLine } from '@remixicon/react'
|
||||
import {
|
||||
memo,
|
||||
@ -11,7 +13,6 @@ import {
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import {
|
||||
Authorized,
|
||||
usePluginAuth,
|
||||
@ -41,7 +42,7 @@ const AuthorizedInNode = ({
|
||||
let label = ''
|
||||
let removed = false
|
||||
let unavailable = false
|
||||
let color = 'green'
|
||||
let color: StatusDotStatus = 'success'
|
||||
let defaultUnavailable = false
|
||||
if (!credentialId) {
|
||||
label = t('auth.workspaceDefault', { ns: 'plugin' })
|
||||
@ -49,7 +50,7 @@ const AuthorizedInNode = ({
|
||||
const defaultCredential = credentials.find(c => c.is_default)
|
||||
|
||||
if (defaultCredential?.not_allowed_to_use) {
|
||||
color = 'gray'
|
||||
color = 'disabled'
|
||||
defaultUnavailable = true
|
||||
}
|
||||
}
|
||||
@ -60,9 +61,9 @@ const AuthorizedInNode = ({
|
||||
unavailable = !!credential?.not_allowed_to_use && !credential?.from_enterprise
|
||||
|
||||
if (removed)
|
||||
color = 'red'
|
||||
color = 'error'
|
||||
else if (unavailable)
|
||||
color = 'gray'
|
||||
color = 'disabled'
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
@ -73,9 +74,9 @@ const AuthorizedInNode = ({
|
||||
)}
|
||||
variant={(defaultUnavailable || unavailable) ? 'ghost' : 'secondary'}
|
||||
>
|
||||
<Indicator
|
||||
<StatusDot
|
||||
className="mr-1.5"
|
||||
color={color as any}
|
||||
status={color}
|
||||
/>
|
||||
{label}
|
||||
{
|
||||
|
||||
@ -238,9 +238,7 @@ describe('Authorized Component', () => {
|
||||
{ wrapper: createWrapper() },
|
||||
)
|
||||
|
||||
// The indicator should be rendered
|
||||
// The indicator should be rendered
|
||||
expect(container.querySelector('[data-testid="status-indicator"]'))!.toBeInTheDocument()
|
||||
expect(container.querySelector('.shadow-status-indicator-gray-shadow'))!.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -18,6 +18,7 @@ import {
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@langgenius/dify-ui/popover'
|
||||
import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import {
|
||||
memo,
|
||||
@ -26,7 +27,6 @@ import {
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import Authorize from '../authorize'
|
||||
import ApiKeyModal from '../authorize/api-key-modal'
|
||||
import {
|
||||
@ -204,7 +204,7 @@ const Authorized = ({
|
||||
mergedIsOpen && 'bg-components-button-secondary-bg-hover',
|
||||
)}
|
||||
>
|
||||
<Indicator className="mr-2" color={unavailableCredential ? 'gray' : 'green'} />
|
||||
<StatusDot className="mr-2" status={unavailableCredential ? 'disabled' : 'success'} />
|
||||
{credentials.length}
|
||||
|
||||
{
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import type { Credential } from '../types'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import {
|
||||
RiCheckLine,
|
||||
@ -17,7 +18,6 @@ import { useTranslation } from 'react-i18next'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import { CredentialTypeEnum } from '../types'
|
||||
|
||||
type ItemProps = {
|
||||
@ -126,9 +126,9 @@ const Item = ({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<Indicator
|
||||
<StatusDot
|
||||
className="mr-1.5 ml-2 shrink-0"
|
||||
color={credential.not_allowed_to_use ? 'gray' : 'green'}
|
||||
status={credential.not_allowed_to_use ? 'disabled' : 'success'}
|
||||
/>
|
||||
<div
|
||||
className="truncate system-md-regular text-text-secondary"
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import type { StatusDotStatus } from '@langgenius/dify-ui/status-dot'
|
||||
import type {
|
||||
Credential,
|
||||
PluginPayload,
|
||||
} from './types'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
||||
import { RiArrowDownSLine } from '@remixicon/react'
|
||||
import {
|
||||
memo,
|
||||
@ -11,7 +13,6 @@ import {
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import Authorize from './authorize'
|
||||
import Authorized from './authorized'
|
||||
import { usePluginAuth } from './hooks/use-plugin-auth'
|
||||
@ -60,7 +61,7 @@ const PluginAuthInAgent = ({
|
||||
let label = ''
|
||||
let removed = false
|
||||
let unavailable = false
|
||||
let color = 'green'
|
||||
let color: StatusDotStatus = 'success'
|
||||
if (!credentialId) {
|
||||
label = t('auth.workspaceDefault', { ns: 'plugin' })
|
||||
}
|
||||
@ -70,9 +71,9 @@ const PluginAuthInAgent = ({
|
||||
removed = !credential
|
||||
unavailable = !!credential?.not_allowed_to_use && !credential?.from_enterprise
|
||||
if (removed)
|
||||
color = 'red'
|
||||
color = 'error'
|
||||
else if (unavailable)
|
||||
color = 'gray'
|
||||
color = 'disabled'
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
@ -82,9 +83,9 @@ const PluginAuthInAgent = ({
|
||||
removed && 'text-text-destructive',
|
||||
)}
|
||||
>
|
||||
<Indicator
|
||||
<StatusDot
|
||||
className="mr-2"
|
||||
color={color as any}
|
||||
status={color}
|
||||
/>
|
||||
{label}
|
||||
{
|
||||
|
||||
@ -72,8 +72,8 @@ vi.mock('@/service/use-endpoints', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/header/indicator', () => ({
|
||||
default: ({ color }: { color: string }) => <span data-testid="indicator" data-color={color} />,
|
||||
vi.mock('@langgenius/dify-ui/status-dot', () => ({
|
||||
StatusDot: ({ status }: { status: string }) => <span data-testid="indicator" data-status={status} />,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/tools/utils/to-form-schema', () => ({
|
||||
@ -176,7 +176,7 @@ describe('EndpointCard', () => {
|
||||
render(<EndpointCard pluginDetail={mockPluginDetail} data={mockEndpointData} handleChange={mockHandleChange} />)
|
||||
|
||||
expect(screen.getByText('plugin.detailPanel.serviceOk'))!.toBeInTheDocument()
|
||||
expect(screen.getByTestId('indicator'))!.toHaveAttribute('data-color', 'green')
|
||||
expect(screen.getByTestId('indicator'))!.toHaveAttribute('data-status', 'success')
|
||||
})
|
||||
|
||||
it('should show disabled status when not enabled', () => {
|
||||
@ -184,7 +184,7 @@ describe('EndpointCard', () => {
|
||||
render(<EndpointCard pluginDetail={mockPluginDetail} data={disabledData} handleChange={mockHandleChange} />)
|
||||
|
||||
expect(screen.getByText('plugin.detailPanel.disabled'))!.toBeInTheDocument()
|
||||
expect(screen.getByTestId('indicator'))!.toHaveAttribute('data-color', 'gray')
|
||||
expect(screen.getByTestId('indicator'))!.toHaveAttribute('data-status', 'disabled')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
// import { useAppContext } from '@/context/app-context'
|
||||
// import { Button } from '@langgenius/dify-ui/button'
|
||||
// import Indicator from '@/app/components/header/indicator'
|
||||
// import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
||||
// import ToolItem from '@/app/components/tools/provider/tool-item'
|
||||
// import ConfigCredential from '@/app/components/tools/setting/build-in/config-credentials'
|
||||
import type { PluginDetail } from '@/app/components/plugins/types'
|
||||
@ -60,7 +60,7 @@ const ActionList = ({
|
||||
onClick={() => setShowSettingAuth(true)}
|
||||
disabled={!isCurrentWorkspaceManager}
|
||||
>
|
||||
<Indicator className='mr-2' color={'green'} />
|
||||
<StatusDot className='mr-2' status={'success'} />
|
||||
{t('tools.auth.authorized')}
|
||||
</Button>
|
||||
)} */}
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
AlertDialogContent,
|
||||
AlertDialogTitle,
|
||||
} from '@langgenius/dify-ui/alert-dialog'
|
||||
import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
||||
import { Switch } from '@langgenius/dify-ui/switch'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
@ -18,7 +19,6 @@ import { useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import { CopyCheck } from '@/app/components/base/icons/src/vender/line/files'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import { addDefaultValue, toolCredentialToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
|
||||
import {
|
||||
useDeleteEndpoint,
|
||||
@ -199,13 +199,13 @@ const EndpointCard = ({
|
||||
<div className="flex items-center justify-between p-2 pl-3">
|
||||
{active && (
|
||||
<div className="flex items-center gap-1 system-xs-semibold-uppercase text-util-colors-green-green-600">
|
||||
<Indicator color="green" />
|
||||
<StatusDot status="success" />
|
||||
{t('detailPanel.serviceOk', { ns: 'plugin' })}
|
||||
</div>
|
||||
)}
|
||||
{!active && (
|
||||
<div className="flex items-center gap-1 system-xs-semibold-uppercase text-text-tertiary">
|
||||
<Indicator color="gray" />
|
||||
<StatusDot status="disabled" />
|
||||
{t('detailPanel.disabled', { ns: 'plugin' })}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
||||
import { Switch } from '@langgenius/dify-ui/switch'
|
||||
import {
|
||||
RiDeleteBinLine,
|
||||
@ -14,7 +15,6 @@ import { useTranslation } from 'react-i18next'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import { Group } from '@/app/components/base/icons/src/vender/other'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import { InstallPluginButton } from '@/app/components/workflow/nodes/_base/components/install-plugin-button'
|
||||
import { useMCPToolAvailability } from '@/app/components/workflow/nodes/_base/components/mcp-tool-availability'
|
||||
import McpToolNotSupportTooltip from '@/app/components/workflow/nodes/_base/components/mcp-tool-not-support-tooltip'
|
||||
@ -128,13 +128,13 @@ const ToolItem = ({
|
||||
{!isError && !uninstalled && !versionMismatch && noAuth && (
|
||||
<Button variant="secondary" size="small">
|
||||
{t('notAuthorized', { ns: 'tools' })}
|
||||
<Indicator className="ml-2" color="orange" />
|
||||
<StatusDot className="ml-2" status="warning" />
|
||||
</Button>
|
||||
)}
|
||||
{!isError && !uninstalled && !versionMismatch && authRemoved && (
|
||||
<Button variant="secondary" size="small">
|
||||
{t('auth.authRemoved', { ns: 'plugin' })}
|
||||
<Indicator className="ml-2" color="red" />
|
||||
<StatusDot className="ml-2" status="error" />
|
||||
</Button>
|
||||
)}
|
||||
{!isError && !uninstalled && versionMismatch && installInfo && (
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import TaskStatusIndicator from '../task-status-indicator'
|
||||
|
||||
vi.mock('@/app/components/base/progress-bar/progress-circle', () => ({
|
||||
default: ({ percentage }: { percentage: number }) => (
|
||||
<div data-testid="progress-circle" data-percentage={percentage} />
|
||||
vi.mock('@langgenius/dify-ui/progress', () => ({
|
||||
ProgressCircle: ({ value }: { value: number }) => (
|
||||
<div data-testid="progress-circle" data-value={value} />
|
||||
),
|
||||
}))
|
||||
|
||||
@ -68,7 +68,7 @@ describe('TaskStatusIndicator', () => {
|
||||
/>,
|
||||
)
|
||||
const progress = screen.getByTestId('progress-circle')
|
||||
expect(progress).toHaveAttribute('data-percentage', '40')
|
||||
expect(progress).toHaveAttribute('data-value', '40')
|
||||
})
|
||||
|
||||
it('should show progress circle when isInstallingWithSuccess', () => {
|
||||
@ -81,7 +81,7 @@ describe('TaskStatusIndicator', () => {
|
||||
/>,
|
||||
)
|
||||
const progress = screen.getByTestId('progress-circle')
|
||||
expect(progress).toHaveAttribute('data-percentage', '75')
|
||||
expect(progress).toHaveAttribute('data-value', '75')
|
||||
})
|
||||
|
||||
it('should show error progress circle when isInstallingWithError', () => {
|
||||
@ -106,7 +106,7 @@ describe('TaskStatusIndicator', () => {
|
||||
/>,
|
||||
)
|
||||
const progress = screen.getByTestId('progress-circle')
|
||||
expect(progress).toHaveAttribute('data-percentage', '0')
|
||||
expect(progress).toHaveAttribute('data-value', '0')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import type { FC } from 'react'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { ProgressCircle } from '@langgenius/dify-ui/progress'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import ProgressCircle from '@/app/components/base/progress-bar/progress-circle'
|
||||
import DownloadingIcon from '@/app/components/header/plugins-nav/downloading-icon'
|
||||
|
||||
type TaskStatusIndicatorProps = {
|
||||
@ -67,16 +67,15 @@ const TaskStatusIndicator: FC<TaskStatusIndicatorProps> = ({
|
||||
<div className="absolute -top-1 -right-1">
|
||||
{(isInstalling || isInstallingWithSuccess) && (
|
||||
<ProgressCircle
|
||||
percentage={(totalPluginsLength > 0 ? successPluginsLength / totalPluginsLength : 0) * 100}
|
||||
circleFillColor="fill-components-progress-brand-bg"
|
||||
value={(totalPluginsLength > 0 ? successPluginsLength / totalPluginsLength : 0) * 100}
|
||||
aria-label={tip}
|
||||
/>
|
||||
)}
|
||||
{isInstallingWithError && (
|
||||
<ProgressCircle
|
||||
percentage={(totalPluginsLength > 0 ? runningPluginsLength / totalPluginsLength : 0) * 100}
|
||||
circleFillColor="fill-components-progress-brand-bg"
|
||||
sectorFillColor="fill-components-progress-error-border"
|
||||
circleStrokeColor="stroke-components-progress-error-border"
|
||||
value={(totalPluginsLength > 0 ? runningPluginsLength / totalPluginsLength : 0) * 100}
|
||||
color="error"
|
||||
aria-label={tip}
|
||||
/>
|
||||
)}
|
||||
{showSuccessIcon && !isInstalling && !isInstallingWithSuccess && !isInstallingWithError && (
|
||||
|
||||
@ -12,6 +12,7 @@ import {
|
||||
} from '@langgenius/dify-ui/alert-dialog'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import copy from 'copy-to-clipboard'
|
||||
@ -19,7 +20,6 @@ import * as React from 'react'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import Icon from '@/app/components/plugins/card/base/card-icon'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { openOAuthPopup } from '@/hooks/use-oauth'
|
||||
@ -231,7 +231,7 @@ const MCPDetailContent: FC<Props> = ({
|
||||
onClick={handleAuthorize}
|
||||
disabled={!isCurrentWorkspaceManager}
|
||||
>
|
||||
<Indicator className="mr-2" color="green" />
|
||||
<StatusDot className="mr-2" status="success" />
|
||||
{t('auth.authorized', { ns: 'tools' })}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@ -16,6 +16,7 @@ import {
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
||||
import { Switch } from '@langgenius/dify-ui/switch'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import { RiEditLine, RiLoopLeftLine } from '@remixicon/react'
|
||||
@ -24,7 +25,6 @@ import { useTranslation } from 'react-i18next'
|
||||
import CopyFeedback from '@/app/components/base/copy-feedback'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import { Mcp } from '@/app/components/base/icons/src/vender/other'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import MCPServerModal from '@/app/components/tools/mcp/mcp-server-modal'
|
||||
import { collaborationManager } from '@/app/components/workflow/collaboration/core/collaboration-manager'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
@ -40,7 +40,7 @@ const StatusIndicator: FC<StatusIndicatorProps> = ({ serverActivated }) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
<Indicator color={serverActivated ? 'green' : 'yellow'} />
|
||||
<StatusDot status={serverActivated ? 'success' : 'warning'} />
|
||||
<div className={cn('system-xs-semibold-uppercase', serverActivated ? 'text-text-success' : 'text-text-warning')}>
|
||||
{serverActivated
|
||||
? t('overview.status.running', { ns: 'appOverview' })
|
||||
|
||||
@ -9,11 +9,11 @@ import {
|
||||
AlertDialogTitle,
|
||||
} from '@langgenius/dify-ui/alert-dialog'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
||||
import { RiHammerFill } from '@remixicon/react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import Icon from '@/app/components/plugins/card/base/card-icon'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
|
||||
@ -112,11 +112,11 @@ const MCPCard = ({
|
||||
<div className={cn('system-xs-regular text-divider-deep', (!data.is_team_authorization || !data.tools.length) && 'sm:hidden')}>/</div>
|
||||
<div className={cn('truncate system-xs-regular text-text-tertiary', (!data.is_team_authorization || !data.tools.length) && 'sm:hidden')} title={`${t('mcp.updateTime', { ns: 'tools' })} ${formatTimeFromNow(data.updated_at! * 1000)}`}>{`${t('mcp.updateTime', { ns: 'tools' })} ${formatTimeFromNow(data.updated_at! * 1000)}`}</div>
|
||||
</div>
|
||||
{data.is_team_authorization && data.tools.length > 0 && <Indicator color="green" className="shrink-0" />}
|
||||
{data.is_team_authorization && data.tools.length > 0 && <StatusDot status="success" className="shrink-0" />}
|
||||
{(!data.is_team_authorization || !data.tools.length) && (
|
||||
<div className="flex shrink-0 items-center gap-1 rounded-md border border-util-colors-red-red-500 bg-components-badge-bg-red-soft px-1.5 py-0.5 system-xs-medium text-util-colors-red-red-500">
|
||||
{t('mcp.noConfigured', { ns: 'tools' })}
|
||||
<Indicator color="red" />
|
||||
<StatusDot status="error" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -83,8 +83,8 @@ vi.mock('@langgenius/dify-ui/toast', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/header/indicator', () => ({
|
||||
default: () => <span data-testid="indicator" />,
|
||||
vi.mock('@langgenius/dify-ui/status-dot', () => ({
|
||||
StatusDot: () => <span data-testid="indicator" />,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/plugins/card/base/card-icon', () => ({
|
||||
|
||||
@ -20,6 +20,7 @@ import {
|
||||
DrawerPortal,
|
||||
DrawerViewport,
|
||||
} from '@langgenius/dify-ui/drawer'
|
||||
import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import {
|
||||
RiCloseLine,
|
||||
@ -31,7 +32,6 @@ import ActionButton from '@/app/components/base/action-button'
|
||||
import { LinkExternal02, Settings01 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { ConfigurationMethodEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import Icon from '@/app/components/plugins/card/base/card-icon'
|
||||
import Description from '@/app/components/plugins/card/base/description'
|
||||
import OrgInfo from '@/app/components/plugins/card/base/org-info'
|
||||
@ -325,7 +325,7 @@ const ProviderDetail = ({
|
||||
}}
|
||||
disabled={!isCurrentWorkspaceManager}
|
||||
>
|
||||
<Indicator className="mr-2" color="green" />
|
||||
<StatusDot className="mr-2" status="success" />
|
||||
{t('auth.authorized', { ns: 'tools' })}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
'use client'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
import Divider from '../../base/divider'
|
||||
|
||||
@ -90,7 +90,7 @@ const WorkflowToolConfigureButton = ({
|
||||
disabled={!isCurrentWorkspaceManager || disabled}
|
||||
>
|
||||
{t('common.configure', { ns: 'workflow' })}
|
||||
{outdated && <Indicator className="ml-1" color="yellow" />}
|
||||
{outdated && <StatusDot className="ml-1" status="warning" />}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import type { ComponentProps, PropsWithChildren, ReactNode } from 'react'
|
||||
import type { StatusDotStatus } from '@langgenius/dify-ui/status-dot'
|
||||
import type { PropsWithChildren, ReactNode } from 'react'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import { memo } from 'react'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
|
||||
type SettingItemProps = PropsWithChildren<{
|
||||
label: string
|
||||
@ -11,7 +12,7 @@ type SettingItemProps = PropsWithChildren<{
|
||||
}>
|
||||
|
||||
export const SettingItem = memo(({ label, children, status, tooltip }: SettingItemProps) => {
|
||||
const indicator: ComponentProps<typeof Indicator>['color'] = status === 'error' ? 'red' : status === 'warning' ? 'yellow' : undefined
|
||||
const indicator: StatusDotStatus | undefined = status === 'error' ? 'error' : status === 'warning' ? 'warning' : undefined
|
||||
const needTooltip = ['error', 'warning'].includes(status as any)
|
||||
return (
|
||||
<div className="relative flex items-center justify-between space-x-1 rounded-md bg-workflow-block-parma-bg px-1.5 py-1 text-xs font-normal">
|
||||
@ -31,7 +32,7 @@ export const SettingItem = memo(({ label, children, status, tooltip }: SettingIt
|
||||
{tooltip}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
{indicator && <Indicator color={indicator} className="absolute -top-0.5 -right-0.5" />}
|
||||
{indicator && <StatusDot status={indicator} className="absolute -top-0.5 -right-0.5" />}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
@ -51,8 +51,8 @@ vi.mock('@/app/components/header/account-setting/model-provider-page/model-selec
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/header/indicator', () => ({
|
||||
default: ({ color }: any) => <div>{`indicator:${color}`}</div>,
|
||||
vi.mock('@langgenius/dify-ui/status-dot', () => ({
|
||||
StatusDot: ({ status }: any) => <div>{`indicator:${status}`}</div>,
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-tools', () => ({
|
||||
@ -253,15 +253,15 @@ describe('agent path', () => {
|
||||
const { rerender, container } = render(<ModelBar />)
|
||||
|
||||
expect(container).toHaveTextContent('no-model:0')
|
||||
expect(screen.getByText('indicator:red')).toBeInTheDocument()
|
||||
expect(screen.getByText('indicator:error')).toBeInTheDocument()
|
||||
|
||||
rerender(<ModelBar provider="openai" model="gpt-4o" />)
|
||||
expect(container).toHaveTextContent('openai/gpt-4o:1')
|
||||
expect(screen.queryByText('indicator:red')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('indicator:error')).not.toBeInTheDocument()
|
||||
|
||||
rerender(<ModelBar provider="openai" model="gpt-4.1" />)
|
||||
expect(container).toHaveTextContent('openai/gpt-4.1:1')
|
||||
expect(screen.getByText('indicator:red')).toBeInTheDocument()
|
||||
expect(screen.getByText('indicator:error')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render tool icons across loading, marketplace fallback, authorization warning, and fetch-error states', async () => {
|
||||
@ -276,7 +276,7 @@ describe('agent path', () => {
|
||||
unmount()
|
||||
const secondRender = render(<ToolIcon id="tool-1" providerName="author/tool-b" />)
|
||||
expect(screen.getByText('app-icon:#fff:B')).toBeInTheDocument()
|
||||
expect(screen.getByText('indicator:yellow')).toBeInTheDocument()
|
||||
expect(screen.getByText('indicator:warning')).toBeInTheDocument()
|
||||
|
||||
mockBuiltInTools = undefined
|
||||
secondRender.rerender(<ToolIcon id="tool-2" providerName="author/tool-c" />)
|
||||
@ -301,7 +301,7 @@ describe('agent path', () => {
|
||||
expect(screen.getByText('workflow.nodes.agent.model')).toBeInTheDocument()
|
||||
expect(screen.getByText('workflow.nodes.agent.toolbox')).toBeInTheDocument()
|
||||
expect(container).toHaveTextContent('openai/gpt-4o:1')
|
||||
expect(screen.getByText('indicator:yellow')).toBeInTheDocument()
|
||||
expect(screen.getByText('indicator:warning')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render the panel, update the selected strategy, and expose memory plus output vars', async () => {
|
||||
|
||||
@ -31,8 +31,8 @@ vi.mock('@/app/components/header/account-setting/model-provider-page/model-selec
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/header/indicator', () => ({
|
||||
default: ({ color }: { color: string }) => <div>{`indicator:${color}`}</div>,
|
||||
vi.mock('@langgenius/dify-ui/status-dot', () => ({
|
||||
StatusDot: ({ status }: { status: string }) => <div>{`indicator:${status}`}</div>,
|
||||
}))
|
||||
|
||||
describe('agent/model-bar', () => {
|
||||
@ -53,7 +53,7 @@ describe('agent/model-bar', () => {
|
||||
const emptySelector = screen.getByText((_, element) => element?.textContent === 'no-model:0')
|
||||
|
||||
expect(emptySelector).toBeInTheDocument()
|
||||
expect(screen.getByText('indicator:red')).toBeInTheDocument()
|
||||
expect(screen.getByText('indicator:error')).toBeInTheDocument()
|
||||
expect(screen.getByLabelText('workflow.nodes.agent.modelNotSelected')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
@ -61,14 +61,14 @@ describe('agent/model-bar', () => {
|
||||
render(<ModelBar provider="openai" model="gpt-4o" />)
|
||||
|
||||
expect(screen.getByText('openai/gpt-4o:1')).toBeInTheDocument()
|
||||
expect(screen.queryByText('indicator:red')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('indicator:error')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show a warning tooltip when the selected model is not installed', () => {
|
||||
render(<ModelBar provider="openai" model="gpt-4.1" />)
|
||||
|
||||
expect(screen.getByText('openai/gpt-4.1:1')).toBeInTheDocument()
|
||||
expect(screen.getByText('indicator:red')).toBeInTheDocument()
|
||||
expect(screen.getByText('indicator:error')).toBeInTheDocument()
|
||||
expect(screen.getByLabelText('workflow.nodes.agent.modelNotInstallTooltip')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -37,8 +37,8 @@ vi.mock('@/app/components/base/icons/src/vender/other', () => ({
|
||||
Group: ({ className }: { className?: string }) => <div className={className}>group-icon</div>,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/header/indicator', () => ({
|
||||
default: ({ color }: { color: string }) => <div>{`indicator:${color}`}</div>,
|
||||
vi.mock('@langgenius/dify-ui/status-dot', () => ({
|
||||
StatusDot: ({ status }: { status: string }) => <div>{`indicator:${status}`}</div>,
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/get-icon', () => ({
|
||||
@ -87,7 +87,7 @@ describe('agent/tool-icon', () => {
|
||||
|
||||
const { rerender } = render(<ToolIcon id="tool-2" providerName="author/tool-b" />)
|
||||
|
||||
expect(screen.getByText('indicator:yellow')).toBeInTheDocument()
|
||||
expect(screen.getByText('indicator:warning')).toBeInTheDocument()
|
||||
expect(screen.getByLabelText('workflow.nodes.agent.toolNotAuthorizedTooltip:{"tool":"tool-b"}')).toBeInTheDocument()
|
||||
|
||||
mockWorkflowTools = []
|
||||
@ -96,7 +96,7 @@ describe('agent/tool-icon', () => {
|
||||
|
||||
const marketplaceIcon = screen.getByRole('img', { name: 'tool icon' })
|
||||
expect(marketplaceIcon).toHaveAttribute('src', 'https://example.com/market-tool.png')
|
||||
expect(screen.getByText('indicator:red')).toBeInTheDocument()
|
||||
expect(screen.getByText('indicator:error')).toBeInTheDocument()
|
||||
expect(screen.getByLabelText('workflow.nodes.agent.toolNotInstallTooltip:{"tool":"tool-c"}')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import type { FC } from 'react'
|
||||
import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { useModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import ModelSelector from '@/app/components/header/account-setting/model-provider-page/model-selector'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
|
||||
type ModelBarProps = {
|
||||
provider: string
|
||||
@ -54,7 +54,7 @@ export const ModelBar: FC<ModelBarProps> = (props) => {
|
||||
readonly
|
||||
deprecatedClassName="opacity-50"
|
||||
/>
|
||||
<Indicator color="red" className="absolute -top-0.5 -right-0.5" />
|
||||
<StatusDot status="error" className="absolute -top-0.5 -right-0.5" />
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
@ -83,7 +83,7 @@ export const ModelBar: FC<ModelBarProps> = (props) => {
|
||||
readonly
|
||||
deprecatedClassName="opacity-50"
|
||||
/>
|
||||
{showWarn && <Indicator color="red" className="absolute -top-0.5 -right-0.5" />}
|
||||
{showWarn && <StatusDot status="error" className="absolute -top-0.5 -right-0.5" />}
|
||||
</div>
|
||||
)
|
||||
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import { memo, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import { Group } from '@/app/components/base/icons/src/vender/other'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools } from '@/service/use-tools'
|
||||
import { getIconFromMarketPlace } from '@/utils/get-icon'
|
||||
|
||||
@ -50,7 +50,7 @@ export const ToolIcon = memo(({ providerName }: ToolIconProps) => {
|
||||
return 'not-authorized'
|
||||
return undefined
|
||||
}, [currentProvider, isDataReady])
|
||||
const indicator = status === 'not-installed' ? 'red' : status === 'not-authorized' ? 'yellow' : undefined
|
||||
const indicator = status === 'not-installed' ? 'error' : status === 'not-authorized' ? 'warning' : undefined
|
||||
const notSuccess = (['not-installed', 'not-authorized'] as Array<Status>).includes(status)
|
||||
const { t } = useTranslation()
|
||||
const tooltip = useMemo(() => {
|
||||
@ -96,7 +96,7 @@ export const ToolIcon = memo(({ providerName }: ToolIconProps) => {
|
||||
<div className="flex size-5 items-center justify-center overflow-hidden rounded-md border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge">
|
||||
{iconContent}
|
||||
</div>
|
||||
{indicator && <Indicator color={indicator} className="absolute -top-px -right-px" />}
|
||||
{indicator && <StatusDot status={indicator} className="absolute -top-px -right-px" />}
|
||||
</div>
|
||||
)
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@ import type {
|
||||
} from '@/app/components/workflow/types'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
||||
import { Switch } from '@langgenius/dify-ui/switch'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import {
|
||||
@ -19,7 +20,6 @@ import { useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
|
||||
import Badge from '@/app/components/base/badge/index'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
|
||||
import { DeliveryMethodType } from '../../types'
|
||||
import EmailConfigureModal from './email-configure-modal'
|
||||
@ -177,7 +177,7 @@ const DeliveryMethodItem: FC<DeliveryMethodItemProps> = ({
|
||||
disabled={readonly}
|
||||
>
|
||||
{t(`${i18nPrefix}.deliveryMethod.notConfigured`, { ns: 'workflow' })}
|
||||
<Indicator color="orange" className="ml-1" />
|
||||
<StatusDot status="warning" className="ml-1" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
||||
import { useMemo } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import StatusContainer from '@/app/components/workflow/run/status-container'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import { useWorkflowPausedDetails } from '@/service/use-log'
|
||||
@ -112,43 +112,43 @@ const StatusPanel: FC<ResultProps> = ({
|
||||
>
|
||||
{status === 'running' && (
|
||||
<>
|
||||
<Indicator color="blue" />
|
||||
<StatusDot status="normal" />
|
||||
<span>{isListening ? 'Listening' : 'Running'}</span>
|
||||
</>
|
||||
)}
|
||||
{status === 'succeeded' && (
|
||||
<>
|
||||
<Indicator color="green" />
|
||||
<StatusDot status="success" />
|
||||
<span>SUCCESS</span>
|
||||
</>
|
||||
)}
|
||||
{status === 'partial-succeeded' && (
|
||||
<>
|
||||
<Indicator color="green" />
|
||||
<StatusDot status="success" />
|
||||
<span>PARTIAL SUCCESS</span>
|
||||
</>
|
||||
)}
|
||||
{status === 'exception' && (
|
||||
<>
|
||||
<Indicator color="yellow" />
|
||||
<StatusDot status="warning" />
|
||||
<span>EXCEPTION</span>
|
||||
</>
|
||||
)}
|
||||
{status === 'failed' && (
|
||||
<>
|
||||
<Indicator color="red" />
|
||||
<StatusDot status="error" />
|
||||
<span>FAIL</span>
|
||||
</>
|
||||
)}
|
||||
{status === 'stopped' && (
|
||||
<>
|
||||
<Indicator color="yellow" />
|
||||
<StatusDot status="warning" />
|
||||
<span>STOP</span>
|
||||
</>
|
||||
)}
|
||||
{status === 'paused' && (
|
||||
<>
|
||||
<Indicator color="yellow" />
|
||||
<StatusDot status="warning" />
|
||||
<span>PENDING</span>
|
||||
</>
|
||||
)}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user