mirror of
https://github.com/langgenius/dify.git
synced 2026-09-05 08:48:10 +08:00
197 lines
6.2 KiB
TypeScript
197 lines
6.2 KiB
TypeScript
'use client'
|
|
|
|
import type { ReactNode } from 'react'
|
|
import type { AccessPointStatus } from './status'
|
|
import { cn } from '@langgenius/dify-ui/cn'
|
|
import { StatusDot, StatusDotSkeleton } from '@langgenius/dify-ui/status-dot'
|
|
import { Switch } from '@langgenius/dify-ui/switch'
|
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
|
import { useId } from 'react'
|
|
|
|
type AccessPointCardProps = {
|
|
actions?: ReactNode
|
|
children: ReactNode
|
|
description: string
|
|
icon: ReactNode | string
|
|
status: AccessPointStatus
|
|
statusLabel: string
|
|
title: string
|
|
className?: string
|
|
headingLevel?: 2 | 3
|
|
highlighted?: boolean
|
|
onEnabledChange?: (enabled: boolean) => void
|
|
showStatus?: boolean
|
|
switchDisabled?: boolean
|
|
switchDisabledReason?: string
|
|
switchLabel?: string
|
|
switchLoading?: boolean
|
|
}
|
|
|
|
export function AccessPointCard({
|
|
actions,
|
|
children,
|
|
className,
|
|
description,
|
|
headingLevel = 2,
|
|
highlighted = false,
|
|
icon,
|
|
onEnabledChange,
|
|
showStatus = true,
|
|
status,
|
|
statusLabel,
|
|
switchDisabled = false,
|
|
switchDisabledReason,
|
|
switchLabel,
|
|
switchLoading = false,
|
|
title,
|
|
}: AccessPointCardProps) {
|
|
const titleId = useId()
|
|
const isEnabled = status === 'inService'
|
|
const isLoading = status === 'loading'
|
|
const showSwitch = (status === 'disabled' || status === 'inService') && Boolean(onEnabledChange)
|
|
const hasSwitchDisabledReason = switchDisabled && Boolean(switchDisabledReason)
|
|
const Heading = headingLevel === 3 ? 'h3' : 'h2'
|
|
const switchControl = (
|
|
<Switch
|
|
checked={isEnabled}
|
|
disabled={switchDisabled && !hasSwitchDisabledReason}
|
|
loading={switchLoading}
|
|
{...(hasSwitchDisabledReason
|
|
? { readOnly: true, 'aria-disabled': true, 'data-disabled': '' }
|
|
: {})}
|
|
aria-label={switchLabel || title}
|
|
onCheckedChange={(enabled) => {
|
|
if (!switchDisabled) onEnabledChange?.(enabled)
|
|
}}
|
|
/>
|
|
)
|
|
|
|
return (
|
|
<section
|
|
aria-labelledby={titleId}
|
|
aria-busy={isLoading || undefined}
|
|
data-highlighted={highlighted || undefined}
|
|
className={cn(
|
|
'flex min-h-68 min-w-0 flex-col gap-0.5 rounded-xl bg-background-section-burn p-1',
|
|
highlighted && 'ring-2 ring-state-accent-solid',
|
|
className,
|
|
)}
|
|
>
|
|
<header className="flex min-h-14 shrink-0 items-center gap-2.5 py-2 pr-5 pl-2">
|
|
{typeof icon === 'string' ? (
|
|
<span className="flex size-10 shrink-0 items-center justify-center rounded-[10px] border-[0.5px] border-divider-regular bg-components-panel-on-panel-item-bg text-text-secondary">
|
|
<span aria-hidden className={cn(icon, 'size-5')} />
|
|
</span>
|
|
) : (
|
|
icon
|
|
)}
|
|
<span className="min-w-0 flex-1">
|
|
<Heading
|
|
id={titleId}
|
|
className="truncate system-md-semibold text-text-primary"
|
|
title={title}
|
|
>
|
|
{title}
|
|
</Heading>
|
|
<span className="block truncate system-xs-regular text-text-tertiary" title={description}>
|
|
{description}
|
|
</span>
|
|
</span>
|
|
{showStatus && (
|
|
<>
|
|
<span
|
|
aria-live="polite"
|
|
className={cn(
|
|
'flex shrink-0 items-center gap-1 system-xs-semibold-uppercase',
|
|
status === 'inService' ? 'text-text-success' : 'text-text-tertiary',
|
|
)}
|
|
>
|
|
{isLoading ? (
|
|
<StatusDotSkeleton className="animate-pulse motion-reduce:animate-none" />
|
|
) : (
|
|
<StatusDot status={status === 'inService' ? 'success' : 'disabled'} />
|
|
)}
|
|
{statusLabel}
|
|
</span>
|
|
{showSwitch &&
|
|
(hasSwitchDisabledReason ? (
|
|
<Tooltip>
|
|
<TooltipTrigger render={switchControl} />
|
|
<TooltipContent>{switchDisabledReason}</TooltipContent>
|
|
</Tooltip>
|
|
) : (
|
|
switchControl
|
|
))}
|
|
</>
|
|
)}
|
|
</header>
|
|
|
|
<div className="flex min-h-0 flex-1 flex-col rounded-[10px] bg-components-panel-on-panel-item-bg">
|
|
<div className="min-h-0 flex-1">{children}</div>
|
|
{actions !== undefined && (
|
|
<footer className="flex shrink-0 flex-wrap items-center gap-2 border-t-[0.5px] border-divider-subtle p-4">
|
|
{actions}
|
|
</footer>
|
|
)}
|
|
</div>
|
|
</section>
|
|
)
|
|
}
|
|
|
|
type AccessPointEndpointProps = {
|
|
actions?: ReactNode
|
|
label: string
|
|
unavailableLabel: string
|
|
value: string
|
|
dimmed?: boolean
|
|
loading?: boolean
|
|
unavailable?: boolean
|
|
}
|
|
|
|
export function AccessPointEndpoint({
|
|
actions,
|
|
dimmed = false,
|
|
label,
|
|
loading = false,
|
|
unavailable = false,
|
|
unavailableLabel,
|
|
value,
|
|
}: AccessPointEndpointProps) {
|
|
return (
|
|
<div aria-busy={loading || undefined} className="flex flex-col gap-1 px-4 py-3">
|
|
<div className="flex h-6 items-center system-xs-medium text-text-secondary">{label}</div>
|
|
<div className="flex h-9 min-w-0 items-center gap-0.5 rounded-lg border-[0.5px] border-divider-subtle bg-components-input-bg-normal py-1 pr-1 pl-2">
|
|
{unavailable && !loading && (
|
|
<span className="shrink-0 rounded-[5px] border border-divider-deep px-1 py-0.5 system-2xs-medium-uppercase text-text-tertiary">
|
|
{unavailableLabel}
|
|
</span>
|
|
)}
|
|
<div className="flex min-w-0 flex-1 items-center px-1">
|
|
{loading ? (
|
|
<span className="h-2 w-[42%] animate-pulse rounded-full bg-text-quaternary opacity-20 motion-reduce:animate-none" />
|
|
) : (
|
|
<span
|
|
className={cn(
|
|
'truncate system-sm-regular text-text-secondary',
|
|
(dimmed || unavailable) && 'text-text-quaternary',
|
|
)}
|
|
translate="no"
|
|
>
|
|
{value}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div className="flex shrink-0 items-center gap-0.5">{actions}</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function AccessPointEmptyContent({ children }: { children: ReactNode }) {
|
|
return (
|
|
<div className="flex h-full min-h-36 items-center justify-center px-6 py-8 text-center system-xs-regular whitespace-pre-line text-text-tertiary">
|
|
{children}
|
|
</div>
|
|
)
|
|
}
|