mirror of
https://github.com/langgenius/dify.git
synced 2026-05-05 09:58:04 +08:00
Merge branch 'feat/model-plugins-implementing' into deploy/dev
# Conflicts: # web/contract/router.ts
This commit is contained in:
@ -83,8 +83,21 @@ vi.mock('./system-model-selector', () => ({
|
||||
default: () => <div data-testid="system-model-selector" />,
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-plugins', () => ({
|
||||
useCheckInstalled: () => ({ data: undefined }),
|
||||
vi.mock('@tanstack/react-query', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@tanstack/react-query')>()
|
||||
return {
|
||||
...actual,
|
||||
useQuery: () => ({ data: undefined }),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/service/client', () => ({
|
||||
consoleQuery: {
|
||||
plugins: {
|
||||
checkInstalled: { queryOptions: () => ({}) },
|
||||
latestVersions: { queryOptions: () => ({}) },
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
describe('ModelProviderPage', () => {
|
||||
|
||||
@ -2,13 +2,15 @@ import type {
|
||||
ModelProvider,
|
||||
} from './declarations'
|
||||
import type { PluginDetail } from '@/app/components/plugins/types'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useDebounce } from 'ahooks'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { usePluginsWithLatestVersion } from '@/app/components/plugins/hooks'
|
||||
import { IS_CLOUD_EDITION } from '@/config'
|
||||
import { useSystemFeaturesQuery } from '@/context/global-public-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { useCheckInstalled } from '@/service/use-plugins'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import {
|
||||
CustomConfigurationStatusEnum,
|
||||
@ -45,18 +47,18 @@ const ModelProviderPage = ({ searchText }: Props) => {
|
||||
const allPluginIds = useMemo(() => {
|
||||
return [...new Set(providers.map(p => providerToPluginId(p.provider)).filter(Boolean))]
|
||||
}, [providers])
|
||||
const { data: installedPlugins } = useCheckInstalled({
|
||||
pluginIds: allPluginIds,
|
||||
const { data: installedPlugins } = useQuery(consoleQuery.plugins.checkInstalled.queryOptions({
|
||||
input: { body: { plugin_ids: allPluginIds } },
|
||||
enabled: allPluginIds.length > 0,
|
||||
})
|
||||
staleTime: 0,
|
||||
}))
|
||||
const enrichedPlugins = usePluginsWithLatestVersion(installedPlugins?.plugins)
|
||||
const pluginDetailMap = useMemo(() => {
|
||||
const map = new Map<string, PluginDetail>()
|
||||
if (installedPlugins?.plugins) {
|
||||
for (const plugin of installedPlugins.plugins)
|
||||
map.set(plugin.plugin_id, plugin)
|
||||
}
|
||||
for (const plugin of enrichedPlugins)
|
||||
map.set(plugin.plugin_id, plugin)
|
||||
return map
|
||||
}, [installedPlugins])
|
||||
}, [enrichedPlugins])
|
||||
const enableMarketplace = systemFeatures?.enable_marketplace ?? false
|
||||
const isDefaultModelLoading = isTextGenerationDefaultModelLoading
|
||||
|| isEmbeddingsDefaultModelLoading
|
||||
@ -72,7 +74,7 @@ const ModelProviderPage = ({ searchText }: Props) => {
|
||||
provider.custom_configuration.status === CustomConfigurationStatusEnum.active
|
||||
|| (
|
||||
provider.system_configuration.enabled === true
|
||||
&& provider.system_configuration.quota_configurations.find(item => item.quota_type === provider.system_configuration.current_quota_type)
|
||||
&& provider.system_configuration.quota_configurations.some(item => item.quota_type === provider.system_configuration.current_quota_type)
|
||||
)
|
||||
) {
|
||||
configuredProviders.push(provider)
|
||||
|
||||
@ -2,7 +2,9 @@ import type { FC } from 'react'
|
||||
import type { PluginDetail } from '@/app/components/plugins/types'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/app/components/base/ui/tooltip'
|
||||
import { HeaderModals } from '@/app/components/plugins/plugin-detail-panel/detail-header/components'
|
||||
import { useDetailHeaderState, usePluginOperations } from '@/app/components/plugins/plugin-detail-panel/detail-header/hooks'
|
||||
import OperationDropdown from '@/app/components/plugins/plugin-detail-panel/operation-dropdown'
|
||||
@ -84,31 +86,42 @@ const ProviderCardActions: FC<Props> = ({ detail, onUpdate }) => {
|
||||
sideOffset={4}
|
||||
alignOffset={0}
|
||||
trigger={(
|
||||
<span
|
||||
<Badge
|
||||
className={cn(
|
||||
'relative inline-flex min-w-5 items-center justify-center gap-[3px] rounded-md border border-divider-deep bg-state-base-hover px-[5px] py-[2px] text-text-tertiary system-xs-medium-uppercase',
|
||||
isFromMarketplace && 'cursor-pointer hover:bg-state-base-hover-alt',
|
||||
isFromMarketplace && 'cursor-pointer hover:bg-state-base-hover',
|
||||
)}
|
||||
>
|
||||
<span>{version}</span>
|
||||
{isFromMarketplace && <span aria-hidden className="i-ri-arrow-left-right-line h-3 w-3" />}
|
||||
{hasNewVersion && (
|
||||
<span className="absolute -right-0.5 -top-0.5 h-1.5 w-1.5 rounded-full bg-state-destructive-solid" />
|
||||
uppercase={false}
|
||||
text={(
|
||||
<>
|
||||
<span>{version}</span>
|
||||
{isFromMarketplace && <span aria-hidden className="i-ri-arrow-left-right-line ml-1 h-3 w-3" />}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
hasRedCornerMark={hasNewVersion}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{(hasNewVersion || isFromGitHub) && (
|
||||
<Button
|
||||
variant="secondary-accent"
|
||||
size="small"
|
||||
className="!h-5"
|
||||
onClick={handleTriggerLatestUpdate}
|
||||
>
|
||||
{t('detailPanel.operation.update', { ns: 'plugin' })}
|
||||
</Button>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
delay={300}
|
||||
render={(
|
||||
<Button
|
||||
variant="secondary-accent"
|
||||
size="small"
|
||||
className="!h-5"
|
||||
onClick={handleTriggerLatestUpdate}
|
||||
>
|
||||
{t('detailPanel.operation.update', { ns: 'plugin' })}
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
<TooltipContent>
|
||||
{t('detailPanel.operation.updateTooltip', { ns: 'plugin' })}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<OperationDropdown
|
||||
@ -118,7 +131,6 @@ const ProviderCardActions: FC<Props> = ({ detail, onUpdate }) => {
|
||||
onRemove={modalStates.showDeleteConfirm}
|
||||
detailUrl={detailUrl}
|
||||
placement="bottom-start"
|
||||
popupClassName="w-[192px]"
|
||||
/>
|
||||
|
||||
<HeaderModals
|
||||
|
||||
@ -1,11 +1,14 @@
|
||||
import type { CategoryKey, TagKey } from './constants'
|
||||
import type { PluginDetail } from './types'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import {
|
||||
categoryKeys,
|
||||
tagKeys,
|
||||
} from './constants'
|
||||
import { PluginCategoryEnum } from './types'
|
||||
import { PluginCategoryEnum, PluginSource } from './types'
|
||||
|
||||
export type Tag = {
|
||||
name: TagKey
|
||||
@ -95,3 +98,39 @@ export const usePluginPageTabs = () => {
|
||||
]
|
||||
return tabs
|
||||
}
|
||||
|
||||
const EMPTY_PLUGINS: PluginDetail[] = []
|
||||
|
||||
export function usePluginsWithLatestVersion(plugins: PluginDetail[] = EMPTY_PLUGINS): PluginDetail[] {
|
||||
const marketplacePluginIds = useMemo(
|
||||
() => plugins
|
||||
.filter(p => p.source === PluginSource.marketplace)
|
||||
.map(p => p.plugin_id),
|
||||
[plugins],
|
||||
)
|
||||
|
||||
const { data: latestVersionData } = useQuery(consoleQuery.plugins.latestVersions.queryOptions({
|
||||
input: { body: { plugin_ids: marketplacePluginIds } },
|
||||
enabled: !!marketplacePluginIds.length,
|
||||
}))
|
||||
|
||||
return useMemo(() => {
|
||||
const versions = latestVersionData?.versions
|
||||
if (!versions)
|
||||
return plugins
|
||||
|
||||
return plugins.map((plugin) => {
|
||||
const info = versions[plugin.plugin_id]
|
||||
if (!info)
|
||||
return plugin
|
||||
return {
|
||||
...plugin,
|
||||
latest_version: info.version,
|
||||
latest_unique_identifier: info.unique_identifier,
|
||||
status: info.status,
|
||||
deprecated_reason: info.deprecated_reason,
|
||||
alternative_plugin_id: info.alternative_plugin_id,
|
||||
}
|
||||
})
|
||||
}, [plugins, latestVersionData])
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/app/components/base/ui/tooltip'
|
||||
import { AuthCategory, PluginAuth } from '@/app/components/plugins/plugin-auth'
|
||||
import OperationDropdown from '@/app/components/plugins/plugin-detail-panel/operation-dropdown'
|
||||
import PluginVersionPicker from '@/app/components/plugins/update-plugin/plugin-version-picker'
|
||||
@ -191,25 +191,43 @@ const DetailHeader = ({
|
||||
|
||||
{/* Auto Update Badge */}
|
||||
{isAutoUpgradeEnabled && !isReadmeView && (
|
||||
<Tooltip popupContent={t('autoUpdate.nextUpdateTime', { ns: 'plugin', time: timeOfDayToDayjs(convertUTCDaySecondsToLocalSeconds(autoUpgradeInfo?.upgrade_time_of_day || 0, timezone!)).format('hh:mm A') })}>
|
||||
<div>
|
||||
<Badge className="mr-1 cursor-pointer px-1">
|
||||
<AutoUpdateLine className="size-3" />
|
||||
</Badge>
|
||||
</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
delay={0}
|
||||
render={(
|
||||
<div>
|
||||
<Badge className="mr-1 cursor-pointer px-1">
|
||||
<AutoUpdateLine className="size-3" />
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<TooltipContent>
|
||||
{t('autoUpdate.nextUpdateTime', { ns: 'plugin', time: timeOfDayToDayjs(convertUTCDaySecondsToLocalSeconds(autoUpgradeInfo?.upgrade_time_of_day || 0, timezone!)).format('hh:mm A') })}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* Update Button */}
|
||||
{(hasNewVersion || isFromGitHub) && (
|
||||
<Button
|
||||
variant="secondary-accent"
|
||||
size="small"
|
||||
className="!h-5"
|
||||
onClick={handleTriggerLatestUpdate}
|
||||
>
|
||||
{t('detailPanel.operation.update', { ns: 'plugin' })}
|
||||
</Button>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
delay={300}
|
||||
render={(
|
||||
<Button
|
||||
variant="secondary-accent"
|
||||
size="small"
|
||||
className="!h-5"
|
||||
onClick={handleTriggerLatestUpdate}
|
||||
>
|
||||
{t('detailPanel.operation.update', { ns: 'plugin' })}
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
<TooltipContent>
|
||||
{t('detailPanel.operation.updateTooltip', { ns: 'plugin' })}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@ -52,7 +52,7 @@ const OperationDropdown: FC<Props> = ({
|
||||
placement={placement}
|
||||
sideOffset={sideOffset}
|
||||
alignOffset={alignOffset}
|
||||
popupClassName={cn('w-[160px]', popupClassName)}
|
||||
popupClassName={cn('w-auto min-w-[160px]', popupClassName)}
|
||||
>
|
||||
{source === PluginSource.github && (
|
||||
<DropdownMenuItem onClick={onInfo}>
|
||||
|
||||
@ -9,8 +9,8 @@ import Loading from '@/app/components/base/loading'
|
||||
import PluginDetailPanel from '@/app/components/plugins/plugin-detail-panel'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import { renderI18nObject } from '@/i18n-config'
|
||||
import { useInstalledLatestVersion, useInstalledPluginList, useInvalidateInstalledPluginList } from '@/service/use-plugins'
|
||||
import { PluginSource } from '../types'
|
||||
import { useInstalledPluginList, useInvalidateInstalledPluginList } from '@/service/use-plugins'
|
||||
import { usePluginsWithLatestVersion } from '../hooks'
|
||||
import { usePluginPageContext } from './context'
|
||||
import Empty from './empty'
|
||||
import FilterManagement from './filter-management'
|
||||
@ -47,11 +47,7 @@ const PluginsPanel = () => {
|
||||
const filters = usePluginPageContext(v => v.filters) as FilterState
|
||||
const setFilters = usePluginPageContext(v => v.setFilters)
|
||||
const { data: pluginList, isLoading: isPluginListLoading, isFetching, isLastPage, loadNextPage } = useInstalledPluginList()
|
||||
const { data: installedLatestVersion } = useInstalledLatestVersion(
|
||||
pluginList?.plugins
|
||||
.filter(plugin => plugin.source === PluginSource.marketplace)
|
||||
.map(plugin => plugin.plugin_id) ?? [],
|
||||
)
|
||||
const pluginListWithLatestVersion = usePluginsWithLatestVersion(pluginList?.plugins)
|
||||
const invalidateInstalledPluginList = useInvalidateInstalledPluginList()
|
||||
const currentPluginID = usePluginPageContext(v => v.currentPluginID)
|
||||
const setCurrentPluginID = usePluginPageContext(v => v.setCurrentPluginID)
|
||||
@ -60,17 +56,6 @@ const PluginsPanel = () => {
|
||||
setFilters(filters)
|
||||
}, { wait: 500 })
|
||||
|
||||
const pluginListWithLatestVersion = useMemo(() => {
|
||||
return pluginList?.plugins.map(plugin => ({
|
||||
...plugin,
|
||||
latest_version: installedLatestVersion?.versions[plugin.plugin_id]?.version ?? '',
|
||||
latest_unique_identifier: installedLatestVersion?.versions[plugin.plugin_id]?.unique_identifier ?? '',
|
||||
status: installedLatestVersion?.versions[plugin.plugin_id]?.status ?? 'active',
|
||||
deprecated_reason: installedLatestVersion?.versions[plugin.plugin_id]?.deprecated_reason ?? '',
|
||||
alternative_plugin_id: installedLatestVersion?.versions[plugin.plugin_id]?.alternative_plugin_id ?? '',
|
||||
})) || []
|
||||
}, [pluginList, installedLatestVersion])
|
||||
|
||||
const filteredList = useMemo(() => {
|
||||
const { categories, searchQuery, tags } = filters
|
||||
const filteredList = pluginListWithLatestVersion.filter((plugin) => {
|
||||
|
||||
Reference in New Issue
Block a user