* feat: login limit time config * doc * perf: code * i18n update * update lock * fix: ts * update package
470 lines
16 KiB
TypeScript
470 lines
16 KiB
TypeScript
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
import {
|
|
Box,
|
|
Flex,
|
|
Button,
|
|
ModalBody,
|
|
Input,
|
|
Textarea,
|
|
TableContainer,
|
|
Table,
|
|
Thead,
|
|
Th,
|
|
Tbody,
|
|
Tr,
|
|
Td,
|
|
ModalFooter
|
|
} from '@chakra-ui/react';
|
|
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
|
|
import { useForm } from 'react-hook-form';
|
|
import { useToast } from '@fastgpt/web/hooks/useToast';
|
|
import { useRequest } from '@fastgpt/web/hooks/useRequest';
|
|
import Avatar from '@fastgpt/web/components/common/Avatar';
|
|
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
|
import { useTranslation } from 'next-i18next';
|
|
import { HttpPluginImgUrl } from '@fastgpt/global/common/file/image/constants';
|
|
import {
|
|
postCreateHttpPlugin,
|
|
putUpdateHttpPlugin,
|
|
getApiSchemaByUrl
|
|
} from '@/web/core/app/api/plugin';
|
|
import { str2OpenApiSchema } from '@fastgpt/global/core/app/httpPlugin/utils';
|
|
import MyIcon from '@fastgpt/web/components/common/Icon';
|
|
import MyModal from '@fastgpt/web/components/common/MyModal';
|
|
import HttpInput from '@fastgpt/web/components/common/Input/HttpInput';
|
|
import { OpenApiJsonSchema } from '@fastgpt/global/core/app/httpPlugin/type';
|
|
import { AppSchema } from '@fastgpt/global/core/app/type';
|
|
import { useContextSelector } from 'use-context-selector';
|
|
import { AppListContext } from './context';
|
|
|
|
export type EditHttpPluginProps = {
|
|
id?: string;
|
|
avatar: string;
|
|
name: string;
|
|
intro?: string;
|
|
pluginData?: AppSchema['pluginData'];
|
|
};
|
|
export const defaultHttpPlugin: EditHttpPluginProps = {
|
|
avatar: HttpPluginImgUrl,
|
|
name: '',
|
|
intro: '',
|
|
pluginData: {
|
|
apiSchemaStr: '',
|
|
customHeaders: '{"Authorization":"Bearer"}'
|
|
}
|
|
};
|
|
|
|
const HttpPluginEditModal = ({
|
|
defaultPlugin = defaultHttpPlugin,
|
|
onClose
|
|
}: {
|
|
defaultPlugin?: EditHttpPluginProps;
|
|
onClose: () => void;
|
|
}) => {
|
|
const { t } = useTranslation();
|
|
const { toast } = useToast();
|
|
const isEdit = !!defaultPlugin.id;
|
|
|
|
const { parentId, loadMyApps } = useContextSelector(AppListContext, (v) => v);
|
|
|
|
const [schemaUrl, setSchemaUrl] = useState('');
|
|
const [customHeaders, setCustomHeaders] = useState<{ key: string; value: string }[]>(() => {
|
|
const keyValue = JSON.parse(defaultPlugin.pluginData?.customHeaders || '{}');
|
|
return Object.keys(keyValue).map((key) => ({ key, value: keyValue[key] }));
|
|
});
|
|
const [updateTrigger, setUpdateTrigger] = useState(false);
|
|
|
|
const { register, setValue, handleSubmit, watch } = useForm<EditHttpPluginProps>({
|
|
defaultValues: defaultPlugin
|
|
});
|
|
const avatar = watch('avatar');
|
|
const apiSchemaStr = watch('pluginData.apiSchemaStr');
|
|
const [apiData, setApiData] = useState<OpenApiJsonSchema>({ pathData: [], serverPath: '' });
|
|
|
|
const { mutate: onCreate, isLoading: isCreating } = useRequest({
|
|
mutationFn: async (data: EditHttpPluginProps) => {
|
|
return postCreateHttpPlugin({
|
|
parentId,
|
|
name: data.name,
|
|
intro: data.intro,
|
|
avatar: data.avatar,
|
|
pluginData: {
|
|
apiSchemaStr: data.pluginData?.apiSchemaStr,
|
|
customHeaders: data.pluginData?.customHeaders
|
|
}
|
|
});
|
|
},
|
|
onSuccess() {
|
|
loadMyApps();
|
|
onClose();
|
|
},
|
|
successToast: t('common:create_success'),
|
|
errorToast: t('common:create_failed')
|
|
});
|
|
|
|
const { mutate: updatePlugins, isLoading: isUpdating } = useRequest({
|
|
mutationFn: async (data: EditHttpPluginProps) => {
|
|
if (!data.id || !data.pluginData) return Promise.resolve('');
|
|
|
|
return putUpdateHttpPlugin({
|
|
appId: data.id,
|
|
name: data.name,
|
|
intro: data.intro,
|
|
avatar: data.avatar,
|
|
pluginData: data.pluginData
|
|
});
|
|
},
|
|
onSuccess() {
|
|
loadMyApps();
|
|
onClose();
|
|
},
|
|
successToast: t('common:update_success'),
|
|
errorToast: t('common:update_failed')
|
|
});
|
|
|
|
const {
|
|
File,
|
|
onOpen: onOpenSelectFile,
|
|
onSelectImage
|
|
} = useSelectFile({
|
|
fileType: 'image/*',
|
|
multiple: false
|
|
});
|
|
|
|
/* load api from url */
|
|
const { mutate: onClickUrlLoadApi, isLoading: isLoadingUrlApi } = useRequest({
|
|
mutationFn: async () => {
|
|
if (!schemaUrl || (!schemaUrl.startsWith('https://') && !schemaUrl.startsWith('http://'))) {
|
|
return toast({
|
|
title: t('common:plugin.Invalid URL'),
|
|
status: 'warning'
|
|
});
|
|
}
|
|
|
|
const schema = await getApiSchemaByUrl(schemaUrl);
|
|
setValue('pluginData.apiSchemaStr', JSON.stringify(schema, null, 2));
|
|
},
|
|
errorToast: t('common:plugin.Invalid Schema')
|
|
});
|
|
|
|
useEffect(() => {
|
|
(async () => {
|
|
if (!apiSchemaStr) {
|
|
return setApiData({ pathData: [], serverPath: '' });
|
|
}
|
|
try {
|
|
setApiData(await str2OpenApiSchema(apiSchemaStr));
|
|
} catch (err) {
|
|
toast({
|
|
status: 'warning',
|
|
title: t('common:plugin.Invalid Schema')
|
|
});
|
|
setApiData({ pathData: [], serverPath: '' });
|
|
}
|
|
})();
|
|
}, [apiSchemaStr, t, toast]);
|
|
|
|
return (
|
|
<>
|
|
<MyModal
|
|
isOpen
|
|
onClose={onClose}
|
|
iconSrc="core/app/type/httpPluginFill"
|
|
title={isEdit ? t('common:plugin.Edit Http Plugin') : t('common:plugin.Import Plugin')}
|
|
w={['90vw', '600px']}
|
|
h={['90vh', '80vh']}
|
|
position={'relative'}
|
|
>
|
|
<ModalBody flex={'1 0 0'} overflow={'auto'}>
|
|
<>
|
|
<Box color={'myGray.800'} fontWeight={'bold'}>
|
|
{t('common:input_name')}
|
|
</Box>
|
|
<Flex mt={3} alignItems={'center'}>
|
|
<MyTooltip label={t('common:set_avatar')}>
|
|
<Avatar
|
|
flexShrink={0}
|
|
src={avatar}
|
|
w={['28px', '32px']}
|
|
h={['28px', '32px']}
|
|
cursor={'pointer'}
|
|
borderRadius={'md'}
|
|
onClick={onOpenSelectFile}
|
|
/>
|
|
</MyTooltip>
|
|
<Input
|
|
flex={1}
|
|
ml={4}
|
|
bg={'myWhite.600'}
|
|
{...register('name', {
|
|
required: t('common:name_is_empty')
|
|
})}
|
|
/>
|
|
</Flex>
|
|
<>
|
|
<Box color={'myGray.800'} fontWeight={'bold'} mt={3}>
|
|
{t('common:plugin.Intro')}
|
|
</Box>
|
|
<Textarea
|
|
{...register('intro')}
|
|
bg={'myWhite.600'}
|
|
rows={3}
|
|
mt={3}
|
|
placeholder={t('common:core.plugin.Http plugin intro placeholder')}
|
|
/>
|
|
</>
|
|
</>
|
|
{/* import */}
|
|
<Box mt={4}>
|
|
<Box
|
|
color={'myGray.800'}
|
|
fontWeight={'bold'}
|
|
justifyContent={'space-between'}
|
|
display={'flex'}
|
|
>
|
|
<Box my={'auto'}>{'OpenAPI Schema'}</Box>
|
|
|
|
<Box>
|
|
<Flex alignItems={'center'}>
|
|
<Input
|
|
mr={2}
|
|
placeholder={t('common:plugin.Import from URL')}
|
|
h={'30px'}
|
|
w={['150px', '250px']}
|
|
fontSize={'sm'}
|
|
onBlur={(e) => setSchemaUrl(e.target.value)}
|
|
/>
|
|
<Button
|
|
size={'sm'}
|
|
variant={'whitePrimary'}
|
|
isLoading={isLoadingUrlApi}
|
|
onClick={onClickUrlLoadApi}
|
|
>
|
|
{t('common:Import')}
|
|
</Button>
|
|
</Flex>
|
|
</Box>
|
|
</Box>
|
|
<Textarea
|
|
{...register('pluginData.apiSchemaStr')}
|
|
bg={'myWhite.600'}
|
|
rows={10}
|
|
mt={3}
|
|
onBlur={(e) => {
|
|
const content = e.target.value;
|
|
if (!content) return;
|
|
setValue('pluginData.apiSchemaStr', content);
|
|
}}
|
|
/>
|
|
</Box>
|
|
<>
|
|
<Box color={'myGray.800'} fontWeight={'bold'} mt={3}>
|
|
{t('common:core.plugin.Custom headers')}
|
|
</Box>
|
|
<Box
|
|
mt={1}
|
|
borderRadius={'md'}
|
|
overflow={'hidden'}
|
|
borderWidth={'1px'}
|
|
borderBottom={'none'}
|
|
>
|
|
<TableContainer overflowY={'visible'} overflowX={'unset'}>
|
|
<Table>
|
|
<Thead>
|
|
<Tr>
|
|
<Th px={2} borderRadius="none !important">
|
|
{t('common:core.module.http.Props name')}
|
|
</Th>
|
|
<Th px={2} borderRadius="none !important">
|
|
{t('common:core.module.http.Props value')}
|
|
</Th>
|
|
</Tr>
|
|
</Thead>
|
|
<Tbody>
|
|
{customHeaders.map((item, index) => (
|
|
<Tr key={`${index}`}>
|
|
<Td p={0} w={'150px'}>
|
|
<HttpInput
|
|
placeholder={t('common:core.module.http.Props name')}
|
|
value={item.key}
|
|
onBlur={(val) => {
|
|
setCustomHeaders((prev) => {
|
|
const newHeaders = prev.map((item, i) =>
|
|
i === index ? { ...item, key: val } : item
|
|
);
|
|
setValue(
|
|
'pluginData.customHeaders',
|
|
'{\n' +
|
|
newHeaders
|
|
.map((item) => `"${item.key}":"${item.value}"`)
|
|
.join(',\n') +
|
|
'\n}'
|
|
);
|
|
return newHeaders;
|
|
});
|
|
}}
|
|
updateTrigger={updateTrigger}
|
|
/>
|
|
</Td>
|
|
<Td p={0}>
|
|
<Box display={'flex'} alignItems={'center'}>
|
|
<HttpInput
|
|
placeholder={t('common:core.module.http.Props value')}
|
|
value={item.value}
|
|
onBlur={(val) =>
|
|
setCustomHeaders((prev) => {
|
|
const newHeaders = prev.map((item, i) =>
|
|
i === index ? { ...item, value: val } : item
|
|
);
|
|
setValue(
|
|
'pluginData.customHeaders',
|
|
'{\n' +
|
|
newHeaders
|
|
.map((item) => `"${item.key}":"${item.value}"`)
|
|
.join(',\n') +
|
|
'\n}'
|
|
);
|
|
return newHeaders;
|
|
})
|
|
}
|
|
/>
|
|
<MyIcon
|
|
name={'delete'}
|
|
cursor={'pointer'}
|
|
_hover={{ color: 'red.600' }}
|
|
w={'14px'}
|
|
onClick={() =>
|
|
setCustomHeaders((prev) => {
|
|
const newHeaders = prev.filter((val) => val.key !== item.key);
|
|
setValue(
|
|
'pluginData.customHeaders',
|
|
'{\n' +
|
|
newHeaders
|
|
.map((item) => `"${item.key}":"${item.value}"`)
|
|
.join(',\n') +
|
|
'\n}'
|
|
);
|
|
return newHeaders;
|
|
})
|
|
}
|
|
/>
|
|
</Box>
|
|
</Td>
|
|
</Tr>
|
|
))}
|
|
<Tr>
|
|
<Td p={0} w={'150px'}>
|
|
<HttpInput
|
|
placeholder={t('common:core.module.http.Add props')}
|
|
value={''}
|
|
updateTrigger={updateTrigger}
|
|
onBlur={(val) => {
|
|
if (!val) return;
|
|
setCustomHeaders((prev) => {
|
|
const newHeaders = [...prev, { key: val, value: '' }];
|
|
setValue(
|
|
'pluginData.customHeaders',
|
|
'{\n' +
|
|
newHeaders
|
|
.map((item) => `"${item.key}":"${item.value}"`)
|
|
.join(',\n') +
|
|
'\n}'
|
|
);
|
|
return newHeaders;
|
|
});
|
|
setUpdateTrigger((prev) => !prev);
|
|
}}
|
|
/>
|
|
</Td>
|
|
<Td p={0}>
|
|
<Box display={'flex'} alignItems={'center'}>
|
|
<HttpInput />
|
|
</Box>
|
|
</Td>
|
|
</Tr>
|
|
</Tbody>
|
|
</Table>
|
|
</TableContainer>
|
|
</Box>
|
|
</>
|
|
<>
|
|
<Box color={'myGray.800'} fontWeight={'bold'} mt={3}>
|
|
{t('common:plugin.Plugin List')}
|
|
</Box>
|
|
<Box
|
|
mt={3}
|
|
borderRadius={'md'}
|
|
overflow={'hidden'}
|
|
borderWidth={'1px'}
|
|
borderBottom={'none'}
|
|
>
|
|
<TableContainer maxH={400} overflowY={'auto'}>
|
|
<Table bg={'white'}>
|
|
<Thead bg={'myGray.50'}>
|
|
<Th>{t('common:Name')}</Th>
|
|
<Th>{t('common:plugin.Description')}</Th>
|
|
<Th>{t('common:plugin.Method')}</Th>
|
|
<Th>{t('common:plugin.Path')}</Th>
|
|
</Thead>
|
|
<Tbody>
|
|
{apiData.pathData?.map((item, index) => (
|
|
<Tr key={index}>
|
|
<Td>{item.name}</Td>
|
|
<Td
|
|
fontSize={'sm'}
|
|
textColor={'gray.600'}
|
|
w={'auto'}
|
|
maxW={80}
|
|
whiteSpace={'pre-wrap'}
|
|
>
|
|
{item.description}
|
|
</Td>
|
|
<Td>{item.method}</Td>
|
|
<Td>{item.path}</Td>
|
|
</Tr>
|
|
))}
|
|
</Tbody>
|
|
</Table>
|
|
</TableContainer>
|
|
</Box>
|
|
</>
|
|
</ModalBody>
|
|
|
|
<ModalFooter>
|
|
<Button variant={'whiteBase'} mr={3} onClick={onClose}>
|
|
{t('common:Close')}
|
|
</Button>
|
|
{!isEdit ? (
|
|
<Button
|
|
isDisabled={apiData.pathData.length === 0}
|
|
onClick={handleSubmit((data) => onCreate(data))}
|
|
isLoading={isCreating}
|
|
>
|
|
{t('common:comfirn_create')}
|
|
</Button>
|
|
) : (
|
|
<Button
|
|
isDisabled={apiData.pathData.length === 0}
|
|
isLoading={isUpdating}
|
|
onClick={handleSubmit((data) => updatePlugins(data))}
|
|
>
|
|
{t('common:confirm_update')}
|
|
</Button>
|
|
)}
|
|
</ModalFooter>
|
|
</MyModal>
|
|
<File
|
|
onSelect={(e) =>
|
|
onSelectImage(e, {
|
|
maxH: 300,
|
|
maxW: 300,
|
|
callback: (e) => setValue('avatar', e)
|
|
})
|
|
}
|
|
/>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default HttpPluginEditModal;
|