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 ProviderCard from '@/app/components/plugins/provider-card'
import List from '@/app/components/plugins/marketplace/list' import List from '@/app/components/plugins/marketplace/list'
import type { Plugin } from '@/app/components/plugins/types' import type { Plugin } from '@/app/components/plugins/types'
import { MARKETPLACE_URL_PREFIX } from '@/config'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { getLocaleOnClient } from '@/i18n' import { getLocaleOnClient } from '@/i18n'
import { getMarketplaceUrl } from '@/utils/var'
type InstallFromMarketplaceProps = { type InstallFromMarketplaceProps = {
providers: ModelProvider[] providers: ModelProvider[]
@@ -55,7 +55,7 @@ const InstallFromMarketplace = ({
</div> </div>
<div className='mb-2 flex items-center pt-2'> <div className='mb-2 flex items-center pt-2'>
<span className='system-sm-regular pr-1 text-text-tertiary'>{t('common.modelProvider.discoverMore')}</span> <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')} {t('plugin.marketplace.difyMarketplace')}
<RiArrowRightUpLine className='h-4 w-4' /> <RiArrowRightUpLine className='h-4 w-4' />
</Link> </Link>

View File

@@ -15,6 +15,7 @@ import { renderI18nObject } from '@/i18n'
import { useMixedTranslation } from '@/app/components/plugins/marketplace/hooks' import { useMixedTranslation } from '@/app/components/plugins/marketplace/hooks'
import Partner from '../base/badges/partner' import Partner from '../base/badges/partner'
import Verified from '../base/badges/verified' import Verified from '../base/badges/verified'
import { RiAlertFill } from '@remixicon/react'
export type Props = { export type Props = {
className?: string className?: string
@@ -28,6 +29,7 @@ export type Props = {
isLoading?: boolean isLoading?: boolean
loadingFileName?: string loadingFileName?: string
locale?: string locale?: string
limitedInstall?: boolean
} }
const Card = ({ const Card = ({
@@ -42,6 +44,7 @@ const Card = ({
isLoading = false, isLoading = false,
loadingFileName, loadingFileName,
locale: localeFromProps, locale: localeFromProps,
limitedInstall = false,
}: Props) => { }: Props) => {
const defaultLocale = useGetLanguage() const defaultLocale = useGetLanguage()
const locale = localeFromProps ? getLanguage(localeFromProps) : defaultLocale const locale = localeFromProps ? getLanguage(localeFromProps) : defaultLocale
@@ -54,7 +57,7 @@ const Card = ({
obj ? renderI18nObject(obj, locale) : '' obj ? renderI18nObject(obj, locale) : ''
const isPartner = badges.includes('partner') 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) { if (isLoading) {
return ( return (
<Placeholder <Placeholder
@@ -66,30 +69,39 @@ const Card = ({
return ( return (
<div className={wrapClassName}> <div className={wrapClassName}>
{!hideCornerMark && <CornerMark text={cornerMark} />} <div className={cn('p-4 pb-3', limitedInstall && 'pb-1')}>
{/* Header */} {!hideCornerMark && <CornerMark text={cornerMark} />}
<div className="flex"> {/* Header */}
<Icon src={icon} installed={installed} installFailed={installFailed} /> <div className="flex">
<div className="ml-3 w-0 grow"> <Icon src={icon} installed={installed} installFailed={installFailed} />
<div className="flex h-5 items-center"> <div className="ml-3 w-0 grow">
<Title title={getLocalizedText(label)} /> <div className="flex h-5 items-center">
{isPartner && <Partner className='ml-0.5 h-4 w-4' text={t('plugin.marketplace.partnerTip')} />} <Title title={getLocalizedText(label)} />
{verified && <Verified className='ml-0.5 h-4 w-4' text={t('plugin.marketplace.verifiedTip')} />} {isPartner && <Partner className='ml-0.5 h-4 w-4' text={t('plugin.marketplace.partnerTip')} />}
{titleLeft} {/* This can be version badge */} {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> </div>
<OrgInfo
className="mt-0.5"
orgName={org}
packageName={name}
/>
</div> </div>
<Description
className="mt-3"
text={getLocalizedText(brief)}
descriptionLineRows={descriptionLineRows}
/>
{footer && <div>{footer}</div>}
</div> </div>
<Description {limitedInstall
className="mt-3" && <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'>
text={getLocalizedText(brief)} <RiAlertFill className='h-3 w-3 shrink-0 text-text-warning-secondary' />
descriptionLineRows={descriptionLineRows} <p className='system-xs-regular z-10 grow text-text-secondary'>
/> {t('plugin.installModal.installWarning')}
{footer && <div>{footer}</div>} </p>
</div>}
</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, plugin_id: data.unique_identifier,
} }
onFetchedPayload(payload) onFetchedPayload(payload)
setPayload(payload) setPayload({ ...payload, from: dependency.type })
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [data]) }, [data])

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
'use client' '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 React, { useCallback, useEffect, useMemo, useState } from 'react'
import type { Dependency, GitHubItemAndMarketPlaceDependency, PackageDependency, Plugin, VersionInfo } from '../../../types' import type { Dependency, GitHubItemAndMarketPlaceDependency, PackageDependency, Plugin, VersionInfo } from '../../../types'
import MarketplaceItem from '../item/marketplace-item' 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 produce from 'immer'
import PackageItem from '../item/package-item' import PackageItem from '../item/package-item'
import LoadingError from '../../base/loading-error' import LoadingError from '../../base/loading-error'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { pluginInstallLimit } from '../../hooks/use-install-plugin-limit'
type Props = { type Props = {
allPlugins: Dependency[] allPlugins: Dependency[]
selectedPlugins: Plugin[] 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 onLoadedAllPlugin: (installedInfo: Record<string, VersionInfo>) => void
isFromMarketPlace?: boolean isFromMarketPlace?: boolean
} }
const InstallByDSLList: FC<Props> = ({ export type ExposeRefs = {
selectAllPlugins: () => void
deSelectAllPlugins: () => void
}
const InstallByDSLList: ForwardRefRenderFunction<ExposeRefs, Props> = ({
allPlugins, allPlugins,
selectedPlugins, selectedPlugins,
onSelect, onSelect,
onSelectAll,
onDeSelectAll,
onLoadedAllPlugin, onLoadedAllPlugin,
isFromMarketPlace, isFromMarketPlace,
}) => { }, ref) => {
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
// DSL has id, to get plugin info to show more info // 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 { isLoading: isFetchingMarketplaceDataById, data: infoGetById, error: infoByIdError } = useFetchPluginsInMarketPlaceByInfo(allPlugins.filter(d => d.type === 'marketplace').map((d) => {
const dependecy = (d as GitHubItemAndMarketPlaceDependency).value 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 sortedList = allPlugins.filter(d => d.type === 'marketplace').map((d) => {
const p = d as GitHubItemAndMarketPlaceDependency const p = d as GitHubItemAndMarketPlaceDependency
const id = p.value.marketplace_plugin_unique_identifier?.split(':')[0] 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 payloads = sortedList
const failedIndex: number[] = [] const failedIndex: number[] = []
@@ -106,7 +120,7 @@ const InstallByDSLList: FC<Props> = ({
if (payloads[i]) { if (payloads[i]) {
draft[index] = { draft[index] = {
...payloads[i], ...payloads[i],
version: payloads[i].version || payloads[i].latest_version, version: payloads[i]!.version || payloads[i]!.latest_version,
} }
} }
else { failedIndex.push(index) } else { failedIndex.push(index) }
@@ -181,9 +195,35 @@ const InstallByDSLList: FC<Props> = ({
const handleSelect = useCallback((index: number) => { const handleSelect = useCallback((index: number) => {
return () => { 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 ( return (
<> <>
{allPlugins.map((d, index) => { {allPlugins.map((d, index) => {
@@ -211,7 +251,7 @@ const InstallByDSLList: FC<Props> = ({
key={index} key={index}
checked={!!selectedPlugins.find(p => p.plugin_id === plugins[index]?.plugin_id)} checked={!!selectedPlugins.find(p => p.plugin_id === plugins[index]?.plugin_id)}
onCheckedChange={handleSelect(index)} onCheckedChange={handleSelect(index)}
payload={plugin} payload={{ ...plugin, from: d.type } as Plugin}
version={(d as GitHubItemAndMarketPlaceDependency).value.version! || plugin?.version || ''} version={(d as GitHubItemAndMarketPlaceDependency).value.version! || plugin?.version || ''}
versionInfo={getVersionInfo(`${plugin?.org || plugin?.author}/${plugin?.name}`)} 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' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import { useRef } from 'react'
import React, { useCallback, useState } from 'react' import React, { useCallback, useState } from 'react'
import type { Dependency, InstallStatusResponse, Plugin, VersionInfo } from '../../../types' import type { Dependency, InstallStatusResponse, Plugin, VersionInfo } from '../../../types'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import { RiLoader2Line } from '@remixicon/react' import { RiLoader2Line } from '@remixicon/react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import type { ExposeRefs } from './install-multi'
import InstallMulti from './install-multi' import InstallMulti from './install-multi'
import { useInstallOrUpdate } from '@/service/use-plugins' import { useInstallOrUpdate } from '@/service/use-plugins'
import useRefreshPluginList from '../../hooks/use-refresh-plugin-list' import useRefreshPluginList from '../../hooks/use-refresh-plugin-list'
import { useCanInstallPluginFromMarketplace } from '@/app/components/plugins/plugin-page/use-permission' import { useCanInstallPluginFromMarketplace } from '@/app/components/plugins/plugin-page/use-permission'
import { useMittContextSelector } from '@/context/mitt-context' import { useMittContextSelector } from '@/context/mitt-context'
import Checkbox from '@/app/components/base/checkbox'
const i18nPrefix = 'plugin.installModal' const i18nPrefix = 'plugin.installModal'
type Props = { type Props = {
@@ -34,18 +37,8 @@ const Install: FC<Props> = ({
const [selectedPlugins, setSelectedPlugins] = React.useState<Plugin[]>([]) const [selectedPlugins, setSelectedPlugins] = React.useState<Plugin[]>([])
const [selectedIndexes, setSelectedIndexes] = React.useState<number[]>([]) const [selectedIndexes, setSelectedIndexes] = React.useState<number[]>([])
const selectedPluginsNum = selectedPlugins.length const selectedPluginsNum = selectedPlugins.length
const installMultiRef = useRef<ExposeRefs>(null)
const { refreshPluginList } = useRefreshPluginList() 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 [canInstall, setCanInstall] = React.useState(false)
const [installedInfo, setInstalledInfo] = useState<Record<string, VersionInfo> | undefined>(undefined) const [installedInfo, setInstalledInfo] = useState<Record<string, VersionInfo> | undefined>(undefined)
@@ -81,6 +74,51 @@ const Install: FC<Props> = ({
installedInfo: installedInfo!, 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() const { canInstallPluginFromMarketplace } = useCanInstallPluginFromMarketplace()
return ( return (
<> <>
@@ -90,9 +128,12 @@ const Install: FC<Props> = ({
</div> </div>
<div className='w-full space-y-1 rounded-2xl bg-background-section-burn p-2'> <div className='w-full space-y-1 rounded-2xl bg-background-section-burn p-2'>
<InstallMulti <InstallMulti
ref={installMultiRef}
allPlugins={allPlugins} allPlugins={allPlugins}
selectedPlugins={selectedPlugins} selectedPlugins={selectedPlugins}
onSelect={handleSelect} onSelect={handleSelect}
onSelectAll={handleSelectAll}
onDeSelectAll={handleDeSelectAll}
onLoadedAllPlugin={handleLoadedAllPlugin} onLoadedAllPlugin={handleLoadedAllPlugin}
isFromMarketPlace={isFromMarketPlace} isFromMarketPlace={isFromMarketPlace}
/> />
@@ -100,21 +141,29 @@ const Install: FC<Props> = ({
</div> </div>
{/* Action Buttons */} {/* Action Buttons */}
{!isHideButton && ( {!isHideButton && (
<div className='flex items-center justify-end gap-2 self-stretch p-6 pt-5'> <div className='flex items-center justify-between gap-2 self-stretch p-6 pt-5'>
{!canInstall && ( <div className='px-2'>
<Button variant='secondary' className='min-w-[72px]' onClick={onCancel}> {canInstall && <div className='flex items-center gap-x-2' onClick={handleClickSelectAll}>
{t('common.operation.cancel')} <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>
)} </div>
<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> </p>
{!isDifyVersionCompatible && ( {!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 })} {t('plugin.difyVersionNotCompatible', { minimalDifyVersion: payload.meta.minimum_dify_version })}
</p> </p>
)} )}

View File

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

View File

@@ -8,8 +8,8 @@ import type {
} from '@/app/components/plugins/marketplace/types' } from '@/app/components/plugins/marketplace/types'
import { import {
MARKETPLACE_API_PREFIX, MARKETPLACE_API_PREFIX,
MARKETPLACE_URL_PREFIX,
} from '@/config' } from '@/config'
import { getMarketplaceUrl } from '@/utils/var'
export const getPluginIconInMarketplace = (plugin: Plugin) => { export const getPluginIconInMarketplace = (plugin: Plugin) => {
if (plugin.type === 'bundle') 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') if (plugin.type === 'bundle')
return `${MARKETPLACE_URL_PREFIX}/bundles/${plugin.org}/${plugin.name}` return getMarketplaceUrl(`/bundles/${plugin.org}/${plugin.name}`, params)
return `${MARKETPLACE_URL_PREFIX}/plugins/${plugin.org}/${plugin.name}` return getMarketplaceUrl(`/plugins/${plugin.org}/${plugin.name}`, params)
} }
export const getMarketplacePluginsByCollectionId = async (collectionId: string, query?: CollectionsAndPluginsSearchParams) => { 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 { useModalContext } from '@/context/modal-context'
import { useProviderContext } from '@/context/provider-context' import { useProviderContext } from '@/context/provider-context'
import { useInvalidateAllToolProviders } from '@/service/use-tools' 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 cn from '@/utils/classnames'
import { getMarketplaceUrl } from '@/utils/var'
const i18nPrefix = 'plugin.action' const i18nPrefix = 'plugin.action'
@@ -87,7 +88,7 @@ const DetailHeader = ({
if (isFromGitHub) if (isFromGitHub)
return `https://github.com/${meta!.repo}` return `https://github.com/${meta!.repo}`
if (isFromMarketplace) if (isFromMarketplace)
return `${MARKETPLACE_URL_PREFIX}/plugins/${author}/${name}${theme ? `?theme=${theme}` : ''}` return getMarketplaceUrl(`/plugins/${author}/${name}`, { theme })
return '' return ''
}, [author, isFromGitHub, isFromMarketplace, meta, name, theme]) }, [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 Title from '../card/base/title'
import Action from './action' import Action from './action'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { API_PREFIX, MARKETPLACE_URL_PREFIX } from '@/config' import { API_PREFIX } from '@/config'
import { useSingleCategories } from '../hooks' import { useSingleCategories } from '../hooks'
import { useRenderI18nObject } from '@/hooks/use-i18n' import { useRenderI18nObject } from '@/hooks/use-i18n'
import useRefreshPluginList from '@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list' import useRefreshPluginList from '@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list'
import { useAppContext } from '@/context/app-context' import { useAppContext } from '@/context/app-context'
import { gte } from 'semver' import { gte } from 'semver'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
import { getMarketplaceUrl } from '@/utils/var'
type Props = { type Props = {
className?: string className?: string
@@ -166,7 +167,7 @@ const PluginItem: FC<Props> = ({
} }
{source === PluginSource.marketplace {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> <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' /> <RiArrowRightUpLine className='h-3 w-3 text-text-tertiary' />
</a> </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 { MagicBox } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
import { FileZip } from '@/app/components/base/icons/src/vender/solid/files' import { FileZip } from '@/app/components/base/icons/src/vender/solid/files'
import { Github } from '@/app/components/base/icons/src/vender/solid/general' 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 { useGlobalPublicStore } from '@/context/global-public-context'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
type InstallMethod = {
icon: React.FC<{ className?: string }>
text: string
action: string
}
const Empty = () => { const Empty = () => {
const { t } = useTranslation() const { t } = useTranslation()
const fileInputRef = useRef<HTMLInputElement>(null) const fileInputRef = useRef<HTMLInputElement>(null)
const [selectedAction, setSelectedAction] = useState<string | null>(null) const [selectedAction, setSelectedAction] = useState<string | null>(null)
const [selectedFile, setSelectedFile] = useState<File | 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 setActiveTab = usePluginPageContext(v => v.setActiveTab)
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => { const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
@@ -39,6 +46,22 @@ const Empty = () => {
return t('plugin.list.notFound') return t('plugin.list.notFound')
}, [pluginList?.plugins.length, t, filters.categories.length, filters.tags.length, filters.searchQuery]) }, [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 ( return (
<div className='relative z-0 w-full grow'> <div className='relative z-0 w-full grow'>
{/* skeleton */} {/* skeleton */}
@@ -71,15 +94,7 @@ const Empty = () => {
accept={SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS} accept={SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS}
/> />
<div className='flex w-full flex-col gap-y-1'> <div className='flex w-full flex-col gap-y-1'>
{[ {installMethods.map(({ icon: Icon, text, action }) => (
...(
(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 }) => (
<Button <Button
key={action} key={action}
className='justify-start gap-x-0.5 px-3' className='justify-start gap-x-0.5 px-3'

View File

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

View File

@@ -1,6 +1,6 @@
'use client' 'use client'
import { useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { RiAddLine, RiArrowDownSLine } from '@remixicon/react' import { RiAddLine, RiArrowDownSLine } from '@remixicon/react'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import { MagicBox } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices' import { MagicBox } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
@@ -22,6 +22,13 @@ import { useGlobalPublicStore } from '@/context/global-public-context'
type Props = { type Props = {
onSwitchToMarketplaceTab: () => void onSwitchToMarketplaceTab: () => void
} }
type InstallMethod = {
icon: React.FC<{ className?: string }>
text: string
action: string
}
const InstallPluginDropdown = ({ const InstallPluginDropdown = ({
onSwitchToMarketplaceTab, onSwitchToMarketplaceTab,
}: Props) => { }: Props) => {
@@ -30,7 +37,7 @@ const InstallPluginDropdown = ({
const [isMenuOpen, setIsMenuOpen] = useState(false) const [isMenuOpen, setIsMenuOpen] = useState(false)
const [selectedAction, setSelectedAction] = useState<string | null>(null) const [selectedAction, setSelectedAction] = useState<string | null>(null)
const [selectedFile, setSelectedFile] = useState<File | 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 handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0] const file = event.target.files?.[0]
@@ -54,6 +61,22 @@ const InstallPluginDropdown = ({
// console.log(res) // 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 ( return (
<PortalToFollowElem <PortalToFollowElem
open={isMenuOpen} open={isMenuOpen}
@@ -84,15 +107,7 @@ const InstallPluginDropdown = ({
accept={SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS} accept={SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS}
/> />
<div className='w-full'> <div className='w-full'>
{[ {installMethods.map(({ icon: Icon, text, action }) => (
...(
(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 }) => (
<div <div
key={action} key={action}
className='flex w-full !cursor-pointer items-center gap-1 rounded-lg px-2 py-1.5 hover:bg-state-base-hover' 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 introduction: string
verified: boolean verified: boolean
install_count: number install_count: number
badges: string[] badges: string[],
verification: {
authorized_category: 'langgenius' | 'partner' | 'community'
},
from: Dependency['type']
} }
export type PluginDetail = { export type PluginDetail = {
@@ -145,7 +149,11 @@ export type Plugin = {
settings: CredentialFormSchemaBase[] settings: CredentialFormSchemaBase[]
} }
tags: { name: string }[] tags: { name: string }[]
badges: string[] badges: string[],
verification: {
authorized_category: 'langgenius' | 'partner' | 'community'
},
from: Dependency['type']
} }
export enum PermissionType { export enum PermissionType {

View File

@@ -12,7 +12,7 @@ import { useMarketplace } from './hooks'
import List from '@/app/components/plugins/marketplace/list' import List from '@/app/components/plugins/marketplace/list'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
import { getLocaleOnClient } from '@/i18n' import { getLocaleOnClient } from '@/i18n'
import { MARKETPLACE_URL_PREFIX } from '@/config' import { getMarketplaceUrl } from '@/utils/var'
type MarketplaceProps = { type MarketplaceProps = {
searchPluginText: string searchPluginText: string
@@ -84,7 +84,7 @@ const Marketplace = ({
</span> </span>
{t('common.operation.in')} {t('common.operation.in')}
<a <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' className='system-sm-medium ml-1 flex items-center text-text-accent'
target='_blank' target='_blank'
> >

View File

@@ -12,9 +12,9 @@ import {
PortalToFollowElemTrigger, PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem' } from '@/app/components/base/portal-to-follow-elem'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { MARKETPLACE_URL_PREFIX } from '@/config'
import { useDownloadPlugin } from '@/service/use-plugins' import { useDownloadPlugin } from '@/service/use-plugins'
import { downloadFile } from '@/utils/format' import { downloadFile } from '@/utils/format'
import { getMarketplaceUrl } from '@/utils/var'
type Props = { type Props = {
open: boolean open: boolean
@@ -80,7 +80,7 @@ const OperationDropdown: FC<Props> = ({
<PortalToFollowElemContent className='z-[9999]'> <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 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> <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> </div>
</PortalToFollowElemContent> </PortalToFollowElemContent>
</PortalToFollowElem> </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', skip: 'Skip',
format: 'Format', format: 'Format',
more: 'More', more: 'More',
selectAll: 'Select All',
deSelectAll: 'Deselect All',
}, },
errorMsg: { errorMsg: {
fieldRequired: '{{field}} is required', fieldRequired: '{{field}} is required',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,12 +13,23 @@ export enum LicenseStatus {
LOST = 'lost', LOST = 'lost',
} }
export enum InstallationScope {
ALL = 'all',
NONE = 'none',
OFFICIAL_ONLY = 'official_only',
OFFICIAL_AND_PARTNER = 'official_and_specific_partners',
}
type License = { type License = {
status: LicenseStatus status: LicenseStatus
expired_at: string | null expired_at: string | null
} }
export type SystemFeatures = { export type SystemFeatures = {
plugin_installation_permission: {
plugin_installation_scope: InstallationScope,
restrict_to_marketplace_only: boolean
},
sso_enforced_for_signin: boolean sso_enforced_for_signin: boolean
sso_enforced_for_signin_protocol: SSOProtocol | '' sso_enforced_for_signin_protocol: SSOProtocol | ''
sso_enforced_for_web: boolean sso_enforced_for_web: boolean
@@ -50,6 +61,10 @@ export type SystemFeatures = {
} }
export const defaultSystemFeatures: 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: false,
sso_enforced_for_signin_protocol: '', sso_enforced_for_signin_protocol: '',
sso_enforced_for_web: false, 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 { import {
CONTEXT_PLACEHOLDER_TEXT, CONTEXT_PLACEHOLDER_TEXT,
HISTORY_PLACEHOLDER_TEXT, HISTORY_PLACEHOLDER_TEXT,
@@ -108,3 +108,15 @@ export const getVars = (value: string) => {
// Set the value of basePath // Set the value of basePath
// example: /dify // example: /dify
export const basePath = '' 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()}`
}