Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions api/core/plugin/entities/marketplace.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ class MarketplacePluginDeclaration(BaseModel):
latest_package_identifier: str = Field(
..., description="Unique identifier for the latest package release of the plugin"
)
status: str = Field(..., description="Indicate the status of marketplace plugin, enum from `active` `deleted`")
deprecated_reason: str = Field(
..., description="Not empty when status='deleted', indicates the reason why this plugin is deleted(deprecated)"
)
alternative_plugin_id: str = Field(
..., description="Optional, indicates the alternative plugin for user to switch to"
)

@model_validator(mode="before")
@classmethod
Expand Down
6 changes: 6 additions & 0 deletions api/services/plugin/plugin_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ class LatestPluginCache(BaseModel):
plugin_id: str
version: str
unique_identifier: str
status: str
deprecated_reason: str
alternative_plugin_id: str

REDIS_KEY_PREFIX = "plugin_service:latest_plugin:"
REDIS_TTL = 60 * 5 # 5 minutes
Expand Down Expand Up @@ -71,6 +74,9 @@ def fetch_latest_plugin_version(plugin_ids: Sequence[str]) -> Mapping[str, Optio
plugin_id=plugin_id,
version=manifest.latest_version,
unique_identifier=manifest.latest_package_identifier,
status=manifest.status,
deprecated_reason=manifest.deprecated_reason,
alternative_plugin_id=manifest.alternative_plugin_id,
)

# Store in Redis
Expand Down
104 changes: 104 additions & 0 deletions web/app/components/plugins/base/deprecation-notice.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import React, { useMemo } from 'react'
import type { FC } from 'react'
import Link from 'next/link'
import cn from '@/utils/classnames'
import { RiAlertFill } from '@remixicon/react'
import { Trans } from 'react-i18next'
import { snakeCase2CamelCase } from '@/utils/format'
import { useMixedTranslation } from '../marketplace/hooks'

type DeprecationNoticeProps = {
status: 'deleted' | 'active'
deprecatedReason: string
alternativePluginId: string
alternativePluginURL: string
locale?: string
className?: string
innerWrapperClassName?: string
iconWrapperClassName?: string
textClassName?: string
}

const i18nPrefix = 'plugin.detailPanel.deprecation'

const DeprecationNotice: FC<DeprecationNoticeProps> = ({
status,
deprecatedReason,
alternativePluginId,
alternativePluginURL,
locale,
className,
innerWrapperClassName,
iconWrapperClassName,
textClassName,
}) => {
const { t } = useMixedTranslation(locale)

const deprecatedReasonKey = useMemo(() => {
if (!deprecatedReason) return ''
return snakeCase2CamelCase(deprecatedReason)
}, [deprecatedReason])

// Check if the deprecatedReasonKey exists in i18n
const hasValidDeprecatedReason = useMemo(() => {
if (!deprecatedReason || !deprecatedReasonKey) return false

// Define valid reason keys that exist in i18n
const validReasonKeys = ['businessAdjustments', 'ownershipTransferred', 'noMaintainer']
return validReasonKeys.includes(deprecatedReasonKey)
}, [deprecatedReason, deprecatedReasonKey])

if (status !== 'deleted')
return null

return (
<div className={cn('w-full', className)}>
<div className={cn(
'relative flex items-start gap-x-0.5 overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-2 shadow-xs shadow-shadow-shadow-3 backdrop-blur-[5px]',
innerWrapperClassName,
)}>
<div className='absolute left-0 top-0 -z-10 h-full w-full bg-toast-warning-bg opacity-40' />
<div className={cn('flex size-6 shrink-0 items-center justify-center', iconWrapperClassName)}>
<RiAlertFill className='size-4 text-text-warning-secondary' />
</div>
<div className={cn('system-xs-regular grow py-1 text-text-primary', textClassName)}>
{
hasValidDeprecatedReason && alternativePluginId && (
<Trans
i18nKey={`${i18nPrefix}.fullMessage`}
components={{
CustomLink: (
<Link
href={alternativePluginURL}
target='_blank'
rel='noopener noreferrer'
className='underline'
/>
),
}}
values={{
deprecatedReason: t(`${i18nPrefix}.reason.${deprecatedReasonKey}`),
alternativePluginId,
}}
/>
)
}
{
hasValidDeprecatedReason && !alternativePluginId && (
<span>
{t(`${i18nPrefix}.onlyReason`, { deprecatedReason: t(`${i18nPrefix}.reason.${deprecatedReasonKey}`) })}
</span>
)
}
{
!hasValidDeprecatedReason && (
<span>{t(`${i18nPrefix}.noReason`)}</span>
)
}
</div>
</div>
</div>
)
}

