| @@ -0,0 +1,53 @@ | |||
| import cn from '@/utils/classnames' | |||
| import React, { useMemo } from 'react' | |||
| type CredentialIconProps = { | |||
| avatar_url?: string | |||
| name: string | |||
| size?: number | |||
| className?: string | |||
| } | |||
| const ICON_BG_COLORS = [ | |||
| 'bg-components-icon-bg-orange-dark-solid', | |||
| 'bg-components-icon-bg-pink-solid', | |||
| 'bg-components-icon-bg-indigo-solid', | |||
| 'bg-components-icon-bg-teal-solid', | |||
| ] | |||
| export const CredentialIcon: React.FC<CredentialIconProps> = ({ | |||
| avatar_url, | |||
| name, | |||
| size = 20, | |||
| className = '', | |||
| }) => { | |||
| const firstLetter = useMemo(() => name.charAt(0).toUpperCase(), [name]) | |||
| const bgColor = useMemo(() => ICON_BG_COLORS[firstLetter.charCodeAt(0) % ICON_BG_COLORS.length], [firstLetter]) | |||
| if (avatar_url && avatar_url !== 'default') { | |||
| return ( | |||
| <img | |||
| src={avatar_url} | |||
| alt={`${name} logo`} | |||
| width={size} | |||
| height={size} | |||
| className={cn('shrink-0 rounded-md border border-divider-regular object-contain', className)} | |||
| /> | |||
| ) | |||
| } | |||
| return ( | |||
| <div | |||
| className={cn( | |||
| 'flex shrink-0 items-center justify-center rounded-md border border-divider-regular', | |||
| bgColor, | |||
| className, | |||
| )} | |||
| style={{ width: `${size}px`, height: `${size}px` }} | |||
| > | |||
| <span className='bg-gradient-to-b from-components-avatar-shape-fill-stop-0 to-components-avatar-shape-fill-stop-100 bg-clip-text text-[13px] font-semibold leading-[1.2] text-transparent opacity-90'> | |||
| {firstLetter} | |||
| </span> | |||
| </div> | |||
| ) | |||
| } | |||
| @@ -1,4 +1,4 @@ | |||
| import React, { useCallback } from 'react' | |||
| import React, { useCallback, useEffect, useMemo } from 'react' | |||
| import { | |||
| PortalToFollowElem, | |||
| PortalToFollowElemContent, | |||
| @@ -24,7 +24,14 @@ const CredentialSelector = ({ | |||
| }: CredentialSelectorProps) => { | |||
| const [open, { toggle }] = useBoolean(false) | |||
| const currentCredential = credentials.find(cred => cred.id === currentCredentialId) as DataSourceCredential | |||
| const currentCredential = useMemo(() => { | |||
| return credentials.find(cred => cred.id === currentCredentialId) | |||
| }, [credentials, currentCredentialId]) | |||
| useEffect(() => { | |||
| if (!currentCredential && credentials.length) | |||
| onCredentialChange(credentials[0].id) | |||
| }, [currentCredential, credentials]) | |||
| const handleCredentialChange = useCallback((credentialId: string) => { | |||
| onCredentialChange(credentialId) | |||
| @@ -1,3 +1,4 @@ | |||
| import { CredentialIcon } from '@/app/components/datasets/common/credential-icon' | |||
| import type { DataSourceCredential } from '@/types/pipeline' | |||
| import { RiCheckLine } from '@remixicon/react' | |||
| import React, { useCallback } from 'react' | |||
| @@ -28,8 +29,12 @@ const Item = ({ | |||
| className='flex cursor-pointer items-center gap-x-2 rounded-lg p-2 hover:bg-state-base-hover' | |||
| onClick={handleCredentialChange} | |||
| > | |||
| <img src={avatar_url} className='size-5 shrink-0 rounded-md border border-divider-regular object-contain' /> | |||
| <span className='system-sm-medium grow text-text-secondary'> | |||
| <CredentialIcon | |||
| avatar_url={avatar_url} | |||
| name={name} | |||
| size={20} | |||
| /> | |||
| <span className='system-sm-medium grow truncate text-text-secondary'> | |||
| {t('datasetPipeline.credentialSelector.name', { | |||
| credentialName: name, | |||
| pluginName, | |||
| @@ -3,9 +3,10 @@ import type { DataSourceCredential } from '@/types/pipeline' | |||
| import { useTranslation } from 'react-i18next' | |||
| import { RiArrowDownSLine } from '@remixicon/react' | |||
| import cn from '@/utils/classnames' | |||
| import { CredentialIcon } from '@/app/components/datasets/common/credential-icon' | |||
| type TriggerProps = { | |||
| currentCredential: DataSourceCredential | |||
| currentCredential: DataSourceCredential | undefined | |||
| pluginName: string | |||
| isOpen: boolean | |||
| } | |||
| @@ -19,23 +20,29 @@ const Trigger = ({ | |||
| const { | |||
| avatar_url, | |||
| name, | |||
| } = currentCredential | |||
| name = '', | |||
| } = currentCredential || {} | |||
| return ( | |||
| <div className={cn( | |||
| 'flex cursor-pointer items-center gap-x-2 rounded-md p-1 pr-2', | |||
| isOpen ? 'bg-state-base-hover' : 'hover:bg-state-base-hover', | |||
| )}> | |||
| <img src={avatar_url} className='size-5 shrink-0 rounded-md border border-divider-regular object-contain' /> | |||
| <div className='flex grow items-center gap-x-1'> | |||
| <span className='system-md-semibold text-text-secondary'> | |||
| <div | |||
| className={cn( | |||
| 'flex cursor-pointer items-center gap-x-2 rounded-md p-1 pr-2', | |||
| isOpen ? 'bg-state-base-hover' : 'hover:bg-state-base-hover', | |||
| )} | |||
| > | |||
| <CredentialIcon | |||
| avatar_url={avatar_url} | |||
| name={name} | |||
| size={20} | |||
| /> | |||
| <div className='flex items-center gap-x-1'> | |||
| <span className='system-md-semibold min-w-0 truncate text-text-secondary'> | |||
| {t('datasetPipeline.credentialSelector.name', { | |||
| credentialName: name, | |||
| pluginName, | |||
| })} | |||
| </span> | |||
| <RiArrowDownSLine className='size-4 text-text-secondary' /> | |||
| <RiArrowDownSLine className='size-4 shrink-0 text-text-secondary' /> | |||
| </div> | |||
| </div> | |||
| ) | |||
| @@ -23,11 +23,11 @@ const Header = ({ | |||
| return ( | |||
| <div className='flex items-center gap-x-2'> | |||
| <div className='flex shrink-0 grow items-center gap-x-1'> | |||
| <div className='flex grow items-center gap-x-1'> | |||
| <CredentialSelector | |||
| {...rest} | |||
| /> | |||
| <Divider type='vertical' className='mx-1 h-3.5' /> | |||
| <Divider type='vertical' className='mx-1 h-3.5 shrink-0' /> | |||
| <Tooltip | |||
| popupContent={t('datasetPipeline.configurationTip', { pluginName: rest.pluginName })} | |||
| position='top' | |||
| @@ -35,7 +35,7 @@ const Header = ({ | |||
| <Button | |||
| variant='ghost' | |||
| size='small' | |||
| className='size-6 px-1' | |||
| className='size-6 shrink-0 px-1' | |||
| > | |||
| <RiEqualizer2Line | |||
| className='h-4 w-4' | |||
| @@ -45,13 +45,13 @@ const Header = ({ | |||
| </Tooltip> | |||
| </div> | |||
| <a | |||
| className='system-xs-medium flex items-center gap-x-1 overflow-hidden text-text-accent' | |||
| className='system-xs-medium flex shrink-0 items-center gap-x-1 text-text-accent' | |||
| href={docLink} | |||
| target='_blank' | |||
| rel='noopener noreferrer' | |||
| > | |||
| <RiBookOpenLine className='size-3.5 shrink-0' /> | |||
| <span className='grow truncate' title={docTitle}>{docTitle}</span> | |||
| <span title={docTitle}>{docTitle}</span> | |||
| </a> | |||
| </div> | |||
| ) | |||
| @@ -13,8 +13,8 @@ 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' | |||
| import { useGetDataSourceAuth } from '@/service/use-datasource' | |||
| import Loading from '@/app/components/base/loading' | |||
| type OnlineDocumentsProps = { | |||
| isInPipeline?: boolean | |||
| @@ -34,12 +34,20 @@ const OnlineDocuments = ({ | |||
| searchValue, | |||
| selectedPagesId, | |||
| currentWorkspaceId, | |||
| currentCredentialId, | |||
| } = useDataSourceStoreWithSelector(useShallow(state => ({ | |||
| documentsData: state.documentsData, | |||
| searchValue: state.searchValue, | |||
| selectedPagesId: state.selectedPagesId, | |||
| currentWorkspaceId: state.currentWorkspaceId, | |||
| currentCredentialId: state.currentCredentialId, | |||
| }))) | |||
| const { data: dataSourceAuth } = useGetDataSourceAuth({ | |||
| pluginId: nodeData.plugin_id, | |||
| provider: nodeData.provider_name, | |||
| }) | |||
| const dataSourceStore = useDataSourceStore() | |||
| const PagesMapAndSelectedPagesId: DataSourceNotionPageMap = useMemo(() => { | |||
| @@ -137,29 +145,16 @@ const OnlineDocuments = ({ | |||
| }) | |||
| }, [setShowAccountSettingModal]) | |||
| if (!documentsData?.length) | |||
| return null | |||
| return ( | |||
| <div className='flex flex-col gap-y-2'> | |||
| <Header | |||
| // todo: delete mock data | |||
| docTitle='How to use?' | |||
| 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, | |||
| }]} | |||
| currentCredentialId={currentCredentialId} | |||
| onCredentialChange={dataSourceStore.getState().setCurrentCredentialId} | |||
| credentials={dataSourceAuth?.result || []} | |||
| /> | |||
| <div className='rounded-xl border border-components-panel-border bg-background-default-subtle'> | |||
| <div className='flex items-center gap-x-2 rounded-t-xl border-b border-b-divider-regular bg-components-panel-bg p-1 pl-3'> | |||
| @@ -172,18 +167,24 @@ const OnlineDocuments = ({ | |||
| /> | |||
| </div> | |||
| <div className='overflow-hidden rounded-b-xl'> | |||
| <PageSelector | |||
| checkedIds={selectedPagesId} | |||
| disabledValue={new Set()} | |||
| searchValue={searchValue} | |||
| list={currentWorkspace?.pages || []} | |||
| pagesMap={PagesMapAndSelectedPagesId} | |||
| onSelect={handleSelectPages} | |||
| canPreview={!isInPipeline} | |||
| onPreview={handlePreviewPage} | |||
| isMultipleChoice={!isInPipeline} | |||
| currentWorkspaceId={currentWorkspaceId} | |||
| /> | |||
| {documentsData?.length ? ( | |||
| <PageSelector | |||
| checkedIds={selectedPagesId} | |||
| disabledValue={new Set()} | |||
| searchValue={searchValue} | |||
| list={currentWorkspace?.pages || []} | |||
| pagesMap={PagesMapAndSelectedPagesId} | |||
| onSelect={handleSelectPages} | |||
| canPreview={!isInPipeline} | |||
| onPreview={handlePreviewPage} | |||
| isMultipleChoice={!isInPipeline} | |||
| currentWorkspaceId={currentWorkspaceId} | |||
| /> | |||
| ) : ( | |||
| <div className='flex h-[296px] items-center justify-center'> | |||
| <Loading type='app' /> | |||
| </div> | |||
| )} | |||
| </div> | |||
| </div> | |||
| </div> | |||
| @@ -12,12 +12,11 @@ import { createWebsiteCrawlSlice } from './slices/website-crawl' | |||
| import type { OnlineDriveSliceShape } from './slices/online-drive' | |||
| import { createOnlineDriveSlice } from './slices/online-drive' | |||
| export type DataSourceShape = | |||
| CommonShape & | |||
| LocalFileSliceShape & | |||
| OnlineDocumentSliceShape & | |||
| WebsiteCrawlSliceShape & | |||
| OnlineDriveSliceShape | |||
| export type DataSourceShape = CommonShape | |||
| & LocalFileSliceShape | |||
| & OnlineDocumentSliceShape | |||
| & WebsiteCrawlSliceShape | |||
| & OnlineDriveSliceShape | |||
| export const createDataSourceStore = () => { | |||
| return createStore<DataSourceShape>((...args) => ({ | |||
| @@ -14,7 +14,7 @@ type DataSourceProviderProps = { | |||
| const DataSourceProvider = ({ | |||
| children, | |||
| }: DataSourceProviderProps) => { | |||
| const storeRef = useRef<DataSourceStoreApi>() | |||
| const storeRef = useRef<DataSourceStoreApi>(null) | |||
| if (!storeRef.current) | |||
| storeRef.current = createDataSourceStore() | |||
| @@ -1,11 +1,19 @@ | |||
| import type { StateCreator } from 'zustand' | |||
| export type CommonShape = { | |||
| currentNodeIdRef: React.MutableRefObject<string | undefined> | |||
| currentNodeIdRef: React.RefObject<string> | |||
| currentCredentialId: string | |||
| setCurrentCredentialId: (credentialId: string) => void | |||
| currentCredentialIdRef: React.RefObject<string> | |||
| } | |||
| export const createCommonSlice: StateCreator<CommonShape> = () => { | |||
| export const createCommonSlice: StateCreator<CommonShape> = (set) => { | |||
| return ({ | |||
| currentNodeIdRef: { current: undefined }, | |||
| currentNodeIdRef: { current: '' }, | |||
| currentCredentialId: '', | |||
| setCurrentCredentialId: (credentialId: string) => { | |||
| set({ currentCredentialId: credentialId }) | |||
| }, | |||
| currentCredentialIdRef: { current: '' }, | |||
| }) | |||
| } | |||
| @@ -6,7 +6,7 @@ export type LocalFileSliceShape = { | |||
| setLocalFileList: (fileList: FileItem[]) => void | |||
| currentLocalFile: File | undefined | |||
| setCurrentLocalFile: (file: File | undefined) => void | |||
| previewLocalFileRef: React.MutableRefObject<DocumentItem | undefined> | |||
| previewLocalFileRef: React.RefObject<DocumentItem | undefined> | |||
| } | |||
| export const createLocalFileSlice: StateCreator<LocalFileSliceShape> = (set, get) => { | |||
| @@ -14,7 +14,7 @@ export type OnlineDocumentSliceShape = { | |||
| setCurrentDocument: (document: NotionPage | undefined) => void | |||
| selectedPagesId: Set<string> | |||
| setSelectedPagesId: (selectedPagesId: Set<string>) => void | |||
| previewOnlineDocumentRef: React.MutableRefObject<NotionPage | undefined> | |||
| previewOnlineDocumentRef: React.RefObject<NotionPage | undefined> | |||
| } | |||
| export const createOnlineDocumentSlice: StateCreator<OnlineDocumentSliceShape> = (set, get) => { | |||
| @@ -12,9 +12,9 @@ export type OnlineDriveSliceShape = { | |||
| setFileList: (fileList: OnlineDriveFile[]) => void | |||
| bucket: string | |||
| setBucket: (bucket: string) => void | |||
| startAfter: React.MutableRefObject<string> | |||
| isTruncated: React.MutableRefObject<boolean> | |||
| previewOnlineDriveFileRef: React.MutableRefObject<OnlineDriveFile | undefined> | |||
| startAfter: React.RefObject<string> | |||
| isTruncated: React.RefObject<boolean> | |||
| previewOnlineDriveFileRef: React.RefObject<OnlineDriveFile | undefined> | |||
| } | |||
| export const createOnlineDriveSlice: StateCreator<OnlineDriveSliceShape> = (set, get) => { | |||
| @@ -13,7 +13,7 @@ export type WebsiteCrawlSliceShape = { | |||
| setStep: (step: CrawlStep) => void | |||
| previewIndex: number | |||
| setPreviewIndex: (index: number) => void | |||
| previewWebsitePageRef: React.MutableRefObject<CrawlResultItem | undefined> | |||
| previewWebsitePageRef: React.RefObject<CrawlResultItem | undefined> | |||
| } | |||
| export const createWebsiteCrawlSlice: StateCreator<WebsiteCrawlSliceShape> = (set, get) => { | |||
| @@ -88,7 +88,6 @@ const DocumentDetail: FC<DocumentDetailProps> = ({ datasetId, documentId }) => { | |||
| documentId, | |||
| params: { metadata: 'without' }, | |||
| }) | |||
| console.log('🚀 ~ DocumentDetail ~ documentDetail:', documentDetail) | |||
| const { data: documentMetadata } = useDocumentMetadata({ | |||
| datasetId, | |||
| @@ -43,7 +43,10 @@ const Card = ({ | |||
| category: AuthCategory.datasource, | |||
| provider: `${item.plugin_id}/${item.name}`, | |||
| } | |||
| const { handleAuthUpdate } = useDataSourceAuthUpdate() | |||
| const { handleAuthUpdate } = useDataSourceAuthUpdate({ | |||
| pluginId: item.plugin_id, | |||
| provider: item.name, | |||
| }) | |||
| const { | |||
| deleteCredentialId, | |||
| doingAction, | |||
| @@ -1,17 +1,28 @@ | |||
| import { useCallback } from 'react' | |||
| import { useInvalidDataSourceListAuth } from '@/service/use-datasource' | |||
| import { useInvalidDataSourceAuth, useInvalidDataSourceListAuth } from '@/service/use-datasource' | |||
| import { useInvalidDefaultDataSourceListAuth } from '@/service/use-datasource' | |||
| import { useInvalidDataSourceList } from '@/service/use-pipeline' | |||
| export const useDataSourceAuthUpdate = () => { | |||
| export const useDataSourceAuthUpdate = ({ | |||
| pluginId, | |||
| provider, | |||
| }: { | |||
| pluginId: string | |||
| provider: string | |||
| }) => { | |||
| const invalidateDataSourceListAuth = useInvalidDataSourceListAuth() | |||
| const invalidDefaultDataSourceListAuth = useInvalidDefaultDataSourceListAuth() | |||
| const invalidateDataSourceList = useInvalidDataSourceList() | |||
| const invalidateDataSourceAuth = useInvalidDataSourceAuth({ | |||
| pluginId, | |||
| provider, | |||
| }) | |||
| const handleAuthUpdate = useCallback(() => { | |||
| invalidateDataSourceListAuth() | |||
| invalidDefaultDataSourceListAuth() | |||
| invalidateDataSourceList() | |||
| }, [invalidateDataSourceListAuth, invalidateDataSourceList]) | |||
| invalidateDataSourceAuth() | |||
| }, [invalidateDataSourceListAuth, invalidateDataSourceList, invalidateDataSourceAuth, invalidDefaultDataSourceListAuth]) | |||
| return { | |||
| handleAuthUpdate, | |||
| @@ -4,7 +4,10 @@ import { | |||
| } from '@tanstack/react-query' | |||
| import { get } from './base' | |||
| import { useInvalid } from './use-base' | |||
| import type { DataSourceAuth } from '@/app/components/header/account-setting/data-source-page-new/types' | |||
| import type { | |||
| DataSourceAuth, | |||
| DataSourceCredential, | |||
| } from '@/app/components/header/account-setting/data-source-page-new/types' | |||
| const NAME_SPACE = 'data-source-auth' | |||
| @@ -34,6 +37,7 @@ export const useInvalidDefaultDataSourceListAuth = ( | |||
| ) => { | |||
| return useInvalid([NAME_SPACE, 'default-list']) | |||
| } | |||
| export const useGetDataSourceOAuthUrl = ( | |||
| provider: string, | |||
| ) => { | |||
| @@ -41,11 +45,35 @@ export const useGetDataSourceOAuthUrl = ( | |||
| mutationKey: [NAME_SPACE, 'oauth-url', provider], | |||
| mutationFn: (credentialId?: string) => { | |||
| return get< | |||
| { | |||
| authorization_url: string | |||
| state: string | |||
| context_id: string | |||
| }>(`/oauth/plugin/${provider}/datasource/get-authorization-url?credential_id=${credentialId}`) | |||
| { | |||
| authorization_url: string | |||
| state: string | |||
| context_id: string | |||
| }>(`/oauth/plugin/${provider}/datasource/get-authorization-url?credential_id=${credentialId}`) | |||
| }, | |||
| }) | |||
| } | |||
| export const useGetDataSourceAuth = ({ | |||
| pluginId, | |||
| provider, | |||
| }: { | |||
| pluginId: string | |||
| provider: string | |||
| }) => { | |||
| return useQuery({ | |||
| queryKey: [NAME_SPACE, 'specific-data-source', pluginId, provider], | |||
| queryFn: () => get<{ result: DataSourceCredential[] }>(`/auth/plugin/datasource/${pluginId}/${provider}`), | |||
| retry: 0, | |||
| }) | |||
| } | |||
| export const useInvalidDataSourceAuth = ({ | |||
| pluginId, | |||
| provider, | |||
| }: { | |||
| pluginId: string | |||
| provider: string | |||
| }) => { | |||
| return useInvalid([NAME_SPACE, 'specific-data-source', pluginId, provider]) | |||
| } | |||