mirror of
https://github.com/langgenius/dify.git
synced 2026-09-07 18:36:02 +08:00
fix(knowledge-fs): show installed datasource providers
This commit is contained in:
parent
523844a774
commit
2ad99bf5cd
@ -1184,7 +1184,7 @@ describe('CreateKnowledgePage', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('enables every atomic source type and distinguishes installed providers', async () => {
|
||||
it('enables every atomic source type and only lists installed providers', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderPage()
|
||||
|
||||
@ -1208,12 +1208,13 @@ describe('CreateKnowledgePage', () => {
|
||||
expect(onlineDocuments).toBeEnabled()
|
||||
expect(screen.getByRole('radio', { name: 'dataset.newKnowledge.onlineDrive' })).toBeEnabled()
|
||||
expect(screen.getByRole('radio', { name: 'Firecrawl' })).toBeChecked()
|
||||
expect(screen.getByRole('radio', { name: 'Jina Reader' })).toBeEnabled()
|
||||
expect(screen.getByRole('radio', { name: 'WaterCrawl' })).toBeEnabled()
|
||||
expect(screen.queryByRole('radio', { name: 'Jina Reader' })).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('radio', { name: 'WaterCrawl' })).not.toBeInTheDocument()
|
||||
await user.click(onlineDocuments)
|
||||
expect(onlineDocuments).toBeChecked()
|
||||
expect(screen.getByRole('radio', { name: 'Notion' })).toBeChecked()
|
||||
expect(screen.getByText('workflow.nodes.common.pluginNotInstalled')).toBeInTheDocument()
|
||||
expect(screen.queryByRole('radio', { name: 'Notion' })).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('status')).toHaveTextContent('plugin.list.notFound')
|
||||
expect(screen.queryByText('workflow.nodes.common.pluginNotInstalled')).not.toBeInTheDocument()
|
||||
await user.click(screen.getByRole('radio', { name: 'dataset.newKnowledge.websiteCrawl' }))
|
||||
expect(screen.getByRole('button', { name: 'dataset.newKnowledge.moreProviders' })).toBeEnabled()
|
||||
expect(screen.queryByRole('alert')).not.toBeInTheDocument()
|
||||
@ -1253,6 +1254,54 @@ describe('CreateKnowledgePage', () => {
|
||||
expect(screen.getByRole('button', { name: 'dataset.newKnowledge.createTitle' })).toBeDisabled()
|
||||
})
|
||||
|
||||
it('prompts for provider installation when source setup has no installed integration', async () => {
|
||||
const user = userEvent.setup()
|
||||
datasourceQueryMock.plugins.data = []
|
||||
datasourceQueryMock.auth.data = { result: [] }
|
||||
|
||||
renderPage()
|
||||
|
||||
await user.click(screen.getByRole('radio', { name: 'dataset.newKnowledge.connectSource' }))
|
||||
|
||||
expect(screen.getByRole('status')).toHaveTextContent('plugin.list.notFound')
|
||||
expect(screen.queryByRole('radio', { name: 'Firecrawl' })).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'dataset.newKnowledge.moreProviders' })).toBeEnabled()
|
||||
})
|
||||
|
||||
it('clears a completed crawl when the installed provider changes during setup', async () => {
|
||||
const user = userEvent.setup()
|
||||
datasourceQueryMock.plugins.data = [firecrawlDatasourcePlugin, jinaDatasourcePlugin]
|
||||
datasourceQueryMock.auth.data = { result: [firecrawlDatasourceAuth, jinaDatasourceAuth] }
|
||||
const view = renderPage()
|
||||
|
||||
await fillRequiredFields(user)
|
||||
await user.click(screen.getByRole('radio', { name: 'dataset.newKnowledge.connectSource' }))
|
||||
await user.type(
|
||||
screen.getByPlaceholderText('dataset.newKnowledge.rootUrlPlaceholder'),
|
||||
'https://docs.dify.ai',
|
||||
)
|
||||
await user.type(
|
||||
screen.getByPlaceholderText('dataset.newKnowledge.sourceNamePlaceholder'),
|
||||
'Dify docs',
|
||||
)
|
||||
await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.crawlAndPreview' }))
|
||||
await user.click(await screen.findByRole('checkbox', { name: 'Getting started' }))
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.getByRole('button', { name: 'dataset.newKnowledge.createTitle' }),
|
||||
).toBeEnabled(),
|
||||
)
|
||||
|
||||
datasourceQueryMock.plugins.data = [jinaDatasourcePlugin]
|
||||
datasourceQueryMock.auth.data = { result: [jinaDatasourceAuth] }
|
||||
view.rerender(<CreateKnowledgePage />)
|
||||
|
||||
expect(screen.getByRole('radio', { name: 'Jina Reader' })).toBeChecked()
|
||||
expect(screen.queryByText('Getting started')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('dataset.newKnowledge.pagesAppearTitle')).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'dataset.newKnowledge.createTitle' })).toBeDisabled()
|
||||
})
|
||||
|
||||
it('disables upload before creating a space when direct upload is unavailable', () => {
|
||||
navigationMock.startMode = 'upload'
|
||||
systemFeaturesStateMock.uploadEnabled = false
|
||||
|
||||
@ -21,8 +21,6 @@ import { CrawlPreviewPageSelection } from '../sources/setup/crawl-selection'
|
||||
import { WebsiteDatasourceParameterForm } from '../sources/setup/datasource-parameter-form'
|
||||
import {
|
||||
datasourceIncludeSubpages,
|
||||
datasourceParameterDefaults,
|
||||
datasourceParameterSchemas,
|
||||
invalidDatasourceParameters,
|
||||
missingRequiredDatasourceParameters,
|
||||
websiteDatasourceParameterSchemas,
|
||||
@ -31,14 +29,15 @@ import {
|
||||
import {
|
||||
SourceNameField,
|
||||
SourceProviderCredentialRequiredCard,
|
||||
SourceProviderEmptyState,
|
||||
SourceProviderIcon,
|
||||
SourceProviderNotInstalledCard,
|
||||
SourceProviderRadioGroup,
|
||||
SourceSyncPolicyField,
|
||||
SourceTypeSelector,
|
||||
} from '../sources/setup/fields'
|
||||
import {
|
||||
discoverSourceProviderOptions,
|
||||
sourceDraftForProviderOption,
|
||||
sourceProviderOptionForDraft,
|
||||
} from '../sources/setup/provider-options'
|
||||
|
||||
@ -52,6 +51,13 @@ const CRAWL_POLL_INTERVAL_MS = 1500
|
||||
|
||||
type LocalCrawlState = 'error' | 'idle' | 'running' | 'stopped' | 'success'
|
||||
type InitialSource = NonNullable<KnowledgeFsSpaceCreatePayload['initial_source']>
|
||||
type CreateSourceSetupProps = {
|
||||
disabled: boolean
|
||||
draft: NewKnowledgeSourceDraft
|
||||
onDraftChange: (draft: NewKnowledgeSourceDraft) => void
|
||||
onInitialSourceChange: (source?: InitialSource) => void
|
||||
onSourceTypeChange: (sourceType: NewKnowledgeSourceDraft['sourceType']) => void
|
||||
}
|
||||
|
||||
function datasourceAuthForProvider(
|
||||
authProviders: DataSourceAuth[],
|
||||
@ -90,19 +96,106 @@ function websiteSourceUri(parameters: Record<string, boolean | number | string>,
|
||||
return `datasource://${encodeURIComponent(fallback)}`
|
||||
}
|
||||
|
||||
export function CreateSourceSetup({
|
||||
export function CreateSourceSetup(props: CreateSourceSetupProps) {
|
||||
const { disabled, draft, onDraftChange, onSourceTypeChange } = props
|
||||
const { t } = useTranslation('dataset')
|
||||
const datasourcePluginsQuery = useDataSourceList(true)
|
||||
const datasourceAuthQuery = useGetDataSourceListAuth()
|
||||
const providerOptions = useMemo(
|
||||
() => discoverSourceProviderOptions(draft.sourceType, datasourcePluginsQuery.data ?? []),
|
||||
[datasourcePluginsQuery.data, draft.sourceType],
|
||||
)
|
||||
const providerOption = sourceProviderOptionForDraft(providerOptions, draft)
|
||||
const providerDraft = useMemo(
|
||||
() => (providerOption ? sourceDraftForProviderOption(draft, providerOption) : draft),
|
||||
[draft, providerOption],
|
||||
)
|
||||
const datasourceAuth = providerOption
|
||||
? datasourceAuthForProvider(
|
||||
datasourceAuthQuery.data?.result ?? [],
|
||||
providerOption.plugin.plugin_id,
|
||||
providerOption.plugin.provider,
|
||||
)
|
||||
: undefined
|
||||
const credential = preferredCredential(datasourceAuth)
|
||||
const sessionKey = [
|
||||
draft.sourceType,
|
||||
providerOption?.key ?? 'no-provider',
|
||||
providerOption?.plugin.plugin_unique_identifier ?? 'no-plugin-version',
|
||||
credential?.id ?? 'no-credential',
|
||||
].join(':')
|
||||
|
||||
const selectProvider = (providerKey: string) => {
|
||||
const nextProvider = providerOptions.find((option) => option.key === providerKey)
|
||||
if (!nextProvider) return
|
||||
onDraftChange(sourceDraftForProviderOption(providerDraft, nextProvider))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-4 -mt-1 mb-3.75 flex flex-col gap-4">
|
||||
<SourceTypeSelector
|
||||
appearance="embedded"
|
||||
disabled={disabled}
|
||||
value={draft.sourceType}
|
||||
onChange={onSourceTypeChange}
|
||||
/>
|
||||
|
||||
<Fieldset disabled={disabled}>
|
||||
<FieldsetLegend className="sr-only">
|
||||
{t(($) => $['newKnowledge.providerLabel'])}
|
||||
</FieldsetLegend>
|
||||
<div className="mb-1.5 flex items-center justify-between gap-3">
|
||||
<span className="system-xs-medium text-text-secondary">
|
||||
{t(($) => $['newKnowledge.providerLabel'])}
|
||||
</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost-accent"
|
||||
size="small"
|
||||
disabled={disabled}
|
||||
className="gap-0.5 px-2.75"
|
||||
onClick={() =>
|
||||
globalThis.open(buildIntegrationPath('data-source'), '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
>
|
||||
{t(($) => $['newKnowledge.moreProviders'])}
|
||||
<span aria-hidden className="i-ri-arrow-right-up-line size-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
{providerOptions.length > 0 ? (
|
||||
<SourceProviderRadioGroup
|
||||
value={providerOption?.key ?? ''}
|
||||
disabled={disabled}
|
||||
layout={draft.sourceType === 'websiteCrawl' ? 'grid-four' : 'grid-three'}
|
||||
options={providerOptions.map((option) => ({
|
||||
icon: (
|
||||
<SourceProviderIcon
|
||||
fallbackIcon={option.fallbackIcon}
|
||||
icon={option.datasource.identity.icon ?? option.plugin.declaration.identity.icon}
|
||||
/>
|
||||
),
|
||||
label: option.label,
|
||||
value: option.key,
|
||||
}))}
|
||||
surface="default"
|
||||
onChange={selectProvider}
|
||||
/>
|
||||
) : !datasourcePluginsQuery.isPending && !datasourcePluginsQuery.error ? (
|
||||
<SourceProviderEmptyState className="min-h-16" />
|
||||
) : null}
|
||||
</Fieldset>
|
||||
|
||||
<CreateSourceSetupSession key={sessionKey} {...props} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CreateSourceSetupSession({
|
||||
disabled,
|
||||
draft,
|
||||
onDraftChange,
|
||||
onInitialSourceChange,
|
||||
onSourceTypeChange,
|
||||
}: {
|
||||
disabled: boolean
|
||||
draft: NewKnowledgeSourceDraft
|
||||
onDraftChange: (draft: NewKnowledgeSourceDraft) => void
|
||||
onInitialSourceChange: (source?: InitialSource) => void
|
||||
onSourceTypeChange: (sourceType: NewKnowledgeSourceDraft['sourceType']) => void
|
||||
}) {
|
||||
}: CreateSourceSetupProps) {
|
||||
const { t } = useTranslation('dataset')
|
||||
const datasourcePluginsQuery = useDataSourceList(true)
|
||||
const datasourceAuthQuery = useGetDataSourceListAuth()
|
||||
@ -112,13 +205,16 @@ export function CreateSourceSetup({
|
||||
const [selectedPageIds, setSelectedPageIds] = useState<Set<string>>(() => new Set())
|
||||
const crawlAttemptRef = useRef(0)
|
||||
const previewJobIdRef = useRef<string | undefined>(undefined)
|
||||
const sourceType = draft.sourceType
|
||||
const providerOptions = useMemo(
|
||||
() => discoverSourceProviderOptions(sourceType, datasourcePluginsQuery.data ?? []),
|
||||
[datasourcePluginsQuery.data, sourceType],
|
||||
() => discoverSourceProviderOptions(draft.sourceType, datasourcePluginsQuery.data ?? []),
|
||||
[datasourcePluginsQuery.data, draft.sourceType],
|
||||
)
|
||||
const providerOption = sourceProviderOptionForDraft(providerOptions, draft)
|
||||
const installedProviderOption = providerOption?.installed ? providerOption : undefined
|
||||
const providerDraft = useMemo(
|
||||
() => (providerOption ? sourceDraftForProviderOption(draft, providerOption) : draft),
|
||||
[draft, providerOption],
|
||||
)
|
||||
const installedProviderOption = providerOption
|
||||
const datasourceAuth = installedProviderOption
|
||||
? datasourceAuthForProvider(
|
||||
datasourceAuthQuery.data?.result ?? [],
|
||||
@ -135,26 +231,26 @@ export function CreateSourceSetup({
|
||||
[draft.sourceType, installedProviderOption],
|
||||
)
|
||||
const parameters = useMemo(() => {
|
||||
const current = withDatasourceParameterDefaults(parameterSchemas, draft.parameters)
|
||||
const current = withDatasourceParameterDefaults(parameterSchemas, providerDraft.parameters)
|
||||
if (
|
||||
draft.sourceType === 'websiteCrawl' &&
|
||||
draft.rootUrl &&
|
||||
providerDraft.sourceType === 'websiteCrawl' &&
|
||||
providerDraft.rootUrl &&
|
||||
parameterSchemas.some((parameter) => parameter.name === 'url') &&
|
||||
current.url === undefined
|
||||
)
|
||||
current.url = draft.rootUrl
|
||||
current.url = providerDraft.rootUrl
|
||||
return current
|
||||
}, [draft, parameterSchemas])
|
||||
}, [parameterSchemas, providerDraft])
|
||||
const parametersValid =
|
||||
!missingRequiredDatasourceParameters(parameterSchemas, parameters).length &&
|
||||
!invalidDatasourceParameters(parameterSchemas, parameters).length
|
||||
const selectionPages = previewPages
|
||||
const previewReady = Boolean(
|
||||
draft.sourceType === 'websiteCrawl' &&
|
||||
providerDraft.sourceType === 'websiteCrawl' &&
|
||||
credential &&
|
||||
installedProviderOption &&
|
||||
parametersValid &&
|
||||
draft.sourceName.trim(),
|
||||
providerDraft.sourceName.trim(),
|
||||
)
|
||||
const sourceUri = installedProviderOption
|
||||
? websiteSourceUri(parameters, installedProviderOption.key)
|
||||
@ -227,25 +323,6 @@ export function CreateSourceSetup({
|
||||
const updateDraftWithoutReset = (nextDraft: NewKnowledgeSourceDraft) => {
|
||||
onDraftChange(nextDraft)
|
||||
}
|
||||
const selectProvider = (providerKey: string) => {
|
||||
const nextProvider = providerOptions.find((option) => option.key === providerKey)
|
||||
if (!nextProvider) return
|
||||
updateDraft({
|
||||
...draft,
|
||||
parameters: nextProvider.installed
|
||||
? datasourceParameterDefaults(
|
||||
draft.sourceType === 'websiteCrawl'
|
||||
? websiteDatasourceParameterSchemas(nextProvider.datasource)
|
||||
: datasourceParameterSchemas(nextProvider.datasource),
|
||||
)
|
||||
: {},
|
||||
provider: nextProvider.label,
|
||||
providerKey: nextProvider.key,
|
||||
sourceName: '',
|
||||
...(draft.sourceType === 'websiteCrawl' ? { rootUrl: '' } : {}),
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
crawlAttemptRef.current += 1
|
||||
@ -256,13 +333,14 @@ export function CreateSourceSetup({
|
||||
params: { job_id: jobId },
|
||||
})
|
||||
.catch(() => {})
|
||||
onInitialSourceChange(undefined)
|
||||
},
|
||||
[],
|
||||
[onInitialSourceChange],
|
||||
)
|
||||
|
||||
const startPreview = async () => {
|
||||
if (
|
||||
draft.sourceType !== 'websiteCrawl' ||
|
||||
providerDraft.sourceType !== 'websiteCrawl' ||
|
||||
!previewReady ||
|
||||
!credential ||
|
||||
!installedProviderOption
|
||||
@ -342,7 +420,7 @@ export function CreateSourceSetup({
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (draft.sourceType !== 'websiteCrawl') {
|
||||
if (providerDraft.sourceType !== 'websiteCrawl') {
|
||||
if (!installedProviderOption || !credential) onInitialSourceChange(undefined)
|
||||
return
|
||||
}
|
||||
@ -368,7 +446,7 @@ export function CreateSourceSetup({
|
||||
credentialId: credential.id,
|
||||
datasource: installedProviderOption.datasource.identity.name,
|
||||
kind: 'website_crawl',
|
||||
name: draft.sourceName.trim(),
|
||||
name: providerDraft.sourceName.trim(),
|
||||
pluginId: installedProviderOption.plugin.plugin_id,
|
||||
provider: installedProviderOption.plugin.provider,
|
||||
providerDisplayName: installedProviderOption.label,
|
||||
@ -378,70 +456,25 @@ export function CreateSourceSetup({
|
||||
source_url: page.sourceUrl,
|
||||
...(page.title ? { title: page.title } : {}),
|
||||
})),
|
||||
...(draft.syncPolicy === 'custom' && draft.customIntervalSeconds
|
||||
? { custom_interval_seconds: draft.customIntervalSeconds }
|
||||
...(providerDraft.syncPolicy === 'custom' && providerDraft.customIntervalSeconds
|
||||
? { custom_interval_seconds: providerDraft.customIntervalSeconds }
|
||||
: {}),
|
||||
sync_policy: draft.syncPolicy,
|
||||
sync_policy: providerDraft.syncPolicy,
|
||||
})
|
||||
}, [
|
||||
crawlState,
|
||||
credential,
|
||||
draft,
|
||||
installedProviderOption,
|
||||
onInitialSourceChange,
|
||||
parameters,
|
||||
providerDraft,
|
||||
selectedPageIds,
|
||||
selectionPages,
|
||||
sourceUri,
|
||||
])
|
||||
|
||||
return (
|
||||
<div className="mx-4 -mt-1 mb-3.75 flex flex-col gap-4">
|
||||
<SourceTypeSelector
|
||||
appearance="embedded"
|
||||
disabled={disabled}
|
||||
value={sourceType}
|
||||
onChange={(value) => {
|
||||
onSourceTypeChange(value)
|
||||
}}
|
||||
/>
|
||||
|
||||
<Fieldset disabled={disabled}>
|
||||
<FieldsetLegend className="sr-only">
|
||||
{t(($) => $['newKnowledge.providerLabel'])}
|
||||
</FieldsetLegend>
|
||||
<div className="mb-1.5 flex items-center justify-between gap-3">
|
||||
<span className="system-xs-medium text-text-secondary">
|
||||
{t(($) => $['newKnowledge.providerLabel'])}
|
||||
</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost-accent"
|
||||
size="small"
|
||||
disabled={disabled}
|
||||
className="gap-0.5 px-2.75"
|
||||
onClick={() =>
|
||||
globalThis.open(buildIntegrationPath('data-source'), '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
>
|
||||
{t(($) => $['newKnowledge.moreProviders'])}
|
||||
<span aria-hidden className="i-ri-arrow-right-up-line size-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
<SourceProviderRadioGroup
|
||||
value={providerOption?.key ?? ''}
|
||||
disabled={disabled}
|
||||
layout={sourceType === 'websiteCrawl' ? 'grid-four' : 'grid-three'}
|
||||
options={providerOptions.map((option) => ({
|
||||
icon: <SourceProviderIcon fallbackIcon={option.fallbackIcon} />,
|
||||
label: option.label,
|
||||
value: option.key,
|
||||
}))}
|
||||
surface="default"
|
||||
onChange={selectProvider}
|
||||
/>
|
||||
</Fieldset>
|
||||
|
||||
<>
|
||||
{datasourcePluginsQuery.isPending || datasourceAuthQuery.isPending ? (
|
||||
<div className="flex min-h-44 items-center justify-center">
|
||||
<span aria-hidden className="i-ri-loader-4-line size-5 animate-spin text-text-tertiary" />
|
||||
@ -460,22 +493,18 @@ export function CreateSourceSetup({
|
||||
{t(($) => $['newKnowledge.retryProviderLoad'])}
|
||||
</Button>
|
||||
</div>
|
||||
) : providerOption && !providerOption.installed ? (
|
||||
<SourceProviderNotInstalledCard
|
||||
icon={<SourceProviderIcon fallbackIcon={providerOption.fallbackIcon} />}
|
||||
provider={providerOption.label}
|
||||
onInstall={() =>
|
||||
globalThis.open(
|
||||
providerIntegrationPath(providerOption.packageId),
|
||||
'_blank',
|
||||
'noopener,noreferrer',
|
||||
)
|
||||
}
|
||||
/>
|
||||
) : installedProviderOption && !credential ? (
|
||||
<SourceProviderCredentialRequiredCard
|
||||
disabled={disabled}
|
||||
icon={<SourceProviderIcon fallbackIcon={installedProviderOption.fallbackIcon} />}
|
||||
icon={
|
||||
<SourceProviderIcon
|
||||
fallbackIcon={installedProviderOption.fallbackIcon}
|
||||
icon={
|
||||
installedProviderOption.datasource.identity.icon ??
|
||||
installedProviderOption.plugin.declaration.identity.icon
|
||||
}
|
||||
/>
|
||||
}
|
||||
provider={installedProviderOption.label}
|
||||
onConnect={() =>
|
||||
globalThis.open(
|
||||
@ -485,13 +514,13 @@ export function CreateSourceSetup({
|
||||
)
|
||||
}
|
||||
/>
|
||||
) : draft.sourceType === 'websiteCrawl' && installedProviderOption && credential ? (
|
||||
) : providerDraft.sourceType === 'websiteCrawl' && installedProviderOption && credential ? (
|
||||
<div className="space-y-4">
|
||||
<WebsiteDatasourceParameterForm
|
||||
additionalPrimaryField={
|
||||
<SourceNameField
|
||||
disabled={disabled}
|
||||
draft={draft}
|
||||
draft={providerDraft}
|
||||
preventSubmitOnEnter
|
||||
onDraftChange={updateDraft}
|
||||
/>
|
||||
@ -501,7 +530,7 @@ export function CreateSourceSetup({
|
||||
schemas={parameterSchemas}
|
||||
onChange={(nextParameters) =>
|
||||
updateDraft({
|
||||
...draft,
|
||||
...providerDraft,
|
||||
parameters: nextParameters,
|
||||
rootUrl: typeof nextParameters.url === 'string' ? nextParameters.url : '',
|
||||
})
|
||||
@ -617,16 +646,16 @@ export function CreateSourceSetup({
|
||||
<SourceSyncPolicyField
|
||||
className="w-full sm:w-75.25"
|
||||
disabled={disabled}
|
||||
draft={draft}
|
||||
draft={providerDraft}
|
||||
onDraftChange={updateDraftWithoutReset}
|
||||
size="medium"
|
||||
/>
|
||||
</div>
|
||||
) : draft.sourceType !== 'websiteCrawl' && installedProviderOption && credential ? (
|
||||
) : providerDraft.sourceType !== 'websiteCrawl' && installedProviderOption && credential ? (
|
||||
<ConnectedSourceConfiguration
|
||||
key={`${draft.sourceType}:${installedProviderOption.key}:${credential.id}`}
|
||||
key={`${providerDraft.sourceType}:${installedProviderOption.key}:${credential.id}`}
|
||||
disabled={disabled}
|
||||
draft={draft}
|
||||
draft={providerDraft}
|
||||
previewBinding={{
|
||||
credentialId: credential.id,
|
||||
datasource: installedProviderOption.datasource.identity.name,
|
||||
@ -639,6 +668,6 @@ export function CreateSourceSetup({
|
||||
onInitialSourceChange={onInitialSourceChange}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -631,25 +631,151 @@ describe('ConnectedSourceWorkflow', () => {
|
||||
expect(clientMock.createConnection).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('distinguishes an uninstalled provider from an installed provider without credentials', async () => {
|
||||
const user = userEvent.setup()
|
||||
it('falls back to an installed provider when the requested provider is not installed', async () => {
|
||||
renderSetup({
|
||||
...defaultDraft,
|
||||
provider: 'Confluence',
|
||||
})
|
||||
|
||||
expect(await screen.findByText('workflow.nodes.common.pluginNotInstalled')).toBeInTheDocument()
|
||||
expect(screen.queryByRole('radio', { name: 'Confluence' })).not.toBeInTheDocument()
|
||||
expect(await screen.findByRole('radio', { name: 'Notion' })).toBeChecked()
|
||||
expect(
|
||||
screen.queryByText('dataset.newKnowledge.providerNotConfigured:{"provider":"Notion"}'),
|
||||
).not.toBeInTheDocument()
|
||||
await screen.findByText('dataset.newKnowledge.providerNotConfigured:{"provider":"Notion"}'),
|
||||
).toBeInTheDocument()
|
||||
expect(screen.queryByText('workflow.nodes.common.pluginNotInstalled')).not.toBeInTheDocument()
|
||||
expect(clientMock.createConnection).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'plugin.installPlugin' }))
|
||||
expect(openMock).toHaveBeenCalledWith(
|
||||
'/integrations/data-source?package-ids=%5B%22langgenius%2Fconfluence_datasource%22%5D',
|
||||
'_blank',
|
||||
'noopener,noreferrer',
|
||||
it('uses the fallback provider transport and clears stale provider parameters', async () => {
|
||||
clientMock.listDatasourceAuth.mockResolvedValue({
|
||||
result: [notionDatasourceAuth([notionCredential])],
|
||||
})
|
||||
clientMock.createConnection.mockResolvedValue(connectionResponse())
|
||||
|
||||
renderSetup({
|
||||
...defaultDraft,
|
||||
parameters: { folderId: 'stale-google-folder' },
|
||||
provider: 'Google Docs',
|
||||
sourceName: 'Old Google source',
|
||||
})
|
||||
|
||||
await waitFor(() =>
|
||||
expect(clientMock.createConnection).toHaveBeenCalledWith({
|
||||
body: {
|
||||
authKind: 'endpoint',
|
||||
configuration: {
|
||||
credentialId: notionCredential.id,
|
||||
datasource: 'notion',
|
||||
pluginId: 'langgenius/notion_datasource',
|
||||
provider: 'notion',
|
||||
providerKind: 'online-document',
|
||||
},
|
||||
credentials: {},
|
||||
name: notionCredential.name,
|
||||
providerId: notionProvider.id,
|
||||
},
|
||||
params: { control_space_id: 'space-1' },
|
||||
}),
|
||||
)
|
||||
await waitFor(() =>
|
||||
expect(clientMock.createSource).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
body: expect.objectContaining({
|
||||
metadata: expect.objectContaining({
|
||||
parameters: {},
|
||||
providerKind: 'online-document',
|
||||
providerName: 'Notion',
|
||||
}),
|
||||
name: 'Notion',
|
||||
}),
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
it('prompts for provider installation when no matching integration is installed', async () => {
|
||||
clientMock.listDatasourcePlugins.mockResolvedValue([s3DatasourcePlugin])
|
||||
|
||||
renderSetup()
|
||||
|
||||
const prompt = await screen.findByText('plugin.list.notFound')
|
||||
expect(prompt.closest('[role="status"]')).toBeInTheDocument()
|
||||
expect(screen.queryByRole('radio', { name: 'Notion' })).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('dataset.newKnowledge.providerUnavailable')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('clears a failed connection attempt when a provider refresh selects another integration', async () => {
|
||||
const outlineProvider: KnowledgeFsSourceProviderResponse = {
|
||||
...notionProvider,
|
||||
display_name: 'Outline',
|
||||
id: 'outline-provider',
|
||||
}
|
||||
const outlineDatasourcePlugin: DataSourceItem = {
|
||||
...notionDatasourcePlugin,
|
||||
declaration: {
|
||||
...notionDatasourcePlugin.declaration,
|
||||
datasources: [
|
||||
{
|
||||
...notionDatasourcePlugin.declaration.datasources[0]!,
|
||||
identity: {
|
||||
...notionDatasourcePlugin.declaration.datasources[0]!.identity,
|
||||
label: { en_US: 'Outline', zh_Hans: 'Outline' },
|
||||
name: 'outline',
|
||||
provider: 'outline',
|
||||
},
|
||||
},
|
||||
],
|
||||
identity: {
|
||||
...notionDatasourcePlugin.declaration.identity,
|
||||
label: { en_US: 'Outline', zh_Hans: 'Outline' },
|
||||
name: 'outline',
|
||||
},
|
||||
},
|
||||
plugin_id: 'langgenius/outline_datasource',
|
||||
plugin_unique_identifier: 'langgenius/outline_datasource:1.0.0@local',
|
||||
provider: 'outline',
|
||||
}
|
||||
const outlineDatasourceAuth: DataSourceAuth = {
|
||||
...notionDatasourceAuth(),
|
||||
label: { en_US: 'Outline', zh_Hans: 'Outline' },
|
||||
name: 'outline',
|
||||
plugin_id: outlineDatasourcePlugin.plugin_id,
|
||||
plugin_unique_identifier: outlineDatasourcePlugin.plugin_unique_identifier,
|
||||
provider: outlineDatasourcePlugin.provider,
|
||||
}
|
||||
clientMock.listDatasourceAuth.mockResolvedValue({
|
||||
result: [notionDatasourceAuth([notionCredential])],
|
||||
})
|
||||
clientMock.createConnection.mockRejectedValue(new Error('provider unavailable'))
|
||||
const { queryClient } = renderSetup()
|
||||
|
||||
expect(
|
||||
await screen.findByText('dataset.newKnowledge.connectionFailed:{"provider":"Notion"}'),
|
||||
).toHaveAttribute('role', 'alert')
|
||||
|
||||
clientMock.listProviders.mockResolvedValue({
|
||||
data: [outlineProvider],
|
||||
} satisfies KnowledgeFsSourceProviderListResponse)
|
||||
await act(async () => {
|
||||
queryClient.setQueryData(['pipeline', 'datasource'], [outlineDatasourcePlugin])
|
||||
queryClient.setQueryData(['data-source-auth', 'list'], {
|
||||
result: [outlineDatasourceAuth],
|
||||
})
|
||||
await queryClient.refetchQueries({ queryKey: ['source-providers'] })
|
||||
})
|
||||
|
||||
expect(await screen.findByRole('radio', { name: 'Outline' })).toBeChecked()
|
||||
expect(
|
||||
await screen.findByText('dataset.newKnowledge.providerNotConfigured:{"provider":"Outline"}'),
|
||||
).toBeInTheDocument()
|
||||
expect(
|
||||
screen.queryByText('dataset.newKnowledge.connectionFailed:{"provider":"Outline"}'),
|
||||
).not.toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByRole('button', {
|
||||
name: 'dataset.newKnowledge.connectProvider:{"provider":"Outline"}',
|
||||
}),
|
||||
).toBeEnabled()
|
||||
})
|
||||
|
||||
it('starts the selected Notion import and completes setup without waiting for indexing', async () => {
|
||||
|
||||
@ -727,8 +727,7 @@ describe('AddSourcePage', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps the exact website provider selected while loading website dependencies', async () => {
|
||||
const user = userEvent.setup()
|
||||
it('falls back to an installed website provider when the requested provider is not installed', () => {
|
||||
render(
|
||||
<AddSourcePage
|
||||
initialSourceDraft={{
|
||||
@ -744,28 +743,52 @@ describe('AddSourcePage', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByRole('radio', { name: 'Jina Reader' })).toBeChecked()
|
||||
expect(providerHookOptionsMock.mock.lastCall?.[0]).toMatchObject({ enabled: true })
|
||||
expect(connectionHookOptionsMock.mock.lastCall?.[0]).toMatchObject({ enabled: true })
|
||||
expect(screen.getByText('workflow.nodes.common.pluginNotInstalled')).toBeInTheDocument()
|
||||
await user.click(
|
||||
screen.getByRole('button', {
|
||||
name: 'plugin.installPlugin',
|
||||
}),
|
||||
)
|
||||
expect(openMock).toHaveBeenCalledWith(
|
||||
'/integrations/data-source?package-ids=%5B%22langgenius%2Fjina_datasource%22%5D',
|
||||
'_blank',
|
||||
'noopener,noreferrer',
|
||||
)
|
||||
await user.click(screen.getByRole('radio', { name: 'Firecrawl' }))
|
||||
expect(screen.queryByRole('radio', { name: 'Jina Reader' })).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('radio', { name: 'Firecrawl' })).toBeChecked()
|
||||
expect(providerHookOptionsMock.mock.lastCall?.[0]).toMatchObject({ enabled: true })
|
||||
expect(connectionHookOptionsMock.mock.lastCall?.[0]).toMatchObject({ enabled: true })
|
||||
expect(screen.queryByText('workflow.nodes.common.pluginNotInstalled')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('keeps crawl fields hidden until the selected website provider is configured', async () => {
|
||||
it('clears provider-specific draft fields when falling back to another website provider', async () => {
|
||||
const user = userEvent.setup()
|
||||
queryState.connections.data = { pages: [{ items: [connection('active')] }] }
|
||||
|
||||
render(
|
||||
<AddSourcePage
|
||||
initialSourceDraft={{
|
||||
includeSubpages: false,
|
||||
maxPages: 25,
|
||||
parameters: {
|
||||
crawl_sub_pages: false,
|
||||
jinaOnly: 'stale-value',
|
||||
url: 'https://jina.example.com',
|
||||
},
|
||||
provider: 'Jina Reader',
|
||||
rootUrl: 'https://jina.example.com',
|
||||
sourceName: 'Old Jina source',
|
||||
sourceType: 'websiteCrawl',
|
||||
syncPolicy: 'daily',
|
||||
}}
|
||||
knowledgeSpaceId="space-1"
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByRole('radio', { name: 'Firecrawl' })).toBeChecked()
|
||||
expect(screen.getByRole('textbox', { name: /dataset\.newKnowledge\.rootUrl/ })).toHaveValue('')
|
||||
expect(screen.getByRole('textbox', { name: /dataset\.newKnowledge\.sourceName/ })).toHaveValue(
|
||||
'',
|
||||
)
|
||||
await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.crawlOptions' }))
|
||||
expect(
|
||||
screen.getByRole('checkbox', { name: 'dataset.newKnowledge.includeSubpages' }),
|
||||
).toBeChecked()
|
||||
expect(screen.getByRole('spinbutton', { name: 'dataset.newKnowledge.maxPages' })).toHaveValue(
|
||||
100,
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps crawl fields hidden until the selected website provider is configured', () => {
|
||||
render(
|
||||
<AddSourcePage
|
||||
initialSourceDraft={{
|
||||
@ -791,8 +814,8 @@ describe('AddSourcePage', () => {
|
||||
).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('combobox', { name: 'dataset.newKnowledge.syncPolicy' })).toBeEnabled()
|
||||
|
||||
await user.click(screen.getByRole('radio', { name: 'Jina Reader' }))
|
||||
expect(screen.getByText('workflow.nodes.common.pluginNotInstalled')).toBeInTheDocument()
|
||||
expect(screen.queryByRole('radio', { name: 'Jina Reader' })).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('workflow.nodes.common.pluginNotInstalled')).not.toBeInTheDocument()
|
||||
expect(
|
||||
screen.queryByRole('textbox', { name: 'dataset.newKnowledge.rootUrl' }),
|
||||
).not.toBeInTheDocument()
|
||||
@ -1245,6 +1268,19 @@ describe('AddSourcePage', () => {
|
||||
expect(moreProvidersLink).toHaveAttribute('rel', 'noopener noreferrer')
|
||||
})
|
||||
|
||||
it('prompts for provider installation when no datasource integration is installed', () => {
|
||||
queryState.datasourcePlugins.data = []
|
||||
|
||||
render(<AddSourcePage knowledgeSpaceId="space-1" />)
|
||||
|
||||
expect(screen.getByRole('status')).toHaveTextContent('plugin.list.notFound')
|
||||
expect(screen.queryByRole('radio', { name: 'Firecrawl' })).not.toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByRole('link', { name: 'dataset.newKnowledge.moreProviders' }),
|
||||
).toHaveAttribute('href', '/integrations/data-source')
|
||||
expect(screen.queryByText('dataset.newKnowledge.providerUnavailable')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('keeps the handed-off website draft when the provider connection becomes active', async () => {
|
||||
const initialSourceDraft = {
|
||||
includeSubpages: true,
|
||||
@ -1578,7 +1614,7 @@ describe('AddSourcePage', () => {
|
||||
})
|
||||
expect(onlineDocuments).toBeEnabled()
|
||||
expect(screen.getByRole('radio', { name: 'dataset.newKnowledge.onlineDrive' })).toBeEnabled()
|
||||
expect(screen.getByRole('radio', { name: 'Jina Reader' })).toBeEnabled()
|
||||
expect(screen.queryByRole('radio', { name: 'Jina Reader' })).not.toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByRole('group', { name: 'dataset.newKnowledge.providerLabel' }),
|
||||
).toBeInTheDocument()
|
||||
@ -1613,7 +1649,6 @@ describe('AddSourcePage', () => {
|
||||
})
|
||||
|
||||
it.each([
|
||||
['websiteCrawl', 'Jina Reader'],
|
||||
['onlineDocuments', 'Confluence'],
|
||||
['onlineDrive', 'OneDrive'],
|
||||
] as const)('restores the %s provider from a shortcut URL', (initialSourceType, provider) => {
|
||||
@ -1628,6 +1663,19 @@ describe('AddSourcePage', () => {
|
||||
expect(screen.getByRole('radio', { name: provider })).toBeChecked()
|
||||
})
|
||||
|
||||
it('does not expose an uninstalled website provider from a shortcut URL', () => {
|
||||
render(
|
||||
<AddSourcePage
|
||||
initialSourceProvider="Jina Reader"
|
||||
initialSourceType="websiteCrawl"
|
||||
knowledgeSpaceId="space-1"
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.queryByRole('radio', { name: 'Jina Reader' })).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('radio', { name: 'Firecrawl' })).toBeChecked()
|
||||
})
|
||||
|
||||
it('disables the final Add source action while its backend dependency is missing', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<AddSourcePage knowledgeSpaceId="space-1" />)
|
||||
|
||||
@ -39,7 +39,6 @@ import {
|
||||
} from '../connections/model'
|
||||
import { DatasourceParameterForm } from '../setup/datasource-parameter-form'
|
||||
import {
|
||||
datasourceParameterDefaults,
|
||||
datasourceParameterSchemas,
|
||||
invalidDatasourceParameters,
|
||||
missingRequiredDatasourceParameters,
|
||||
@ -49,13 +48,13 @@ import {
|
||||
SourceNameField,
|
||||
SourceProviderCredentialRequiredCard,
|
||||
SourceProviderIcon,
|
||||
SourceProviderNotInstalledCard,
|
||||
SourceProviderSelector,
|
||||
SourceSyncPolicyField,
|
||||
} from '../setup/fields'
|
||||
import {
|
||||
discoverSourceProviderOptions,
|
||||
normalizeSourceProviderName,
|
||||
sourceDraftForProviderOption,
|
||||
sourceProviderOptionForDraft,
|
||||
} from '../setup/provider-options'
|
||||
import { NEW_KNOWLEDGE_SOURCE_NAME_MAX_LENGTH } from '../setup/source-draft'
|
||||
@ -71,6 +70,13 @@ import {
|
||||
type ConnectedSourceDraft =
|
||||
| NewKnowledgeOnlineDocumentsSourceDraft
|
||||
| NewKnowledgeOnlineDriveSourceDraft
|
||||
type ConnectedSourceWorkflowProps = {
|
||||
draft: ConnectedSourceDraft
|
||||
knowledgeSpaceId: string
|
||||
onCompleted: () => void
|
||||
onDraftChange: (draft: NewKnowledgeSourceDraft) => void
|
||||
onExit: () => void
|
||||
}
|
||||
type PageResource = {
|
||||
ancestorKeys: string[]
|
||||
depth: number
|
||||
@ -136,7 +142,7 @@ function providerForDraft(
|
||||
}
|
||||
|
||||
function datasourceProviderForOption(option?: SourceProviderOption) {
|
||||
return option?.installed
|
||||
return option
|
||||
? {
|
||||
datasource: option.datasource,
|
||||
plugin: option.plugin,
|
||||
@ -1493,19 +1499,62 @@ function AppliedResourceConfiguration({
|
||||
)
|
||||
}
|
||||
|
||||
export function ConnectedSourceWorkflow({
|
||||
export function ConnectedSourceWorkflow(props: ConnectedSourceWorkflowProps) {
|
||||
const { draft, onDraftChange } = props
|
||||
const datasourcePluginsQuery = useDataSourceList(true)
|
||||
const datasourceAuthQuery = useGetDataSourceListAuth()
|
||||
const providerOptions = useMemo(
|
||||
() => discoverSourceProviderOptions(draft.sourceType, datasourcePluginsQuery.data ?? []),
|
||||
[datasourcePluginsQuery.data, draft.sourceType],
|
||||
)
|
||||
const providerOption = sourceProviderOptionForDraft(providerOptions, draft)
|
||||
const providerDraft = useMemo(
|
||||
() => (providerOption ? sourceDraftForProviderOption(draft, providerOption) : draft),
|
||||
[draft, providerOption],
|
||||
)
|
||||
const datasourceProvider = datasourceProviderForOption(providerOption)
|
||||
const datasourceAuth = datasourceAuthForProvider(
|
||||
datasourceAuthQuery.data?.result ?? [],
|
||||
datasourceProvider,
|
||||
)
|
||||
const credential = preferredCredential(datasourceAuth)
|
||||
const sessionKey = [
|
||||
draft.sourceType,
|
||||
providerOption?.key ?? 'no-provider',
|
||||
providerOption?.plugin.plugin_unique_identifier ?? 'no-plugin-version',
|
||||
credential?.id ?? 'no-credential',
|
||||
].join(':')
|
||||
|
||||
const selectProvider = (providerKey: string) => {
|
||||
const nextProvider = providerOptions.find((option) => option.key === providerKey)
|
||||
if (!nextProvider) return
|
||||
onDraftChange(sourceDraftForProviderOption(providerDraft, nextProvider))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<SourceProviderSelector
|
||||
options={providerOptions}
|
||||
providerKey={providerOption?.key ?? ''}
|
||||
showEmptyState={
|
||||
!datasourcePluginsQuery.error &&
|
||||
!datasourcePluginsQuery.isPending &&
|
||||
providerOptions.length === 0
|
||||
}
|
||||
onChange={selectProvider}
|
||||
/>
|
||||
<ConnectedSourceWorkflowSession key={sessionKey} {...props} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ConnectedSourceWorkflowSession({
|
||||
draft,
|
||||
knowledgeSpaceId,
|
||||
onCompleted,
|
||||
onDraftChange,
|
||||
onExit,
|
||||
}: {
|
||||
draft: ConnectedSourceDraft
|
||||
knowledgeSpaceId: string
|
||||
onCompleted: () => void
|
||||
onDraftChange: (draft: NewKnowledgeSourceDraft) => void
|
||||
onExit: () => void
|
||||
}) {
|
||||
}: ConnectedSourceWorkflowProps) {
|
||||
const { t } = useTranslation('dataset')
|
||||
const queryClient = useQueryClient()
|
||||
const providersQuery = useQuery(
|
||||
@ -1538,21 +1587,25 @@ export function ConnectedSourceWorkflow({
|
||||
[datasourcePluginsQuery.data, draft.sourceType],
|
||||
)
|
||||
const providerOption = sourceProviderOptionForDraft(providerOptions, draft)
|
||||
const installedProviderOption = providerOption?.installed ? providerOption : undefined
|
||||
const providerDraft = useMemo(
|
||||
() => (providerOption ? sourceDraftForProviderOption(draft, providerOption) : draft),
|
||||
[draft, providerOption],
|
||||
)
|
||||
const installedProviderOption = providerOption
|
||||
const parameterSchemas = useMemo(
|
||||
() =>
|
||||
installedProviderOption ? datasourceParameterSchemas(installedProviderOption.datasource) : [],
|
||||
[installedProviderOption],
|
||||
)
|
||||
const parameters = useMemo(
|
||||
() => withDatasourceParameterDefaults(parameterSchemas, draft.parameters),
|
||||
[draft.parameters, parameterSchemas],
|
||||
() => withDatasourceParameterDefaults(parameterSchemas, providerDraft.parameters),
|
||||
[parameterSchemas, providerDraft.parameters],
|
||||
)
|
||||
const parametersValid =
|
||||
!missingRequiredDatasourceParameters(parameterSchemas, parameters).length &&
|
||||
!invalidDatasourceParameters(parameterSchemas, parameters).length
|
||||
const provider = providerForDraft(providersQuery.data ?? [], draft, providerOption)
|
||||
const driveTransport = usesDriveTransport(draft)
|
||||
const provider = providerForDraft(providersQuery.data ?? [], providerDraft, providerOption)
|
||||
const driveTransport = usesDriveTransport(providerDraft)
|
||||
const datasourceProvider = datasourceProviderForOption(installedProviderOption)
|
||||
const datasourceAuth = datasourceAuthForProvider(
|
||||
datasourceAuthQuery.data?.result ?? [],
|
||||
@ -1659,7 +1712,7 @@ export function ConnectedSourceWorkflow({
|
||||
providerKind: driveTransport ? 'online-drive' : 'online-document',
|
||||
},
|
||||
credentials: {},
|
||||
name: credential.name || draft.provider,
|
||||
name: credential.name || providerDraft.provider,
|
||||
providerId: provider.id,
|
||||
},
|
||||
params: { control_space_id: knowledgeSpaceId },
|
||||
@ -1687,9 +1740,9 @@ export function ConnectedSourceWorkflow({
|
||||
datasourceIdentity,
|
||||
datasourceProvider,
|
||||
driveTransport,
|
||||
draft.provider,
|
||||
knowledgeSpaceId,
|
||||
provider,
|
||||
providerDraft.provider,
|
||||
provisioningConnection,
|
||||
refetchConnections,
|
||||
rememberConnection,
|
||||
@ -1716,40 +1769,8 @@ export function ConnectedSourceWorkflow({
|
||||
providersQuery.isPending,
|
||||
provisionConnection,
|
||||
])
|
||||
const selectProvider = (providerKey: string) => {
|
||||
const nextProvider = providerOptions.find((option) => option.key === providerKey)
|
||||
if (!nextProvider) return
|
||||
setConnectionOverride(undefined)
|
||||
setProvisionError(false)
|
||||
if (draft.sourceType === 'onlineDocuments') {
|
||||
onDraftChange({
|
||||
...draft,
|
||||
parameters: nextProvider.installed
|
||||
? datasourceParameterDefaults(datasourceParameterSchemas(nextProvider.datasource))
|
||||
: {},
|
||||
provider: nextProvider.label,
|
||||
providerKey: nextProvider.key,
|
||||
sourceName: '',
|
||||
})
|
||||
return
|
||||
}
|
||||
onDraftChange({
|
||||
...draft,
|
||||
parameters: nextProvider.installed
|
||||
? datasourceParameterDefaults(datasourceParameterSchemas(nextProvider.datasource))
|
||||
: {},
|
||||
provider: nextProvider.label,
|
||||
providerKey: nextProvider.key,
|
||||
sourceName: '',
|
||||
})
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<SourceProviderSelector
|
||||
options={providerOptions}
|
||||
providerKey={providerOption?.key ?? ''}
|
||||
onChange={selectProvider}
|
||||
/>
|
||||
<>
|
||||
{providersQuery.isPending ||
|
||||
datasourcePluginsQuery.isPending ||
|
||||
datasourceAuthQuery.isPending ||
|
||||
@ -1776,21 +1797,13 @@ export function ConnectedSourceWorkflow({
|
||||
{t(($) => $['newKnowledge.retryProviderLoad'])}
|
||||
</Button>
|
||||
</div>
|
||||
) : providerOption && !providerOption.installed ? (
|
||||
<SourceProviderNotInstalledCard
|
||||
icon={<SourceProviderIcon fallbackIcon={providerOption.fallbackIcon} />}
|
||||
provider={providerOption.label}
|
||||
onInstall={() =>
|
||||
globalThis.open(
|
||||
providerIntegrationPath(providerOption),
|
||||
'_blank',
|
||||
'noopener,noreferrer',
|
||||
)
|
||||
}
|
||||
/>
|
||||
) : !installedProviderOption || !provider ? (
|
||||
) : providerOptions.length === 0 ? null : !installedProviderOption ? (
|
||||
<div className="rounded-xl bg-background-section p-4 system-sm-regular text-text-tertiary">
|
||||
{t(($) => $['newKnowledge.providerUnavailable'])}
|
||||
</div>
|
||||
) : !provider ? (
|
||||
<div className="rounded-xl bg-background-section p-4">
|
||||
<p className="system-sm-semibold text-text-primary">{draft.provider}</p>
|
||||
<p className="system-sm-semibold text-text-primary">{installedProviderOption.label}</p>
|
||||
<p className="mt-1 system-xs-regular text-text-tertiary">
|
||||
{t(($) => $['newKnowledge.providerUnavailable'])}
|
||||
</p>
|
||||
@ -1806,7 +1819,7 @@ export function ConnectedSourceWorkflow({
|
||||
<AppliedResourceConfiguration
|
||||
key={`${provider.id}:${connection.id}`}
|
||||
connection={connection}
|
||||
draft={draft}
|
||||
draft={providerDraft}
|
||||
knowledgeSpaceId={knowledgeSpaceId}
|
||||
onCompleted={onCompleted}
|
||||
onDraftChange={onDraftChange}
|
||||
@ -1815,7 +1828,9 @@ export function ConnectedSourceWorkflow({
|
||||
parametersValid={parametersValid}
|
||||
parameterSchemas={parameterSchemas}
|
||||
provider={provider}
|
||||
providerRegion={draft.provider === 'Amazon S3' ? credentialRegion(credential) : undefined}
|
||||
providerRegion={
|
||||
providerDraft.provider === 'Amazon S3' ? credentialRegion(credential) : undefined
|
||||
}
|
||||
/>
|
||||
) : connection?.status === 'provisioning' ? (
|
||||
<div className="rounded-xl bg-background-section p-4">
|
||||
@ -1837,7 +1852,7 @@ export function ConnectedSourceWorkflow({
|
||||
</p>
|
||||
<p className="mt-1 system-xs-regular text-text-tertiary">
|
||||
{t(($) => $['newKnowledge.providerCredentialRequiredDescription'], {
|
||||
provider: draft.provider,
|
||||
provider: providerDraft.provider,
|
||||
})}
|
||||
</p>
|
||||
<div className="mt-3 flex gap-2">
|
||||
@ -1851,7 +1866,7 @@ export function ConnectedSourceWorkflow({
|
||||
)
|
||||
}
|
||||
>
|
||||
{t(($) => $['newKnowledge.connectProvider'], { provider: draft.provider })}
|
||||
{t(($) => $['newKnowledge.connectProvider'], { provider: providerDraft.provider })}
|
||||
</Button>
|
||||
<Button loading={provisioningConnection} onClick={() => void provisionConnection(true)}>
|
||||
{t(($) => $['newKnowledge.retryProviderLoad'])}
|
||||
@ -1870,7 +1885,7 @@ export function ConnectedSourceWorkflow({
|
||||
) : provisionError ? (
|
||||
<div className="rounded-xl bg-background-section p-4">
|
||||
<p role="alert" className="system-sm-semibold text-text-primary">
|
||||
{t(($) => $['newKnowledge.connectionFailed'], { provider: draft.provider })}
|
||||
{t(($) => $['newKnowledge.connectionFailed'], { provider: providerDraft.provider })}
|
||||
</p>
|
||||
<Button
|
||||
className="mt-3"
|
||||
@ -1904,7 +1919,7 @@ export function ConnectedSourceWorkflow({
|
||||
/>
|
||||
)}
|
||||
{connection?.status !== 'active' && (
|
||||
<ConnectedSourceSyncPolicyField draft={draft} onDraftChange={onDraftChange} />
|
||||
<ConnectedSourceSyncPolicyField draft={providerDraft} onDraftChange={onDraftChange} />
|
||||
)}
|
||||
{!connection && (
|
||||
<div className="mt-1 flex justify-between gap-2 border-t border-divider-subtle pt-4.75">
|
||||
@ -1916,6 +1931,6 @@ export function ConnectedSourceWorkflow({
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -36,19 +36,15 @@ import {
|
||||
sourceConnectionStatusRank,
|
||||
sourceProviderUsesManagedConfiguration,
|
||||
} from '../connections/model'
|
||||
import {
|
||||
datasourceParameterDefaults,
|
||||
websiteDatasourceParameterSchemas,
|
||||
} from '../setup/datasource-parameter-model'
|
||||
import {
|
||||
SourceProviderIcon,
|
||||
SourceProviderNotInstalledCard,
|
||||
SourceProviderSelector,
|
||||
SourceSyncPolicyField,
|
||||
SourceTypeSelector,
|
||||
} from '../setup/fields'
|
||||
import {
|
||||
discoverSourceProviderOptions,
|
||||
sourceDraftForProviderOption,
|
||||
sourceProviderOptionForDraft,
|
||||
} from '../setup/provider-options'
|
||||
import {
|
||||
@ -571,10 +567,8 @@ function UnconfiguredProvider({
|
||||
<SourceProviderIcon
|
||||
fallbackIcon={providerOption.fallbackIcon}
|
||||
icon={
|
||||
providerOption.installed
|
||||
? (providerOption.datasource.identity.icon ??
|
||||
providerOption.plugin.declaration.identity.icon)
|
||||
: undefined
|
||||
providerOption.datasource.identity.icon ??
|
||||
providerOption.plugin.declaration.identity.icon
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
@ -847,8 +841,15 @@ function AddSourcePageContent({
|
||||
[datasourcePluginsQuery.data],
|
||||
)
|
||||
const websiteProviderOption = sourceProviderOptionForDraft(websiteProviderOptions, sourceDraft)
|
||||
const websiteProviderName = websiteProviderOption?.label ?? sourceDraft.provider
|
||||
const datasourceProvider = websiteProviderOption?.installed ? websiteProviderOption : undefined
|
||||
const providerDraft = useMemo(
|
||||
() =>
|
||||
sourceDraft.sourceType === 'websiteCrawl' && websiteProviderOption
|
||||
? sourceDraftForProviderOption(sourceDraft, websiteProviderOption)
|
||||
: sourceDraft,
|
||||
[sourceDraft, websiteProviderOption],
|
||||
)
|
||||
const websiteProviderName = websiteProviderOption?.label ?? providerDraft.provider
|
||||
const datasourceProvider = websiteProviderOption
|
||||
const datasourceProviders = datasourceAuthQuery.data?.result ?? []
|
||||
const datasourceCredential = findDatasourceCredential(datasourceProviders, datasourceProvider)
|
||||
const difyManagedProvider = provider ? isDifyManagedProvider(provider) : false
|
||||
@ -1041,35 +1042,26 @@ function AddSourcePageContent({
|
||||
disabled={websiteSetupLocked}
|
||||
value={sourceType}
|
||||
onChange={(value) => {
|
||||
sourceDraftsRef.current[sourceDraft.sourceType] = sourceDraft
|
||||
sourceDraftsRef.current[providerDraft.sourceType] = providerDraft
|
||||
updateSourceDraft(
|
||||
sourceDraftsRef.current[value] ?? createNewKnowledgeSourceDraft(value),
|
||||
)
|
||||
}}
|
||||
/>
|
||||
{sourceDraft.sourceType === 'websiteCrawl' ? (
|
||||
{providerDraft.sourceType === 'websiteCrawl' ? (
|
||||
<>
|
||||
<SourceProviderSelector
|
||||
disabled={websiteSetupLocked}
|
||||
layout="grid-four"
|
||||
options={websiteProviderOptions}
|
||||
providerKey={websiteProviderOption?.key ?? ''}
|
||||
showEmptyState={!queryError && websiteProviderOptions.length === 0}
|
||||
onChange={(providerKey) => {
|
||||
const nextProvider = websiteProviderOptions.find(
|
||||
(option) => option.key === providerKey,
|
||||
)
|
||||
if (!nextProvider) return
|
||||
updateSourceDraft({
|
||||
...sourceDraft,
|
||||
parameters: nextProvider.installed
|
||||
? datasourceParameterDefaults(
|
||||
websiteDatasourceParameterSchemas(nextProvider.datasource),
|
||||
)
|
||||
: {},
|
||||
provider: nextProvider.label,
|
||||
providerKey: nextProvider.key,
|
||||
rootUrl: '',
|
||||
})
|
||||
updateSourceDraft(sourceDraftForProviderOption(providerDraft, nextProvider))
|
||||
}}
|
||||
/>
|
||||
{queryError ? (
|
||||
@ -1091,19 +1083,7 @@ function AddSourcePageContent({
|
||||
{t(($) => $['newKnowledge.retryProviderLoad'])}
|
||||
</Button>
|
||||
</div>
|
||||
) : websiteProviderOption && !websiteProviderOption.installed ? (
|
||||
<SourceProviderNotInstalledCard
|
||||
icon={<SourceProviderIcon fallbackIcon={websiteProviderOption.fallbackIcon} />}
|
||||
provider={websiteProviderOption.label}
|
||||
onInstall={() =>
|
||||
globalThis.open(
|
||||
websiteProviderIntegrationPath(websiteProviderOption),
|
||||
'_blank',
|
||||
'noopener,noreferrer',
|
||||
)
|
||||
}
|
||||
/>
|
||||
) : !datasourceProvider || !provider ? (
|
||||
) : websiteProviderOptions.length === 0 ? null : !datasourceProvider || !provider ? (
|
||||
<div className="rounded-xl bg-background-section p-4 system-sm-regular text-text-tertiary">
|
||||
{t(($) => $['newKnowledge.providerUnavailable'])}
|
||||
</div>
|
||||
@ -1118,7 +1098,7 @@ function AddSourcePageContent({
|
||||
<WebsiteCrawlPreview
|
||||
key={`${datasourceProvider.key}:${activeConnection.id}`}
|
||||
connection={activeConnection}
|
||||
initialDraft={sourceDraft}
|
||||
initialDraft={providerDraft}
|
||||
knowledgeSpaceId={knowledgeSpaceId}
|
||||
onDraftFinished={clearStoredSourceDraft}
|
||||
onInteractionLockChange={setWebsiteSetupLocked}
|
||||
@ -1128,7 +1108,7 @@ function AddSourcePageContent({
|
||||
<SourceSyncPolicyField
|
||||
className="w-full sm:w-75.25"
|
||||
disabled={websiteSetupLocked}
|
||||
draft={sourceDraft}
|
||||
draft={providerDraft}
|
||||
size="medium"
|
||||
onDraftChange={updateSourceDraft}
|
||||
/>
|
||||
@ -1174,7 +1154,7 @@ function AddSourcePageContent({
|
||||
</>
|
||||
) : (
|
||||
<ConnectedSourceWorkflow
|
||||
draft={sourceDraft}
|
||||
draft={providerDraft}
|
||||
knowledgeSpaceId={knowledgeSpaceId}
|
||||
onCompleted={() => {
|
||||
clearStoredSourceDraft()
|
||||
@ -1189,7 +1169,7 @@ function AddSourcePageContent({
|
||||
{sourceType === 'websiteCrawl' && !websiteReady && (
|
||||
<SourceSyncPolicyField
|
||||
className="w-full sm:w-75.25"
|
||||
draft={sourceDraft}
|
||||
draft={providerDraft}
|
||||
size="medium"
|
||||
onDraftChange={updateSourceDraft}
|
||||
/>
|
||||
|
||||
@ -1,4 +1,22 @@
|
||||
import { sourceProviderPresentation } from '../provider-options'
|
||||
import type { SourceProviderOption } from '../provider-options'
|
||||
import { sourceDraftForProviderOption, sourceProviderPresentation } from '../provider-options'
|
||||
|
||||
function providerOption({ key, label }: { key: string; label: string }) {
|
||||
return {
|
||||
datasource: {
|
||||
parameters: [
|
||||
{
|
||||
default: 'new-default',
|
||||
label: { en_US: 'Workspace' },
|
||||
name: 'workspace',
|
||||
type: 'string',
|
||||
},
|
||||
],
|
||||
},
|
||||
key,
|
||||
label,
|
||||
} as SourceProviderOption
|
||||
}
|
||||
|
||||
describe('sourceProviderPresentation', () => {
|
||||
it.each([
|
||||
@ -19,3 +37,51 @@ describe('sourceProviderPresentation', () => {
|
||||
expect(sourceProviderPresentation('Notion Backup', 'onlineDocuments')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('sourceDraftForProviderOption', () => {
|
||||
it('resets provider-specific fields when a different provider has the same label', () => {
|
||||
const draft = {
|
||||
parameters: { oldWorkspace: 'stale-value' },
|
||||
provider: 'Docs',
|
||||
providerKey: 'onlineDocuments:old-provider',
|
||||
sourceName: 'Existing source',
|
||||
sourceType: 'onlineDocuments' as const,
|
||||
syncPolicy: 'daily' as const,
|
||||
}
|
||||
|
||||
expect(
|
||||
sourceDraftForProviderOption(
|
||||
draft,
|
||||
providerOption({ key: 'onlineDocuments:new-provider', label: 'Docs' }),
|
||||
),
|
||||
).toEqual({
|
||||
parameters: { workspace: 'new-default' },
|
||||
provider: 'Docs',
|
||||
providerKey: 'onlineDocuments:new-provider',
|
||||
sourceName: '',
|
||||
sourceType: 'onlineDocuments',
|
||||
syncPolicy: 'daily',
|
||||
})
|
||||
})
|
||||
|
||||
it('preserves provider fields when the stable provider key matches after a label change', () => {
|
||||
const draft = {
|
||||
parameters: { workspace: 'saved-workspace' },
|
||||
provider: 'Old Docs Name',
|
||||
providerKey: 'onlineDocuments:provider',
|
||||
sourceName: 'Existing source',
|
||||
sourceType: 'onlineDocuments' as const,
|
||||
syncPolicy: 'daily' as const,
|
||||
}
|
||||
|
||||
expect(
|
||||
sourceDraftForProviderOption(
|
||||
draft,
|
||||
providerOption({ key: 'onlineDocuments:provider', label: 'New Docs Name' }),
|
||||
),
|
||||
).toEqual({
|
||||
...draft,
|
||||
provider: 'New Docs Name',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -128,6 +128,26 @@ export function SourceProviderRadioGroup<T extends string>({
|
||||
)
|
||||
}
|
||||
|
||||
export function SourceProviderEmptyState({ className }: { className?: string }) {
|
||||
const { t } = useTranslation('plugin')
|
||||
|
||||
return (
|
||||
<div
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
className={cn(
|
||||
'flex min-h-20 flex-col items-center justify-center gap-2 rounded-lg border border-dashed border-divider-regular bg-background-section px-4 py-3 text-center',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<span className="flex size-8 items-center justify-center rounded-lg bg-background-default">
|
||||
<span aria-hidden className="i-ri-plug-line size-4 text-text-tertiary" />
|
||||
</span>
|
||||
<span className="system-xs-regular text-text-tertiary">{t(($) => $['list.notFound'])}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type SourceProviderIconValue =
|
||||
| InstalledSourceProviderOption['datasource']['identity']['icon']
|
||||
| InstalledSourceProviderOption['plugin']['declaration']['identity']['icon']
|
||||
@ -174,6 +194,7 @@ export function SourceProviderSelector({
|
||||
layout = 'grid-three',
|
||||
options,
|
||||
providerKey,
|
||||
showEmptyState = false,
|
||||
onChange,
|
||||
}: {
|
||||
appearance?: 'embedded' | 'page'
|
||||
@ -181,6 +202,7 @@ export function SourceProviderSelector({
|
||||
layout?: 'grid-four' | 'grid-three'
|
||||
options: SourceProviderOption[]
|
||||
providerKey: string
|
||||
showEmptyState?: boolean
|
||||
onChange: (providerKey: string) => void
|
||||
}) {
|
||||
const { t } = useTranslation('dataset')
|
||||
@ -213,28 +235,28 @@ export function SourceProviderSelector({
|
||||
<span aria-hidden className="i-ri-arrow-right-up-line size-3.5" />
|
||||
</Link>
|
||||
</div>
|
||||
<SourceProviderRadioGroup
|
||||
value={providerKey}
|
||||
disabled={disabled}
|
||||
layout={layout}
|
||||
options={options.map((option) => ({
|
||||
icon: (
|
||||
<SourceProviderIcon
|
||||
fallbackIcon={option.fallbackIcon}
|
||||
icon={
|
||||
option.installed
|
||||
? (option.datasource.identity.icon ?? option.plugin.declaration.identity.icon)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
),
|
||||
label: option.label,
|
||||
value: option.key,
|
||||
}))}
|
||||
size="medium"
|
||||
surface="default"
|
||||
onChange={onChange}
|
||||
/>
|
||||
{options.length > 0 ? (
|
||||
<SourceProviderRadioGroup
|
||||
value={providerKey}
|
||||
disabled={disabled}
|
||||
layout={layout}
|
||||
options={options.map((option) => ({
|
||||
icon: (
|
||||
<SourceProviderIcon
|
||||
fallbackIcon={option.fallbackIcon}
|
||||
icon={option.datasource.identity.icon ?? option.plugin.declaration.identity.icon}
|
||||
/>
|
||||
),
|
||||
label: option.label,
|
||||
value: option.key,
|
||||
}))}
|
||||
size="medium"
|
||||
surface="default"
|
||||
onChange={onChange}
|
||||
/>
|
||||
) : showEmptyState ? (
|
||||
<SourceProviderEmptyState />
|
||||
) : null}
|
||||
</Fieldset>
|
||||
)
|
||||
}
|
||||
@ -384,31 +406,3 @@ export function SourceProviderCredentialRequiredCard({
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function SourceProviderNotInstalledCard({
|
||||
icon,
|
||||
provider,
|
||||
onInstall,
|
||||
}: {
|
||||
icon: ReactNode
|
||||
provider: string
|
||||
onInstall: () => void
|
||||
}) {
|
||||
const { t: tPlugin } = useTranslation('plugin')
|
||||
const { t: tWorkflow } = useTranslation('workflow')
|
||||
|
||||
return (
|
||||
<section className="flex min-h-44 flex-col items-start gap-2.5 rounded-xl bg-background-section p-4">
|
||||
<span className="flex size-9 items-center justify-center rounded-lg border-[0.5px] border-divider-subtle bg-background-default">
|
||||
{icon}
|
||||
</span>
|
||||
<h3 className="system-sm-semibold text-text-primary">{provider}</h3>
|
||||
<p className="system-xs-regular text-text-tertiary">
|
||||
{tWorkflow(($) => $['nodes.common.pluginNotInstalled'])}
|
||||
</p>
|
||||
<Button type="button" variant="primary" className="mt-auto" onClick={onInstall}>
|
||||
{tPlugin(($) => $.installPlugin)}
|
||||
</Button>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
import type { NewKnowledgeSourceDraft, NewKnowledgeSourceType } from './source-draft'
|
||||
import type { DataSourceItem } from '@/app/components/workflow/block-selector/types'
|
||||
import {
|
||||
datasourceParameterDefaults,
|
||||
datasourceParameterSchemas,
|
||||
websiteDatasourceParameterSchemas,
|
||||
} from './datasource-parameter-model'
|
||||
|
||||
type Datasource = DataSourceItem['declaration']['datasources'][number]
|
||||
|
||||
@ -15,7 +20,6 @@ type RecommendedProvider = {
|
||||
export type InstalledSourceProviderOption = {
|
||||
datasource: Datasource
|
||||
fallbackIcon: string
|
||||
installed: true
|
||||
key: string
|
||||
label: string
|
||||
packageId: string
|
||||
@ -24,17 +28,7 @@ export type InstalledSourceProviderOption = {
|
||||
sourceType: NewKnowledgeSourceType
|
||||
}
|
||||
|
||||
export type UninstalledSourceProviderOption = {
|
||||
fallbackIcon: string
|
||||
installed: false
|
||||
key: string
|
||||
label: string
|
||||
packageId: string
|
||||
providerType: DataSourceItem['declaration']['provider_type']
|
||||
sourceType: NewKnowledgeSourceType
|
||||
}
|
||||
|
||||
export type SourceProviderOption = InstalledSourceProviderOption | UninstalledSourceProviderOption
|
||||
export type SourceProviderOption = InstalledSourceProviderOption
|
||||
|
||||
const recommendedProviders: RecommendedProvider[] = [
|
||||
{
|
||||
@ -185,7 +179,6 @@ function installedRecommendedProvider(
|
||||
return {
|
||||
datasource,
|
||||
fallbackIcon: definition.fallbackIcon,
|
||||
installed: true,
|
||||
key: providerKey(definition.sourceType, plugin, datasource),
|
||||
label: definition.label,
|
||||
packageId: definition.packageId,
|
||||
@ -226,23 +219,11 @@ export function discoverSourceProviderOptions(
|
||||
const definitions = recommendedProviders.filter(
|
||||
(definition) => definition.sourceType === sourceType,
|
||||
)
|
||||
const recommended: SourceProviderOption[] = definitions.map((definition) => {
|
||||
const recommended = definitions.flatMap((definition) => {
|
||||
const installed = installedRecommendedProvider(definition, datasourcePlugins)
|
||||
return (
|
||||
installed ?? {
|
||||
fallbackIcon: definition.fallbackIcon,
|
||||
installed: false as const,
|
||||
key: `${sourceType}:marketplace:${definition.packageId}`,
|
||||
label: definition.label,
|
||||
packageId: definition.packageId,
|
||||
providerType: definition.providerType,
|
||||
sourceType,
|
||||
}
|
||||
)
|
||||
return installed ? [installed] : []
|
||||
})
|
||||
const consumedKeys = new Set(
|
||||
recommended.flatMap((option) => (option.installed ? [option.key] : [])),
|
||||
)
|
||||
const consumedKeys = new Set(recommended.map((option) => option.key))
|
||||
const labels = new Set(recommended.map((option) => option.label))
|
||||
const discovered: InstalledSourceProviderOption[] = []
|
||||
|
||||
@ -261,7 +242,6 @@ export function discoverSourceProviderOptions(
|
||||
discovered.push({
|
||||
datasource,
|
||||
fallbackIcon: fallbackIcon(sourceType),
|
||||
installed: true,
|
||||
key,
|
||||
label,
|
||||
packageId: plugin.plugin_id,
|
||||
@ -288,3 +268,32 @@ export function sourceProviderOptionForDraft(
|
||||
options[0]
|
||||
)
|
||||
}
|
||||
|
||||
export function sourceDraftForProviderOption<T extends NewKnowledgeSourceDraft>(
|
||||
draft: T,
|
||||
option: SourceProviderOption,
|
||||
): T {
|
||||
const providerMatches =
|
||||
draft.providerKey !== undefined
|
||||
? draft.providerKey === option.key
|
||||
: normalizeSourceProviderName(draft.provider) === normalizeSourceProviderName(option.label)
|
||||
if (providerMatches)
|
||||
return draft.provider === option.label && draft.providerKey === option.key
|
||||
? draft
|
||||
: ({ ...draft, provider: option.label, providerKey: option.key } as T)
|
||||
|
||||
const schemas =
|
||||
draft.sourceType === 'websiteCrawl'
|
||||
? websiteDatasourceParameterSchemas(option.datasource)
|
||||
: datasourceParameterSchemas(option.datasource)
|
||||
return {
|
||||
...draft,
|
||||
parameters: datasourceParameterDefaults(schemas),
|
||||
provider: option.label,
|
||||
providerKey: option.key,
|
||||
sourceName: '',
|
||||
...(draft.sourceType === 'websiteCrawl'
|
||||
? { includeSubpages: true, maxPages: 100, rootUrl: '' }
|
||||
: {}),
|
||||
} as T
|
||||
}
|
||||
|
||||
@ -224,7 +224,7 @@ function ConnectedSourceEditDialogContent({
|
||||
const providerOption = providerOptions.find(
|
||||
(option) => normalizeSourceProviderName(option.label) === normalizedProviderName,
|
||||
)
|
||||
const installedProviderOption = providerOption?.installed ? providerOption : undefined
|
||||
const installedProviderOption = providerOption
|
||||
const connections =
|
||||
connectionsData?.pages.flatMap((page) => sourceConnectionListFromApi(page).items) ?? []
|
||||
const connection = connections.find((item) => item.id === source.connectionId)
|
||||
@ -516,13 +516,10 @@ function WebsiteSourceEditDialogContent({
|
||||
const providerLoading = usesProviderDeclaration && datasourcePluginsQuery.isPending
|
||||
const providerLoadFailed = usesProviderDeclaration && datasourcePluginsQuery.isError
|
||||
const providerConfigurationReady =
|
||||
!usesProviderDeclaration ||
|
||||
(!providerLoading && !providerLoadFailed && providerOption?.installed === true)
|
||||
!usesProviderDeclaration || (!providerLoading && !providerLoadFailed && Boolean(providerOption))
|
||||
const parameterSchemas = useMemo(() => {
|
||||
if (!usesProviderDeclaration) return websiteDatasourceParameterSchemas()
|
||||
return providerOption?.installed
|
||||
? websiteDatasourceParameterSchemas(providerOption.datasource)
|
||||
: []
|
||||
return providerOption ? websiteDatasourceParameterSchemas(providerOption.datasource) : []
|
||||
}, [providerOption, usesProviderDeclaration])
|
||||
const displayedParameters = useMemo(
|
||||
() => sourceParametersForSchemas(initialSource, nextParameters, parameterSchemas),
|
||||
@ -577,7 +574,7 @@ function WebsiteSourceEditDialogContent({
|
||||
}
|
||||
|
||||
const startPreview = async () => {
|
||||
if (previewing || !parametersValid || !providerOption?.installed || !previewBindingReady) return
|
||||
if (previewing || !parametersValid || !providerOption || !previewBindingReady) return
|
||||
const attempt = previewAttemptRef.current + 1
|
||||
previewAttemptRef.current = attempt
|
||||
setPreviewing(true)
|
||||
@ -735,7 +732,7 @@ function WebsiteSourceEditDialogContent({
|
||||
pending ||
|
||||
previewing ||
|
||||
!parametersValid ||
|
||||
!providerOption?.installed ||
|
||||
!providerOption ||
|
||||
!previewBindingReady
|
||||
}
|
||||
loading={previewing}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user