mirror of https://github.com/langgenius/dify.git
feat: add CredentialIcon component and integrate it into credential selector for improved avatar display
This commit is contained in:
parent
9e882122ca
commit
5729d38776
|
|
@ -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])
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue