refactor(web): localize model load balancing modal state (#39789)

This commit is contained in:
yyh 2026-07-30 14:10:57 +08:00 committed by GitHub
parent 1fd9ab14f9
commit 44a3ea7c0c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 147 additions and 95 deletions

View File

@ -69,7 +69,6 @@ const buildModalContext = (): ModalContextState => ({
setShowAnnotationFullModal: vi.fn(),
setShowModelModal: vi.fn(),
setShowExternalKnowledgeAPIModal: vi.fn(),
setShowModelLoadBalancingModal: vi.fn(),
setShowOpeningModal: vi.fn(),
setShowUpdatePluginModal: vi.fn(),
setShowTriggerEventsLimitModal: vi.fn(),

View File

@ -1,10 +1,12 @@
import type { ModelItem, ModelProvider } from '../../declarations'
import { fireEvent, screen } from '@testing-library/react'
import type { ModelLoadBalancingModalProps } from '../model-load-balancing-modal'
import { fireEvent, screen, waitFor, within } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import * as React from 'react'
import { render } from '@/test/console/render'
import { ConfigurationMethodEnum } from '../../declarations'
import ModelList from '../model-list'
const mockSetShowModelLoadBalancingModal = vi.fn()
let mockWorkspacePermissionKeys: string[] = [
'plugin.model_config',
'credential.manage',
@ -18,12 +20,38 @@ vi.mock('@/context/permission-state', async () => {
}))
})
vi.mock('@/context/modal-context', () => ({
useModalContextSelector: (
selector: (state: {
setShowModelLoadBalancingModal: typeof mockSetShowModelLoadBalancingModal
}) => unknown,
) => selector({ setShowModelLoadBalancingModal: mockSetShowModelLoadBalancingModal }),
vi.mock('@/next/dynamic', () => ({
default: (loader: () => Promise<{ default: React.ComponentType }>) => {
const LazyComponent = React.lazy(loader)
return function DynamicComponent(props: Record<string, unknown>) {
return React.createElement(
React.Suspense,
{ fallback: null },
React.createElement(LazyComponent, props),
)
}
},
}))
vi.mock('../model-load-balancing-modal', () => ({
default: ({ model, onClose, onSave, open, provider }: ModelLoadBalancingModalProps) =>
open ? (
<div role="dialog" aria-label={`Load balancing for ${model.model}`}>
<span>{provider.provider}</span>
<button type="button" onClick={onClose}>
Close
</button>
<button
type="button"
onClick={() => {
onSave?.(provider.provider)
onClose?.()
}}
>
Save
</button>
</div>
) : null,
}))
vi.mock('../model-list-item', () => ({
@ -34,7 +62,11 @@ vi.mock('../model-list-item', () => ({
model: ModelItem
onModifyLoadBalancing: (model: ModelItem) => void
}) => (
<button type="button" onClick={() => onModifyLoadBalancing(model)}>
<button
type="button"
aria-label={`Modify load balancing for ${model.model}`}
onClick={() => onModifyLoadBalancing(model)}
>
{model.model}
</button>
),
@ -74,8 +106,12 @@ describe('ModelList', () => {
/>,
)
expect(screen.getAllByText(/modelProvider\.modelsNum/).length).toBeGreaterThan(0)
expect(screen.getByRole('button', { name: 'gpt-4' }))!.toBeInTheDocument()
expect(screen.getByRole('button', { name: 'gpt-3.5' }))!.toBeInTheDocument()
expect(
screen.getByRole('button', { name: 'Modify load balancing for gpt-4' }),
).toBeInTheDocument()
expect(
screen.getByRole('button', { name: 'Modify load balancing for gpt-3.5' }),
).toBeInTheDocument()
})
it('should trigger collapse when collapsed label is clicked', () => {
@ -93,7 +129,8 @@ describe('ModelList', () => {
expect(mockOnCollapse).toHaveBeenCalled()
})
it('should open load balancing modal for selected model', () => {
it('should open the selected model and reset the payload after close', async () => {
const user = userEvent.setup()
render(
<ModelList
provider={mockProvider}
@ -103,8 +140,28 @@ describe('ModelList', () => {
/>,
)
fireEvent.click(screen.getByRole('button', { name: 'gpt-4' }))
expect(mockSetShowModelLoadBalancingModal).toHaveBeenCalled()
await user.click(screen.getByRole('button', { name: 'Modify load balancing for gpt-4' }))
const firstDialog = await screen.findByRole('dialog', {
name: 'Load balancing for gpt-4',
})
expect(within(firstDialog).getByText('test-provider')).toBeInTheDocument()
await user.click(within(firstDialog).getByRole('button', { name: 'Close' }))
await waitFor(() => {
expect(
screen.queryByRole('dialog', { name: 'Load balancing for gpt-4' }),
).not.toBeInTheDocument()
})
await user.click(screen.getByRole('button', { name: 'Modify load balancing for gpt-3.5' }))
expect(
await screen.findByRole('dialog', { name: 'Load balancing for gpt-3.5' }),
).toBeInTheDocument()
expect(
screen.queryByRole('dialog', { name: 'Load balancing for gpt-4' }),
).not.toBeInTheDocument()
})
it('should hide custom model actions without plugin.model_config', () => {
@ -173,7 +230,8 @@ describe('ModelList', () => {
expect(screen.queryByTestId('add-custom-model')).not.toBeInTheDocument()
})
it('should call onSave (onChange) and onClose from the load balancing modal callbacks', () => {
it('should refresh the provider and close after saving load balancing changes', async () => {
const user = userEvent.setup()
render(
<ModelList
provider={mockProvider}
@ -183,16 +241,19 @@ describe('ModelList', () => {
/>,
)
fireEvent.click(screen.getByRole('button', { name: 'gpt-4' }))
expect(mockSetShowModelLoadBalancingModal).toHaveBeenCalled()
await user.click(screen.getByRole('button', { name: 'Modify load balancing for gpt-4' }))
const dialog = await screen.findByRole('dialog', {
name: 'Load balancing for gpt-4',
})
const callArg = mockSetShowModelLoadBalancingModal.mock.calls[0]![0]
await user.click(within(dialog).getByRole('button', { name: 'Save' }))
callArg.onSave('test-provider')
expect(mockOnChange).toHaveBeenCalledWith('test-provider')
callArg.onClose()
expect(mockSetShowModelLoadBalancingModal).toHaveBeenCalledWith(null)
await waitFor(() => {
expect(
screen.queryByRole('dialog', { name: 'Load balancing for gpt-4' }),
).not.toBeInTheDocument()
})
})
it('should hide custom model actions when provider uses fetchFromRemote only', () => {

View File

@ -1,19 +1,24 @@
import type { FC } from 'react'
import type { Credential, ModelItem, ModelProvider } from '../declarations'
import type { ModelLoadBalancingModalProps } from './model-load-balancing-modal'
import { useAtomValue } from 'jotai'
import { useCallback } from 'react'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
AddCustomModel,
ManageCustomModelCredentials,
} from '@/app/components/header/account-setting/model-provider-page/model-auth'
import { useModalContextSelector } from '@/context/modal-context'
import { workspacePermissionKeysAtom } from '@/context/permission-state'
import dynamic from '@/next/dynamic'
import { hasPermission } from '@/utils/permission'
import { ConfigurationMethodEnum } from '../declarations'
// import Tab from './tab'
import ModelListItem from './model-list-item'
const ModelLoadBalancingModal = dynamic(() => import('./model-load-balancing-modal'), {
ssr: false,
})
type ModelListProps = {
provider: ModelProvider
models: ModelItem[]
@ -28,70 +33,76 @@ const ModelList: FC<ModelListProps> = ({ provider, models, onCollapse, onChange
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const canConfigureModels = hasPermission(workspacePermissionKeys, 'plugin.model_config')
const isConfigurable = configurativeMethods.includes(ConfigurationMethodEnum.customizableModel)
const setShowModelLoadBalancingModal = useModalContextSelector(
(state) => state.setShowModelLoadBalancingModal,
)
const [modelLoadBalancingModalProps, setModelLoadBalancingModalProps] =
useState<ModelLoadBalancingModalProps | null>(null)
const onModifyLoadBalancing = useCallback(
(model: ModelItem, credential?: Credential) => {
setShowModelLoadBalancingModal({
setModelLoadBalancingModalProps({
provider,
credential,
configurateMethod: model.fetch_from,
model: model!,
open: !!model,
onClose: () => setShowModelLoadBalancingModal(null),
onSave: onChange,
model,
open: true,
})
},
[onChange, provider, setShowModelLoadBalancingModal],
[provider],
)
return (
<div className="rounded-b-xl px-2 pb-2">
<div className="rounded-lg bg-components-panel-bg py-1">
<div className="flex items-center pr-0.75 pl-1">
<span className="group mr-2 flex shrink-0 items-center">
<span className="inline-flex h-6 items-center pr-1.5 pl-1 system-xs-medium text-text-tertiary group-hover:hidden">
{t(($) => $['modelProvider.modelsNum'], { ns: 'common', num: models.length })}
<span className="mr-0.5 i-ri-arrow-right-s-line size-4 rotate-90" />
<>
<div className="rounded-b-xl px-2 pb-2">
<div className="rounded-lg bg-components-panel-bg py-1">
<div className="flex items-center pr-0.75 pl-1">
<span className="group mr-2 flex shrink-0 items-center">
<span className="inline-flex h-6 items-center pr-1.5 pl-1 system-xs-medium text-text-tertiary group-hover:hidden">
{t(($) => $['modelProvider.modelsNum'], { ns: 'common', num: models.length })}
<span className="mr-0.5 i-ri-arrow-right-s-line size-4 rotate-90" />
</span>
<button
type="button"
className="hidden h-6 cursor-pointer items-center rounded-lg border-none bg-state-base-hover pr-1.5 pl-1 system-xs-medium text-text-tertiary group-hover:inline-flex"
onClick={() => onCollapse()}
>
{t(($) => $['modelProvider.modelsNum'], { ns: 'common', num: models.length })}
<span className="mr-0.5 i-ri-arrow-right-s-line size-4 rotate-90" />
</button>
</span>
<button
type="button"
className="hidden h-6 cursor-pointer items-center rounded-lg border-none bg-state-base-hover pr-1.5 pl-1 system-xs-medium text-text-tertiary group-hover:inline-flex"
onClick={() => onCollapse()}
>
{t(($) => $['modelProvider.modelsNum'], { ns: 'common', num: models.length })}
<span className="mr-0.5 i-ri-arrow-right-s-line size-4 rotate-90" />
</button>
</span>
{isConfigurable && canConfigureModels && (
<div className="flex grow justify-end">
<ManageCustomModelCredentials
provider={provider}
currentCustomConfigurationModelFixedFields={undefined}
/>
<AddCustomModel
provider={provider}
configurationMethod={ConfigurationMethodEnum.customizableModel}
currentCustomConfigurationModelFixedFields={undefined}
/>
</div>
)}
{isConfigurable && canConfigureModels && (
<div className="flex grow justify-end">
<ManageCustomModelCredentials
provider={provider}
currentCustomConfigurationModelFixedFields={undefined}
/>
<AddCustomModel
provider={provider}
configurationMethod={ConfigurationMethodEnum.customizableModel}
currentCustomConfigurationModelFixedFields={undefined}
/>
</div>
)}
</div>
{models.map((model) => (
<ModelListItem
key={`${model.model}-${model.model_type}-${model.fetch_from}`}
{...{
model,
provider,
isConfigurable,
onChange,
onModifyLoadBalancing,
}}
/>
))}
</div>
{models.map((model) => (
<ModelListItem
key={`${model.model}-${model.model_type}-${model.fetch_from}`}
{...{
model,
provider,
isConfigurable,
onChange,
onModifyLoadBalancing,
}}
/>
))}
</div>
</div>
{modelLoadBalancingModalProps && (
<ModelLoadBalancingModal
{...modelLoadBalancingModalProps}
onClose={() => setModelLoadBalancingModalProps(null)}
onSave={onChange}
/>
)}
</>
)
}

View File

@ -31,7 +31,6 @@ vi.mock('@/context/modal-context', () => ({
setShowAnnotationFullModal: vi.fn(),
setShowModelModal: vi.fn(),
setShowExternalKnowledgeAPIModal: vi.fn(),
setShowModelLoadBalancingModal: vi.fn(),
setShowOpeningModal: vi.fn(),
setShowUpdatePluginModal: vi.fn(),
setShowTriggerEventsLimitModal: vi.fn(),

View File

@ -4,7 +4,6 @@ import type { ReactNode } from 'react'
import type { ModalState, ModelModalType } from './modal-context'
import type { OpeningStatement } from '@/app/components/base/features/types'
import type { CreateExternalAPIReq } from '@/app/components/datasets/external-api/declarations'
import type { ModelLoadBalancingModalProps } from '@/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-modal'
import type { UpdatePluginPayload } from '@/app/components/plugins/types'
import type { InputVar } from '@/app/components/workflow/types'
import type { ExternalDataTool } from '@/models/common'
@ -52,13 +51,6 @@ const ExternalAPIModal = dynamic(
ssr: false,
},
)
const ModelLoadBalancingModal = dynamic(
() =>
import('@/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-modal'),
{
ssr: false,
},
)
const OpeningSettingModal = dynamic(
() => import('@/app/components/base/features/new-feature-panel/conversation-opener/modal'),
{
@ -88,8 +80,6 @@ export const ModalContextProvider = ({ children }: ModalContextProviderProps) =>
const [showModelModal, setShowModelModal] = useState<ModalState<ModelModalType> | null>(null)
const [showExternalKnowledgeAPIModal, setShowExternalKnowledgeAPIModal] =
useState<ModalState<CreateExternalAPIReq> | null>(null)
const [showModelLoadBalancingModal, setShowModelLoadBalancingModal] =
useState<ModelLoadBalancingModalProps | null>(null)
const [showOpeningModal, setShowOpeningModal] = useState<ModalState<
OpeningStatement & {
promptVariables?: PromptVariable[]
@ -212,7 +202,6 @@ export const ModalContextProvider = ({ children }: ModalContextProviderProps) =>
showAnnotationFullModal ||
showModelModal ||
showExternalKnowledgeAPIModal ||
showModelLoadBalancingModal ||
showOpeningModal ||
showUpdatePluginModal ||
showTriggerEventsLimitModal,
@ -228,7 +217,6 @@ export const ModalContextProvider = ({ children }: ModalContextProviderProps) =>
setShowAnnotationFullModal: () => setShowAnnotationFullModal(true),
setShowModelModal,
setShowExternalKnowledgeAPIModal,
setShowModelLoadBalancingModal,
setShowOpeningModal,
setShowUpdatePluginModal,
setShowTriggerEventsLimitModal,
@ -286,9 +274,6 @@ export const ModalContextProvider = ({ children }: ModalContextProviderProps) =>
isEditMode={showExternalKnowledgeAPIModal.isEditMode ?? false}
/>
)}
{Boolean(showModelLoadBalancingModal) && (
<ModelLoadBalancingModal {...showModelLoadBalancingModal!} />
)}
{showOpeningModal && (
<OpeningSettingModal
data={showOpeningModal.payload}

View File

@ -12,7 +12,6 @@ import type {
ModelModalModeEnum,
ModelProvider,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { ModelLoadBalancingModalProps } from '@/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-modal'
import type { UpdatePluginPayload } from '@/app/components/plugins/types'
import type { InputVar } from '@/app/components/workflow/types'
import type { ExternalDataTool } from '@/models/common'
@ -51,7 +50,6 @@ export type ModalContextState = {
setShowExternalKnowledgeAPIModal: Dispatch<
SetStateAction<ModalState<CreateExternalAPIReq> | null>
>
setShowModelLoadBalancingModal: Dispatch<SetStateAction<ModelLoadBalancingModalProps | null>>
setShowOpeningModal: Dispatch<
SetStateAction<ModalState<
OpeningStatement & {
@ -75,7 +73,6 @@ export const ModalContext = createContext<ModalContextState>({
setShowAnnotationFullModal: noop,
setShowModelModal: noop,
setShowExternalKnowledgeAPIModal: noop,
setShowModelLoadBalancingModal: noop,
setShowOpeningModal: noop,
setShowUpdatePluginModal: noop,
setShowTriggerEventsLimitModal: noop,