Feat: add check before install plugin (#20014)

This commit is contained in:
NFish
2025-06-18 15:51:23 +08:00
committed by GitHub
parent 83719cab73
commit 614c5e087e
30 changed files with 337 additions and 135 deletions

View File

@@ -17,9 +17,9 @@ import Loading from '@/app/components/base/loading'
import ProviderCard from '@/app/components/plugins/provider-card'
import List from '@/app/components/plugins/marketplace/list'
import type { Plugin } from '@/app/components/plugins/types'
import { MARKETPLACE_URL_PREFIX } from '@/config'
import cn from '@/utils/classnames'
import { getLocaleOnClient } from '@/i18n'
import { getMarketplaceUrl } from '@/utils/var'
type InstallFromMarketplaceProps = {
providers: ModelProvider[]
@@ -55,7 +55,7 @@ const InstallFromMarketplace = ({
</div>
<div className='mb-2 flex items-center pt-2'>
<span className='system-sm-regular pr-1 text-text-tertiary'>{t('common.modelProvider.discoverMore')}</span>
<Link target="_blank" href={`${MARKETPLACE_URL_PREFIX}${theme ? `?theme=${theme}` : ''}`} className='system-sm-medium inline-flex items-center text-text-accent'>
<Link target="_blank" href={getMarketplaceUrl('', { theme })} className='system-sm-medium inline-flex items-center text-text-accent'>
{t('plugin.marketplace.difyMarketplace')}
<RiArrowRightUpLine className='h-4 w-4' />
</Link>

View File

@@ -15,6 +15,7 @@ import { renderI18nObject } from '@/i18n'
import { useMixedTranslation } from '@/app/components/plugins/marketplace/hooks'
import Partner from '../base/badges/partner'
import Verified from '../base/badges/verified'
import { RiAlertFill } from '@remixicon/react'
export type Props = {
className?: string
@@ -28,6 +29,7 @@ export type Props = {
isLoading?: boolean
loadingFileName?: string
locale?: string
limitedInstall?: boolean
}
const Card = ({
@@ -42,6 +44,7 @@ const Card = ({
isLoading = false,
loadingFileName,
locale: localeFromProps,
limitedInstall = false,
}: Props) => {
const defaultLocale = useGetLanguage()
const locale = localeFromProps ? getLanguage(localeFromProps) : defaultLocale
@@ -54,7 +57,7 @@ const Card = ({
obj ? renderI18nObject(obj, locale) : ''
const isPartner = badges.includes('partner')
const wrapClassName = 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)
const wrapClassName = cn('hover-bg-components-panel-on-panel-item-bg relative overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg shadow-xs', className)
if (isLoading) {
return (
<Placeholder
@@ -66,30 +69,39 @@ const Card = ({
return (
<div className={wrapClassName}>
{!hideCornerMark && <CornerMark text={cornerMark} />}
{/* Header */}
<div className="flex">
<Icon src={icon} installed={installed} installFailed={installFailed} />
<div className="ml-3 w-0 grow">
<div className="flex h-5 items-center">
<Title title={getLocalizedText(label)} />
{isPartner && <Partner className='ml-0.5 h-4 w-4' text={t('plugin.marketplace.partnerTip')} />}
{verified && <Verified className='ml-0.5 h-4 w-4' text={t('plugin.marketplace.verifiedTip')} />}
{titleLeft} {/* This can be version badge */}
<div className={cn('p-4 pb-3', limitedInstall && 'pb-1')}>
{!hideCornerMark && <CornerMark text={cornerMark} />}
{/* Header */}
<div className="flex">
<Icon src={icon} installed={installed} installFailed={installFailed} />
<div className="ml-3 w-0 grow">
<div className="flex h-5 items-center">
<Title title={getLocalizedText(label)} />
{isPartner && <Partner className='ml-0.5 h-4 w-4' text={t('plugin.marketplace.partnerTip')} />}
{verified && <Verified className='ml-0.5 h-4 w-4' text={t('plugin.marketplace.verifiedTip')} />}
{titleLeft} {/* This can be version badge */}
</div>
<OrgInfo
className="mt-0.5"
orgName={org}
packageName={name}
/>
</div>
<OrgInfo
className="mt-0.5"
orgName={org}
packageName={name}
/>
</div>
<Description
className="mt-3"
text={getLocalizedText(brief)}
descriptionLineRows={descriptionLineRows}
/>
{footer && <div>{footer}</div>}
</div>
<Description
className="mt-3"
text={getLocalizedText(brief)}
descriptionLineRows={descriptionLineRows}
/>
{footer && <div>{footer}</div>}
{limitedInstall
&& <div className='relative flex h-8 items-center gap-x-2 px-3 after:absolute after:bottom-0 after:left-0 after:right-0 after:top-0 after:bg-toast-warning-bg after:opacity-40'>
<RiAlertFill className='h-3 w-3 shrink-0 text-text-warning-secondary' />
<p className='system-xs-regular z-10 grow text-text-secondary'>
{t('plugin.installModal.installWarning')}
</p>
</div>}
</div>
)
}

View File

@@ -0,0 +1,46 @@
import { useGlobalPublicStore } from '@/context/global-public-context'
import type { SystemFeatures } from '@/types/feature'
import { InstallationScope } from '@/types/feature'
import type { Plugin, PluginManifestInMarket } from '../../types'
type PluginProps = (Plugin | PluginManifestInMarket) & { from: 'github' | 'marketplace' | 'package' }
export function pluginInstallLimit(plugin: PluginProps, systemFeatures: SystemFeatures) {
if (systemFeatures.plugin_installation_permission.restrict_to_marketplace_only) {
if (plugin.from === 'github' || plugin.from === 'package')
return { canInstall: false }
}
if (systemFeatures.plugin_installation_permission.plugin_installation_scope === InstallationScope.ALL) {
return {
canInstall: true,
}
}
if (systemFeatures.plugin_installation_permission.plugin_installation_scope === InstallationScope.NONE) {
return {
canInstall: false,
}
}
const verification = plugin.verification || {}
if (!plugin.verification || !plugin.verification.authorized_category)
verification.authorized_category = 'langgenius'
if (systemFeatures.plugin_installation_permission.plugin_installation_scope === InstallationScope.OFFICIAL_ONLY) {
return {
canInstall: verification.authorized_category === 'langgenius',
}
}
if (systemFeatures.plugin_installation_permission.plugin_installation_scope === InstallationScope.OFFICIAL_AND_PARTNER) {
return {
canInstall: verification.authorized_category === 'langgenius' || verification.authorized_category === 'partner',
}
}
return {
canInstall: true,
}
}
export default function usePluginInstallLimit(plugin: PluginProps) {
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
return pluginInstallLimit(plugin, systemFeatures)
}

View File

@@ -39,7 +39,7 @@ const Item: FC<Props> = ({
plugin_id: data.unique_identifier,
}
onFetchedPayload(payload)
setPayload(payload)
setPayload({ ...payload, from: dependency.type })
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data])

View File

@@ -8,6 +8,7 @@ import useGetIcon from '../../base/use-get-icon'
import { MARKETPLACE_API_PREFIX } from '@/config'
import Version from '../../base/version'
import type { VersionProps } from '../../../types'
import usePluginInstallLimit from '../../hooks/use-install-plugin-limit'
type Props = {
checked: boolean
@@ -29,9 +30,11 @@ const LoadedItem: FC<Props> = ({
...particleVersionInfo,
toInstallVersion: payload.version,
}
const { canInstall } = usePluginInstallLimit(payload)
return (
<div className='flex items-center space-x-2'>
<Checkbox
disabled={!canInstall}
className='shrink-0'
checked={checked}
onCheck={() => onCheckedChange(payload)}
@@ -43,6 +46,7 @@ const LoadedItem: FC<Props> = ({
icon: isFromMarketPlace ? `${MARKETPLACE_API_PREFIX}/plugins/${payload.org}/${payload.name}/icon` : getIconUrl(payload.icon),
}}
titleLeft={payload.version ? <Version {...versionInfo} /> : null}
limitedInstall={!canInstall}
/>
</div>
)

View File

@@ -29,7 +29,7 @@ const PackageItem: FC<Props> = ({
const plugin = pluginManifestToCardPluginProps(payload.value.manifest)
return (
<LoadedItem
payload={plugin}
payload={{ ...plugin, from: payload.type }}
checked={checked}
onCheckedChange={onCheckedChange}
isFromMarketPlace={isFromMarketPlace}

View File

@@ -1,5 +1,6 @@
'use client'
import type { FC } from 'react'
import type { ForwardRefRenderFunction } from 'react'
import { useImperativeHandle } from 'react'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import type { Dependency, GitHubItemAndMarketPlaceDependency, PackageDependency, Plugin, VersionInfo } from '../../../types'
import MarketplaceItem from '../item/marketplace-item'
@@ -9,22 +10,34 @@ import useCheckInstalled from '@/app/components/plugins/install-plugin/hooks/use
import produce from 'immer'
import PackageItem from '../item/package-item'
import LoadingError from '../../base/loading-error'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { pluginInstallLimit } from '../../hooks/use-install-plugin-limit'
type Props = {
allPlugins: Dependency[]
selectedPlugins: Plugin[]
onSelect: (plugin: Plugin, selectedIndex: number) => void
onSelect: (plugin: Plugin, selectedIndex: number, allCanInstallPluginsLength: number) => void
onSelectAll: (plugins: Plugin[], selectedIndexes: number[]) => void
onDeSelectAll: () => void
onLoadedAllPlugin: (installedInfo: Record<string, VersionInfo>) => void
isFromMarketPlace?: boolean
}
const InstallByDSLList: FC<Props> = ({
export type ExposeRefs = {
selectAllPlugins: () => void
deSelectAllPlugins: () => void
}
const InstallByDSLList: ForwardRefRenderFunction<ExposeRefs, Props> = ({
allPlugins,
selectedPlugins,
onSelect,
onSelectAll,
onDeSelectAll,
onLoadedAllPlugin,
isFromMarketPlace,
}) => {
}, ref) => {
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
// DSL has id, to get plugin info to show more info
const { isLoading: isFetchingMarketplaceDataById, data: infoGetById, error: infoByIdError } = useFetchPluginsInMarketPlaceByInfo(allPlugins.filter(d => d.type === 'marketplace').map((d) => {
const dependecy = (d as GitHubItemAndMarketPlaceDependency).value
@@ -97,7 +110,8 @@ const InstallByDSLList: FC<Props> = ({
const sortedList = allPlugins.filter(d => d.type === 'marketplace').map((d) => {
const p = d as GitHubItemAndMarketPlaceDependency
const id = p.value.marketplace_plugin_unique_identifier?.split(':')[0]
return infoGetById.data.list.find(item => item.plugin.plugin_id === id)?.plugin
const retPluginInfo = infoGetById.data.list.find(item => item.plugin.plugin_id === id)?.plugin
return { ...retPluginInfo, from: d.type } as Plugin
})
const payloads = sortedList
const failedIndex: number[] = []
@@ -106,7 +120,7 @@ const InstallByDSLList: FC<Props> = ({
if (payloads[i]) {
draft[index] = {
...payloads[i],
version: payloads[i].version || payloads[i].latest_version,
version: payloads[i]!.version || payloads[i]!.latest_version,
}
}
else { failedIndex.push(index) }
@@ -181,9 +195,35 @@ const InstallByDSLList: FC<Props> = ({
const handleSelect = useCallback((index: number) => {
return () => {
onSelect(plugins[index]!, index)
const canSelectPlugins = plugins.filter((p) => {
const { canInstall } = pluginInstallLimit(p!, systemFeatures)
return canInstall
})
onSelect(plugins[index]!, index, canSelectPlugins.length)
}
}, [onSelect, plugins])
}, [onSelect, plugins, systemFeatures])
useImperativeHandle(ref, () => ({
selectAllPlugins: () => {
const selectedIndexes: number[] = []
const selectedPlugins: Plugin[] = []
allPlugins.forEach((d, index) => {
const p = plugins[index]
if (!p)
return
const { canInstall } = pluginInstallLimit(p, systemFeatures)
if (canInstall) {
selectedIndexes.push(index)
selectedPlugins.push(p)
}
})
onSelectAll(selectedPlugins, selectedIndexes)
},
deSelectAllPlugins: () => {
onDeSelectAll()
},
}))
return (
<>
{allPlugins.map((d, index) => {
@@ -211,7 +251,7 @@ const InstallByDSLList: FC<Props> = ({
key={index}
checked={!!selectedPlugins.find(p => p.plugin_id === plugins[index]?.plugin_id)}
onCheckedChange={handleSelect(index)}
payload={plugin}
payload={{ ...plugin, from: d.type } as Plugin}
version={(d as GitHubItemAndMarketPlaceDependency).value.version! || plugin?.version || ''}
versionInfo={getVersionInfo(`${plugin?.org || plugin?.author}/${plugin?.name}`)}
/>
@@ -234,4 +274,4 @@ const InstallByDSLList: FC<Props> = ({
</>
)
}
export default React.memo(InstallByDSLList)
export default React.forwardRef(InstallByDSLList)

View File

@@ -1,15 +1,18 @@
'use client'
import type { FC } from 'react'
import { useRef } from 'react'
import React, { useCallback, useState } from 'react'
import type { Dependency, InstallStatusResponse, Plugin, VersionInfo } from '../../../types'
import Button from '@/app/components/base/button'
import { RiLoader2Line } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import type { ExposeRefs } from './install-multi'
import InstallMulti from './install-multi'
import { useInstallOrUpdate } from '@/service/use-plugins'
import useRefreshPluginList from '../../hooks/use-refresh-plugin-list'
import { useCanInstallPluginFromMarketplace } from '@/app/components/plugins/plugin-page/use-permission'
import { useMittContextSelector } from '@/context/mitt-context'
import Checkbox from '@/app/components/base/checkbox'
const i18nPrefix = 'plugin.installModal'
type Props = {
@@ -34,18 +37,8 @@ const Install: FC<Props> = ({
const [selectedPlugins, setSelectedPlugins] = React.useState<Plugin[]>([])
const [selectedIndexes, setSelectedIndexes] = React.useState<number[]>([])
const selectedPluginsNum = selectedPlugins.length
const installMultiRef = useRef<ExposeRefs>(null)
const { refreshPluginList } = useRefreshPluginList()
const handleSelect = (plugin: Plugin, selectedIndex: number) => {
const isSelected = !!selectedPlugins.find(p => p.plugin_id === plugin.plugin_id)
let nextSelectedPlugins
if (isSelected)
nextSelectedPlugins = selectedPlugins.filter(p => p.plugin_id !== plugin.plugin_id)
else
nextSelectedPlugins = [...selectedPlugins, plugin]
setSelectedPlugins(nextSelectedPlugins)
const nextSelectedIndexes = isSelected ? selectedIndexes.filter(i => i !== selectedIndex) : [...selectedIndexes, selectedIndex]
setSelectedIndexes(nextSelectedIndexes)
}
const [canInstall, setCanInstall] = React.useState(false)
const [installedInfo, setInstalledInfo] = useState<Record<string, VersionInfo> | undefined>(undefined)
@@ -81,6 +74,51 @@ const Install: FC<Props> = ({
installedInfo: installedInfo!,
})
}
const [isSelectAll, setIsSelectAll] = useState(false)
const [isIndeterminate, setIsIndeterminate] = useState(false)
const handleClickSelectAll = useCallback(() => {
if (isSelectAll)
installMultiRef.current?.deSelectAllPlugins()
else
installMultiRef.current?.selectAllPlugins()
}, [isSelectAll])
const handleSelectAll = useCallback((plugins: Plugin[], selectedIndexes: number[]) => {
setSelectedPlugins(plugins)
setSelectedIndexes(selectedIndexes)
setIsSelectAll(true)
setIsIndeterminate(false)
}, [])
const handleDeSelectAll = useCallback(() => {
setSelectedPlugins([])
setSelectedIndexes([])
setIsSelectAll(false)
setIsIndeterminate(false)
}, [])
const handleSelect = useCallback((plugin: Plugin, selectedIndex: number, allPluginsLength: number) => {
const isSelected = !!selectedPlugins.find(p => p.plugin_id === plugin.plugin_id)
let nextSelectedPlugins
if (isSelected)
nextSelectedPlugins = selectedPlugins.filter(p => p.plugin_id !== plugin.plugin_id)
else
nextSelectedPlugins = [...selectedPlugins, plugin]
setSelectedPlugins(nextSelectedPlugins)
const nextSelectedIndexes = isSelected ? selectedIndexes.filter(i => i !== selectedIndex) : [...selectedIndexes, selectedIndex]
setSelectedIndexes(nextSelectedIndexes)
if (nextSelectedPlugins.length === 0) {
setIsSelectAll(false)
setIsIndeterminate(false)
}
else if (nextSelectedPlugins.length === allPluginsLength) {
setIsSelectAll(true)
setIsIndeterminate(false)
}
else {
setIsIndeterminate(true)
setIsSelectAll(false)
}
}, [selectedPlugins, selectedIndexes])
const { canInstallPluginFromMarketplace } = useCanInstallPluginFromMarketplace()
return (
<>
@@ -90,9 +128,12 @@ const Install: FC<Props> = ({
</div>
<div className='w-full space-y-1 rounded-2xl bg-background-section-burn p-2'>
<InstallMulti
ref={installMultiRef}
allPlugins={allPlugins}
selectedPlugins={selectedPlugins}
onSelect={handleSelect}
onSelectAll={handleSelectAll}
onDeSelectAll={handleDeSelectAll}
onLoadedAllPlugin={handleLoadedAllPlugin}
isFromMarketPlace={isFromMarketPlace}
/>
@@ -100,21 +141,29 @@ const Install: FC<Props> = ({
</div>
{/* Action Buttons */}
{!isHideButton && (
<div className='flex items-center justify-end gap-2 self-stretch p-6 pt-5'>
{!canInstall && (
<Button variant='secondary' className='min-w-[72px]' onClick={onCancel}>
{t('common.operation.cancel')}
<div className='flex items-center justify-between gap-2 self-stretch p-6 pt-5'>
<div className='px-2'>
{canInstall && <div className='flex items-center gap-x-2' onClick={handleClickSelectAll}>
<Checkbox checked={isSelectAll} indeterminate={isIndeterminate} />
<p className='system-sm-medium cursor-pointer text-text-secondary'>{isSelectAll ? t('common.operation.deSelectAll') : t('common.operation.selectAll')}</p>
</div>}
</div>
<div className='flex items-center justify-end gap-2 self-stretch'>
{!canInstall && (
<Button variant='secondary' className='min-w-[72px]' onClick={onCancel}>
{t('common.operation.cancel')}
</Button>
)}
<Button
variant='primary'
className='flex min-w-[72px] space-x-0.5'
disabled={!canInstall || isInstalling || selectedPlugins.length === 0 || !canInstallPluginFromMarketplace}
onClick={handleInstall}
>
{isInstalling && <RiLoader2Line className='h-4 w-4 animate-spin-slow' />}
<span>{t(`${i18nPrefix}.${isInstalling ? 'installing' : 'install'}`)}</span>
</Button>
)}
<Button
variant='primary'
className='flex min-w-[72px] space-x-0.5'
disabled={!canInstall || isInstalling || selectedPlugins.length === 0 || !canInstallPluginFromMarketplace}
onClick={handleInstall}
>
{isInstalling && <RiLoader2Line className='h-4 w-4 animate-spin-slow' />}
<span>{t(`${i18nPrefix}.${isInstalling ? 'installing' : 'install'}`)}</span>
</Button>
</div>
</div>
)}

View File

@@ -124,7 +124,7 @@ const Installed: FC<Props> = ({
/>
</p>
{!isDifyVersionCompatible && (
<p className='system-md-regular flex items-center gap-1 text-text-secondary text-text-warning'>
<p className='system-md-regular flex items-center gap-1 text-text-warning'>
{t('plugin.difyVersionNotCompatible', { minimalDifyVersion: payload.meta.minimum_dify_version })}
</p>
)}

View File

@@ -15,6 +15,7 @@ import Version from '../../base/version'
import { usePluginTaskList } from '@/service/use-plugins'
import { gte } from 'semver'
import { useAppContext } from '@/context/app-context'
import useInstallPluginLimit from '../../hooks/use-install-plugin-limit'
const i18nPrefix = 'plugin.installModal'
@@ -124,15 +125,16 @@ const Installed: FC<Props> = ({
const isDifyVersionCompatible = useMemo(() => {
if (!pluginDeclaration || !langeniusVersionInfo.current_version) return true
return gte(langeniusVersionInfo.current_version, pluginDeclaration?.manifest.meta.minimum_dify_version ?? '0.0.0')
}, [langeniusVersionInfo.current_version, pluginDeclaration?.manifest.meta.minimum_dify_version])
}, [langeniusVersionInfo.current_version, pluginDeclaration])
const { canInstall } = useInstallPluginLimit({ ...payload, from: 'marketplace' })
return (
<>
<div className='flex flex-col items-start justify-center gap-4 self-stretch px-6 py-3'>
<div className='system-md-regular text-text-secondary'>
<p>{t(`${i18nPrefix}.readyToInstall`)}</p>
{!isDifyVersionCompatible && (
<p className='system-md-regular text-text-secondary text-text-warning'>
<p className='system-md-regular text-text-warning'>
{t('plugin.difyVersionNotCompatible', { minimalDifyVersion: pluginDeclaration?.manifest.meta.minimum_dify_version })}
</p>
)}
@@ -146,6 +148,7 @@ const Installed: FC<Props> = ({
installedVersion={installedVersion}
toInstallVersion={toInstallVersion}
/>}
limitedInstall={!canInstall}
/>
</div>
</div>
@@ -159,7 +162,7 @@ const Installed: FC<Props> = ({
<Button
variant='primary'
className='flex min-w-[72px] space-x-0.5'
disabled={isInstalling || isLoading}
disabled={isInstalling || isLoading || !canInstall}
onClick={handleInstall}
>
{isInstalling && <RiLoader2Line className='h-4 w-4 animate-spin-slow' />}

View File

@@ -1,5 +1,6 @@
import type { Plugin, PluginDeclaration, PluginManifestInMarket } from '../types'
import type { GitHubUrlInfo } from '@/app/components/plugins/types'
import { isEmpty } from 'lodash-es'
export const pluginManifestToCardPluginProps = (pluginManifest: PluginDeclaration): Plugin => {
return {
@@ -47,6 +48,7 @@ export const pluginManifestInMarketToPluginProps = (pluginManifest: PluginManife
},
tags: [],
badges: pluginManifest.badges,
verification: isEmpty(pluginManifest.verification) ? { authorized_category: 'langgenius' } : pluginManifest.verification,
}
}

View File

@@ -56,7 +56,7 @@ const CardWrapper = ({
>
{t('plugin.detailPanel.operation.install')}
</Button>
<a href={`${getPluginLinkInMarketplace(plugin)}?language=${localeFromLocale}${theme ? `&theme=${theme}` : ''}`} target='_blank' className='block w-[calc(50%-4px)] flex-1 shrink-0'>
<a href={getPluginLinkInMarketplace(plugin, { language: localeFromLocale, theme })} target='_blank' className='block w-[calc(50%-4px)] flex-1 shrink-0'>
<Button
className='w-full gap-0.5'
>

View File

@@ -8,8 +8,8 @@ import type {
} from '@/app/components/plugins/marketplace/types'
import {
MARKETPLACE_API_PREFIX,
MARKETPLACE_URL_PREFIX,
} from '@/config'
import { getMarketplaceUrl } from '@/utils/var'
export const getPluginIconInMarketplace = (plugin: Plugin) => {
if (plugin.type === 'bundle')
@@ -32,10 +32,10 @@ export const getFormattedPlugin = (bundle: any) => {
}
}
export const getPluginLinkInMarketplace = (plugin: Plugin) => {
export const getPluginLinkInMarketplace = (plugin: Plugin, params?: Record<string, string | undefined>) => {
if (plugin.type === 'bundle')
return `${MARKETPLACE_URL_PREFIX}/bundles/${plugin.org}/${plugin.name}`
return `${MARKETPLACE_URL_PREFIX}/plugins/${plugin.org}/${plugin.name}`
return getMarketplaceUrl(`/bundles/${plugin.org}/${plugin.name}`, params)
return getMarketplaceUrl(`/plugins/${plugin.org}/${plugin.name}`, params)
}
export const getMarketplacePluginsByCollectionId = async (collectionId: string, query?: CollectionsAndPluginsSearchParams) => {

View File

@@ -33,8 +33,9 @@ import { useGetLanguage } from '@/context/i18n'
import { useModalContext } from '@/context/modal-context'
import { useProviderContext } from '@/context/provider-context'
import { useInvalidateAllToolProviders } from '@/service/use-tools'
import { API_PREFIX, MARKETPLACE_URL_PREFIX } from '@/config'
import { API_PREFIX } from '@/config'
import cn from '@/utils/classnames'
import { getMarketplaceUrl } from '@/utils/var'
const i18nPrefix = 'plugin.action'
@@ -87,7 +88,7 @@ const DetailHeader = ({
if (isFromGitHub)
return `https://github.com/${meta!.repo}`
if (isFromMarketplace)
return `${MARKETPLACE_URL_PREFIX}/plugins/${author}/${name}${theme ? `?theme=${theme}` : ''}`
return getMarketplaceUrl(`/plugins/${author}/${name}`, { theme })
return ''
}, [author, isFromGitHub, isFromMarketplace, meta, name, theme])

View File

@@ -21,13 +21,14 @@ import OrgInfo from '../card/base/org-info'
import Title from '../card/base/title'
import Action from './action'
import cn from '@/utils/classnames'
import { API_PREFIX, MARKETPLACE_URL_PREFIX } from '@/config'
import { API_PREFIX } from '@/config'
import { useSingleCategories } from '../hooks'
import { useRenderI18nObject } from '@/hooks/use-i18n'
import useRefreshPluginList from '@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list'
import { useAppContext } from '@/context/app-context'
import { gte } from 'semver'
import Tooltip from '@/app/components/base/tooltip'
import { getMarketplaceUrl } from '@/utils/var'
type Props = {
className?: string
@@ -166,7 +167,7 @@ const PluginItem: FC<Props> = ({
}
{source === PluginSource.marketplace
&& <>
<a href={`${MARKETPLACE_URL_PREFIX}/plugins/${author}/${name}${theme ? `?theme=${theme}` : ''}`} target='_blank' className='flex items-center gap-0.5'>
<a href={getMarketplaceUrl(`/plugins/${author}/${name}`, { theme })} target='_blank' className='flex items-center gap-0.5'>
<div className='system-2xs-medium-uppercase text-text-tertiary'>{t('plugin.from')} <span className='text-text-secondary'>marketplace</span></div>
<RiArrowRightUpLine className='h-3 w-3 text-text-tertiary' />
</a>

View File

@@ -1,4 +1,5 @@
import React, { useMemo, useRef, useState } from 'react'
'use client'
import React, { useEffect, useMemo, useRef, useState } from 'react'
import { MagicBox } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
import { FileZip } from '@/app/components/base/icons/src/vender/solid/files'
import { Github } from '@/app/components/base/icons/src/vender/solid/general'
@@ -14,12 +15,18 @@ import { noop } from 'lodash-es'
import { useGlobalPublicStore } from '@/context/global-public-context'
import Button from '@/app/components/base/button'
type InstallMethod = {
icon: React.FC<{ className?: string }>
text: string
action: string
}
const Empty = () => {
const { t } = useTranslation()
const fileInputRef = useRef<HTMLInputElement>(null)
const [selectedAction, setSelectedAction] = useState<string | null>(null)
const [selectedFile, setSelectedFile] = useState<File | null>(null)
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
const { enable_marketplace, plugin_installation_permission } = useGlobalPublicStore(s => s.systemFeatures)
const setActiveTab = usePluginPageContext(v => v.setActiveTab)
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
@@ -39,6 +46,22 @@ const Empty = () => {
return t('plugin.list.notFound')
}, [pluginList?.plugins.length, t, filters.categories.length, filters.tags.length, filters.searchQuery])
const [installMethods, setInstallMethods] = useState<InstallMethod[]>([])
useEffect(() => {
const methods = []
if (enable_marketplace)
methods.push({ icon: MagicBox, text: t('plugin.source.marketplace'), action: 'marketplace' })
if (plugin_installation_permission.restrict_to_marketplace_only) {
setInstallMethods(methods)
}
else {
methods.push({ icon: Github, text: t('plugin.source.github'), action: 'github' })
methods.push({ icon: FileZip, text: t('plugin.source.local'), action: 'local' })
setInstallMethods(methods)
}
}, [plugin_installation_permission, enable_marketplace, t])
return (
<div className='relative z-0 w-full grow'>
{/* skeleton */}
@@ -71,15 +94,7 @@ const Empty = () => {
accept={SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS}
/>
<div className='flex w-full flex-col gap-y-1'>
{[
...(
(enable_marketplace)
? [{ icon: MagicBox, text: t('plugin.list.source.marketplace'), action: 'marketplace' }]
: []
),
{ icon: Github, text: t('plugin.list.source.github'), action: 'github' },
{ icon: FileZip, text: t('plugin.list.source.local'), action: 'local' },
].map(({ icon: Icon, text, action }) => (
{installMethods.map(({ icon: Icon, text, action }) => (
<Button
key={action}
className='justify-start gap-x-0.5 px-3'

View File

@@ -136,7 +136,7 @@ const PluginPage = ({
const options = usePluginPageContext(v => v.options)
const activeTab = usePluginPageContext(v => v.activeTab)
const setActiveTab = usePluginPageContext(v => v.setActiveTab)
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
const { enable_marketplace, branding } = useGlobalPublicStore(s => s.systemFeatures)
const isPluginsTab = useMemo(() => activeTab === PLUGIN_PAGE_TABS_MAP.plugins, [activeTab])
const isExploringMarketplace = useMemo(() => {
@@ -225,7 +225,7 @@ const PluginPage = ({
)
}
{
canSetPermissions && (
canSetPermissions && !branding.enabled && (
<Tooltip
popupContent={t('plugin.privilege.title')}
>

View File

@@ -1,6 +1,6 @@
'use client'
import { useRef, useState } from 'react'
import { useEffect, useRef, useState } from 'react'
import { RiAddLine, RiArrowDownSLine } from '@remixicon/react'
import Button from '@/app/components/base/button'
import { MagicBox } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
@@ -22,6 +22,13 @@ import { useGlobalPublicStore } from '@/context/global-public-context'
type Props = {
onSwitchToMarketplaceTab: () => void
}
type InstallMethod = {
icon: React.FC<{ className?: string }>
text: string
action: string
}
const InstallPluginDropdown = ({
onSwitchToMarketplaceTab,
}: Props) => {
@@ -30,7 +37,7 @@ const InstallPluginDropdown = ({
const [isMenuOpen, setIsMenuOpen] = useState(false)
const [selectedAction, setSelectedAction] = useState<string | null>(null)
const [selectedFile, setSelectedFile] = useState<File | null>(null)
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
const { enable_marketplace, plugin_installation_permission } = useGlobalPublicStore(s => s.systemFeatures)
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]
@@ -54,6 +61,22 @@ const InstallPluginDropdown = ({
// console.log(res)
// }
const [installMethods, setInstallMethods] = useState<InstallMethod[]>([])
useEffect(() => {
const methods = []
if (enable_marketplace)
methods.push({ icon: MagicBox, text: t('plugin.source.marketplace'), action: 'marketplace' })
if (plugin_installation_permission.restrict_to_marketplace_only) {
setInstallMethods(methods)
}
else {
methods.push({ icon: Github, text: t('plugin.source.github'), action: 'github' })
methods.push({ icon: FileZip, text: t('plugin.source.local'), action: 'local' })
setInstallMethods(methods)
}
}, [plugin_installation_permission, enable_marketplace, t])
return (
<PortalToFollowElem
open={isMenuOpen}
@@ -84,15 +107,7 @@ const InstallPluginDropdown = ({
accept={SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS}
/>
<div className='w-full'>
{[
...(
(enable_marketplace)
? [{ icon: MagicBox, text: t('plugin.source.marketplace'), action: 'marketplace' }]
: []
),
{ icon: Github, text: t('plugin.source.github'), action: 'github' },
{ icon: FileZip, text: t('plugin.source.local'), action: 'local' },
].map(({ icon: Icon, text, action }) => (
{installMethods.map(({ icon: Icon, text, action }) => (
<div
key={action}
className='flex w-full !cursor-pointer items-center gap-1 rounded-lg px-2 py-1.5 hover:bg-state-base-hover'

View File

@@ -94,7 +94,11 @@ export type PluginManifestInMarket = {
introduction: string
verified: boolean
install_count: number
badges: string[]
badges: string[],
verification: {
authorized_category: 'langgenius' | 'partner' | 'community'
},
from: Dependency['type']
}
export type PluginDetail = {
@@ -145,7 +149,11 @@ export type Plugin = {
settings: CredentialFormSchemaBase[]
}
tags: { name: string }[]
badges: string[]
badges: string[],
verification: {
authorized_category: 'langgenius' | 'partner' | 'community'
},
from: Dependency['type']
}
export enum PermissionType {

View File

@@ -12,7 +12,7 @@ import { useMarketplace } from './hooks'
import List from '@/app/components/plugins/marketplace/list'
import Loading from '@/app/components/base/loading'
import { getLocaleOnClient } from '@/i18n'
import { MARKETPLACE_URL_PREFIX } from '@/config'
import { getMarketplaceUrl } from '@/utils/var'
type MarketplaceProps = {
searchPluginText: string
@@ -84,7 +84,7 @@ const Marketplace = ({
</span>
{t('common.operation.in')}
<a
href={`${MARKETPLACE_URL_PREFIX}?language=${locale}&q=${searchPluginText}&tags=${filterPluginTags.join(',')}${theme ? `&theme=${theme}` : ''}`}
href={getMarketplaceUrl('', { language: locale, q: searchPluginText, tags: filterPluginTags.join(','), theme })}
className='system-sm-medium ml-1 flex items-center text-text-accent'
target='_blank'
>

View File

@@ -12,9 +12,9 @@ import {
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import cn from '@/utils/classnames'
import { MARKETPLACE_URL_PREFIX } from '@/config'
import { useDownloadPlugin } from '@/service/use-plugins'
import { downloadFile } from '@/utils/format'
import { getMarketplaceUrl } from '@/utils/var'
type Props = {
open: boolean
@@ -80,7 +80,7 @@ const OperationDropdown: FC<Props> = ({
<PortalToFollowElemContent className='z-[9999]'>
<div className='w-[112px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg'>
<div onClick={handleDownload} className='system-md-regular cursor-pointer rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover'>{t('common.operation.download')}</div>
<a href={`${MARKETPLACE_URL_PREFIX}/plugins/${author}/${name}${theme ? `?theme=${theme}` : ''}`} target='_blank' className='system-md-regular block cursor-pointer rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover'>{t('common.operation.viewDetails')}</a>
<a href={getMarketplaceUrl(`/plugins/${author}/${name}`, { theme })} target='_blank' className='system-md-regular block cursor-pointer rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover'>{t('common.operation.viewDetails')}</a>
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>

View File

@@ -1,30 +0,0 @@
'use client'
import type { FC } from 'react'
import classNames from '@/utils/classnames'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useTheme } from 'next-themes'
type LoginLogoProps = {
className?: string
}
const LoginLogo: FC<LoginLogoProps> = ({
className,
}) => {
const { systemFeatures } = useGlobalPublicStore()
const { theme } = useTheme()
let src = theme === 'light' ? '/logo/logo-site.png' : `/logo/logo-site-${theme}.png`
if (systemFeatures.branding.enabled)
src = systemFeatures.branding.login_page_logo
return (
<img
src={src}
className={classNames('block w-auto h-10', className)}
alt='logo'
/>
)
}
export default LoginLogo

View File

@@ -64,6 +64,8 @@ const translation = {
skip: 'Skip',
format: 'Format',
more: 'More',
selectAll: 'Select All',
deSelectAll: 'Deselect All',
},
errorMsg: {
fieldRequired: '{{field}} is required',

View File

@@ -154,6 +154,7 @@ const translation = {
next: 'Next',
pluginLoadError: 'Plugin load error',
pluginLoadErrorDesc: 'This plugin will not be installed',
installWarning: 'This plugin is not allowed to be installed.',
},
installFromGitHub: {
installPlugin: 'Install plugin from GitHub',

View File

@@ -64,6 +64,8 @@ const translation = {
in: '中',
format: 'フォーマット',
more: 'もっと',
selectAll: 'すべて選択',
deSelectAll: 'すべて選択解除',
},
errorMsg: {
fieldRequired: '{{field}}は必要です',

View File

@@ -137,6 +137,7 @@ const translation = {
installPlugin: 'プラグインをインストールする',
back: '戻る',
uploadingPackage: '{{packageName}}をアップロード中...',
installWarning: 'このプラグインはインストールを許可されていません。',
},
installFromGitHub: {
installedSuccessfully: 'インストールに成功しました',

View File

@@ -64,6 +64,8 @@ const translation = {
skip: '跳过',
format: '格式化',
more: '更多',
selectAll: '全选',
deSelectAll: '取消全选',
},
errorMsg: {
fieldRequired: '{{field}} 为必填项',

View File

@@ -154,6 +154,7 @@ const translation = {
next: '下一步',
pluginLoadError: '插件加载错误',
pluginLoadErrorDesc: '此插件将不会被安装',
installWarning: '此插件不允许安装。',
},
installFromGitHub: {
installPlugin: '从 GitHub 安装插件',

View File

@@ -13,12 +13,23 @@ export enum LicenseStatus {
LOST = 'lost',
}
export enum InstallationScope {
ALL = 'all',
NONE = 'none',
OFFICIAL_ONLY = 'official_only',
OFFICIAL_AND_PARTNER = 'official_and_specific_partners',
}
type License = {
status: LicenseStatus
expired_at: string | null
}
export type SystemFeatures = {
plugin_installation_permission: {
plugin_installation_scope: InstallationScope,
restrict_to_marketplace_only: boolean
},
sso_enforced_for_signin: boolean
sso_enforced_for_signin_protocol: SSOProtocol | ''
sso_enforced_for_web: boolean
@@ -50,6 +61,10 @@ export type SystemFeatures = {
}
export const defaultSystemFeatures: SystemFeatures = {
plugin_installation_permission: {
plugin_installation_scope: InstallationScope.ALL,
restrict_to_marketplace_only: false,
},
sso_enforced_for_signin: false,
sso_enforced_for_signin_protocol: '',
sso_enforced_for_web: false,

View File

@@ -1,4 +1,4 @@
import { MAX_VAR_KEY_LENGTH, VAR_ITEM_TEMPLATE, VAR_ITEM_TEMPLATE_IN_WORKFLOW, getMaxVarNameLength } from '@/config'
import { MARKETPLACE_URL_PREFIX, MAX_VAR_KEY_LENGTH, VAR_ITEM_TEMPLATE, VAR_ITEM_TEMPLATE_IN_WORKFLOW, getMaxVarNameLength } from '@/config'
import {
CONTEXT_PLACEHOLDER_TEXT,
HISTORY_PLACEHOLDER_TEXT,
@@ -108,3 +108,15 @@ export const getVars = (value: string) => {
// Set the value of basePath
// example: /dify
export const basePath = ''
export function getMarketplaceUrl(path: string, params?: Record<string, string | undefined>) {
const searchParams = new URLSearchParams({ source: encodeURIComponent(window.location.origin) })
if (params) {
Object.keys(params).forEach((key) => {
const value = params[key]
if (value !== undefined && value !== null)
searchParams.append(key, value)
})
}
return `${MARKETPLACE_URL_PREFIX}${path}?${searchParams.toString()}`
}