Browse Source

feat: add CredentialIcon component and integrate it into credential selector for improved avatar display

tags/2.0.0-beta.1
twwu 2 months ago
parent
commit
5729d38776
17 changed files with 198 additions and 77 deletions
  1. 53
    0
      web/app/components/datasets/common/credential-icon.tsx
  2. 9
    2
      web/app/components/datasets/documents/create-from-pipeline/data-source/base/credential-selector/index.tsx
  3. 7
    2
      web/app/components/datasets/documents/create-from-pipeline/data-source/base/credential-selector/item.tsx
  4. 18
    11
      web/app/components/datasets/documents/create-from-pipeline/data-source/base/credential-selector/trigger.tsx
  5. 5
    5
      web/app/components/datasets/documents/create-from-pipeline/data-source/base/header.tsx
  6. 31
    30
      web/app/components/datasets/documents/create-from-pipeline/data-source/online-documents/index.tsx
  7. 5
    6
      web/app/components/datasets/documents/create-from-pipeline/data-source/store/index.ts
  8. 1
    1
      web/app/components/datasets/documents/create-from-pipeline/data-source/store/provider.tsx
  9. 11
    3
      web/app/components/datasets/documents/create-from-pipeline/data-source/store/slices/common.ts
  10. 1
    1
      web/app/components/datasets/documents/create-from-pipeline/data-source/store/slices/local-file.ts
  11. 1
    1
      web/app/components/datasets/documents/create-from-pipeline/data-source/store/slices/online-document.ts
  12. 3
    3
      web/app/components/datasets/documents/create-from-pipeline/data-source/store/slices/online-drive.ts
  13. 1
    1
      web/app/components/datasets/documents/create-from-pipeline/data-source/store/slices/website-crawl.ts
  14. 0
    1
      web/app/components/datasets/documents/detail/index.tsx
  15. 4
    1
      web/app/components/header/account-setting/data-source-page-new/card.tsx
  16. 14
    3
      web/app/components/header/account-setting/data-source-page-new/hooks/use-data-source-auth-update.ts
  17. 34
    6
      web/service/use-datasource.ts

+ 53
- 0
web/app/components/datasets/common/credential-icon.tsx View File

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>
)
}

+ 9
- 2
web/app/components/datasets/documents/create-from-pipeline/data-source/base/credential-selector/index.tsx View File

import React, { useCallback } from 'react'
import React, { useCallback, useEffect, useMemo } from 'react'
import { import {
PortalToFollowElem, PortalToFollowElem,
PortalToFollowElemContent, PortalToFollowElemContent,
}: CredentialSelectorProps) => { }: CredentialSelectorProps) => {
const [open, { toggle }] = useBoolean(false) 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) => { const handleCredentialChange = useCallback((credentialId: string) => {
onCredentialChange(credentialId) onCredentialChange(credentialId)

+ 7
- 2
web/app/components/datasets/documents/create-from-pipeline/data-source/base/credential-selector/item.tsx View File

import { CredentialIcon } from '@/app/components/datasets/common/credential-icon'
import type { DataSourceCredential } from '@/types/pipeline' import type { DataSourceCredential } from '@/types/pipeline'
import { RiCheckLine } from '@remixicon/react' import { RiCheckLine } from '@remixicon/react'
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
className='flex cursor-pointer items-center gap-x-2 rounded-lg p-2 hover:bg-state-base-hover' className='flex cursor-pointer items-center gap-x-2 rounded-lg p-2 hover:bg-state-base-hover'
onClick={handleCredentialChange} 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', { {t('datasetPipeline.credentialSelector.name', {
credentialName: name, credentialName: name,
pluginName, pluginName,

+ 18
- 11
web/app/components/datasets/documents/create-from-pipeline/data-source/base/credential-selector/trigger.tsx View File

import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { RiArrowDownSLine } from '@remixicon/react' import { RiArrowDownSLine } from '@remixicon/react'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { CredentialIcon } from '@/app/components/datasets/common/credential-icon'


type TriggerProps = { type TriggerProps = {
currentCredential: DataSourceCredential
currentCredential: DataSourceCredential | undefined
pluginName: string pluginName: string
isOpen: boolean isOpen: boolean
} }


const { const {
avatar_url, avatar_url,
name,
} = currentCredential
name = '',
} = currentCredential || {}


return ( 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', { {t('datasetPipeline.credentialSelector.name', {
credentialName: name, credentialName: name,
pluginName, pluginName,
})} })}
</span> </span>
<RiArrowDownSLine className='size-4 text-text-secondary' />
<RiArrowDownSLine className='size-4 shrink-0 text-text-secondary' />
</div> </div>
</div> </div>
) )

