Files
ragflow/web/src/pages/user-setting/data-source/data-source-detail-page/index.tsx

284 lines
8.5 KiB
TypeScript

import {
DynamicForm,
DynamicFormRef,
FormFieldConfig,
FormFieldType,
} from '@/components/dynamic-form';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Separator } from '@/components/ui/separator';
import { RunningStatus, RunningStatusOld } from '@/constants/knowledge';
import { isEqual } from 'lodash';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { FieldValues } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import {
DataSourceFormDefaultValues,
DataSourceKey,
getCommonExtraDefaultValues,
getDataSourceFormBaseFields,
getDataSourceFieldsWithExtras,
mergeDataSourceFormValues,
useDataSourceInfo,
} from '../constant';
import {
useAddDataSource,
useFetchDataSourceDetail,
useTestDataSource,
useUpdateDataSourceStatus,
} from '../hooks';
import { DataSourceLogsTable } from './log-table';
const SourceDetailPage = () => {
const { t } = useTranslation();
const formRef = useRef<DynamicFormRef>(null);
const { data: detail } = useFetchDataSourceDetail();
const { updateStatus, loading: statusUpdateLoading } =
useUpdateDataSourceStatus();
const { dataSourceInfo } = useDataSourceInfo();
const detailInfo = useMemo(() => {
if (detail) {
return dataSourceInfo[detail.source];
}
}, [detail, dataSourceInfo]);
const [fields, setFields] = useState<FormFieldConfig[]>([]);
const [isDirty, setIsDirty] = useState(false);
const [defaultValues, setDefaultValues] = useState<FieldValues>(
DataSourceFormDefaultValues[
detail?.source as keyof typeof DataSourceFormDefaultValues
] as FieldValues,
);
const customFields = useMemo(() => {
return [
{
label: t('setting.dataSourcePruneFreq'),
name: 'prune_freq',
type: FormFieldType.Number,
required: false,
shouldRender: (values: any) => !!values?.config?.sync_deleted_files,
render: (fieldProps: FormFieldConfig) => {
return (
<Input
{...fieldProps}
type={FormFieldType.Number}
suffix={
<span className="px-2 text-text-secondary italic">
{t('setting.minutes')}
</span>
}
/>
);
},
},
{
label: t('setting.dataSourceRefreshFreq'),
name: 'refresh_freq',
type: FormFieldType.Number,
required: false,
render: (fieldProps: FormFieldConfig) => (
<Input
{...fieldProps}
type={FormFieldType.Number}
suffix={
<span className="px-2 text-text-secondary italic">
{t('setting.minutes')}
</span>
}
/>
),
},
{
label: t('setting.dataSourceTimeoutSecs'),
name: 'timeout_secs',
type: FormFieldType.Number,
required: false,
render: (fieldProps: FormFieldConfig) => (
<div className="flex items-center gap-1 w-full relative">
<div className="flex-1">
<Input
{...fieldProps}
type={FormFieldType.Number}
suffix={
<span className="px-2 text-text-secondary italic">
{t('setting.seconds')}
</span>
}
/>
</div>
</div>
),
},
];
}, [t]);
const { addLoading, handleAddOk } = useAddDataSource({ isEdit: true });
const { loading: testLoading, handleTest } = useTestDataSource();
const onSubmit = useCallback(() => {
formRef?.current?.submit();
}, []);
const isUnstarted = useMemo(
() =>
detail?.status === RunningStatus.UNSTART ||
detail?.status === RunningStatusOld.UNSTART,
[detail?.status],
);
const isConnectorActive = useMemo(
() =>
detail?.status === RunningStatus.RUNNING ||
detail?.status === RunningStatus.SCHEDULE ||
detail?.status === RunningStatusOld.RUNNING ||
detail?.status === RunningStatusOld.SCHEDULE,
[detail?.status],
);
const actionMode = useMemo(() => {
if (isDirty) {
return 'save' as const;
}
if (isUnstarted) {
return 'save' as const;
}
if (isConnectorActive) {
return 'stop' as const;
}
return 'resume' as const;
}, [isConnectorActive, isDirty, isUnstarted]);
const handlePrimaryAction = useCallback(() => {
if (actionMode === 'save') {
onSubmit();
return;
}
updateStatus(
actionMode === 'resume' ? RunningStatus.SCHEDULE : RunningStatus.CANCEL,
);
}, [actionMode, onSubmit, updateStatus]);
const primaryActionLabel = useMemo(() => {
if (actionMode === 'stop') return t('common.stop');
if (actionMode === 'resume') return t('common.resume');
return t('common.save');
}, [actionMode, t]);
useEffect(() => {
const baseFields = getDataSourceFormBaseFields(t).map((field) => {
if (field.name === 'name') {
return {
...field,
disabled: true,
};
} else {
return {
...field,
};
}
});
if (detail) {
const fields = [
...baseFields,
...getDataSourceFieldsWithExtras(t, detail.source as any),
...customFields,
] as FormFieldConfig[];
const newFields = fields.map((field) => {
return {
...field,
horizontal: true,
onChange: undefined,
};
});
setFields(newFields);
const defaultValueTemp = {
...mergeDataSourceFormValues(
DataSourceFormDefaultValues[
detail?.source as keyof typeof DataSourceFormDefaultValues
] as FieldValues,
getCommonExtraDefaultValues(),
detail as FieldValues,
),
};
setDefaultValues(defaultValueTemp);
setIsDirty(false);
}
}, [detail, customFields, onSubmit, t]);
useEffect(() => {
const instance = formRef.current;
if (!instance) return;
setIsDirty(!isEqual(instance.getValues(), defaultValues));
return instance.watchDirty((_nextIsDirty, values) => {
setIsDirty(!isEqual(values, defaultValues));
});
}, [defaultValues, fields]);
return (
<div className="px-10 py-5">
{/* <BackButton /> */}
<Card className="bg-transparent border border-border-button px-5 pt-[10px] pb-5 rounded-md mt-5">
<CardHeader className="flex flex-row items-center justify-between space-y-0 p-0 pb-3">
{/* <Users className="mr-2 h-5 w-5 text-[#1677ff]" /> */}
<CardTitle className="text-2xl text-text-primary flex gap-1 items-center font-normal pb-3">
{detailInfo?.icon}
{detail?.name}
</CardTitle>
</CardHeader>
<Separator className="border-border-button bg-border-button w-[calc(100%+2rem)] -translate-x-4 -translate-y-4" />
<CardContent className="p-2 flex flex-col gap-10 max-h-[calc(100vh-230px)] overflow-y-auto scrollbar-auto">
<div className="max-w-[1200px]">
<DynamicForm.Root
ref={formRef}
fields={fields}
onSubmit={(data) => handleAddOk(data)}
defaultValues={defaultValues}
/>
</div>
<div className="max-w-[1200px] flex justify-end gap-2">
{(detail?.source === DataSourceKey.REST_API ||
detail?.source === DataSourceKey.BIGQUERY) && (
<Button
type="button"
variant="outline"
onClick={handleTest}
disabled={testLoading}
loading={testLoading}
>
{t('setting.restApiTestConnection')}
</Button>
)}
<Button
type="button"
onClick={handlePrimaryAction}
disabled={addLoading || statusUpdateLoading}
loading={
(addLoading && actionMode === 'save') ||
(statusUpdateLoading && actionMode !== 'save')
}
>
{primaryActionLabel}
</Button>
</div>
<section className="flex flex-col gap-2">
<div className="text-2xl text-text-primary mb-2">
{t('setting.log')}
</div>
<DataSourceLogsTable autoRefresh={isConnectorActive} />
</section>
</CardContent>
</Card>
</div>
);
};
export default SourceDetailPage;