diff --git a/web/app/components/datasets/create/website/base/header.tsx b/web/app/components/datasets/create/website/base/header.tsx index 963efd1761..298392134b 100644 --- a/web/app/components/datasets/create/website/base/header.tsx +++ b/web/app/components/datasets/create/website/base/header.tsx @@ -30,24 +30,22 @@ const Header = ({ )}> {title} - {!isInPipeline && ( - <> - - - - )} + + void + credentials: Array +} + +const CredentialSelector = ({ + pluginName, + currentCredentialId, + onCredentialChange, + credentials, +}: CredentialSelectorProps) => { + const [open, { toggle }] = useBoolean(false) + + const currentCredential = credentials.find(cred => cred.id === currentCredentialId) as DataSourceCredential + + const handleCredentialChange = useCallback((credentialId: string) => { + onCredentialChange(credentialId) + toggle() + }, [onCredentialChange, toggle]) + + return ( + + + + + + + + + ) +} + +export default React.memo(CredentialSelector) diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/base/credential-selector/item.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/base/credential-selector/item.tsx new file mode 100644 index 0000000000..c833f27403 --- /dev/null +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/base/credential-selector/item.tsx @@ -0,0 +1,47 @@ +import type { DataSourceCredential } from '@/types/pipeline' +import { RiCheckLine } from '@remixicon/react' +import React, { useCallback } from 'react' +import { useTranslation } from 'react-i18next' + +type ItemProps = { + credential: DataSourceCredential + pluginName: string + isSelected: boolean + onCredentialChange: (credentialId: string) => void +} + +const Item = ({ + credential, + pluginName, + isSelected, + onCredentialChange, +}: ItemProps) => { + const { t } = useTranslation() + const { avatar_url, name } = credential + + const handleCredentialChange = useCallback(() => { + onCredentialChange(credential.id) + }, [credential.id, onCredentialChange]) + + return ( +
+ + + {t('datasetPipeline.credentialSelector.name', { + credentialName: name, + pluginName, + })} + + { + isSelected && ( + + ) + } +
+ ) +} + +export default React.memo(Item) diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/base/credential-selector/list.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/base/credential-selector/list.tsx new file mode 100644 index 0000000000..b161a80309 --- /dev/null +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/base/credential-selector/list.tsx @@ -0,0 +1,38 @@ +import type { DataSourceCredential } from '@/types/pipeline' +import React from 'react' +import Item from './item' + +type ListProps = { + currentCredentialId: string + credentials: Array + pluginName: string + onCredentialChange: (credentialId: string) => void +} + +const List = ({ + currentCredentialId, + credentials, + pluginName, + onCredentialChange, +}: ListProps) => { + return ( +
+ { + credentials.map((credential) => { + const isSelected = credential.id === currentCredentialId + return ( + + ) + }) + } +
+ ) +} + +export default React.memo(List) diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/base/credential-selector/trigger.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/base/credential-selector/trigger.tsx new file mode 100644 index 0000000000..0b21571645 --- /dev/null +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/base/credential-selector/trigger.tsx @@ -0,0 +1,44 @@ +import React from 'react' +import type { DataSourceCredential } from '@/types/pipeline' +import { useTranslation } from 'react-i18next' +import { RiArrowDownSLine } from '@remixicon/react' +import cn from '@/utils/classnames' + +type TriggerProps = { + currentCredential: DataSourceCredential + pluginName: string + isOpen: boolean +} + +const Trigger = ({ + currentCredential, + pluginName, + isOpen, +}: TriggerProps) => { + const { t } = useTranslation() + + const { + avatar_url, + name, + } = currentCredential + + return ( +
+ +
+ + {t('datasetPipeline.credentialSelector.name', { + credentialName: name, + pluginName, + })} + + +
+
+ ) +} + +export default React.memo(Trigger) diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/base/header.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/base/header.tsx new file mode 100644 index 0000000000..96eef3350d --- /dev/null +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/base/header.tsx @@ -0,0 +1,51 @@ +import React from 'react' +import Divider from '@/app/components/base/divider' +import Button from '@/app/components/base/button' +import { RiBookOpenLine, RiEqualizer2Line } from '@remixicon/react' +import type { CredentialSelectorProps } from './credential-selector' +import CredentialSelector from './credential-selector' + +type HeaderProps = { + docTitle: string + docLink: string + onClickConfiguration?: () => void +} & CredentialSelectorProps + +const Header = ({ + docTitle, + docLink, + onClickConfiguration, + ...rest +}: HeaderProps) => { + return ( +
+
+ + + +
+
+ + {docTitle} + +
+ ) +} + +export default React.memo(Header) diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/online-documents/index.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-documents/index.tsx index a36345e28c..7fb4be3c6e 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/data-source/online-documents/index.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-documents/index.tsx @@ -1,9 +1,8 @@ import { useCallback, useEffect, useMemo } from 'react' -import WorkspaceSelector from '@/app/components/base/notion-page-selector/workspace-selector' import SearchInput from '@/app/components/base/notion-page-selector/search-input' import PageSelector from './page-selector' import type { DataSourceNotionPageMap, DataSourceNotionWorkspace } from '@/models/common' -import Header from '@/app/components/datasets/create/website/base/header' +import Header from '../base/header' import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail' import { DatasourceType } from '@/models/pipeline' import { ssePost } from '@/service/base' @@ -12,6 +11,10 @@ import type { DataSourceNodeCompletedResponse, DataSourceNodeErrorResponse } fro import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types' import { useDataSourceStore, useDataSourceStoreWithSelector } from '../store' import { useShallow } from 'zustand/react/shallow' +import { useModalContextSelector } from '@/context/modal-context' +import Title from './title' +import { CredentialTypeEnum } from '@/app/components/plugins/plugin-auth' +import { noop } from 'lodash-es' type OnlineDocumentsProps = { isInPipeline?: boolean @@ -20,11 +23,12 @@ type OnlineDocumentsProps = { } const OnlineDocuments = ({ - isInPipeline = false, nodeId, nodeData, + isInPipeline = false, }: OnlineDocumentsProps) => { const pipelineId = useDatasetDetailContextWithSelector(s => s.dataset?.pipeline_id) + const setShowAccountSettingModal = useModalContextSelector(s => s.setShowAccountSettingModal) const { documentsData, searchValue, @@ -106,7 +110,6 @@ const OnlineDocuments = ({ if (!documentsData.length) getOnlineDocuments() } - // eslint-disable-next-line react-hooks/exhaustive-deps }, [nodeId]) const currentWorkspace = documentsData.find(workspace => workspace.workspace_id === currentWorkspaceId) @@ -116,11 +119,6 @@ const OnlineDocuments = ({ setSearchValue(value) }, [dataSourceStore]) - const handleSelectWorkspace = useCallback((workspaceId: string) => { - const { setCurrentWorkspaceId } = dataSourceStore.getState() - setCurrentWorkspaceId(workspaceId) - }, [dataSourceStore]) - const handleSelectPages = useCallback((newSelectedPagesId: Set) => { const { setSelectedPagesId, setOnlineDocuments } = dataSourceStore.getState() const selectedPages = Array.from(newSelectedPagesId).map(pageId => PagesMapAndSelectedPagesId[pageId]) @@ -133,13 +131,11 @@ const OnlineDocuments = ({ setCurrentDocument(PagesMapAndSelectedPagesId[previewPageId]) }, [PagesMapAndSelectedPagesId, dataSourceStore]) - const headerInfo = useMemo(() => { - return { - title: nodeData.title, - docTitle: 'How to use?', - docLink: 'https://docs.dify.ai', - } - }, [nodeData]) + const handleSetting = useCallback(() => { + setShowAccountSettingModal({ + payload: 'data-source', + }) + }, [setShowAccountSettingModal]) if (!documentsData?.length) return null @@ -147,17 +143,28 @@ const OnlineDocuments = ({ return (
-
-
- +
+
+ </div> <SearchInput value={searchValue} diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/online-documents/title.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-documents/title.tsx new file mode 100644 index 0000000000..d7e3d68afc --- /dev/null +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-documents/title.tsx @@ -0,0 +1,20 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' + +type TitleProps = { + name: string +} + +const Title = ({ + name, +}: TitleProps) => { + const { t } = useTranslation() + + return ( + <div className='system-sm-medium px-[5px] py-1 text-text-secondary'> + {t('datasetPipeline.onlineDocument.pageSelectorTitle', { name })} + </div> + ) +} + +export default React.memo(Title) diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/index.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/index.tsx index 82a88e31bd..7fe5f01471 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/index.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/index.tsx @@ -1,5 +1,5 @@ import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types' -import Header from './header' +import Header from '../base/header' import { useCallback, useEffect, useMemo, useState } from 'react' import FileList from './file-list' import type { OnlineDriveFile } from '@/models/pipeline' @@ -12,6 +12,9 @@ import { useDataSourceStore, useDataSourceStoreWithSelector } from '../store' import { convertOnlineDriveData } from './utils' import produce from 'immer' import { useShallow } from 'zustand/react/shallow' +import { useModalContextSelector } from '@/context/modal-context' +import { noop } from 'lodash-es' +import { CredentialTypeEnum } from '@/app/components/plugins/plugin-auth' type OnlineDriveProps = { nodeId: string @@ -25,6 +28,7 @@ const OnlineDrive = ({ isInPipeline = false, }: OnlineDriveProps) => { const pipelineId = useDatasetDetailContextWithSelector(s => s.dataset?.pipeline_id) + const setShowAccountSettingModal = useModalContextSelector(s => s.setShowAccountSettingModal) const { prefix, keywords, @@ -118,7 +122,6 @@ const OnlineDrive = ({ if (fileList.length > 0) return getOnlineDriveFiles({}) } - // eslint-disable-next-line react-hooks/exhaustive-deps }, [nodeId]) const onlineDriveFileList = useMemo(() => { @@ -173,11 +176,32 @@ const OnlineDrive = ({ } }, [dataSourceStore, getOnlineDriveFiles]) + const handleSetting = useCallback(() => { + setShowAccountSettingModal({ + payload: 'data-source', + }) + }, [setShowAccountSettingModal]) + return ( <div className='flex flex-col gap-y-2'> <Header + // todo: delete mock data docTitle='Online Drive Docs' docLink='https://docs.dify.ai/' + onClickConfiguration={handleSetting} + pluginName={nodeData.datasource_label} + currentCredentialId={'12345678'} + onCredentialChange={noop} + credentials={[{ + avatar_url: 'https://cloud.dify.ai/logo/logo.svg', + credential: { + credentials: '......', + }, + id: '12345678', + is_default: true, + name: 'test123', + type: CredentialTypeEnum.API_KEY, + }]} /> <FileList fileList={onlineDriveFileList} diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/website-crawl/base/options/index.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/website-crawl/base/options/index.tsx index 9b51590338..b6960fc1fe 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/data-source/website-crawl/base/options/index.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/website-crawl/base/options/index.tsx @@ -67,7 +67,6 @@ const Options = ({ useEffect(() => { if (controlFoldOptions !== 0) foldHide() - // eslint-disable-next-line react-hooks/exhaustive-deps }, [controlFoldOptions]) return ( @@ -87,7 +86,7 @@ const Options = ({ <span className='system-sm-semibold-uppercase text-text-secondary'> {t(`${I18N_PREFIX}.options`)} </span> - <ArrowDownRoundFill className={cn('h-4 w-4 shrink-0 text-text-tertiary', fold && '-rotate-90')} /> + <ArrowDownRoundFill className={cn('h-4 w-4 shrink-0 text-text-quaternary', fold && '-rotate-90')} /> </div> <Button variant='primary' diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/website-crawl/index.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/website-crawl/index.tsx index 6ccd06fc19..58da88af1e 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/data-source/website-crawl/index.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/website-crawl/index.tsx @@ -23,6 +23,7 @@ import type { import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types' import { useDataSourceStore, useDataSourceStoreWithSelector } from '../store' import { useShallow } from 'zustand/react/shallow' +import { useModalContextSelector } from '@/context/modal-context' const I18N_PREFIX = 'datasetCreation.stepOne.website' @@ -43,6 +44,7 @@ const WebsiteCrawl = ({ const [crawledNum, setCrawledNum] = useState(0) const [crawlErrorMessage, setCrawlErrorMessage] = useState('') const pipelineId = useDatasetDetailContextWithSelector(s => s.dataset?.pipeline_id) + const setShowAccountSettingModal = useModalContextSelector(s => s.setShowAccountSettingModal) const { crawlResult, step, @@ -87,7 +89,6 @@ const WebsiteCrawl = ({ setCrawlErrorMessage('') currentNodeIdRef.current = nodeId } - // eslint-disable-next-line react-hooks/exhaustive-deps }, [nodeId]) const isInit = step === CrawlStep.init @@ -164,10 +165,17 @@ const WebsiteCrawl = ({ } }, [nodeData]) + const handleSetting = useCallback(() => { + setShowAccountSettingModal({ + payload: 'data-source', + }) + }, [setShowAccountSettingModal]) + return ( <div className='flex flex-col'> <Header isInPipeline + onClickConfiguration={handleSetting} {...headerInfo} /> <div className='mt-2 rounded-xl border border-components-panel-border bg-background-default-subtle'> diff --git a/web/i18n/en-US/dataset-pipeline.ts b/web/i18n/en-US/dataset-pipeline.ts index 96761955ba..31af466e41 100644 --- a/web/i18n/en-US/dataset-pipeline.ts +++ b/web/i18n/en-US/dataset-pipeline.ts @@ -114,6 +114,9 @@ const translation = { documentSettings: { title: 'Document Settings', }, + onlineDocument: { + pageSelectorTitle: '{{name}} pages', + }, onlineDrive: { notConnected: '{{name}} is not connected', notConnectedTip: 'To sync with {{name}}, connection to {{name}} must be established first.', @@ -127,6 +130,9 @@ const translation = { emptySearchResult: 'No items were found', resetKeywords: 'Reset keywords', }, + credentialSelector: { + name: '{{credentialName}}\'s {{pluginName}}', + }, conversion: { title: 'Convert to Knowledge Pipeline', descriptionChunk1: 'You can now convert your existing knowledge base to use the Knowledge Pipeline for document processing', diff --git a/web/i18n/zh-Hans/dataset-pipeline.ts b/web/i18n/zh-Hans/dataset-pipeline.ts index dc8b11a840..edb70c0dbd 100644 --- a/web/i18n/zh-Hans/dataset-pipeline.ts +++ b/web/i18n/zh-Hans/dataset-pipeline.ts @@ -114,6 +114,9 @@ const translation = { documentSettings: { title: '文档设置', }, + onlineDocument: { + pageSelectorTitle: '{{name}} 页面', + }, onlineDrive: { notConnected: '{{name}} 未绑定', notConnectedTip: '同步 {{name}} 内容前, 须先绑定 {{name}}。', @@ -127,6 +130,9 @@ const translation = { emptySearchResult: '未找到任何项目', resetKeywords: '重置关键词', }, + credentialSelector: { + name: '{{credentialName}} 的 {{pluginName}}', + }, conversion: { title: '转换为知识库 pipeline', descriptionChunk1: '您现在可以将现有知识库转换为使用知识库 pipeline 来处理文档', diff --git a/web/types/pipeline.tsx b/web/types/pipeline.tsx index a4321c6707..768664d053 100644 --- a/web/types/pipeline.tsx +++ b/web/types/pipeline.tsx @@ -1,3 +1,5 @@ +import type { CredentialTypeEnum } from '@/app/components/plugins/plugin-auth' + export type DataSourceNodeProcessingResponse = { event: 'datasource_processing' total: number @@ -31,3 +33,12 @@ export type DataSourceNodeErrorResponse = { event: 'datasource_error' error: string } + +export type DataSourceCredential = { + avatar_url?: string + credential: Record<string, any> + id: string + is_default: boolean + name: string + type: CredentialTypeEnum +}