+ 5
- 5
web/app/components/datasets/documents/create-from-pipeline/data-source/base/header.tsx View File



return ( return (
<div className='flex items-center gap-x-2'> <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 <CredentialSelector
{...rest} {...rest}
/> />
<Divider type='vertical' className='mx-1 h-3.5' />
<Divider type='vertical' className='mx-1 h-3.5 shrink-0' />
<Tooltip <Tooltip
popupContent={t('datasetPipeline.configurationTip', { pluginName: rest.pluginName })} popupContent={t('datasetPipeline.configurationTip', { pluginName: rest.pluginName })}
position='top' position='top'
<Button <Button
variant='ghost' variant='ghost'
size='small' size='small'
className='size-6 px-1'
className='size-6 shrink-0 px-1'
> >
<RiEqualizer2Line <RiEqualizer2Line
className='h-4 w-4' className='h-4 w-4'
</Tooltip> </Tooltip>
</div> </div>
<a <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} href={docLink}
target='_blank' target='_blank'
rel='noopener noreferrer' rel='noopener noreferrer'
> >
<RiBookOpenLine className='size-3.5 shrink-0' /> <RiBookOpenLine className='size-3.5 shrink-0' />
<span className='grow truncate' title={docTitle}>{docTitle}</span>
<span title={docTitle}>{docTitle}</span>
</a> </a>
</div> </div>
) )

+ 31
- 30
web/app/components/datasets/documents/create-from-pipeline/data-source/online-documents/index.tsx View File

import { useShallow } from 'zustand/react/shallow' import { useShallow } from 'zustand/react/shallow'
import { useModalContextSelector } from '@/context/modal-context' import { useModalContextSelector } from '@/context/modal-context'
import Title from './title' 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 = { type OnlineDocumentsProps = {
isInPipeline?: boolean isInPipeline?: boolean
searchValue, searchValue,
selectedPagesId, selectedPagesId,
currentWorkspaceId, currentWorkspaceId,
currentCredentialId,
} = useDataSourceStoreWithSelector(useShallow(state => ({ } = useDataSourceStoreWithSelector(useShallow(state => ({
documentsData: state.documentsData, documentsData: state.documentsData,
searchValue: state.searchValue, searchValue: state.searchValue,
selectedPagesId: state.selectedPagesId, selectedPagesId: state.selectedPagesId,
currentWorkspaceId: state.currentWorkspaceId, currentWorkspaceId: state.currentWorkspaceId,
currentCredentialId: state.currentCredentialId,
}))) })))

const { data: dataSourceAuth } = useGetDataSourceAuth({
pluginId: nodeData.plugin_id,
provider: nodeData.provider_name,
})

const dataSourceStore = useDataSourceStore() const dataSourceStore = useDataSourceStore()


const PagesMapAndSelectedPagesId: DataSourceNotionPageMap = useMemo(() => { const PagesMapAndSelectedPagesId: DataSourceNotionPageMap = useMemo(() => {
}) })
}, [setShowAccountSettingModal]) }, [setShowAccountSettingModal])


if (!documentsData?.length)
return null

