feat: plugin uninstall & plugin list filtering

pull/12372/head
twwu 2 years ago
parent 4adb61d6c7
commit 36ab121b87

@ -2,6 +2,7 @@ import type { FC } from 'react'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import Badge, { BadgeState } from '@/app/components/base/badge/index' import Badge, { BadgeState } from '@/app/components/base/badge/index'
import { usePluginPageContext } from '../../plugins/plugin-page/context'
type Option = { type Option = {
value: string value: string
text: string text: string
@ -22,6 +23,7 @@ const TabSlider: FC<TabSliderProps> = ({
}) => { }) => {
const [activeIndex, setActiveIndex] = useState(options.findIndex(option => option.value === value)) const [activeIndex, setActiveIndex] = useState(options.findIndex(option => option.value === value))
const [sliderStyle, setSliderStyle] = useState({}) const [sliderStyle, setSliderStyle] = useState({})
const pluginList = usePluginPageContext(v => v.installedPluginList)
const updateSliderStyle = (index: number) => { const updateSliderStyle = (index: number) => {
const tabElement = document.getElementById(`tab-${index}`) const tabElement = document.getElementById(`tab-${index}`)
@ -71,7 +73,7 @@ const TabSlider: FC<TabSliderProps> = ({
uppercase={true} uppercase={true}
state={BadgeState.Default} state={BadgeState.Default}
> >
6 {pluginList.length}
</Badge> </Badge>
} }
</div> </div>

@ -9,6 +9,7 @@ import Install from './steps/install'
import Installed from '../base/installed' import Installed from '../base/installed'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon' import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon'
import { usePluginPageContext } from '../../plugin-page/context'
const i18nPrefix = 'plugin.installModal' const i18nPrefix = 'plugin.installModal'
@ -28,6 +29,8 @@ const InstallFromLocalPackage: React.FC<InstallFromLocalPackageProps> = ({
const [uniqueIdentifier, setUniqueIdentifier] = useState<string | null>(null) const [uniqueIdentifier, setUniqueIdentifier] = useState<string | null>(null)
const [manifest, setManifest] = useState<PluginDeclaration | null>(null) const [manifest, setManifest] = useState<PluginDeclaration | null>(null)
const [errorMsg, setErrorMsg] = useState<string | null>(null) const [errorMsg, setErrorMsg] = useState<string | null>(null)
const mutateInstalledPluginList = usePluginPageContext(v => v.mutateInstalledPluginList)
const getTitle = useCallback(() => { const getTitle = useCallback(() => {
if (step === InstallStep.uploadFailed) if (step === InstallStep.uploadFailed)
return t(`${i18nPrefix}.uploadFailed`) return t(`${i18nPrefix}.uploadFailed`)
@ -63,9 +66,10 @@ const InstallFromLocalPackage: React.FC<InstallFromLocalPackageProps> = ({
setStep(InstallStep.uploadFailed) setStep(InstallStep.uploadFailed)
}, []) }, [])
const handleInstalled = useCallback(async () => { const handleInstalled = useCallback(() => {
mutateInstalledPluginList()
setStep(InstallStep.installed) setStep(InstallStep.installed)
}, []) }, [mutateInstalledPluginList])
const handleFailed = useCallback((errorMsg?: string) => { const handleFailed = useCallback((errorMsg?: string) => {
setStep(InstallStep.installFailed) setStep(InstallStep.installFailed)

@ -1,7 +1,7 @@
'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import React from 'react' import React, { useCallback } from 'react'
import { useRouter } from 'next/navigation' import type { MetaData } from '../types'
import { RiDeleteBinLine, RiInformation2Line, RiLoopLeftLine } from '@remixicon/react' import { RiDeleteBinLine, RiInformation2Line, RiLoopLeftLine } from '@remixicon/react'
import { useBoolean } from 'ahooks' import { useBoolean } from 'ahooks'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -9,6 +9,8 @@ import PluginInfo from '../plugin-page/plugin-info'
import ActionButton from '../../base/action-button' import ActionButton from '../../base/action-button'
import Tooltip from '../../base/tooltip' import Tooltip from '../../base/tooltip'
import Confirm from '../../base/confirm' import Confirm from '../../base/confirm'
import { uninstallPlugin } from '@/service/plugins'
import { usePluginPageContext } from '../plugin-page/context'
const i18nPrefix = 'plugin.action' const i18nPrefix = 'plugin.action'
@ -20,22 +22,23 @@ type Props = {
isShowInfo: boolean isShowInfo: boolean
isShowDelete: boolean isShowDelete: boolean
onDelete: () => void onDelete: () => void
meta: MetaData
} }
const Action: FC<Props> = ({ const Action: FC<Props> = ({
pluginId,
pluginName, pluginName,
usedInApps,
isShowFetchNewVersion, isShowFetchNewVersion,
isShowInfo, isShowInfo,
isShowDelete, isShowDelete,
onDelete, onDelete,
meta,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const router = useRouter()
const [isShowPluginInfo, { const [isShowPluginInfo, {
setTrue: showPluginInfo, setTrue: showPluginInfo,
setFalse: hidePluginInfo, setFalse: hidePluginInfo,
}] = useBoolean(false) }] = useBoolean(false)
const mutateInstalledPluginList = usePluginPageContext(v => v.mutateInstalledPluginList)
const handleFetchNewVersion = () => { } const handleFetchNewVersion = () => { }
@ -44,7 +47,14 @@ const Action: FC<Props> = ({
setFalse: hideDeleteConfirm, setFalse: hideDeleteConfirm,
}] = useBoolean(false) }] = useBoolean(false)
// const handleDelete = () => { } const handleDelete = useCallback(async () => {
const res = await uninstallPlugin(pluginId)
if (res.success) {
hideDeleteConfirm()
mutateInstalledPluginList()
onDelete()
}
}, [pluginId, onDelete])
return ( return (
<div className='flex space-x-1'> <div className='flex space-x-1'>
{/* Only plugin installed from GitHub need to check if it's the new version */} {/* Only plugin installed from GitHub need to check if it's the new version */}
@ -83,9 +93,9 @@ const Action: FC<Props> = ({
{isShowPluginInfo && ( {isShowPluginInfo && (
<PluginInfo <PluginInfo
repository='https://github.com/langgenius/dify-github-plugin' repository={meta.repo}
release='1.2.5' release={meta.version}
packageName='notion-sync.difypkg' packageName={meta.package}
onHide={hidePluginInfo} onHide={hidePluginInfo}
/> />
)} )}
@ -97,11 +107,12 @@ const Action: FC<Props> = ({
content={ content={
<div> <div>
{t(`${i18nPrefix}.deleteContentLeft`)}<span className='system-md-semibold'>{pluginName}</span>{t(`${i18nPrefix}.deleteContentRight`)}<br /> {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> </div>
} }
onCancel={hideDeleteConfirm} onCancel={hideDeleteConfirm}
onConfirm={onDelete} onConfirm={handleDelete}
/> />
) )
} }

@ -1,67 +1,94 @@
'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import React from 'react' import React, { useMemo } from 'react'
import { useContext } from 'use-context-selector' 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 { useTranslation } from 'react-i18next'
import { Github } from '../../base/icons/src/public/common' import { Github } from '../../base/icons/src/public/common'
import Badge from '../../base/badge' import Badge from '../../base/badge'
import type { Plugin } from '../types' import { type InstalledPlugin, PluginSource } from '../types'
import CornerMark from '../card/base/corner-mark' import CornerMark from '../card/base/corner-mark'
import Description from '../card/base/description' import Description from '../card/base/description'
import Icon from '../card/base/card-icon'
import OrgInfo from '../card/base/org-info' 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 I18n from '@/context/i18n' import I18n from '@/context/i18n'
import { API_PREFIX } from '@/config'
type Props = { type Props = {
className?: string className?: string
payload: Plugin plugin: InstalledPlugin
source: 'github' | 'marketplace' | 'local' | 'debug'
onDelete: () => void
} }
const PluginItem: FC<Props> = ({ const PluginItem: FC<Props> = ({
className, className,
payload, plugin,
source,
onDelete,
}) => { }) => {
const { locale } = useContext(I18n) const { locale } = useContext(I18n)
const { t } = useTranslation() const { t } = useTranslation()
const { type, name, org, label } = payload const {
const hasNewVersion = payload.latest_version !== payload.version 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 ( 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-[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'} : 'bg-background-section-burn'}
rounded-xl`} 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)}> <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 */} {/* Header */}
<div className="flex"> <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="ml-3 w-0 grow">
<div className="flex items-center h-5"> <div className="flex items-center h-5">
<Title title={label[locale]} /> <Title title={label[tLocale]} />
<RiVerifiedBadgeLine className="shrink-0 ml-0.5 w-4 h-4 text-text-accent" /> {verified && <RiVerifiedBadgeLine className="shrink-0 ml-0.5 w-4 h-4 text-text-accent" />}
<Badge className='ml-1' text={payload.version} hasRedCornerMark={hasNewVersion} /> <Badge className='ml-1' text={plugin.version} hasRedCornerMark={hasNewVersion} />
</div> </div>
<div className='flex items-center justify-between'> <div className='flex items-center justify-between'>
<Description text={payload.brief[locale]} descriptionLineRows={1}></Description> <Description text={description[tLocale]} descriptionLineRows={1}></Description>
<Action <Action
pluginId='xxx' pluginId={installation_id}
pluginName={label[locale]} pluginName={label[tLocale]}
usedInApps={5} usedInApps={5}
isShowFetchNewVersion={hasNewVersion} isShowFetchNewVersion={hasNewVersion}
isShowInfo isShowInfo={source === PluginSource.github}
isShowDelete isShowDelete
onDelete={onDelete} meta={meta}
onDelete={() => {}}
/> />
</div> </div>
</div> </div>
@ -71,19 +98,19 @@ const PluginItem: FC<Props> = ({
<div className='flex items-center'> <div className='flex items-center'>
<OrgInfo <OrgInfo
className="mt-0.5" className="mt-0.5"
orgName={org} orgName={orgName}
packageName={name} packageName={name}
packageNameClassName='w-auto max-w-[150px]' packageNameClassName='w-auto max-w-[150px]'
/> />
<div className='mx-2 text-text-quaternary system-xs-regular'>·</div> <div className='mx-2 text-text-quaternary system-xs-regular'>·</div>
<div className='flex text-text-tertiary system-xs-regular space-x-1'> <div className='flex text-text-tertiary system-xs-regular space-x-1'>
<RiLoginCircleLine className='w-4 h-4' /> <RiLoginCircleLine className='w-4 h-4' />
<span>{t('plugin.endpointsEnabled', { num: 2 })}</span> <span>{t('plugin.endpointsEnabled', { num: endpoints_active })}</span>
</div> </div>
</div> </div>
<div className='flex items-center'> <div className='flex items-center'>
{source === 'github' {source === PluginSource.github
&& <> && <>
<a href='' target='_blank' className='flex items-center gap-1'> <a href='' target='_blank' className='flex items-center gap-1'>
<div className='text-text-tertiary system-2xs-medium-uppercase'>{t('plugin.from')}</div> <div className='text-text-tertiary system-2xs-medium-uppercase'>{t('plugin.from')}</div>
@ -95,7 +122,7 @@ const PluginItem: FC<Props> = ({
</a> </a>
</> </>
} }
{source === 'marketplace' {source === PluginSource.marketplace
&& <> && <>
<a href='' target='_blank' className='flex items-center gap-0.5'> <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> <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> </a>
</> </>
} }
{source === 'local' {source === PluginSource.local
&& <> && <>
<div className='flex items-center gap-1'> <div className='flex items-center gap-1'>
<RiHardDrive3Line className='text-text-tertiary w-3 h-3' /> <RiHardDrive3Line className='text-text-tertiary w-3 h-3' />
@ -111,7 +138,7 @@ const PluginItem: FC<Props> = ({
</div> </div>
</> </>
} }
{source === 'debug' {source === PluginSource.debugging
&& <> && <>
<div className='flex items-center gap-1'> <div className='flex items-center gap-1'>
<RiBugLine className='w-3 h-3 text-text-warning' /> <RiBugLine className='w-3 h-3 text-text-warning' />

@ -9,14 +9,20 @@ import {
createContext, createContext,
useContextSelector, useContextSelector,
} from 'use-context-selector' } 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 { PermissionType } from '../types'
import { fetchInstalledPluginList } from '@/service/plugins'
import useSWR from 'swr'
export type PluginPageContextValue = { export type PluginPageContextValue = {
containerRef: React.RefObject<HTMLDivElement> containerRef: React.RefObject<HTMLDivElement>
permissions: Permissions permissions: Permissions
setPermissions: (permissions: PluginPageContextValue['permissions']) => void setPermissions: (permissions: PluginPageContextValue['permissions']) => void
installedPluginList: InstalledPlugin[]
mutateInstalledPluginList: () => void
filters: FilterState
setFilters: (filter: FilterState) => void
} }
export const PluginPageContext = createContext<PluginPageContextValue>({ export const PluginPageContext = createContext<PluginPageContextValue>({
@ -26,6 +32,14 @@ export const PluginPageContext = createContext<PluginPageContextValue>({
debug_permission: PermissionType.noOne, debug_permission: PermissionType.noOne,
}, },
setPermissions: () => { }, setPermissions: () => { },
installedPluginList: [],
mutateInstalledPluginList: () => {},
filters: {
categories: [],
tags: [],
searchQuery: '',
},
setFilters: () => {},
}) })
type PluginPageContextProviderProps = { type PluginPageContextProviderProps = {
@ -44,6 +58,12 @@ export const PluginPageContextProvider = ({
install_permission: PermissionType.noOne, install_permission: PermissionType.noOne,
debug_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 ( return (
<PluginPageContext.Provider <PluginPageContext.Provider
@ -51,6 +71,10 @@ export const PluginPageContextProvider = ({
containerRef, containerRef,
permissions, permissions,
setPermissions, setPermissions,
installedPluginList: data?.plugins || [],
mutateInstalledPluginList,
filters,
setFilters,
}} }}
> >
{children} {children}

@ -1,22 +1,21 @@
import type { FC } from 'react'
import PluginItem from '../../plugin-item' import PluginItem from '../../plugin-item'
import { customTool, extensionDallE, modelGPT4, toolNotion } from '@/app/components/plugins/card/card-mock' import type { InstalledPlugin } from '../../types'
const PluginList = () => { type IPluginListProps = {
const pluginList = [toolNotion, extensionDallE, modelGPT4, customTool] pluginList: InstalledPlugin[]
}
const PluginList: FC<IPluginListProps> = ({ pluginList }) => {
return ( return (
<div className='pb-3 bg-white'> <div className='pb-3 bg-white'>
<div> <div className='grid grid-cols-2 gap-3'>
<div className='grid grid-cols-2 gap-3'> {pluginList.map(plugin => (
{pluginList.map((plugin, index) => ( <PluginItem
<PluginItem key={plugin.plugin_id}
key={index} plugin={plugin}
payload={plugin as any} />
onDelete={() => {}} ))}
source={'debug'}
/>
))}
</div>
</div> </div>
</div> </div>
) )

@ -1,16 +1,33 @@
'use client' 'use client'
import { useState } from 'react' import { useMemo, useState } from 'react'
import type { EndpointListItem, PluginDetail } from '../types' import type { EndpointListItem, InstalledPlugin, PluginDetail } from '../types'
import type { FilterState } from './filter-management' import type { FilterState } from './filter-management'
import FilterManagement from './filter-management' import FilterManagement from './filter-management'
import List from './list' import List from './list'
import PluginDetailPanel from '@/app/components/plugins/plugin-detail-panel' import PluginDetailPanel from '@/app/components/plugins/plugin-detail-panel'
import { toolNotion, toolNotionEndpoints } from '@/app/components/plugins/plugin-detail-panel/mock' import { toolNotion, toolNotionEndpoints } from '@/app/components/plugins/plugin-detail-panel/mock'
import { usePluginPageContext } from './context'
import { useDebounceFn } from 'ahooks'
const PluginsPanel = () => { 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 [currentPluginDetail, setCurrentPluginDetail] = useState<PluginDetail | undefined>(toolNotion as any)
const [currentPluginEndpoints, setCurrentEndpoints] = useState<EndpointListItem[]>(toolNotionEndpoints as any) const [currentPluginEndpoints, setCurrentEndpoints] = useState<EndpointListItem[]>(toolNotionEndpoints as any)
@ -24,7 +41,7 @@ const PluginsPanel = () => {
</div> </div>
<div className='flex px-12 items-start content-start gap-2 flex-grow self-stretch flex-wrap'> <div className='flex px-12 items-start content-start gap-2 flex-grow self-stretch flex-wrap'>
<div className='w-full'> <div className='w-full'>
<List /> <List pluginList={filteredList} />
</div> </div>
</div> </div>
<PluginDetailPanel <PluginDetailPanel

@ -112,7 +112,7 @@ export type Plugin = {
// Repo readme.md content // Repo readme.md content
introduction: string introduction: string
repository: string repository: string
category: string category: PluginType
install_count: number install_count: number
endpoint: { endpoint: {
settings: CredentialFormSchemaBase[] settings: CredentialFormSchemaBase[]
@ -235,3 +235,29 @@ export type TaskStatusResponse = {
plugins: PluginStatus[] 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, EndpointsRequest,
EndpointsResponse, EndpointsResponse,
InstallPackageResponse, InstallPackageResponse,
InstalledPluginListResponse,
Permissions, Permissions,
PluginDeclaration, PluginDeclaration,
PluginManifestInMarket, PluginManifestInMarket,
TaskStatusResponse, TaskStatusResponse,
UninstallPluginResponse,
UpdateEndpointRequest, UpdateEndpointRequest,
} from '@/app/components/plugins/types' } from '@/app/components/plugins/types'
import type { DebugInfo as DebugInfoTypes } 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) => { export const updatePermission = async (permissions: Permissions) => {
return post('/workspaces/current/plugin/permission/change', { body: 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…
Cancel
Save