export default React.memo(DeprecationNotice)
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import Toast from '@/app/components/base/toast'
import { BoxSparkleFill } from '@/app/components/base/icons/src/vender/plugin'
import { Github } from '@/app/components/base/icons/src/public/common'
import { uninstallPlugin } from '@/service/plugins'
import { useGetLanguage } from '@/context/i18n'
import { useGetLanguage, useI18N } from '@/context/i18n'
import { useModalContext } from '@/context/modal-context'
import { useProviderContext } from '@/context/provider-context'
import { useInvalidateAllToolProviders } from '@/service/use-tools'
Expand All @@ -39,6 +39,7 @@ import { getMarketplaceUrl } from '@/utils/var'
import { PluginAuth } from '@/app/components/plugins/plugin-auth'
import { AuthCategory } from '@/app/components/plugins/plugin-auth'
import { useAllToolProviders } from '@/service/use-tools'
import DeprecationNotice from '../base/deprecation-notice'

const i18nPrefix = 'plugin.action'

Expand All @@ -56,6 +57,7 @@ const DetailHeader = ({
const { t } = useTranslation()
const { theme } = useTheme()
const locale = useGetLanguage()
const { locale: currentLocale } = useI18N()
const { checkForUpdates, fetchReleases } = useGitHubReleases()
const { setShowUpdatePluginModal } = useModalContext()
const { refreshModelProviders } = useProviderContext()
Expand All @@ -70,6 +72,9 @@ const DetailHeader = ({
latest_version,
meta,
plugin_id,
status,
deprecated_reason,
alternative_plugin_id,
} = detail
const { author, category, name, label, description, icon, verified, tool } = detail.declaration
const isTool = category === PluginType.tool
Expand Down Expand Up @@ -98,7 +103,7 @@ const DetailHeader = ({
if (isFromGitHub)
return `https://github.com/${meta!.repo}`
if (isFromMarketplace)
return getMarketplaceUrl(`/plugins/${author}/${name}`, { theme })
return getMarketplaceUrl(`/plugins/${author}/${name}`, { language: currentLocale, theme })
return ''
}, [author, isFromGitHub, isFromMarketplace, meta, name, theme])

Expand Down Expand Up @@ -272,6 +277,15 @@ const DetailHeader = ({
</ActionButton>
</div>
</div>
{isFromMarketplace && (
<DeprecationNotice
status={status}
deprecatedReason={deprecated_reason}
alternativePluginId={alternative_plugin_id}
alternativePluginURL={getMarketplaceUrl(`/plugins/${alternative_plugin_id}`, { language: currentLocale, theme })}
className='mt-3'
/>
)}
<Description className='mb-2 mt-3 h-auto' text={description[locale]} descriptionLineRows={2}></Description>
{
category === PluginType.tool && (
Expand Down
62 changes: 44 additions & 18 deletions web/app/components/plugins/plugin-item/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'
import type { FC } from 'react'
import React, { useMemo } from 'react'
import React, { useCallback, useMemo } from 'react'
import { useTheme } from 'next-themes'
import {
RiArrowRightUpLine,
Expand Down Expand Up @@ -55,6 +55,8 @@ const PluginItem: FC<Props> = ({
endpoints_active,
meta,
plugin_id,
status,
deprecated_reason,
} = plugin
const { category, author, name, label, description, icon, verified, meta: declarationMeta } = plugin.declaration

Expand All @@ -70,9 +72,14 @@ const PluginItem: FC<Props> = ({
return gte(langeniusVersionInfo.current_version, declarationMeta.minimum_dify_version ?? '0.0.0')
}, [declarationMeta.minimum_dify_version, langeniusVersionInfo.current_version])

const handleDelete = () => {
const isDeprecated = useMemo(() => {
return status === 'deleted' && !!deprecated_reason
}, [status, deprecated_reason])

const handleDelete = useCallback(() => {
refreshPluginList({ category } as any)
}
}, [category, refreshPluginList])

const getValueFromI18nObject = useRenderI18nObject()
const title = getValueFromI18nObject(label)
const descriptionText = getValueFromI18nObject(description)
Expand All @@ -81,7 +88,7 @@ const PluginItem: FC<Props> = ({
return (
<div
className={cn(
'rounded-xl border-[1.5px] border-background-section-burn p-1',
'relative overflow-hidden rounded-xl border-[1.5px] border-background-section-burn p-1',
currentPluginID === plugin_id && 'border-components-option-card-option-selected-border',
source === PluginSource.debugging
? 'bg-[repeating-linear-gradient(-45deg,rgba(16,24,40,0.04),rgba(16,24,40,0.04)_5px,rgba(0,0,0,0.02)_5px,rgba(0,0,0,0.02)_10px)]'
Expand All @@ -91,24 +98,24 @@ const PluginItem: FC<Props> = ({
setCurrentPluginID(plugin.plugin_id)
}}
>
<div className={cn('hover-bg-components-panel-on-panel-item-bg relative rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg p-4 pb-3 shadow-xs', className)}>
<div className={cn('hover-bg-components-panel-on-panel-item-bg relative z-10 rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg p-4 pb-3 shadow-xs', className)}>
<CornerMark text={categoriesMap[category].label} />
{/* Header */}
<div className="flex">
<div className='flex'>
<div className='flex h-10 w-10 items-center justify-center overflow-hidden rounded-xl border-[1px] border-components-panel-border-subtle'>
<img
className='h-full w-full'
src={`${API_PREFIX}/workspaces/current/plugin/icon?tenant_id=${tenant_id}&filename=${icon}`}
alt={`plugin-${plugin_unique_identifier}-logo`}
/>
</div>
<div className="ml-3 w-0 grow">
<div className="flex h-5 items-center">
<div className='ml-3 w-0 grow'>
<div className='flex h-5 items-center'>
<Title title={title} />
{verified && <RiVerifiedBadgeLine className="ml-0.5 h-4 w-4 shrink-0 text-text-accent" />}
{verified && <RiVerifiedBadgeLine className='ml-0.5 h-4 w-4 shrink-0 text-text-accent' />}
{!isDifyVersionCompatible && <Tooltip popupContent={
t('plugin.difyVersionNotCompatible', { minimalDifyVersion: declarationMeta.minimum_dify_version })
}><RiErrorWarningLine color='red' className="ml-0.5 h-4 w-4 shrink-0 text-text-accent" /></Tooltip>}
}><RiErrorWarningLine color='red' className='ml-0.5 h-4 w-4 shrink-0 text-text-accent' /></Tooltip>}
<Badge className='ml-1 shrink-0'
text={source === PluginSource.github ? plugin.meta!.version : plugin.version}
hasRedCornerMark={(source === PluginSource.marketplace) && !!plugin.latest_version && plugin.latest_version !== plugin.version}
Expand All @@ -135,26 +142,32 @@ const PluginItem: FC<Props> = ({
</div>
</div>
</div>
<div className='mb-1 mt-1.5 flex h-4 items-center justify-between px-4'>
<div className='flex items-center'>
<div className='mb-1 mt-1.5 flex h-4 items-center gap-x-2 px-4'>
{/* Organization & Name */}
<div className='flex grow items-center overflow-hidden'>
<OrgInfo
className="mt-0.5"
className='mt-0.5'
orgName={orgName}
packageName={name}
packageNameClassName='w-auto max-w-[150px]'
/>
{category === PluginType.extension && (
<>
<div className='system-xs-regular mx-2 text-text-quaternary'>·</div>
<div className='system-xs-regular flex space-x-1 text-text-tertiary'>
<RiLoginCircleLine className='h-4 w-4' />
<span>{t('plugin.endpointsEnabled', { num: endpoints_active })}</span>
<div className='system-xs-regular flex space-x-1 overflow-hidden text-text-tertiary'>
<RiLoginCircleLine className='h-4 w-4 shrink-0' />
<span
className='truncate'
title={t('plugin.endpointsEnabled', { num: endpoints_active })}
>
{t('plugin.endpointsEnabled', { num: endpoints_active })}
</span>
</div>
</>
)}
</div>

<div className='flex items-center'>
{/* Source */}
<div className='flex shrink-0 items-center'>
{source === PluginSource.github
&& <>
<a href={`https://github.com/${meta!.repo}`} target='_blank' className='flex items-center gap-1'>
Expand Down Expand Up @@ -192,7 +205,20 @@ const PluginItem: FC<Props> = ({
</>
}
</div>
{/* Deprecated */}
{source === PluginSource.marketplace && enable_marketplace && isDeprecated && (
<div className='system-2xs-medium-uppercase flex shrink-0 items-center gap-x-2'>
<span className='text-text-tertiary'>·</span>
<span className='text-text-warning'>
{t('plugin.deprecated')}
</span>
</div>
)}
</div>
{/* BG Effect for Deprecated Plugin */}
{source === PluginSource.marketplace && enable_marketplace && isDeprecated && (
<div className='absolute bottom-[-71px] right-[-45px] z-0 size-40 bg-components-badge-status-light-warning-halo opacity-60 blur-[120px]' />
)}
</div>
)
}
Expand Down
34 changes: 21 additions & 13 deletions web/app/components/plugins/plugin-page/plugins-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ const PluginsPanel = () => {
...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])

Expand Down Expand Up @@ -66,20 +69,25 @@ const PluginsPanel = () => {
onFilterChange={handleFilterChange}
/>
</div>
{isPluginListLoading ? <Loading type='app' /> : (filteredList?.length ?? 0) > 0 ? (
<div className='flex grow flex-wrap content-start items-start justify-center gap-2 self-stretch px-12'>
<div className='w-full'>
<List pluginList={filteredList || []} />
</div>
{!isLastPage && !isFetching && (
<Button onClick={loadNextPage}>
{t('workflow.common.loadMore')}
</Button>
{isPluginListLoading && <Loading type='app' />}
{!isPluginListLoading && (
<>
{(filteredList?.length ?? 0) > 0 ? (
<div className='flex grow flex-wrap content-start items-start justify-center gap-2 self-stretch px-12'>
<div className='w-full'>
<List pluginList={filteredList || []} />
</div>
{!isLastPage && !isFetching && (
<Button onClick={loadNextPage}>
{t('workflow.common.loadMore')}
</Button>
)}
{isFetching && <div className='system-md-semibold text-text-secondary'>{t('appLog.detail.loading')}</div>}
</div>
) : (
<Empty />
)}
{isFetching && <div className='system-md-semibold text-text-secondary'>{t('appLog.detail.loading')}</div>}
</div>
) : (
<Empty />
</>
)}
<PluginDetailPanel
detail={currentPluginDetail}
Expand Down
Loading
Loading