return ( return (
<div className='flex flex-col gap-y-2'> <div className='flex flex-col gap-y-2'>
<Header <Header
// todo: delete mock data
docTitle='How to use?' docTitle='How to use?'
docLink='https://docs.dify.ai' docLink='https://docs.dify.ai'
onClickConfiguration={handleSetting} onClickConfiguration={handleSetting}
pluginName={nodeData.datasource_label} 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='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'> <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'>
/> />
</div> </div>
<div className='overflow-hidden rounded-b-xl'> <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> </div>
</div> </div>

+ 5
- 6
web/app/components/datasets/documents/create-from-pipeline/data-source/store/index.ts View File

import type { OnlineDriveSliceShape } from './slices/online-drive' import type { OnlineDriveSliceShape } from './slices/online-drive'
import { createOnlineDriveSlice } 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 = () => { export const createDataSourceStore = () => {
return createStore<DataSourceShape>((...args) => ({ return createStore<DataSourceShape>((...args) => ({

+ 1
- 1
web/app/components/datasets/documents/create-from-pipeline/data-source/store/provider.tsx View File

const DataSourceProvider = ({ const DataSourceProvider = ({
children, children,
}: DataSourceProviderProps) => { }: DataSourceProviderProps) => {
const storeRef = useRef<DataSourceStoreApi>()
const storeRef = useRef<DataSourceStoreApi>(null)


if (!storeRef.current) if (!storeRef.current)
storeRef.current = createDataSourceStore() storeRef.current = createDataSourceStore()

+ 11
- 3
web/app/components/datasets/documents/create-from-pipeline/data-source/store/slices/common.ts View File

import type { StateCreator } from 'zustand' import type { StateCreator } from 'zustand'


export type CommonShape = { 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 ({ return ({
currentNodeIdRef: { current: undefined },
currentNodeIdRef: { current: '' },
currentCredentialId: '',
setCurrentCredentialId: (credentialId: string) => {
set({ currentCredentialId: credentialId })
},
currentCredentialIdRef: { current: '' },
}) })
} }

+ 1
- 1
web/app/components/datasets/documents/create-from-pipeline/data-source/store/slices/local-file.ts View File

setLocalFileList: (fileList: FileItem[]) => void setLocalFileList: (fileList: FileItem[]) => void
currentLocalFile: File | undefined currentLocalFile: File | undefined
setCurrentLocalFile: (file: File | undefined) => void setCurrentLocalFile: (file: File | undefined) => void
previewLocalFileRef: React.MutableRefObject<DocumentItem | undefined>
previewLocalFileRef: React.RefObject<DocumentItem | undefined>
} }


export const createLocalFileSlice: StateCreator<LocalFileSliceShape> = (set, get) => { export const createLocalFileSlice: StateCreator<LocalFileSliceShape> = (set, get) => {

+ 1
- 1
web/app/components/datasets/documents/create-from-pipeline/data-source/store/slices/online-document.ts View File

setCurrentDocument: (document: NotionPage | undefined) => void setCurrentDocument: (document: NotionPage | undefined) => void
selectedPagesId: Set<string> selectedPagesId: Set<string>
setSelectedPagesId: (selectedPagesId: Set<string>) => void setSelectedPagesId: (selectedPagesId: Set<string>) => void
previewOnlineDocumentRef: React.MutableRefObject<NotionPage | undefined>
previewOnlineDocumentRef: React.RefObject<NotionPage | undefined>
} }


export const createOnlineDocumentSlice: StateCreator<OnlineDocumentSliceShape> = (set, get) => { export const createOnlineDocumentSlice: StateCreator<OnlineDocumentSliceShape> = (set, get) => {

+ 3
- 3
web/app/components/datasets/documents/create-from-pipeline/data-source/store/slices/online-drive.ts View File

setFileList: (fileList: OnlineDriveFile[]) => void setFileList: (fileList: OnlineDriveFile[]) => void
bucket: string bucket: string
setBucket: (bucket: string) => void 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) => { export const createOnlineDriveSlice: StateCreator<OnlineDriveSliceShape> = (set, get) => {

+ 1
- 1
web/app/components/datasets/documents/create-from-pipeline/data-source/store/slices/website-crawl.ts View File

setStep: (step: CrawlStep) => void setStep: (step: CrawlStep) => void
previewIndex: number previewIndex: number
setPreviewIndex: (index: number) => void setPreviewIndex: (index: number) => void
previewWebsitePageRef: React.MutableRefObject<CrawlResultItem | undefined>
previewWebsitePageRef: React.RefObject<CrawlResultItem | undefined>
} }


export const createWebsiteCrawlSlice: StateCreator<WebsiteCrawlSliceShape> = (set, get) => { export const createWebsiteCrawlSlice: StateCreator<WebsiteCrawlSliceShape> = (set, get) => {

+ 0
- 1
web/app/components/datasets/documents/detail/index.tsx View File

documentId, documentId,
params: { metadata: 'without' }, params: { metadata: 'without' },
}) })
console.log('🚀 ~ DocumentDetail ~ documentDetail:', documentDetail)


const { data: documentMetadata } = useDocumentMetadata({ const { data: documentMetadata } = useDocumentMetadata({
datasetId, datasetId,

+ 4
- 1
web/app/components/header/account-setting/data-source-page-new/card.tsx View File

category: AuthCategory.datasource, category: AuthCategory.datasource,
provider: `${item.plugin_id}/${item.name}`, provider: `${item.plugin_id}/${item.name}`,
} }
const { handleAuthUpdate } = useDataSourceAuthUpdate()
const { handleAuthUpdate } = useDataSourceAuthUpdate({
pluginId: item.plugin_id,
provider: item.name,
})
const { const {
deleteCredentialId, deleteCredentialId,
doingAction, doingAction,

+ 14
- 3
web/app/components/header/account-setting/data-source-page-new/hooks/use-data-source-auth-update.ts View File

import { useCallback } from 'react' import { useCallback } from 'react'
import { useInvalidDataSourceListAuth } from '@/service/use-datasource'
import { useInvalidDataSourceAuth, useInvalidDataSourceListAuth } from '@/service/use-datasource'
import { useInvalidDefaultDataSourceListAuth } from '@/service/use-datasource' import { useInvalidDefaultDataSourceListAuth } from '@/service/use-datasource'
import { useInvalidDataSourceList } from '@/service/use-pipeline' import { useInvalidDataSourceList } from '@/service/use-pipeline'


export const useDataSourceAuthUpdate = () => {
export const useDataSourceAuthUpdate = ({
pluginId,
provider,
}: {
pluginId: string
provider: string
}) => {
const invalidateDataSourceListAuth = useInvalidDataSourceListAuth() const invalidateDataSourceListAuth = useInvalidDataSourceListAuth()
const invalidDefaultDataSourceListAuth = useInvalidDefaultDataSourceListAuth() const invalidDefaultDataSourceListAuth = useInvalidDefaultDataSourceListAuth()
const invalidateDataSourceList = useInvalidDataSourceList() const invalidateDataSourceList = useInvalidDataSourceList()
const invalidateDataSourceAuth = useInvalidDataSourceAuth({
pluginId,
provider,
})
const handleAuthUpdate = useCallback(() => { const handleAuthUpdate = useCallback(() => {
invalidateDataSourceListAuth() invalidateDataSourceListAuth()
invalidDefaultDataSourceListAuth() invalidDefaultDataSourceListAuth()
invalidateDataSourceList() invalidateDataSourceList()
}, [invalidateDataSourceListAuth, invalidateDataSourceList])
invalidateDataSourceAuth()
}, [invalidateDataSourceListAuth, invalidateDataSourceList, invalidateDataSourceAuth, invalidDefaultDataSourceListAuth])


return { return {
handleAuthUpdate, handleAuthUpdate,

+ 34
- 6
web/service/use-datasource.ts View File

} from '@tanstack/react-query' } from '@tanstack/react-query'
import { get } from './base' import { get } from './base'
import { useInvalid } from './use-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' const NAME_SPACE = 'data-source-auth'


) => { ) => {
return useInvalid([NAME_SPACE, 'default-list']) return useInvalid([NAME_SPACE, 'default-list'])
} }

export const useGetDataSourceOAuthUrl = ( export const useGetDataSourceOAuthUrl = (
provider: string, provider: string,
) => { ) => {
mutationKey: [NAME_SPACE, 'oauth-url', provider], mutationKey: [NAME_SPACE, 'oauth-url', provider],
mutationFn: (credentialId?: string) => { mutationFn: (credentialId?: string) => {
return get< 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])
}

Loading…
Cancel
Save