Merge branch 'feat/plugins' of github.com:langgenius/dify into feat/plugins

This commit is contained in:
Yi
2024-11-11 17:21:37 +08:00
42 changed files with 894 additions and 669 deletions

View File

@ -8,7 +8,7 @@ import Button from '@/app/components/base/button'
import { Trans, useTranslation } from 'react-i18next'
import { RiLoader2Line } from '@remixicon/react'
import Badge, { BadgeState } from '@/app/components/base/badge/index'
import { installPackageFromLocal } from '@/service/plugins'
import { useInstallPackageFromLocal } from '@/service/use-plugins'
import checkTaskStatus from '../../base/check-task-status'
import { usePluginTasksStore } from '@/app/components/plugins/plugin-page/store'
@ -33,6 +33,8 @@ const Installed: FC<Props> = ({
}) => {
const { t } = useTranslation()
const [isInstalling, setIsInstalling] = React.useState(false)
const { mutateAsync: installPackageFromLocal } = useInstallPackageFromLocal()
const {
check,
stop,

View File

@ -9,7 +9,7 @@ import Button from '@/app/components/base/button'
import { useTranslation } from 'react-i18next'
import { RiLoader2Line } from '@remixicon/react'
import Badge, { BadgeState } from '@/app/components/base/badge/index'
import { installPackageFromMarketPlace } from '@/service/plugins'
import { useInstallPackageFromMarketPlace } from '@/service/use-plugins'
import checkTaskStatus from '../../base/check-task-status'
const i18nPrefix = 'plugin.installModal'
@ -32,6 +32,7 @@ const Installed: FC<Props> = ({
onFailed,
}) => {
const { t } = useTranslation()
const { mutateAsync: installPackageFromMarketPlace } = useInstallPackageFromMarketPlace()
const [isInstalling, setIsInstalling] = React.useState(false)
const {
check,

View File

@ -34,7 +34,7 @@ export type MarketplaceContextValue = {
activePluginType: string
handleActivePluginTypeChange: (type: string) => void
plugins?: Plugin[]
setPlugins: (plugins: Plugin[]) => void
resetPlugins: () => void
sort: PluginsSort
handleSortChange: (sort: PluginsSort) => void
marketplaceCollectionsFromClient?: MarketplaceCollection[]
@ -53,7 +53,7 @@ export const MarketplaceContext = createContext<MarketplaceContextValue>({
activePluginType: PLUGIN_TYPE_SEARCH_MAP.all,
handleActivePluginTypeChange: () => {},
plugins: undefined,
setPlugins: () => {},
resetPlugins: () => {},
sort: DEFAULT_SORT,
handleSortChange: () => {},
marketplaceCollectionsFromClient: [],
@ -91,7 +91,7 @@ export const MarketplaceContextProvider = ({
} = useMarketplaceCollectionsAndPlugins()
const {
plugins,
setPlugins,
resetPlugins,
queryPlugins,
queryPluginsWithDebounced,
} = useMarketplacePlugins()
@ -104,7 +104,7 @@ export const MarketplaceContextProvider = ({
queryMarketplaceCollectionsAndPlugins({
category: activePluginTypeRef.current === PLUGIN_TYPE_SEARCH_MAP.all ? undefined : activePluginTypeRef.current,
})
setPlugins(undefined)
resetPlugins()
return
}
@ -116,7 +116,7 @@ export const MarketplaceContextProvider = ({
sortBy: sortRef.current.sortBy,
sortOrder: sortRef.current.sortOrder,
})
}, [queryPluginsWithDebounced, queryMarketplaceCollectionsAndPlugins, setPlugins])
}, [queryPluginsWithDebounced, queryMarketplaceCollectionsAndPlugins, resetPlugins])
const handleFilterPluginTagsChange = useCallback((tags: string[]) => {
setFilterPluginTags(tags)
@ -126,7 +126,7 @@ export const MarketplaceContextProvider = ({
queryMarketplaceCollectionsAndPlugins({
category: activePluginTypeRef.current === PLUGIN_TYPE_SEARCH_MAP.all ? undefined : activePluginTypeRef.current,
})
setPlugins(undefined)
resetPlugins()
return
}
@ -138,7 +138,7 @@ export const MarketplaceContextProvider = ({
sortBy: sortRef.current.sortBy,
sortOrder: sortRef.current.sortOrder,
})
}, [queryPlugins, setPlugins, queryMarketplaceCollectionsAndPlugins])
}, [queryPlugins, resetPlugins, queryMarketplaceCollectionsAndPlugins])
const handleActivePluginTypeChange = useCallback((type: string) => {
setActivePluginType(type)
@ -148,7 +148,7 @@ export const MarketplaceContextProvider = ({
queryMarketplaceCollectionsAndPlugins({
category: type === PLUGIN_TYPE_SEARCH_MAP.all ? undefined : type,
})
setPlugins(undefined)
resetPlugins()
return
}
@ -160,7 +160,7 @@ export const MarketplaceContextProvider = ({
sortBy: sortRef.current.sortBy,
sortOrder: sortRef.current.sortOrder,
})
}, [queryPlugins, setPlugins, queryMarketplaceCollectionsAndPlugins])
}, [queryPlugins, resetPlugins, queryMarketplaceCollectionsAndPlugins])
const handleSortChange = useCallback((sort: PluginsSort) => {
setSort(sort)
@ -187,7 +187,7 @@ export const MarketplaceContextProvider = ({
activePluginType,
handleActivePluginTypeChange,
plugins,
setPlugins,
resetPlugins,
sort,
handleSortChange,
marketplaceCollectionsFromClient,

View File

@ -15,10 +15,10 @@ const Description = async ({
return (
<>
<h1 className='shrink-0 mb-2 text-center title-4xl-semi-bold text-text-primary'>
Empower your AI development
{t('marketplace.empower')}
</h1>
<h2 className='shrink-0 flex justify-center items-center text-center body-md-regular text-text-tertiary'>
Discover
{t('marketplace.discover')}
<span className="relative ml-1 body-md-medium text-text-secondary after:content-[''] after:absolute after:left-0 after:bottom-[1.5px] after:w-full after:h-2 after:bg-text-text-selected">
{t('category.models')}
</span>
@ -30,11 +30,11 @@ const Description = async ({
<span className="relative ml-1 mr-1 body-md-medium text-text-secondary after:content-[''] after:absolute after:left-0 after:bottom-[1.5px] after:w-full after:h-2 after:bg-text-text-selected">
{t('category.extensions')}
</span>
and
{t('marketplace.and')}
<span className="relative ml-1 mr-1 body-md-medium text-text-secondary after:content-[''] after:absolute after:left-0 after:bottom-[1.5px] after:w-full after:h-2 after:bg-text-text-selected">
{t('category.bundles')}
</span>
in Dify Marketplace
{t('marketplace.inDifyMarketplace')}
</h2>
</>
)

View File

@ -4,7 +4,9 @@ import {
} from 'react'
import { useTranslation } from 'react-i18next'
import { useDebounceFn } from 'ahooks'
import type { Plugin } from '../types'
import type {
Plugin,
} from '../types'
import type {
CollectionsAndPluginsSearchParams,
MarketplaceCollection,
@ -12,9 +14,9 @@ import type {
} from './types'
import {
getMarketplaceCollectionsAndPlugins,
getMarketplacePlugins,
} from './utils'
import i18n from '@/i18n/i18next-config'
import { useMutationPluginsFromMarketplace } from '@/service/use-plugins'
export const useMarketplaceCollectionsAndPlugins = () => {
const [isLoading, setIsLoading] = useState(false)
@ -41,28 +43,29 @@ export const useMarketplaceCollectionsAndPlugins = () => {
}
export const useMarketplacePlugins = () => {
const [isLoading, setIsLoading] = useState(false)
const [plugins, setPlugins] = useState<Plugin[]>()
const {
data,
mutate,
reset,
isPending,
} = useMutationPluginsFromMarketplace()
const queryPlugins = useCallback(async (query: PluginsSearchParams) => {
setIsLoading(true)
const { marketplacePlugins } = await getMarketplacePlugins(query)
setIsLoading(false)
const queryPlugins = useCallback((pluginsSearchParams: PluginsSearchParams) => {
mutate(pluginsSearchParams)
}, [mutate])
setPlugins(marketplacePlugins)
}, [])
const { run: queryPluginsWithDebounced } = useDebounceFn(queryPlugins, {
const { run: queryPluginsWithDebounced } = useDebounceFn((pluginsSearchParams) => {
mutate(pluginsSearchParams)
}, {
wait: 500,
})
return {
plugins,
setPlugins,
plugins: data?.data?.plugins,
resetPlugins: reset,
queryPlugins,
queryPluginsWithDebounced,
isLoading,
setIsLoading,
isLoading: isPending,
}
}

View File

@ -5,6 +5,7 @@ import SearchBoxWrapper from './search-box/search-box-wrapper'
import PluginTypeSwitch from './plugin-type-switch'
import ListWrapper from './list/list-wrapper'
import { getMarketplaceCollectionsAndPlugins } from './utils'
import { TanstackQueryIniter } from '@/context/query-client'
type MarketplaceProps = {
locale?: string
@ -17,18 +18,20 @@ const Marketplace = async ({
const { marketplaceCollections, marketplaceCollectionPluginsMap } = await getMarketplaceCollectionsAndPlugins()
return (
<MarketplaceContextProvider>
<Description locale={locale} />
<IntersectionLine />
<SearchBoxWrapper locale={locale} />
<PluginTypeSwitch locale={locale} />
<ListWrapper
locale={locale}
marketplaceCollections={marketplaceCollections}
marketplaceCollectionPluginsMap={marketplaceCollectionPluginsMap}
showInstallButton={showInstallButton}
/>
</MarketplaceContextProvider>
<TanstackQueryIniter>
<MarketplaceContextProvider>
<Description locale={locale} />
<IntersectionLine />
<SearchBoxWrapper locale={locale} />
<PluginTypeSwitch locale={locale} />
<ListWrapper
locale={locale}
marketplaceCollections={marketplaceCollections}
marketplaceCollectionPluginsMap={marketplaceCollectionPluginsMap}
showInstallButton={showInstallButton}
/>
</MarketplaceContextProvider>
</TanstackQueryIniter>
)
}

View File

@ -30,7 +30,7 @@ const PluginTypeSwitch = ({
const options = [
{
value: PLUGIN_TYPE_SEARCH_MAP.all,
text: 'All',
text: t('plugin.category.all'),
icon: null,
},
{

View File

@ -34,7 +34,7 @@ const PluginSettingModal: FC<Props> = ({
const handleSave = useCallback(async () => {
await onSave(tempPrivilege)
onHide()
}, [tempPrivilege])
}, [onHide, onSave, tempPrivilege])
return (
<Modal

View File

@ -1,5 +1,4 @@
import React, { useState } from 'react'
import useSWR from 'swr'
import { useTranslation } from 'react-i18next'
import { usePluginPageContext } from '@/app/components/plugins/plugin-page/context'
import { useAppContext } from '@/context/app-context'
@ -9,28 +8,39 @@ import Indicator from '@/app/components/header/indicator'
import ToolItem from '@/app/components/tools/provider/tool-item'
import ConfigCredential from '@/app/components/tools/setting/build-in/config-credentials'
import {
fetchBuiltInToolList,
fetchCollectionDetail,
removeBuiltInToolCredential,
updateBuiltInToolCredential,
} from '@/service/tools'
useBuiltinProviderInfo,
useBuiltinTools,
useInvalidateBuiltinProviderInfo,
useRemoveProviderCredentials,
useUpdateProviderCredentials,
} from '@/service/use-tools'
const ActionList = () => {
const { t } = useTranslation()
const { isCurrentWorkspaceManager } = useAppContext()
const currentPluginDetail = usePluginPageContext(v => v.currentPluginDetail)
const { data: provider } = useSWR(
`builtin/${currentPluginDetail.plugin_id}/${currentPluginDetail.name}`,
fetchCollectionDetail,
)
const { data } = useSWR(
`${currentPluginDetail.plugin_id}/${currentPluginDetail.name}`,
fetchBuiltInToolList,
)
const { data: provider } = useBuiltinProviderInfo(`${currentPluginDetail.plugin_id}/${currentPluginDetail.name}`)
const invalidateProviderInfo = useInvalidateBuiltinProviderInfo()
const { data } = useBuiltinTools(`${currentPluginDetail.plugin_id}/${currentPluginDetail.name}`)
const [showSettingAuth, setShowSettingAuth] = useState(false)
const handleCredentialSettingUpdate = () => {}
const handleCredentialSettingUpdate = () => {
invalidateProviderInfo(`${currentPluginDetail.plugin_id}/${currentPluginDetail.name}`)
Toast.notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
setShowSettingAuth(false)
}
const { mutate: updatePermission } = useUpdateProviderCredentials({
onSuccess: handleCredentialSettingUpdate,
})
const { mutate: removePermission } = useRemoveProviderCredentials({
onSuccess: handleCredentialSettingUpdate,
})
if (!data || !provider)
return null
@ -77,24 +87,11 @@ const ActionList = () => {
<ConfigCredential
collection={provider}
onCancel={() => setShowSettingAuth(false)}
onSaved={async (value) => {
await updateBuiltInToolCredential(provider.name, value)
Toast.notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
handleCredentialSettingUpdate()
setShowSettingAuth(false)
}}
onRemove={async () => {
await removeBuiltInToolCredential(provider.name)
Toast.notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
handleCredentialSettingUpdate()
setShowSettingAuth(false)
}}
onSaved={async value => updatePermission({
providerName: provider.name,
credentials: value,
})}
onRemove={async () => removePermission(provider.name)}
/>
)}
</div>

View File

@ -13,6 +13,8 @@ import Description from '../card/base/description'
import Icon from '../card/base/card-icon'
import Title from '../card/base/title'
import OrgInfo from '../card/base/org-info'
import { useGitHubReleases } from '../install-plugin/hooks'
import { compareVersion, getLatestVersion } from '@/utils/semver'
import OperationDropdown from './operation-dropdown'
import PluginInfo from '@/app/components/plugins/plugin-page/plugin-info'
import ActionButton from '@/app/components/base/action-button'
@ -20,10 +22,13 @@ import Button from '@/app/components/base/button'
import Badge from '@/app/components/base/badge'
import Confirm from '@/app/components/base/confirm'
import Tooltip from '@/app/components/base/tooltip'
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 { useModalContext } from '@/context/modal-context'
import { API_PREFIX, MARKETPLACE_URL_PREFIX } from '@/config'
import cn from '@/utils/classnames'
@ -32,16 +37,18 @@ const i18nPrefix = 'plugin.action'
type Props = {
detail: PluginDetail
onHide: () => void
onDelete: () => void
onUpdate: () => void
}
const DetailHeader = ({
detail,
onHide,
onDelete,
onUpdate,
}: Props) => {
const { t } = useTranslation()
const locale = useGetLanguage()
const { fetchReleases } = useGitHubReleases()
const { setShowUpdatePluginModal } = useModalContext()
const {
installation_id,
@ -53,13 +60,51 @@ const DetailHeader = ({
} = detail
const { author, name, label, description, icon, verified } = detail.declaration
const isFromGitHub = source === PluginSource.github
// Only plugin installed from GitHub need to check if it's the new version
const hasNewVersion = useMemo(() => {
return source === PluginSource.github && latest_version !== version
}, [source, latest_version, version])
// #plugin TODO# update plugin
const handleUpdate = () => { }
const handleUpdate = async () => {
try {
const fetchedReleases = await fetchReleases(author, name)
if (fetchedReleases.length === 0)
return
const versions = fetchedReleases.map(release => release.tag_name)
const latestVersion = getLatestVersion(versions)
if (compareVersion(latestVersion, version) === 1) {
setShowUpdatePluginModal({
onSaveCallback: () => {
onUpdate()
},
payload: {
type: PluginSource.github,
github: {
originalPackageInfo: {
id: installation_id,
repo: meta!.repo,
version: meta!.version,
package: meta!.package,
releases: fetchedReleases,
},
},
},
})
}
else {
Toast.notify({
type: 'info',
message: 'No new version available',
})
}
}
catch {
Toast.notify({
type: 'error',
message: 'Failed to compare versions',
})
}
}
const [isShowPluginInfo, {
setTrue: showPluginInfo,
@ -82,9 +127,9 @@ const DetailHeader = ({
hideDeleting()
if (res.success) {
hideDeleteConfirm()
onDelete()
onUpdate()
}
}, [hideDeleteConfirm, hideDeleting, installation_id, showDeleting, onDelete])
}, [hideDeleteConfirm, hideDeleting, installation_id, showDeleting, onUpdate])
// #plugin TODO# used in apps
// const usedInApps = 3
@ -141,6 +186,7 @@ const DetailHeader = ({
</div>
<div className='flex gap-1'>
<OperationDropdown
source={detail.source}
onInfo={showPluginInfo}
onCheckVersion={handleUpdate}
onRemove={showDeleteConfirm}

View File

@ -4,62 +4,62 @@ import { useBoolean } from 'ahooks'
import { RiDeleteBinLine, RiEditLine, RiLoginCircleLine } from '@remixicon/react'
import type { EndpointListItem } from '../types'
import EndpointModal from './endpoint-modal'
import { NAME_FIELD } from './utils'
import { addDefaultValue, toolCredentialToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
import ActionButton from '@/app/components/base/action-button'
import CopyBtn from '@/app/components/base/copy-btn'
import Confirm from '@/app/components/base/confirm'
import Indicator from '@/app/components/header/indicator'
import Switch from '@/app/components/base/switch'
import Toast from '@/app/components/base/toast'
import {
deleteEndpoint,
disableEndpoint,
enableEndpoint,
updateEndpoint,
} from '@/service/plugins'
useDeleteEndpoint,
useDisableEndpoint,
useEnableEndpoint,
useUpdateEndpoint,
} from '@/service/use-endpoints'
type Props = {
data: EndpointListItem
handleChange: () => void
}
const EndpointCard = ({
data,
handleChange,
}: Props) => {
const { t } = useTranslation()
const [active, setActive] = useState(data.enabled)
const endpointID = data.id
// switch
const [isShowDisableConfirm, {
setTrue: showDisableConfirm,
setFalse: hideDisableConfirm,
}] = useBoolean(false)
const activeEndpoint = async () => {
try {
await enableEndpoint({
url: '/workspaces/current/endpoints/enable',
endpointID,
})
}
catch (error) {
console.error(error)
const { mutate: enableEndpoint } = useEnableEndpoint({
onSuccess: async () => {
await handleChange()
},
onError: () => {
Toast.notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
setActive(false)
}
}
const inactiveEndpoint = async () => {
try {
await disableEndpoint({
url: '/workspaces/current/endpoints/disable',
endpointID,
})
}
catch (error) {
console.error(error)
setActive(true)
}
}
},
})
const { mutate: disableEndpoint } = useDisableEndpoint({
onSuccess: async () => {
await handleChange()
hideDisableConfirm()
},
onError: () => {
Toast.notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
setActive(false)
},
})
const handleSwitch = (state: boolean) => {
if (state) {
setActive(true)
activeEndpoint()
enableEndpoint(endpointID)
}
else {
setActive(false)
@ -67,49 +67,49 @@ const EndpointCard = ({
}
}
// delete
const [isShowDeleteConfirm, {
setTrue: showDeleteConfirm,
setFalse: hideDeleteConfirm,
}] = useBoolean(false)
const handleDelete = async () => {
try {
await deleteEndpoint({
url: '/workspaces/current/endpoints/delete',
endpointID,
})
}
catch (error) {
console.error(error)
}
}
const { mutate: deleteEndpoint } = useDeleteEndpoint({
onSuccess: async () => {
await handleChange()
hideDeleteConfirm()
},
onError: () => {
Toast.notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
},
})
// update
const [isShowEndpointModal, {
setTrue: showEndpointModalConfirm,
setFalse: hideEndpointModalConfirm,
}] = useBoolean(false)
const formSchemas = useMemo(() => {
return toolCredentialToFormSchemas(data.declaration.settings)
return toolCredentialToFormSchemas([NAME_FIELD, ...data.declaration.settings])
}, [data.declaration.settings])
const formValue = useMemo(() => {
return addDefaultValue(data.settings, formSchemas)
}, [data.settings, formSchemas])
const handleUpdate = (state: any) => {
try {
updateEndpoint({
url: '/workspaces/current/endpoints',
body: {
endpoint_id: data.id,
settings: state,
name: state.name,
},
})
const formValue = {
name: data.name,
...data.settings,
}
catch (error) {
console.error(error)
}
}
return addDefaultValue(formValue, formSchemas)
}, [data.name, data.settings, formSchemas])
const { mutate: updateEndpoint } = useUpdateEndpoint({
onSuccess: async () => {
await handleChange()
hideEndpointModalConfirm()
},
onError: () => {
Toast.notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
},
})
const handleUpdate = (state: any) => updateEndpoint({
endpointID,
state,
})
return (
<div className='p-0.5 bg-background-section-burn rounded-xl'>
@ -171,7 +171,7 @@ const EndpointCard = ({
hideDisableConfirm()
setActive(true)
}}
onConfirm={inactiveEndpoint}
onConfirm={() => disableEndpoint(endpointID)}
/>
)}
{isShowDeleteConfirm && (
@ -180,7 +180,7 @@ const EndpointCard = ({
title={t('plugin.detailPanel.endpointDeleteTip')}
content={<div>{t('plugin.detailPanel.endpointDeleteContent', { name: data.name })}</div>}
onCancel={hideDeleteConfirm}
onConfirm={handleDelete}
onConfirm={() => deleteEndpoint(endpointID)}
/>
)}
{isShowEndpointModal && (

View File

@ -1,65 +1,62 @@
import React, { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import useSWR from 'swr'
import { useBoolean } from 'ahooks'
import { RiAddLine } from '@remixicon/react'
import EndpointModal from './endpoint-modal'
import EndpointCard from './endpoint-card'
import { NAME_FIELD } from './utils'
import { toolCredentialToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
import ActionButton from '@/app/components/base/action-button'
import Tooltip from '@/app/components/base/tooltip'
import Toast from '@/app/components/base/toast'
import { usePluginPageContext } from '@/app/components/plugins/plugin-page/context'
import {
createEndpoint,
fetchEndpointList,
} from '@/service/plugins'
useCreateEndpoint,
useEndpointList,
useInvalidateEndpointList,
} from '@/service/use-endpoints'
import cn from '@/utils/classnames'
const EndpointList = () => {
type Props = {
showTopBorder?: boolean
}
const EndpointList = ({ showTopBorder }: Props) => {
const { t } = useTranslation()
const pluginDetail = usePluginPageContext(v => v.currentPluginDetail)
const pluginUniqueID = pluginDetail.plugin_unique_identifier
const declaration = pluginDetail.declaration.endpoint
const { data } = useSWR(
{
url: '/workspaces/current/endpoints/list/plugin',
params: {
plugin_id: pluginDetail.plugin_id,
page: 1,
page_size: 100,
},
},
fetchEndpointList,
)
const { data } = useEndpointList(pluginDetail.plugin_id)
const invalidateEndpointList = useInvalidateEndpointList()
const [isShowEndpointModal, {
setTrue: showEndpointModal,
setFalse: hideEndpointModal,
}] = useBoolean(false)
const formSchemas = useMemo(() => {
return toolCredentialToFormSchemas(declaration.settings)
return toolCredentialToFormSchemas([NAME_FIELD, ...declaration.settings])
}, [declaration.settings])
const handleCreate = (state: any) => {
try {
createEndpoint({
url: '/workspaces/current/endpoints',
body: {
plugin_unique_identifier: pluginUniqueID,
settings: state,
name: state.name,
},
})
}
catch (error) {
console.error(error)
}
}
const { mutate: createEndpoint } = useCreateEndpoint({
onSuccess: async () => {
await invalidateEndpointList(pluginDetail.plugin_id)
hideEndpointModal()
},
onError: () => {
Toast.notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
},
})
const handleCreate = (state: any) => createEndpoint({
pluginUniqueID,
state,
})
if (!data)
return null
return (
<div className='px-4 py-2 border-t border-divider-subtle'>
<div className={cn('px-4 py-2 border-divider-subtle', showTopBorder && 'border-t')}>
<div className='mb-1 h-6 flex items-center justify-between text-text-secondary system-sm-semibold-uppercase'>
<div className='flex items-center gap-0.5'>
{t('plugin.detailPanel.endpoints')}
@ -81,6 +78,7 @@ const EndpointList = () => {
<EndpointCard
key={index}
data={item}
handleChange={() => invalidateEndpointList(pluginDetail.plugin_id)}
/>
))}
</div>

View File

@ -10,11 +10,11 @@ import { usePluginPageContext } from '@/app/components/plugins/plugin-page/conte
import cn from '@/utils/classnames'
type Props = {
onDelete: () => void
onUpdate: () => void
}
const PluginDetailPanel: FC<Props> = ({
onDelete,
onUpdate,
}) => {
const pluginDetail = usePluginPageContext(v => v.currentPluginDetail)
const setCurrentPluginDetail = usePluginPageContext(v => v.setCurrentPluginDetail)
@ -39,11 +39,11 @@ const PluginDetailPanel: FC<Props> = ({
<DetailHeader
detail={pluginDetail}
onHide={handleHide}
onDelete={onDelete}
onUpdate={onUpdate}
/>
<div className='grow overflow-y-auto'>
{!!pluginDetail.declaration.endpoint && <EndpointList />}
{!!pluginDetail.declaration.tool && <ActionList />}
{!!pluginDetail.declaration.endpoint && <EndpointList showTopBorder={!!pluginDetail.declaration.tool} />}
{!!pluginDetail.declaration.model && <ModelList />}
</div>
</>

View File

@ -1,105 +0,0 @@
import { PluginSource, PluginType } from '../types'
export const toolNotion = {
id: 'dlfajkgjdga-dfjalksjfglkds-dfjakld',
created_at: '2024-10-16 16:05:33',
updated_at: '2024-10-16 16:05:33',
name: 'notion page search',
plugin_id: 'Notion/notion-page-search',
plugin_unique_identifier: 'Notion/notion-page-search:1.2.0@fldsjflkdsajfldsakajfkls',
declaration: {
version: '1.2.0',
author: 'Notion',
name: 'notion page search',
category: PluginType.tool,
icon: 'https://via.placeholder.com/150',
label: {
'en-US': 'Notion Page Search',
'zh-Hans': 'Notion 页面搜索',
},
description: {
'en-US': 'Description: Search Notion pages and open visited ones faster. No admin access required.More and more info...More and more info...More and more info...',
'zh-Hans': '搜索 Notion 页面并更快地打开已访问的页面。无需管理员访问权限。More and more info...More and more info...More and more info...',
},
created_at: '2024-10-16 16:05:33',
resource: {},
plugins: {},
endpoint: {
settings: [
{
type: 'secret-input',
name: 'api-key',
required: true,
default: null,
options: null,
label: {
en_US: 'API-key',
zh_Hans: 'API-key',
},
help: null,
url: null,
placeholder: {
en_US: 'Please input your API key',
zh_Hans: '请输入你的 API key',
},
},
],
endpoints: [
{ path: '/duck/<app_id>', method: 'GET' },
{ path: '/neko', method: 'GET' },
],
},
tool: null, // TODO
verified: true,
},
installation_id: 'jflkdsjoewingljlsadjgoijg-dkfjldajglkajglask-dlfkajdg',
tenant_id: 'jflkdsjoewingljlsadjgoijg',
endpoints_setups: 2,
endpoints_active: 1,
version: '1.2.0',
source: PluginSource.marketplace,
meta: null,
}
export const toolNotionEndpoints = [
{
id: 'dlfajkgjdga-dfjalksjfglkds-dfjakld',
created_at: '2024-10-16 16:05:33',
updated_at: '2024-10-16 16:05:33',
settings: {
'api-key': '*******',
},
tenant_id: 'jflkdsjoewingljlsadjgoijg',
plugin_id: 'Notion/notion-page-search',
expired_at: '2024-10-16 16:05:33',
declaration: {
settings: [
{
type: 'secret-input',
name: 'api-key',
required: true,
default: null,
options: null,
label: {
en_US: 'API-key',
zh_Hans: 'API-key',
},
help: null,
url: null,
placeholder: {
en_US: 'Please input your API key',
zh_Hans: '请输入你的 API key',
},
},
],
endpoints: [
{ path: '/duck/<app_id>', method: 'GET' },
{ path: '/neko', method: 'GET' },
],
},
name: 'default',
enabled: true,
url: 'http://localhost:5002/e/45rj9V4TRxAjL0I2wXRZgZdXjdHEKBh8',
hook_id: '45rj9V4TRxAjL0I2wXRZgZdXjdHEKBh8',
},
]

View File

@ -1,19 +1,14 @@
import React from 'react'
import useSWR from 'swr'
import { useTranslation } from 'react-i18next'
import { usePluginPageContext } from '@/app/components/plugins/plugin-page/context'
import ModelIcon from '@/app/components/header/account-setting/model-provider-page/model-icon'
import ModelName from '@/app/components/header/account-setting/model-provider-page/model-name'
import { fetchModelProviderModelList } from '@/service/common'
import { useModelProviderModelList } from '@/service/use-models'
const ModelList = () => {
const { t } = useTranslation()
const currentPluginDetail = usePluginPageContext(v => v.currentPluginDetail)
const { data: res } = useSWR(
`/workspaces/current/model-providers/${currentPluginDetail.plugin_id}/${currentPluginDetail.name}/models`,
fetchModelProviderModelList,
)
const { data: res } = useModelProviderModelList(`${currentPluginDetail.plugin_id}/${currentPluginDetail.name}`)
if (!res)
return null
@ -21,7 +16,7 @@ const ModelList = () => {
return (
<div className='px-4 py-2'>
<div className='mb-1 h-6 flex items-center text-text-secondary system-sm-semibold-uppercase'>{t('plugin.detailPanel.modelNum', { num: res.data.length })}</div>
<div className='h-8 flex items-center'>
<div className='flex flex-col'>
{res.data.map(model => (
<div key={model.model} className='h-6 py-1 flex items-center'>
<ModelIcon

View File

@ -2,6 +2,7 @@
import type { FC } from 'react'
import React, { useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { PluginSource } from '../types'
import { RiArrowRightUpLine, RiMoreFill } from '@remixicon/react'
import ActionButton from '@/app/components/base/action-button'
// import Button from '@/app/components/base/button'
@ -13,6 +14,7 @@ import {
import cn from '@/utils/classnames'
type Props = {
source: PluginSource
onInfo: () => void
onCheckVersion: () => void
onRemove: () => void
@ -20,10 +22,11 @@ type Props = {
}
const OperationDropdown: FC<Props> = ({
source,
detailUrl,
onInfo,
onCheckVersion,
onRemove,
detailUrl,
}) => {
const { t } = useTranslation()
const [open, doSetOpen] = useState(false)
@ -56,25 +59,33 @@ const OperationDropdown: FC<Props> = ({
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-50'>
<div className='w-[160px] p-1 bg-components-panel-bg-blur rounded-xl border-[0.5px] border-components-panel-border shadow-lg'>
<div
onClick={() => {
onInfo()
handleTrigger()
}}
className='px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover'
>{t('plugin.detailPanel.operation.info')}</div>
<div
onClick={() => {
onCheckVersion()
handleTrigger()
}}
className='px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover'
>{t('plugin.detailPanel.operation.checkUpdate')}</div>
<a href={detailUrl} target='_blank' className='flex items-center px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover'>
<span className='grow'>{t('plugin.detailPanel.operation.viewDetail')}</span>
<RiArrowRightUpLine className='shrink-0 w-3.5 h-3.5 text-text-tertiary' />
</a>
<div className='my-1 h-px bg-divider-subtle'></div>
{source === PluginSource.github && (
<div
onClick={() => {
onInfo()
handleTrigger()
}}
className='px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover'
>{t('plugin.detailPanel.operation.info')}</div>
)}
{source === PluginSource.github && (
<div
onClick={() => {
onCheckVersion()
handleTrigger()
}}
className='px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover'
>{t('plugin.detailPanel.operation.checkUpdate')}</div>
)}
{source === PluginSource.marketplace && (
<a href={detailUrl} target='_blank' className='flex items-center px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover'>
<span className='grow'>{t('plugin.detailPanel.operation.viewDetail')}</span>
<RiArrowRightUpLine className='shrink-0 w-3.5 h-3.5 text-text-tertiary' />
</a>
)}
{(source === PluginSource.marketplace || source === PluginSource.github) && (
<div className='my-1 h-px bg-divider-subtle'></div>
)}
<div
onClick={() => {
onRemove()

View File

@ -0,0 +1,21 @@
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
export const NAME_FIELD = {
type: FormTypeEnum.textInput,
name: 'name',
label: {
en_US: 'Endpoint Name',
zh_Hans: '端点名称',
ja_JP: 'エンドポイント名',
pt_BR: 'Nome do ponto final',
},
placeholder: {
en_US: 'Endpoint Name',
zh_Hans: '端点名称',
ja_JP: 'エンドポイント名',
pt_BR: 'Nome do ponto final',
},
required: true,
default: '',
help: null,
}

View File

@ -11,16 +11,13 @@ import {
useContextSelector,
} from 'use-context-selector'
import { useSelector as useAppContextSelector } from '@/context/app-context'
import type { Permissions, PluginDetail } from '../types'
import type { PluginDetail } from '../types'
import type { FilterState } from './filter-management'
import { PermissionType } from '../types'
import { useTranslation } from 'react-i18next'
import { useTabSearchParams } from '@/hooks/use-tab-searchparams'
export type PluginPageContextValue = {
containerRef: React.RefObject<HTMLDivElement>
permissions: Permissions
setPermissions: (permissions: PluginPageContextValue['permissions']) => void
currentPluginDetail: PluginDetail | undefined
setCurrentPluginDetail: (plugin: PluginDetail) => void
filters: FilterState
@ -32,21 +29,16 @@ export type PluginPageContextValue = {
export const PluginPageContext = createContext<PluginPageContextValue>({
containerRef: { current: null },
permissions: {
install_permission: PermissionType.noOne,
debug_permission: PermissionType.noOne,
},
setPermissions: () => {},
currentPluginDetail: undefined,
setCurrentPluginDetail: () => {},
setCurrentPluginDetail: () => { },
filters: {
categories: [],
tags: [],
searchQuery: '',
},
setFilters: () => {},
setFilters: () => { },
activeTab: '',
setActiveTab: () => {},
setActiveTab: () => { },
options: [],
})
@ -63,10 +55,6 @@ export const PluginPageContextProvider = ({
}: PluginPageContextProviderProps) => {
const { t } = useTranslation()
const containerRef = useRef<HTMLDivElement>(null)
const [permissions, setPermissions] = useState<PluginPageContextValue['permissions']>({
install_permission: PermissionType.noOne,
debug_permission: PermissionType.noOne,
})
const [filters, setFilters] = useState<FilterState>({
categories: [],
tags: [],
@ -93,8 +81,6 @@ export const PluginPageContextProvider = ({
<PluginPageContext.Provider
value={{
containerRef,
permissions,
setPermissions,
currentPluginDetail,
setCurrentPluginDetail,
filters,

View File

@ -206,7 +206,7 @@ const PluginPage = ({
{showPluginSettingModal && (
<PermissionSetModal
payload={permissions}
payload={permissions!}
onHide={setHidePluginSettingModal}
onSave={setPermissions}
/>

View File

@ -48,7 +48,7 @@ const PluginsPanel = () => {
) : (
<Empty />
)}
<PluginDetailPanel onDelete={() => invalidateInstalledPluginList()}/>
<PluginDetailPanel onUpdate={() => invalidateInstalledPluginList()}/>
</>
)
}

View File

@ -1,15 +1,12 @@
import { useEffect } from 'react'
import type { Permissions } from '../types'
import { PermissionType } from '../types'
import {
usePluginPageContext,
} from './context'
import { useAppContext } from '@/context/app-context'
import { updatePermission as doUpdatePermission, fetchPermission } from '@/service/plugins'
import Toast from '../../base/toast'
import { useTranslation } from 'react-i18next'
import { useInvalidatePermissions, useMutationPermissions, usePermissions } from '@/service/use-plugins'
const hasPermission = (permission: PermissionType, isAdmin: boolean) => {
const hasPermission = (permission: PermissionType | undefined, isAdmin: boolean) => {
if (!permission)
return false
if (permission === PermissionType.noOne)
return false
@ -22,29 +19,26 @@ const hasPermission = (permission: PermissionType, isAdmin: boolean) => {
const usePermission = () => {
const { t } = useTranslation()
const { isCurrentWorkspaceManager, isCurrentWorkspaceOwner } = useAppContext()
const [permissions, setPermissions] = usePluginPageContext(v => [v.permissions, v.setPermissions])
const { data: permissions } = usePermissions()
const invalidatePermissions = useInvalidatePermissions()
const { mutate: updatePermission, isPending: isUpdatePending } = useMutationPermissions({
onSuccess: () => {
invalidatePermissions()
Toast.notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
},
})
const isAdmin = isCurrentWorkspaceManager || isCurrentWorkspaceOwner
const updatePermission = async (permission: Permissions) => {
await doUpdatePermission(permission)
setPermissions(permission)
Toast.notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
}
useEffect(() => {
(async () => {
const permission = await fetchPermission()
setPermissions(permission)
})()
}, [])
return {
canManagement: hasPermission(permissions.install_permission, isAdmin),
canDebugger: hasPermission(permissions.debug_permission, isAdmin),
canManagement: hasPermission(permissions?.install_permission, isAdmin),
canDebugger: hasPermission(permissions?.debug_permission, isAdmin),
canSetPermissions: isAdmin,
permissions,
setPermissions: updatePermission,
isUpdatePending,
}
}

View File

@ -19,11 +19,13 @@ import { useBoolean } from 'ahooks'
type Props = {
className?: string
payload: Plugin
onSuccess: () => void
}
const ProviderCard: FC<Props> = ({
className,
payload,
onSuccess,
}) => {
const { t } = useTranslation()
const [isShowInstallFromMarketplace, {
@ -84,7 +86,10 @@ const ProviderCard: FC<Props> = ({
manifest={payload as any}
uniqueIdentifier={payload.latest_package_identifier}
onClose={hideInstallFromMarketplace}
onSuccess={hideInstallFromMarketplace}
onSuccess={() => {
onSuccess()
hideInstallFromMarketplace()
}}
/>
)
}

View File

@ -194,19 +194,10 @@ export type GitHubUrlInfo = {
}
// endpoint
export type CreateEndpointRequest = {
plugin_unique_identifier: string
settings: Record<string, any>
name: string
}
export type EndpointOperationResponse = {
result: 'success' | 'error'
}
export type EndpointsRequest = {
page_size: number
page: number
plugin_id: string
}
export type EndpointsResponse = {
endpoints: EndpointListItem[]
has_more: boolean
@ -301,3 +292,7 @@ export type InstalledPluginListResponse = {
export type UninstallPluginResponse = {
success: boolean
}
export type PluginsFromMarketplaceResponse = {
plugins: Plugin[]
}