mirror of
https://github.com/langgenius/dify.git
synced 2024-11-16 19:59:50 +08:00
feat: plugin uninstall & plugin list filtering
This commit is contained in:
parent
4adb61d6c7
commit
36ab121b87
|
@ -2,6 +2,7 @@ import type { FC } from 'react'
|
|||
import { useEffect, useState } from 'react'
|
||||
import cn from '@/utils/classnames'
|
||||
import Badge, { BadgeState } from '@/app/components/base/badge/index'
|
||||
import { usePluginPageContext } from '../../plugins/plugin-page/context'
|
||||
type Option = {
|
||||
value: string
|
||||
text: string
|
||||
|
@ -22,6 +23,7 @@ const TabSlider: FC<TabSliderProps> = ({
|
|||
}) => {
|
||||
const [activeIndex, setActiveIndex] = useState(options.findIndex(option => option.value === value))
|
||||
const [sliderStyle, setSliderStyle] = useState({})
|
||||
const pluginList = usePluginPageContext(v => v.installedPluginList)
|
||||
|
||||
const updateSliderStyle = (index: number) => {
|
||||
const tabElement = document.getElementById(`tab-${index}`)
|
||||
|
@ -71,7 +73,7 @@ const TabSlider: FC<TabSliderProps> = ({
|
|||
uppercase={true}
|
||||
state={BadgeState.Default}
|
||||
>
|
||||
6
|
||||
{pluginList.length}
|
||||
</Badge>
|
||||
}
|
||||
</div>
|
||||
|
|
|
@ -9,6 +9,7 @@ import Install from './steps/install'
|
|||
import Installed from '../base/installed'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon'
|
||||
import { usePluginPageContext } from '../../plugin-page/context'
|
||||
|
||||
const i18nPrefix = 'plugin.installModal'
|
||||
|
||||
|
@ -28,6 +29,8 @@ const InstallFromLocalPackage: React.FC<InstallFromLocalPackageProps> = ({
|
|||
const [uniqueIdentifier, setUniqueIdentifier] = useState<string | null>(null)
|
||||
const [manifest, setManifest] = useState<PluginDeclaration | null>(null)
|
||||
const [errorMsg, setErrorMsg] = useState<string | null>(null)
|
||||
const mutateInstalledPluginList = usePluginPageContext(v => v.mutateInstalledPluginList)
|
||||
|
||||
const getTitle = useCallback(() => {
|
||||
if (step === InstallStep.uploadFailed)
|
||||
return t(`${i18nPrefix}.uploadFailed`)
|
||||
|
@ -63,9 +66,10 @@ const InstallFromLocalPackage: React.FC<InstallFromLocalPackageProps> = ({
|
|||
setStep(InstallStep.uploadFailed)
|
||||
}, [])
|
||||
|
||||
const handleInstalled = useCallback(async () => {
|
||||
const handleInstalled = useCallback(() => {
|
||||
mutateInstalledPluginList()
|
||||
setStep(InstallStep.installed)
|
||||
}, [])
|
||||
}, [mutateInstalledPluginList])
|
||||
|
||||
const handleFailed = useCallback((errorMsg?: string) => {
|
||||
setStep(InstallStep.installFailed)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import React, { useCallback } from 'react'
|
||||
import type { MetaData } from '../types'
|
||||
import { RiDeleteBinLine, RiInformation2Line, RiLoopLeftLine } from '@remixicon/react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
@ -9,6 +9,8 @@ import PluginInfo from '../plugin-page/plugin-info'
|
|||
import ActionButton from '../../base/action-button'
|
||||
import Tooltip from '../../base/tooltip'
|
||||
import Confirm from '../../base/confirm'
|
||||
import { uninstallPlugin } from '@/service/plugins'
|
||||
import { usePluginPageContext } from '../plugin-page/context'
|
||||
|
||||
const i18nPrefix = 'plugin.action'
|
||||
|
||||
|
@ -20,22 +22,23 @@ type Props = {
|
|||
isShowInfo: boolean
|
||||
isShowDelete: boolean
|
||||
onDelete: () => void
|
||||
meta: MetaData
|
||||
}
|
||||
|
||||
const Action: FC<Props> = ({
|
||||
pluginId,
|
||||
pluginName,
|
||||
usedInApps,
|
||||
isShowFetchNewVersion,
|
||||
isShowInfo,
|
||||
isShowDelete,
|
||||
onDelete,
|
||||
meta,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const router = useRouter()
|
||||
const [isShowPluginInfo, {
|
||||
setTrue: showPluginInfo,
|
||||
setFalse: hidePluginInfo,
|
||||
}] = useBoolean(false)
|
||||
const mutateInstalledPluginList = usePluginPageContext(v => v.mutateInstalledPluginList)
|
||||
|
||||
const handleFetchNewVersion = () => { }
|
||||
|
||||
|
@ -44,7 +47,14 @@ const Action: FC<Props> = ({
|
|||
setFalse: hideDeleteConfirm,
|
||||
}] = useBoolean(false)
|
||||
|
||||
// const handleDelete = () => { }
|
||||
const handleDelete = useCallback(async () => {
|
||||
const res = await uninstallPlugin(pluginId)
|
||||
if (res.success) {
|
||||
hideDeleteConfirm()
|
||||
mutateInstalledPluginList()
|
||||
onDelete()
|
||||
}
|
||||
}, [pluginId, onDelete])
|
||||
return (
|
||||
<div className='flex space-x-1'>
|
||||
{/* Only plugin installed from GitHub need to check if it's the new version */}
|
||||
|
@ -83,9 +93,9 @@ const Action: FC<Props> = ({
|
|||
|
||||
{isShowPluginInfo && (
|
||||
<PluginInfo
|
||||
repository='https://github.com/langgenius/dify-github-plugin'
|
||||
release='1.2.5'
|
||||
packageName='notion-sync.difypkg'
|
||||
repository={meta.repo}
|
||||
release={meta.version}
|
||||
packageName={meta.package}
|
||||
onHide={hidePluginInfo}
|
||||
/>
|
||||
)}
|
||||
|
@ -97,11 +107,12 @@ const Action: FC<Props> = ({
|
|||
content={
|
||||
<div>
|
||||
{t(`${i18nPrefix}.deleteContentLeft`)}<span className='system-md-semibold'>{pluginName}</span>{t(`${i18nPrefix}.deleteContentRight`)}<br />
|
||||
{usedInApps > 0 && t(`${i18nPrefix}.usedInApps`, { num: usedInApps })}
|
||||
{/* // todo: add usedInApps */}
|
||||
{/* {usedInApps > 0 && t(`${i18nPrefix}.usedInApps`, { num: usedInApps })} */}
|
||||
</div>
|
||||
}
|
||||
onCancel={hideDeleteConfirm}
|
||||
onConfirm={onDelete}
|
||||
onConfirm={handleDelete}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,67 +1,94 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import React, { useMemo } from 'react'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { RiArrowRightUpLine, RiBugLine, RiHardDrive3Line, RiLoginCircleLine, RiVerifiedBadgeLine } from '@remixicon/react'
|
||||
import {
|
||||
RiArrowRightUpLine,
|
||||
RiBugLine,
|
||||
RiHardDrive3Line,
|
||||
RiLoginCircleLine,
|
||||
RiVerifiedBadgeLine,
|
||||
} from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Github } from '../../base/icons/src/public/common'
|
||||
import Badge from '../../base/badge'
|
||||
import type { Plugin } from '../types'
|
||||
import { type InstalledPlugin, PluginSource } from '../types'
|
||||
import CornerMark from '../card/base/corner-mark'
|
||||
import Description from '../card/base/description'
|
||||
import Icon from '../card/base/card-icon'
|
||||
import OrgInfo from '../card/base/org-info'
|
||||
import Title from '../card/base/title'
|
||||
import Action from './action'
|
||||
import cn from '@/utils/classnames'
|
||||
import I18n from '@/context/i18n'
|
||||
import { API_PREFIX } from '@/config'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
payload: Plugin
|
||||
source: 'github' | 'marketplace' | 'local' | 'debug'
|
||||
onDelete: () => void
|
||||
plugin: InstalledPlugin
|
||||
}
|
||||
|
||||
const PluginItem: FC<Props> = ({
|
||||
className,
|
||||
payload,
|
||||
source,
|
||||
onDelete,
|
||||
plugin,
|
||||
}) => {
|
||||
const { locale } = useContext(I18n)
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { type, name, org, label } = payload
|
||||
const hasNewVersion = payload.latest_version !== payload.version
|
||||
const {
|
||||
source,
|
||||
tenant_id,
|
||||
installation_id,
|
||||
endpoints_active,
|
||||
meta,
|
||||
version,
|
||||
latest_version,
|
||||
} = plugin
|
||||
const { category, author, name, label, description, icon, verified } = plugin.declaration
|
||||
// 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])
|
||||
|
||||
const orgName = useMemo(() => {
|
||||
return [PluginSource.github, PluginSource.marketplace].includes(source) ? author : ''
|
||||
}, [source, author])
|
||||
|
||||
const tLocale = useMemo(() => {
|
||||
return locale.replace('-', '_')
|
||||
}, [locale])
|
||||
return (
|
||||
<div className={`p-1 ${source === 'debug'
|
||||
<div className={`p-1 ${source === PluginSource.debugging
|
||||
? 'bg-[repeating-linear-gradient(-45deg,rgba(16,24,40,0.04),rgba(16,24,40,0.04)_5px,rgba(0,0,0,0.02)_5px,rgba(0,0,0,0.02)_10px)]'
|
||||
: 'bg-background-section-burn'}
|
||||
rounded-xl`}
|
||||
>
|
||||
<div className={cn('relative p-4 pb-3 border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg hover-bg-components-panel-on-panel-item-bg rounded-xl shadow-xs', className)}>
|
||||
<CornerMark text={type} />
|
||||
<CornerMark text={category} />
|
||||
{/* Header */}
|
||||
<div className="flex">
|
||||
<Icon src={payload.icon} />
|
||||
<div className='flex items-center justify-center w-10 h-10 overflow-hidden border-components-panel-border-subtle border-[1px] rounded-xl'>
|
||||
<img
|
||||
src={`${API_PREFIX}/workspaces/current/plugin/icon?tenant_id=${tenant_id}&filename=${icon}`}
|
||||
alt={`plugin-${installation_id}-logo`}
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-3 w-0 grow">
|
||||
<div className="flex items-center h-5">
|
||||
<Title title={label[locale]} />
|
||||
<RiVerifiedBadgeLine className="shrink-0 ml-0.5 w-4 h-4 text-text-accent" />
|
||||
<Badge className='ml-1' text={payload.version} hasRedCornerMark={hasNewVersion} />
|
||||
<Title title={label[tLocale]} />
|
||||
{verified && <RiVerifiedBadgeLine className="shrink-0 ml-0.5 w-4 h-4 text-text-accent" />}
|
||||
<Badge className='ml-1' text={plugin.version} hasRedCornerMark={hasNewVersion} />
|
||||
</div>
|
||||
<div className='flex items-center justify-between'>
|
||||
<Description text={payload.brief[locale]} descriptionLineRows={1}></Description>
|
||||
<Description text={description[tLocale]} descriptionLineRows={1}></Description>
|
||||
<Action
|
||||
pluginId='xxx'
|
||||
pluginName={label[locale]}
|
||||
pluginId={installation_id}
|
||||
pluginName={label[tLocale]}
|
||||
usedInApps={5}
|
||||
isShowFetchNewVersion={hasNewVersion}
|
||||
isShowInfo
|
||||
isShowInfo={source === PluginSource.github}
|
||||
isShowDelete
|
||||
onDelete={onDelete}
|
||||
meta={meta}
|
||||
onDelete={() => {}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -71,19 +98,19 @@ const PluginItem: FC<Props> = ({
|
|||
<div className='flex items-center'>
|
||||
<OrgInfo
|
||||
className="mt-0.5"
|
||||
orgName={org}
|
||||
orgName={orgName}
|
||||
packageName={name}
|
||||
packageNameClassName='w-auto max-w-[150px]'
|
||||
/>
|
||||
<div className='mx-2 text-text-quaternary system-xs-regular'>·</div>
|
||||
<div className='flex text-text-tertiary system-xs-regular space-x-1'>
|
||||
<RiLoginCircleLine className='w-4 h-4' />
|
||||
<span>{t('plugin.endpointsEnabled', { num: 2 })}</span>
|
||||
<span>{t('plugin.endpointsEnabled', { num: endpoints_active })}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center'>
|
||||
{source === 'github'
|
||||
{source === PluginSource.github
|
||||
&& <>
|
||||
<a href='' target='_blank' className='flex items-center gap-1'>
|
||||
<div className='text-text-tertiary system-2xs-medium-uppercase'>{t('plugin.from')}</div>
|
||||
|
@ -95,7 +122,7 @@ const PluginItem: FC<Props> = ({
|
|||
</a>
|
||||
</>
|
||||
}
|
||||
{source === 'marketplace'
|
||||
{source === PluginSource.marketplace
|
||||
&& <>
|
||||
<a href='' target='_blank' className='flex items-center gap-0.5'>
|
||||
<div className='text-text-tertiary system-2xs-medium-uppercase'>{t('plugin.from')} <span className='text-text-secondary'>marketplace</span></div>
|
||||
|
@ -103,7 +130,7 @@ const PluginItem: FC<Props> = ({
|
|||
</a>
|
||||
</>
|
||||
}
|
||||
{source === 'local'
|
||||
{source === PluginSource.local
|
||||
&& <>
|
||||
<div className='flex items-center gap-1'>
|
||||
<RiHardDrive3Line className='text-text-tertiary w-3 h-3' />
|
||||
|
@ -111,7 +138,7 @@ const PluginItem: FC<Props> = ({
|
|||
</div>
|
||||
</>
|
||||
}
|
||||
{source === 'debug'
|
||||
{source === PluginSource.debugging
|
||||
&& <>
|
||||
<div className='flex items-center gap-1'>
|
||||
<RiBugLine className='w-3 h-3 text-text-warning' />
|
||||
|
|
|
@ -9,14 +9,20 @@ import {
|
|||
createContext,
|
||||
useContextSelector,
|
||||
} from 'use-context-selector'
|
||||
import type { Permissions } from '../types'
|
||||
import type { InstalledPlugin, Permissions } from '../types'
|
||||
import type { FilterState } from './filter-management'
|
||||
import { PermissionType } from '../types'
|
||||
import { fetchInstalledPluginList } from '@/service/plugins'
|
||||
import useSWR from 'swr'
|
||||
|
||||
export type PluginPageContextValue = {
|
||||
containerRef: React.RefObject<HTMLDivElement>
|
||||
permissions: Permissions
|
||||
setPermissions: (permissions: PluginPageContextValue['permissions']) => void
|
||||
|
||||
installedPluginList: InstalledPlugin[]
|
||||
mutateInstalledPluginList: () => void
|
||||
filters: FilterState
|
||||
setFilters: (filter: FilterState) => void
|
||||
}
|
||||
|
||||
export const PluginPageContext = createContext<PluginPageContextValue>({
|
||||
|
@ -26,6 +32,14 @@ export const PluginPageContext = createContext<PluginPageContextValue>({
|
|||
debug_permission: PermissionType.noOne,
|
||||
},
|
||||
setPermissions: () => { },
|
||||
installedPluginList: [],
|
||||
mutateInstalledPluginList: () => {},
|
||||
filters: {
|
||||
categories: [],
|
||||
tags: [],
|
||||
searchQuery: '',
|
||||
},
|
||||
setFilters: () => {},
|
||||
})
|
||||
|
||||
type PluginPageContextProviderProps = {
|
||||
|
@ -44,6 +58,12 @@ export const PluginPageContextProvider = ({
|
|||
install_permission: PermissionType.noOne,
|
||||
debug_permission: PermissionType.noOne,
|
||||
})
|
||||
const [filters, setFilters] = useState<FilterState>({
|
||||
categories: [],
|
||||
tags: [],
|
||||
searchQuery: '',
|
||||
})
|
||||
const { data, mutate: mutateInstalledPluginList } = useSWR({ url: '/workspaces/current/plugin/list' }, fetchInstalledPluginList)
|
||||
|
||||
return (
|
||||
<PluginPageContext.Provider
|
||||
|
@ -51,6 +71,10 @@ export const PluginPageContextProvider = ({
|
|||
containerRef,
|
||||
permissions,
|
||||
setPermissions,
|
||||
installedPluginList: data?.plugins || [],
|
||||
mutateInstalledPluginList,
|
||||
filters,
|
||||
setFilters,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
|
|
@ -1,22 +1,21 @@
|
|||
import type { FC } from 'react'
|
||||
import PluginItem from '../../plugin-item'
|
||||
import { customTool, extensionDallE, modelGPT4, toolNotion } from '@/app/components/plugins/card/card-mock'
|
||||
import type { InstalledPlugin } from '../../types'
|
||||
|
||||
const PluginList = () => {
|
||||
const pluginList = [toolNotion, extensionDallE, modelGPT4, customTool]
|
||||
type IPluginListProps = {
|
||||
pluginList: InstalledPlugin[]
|
||||
}
|
||||
|
||||
const PluginList: FC<IPluginListProps> = ({ pluginList }) => {
|
||||
return (
|
||||
<div className='pb-3 bg-white'>
|
||||
<div>
|
||||
<div className='grid grid-cols-2 gap-3'>
|
||||
{pluginList.map((plugin, index) => (
|
||||
<PluginItem
|
||||
key={index}
|
||||
payload={plugin as any}
|
||||
onDelete={() => {}}
|
||||
source={'debug'}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className='grid grid-cols-2 gap-3'>
|
||||
{pluginList.map(plugin => (
|
||||
<PluginItem
|
||||
key={plugin.plugin_id}
|
||||
plugin={plugin}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -1,16 +1,33 @@
|
|||
'use client'
|
||||
import { useState } from 'react'
|
||||
import type { EndpointListItem, PluginDetail } from '../types'
|
||||
import { useMemo, useState } from 'react'
|
||||
import type { EndpointListItem, InstalledPlugin, PluginDetail } from '../types'
|
||||
import type { FilterState } from './filter-management'
|
||||
import FilterManagement from './filter-management'
|
||||
import List from './list'
|
||||
import PluginDetailPanel from '@/app/components/plugins/plugin-detail-panel'
|
||||
import { toolNotion, toolNotionEndpoints } from '@/app/components/plugins/plugin-detail-panel/mock'
|
||||
import { usePluginPageContext } from './context'
|
||||
import { useDebounceFn } from 'ahooks'
|
||||
|
||||
const PluginsPanel = () => {
|
||||
const handleFilterChange = (filters: FilterState) => {
|
||||
//
|
||||
}
|
||||
const [filters, setFilters] = usePluginPageContext(v => [v.filters, v.setFilters])
|
||||
const pluginList = usePluginPageContext(v => v.installedPluginList) as InstalledPlugin[]
|
||||
|
||||
const { run: handleFilterChange } = useDebounceFn((filters: FilterState) => {
|
||||
setFilters(filters)
|
||||
}, { wait: 500 })
|
||||
|
||||
const filteredList = useMemo(() => {
|
||||
// todo: filter by tags
|
||||
const { categories, searchQuery } = filters
|
||||
const filteredList = pluginList.filter((plugin) => {
|
||||
return (
|
||||
(categories.length === 0 || categories.includes(plugin.declaration.category))
|
||||
&& (searchQuery === '' || plugin.plugin_id.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||
)
|
||||
})
|
||||
return filteredList
|
||||
}, [pluginList, filters])
|
||||
|
||||
const [currentPluginDetail, setCurrentPluginDetail] = useState<PluginDetail | undefined>(toolNotion as any)
|
||||
const [currentPluginEndpoints, setCurrentEndpoints] = useState<EndpointListItem[]>(toolNotionEndpoints as any)
|
||||
|
@ -24,7 +41,7 @@ const PluginsPanel = () => {
|
|||
</div>
|
||||
<div className='flex px-12 items-start content-start gap-2 flex-grow self-stretch flex-wrap'>
|
||||
<div className='w-full'>
|
||||
<List />
|
||||
<List pluginList={filteredList} />
|
||||
</div>
|
||||
</div>
|
||||
<PluginDetailPanel
|
||||
|
|
|
@ -112,7 +112,7 @@ export type Plugin = {
|
|||
// Repo readme.md content
|
||||
introduction: string
|
||||
repository: string
|
||||
category: string
|
||||
category: PluginType
|
||||
install_count: number
|
||||
endpoint: {
|
||||
settings: CredentialFormSchemaBase[]
|
||||
|
@ -235,3 +235,29 @@ export type TaskStatusResponse = {
|
|||
plugins: PluginStatus[]
|
||||
}
|
||||
}
|
||||
|
||||
export type MetaData = {
|
||||
repo: string
|
||||
version: string
|
||||
package: string
|
||||
}
|
||||
|
||||
export type InstalledPlugin = {
|
||||
plugin_id: string
|
||||
installation_id: string
|
||||
declaration: PluginDeclaration
|
||||
source: PluginSource
|
||||
tenant_id: string
|
||||
version: string
|
||||
latest_version: string
|
||||
endpoints_active: number
|
||||
meta: MetaData
|
||||
}
|
||||
|
||||
export type InstalledPluginListResponse = {
|
||||
plugins: InstalledPlugin[]
|
||||
}
|
||||
|
||||
export type UninstallPluginResponse = {
|
||||
success: boolean
|
||||
}
|
||||
|
|
|
@ -6,10 +6,12 @@ import type {
|
|||
EndpointsRequest,
|
||||
EndpointsResponse,
|
||||
InstallPackageResponse,
|
||||
InstalledPluginListResponse,
|
||||
Permissions,
|
||||
PluginDeclaration,
|
||||
PluginManifestInMarket,
|
||||
TaskStatusResponse,
|
||||
UninstallPluginResponse,
|
||||
UpdateEndpointRequest,
|
||||
} from '@/app/components/plugins/types'
|
||||
import type { DebugInfo as DebugInfoTypes } from '@/app/components/plugins/types'
|
||||
|
@ -110,3 +112,11 @@ export const fetchPermission = async () => {
|
|||
export const updatePermission = async (permissions: Permissions) => {
|
||||
return post('/workspaces/current/plugin/permission/change', { body: permissions })
|
||||
}
|
||||
|
||||
export const fetchInstalledPluginList: Fetcher<InstalledPluginListResponse, { url: string }> = ({ url }) => {
|
||||
return get<InstalledPluginListResponse>(url)
|
||||
}
|
||||
|
||||
export const uninstallPlugin = async (pluginId: string) => {
|
||||
return post<UninstallPluginResponse>('/workspaces/current/plugin/uninstall', { body: { plugin_installation_id: pluginId } })
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user