mirror of
https://github.com/langgenius/dify.git
synced 2026-07-24 21:18:35 +08:00
Co-authored-by: hjlarry <hjlarry@163.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: yyh <yuanyouhuilyz@gmail.com> Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com>
171 lines
5.7 KiB
TypeScript
171 lines
5.7 KiB
TypeScript
import type { ApiBasedExtensionResponse } from '@dify/contracts/api/console/api-based-extension/types.gen'
|
|
import type { ReactNode } from 'react'
|
|
import { Button } from '@langgenius/dify-ui/button'
|
|
import { useQuery } from '@tanstack/react-query'
|
|
import { useAtomValue } from 'jotai'
|
|
import { useMemo, useState } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { SearchInput } from '@/app/components/base/search-input'
|
|
import { SkeletonContainer, SkeletonRectangle, SkeletonRow } from '@/app/components/base/skeleton'
|
|
import { STEP_BY_STEP_TOUR_TARGETS } from '@/app/components/step-by-step-tour/target-registry'
|
|
import { workspacePermissionKeysAtom } from '@/context/permission-state'
|
|
import { consoleQuery } from '@/service/client'
|
|
import { hasPermission } from '@/utils/permission'
|
|
import { Empty } from './empty'
|
|
import { Item } from './item'
|
|
import { ApiBasedExtensionModal } from './modal'
|
|
|
|
type ApiBasedExtensionDialogState =
|
|
| {
|
|
mode: 'create'
|
|
}
|
|
| {
|
|
mode: 'edit'
|
|
apiBasedExtension: ApiBasedExtensionResponse
|
|
}
|
|
| null
|
|
|
|
type ApiBasedExtensionPageProps = {
|
|
layout?: (parts: { body: ReactNode; toolbar: ReactNode }) => ReactNode
|
|
}
|
|
|
|
function ApiBasedExtensionListSkeleton() {
|
|
const { t } = useTranslation()
|
|
|
|
return (
|
|
<div role="status" aria-label={t(($) => $.loading, { ns: 'common' })} className="space-y-2">
|
|
{Array.from({ length: 2 }, (_, index) => (
|
|
<div
|
|
key={index}
|
|
className="rounded-xl border-[0.5px] border-components-card-border bg-components-card-bg p-4 shadow-xs"
|
|
>
|
|
<SkeletonContainer className="h-16">
|
|
<SkeletonRow>
|
|
<SkeletonRectangle className="size-8 shrink-0 animate-pulse rounded-lg" />
|
|
<div className="flex flex-1 flex-col gap-1">
|
|
<SkeletonRectangle className="h-4 w-1/3 animate-pulse" />
|
|
<SkeletonRectangle className="h-3 w-1/2 animate-pulse" />
|
|
</div>
|
|
<SkeletonRectangle className="h-8 w-8 animate-pulse rounded-lg" />
|
|
</SkeletonRow>
|
|
</SkeletonContainer>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function ApiBasedExtensionPage({ layout }: ApiBasedExtensionPageProps = {}) {
|
|
const { t } = useTranslation()
|
|
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
|
|
const canManage = hasPermission(workspacePermissionKeys, 'api_extension.manage')
|
|
const { data: apiBasedExtensions = [], isPending: isLoading } = useQuery(
|
|
consoleQuery.apiBasedExtension.get.queryOptions(),
|
|
)
|
|
const [dialogState, setDialogState] = useState<ApiBasedExtensionDialogState>(null)
|
|
const [keywords, setKeywords] = useState('')
|
|
|
|
const filteredApiBasedExtensions = useMemo(() => {
|
|
const query = keywords.trim().toLowerCase()
|
|
if (!query) return apiBasedExtensions
|
|
|
|
return apiBasedExtensions.filter((apiBasedExtension) => {
|
|
return (
|
|
apiBasedExtension.name.toLowerCase().includes(query) ||
|
|
apiBasedExtension.api_endpoint.toLowerCase().includes(query)
|
|
)
|
|
})
|
|
}, [apiBasedExtensions, keywords])
|
|
const hasApiBasedExtensions = apiBasedExtensions.length > 0
|
|
const hasSearchKeywords = keywords.trim().length > 0
|
|
|
|
const handleOpenApiBasedExtensionModal = () => {
|
|
if (!canManage) return
|
|
|
|
setDialogState({
|
|
mode: 'create',
|
|
})
|
|
}
|
|
const handleEditApiBasedExtension = (apiBasedExtension: ApiBasedExtensionResponse) => {
|
|
if (!canManage) return
|
|
|
|
setDialogState({
|
|
mode: 'edit',
|
|
apiBasedExtension,
|
|
})
|
|
}
|
|
const handleApiBasedExtensionSaved = () => {
|
|
setDialogState(null)
|
|
}
|
|
const handleApiBasedExtensionModalOpenChange = (open: boolean) => {
|
|
if (!open) setDialogState(null)
|
|
}
|
|
|
|
const toolbar = (
|
|
<div className="flex w-full items-center justify-between gap-2">
|
|
<SearchInput className="w-[200px]" value={keywords} onValueChange={setKeywords} />
|
|
<Button variant="secondary" disabled={!canManage} onClick={handleOpenApiBasedExtensionModal}>
|
|
<span className="mr-1 i-ri-add-line size-4" aria-hidden="true" />
|
|
{t(($) => $['apiBasedExtension.add'], { ns: 'common' })}
|
|
</Button>
|
|
</div>
|
|
)
|
|
|
|
const body = (
|
|
<>
|
|
{isLoading && <ApiBasedExtensionListSkeleton />}
|
|
{!isLoading && !hasApiBasedExtensions && <Empty />}
|
|
{!isLoading &&
|
|
hasApiBasedExtensions &&
|
|
hasSearchKeywords &&
|
|
!filteredApiBasedExtensions.length && (
|
|
<div className="py-10 text-center system-sm-regular text-text-tertiary">
|
|
{t(($) => $['dataSource.notion.selector.noSearchResult'], { ns: 'common' })}
|
|
</div>
|
|
)}
|
|
{!isLoading &&
|
|
!!filteredApiBasedExtensions.length &&
|
|
filteredApiBasedExtensions.map((item, index) => (
|
|
<div
|
|
key={item.id}
|
|
data-step-by-step-tour-target={
|
|
index === 0 ? STEP_BY_STEP_TOUR_TARGETS.integrationCustomEndpointEmpty : undefined
|
|
}
|
|
>
|
|
<Item
|
|
apiBasedExtension={item}
|
|
onEdit={handleEditApiBasedExtension}
|
|
canManage={canManage}
|
|
/>
|
|
</div>
|
|
))}
|
|
{dialogState?.mode === 'create' && (
|
|
<ApiBasedExtensionModal
|
|
open
|
|
mode="create"
|
|
onOpenChange={handleApiBasedExtensionModalOpenChange}
|
|
onSaved={handleApiBasedExtensionSaved}
|
|
/>
|
|
)}
|
|
{dialogState?.mode === 'edit' && (
|
|
<ApiBasedExtensionModal
|
|
open
|
|
mode="edit"
|
|
apiBasedExtension={dialogState.apiBasedExtension}
|
|
onOpenChange={handleApiBasedExtensionModalOpenChange}
|
|
onSaved={handleApiBasedExtensionSaved}
|
|
/>
|
|
)}
|
|
</>
|
|
)
|
|
|
|
if (layout) return layout({ body, toolbar })
|
|
|
|
return (
|
|
<>
|
|
<div className="mb-3">{toolbar}</div>
|
|
{body}
|
|
</>
|
|
)
|
|
}
|