2023-12-31 14:12:51 +08:00

375 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useEffect, useMemo, useState } from 'react';
import {
Box,
Button,
Flex,
ModalFooter,
ModalBody,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
useTheme,
Link,
Input,
MenuList,
MenuItem,
MenuButton,
Menu
} from '@chakra-ui/react';
import {
getOpenApiKeys,
createAOpenApiKey,
delOpenApiById,
putOpenApiKey
} from '@/web/support/openapi/api';
import type { EditApiKeyProps } from '@/global/support/openapi/api.d';
import { useQuery, useMutation } from '@tanstack/react-query';
import { useLoading } from '@/web/common/hooks/useLoading';
import dayjs from 'dayjs';
import { AddIcon, QuestionOutlineIcon } from '@chakra-ui/icons';
import { useCopyData } from '@/web/common/hooks/useCopyData';
import { feConfigs } from '@/web/common/system/staticData';
import { useTranslation } from 'next-i18next';
import MyIcon from '@/components/Icon';
import MyModal from '@/components/MyModal';
import { useForm } from 'react-hook-form';
import { useRequest } from '@/web/common/hooks/useRequest';
import MyTooltip from '@/components/MyTooltip';
import { getDocPath } from '@/web/common/system/doc';
type EditProps = EditApiKeyProps & { _id?: string };
const defaultEditData: EditProps = {
name: '',
limit: {
credit: -1
}
};
const ApiKeyTable = ({ tips, appId }: { tips: string; appId?: string }) => {
const { t } = useTranslation();
const { Loading } = useLoading();
const theme = useTheme();
const { copyData } = useCopyData();
const [baseUrl, setBaseUrl] = useState('https://fastgpt.run/api');
const [editData, setEditData] = useState<EditProps>();
const [apiKey, setApiKey] = useState('');
const { mutate: onclickRemove, isLoading: isDeleting } = useMutation({
mutationFn: async (id: string) => delOpenApiById(id),
onSuccess() {
refetch();
}
});
const {
data: apiKeys = [],
isLoading: isGetting,
refetch
} = useQuery(['getOpenApiKeys', appId], () => getOpenApiKeys({ appId }));
useEffect(() => {
setBaseUrl(feConfigs?.customApiDomain || `${location.origin}/api`);
}, []);
return (
<Flex flexDirection={'column'} h={'100%'} position={'relative'}>
<Box display={['block', 'flex']} py={[0, 3]} px={5} alignItems={'center'}>
<Box flex={1}>
<Flex alignItems={'flex-end'}>
<Box fontSize={['md', 'xl']} fontWeight={'bold'}>
API
</Box>
{feConfigs?.docUrl && (
<Link
href={feConfigs.openAPIDocUrl || getDocPath('/docs/development/openapi')}
target={'_blank'}
ml={1}
color={'primary.500'}
>
</Link>
)}
</Flex>
<Box fontSize={'sm'} color={'myGray.600'}>
{tips}
</Box>
</Box>
<Flex
mt={[2, 0]}
bg={'myWhite.600'}
py={2}
px={4}
borderRadius={'md'}
cursor={'pointer'}
userSelect={'none'}
onClick={() => copyData(baseUrl, '已复制 API 地址')}
>
<Box border={theme.borders.md} px={2} borderRadius={'md'} fontSize={'sm'}>
API根地址
</Box>
<Box ml={2} color={'myGray.900'} fontSize={['sm', 'md']}>
{baseUrl}
</Box>
</Flex>
<Box mt={[2, 0]} textAlign={'right'}>
<Button
ml={3}
leftIcon={<AddIcon fontSize={'md'} />}
variant={'whitePrimary'}
onClick={() =>
setEditData({
...defaultEditData,
appId
})
}
>
</Button>
</Box>
</Box>
<TableContainer mt={2} position={'relative'} minH={'300px'}>
<Table>
<Thead>
<Tr>
<Th>{t('Name')}</Th>
<Th>Api Key</Th>
<Th>()</Th>
{feConfigs?.isPlus && (
<>
<Th>()</Th>
<Th></Th>
</>
)}
<Th></Th>
<Th>使</Th>
<Th />
</Tr>
</Thead>
<Tbody fontSize={'sm'}>
{apiKeys.map(({ _id, name, usage, limit, apiKey, createTime, lastUsedTime }) => (
<Tr key={_id}>
<Td>{name}</Td>
<Td>{apiKey}</Td>
<Td>{usage}</Td>
{feConfigs?.isPlus && (
<>
<Td>{limit?.credit && limit?.credit > -1 ? `${limit?.credit}` : '无限制'}</Td>
<Td whiteSpace={'pre-wrap'}>
{limit?.expiredTime
? dayjs(limit?.expiredTime).format('YYYY/MM/DD\nHH:mm')
: '-'}
</Td>
</>
)}
<Td whiteSpace={'pre-wrap'}>{dayjs(createTime).format('YYYY/MM/DD\nHH:mm:ss')}</Td>
<Td whiteSpace={'pre-wrap'}>
{lastUsedTime ? dayjs(lastUsedTime).format('YYYY/MM/DD\nHH:mm:ss') : '没有使用过'}
</Td>
<Td>
<Menu autoSelect={false} isLazy>
<MenuButton
_hover={{ bg: 'myWhite.600 ' }}
cursor={'pointer'}
borderRadius={'md'}
>
<MyIcon name={'more'} w={'14px'} p={2} />
</MenuButton>
<MenuList color={'myGray.700'} minW={`120px !important`} zIndex={10}>
<MenuItem
onClick={() =>
setEditData({
_id,
name,
limit,
appId
})
}
py={[2, 3]}
>
<MyIcon name={'edit'} w={['14px', '16px']} />
<Box ml={[1, 2]}>{t('common.Edit')}</Box>
</MenuItem>
<MenuItem onClick={() => onclickRemove(_id)} py={[2, 3]}>
<MyIcon name={'delete'} w={['14px', '16px']} />
<Box ml={[1, 2]}>{t('common.Delete')}</Box>
</MenuItem>
</MenuList>
</Menu>
</Td>
</Tr>
))}
</Tbody>
</Table>
<Loading loading={isGetting || isDeleting} fixed={false} />
</TableContainer>
{!!editData && (
<EditKeyModal
defaultData={editData}
onClose={() => setEditData(undefined)}
onCreate={(id) => {
setApiKey(id);
refetch();
setEditData(undefined);
}}
onEdit={() => {
refetch();
setEditData(undefined);
}}
/>
)}
<MyModal
isOpen={!!apiKey}
w={['400px', '600px']}
iconSrc="/imgs/modal/key.svg"
title={
<Box>
<Box fontWeight={'bold'} fontSize={'xl'}>
API
</Box>
<Box fontSize={'sm'} color={'myGray.600'}>
~
</Box>
</Box>
}
onClose={() => setApiKey('')}
>
<ModalBody pt={5}>
<Flex
bg={'myGray.100'}
px={3}
py={2}
whiteSpace={'pre-wrap'}
wordBreak={'break-all'}
cursor={'pointer'}
borderRadius={'md'}
onClick={() => copyData(apiKey)}
>
<Box flex={1}>{apiKey}</Box>
<MyIcon ml={1} name={'copy'} w={'16px'}></MyIcon>
</Flex>
</ModalBody>
<ModalFooter>
<Button variant="whiteBase" onClick={() => setApiKey('')}>
{t('common.OK')}
</Button>
</ModalFooter>
</MyModal>
</Flex>
);
};
export default React.memo(ApiKeyTable);
// edit link modal
function EditKeyModal({
defaultData,
onClose,
onCreate,
onEdit
}: {
defaultData: EditProps;
onClose: () => void;
onCreate: (id: string) => void;
onEdit: () => void;
}) {
const { t } = useTranslation();
const isEdit = useMemo(() => !!defaultData._id, [defaultData]);
const {
register,
setValue,
handleSubmit: submitShareChat
} = useForm({
defaultValues: defaultData
});
const { mutate: onclickCreate, isLoading: creating } = useRequest({
mutationFn: async (e: EditProps) => createAOpenApiKey(e),
errorToast: '创建链接异常',
onSuccess: onCreate
});
const { mutate: onclickUpdate, isLoading: updating } = useRequest({
mutationFn: (e: EditProps) => {
//@ts-ignore
return putOpenApiKey(e);
},
errorToast: '更新链接异常',
onSuccess: onEdit
});
return (
<MyModal
isOpen={true}
iconSrc="/imgs/modal/key.svg"
title={isEdit ? t('outlink.Edit API Key') : t('outlink.Create API Key')}
>
<ModalBody>
<Flex alignItems={'center'}>
<Box flex={'0 0 90px'}>{t('Name')}:</Box>
<Input
placeholder={t('openapi.key alias') || 'key alias'}
maxLength={20}
{...register('name', {
required: t('common.Name is empty') || 'Name is empty'
})}
/>
</Flex>
{feConfigs?.isPlus && (
<>
<Flex alignItems={'center'} mt={4}>
<Flex flex={'0 0 90px'} alignItems={'center'}>
{t('common.Max credit')}:
<MyTooltip label={t('common.Max credit tips' || '')}>
<QuestionOutlineIcon ml={1} />
</MyTooltip>
</Flex>
<Input
{...register('limit.credit', {
min: -1,
max: 1000,
valueAsNumber: true,
required: true
})}
/>
</Flex>
<Flex alignItems={'center'} mt={4}>
<Flex flex={'0 0 90px'} alignItems={'center'}>
{t('common.Expired Time')}:
</Flex>
<Input
type="datetime-local"
defaultValue={
defaultData.limit?.expiredTime
? dayjs(defaultData.limit?.expiredTime).format('YYYY-MM-DDTHH:mm')
: ''
}
onChange={(e) => {
setValue('limit.expiredTime', new Date(e.target.value));
}}
/>
</Flex>
</>
)}
</ModalBody>
<ModalFooter>
<Button variant={'whiteBase'} mr={3} onClick={onClose}>
{t('Cancel')}
</Button>
<Button
isLoading={creating || updating}
onClick={submitShareChat((data) => (isEdit ? onclickUpdate(data) : onclickCreate(data)))}
>
{t('Confirm')}
</Button>
</ModalFooter>
</MyModal>
);
}