mirror of
https://github.com/labring/sealos.git
synced 2026-10-29 17:25:58 +08:00
chore(aiproxy-froent): adjust code format style (#6088)
* chore format * chore
This commit is contained in:
@@ -28,4 +28,5 @@ pnpm-lock.yaml
|
||||
public/
|
||||
.git/
|
||||
.vscode/
|
||||
.idea/
|
||||
.idea/
|
||||
deploy/
|
||||
@@ -11,6 +11,18 @@
|
||||
"jsxBracketSameLine": false,
|
||||
"embeddedLanguageFormatting": "auto",
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.tsx",
|
||||
"options": {
|
||||
"semi": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": "*.jsx",
|
||||
"options": {
|
||||
"semi": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["*.json", "*.jsonc"],
|
||||
"options": {
|
||||
|
||||
+77
-77
@@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
'use client';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Checkbox,
|
||||
@@ -18,51 +18,51 @@ import {
|
||||
Thead,
|
||||
Tr,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react'
|
||||
import { useMessage } from '@sealos/ui'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
} from '@chakra-ui/react';
|
||||
import { useMessage } from '@sealos/ui';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
Column,
|
||||
createColumnHelper,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
} from '@tanstack/react-table'
|
||||
} from '@tanstack/react-table';
|
||||
|
||||
import {
|
||||
deleteChannel,
|
||||
getChannels,
|
||||
getChannelTypeNames,
|
||||
updateChannelStatus,
|
||||
} from '@/api/platform'
|
||||
import { useTranslationClientSide } from '@/app/i18n/client'
|
||||
import SwitchPage from '@/components/common/SwitchPage'
|
||||
import { useI18n } from '@/providers/i18n/i18nContext'
|
||||
import { ChannelInfo, ChannelStatus, ChannelType } from '@/types/admin/channels/channelInfo'
|
||||
import { QueryKey } from '@/types/query-key'
|
||||
import { downloadJson } from '@/utils/common'
|
||||
} from '@/api/platform';
|
||||
import { useTranslationClientSide } from '@/app/i18n/client';
|
||||
import SwitchPage from '@/components/common/SwitchPage';
|
||||
import { useI18n } from '@/providers/i18n/i18nContext';
|
||||
import { ChannelInfo, ChannelStatus, ChannelType } from '@/types/admin/channels/channelInfo';
|
||||
import { QueryKey } from '@/types/query-key';
|
||||
import { downloadJson } from '@/utils/common';
|
||||
|
||||
import UpdateChannelModal from './UpdateChannelModal'
|
||||
import UpdateChannelModal from './UpdateChannelModal';
|
||||
|
||||
export default function ChannelTable({
|
||||
exportData,
|
||||
}: {
|
||||
exportData: (data: ChannelInfo[]) => void
|
||||
exportData: (data: ChannelInfo[]) => void;
|
||||
}) {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||
const { lng } = useI18n()
|
||||
const { t } = useTranslationClientSide(lng, 'common')
|
||||
const [operationType, setOperationType] = useState<'create' | 'update'>('update')
|
||||
const [channelInfo, setChannelInfo] = useState<ChannelInfo | undefined>(undefined)
|
||||
const [total, setTotal] = useState(0)
|
||||
const [page, setPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(10)
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const { lng } = useI18n();
|
||||
const { t } = useTranslationClientSide(lng, 'common');
|
||||
const [operationType, setOperationType] = useState<'create' | 'update'>('update');
|
||||
const [channelInfo, setChannelInfo] = useState<ChannelInfo | undefined>(undefined);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
|
||||
const [selectedChannels, setSelectedChannels] = useState<ChannelInfo[]>([])
|
||||
const [selectedChannels, setSelectedChannels] = useState<ChannelInfo[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
exportData(selectedChannels)
|
||||
}, [selectedChannels])
|
||||
exportData(selectedChannels);
|
||||
}, [selectedChannels]);
|
||||
|
||||
const { message } = useMessage({
|
||||
warningBoxBg: '#FFFAEB',
|
||||
@@ -71,23 +71,23 @@ export default function ChannelTable({
|
||||
successBoxBg: '#EDFBF3',
|
||||
successIconBg: '#039855',
|
||||
successIconFill: 'white',
|
||||
})
|
||||
});
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { isLoading: isChannelTypeNamesLoading, data: channelTypeNames } = useQuery({
|
||||
queryKey: [QueryKey.GetChannelTypeNames],
|
||||
queryFn: () => getChannelTypeNames(),
|
||||
})
|
||||
});
|
||||
|
||||
const { data, isLoading: isChannelsLoading } = useQuery({
|
||||
queryKey: [QueryKey.GetChannels, page, pageSize],
|
||||
queryFn: () => getChannels({ page, perPage: pageSize }),
|
||||
refetchOnReconnect: true,
|
||||
onSuccess(data) {
|
||||
setTotal(data?.total || 0)
|
||||
setTotal(data?.total || 0);
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const updateChannelStatusMutation = useMutation(
|
||||
({ id, status }: { id: string; status: number }) => updateChannelStatus(id, status),
|
||||
@@ -99,9 +99,9 @@ export default function ChannelTable({
|
||||
isClosable: true,
|
||||
duration: 2000,
|
||||
position: 'top',
|
||||
})
|
||||
queryClient.invalidateQueries([QueryKey.GetChannels])
|
||||
queryClient.invalidateQueries([QueryKey.GetChannelTypeNames])
|
||||
});
|
||||
queryClient.invalidateQueries([QueryKey.GetChannels]);
|
||||
queryClient.invalidateQueries([QueryKey.GetChannelTypeNames]);
|
||||
},
|
||||
onError(err: any) {
|
||||
message({
|
||||
@@ -110,10 +110,10 @@ export default function ChannelTable({
|
||||
description: err?.message || t('channel.updateFailed'),
|
||||
isClosable: true,
|
||||
position: 'top',
|
||||
})
|
||||
});
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
const deleteChannelMutation = useMutation(({ id }: { id: string }) => deleteChannel(id), {
|
||||
onSuccess() {
|
||||
message({
|
||||
@@ -122,9 +122,9 @@ export default function ChannelTable({
|
||||
isClosable: true,
|
||||
duration: 2000,
|
||||
position: 'top',
|
||||
})
|
||||
queryClient.invalidateQueries([QueryKey.GetChannels])
|
||||
queryClient.invalidateQueries([QueryKey.GetChannelTypeNames])
|
||||
});
|
||||
queryClient.invalidateQueries([QueryKey.GetChannels]);
|
||||
queryClient.invalidateQueries([QueryKey.GetChannelTypeNames]);
|
||||
},
|
||||
onError(err: any) {
|
||||
message({
|
||||
@@ -133,42 +133,42 @@ export default function ChannelTable({
|
||||
description: err?.message || t('channel.deleteFailed'),
|
||||
isClosable: true,
|
||||
position: 'top',
|
||||
})
|
||||
});
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
// Update the button click handlers in the table actions column:
|
||||
const handleStatusUpdate = (id: string, currentStatus: number) => {
|
||||
const newStatus =
|
||||
currentStatus === ChannelStatus.ChannelStatusDisabled
|
||||
? ChannelStatus.ChannelStatusEnabled
|
||||
: ChannelStatus.ChannelStatusDisabled
|
||||
updateChannelStatusMutation.mutate({ id, status: newStatus })
|
||||
}
|
||||
: ChannelStatus.ChannelStatusDisabled;
|
||||
updateChannelStatusMutation.mutate({ id, status: newStatus });
|
||||
};
|
||||
|
||||
const columnHelper = createColumnHelper<ChannelInfo>()
|
||||
const columnHelper = createColumnHelper<ChannelInfo>();
|
||||
|
||||
const handleHeaderCheckboxChange = (isChecked: boolean) => {
|
||||
if (isChecked) {
|
||||
setSelectedChannels(data?.channels || [])
|
||||
setSelectedChannels(data?.channels || []);
|
||||
} else {
|
||||
setSelectedChannels([])
|
||||
setSelectedChannels([]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleRowCheckboxChange = (channel: ChannelInfo, isChecked: boolean) => {
|
||||
if (isChecked) {
|
||||
setSelectedChannels([...selectedChannels, channel])
|
||||
setSelectedChannels([...selectedChannels, channel]);
|
||||
} else {
|
||||
setSelectedChannels(selectedChannels.filter((c) => c.id !== channel.id))
|
||||
setSelectedChannels(selectedChannels.filter((c) => c.id !== channel.id));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleExportRow = (channel: ChannelInfo) => {
|
||||
const channels = [channel]
|
||||
const filename = `channel_${channels[0].id}_${new Date().toISOString()}.json`
|
||||
downloadJson(channels, filename)
|
||||
}
|
||||
const channels = [channel];
|
||||
const filename = `channel_${channels[0].id}_${new Date().toISOString()}.json`;
|
||||
downloadJson(channels, filename);
|
||||
};
|
||||
|
||||
const columns = [
|
||||
columnHelper.accessor((row) => row.id, {
|
||||
@@ -345,26 +345,26 @@ export default function ChannelTable({
|
||||
</Text>
|
||||
),
|
||||
cell: (info) => {
|
||||
const status = info.getValue()
|
||||
let statusText = ''
|
||||
let statusColor = ''
|
||||
const status = info.getValue();
|
||||
let statusText = '';
|
||||
let statusColor = '';
|
||||
|
||||
switch (status) {
|
||||
case ChannelStatus.ChannelStatusEnabled:
|
||||
statusText = t('keystatus.enabled')
|
||||
statusColor = 'green.600'
|
||||
break
|
||||
statusText = t('keystatus.enabled');
|
||||
statusColor = 'green.600';
|
||||
break;
|
||||
case ChannelStatus.ChannelStatusDisabled:
|
||||
statusText = t('keystatus.disabled')
|
||||
statusColor = 'red.600'
|
||||
break
|
||||
statusText = t('keystatus.disabled');
|
||||
statusColor = 'red.600';
|
||||
break;
|
||||
case ChannelStatus.ChannelStatusAutoDisabled:
|
||||
statusText = t('channelStatus.autoDisabled')
|
||||
statusColor = 'orange.500'
|
||||
break
|
||||
statusText = t('channelStatus.autoDisabled');
|
||||
statusColor = 'orange.500';
|
||||
break;
|
||||
default:
|
||||
statusText = t('keystatus.unknown')
|
||||
statusColor = 'gray.500'
|
||||
statusText = t('keystatus.unknown');
|
||||
statusColor = 'gray.500';
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -378,7 +378,7 @@ export default function ChannelTable({
|
||||
>
|
||||
{statusText}
|
||||
</Text>
|
||||
)
|
||||
);
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -570,9 +570,9 @@ export default function ChannelTable({
|
||||
color: 'brightBlue.600',
|
||||
}}
|
||||
onClick={() => {
|
||||
setOperationType('update')
|
||||
setChannelInfo(info.row.original)
|
||||
onOpen()
|
||||
setOperationType('update');
|
||||
setChannelInfo(info.row.original);
|
||||
onOpen();
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
@@ -689,15 +689,15 @@ export default function ChannelTable({
|
||||
</Menu>
|
||||
),
|
||||
}),
|
||||
]
|
||||
];
|
||||
|
||||
const tableData = useMemo(() => data?.channels || [], [data])
|
||||
const tableData = useMemo(() => data?.channels || [], [data]);
|
||||
|
||||
const table = useReactTable({
|
||||
data: tableData,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
})
|
||||
});
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -780,5 +780,5 @@ export default function ChannelTable({
|
||||
channelInfo={channelInfo}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
+107
-107
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
import { Dispatch, SetStateAction, useEffect } from 'react'
|
||||
import { Controller, FieldErrors, useForm } from 'react-hook-form'
|
||||
'use client';
|
||||
import { Dispatch, SetStateAction, useEffect } from 'react';
|
||||
import { Controller, FieldErrors, useForm } from 'react-hook-form';
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
@@ -20,30 +20,30 @@ import {
|
||||
Spinner,
|
||||
Text,
|
||||
VStack,
|
||||
} from '@chakra-ui/react'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { useMessage } from '@sealos/ui'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { z } from 'zod'
|
||||
} from '@chakra-ui/react';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useMessage } from '@sealos/ui';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
getChannelBuiltInSupportModels,
|
||||
getChannelDefaultModelAndDefaultModeMapping,
|
||||
getChannelTypeNames,
|
||||
} from '@/api/platform'
|
||||
import { createChannel, updateChannel } from '@/api/platform'
|
||||
import { useTranslationClientSide } from '@/app/i18n/client'
|
||||
import ConstructModeMappingComponent from '@/components/common/ConstructModeMappingComponent'
|
||||
import { MultiSelectCombobox } from '@/components/common/MultiSelectCombobox'
|
||||
import { SingleSelectCombobox } from '@/components/common/SingleSelectCombobox'
|
||||
import { useI18n } from '@/providers/i18n/i18nContext'
|
||||
import { ChannelInfo, ChannelStatus, ChannelType } from '@/types/admin/channels/channelInfo'
|
||||
import { QueryKey } from '@/types/query-key'
|
||||
} from '@/api/platform';
|
||||
import { createChannel, updateChannel } from '@/api/platform';
|
||||
import { useTranslationClientSide } from '@/app/i18n/client';
|
||||
import ConstructModeMappingComponent from '@/components/common/ConstructModeMappingComponent';
|
||||
import { MultiSelectCombobox } from '@/components/common/MultiSelectCombobox';
|
||||
import { SingleSelectCombobox } from '@/components/common/SingleSelectCombobox';
|
||||
import { useI18n } from '@/providers/i18n/i18nContext';
|
||||
import { ChannelInfo, ChannelStatus, ChannelType } from '@/types/admin/channels/channelInfo';
|
||||
import { QueryKey } from '@/types/query-key';
|
||||
|
||||
type Model = {
|
||||
name: string
|
||||
isDefault: boolean
|
||||
}
|
||||
name: string;
|
||||
isDefault: boolean;
|
||||
};
|
||||
|
||||
export const UpdateChannelModal = function ({
|
||||
isOpen,
|
||||
@@ -51,14 +51,14 @@ export const UpdateChannelModal = function ({
|
||||
operationType,
|
||||
channelInfo,
|
||||
}: {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
operationType: 'create' | 'update'
|
||||
channelInfo?: ChannelInfo
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
operationType: 'create' | 'update';
|
||||
channelInfo?: ChannelInfo;
|
||||
}): JSX.Element {
|
||||
const { lng } = useI18n()
|
||||
const { t } = useTranslationClientSide(lng, 'common')
|
||||
const queryClient = useQueryClient()
|
||||
const { lng } = useI18n();
|
||||
const { t } = useTranslationClientSide(lng, 'common');
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { message } = useMessage({
|
||||
warningBoxBg: '#FFFAEB',
|
||||
@@ -68,30 +68,30 @@ export const UpdateChannelModal = function ({
|
||||
successBoxBg: '#EDFBF3',
|
||||
successIconBg: '#039855',
|
||||
successIconFill: 'white',
|
||||
})
|
||||
});
|
||||
|
||||
const { isLoading: isChannelTypeNamesLoading, data: channelTypeNames } = useQuery({
|
||||
queryKey: [QueryKey.GetChannelTypeNames],
|
||||
queryFn: () => getChannelTypeNames(),
|
||||
})
|
||||
});
|
||||
|
||||
const { isLoading: isBuiltInSupportModelsLoading, data: builtInSupportModels } = useQuery({
|
||||
queryKey: [QueryKey.GetAllChannelModes],
|
||||
queryFn: () => getChannelBuiltInSupportModels(),
|
||||
})
|
||||
});
|
||||
|
||||
const { isLoading: isDefaultEnabledModelsLoading, data: defaultEnabledModels } = useQuery({
|
||||
queryKey: [QueryKey.GetDefaultModelAndModeMapping],
|
||||
queryFn: () => getChannelDefaultModelAndDefaultModeMapping(),
|
||||
})
|
||||
});
|
||||
|
||||
// model type select combobox
|
||||
const handleModelTypeDropdownItemFilter = (dropdownItems: string[], inputValue: string) => {
|
||||
const lowerCasedInput = inputValue.toLowerCase()
|
||||
const lowerCasedInput = inputValue.toLowerCase();
|
||||
return dropdownItems.filter(
|
||||
(item) => !inputValue || item.toLowerCase().includes(lowerCasedInput)
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const handleModelTypeDropdownItemDisplay = (dropdownItem: string) => {
|
||||
return (
|
||||
@@ -106,8 +106,8 @@ export const UpdateChannelModal = function ({
|
||||
>
|
||||
{dropdownItem}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// model select combobox
|
||||
const handleModelFilteredDropdownItems = (
|
||||
@@ -115,17 +115,17 @@ export const UpdateChannelModal = function ({
|
||||
selectedItems: Model[],
|
||||
inputValue: string
|
||||
) => {
|
||||
const lowerCasedInputValue = inputValue.toLowerCase()
|
||||
const lowerCasedInputValue = inputValue.toLowerCase();
|
||||
|
||||
// First filter out items that are already selected
|
||||
const unselectedItems = dropdownItems.filter(
|
||||
(dropdownItem) =>
|
||||
!selectedItems.some((selectedItem) => selectedItem.name === dropdownItem.name)
|
||||
)
|
||||
);
|
||||
|
||||
// Then filter by input value
|
||||
return unselectedItems.filter((item) => item.name.toLowerCase().includes(lowerCasedInputValue))
|
||||
}
|
||||
return unselectedItems.filter((item) => item.name.toLowerCase().includes(lowerCasedInputValue));
|
||||
};
|
||||
|
||||
const handleModelDropdownItemDisplay = (dropdownItem: Model) => {
|
||||
if (dropdownItem.isDefault) {
|
||||
@@ -190,7 +190,7 @@ export const UpdateChannelModal = function ({
|
||||
</Text>
|
||||
</Badge>
|
||||
</Flex>
|
||||
)
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Text
|
||||
@@ -203,8 +203,8 @@ export const UpdateChannelModal = function ({
|
||||
>
|
||||
{dropdownItem.name}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const handleModelSelectedItemDisplay = (selectedItem: Model) => {
|
||||
if (selectedItem.isDefault) {
|
||||
@@ -259,7 +259,7 @@ export const UpdateChannelModal = function ({
|
||||
</svg>
|
||||
</Badge>
|
||||
</Flex>
|
||||
)
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Text
|
||||
@@ -273,8 +273,8 @@ export const UpdateChannelModal = function ({
|
||||
>
|
||||
{selectedItem.name}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const handleSetCustomModel = (
|
||||
selectedItems: Model[],
|
||||
@@ -286,16 +286,16 @@ export const UpdateChannelModal = function ({
|
||||
const newModel: Model = {
|
||||
name: customModeName.trim(),
|
||||
isDefault: false,
|
||||
}
|
||||
};
|
||||
|
||||
const exists = selectedItems.some((item) => item.name === customModeName.trim())
|
||||
const exists = selectedItems.some((item) => item.name === customModeName.trim());
|
||||
|
||||
if (!exists) {
|
||||
setSelectedItems([...selectedItems, newModel])
|
||||
setCustomModeName('')
|
||||
setSelectedItems([...selectedItems, newModel]);
|
||||
setCustomModeName('');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// form schema
|
||||
const schema = z.object({
|
||||
@@ -306,10 +306,10 @@ export const UpdateChannelModal = function ({
|
||||
base_url: z.string(),
|
||||
models: z.array(z.string()).default([]),
|
||||
model_mapping: z.record(z.string(), z.any()).default({}),
|
||||
})
|
||||
});
|
||||
|
||||
const id = channelInfo?.id
|
||||
type FormData = z.infer<typeof schema>
|
||||
const id = channelInfo?.id;
|
||||
type FormData = z.infer<typeof schema>;
|
||||
|
||||
const {
|
||||
register,
|
||||
@@ -332,18 +332,18 @@ export const UpdateChannelModal = function ({
|
||||
},
|
||||
mode: 'onChange',
|
||||
reValidateMode: 'onChange',
|
||||
})
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (channelInfo) {
|
||||
const { id, type, name, key, base_url, models, model_mapping } = channelInfo
|
||||
reset({ id, type, name, key, base_url, models, model_mapping })
|
||||
const { id, type, name, key, base_url, models, model_mapping } = channelInfo;
|
||||
reset({ id, type, name, key, base_url, models, model_mapping });
|
||||
}
|
||||
}, [channelInfo])
|
||||
}, [channelInfo]);
|
||||
|
||||
const resetModalState = () => {
|
||||
reset()
|
||||
}
|
||||
reset();
|
||||
};
|
||||
|
||||
const createChannelMutation = useMutation({
|
||||
mutationFn: createChannel,
|
||||
@@ -351,9 +351,9 @@ export const UpdateChannelModal = function ({
|
||||
message({
|
||||
title: t('channels.createSuccess'),
|
||||
status: 'success',
|
||||
})
|
||||
});
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const updateChannelMutation = useMutation({
|
||||
mutationFn: (data: FormData) =>
|
||||
@@ -372,9 +372,9 @@ export const UpdateChannelModal = function ({
|
||||
message({
|
||||
title: t('channels.updateSuccess'),
|
||||
status: 'success',
|
||||
})
|
||||
});
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const onValidate = async (data: FormData) => {
|
||||
try {
|
||||
@@ -387,18 +387,18 @@ export const UpdateChannelModal = function ({
|
||||
base_url: data.base_url,
|
||||
models: data.models,
|
||||
model_mapping: data.model_mapping,
|
||||
})
|
||||
break
|
||||
});
|
||||
break;
|
||||
case 'update':
|
||||
await updateChannelMutation.mutateAsync(data)
|
||||
break
|
||||
await updateChannelMutation.mutateAsync(data);
|
||||
break;
|
||||
}
|
||||
queryClient.invalidateQueries({ queryKey: [QueryKey.GetChannels] })
|
||||
queryClient.invalidateQueries({ queryKey: [QueryKey.GetDefaultModelAndModeMapping] })
|
||||
queryClient.invalidateQueries({ queryKey: [QueryKey.GetChannelTypeNames] })
|
||||
queryClient.invalidateQueries({ queryKey: [QueryKey.GetAllChannelModes] })
|
||||
resetModalState()
|
||||
onClose()
|
||||
queryClient.invalidateQueries({ queryKey: [QueryKey.GetChannels] });
|
||||
queryClient.invalidateQueries({ queryKey: [QueryKey.GetDefaultModelAndModeMapping] });
|
||||
queryClient.invalidateQueries({ queryKey: [QueryKey.GetChannelTypeNames] });
|
||||
queryClient.invalidateQueries({ queryKey: [QueryKey.GetAllChannelModes] });
|
||||
resetModalState();
|
||||
onClose();
|
||||
} catch (error) {
|
||||
switch (operationType) {
|
||||
case 'create':
|
||||
@@ -409,8 +409,8 @@ export const UpdateChannelModal = function ({
|
||||
duration: 2000,
|
||||
isClosable: true,
|
||||
description: error instanceof Error ? error.message : t('channels.createFailed'),
|
||||
})
|
||||
break
|
||||
});
|
||||
break;
|
||||
case 'update':
|
||||
message({
|
||||
title: t('channels.updateFailed'),
|
||||
@@ -419,14 +419,14 @@ export const UpdateChannelModal = function ({
|
||||
duration: 2000,
|
||||
isClosable: true,
|
||||
description: error instanceof Error ? error.message : t('channels.updateFailed'),
|
||||
})
|
||||
break
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onInvalid = (errors: FieldErrors<FormData>): void => {
|
||||
const firstErrorMessage = Object.values(errors)[0]?.message
|
||||
const firstErrorMessage = Object.values(errors)[0]?.message;
|
||||
if (firstErrorMessage) {
|
||||
message({
|
||||
title: firstErrorMessage as string,
|
||||
@@ -434,11 +434,11 @@ export const UpdateChannelModal = function ({
|
||||
position: 'top',
|
||||
duration: 2000,
|
||||
isClosable: true,
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = handleSubmit(onValidate, onInvalid)
|
||||
const onSubmit = handleSubmit(onValidate, onInvalid);
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} isCentered>
|
||||
@@ -611,11 +611,11 @@ export const UpdateChannelModal = function ({
|
||||
render={({ field }) => {
|
||||
const availableChannels = Object.entries(channelTypeNames)
|
||||
.filter(([channel]) => channel in builtInSupportModels)
|
||||
.map(([_, name]) => name)
|
||||
.map(([_, name]) => name);
|
||||
|
||||
const initSelectedItem = field.value
|
||||
? channelTypeNames[String(field.value) as ChannelType]
|
||||
: undefined
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<SingleSelectCombobox<string>
|
||||
@@ -625,20 +625,20 @@ export const UpdateChannelModal = function ({
|
||||
if (channelName) {
|
||||
const channelType = Object.entries(channelTypeNames).find(
|
||||
([_, name]) => name === channelName
|
||||
)?.[0]
|
||||
)?.[0];
|
||||
|
||||
if (channelType) {
|
||||
const numericChannel = Number(channelType)
|
||||
field.onChange(numericChannel)
|
||||
setValue('models', [])
|
||||
setValue('model_mapping', {})
|
||||
const numericChannel = Number(channelType);
|
||||
field.onChange(numericChannel);
|
||||
setValue('models', []);
|
||||
setValue('model_mapping', {});
|
||||
}
|
||||
}
|
||||
}}
|
||||
handleDropdownItemFilter={handleModelTypeDropdownItemFilter}
|
||||
handleDropdownItemDisplay={handleModelTypeDropdownItemDisplay}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{errors.type && <FormErrorMessage>{errors.type.message}</FormErrorMessage>}
|
||||
@@ -649,35 +649,35 @@ export const UpdateChannelModal = function ({
|
||||
name="models"
|
||||
control={control}
|
||||
render={({ field }) => {
|
||||
const channelType = String(watch('type')) as ChannelType
|
||||
const channelType = String(watch('type')) as ChannelType;
|
||||
|
||||
const builtInModes =
|
||||
builtInSupportModels[channelType]?.map((mode) => mode.model) || []
|
||||
const defaultModes = defaultEnabledModels.models[channelType] || []
|
||||
builtInSupportModels[channelType]?.map((mode) => mode.model) || [];
|
||||
const defaultModes = defaultEnabledModels.models[channelType] || [];
|
||||
|
||||
const allModes: Model[] = builtInModes.map((modeName) => ({
|
||||
name: modeName,
|
||||
isDefault: defaultModes.includes(modeName),
|
||||
}))
|
||||
}));
|
||||
|
||||
const selectedModels: Model[] = field.value.map((modeName) => ({
|
||||
name: modeName,
|
||||
isDefault: defaultModes.includes(modeName),
|
||||
}))
|
||||
}));
|
||||
|
||||
return (
|
||||
<MultiSelectCombobox<Model>
|
||||
dropdownItems={allModes}
|
||||
selectedItems={selectedModels}
|
||||
setSelectedItems={(models) => {
|
||||
field.onChange((models as Model[]).map((m) => m.name))
|
||||
field.onChange((models as Model[]).map((m) => m.name));
|
||||
}}
|
||||
handleFilteredDropdownItems={handleModelFilteredDropdownItems}
|
||||
handleDropdownItemDisplay={handleModelDropdownItemDisplay}
|
||||
handleSelectedItemDisplay={handleModelSelectedItemDisplay}
|
||||
handleSetCustomSelectedItem={handleSetCustomModel}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{errors.models && <FormErrorMessage>{errors.models.message}</FormErrorMessage>}
|
||||
@@ -688,24 +688,24 @@ export const UpdateChannelModal = function ({
|
||||
name="model_mapping"
|
||||
control={control}
|
||||
render={({ field }) => {
|
||||
const channelType = String(watch('type')) as ChannelType
|
||||
const channelType = String(watch('type')) as ChannelType;
|
||||
|
||||
const selectedModels = watch('models')
|
||||
const defaultModes = defaultEnabledModels.models[channelType] || []
|
||||
const selectedModels = watch('models');
|
||||
const defaultModes = defaultEnabledModels.models[channelType] || [];
|
||||
|
||||
const covertedSelectedModels: Model[] = selectedModels.map((modeName) => ({
|
||||
name: modeName,
|
||||
isDefault: defaultModes.includes(modeName),
|
||||
}))
|
||||
}));
|
||||
return (
|
||||
<ConstructModeMappingComponent
|
||||
mapKeys={covertedSelectedModels}
|
||||
mapData={field.value}
|
||||
setMapData={(mapping) => {
|
||||
field.onChange(mapping)
|
||||
field.onChange(mapping);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{errors.model_mapping?.message && (
|
||||
@@ -849,7 +849,7 @@ export const UpdateChannelModal = function ({
|
||||
</>
|
||||
))}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default UpdateChannelModal
|
||||
export default UpdateChannelModal;
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
'use client'
|
||||
import { useRef, useState } from 'react'
|
||||
import { Button, Flex, Text, useDisclosure, useToast } from '@chakra-ui/react'
|
||||
import { useMessage } from '@sealos/ui'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
'use client';
|
||||
import { useRef, useState } from 'react';
|
||||
import { Button, Flex, Text, useDisclosure, useToast } from '@chakra-ui/react';
|
||||
import { useMessage } from '@sealos/ui';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { getAllChannels, uploadChannels } from '@/api/platform'
|
||||
import { useTranslationClientSide } from '@/app/i18n/client'
|
||||
import { useI18n } from '@/providers/i18n/i18nContext'
|
||||
import { ChannelInfo } from '@/types/admin/channels/channelInfo'
|
||||
import { QueryKey } from '@/types/query-key'
|
||||
import { downloadJson } from '@/utils/common'
|
||||
import { getAllChannels, uploadChannels } from '@/api/platform';
|
||||
import { useTranslationClientSide } from '@/app/i18n/client';
|
||||
import { useI18n } from '@/providers/i18n/i18nContext';
|
||||
import { ChannelInfo } from '@/types/admin/channels/channelInfo';
|
||||
import { QueryKey } from '@/types/query-key';
|
||||
import { downloadJson } from '@/utils/common';
|
||||
|
||||
import ChannelTable from './components/ChannelTable'
|
||||
import UpdateChannelModal from './components/UpdateChannelModal'
|
||||
import ChannelTable from './components/ChannelTable';
|
||||
import UpdateChannelModal from './components/UpdateChannelModal';
|
||||
|
||||
export default function DashboardPage() {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||
const [operationType, setOperationType] = useState<'create' | 'update'>('create')
|
||||
const { lng } = useI18n()
|
||||
const { t } = useTranslationClientSide(lng, 'common')
|
||||
const [exportData, setExportData] = useState<ChannelInfo[]>([])
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const [operationType, setOperationType] = useState<'create' | 'update'>('create');
|
||||
const { lng } = useI18n();
|
||||
const { t } = useTranslationClientSide(lng, 'common');
|
||||
const [exportData, setExportData] = useState<ChannelInfo[]>([]);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const { message } = useMessage({
|
||||
warningBoxBg: 'var(--Yellow-50, #FFFAEB)',
|
||||
@@ -30,9 +30,9 @@ export default function DashboardPage() {
|
||||
successBoxBg: 'var(--Green-50, #EDFBF3)',
|
||||
successIconBg: 'var(--Green-600, #039855)',
|
||||
successIconFill: 'white',
|
||||
})
|
||||
});
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const {
|
||||
data: allChannels,
|
||||
@@ -43,57 +43,57 @@ export default function DashboardPage() {
|
||||
queryFn: getAllChannels,
|
||||
refetchOnReconnect: false,
|
||||
enabled: false,
|
||||
})
|
||||
});
|
||||
|
||||
const uploadMutation = useMutation({
|
||||
mutationFn: uploadChannels,
|
||||
})
|
||||
});
|
||||
|
||||
const handleExport = async () => {
|
||||
if (exportData.length === 0) {
|
||||
const result = await refetch()
|
||||
const dataToExport = result.data || []
|
||||
downloadJson(dataToExport, 'channels')
|
||||
const result = await refetch();
|
||||
const dataToExport = result.data || [];
|
||||
downloadJson(dataToExport, 'channels');
|
||||
} else {
|
||||
downloadJson(exportData, 'channels')
|
||||
downloadJson(exportData, 'channels');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleImport = () => {
|
||||
fileInputRef.current?.click()
|
||||
}
|
||||
fileInputRef.current?.click();
|
||||
};
|
||||
|
||||
const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0]
|
||||
if (!file) return
|
||||
const file = event.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
try {
|
||||
await uploadMutation.mutateAsync(formData)
|
||||
await uploadMutation.mutateAsync(formData);
|
||||
message({
|
||||
title: t('dashboard.importSuccess'),
|
||||
status: 'success',
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
})
|
||||
queryClient.invalidateQueries([QueryKey.GetChannels])
|
||||
queryClient.invalidateQueries([QueryKey.GetChannelTypeNames])
|
||||
});
|
||||
queryClient.invalidateQueries([QueryKey.GetChannels]);
|
||||
queryClient.invalidateQueries([QueryKey.GetChannelTypeNames]);
|
||||
} catch (error) {
|
||||
console.error('Import error:', error)
|
||||
console.error('Import error:', error);
|
||||
message({
|
||||
title: t('dashboard.importError'),
|
||||
status: 'error',
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
})
|
||||
});
|
||||
} finally {
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = ''
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex pt="4px" pb="12px" pr="12px" h="full" width="full">
|
||||
@@ -158,8 +158,8 @@ export default function DashboardPage() {
|
||||
},
|
||||
}}
|
||||
onClick={() => {
|
||||
setOperationType('create')
|
||||
onOpen()
|
||||
setOperationType('create');
|
||||
onOpen();
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
@@ -298,5 +298,5 @@ export default function DashboardPage() {
|
||||
<UpdateChannelModal isOpen={isOpen} onClose={onClose} operationType={operationType} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
+47
-47
@@ -1,17 +1,17 @@
|
||||
'use client'
|
||||
import { useState } from 'react'
|
||||
import { Button, Flex, Text, useDisclosure } from '@chakra-ui/react'
|
||||
import { Switch } from '@chakra-ui/react'
|
||||
import { useMessage } from '@sealos/ui'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { produce } from 'immer'
|
||||
'use client';
|
||||
import { useState } from 'react';
|
||||
import { Button, Flex, Text, useDisclosure } from '@chakra-ui/react';
|
||||
import { Switch } from '@chakra-ui/react';
|
||||
import { useMessage } from '@sealos/ui';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { produce } from 'immer';
|
||||
|
||||
import { getOption, updateOption } from '@/api/platform'
|
||||
import { useTranslationClientSide } from '@/app/i18n/client'
|
||||
import { useI18n } from '@/providers/i18n/i18nContext'
|
||||
import { QueryKey } from '@/types/query-key'
|
||||
import { getOption, updateOption } from '@/api/platform';
|
||||
import { useTranslationClientSide } from '@/app/i18n/client';
|
||||
import { useI18n } from '@/providers/i18n/i18nContext';
|
||||
import { QueryKey } from '@/types/query-key';
|
||||
|
||||
import { EditableText } from './EditableText'
|
||||
import { EditableText } from './EditableText';
|
||||
|
||||
export enum CommonConfigKey {
|
||||
GlobalApiRateLimitNum = 'GlobalApiRateLimitNum',
|
||||
@@ -21,25 +21,25 @@ export enum CommonConfigKey {
|
||||
}
|
||||
|
||||
type CommonConfig = {
|
||||
[CommonConfigKey.GlobalApiRateLimitNum]: string
|
||||
[CommonConfigKey.DisableServe]: string
|
||||
[CommonConfigKey.RetryTimes]: string
|
||||
[CommonConfigKey.GroupMaxTokenNum]: string
|
||||
}
|
||||
[CommonConfigKey.GlobalApiRateLimitNum]: string;
|
||||
[CommonConfigKey.DisableServe]: string;
|
||||
[CommonConfigKey.RetryTimes]: string;
|
||||
[CommonConfigKey.GroupMaxTokenNum]: string;
|
||||
};
|
||||
|
||||
const CommonConfig = () => {
|
||||
const { lng } = useI18n()
|
||||
const { t } = useTranslationClientSide(lng, 'common')
|
||||
const queryClient = useQueryClient()
|
||||
const { lng } = useI18n();
|
||||
const { t } = useTranslationClientSide(lng, 'common');
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [commonConfig, setCommonConfig] = useState<CommonConfig>(() =>
|
||||
produce({} as CommonConfig, (draft) => {
|
||||
draft.GlobalApiRateLimitNum = ''
|
||||
draft.DisableServe = ''
|
||||
draft.RetryTimes = ''
|
||||
draft.GroupMaxTokenNum = ''
|
||||
draft.GlobalApiRateLimitNum = '';
|
||||
draft.DisableServe = '';
|
||||
draft.RetryTimes = '';
|
||||
draft.GroupMaxTokenNum = '';
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const { message } = useMessage({
|
||||
warningBoxBg: '#FFFAEB',
|
||||
@@ -48,24 +48,24 @@ const CommonConfig = () => {
|
||||
successBoxBg: '#EDFBF3',
|
||||
successIconBg: '#039855',
|
||||
successIconFill: 'white',
|
||||
})
|
||||
});
|
||||
|
||||
const { isLoading: isOptionLoading, data: optionData } = useQuery({
|
||||
queryKey: [QueryKey.GetCommonConfig],
|
||||
queryFn: () => getOption(),
|
||||
onSuccess: (data) => {
|
||||
if (!data) return
|
||||
if (!data) return;
|
||||
|
||||
setCommonConfig(
|
||||
produce(commonConfig, (draft) => {
|
||||
draft.GlobalApiRateLimitNum = data.GlobalApiRateLimitNum || ''
|
||||
draft.DisableServe = data.DisableServe || ''
|
||||
draft.RetryTimes = data.RetryTimes || ''
|
||||
draft.GroupMaxTokenNum = data.GroupMaxTokenNum || ''
|
||||
draft.GlobalApiRateLimitNum = data.GlobalApiRateLimitNum || '';
|
||||
draft.DisableServe = data.DisableServe || '';
|
||||
draft.RetryTimes = data.RetryTimes || '';
|
||||
draft.GroupMaxTokenNum = data.GroupMaxTokenNum || '';
|
||||
})
|
||||
)
|
||||
);
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const updateOptionMutation = useMutation({
|
||||
mutationFn: (params: { key: string; value: string }) => updateOption(params),
|
||||
@@ -73,30 +73,30 @@ const CommonConfig = () => {
|
||||
message({
|
||||
title: t('globalConfigs.saveCommonConfigSuccess'),
|
||||
status: 'success',
|
||||
})
|
||||
queryClient.invalidateQueries({ queryKey: [QueryKey.GetCommonConfig] })
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: [QueryKey.GetCommonConfig] });
|
||||
},
|
||||
onError: () => {
|
||||
message({
|
||||
title: t('globalConfigs.saveCommonConfigFailed'),
|
||||
status: 'error',
|
||||
})
|
||||
});
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const updateConfigField = (field: CommonConfigKey, value: string) => {
|
||||
setCommonConfig(
|
||||
produce((draft) => {
|
||||
draft[field] = value
|
||||
draft[field] = value;
|
||||
})
|
||||
)
|
||||
updateOptionMutation.mutate({ key: field, value })
|
||||
}
|
||||
);
|
||||
updateOptionMutation.mutate({ key: field, value });
|
||||
};
|
||||
|
||||
const handleDisableServeChange = (checked: boolean) => {
|
||||
const value = checked ? 'true' : 'false'
|
||||
updateConfigField(CommonConfigKey.DisableServe, value)
|
||||
}
|
||||
const value = checked ? 'true' : 'false';
|
||||
updateConfigField(CommonConfigKey.DisableServe, value);
|
||||
};
|
||||
|
||||
return (
|
||||
/*
|
||||
@@ -163,7 +163,7 @@ const CommonConfig = () => {
|
||||
|
||||
{/* -- config end */}
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default CommonConfig
|
||||
export default CommonConfig;
|
||||
|
||||
+18
-18
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
import React, { useState } from 'react'
|
||||
import { CheckIcon, CloseIcon } from '@chakra-ui/icons'
|
||||
'use client';
|
||||
import React, { useState } from 'react';
|
||||
import { CheckIcon, CloseIcon } from '@chakra-ui/icons';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
@@ -14,29 +14,29 @@ import {
|
||||
PopoverTrigger,
|
||||
Text,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react'
|
||||
} from '@chakra-ui/react';
|
||||
|
||||
interface EditableTextProps {
|
||||
value: string | number
|
||||
label: string
|
||||
onSubmit: (value: string) => void
|
||||
flexProps?: FlexProps
|
||||
value: string | number;
|
||||
label: string;
|
||||
onSubmit: (value: string) => void;
|
||||
flexProps?: FlexProps;
|
||||
}
|
||||
|
||||
export const EditableText = ({ value, label, onSubmit, flexProps }: EditableTextProps) => {
|
||||
const [editValue, setEditValue] = useState(String(value))
|
||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||
const [editValue, setEditValue] = useState(String(value));
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
|
||||
const handleSubmit = () => {
|
||||
onSubmit(editValue)
|
||||
onClose()
|
||||
}
|
||||
onSubmit(editValue);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
// 关闭时 恢复到传递来的初始值
|
||||
setEditValue(String(value))
|
||||
onClose()
|
||||
}
|
||||
setEditValue(String(value));
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex alignItems="center" gap="16px" justifyContent="space-between" {...flexProps}>
|
||||
@@ -187,5 +187,5 @@ export const EditableText = ({ value, label, onSubmit, flexProps }: EditableText
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
+136
-136
@@ -1,32 +1,32 @@
|
||||
'use client'
|
||||
import { Dispatch, SetStateAction, useEffect, useState } from 'react'
|
||||
import { Controller, FieldError, FieldErrors, FieldErrorsImpl, useForm } from 'react-hook-form'
|
||||
import { Button, Flex, FormControl, Skeleton, Text, VStack } from '@chakra-ui/react'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { useMessage } from '@sealos/ui'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { z } from 'zod'
|
||||
'use client';
|
||||
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
|
||||
import { Controller, FieldError, FieldErrors, FieldErrorsImpl, useForm } from 'react-hook-form';
|
||||
import { Button, Flex, FormControl, Skeleton, Text, VStack } from '@chakra-ui/react';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useMessage } from '@sealos/ui';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
batchOption,
|
||||
getChannelBuiltInSupportModels,
|
||||
getChannelTypeNames,
|
||||
getOption,
|
||||
} from '@/api/platform'
|
||||
import { useTranslationClientSide } from '@/app/i18n/client'
|
||||
import ConstructMappingComponent from '@/components/common/ConstructMappingComponent'
|
||||
import { MultiSelectCombobox } from '@/components/common/MultiSelectCombobox'
|
||||
import { SingleSelectCombobox } from '@/components/common/SingleSelectCombobox'
|
||||
import { useI18n } from '@/providers/i18n/i18nContext'
|
||||
import { ChannelType } from '@/types/admin/channels/channelInfo'
|
||||
import { DefaultChannelModel, DefaultChannelModelMapping } from '@/types/admin/option'
|
||||
import { BatchOptionData } from '@/types/admin/option'
|
||||
import { QueryKey } from '@/types/query-key'
|
||||
} from '@/api/platform';
|
||||
import { useTranslationClientSide } from '@/app/i18n/client';
|
||||
import ConstructMappingComponent from '@/components/common/ConstructMappingComponent';
|
||||
import { MultiSelectCombobox } from '@/components/common/MultiSelectCombobox';
|
||||
import { SingleSelectCombobox } from '@/components/common/SingleSelectCombobox';
|
||||
import { useI18n } from '@/providers/i18n/i18nContext';
|
||||
import { ChannelType } from '@/types/admin/channels/channelInfo';
|
||||
import { DefaultChannelModel, DefaultChannelModelMapping } from '@/types/admin/option';
|
||||
import { BatchOptionData } from '@/types/admin/option';
|
||||
import { QueryKey } from '@/types/query-key';
|
||||
|
||||
const ModelConfig = () => {
|
||||
const { lng } = useI18n()
|
||||
const { t } = useTranslationClientSide(lng, 'common')
|
||||
const queryClient = useQueryClient()
|
||||
const { lng } = useI18n();
|
||||
const { t } = useTranslationClientSide(lng, 'common');
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { message } = useMessage({
|
||||
warningBoxBg: '#FFFAEB',
|
||||
@@ -36,51 +36,51 @@ const ModelConfig = () => {
|
||||
successBoxBg: '#EDFBF3',
|
||||
successIconBg: '#039855',
|
||||
successIconFill: 'white',
|
||||
})
|
||||
});
|
||||
|
||||
const [allSupportChannel, setAllSupportChannel] = useState<string[]>([])
|
||||
const [allSupportChannel, setAllSupportChannel] = useState<string[]>([]);
|
||||
const [allSupportChannelWithMode, setAllSupportChannelWithMode] = useState<{
|
||||
[key in ChannelType]: string[]
|
||||
}>({})
|
||||
[key in ChannelType]: string[];
|
||||
}>({});
|
||||
|
||||
const [defaultModel, setDefaultModel] = useState<DefaultChannelModel>({})
|
||||
const [defaultModelMapping, setDefaultModelMapping] = useState<DefaultChannelModelMapping>({})
|
||||
const [defaultModel, setDefaultModel] = useState<DefaultChannelModel>({});
|
||||
const [defaultModelMapping, setDefaultModelMapping] = useState<DefaultChannelModelMapping>({});
|
||||
|
||||
const { isLoading: isChannelTypeNamesLoading, data: channelTypeNames } = useQuery({
|
||||
queryKey: [QueryKey.GetChannelTypeNames],
|
||||
queryFn: () => getChannelTypeNames(),
|
||||
})
|
||||
});
|
||||
|
||||
const { isLoading: isBuiltInSupportModelsLoading, data: builtInSupportModels } = useQuery({
|
||||
queryKey: [QueryKey.GetAllChannelModes],
|
||||
queryFn: () => getChannelBuiltInSupportModels(),
|
||||
})
|
||||
});
|
||||
|
||||
const { isLoading: isOptionLoading, data: optionData } = useQuery({
|
||||
queryKey: [QueryKey.GetOption],
|
||||
queryFn: () => getOption(),
|
||||
onSuccess: (data) => {
|
||||
if (!data) return
|
||||
if (!data) return;
|
||||
|
||||
const defaultModels: DefaultChannelModel = JSON.parse(data.DefaultChannelModels)
|
||||
const defaultModels: DefaultChannelModel = JSON.parse(data.DefaultChannelModels);
|
||||
const defaultModelMappings: DefaultChannelModelMapping = JSON.parse(
|
||||
data.DefaultChannelModelMapping
|
||||
)
|
||||
);
|
||||
|
||||
setDefaultModel(defaultModels)
|
||||
setDefaultModelMapping(defaultModelMappings)
|
||||
setDefaultModel(defaultModels);
|
||||
setDefaultModelMapping(defaultModelMappings);
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!channelTypeNames || !builtInSupportModels) return
|
||||
if (!channelTypeNames || !builtInSupportModels) return;
|
||||
|
||||
// 1. 处理 allSupportChannel
|
||||
const supportedChannels = Object.entries(channelTypeNames)
|
||||
.filter(([channel]) => channel in builtInSupportModels)
|
||||
.map(([_, name]) => name)
|
||||
.map(([_, name]) => name);
|
||||
|
||||
setAllSupportChannel(supportedChannels)
|
||||
setAllSupportChannel(supportedChannels);
|
||||
|
||||
// 2. 处理 allSupportChannelWithMode
|
||||
// 渠道类型可能出现在 channelTypeNames 中,但不在 builtInSupportModels 中,所以需要过滤
|
||||
@@ -88,17 +88,17 @@ const ModelConfig = () => {
|
||||
const channelWithModes = Object.entries(channelTypeNames)
|
||||
.filter(([channelType, _]) => channelType in builtInSupportModels)
|
||||
.reduce((acc, [channelType, channelName]) => {
|
||||
const modelInfos = builtInSupportModels[channelType as ChannelType] || []
|
||||
const models = [...new Set(modelInfos.map((info) => info.model))]
|
||||
const modelInfos = builtInSupportModels[channelType as ChannelType] || [];
|
||||
const models = [...new Set(modelInfos.map((info) => info.model))];
|
||||
|
||||
return {
|
||||
...acc,
|
||||
[channelType]: models,
|
||||
}
|
||||
}, {} as { [key in ChannelType]: string[] })
|
||||
};
|
||||
}, {} as { [key in ChannelType]: string[] });
|
||||
|
||||
setAllSupportChannelWithMode(channelWithModes)
|
||||
}, [channelTypeNames, builtInSupportModels])
|
||||
setAllSupportChannelWithMode(channelWithModes);
|
||||
}, [channelTypeNames, builtInSupportModels]);
|
||||
|
||||
// form schema
|
||||
const itemSchema = z.object({
|
||||
@@ -108,15 +108,15 @@ const ModelConfig = () => {
|
||||
.record(z.string(), z.string())
|
||||
.refine((mapping) => {
|
||||
// 检查所有值不能为空字符串
|
||||
return Object.values(mapping).every((value) => value.trim() !== '')
|
||||
return Object.values(mapping).every((value) => value.trim() !== '');
|
||||
})
|
||||
.default({}),
|
||||
})
|
||||
});
|
||||
|
||||
const schema = z.array(itemSchema)
|
||||
const schema = z.array(itemSchema);
|
||||
|
||||
type ConfigItem = z.infer<typeof itemSchema>
|
||||
type FormData = ConfigItem[]
|
||||
type ConfigItem = z.infer<typeof itemSchema>;
|
||||
type FormData = ConfigItem[];
|
||||
|
||||
const {
|
||||
register,
|
||||
@@ -131,12 +131,12 @@ const ModelConfig = () => {
|
||||
defaultValues: [],
|
||||
mode: 'onChange',
|
||||
reValidateMode: 'onChange',
|
||||
})
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!defaultModel || !defaultModelMapping) return
|
||||
if (!defaultModel || !defaultModelMapping) return;
|
||||
// Only proceed if both defaultModel and defaultModelMapping are available
|
||||
if (Object.keys(defaultModel).length === 0) return
|
||||
if (Object.keys(defaultModel).length === 0) return;
|
||||
|
||||
// Transform the data into form format
|
||||
const formData: FormData = Object.entries(defaultModel).map(([channelType, modes]) => {
|
||||
@@ -144,33 +144,33 @@ const ModelConfig = () => {
|
||||
type: Number(channelType),
|
||||
defaultMode: modes || [], // Using first mode as default
|
||||
defaultModeMapping: defaultModelMapping[channelType as ChannelType] || {},
|
||||
}
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
// Reset form with the new values
|
||||
reset(formData)
|
||||
}, [defaultModel, defaultModelMapping, reset])
|
||||
reset(formData);
|
||||
}, [defaultModel, defaultModelMapping, reset]);
|
||||
|
||||
const handleAddDefaultModel = () => {
|
||||
const newItem = {
|
||||
type: undefined, // Default type value
|
||||
defaultMode: [],
|
||||
defaultModeMapping: {},
|
||||
}
|
||||
};
|
||||
|
||||
// Get current form values
|
||||
const currentValues = watch()
|
||||
const currentValues = watch();
|
||||
// Create new array with new item at the beginning
|
||||
const newValues = [newItem, ...Object.values(currentValues)]
|
||||
const newValues = [newItem, ...Object.values(currentValues)];
|
||||
// Reset form with new values
|
||||
reset(newValues)
|
||||
}
|
||||
reset(newValues);
|
||||
};
|
||||
|
||||
const formValues = watch()
|
||||
const formValues = watch();
|
||||
|
||||
const formValuesArray: FormData = Array.isArray(formValues)
|
||||
? formValues
|
||||
: Object.values(formValues)
|
||||
: Object.values(formValues);
|
||||
|
||||
const batchOptionMutation = useMutation({
|
||||
mutationFn: batchOption,
|
||||
@@ -178,66 +178,66 @@ const ModelConfig = () => {
|
||||
message({
|
||||
title: t('globalConfigs.saveDefaultModelSuccess'),
|
||||
status: 'success',
|
||||
})
|
||||
});
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const transformFormDataToConfig = (formData: FormData): BatchOptionData => {
|
||||
// 初始化两个对象
|
||||
const defaultChannelModelMapping: Record<string, Record<string, string>> = {}
|
||||
const defaultChannelModels: Record<string, string[]> = {}
|
||||
const defaultChannelModelMapping: Record<string, Record<string, string>> = {};
|
||||
const defaultChannelModels: Record<string, string[]> = {};
|
||||
|
||||
// 遍历 FormData
|
||||
formData.forEach((item) => {
|
||||
const type = item.type.toString()
|
||||
const type = item.type.toString();
|
||||
|
||||
// 处理 DefaultChannelModelMapping
|
||||
if (Object.keys(item.defaultModeMapping).length > 0) {
|
||||
defaultChannelModelMapping[type] = item.defaultModeMapping
|
||||
defaultChannelModelMapping[type] = item.defaultModeMapping;
|
||||
}
|
||||
|
||||
// 处理 DefaultChannelModels
|
||||
if (item.defaultMode.length > 0) {
|
||||
defaultChannelModels[type] = item.defaultMode
|
||||
defaultChannelModels[type] = item.defaultMode;
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
return {
|
||||
// 转换为 JSON 字符串
|
||||
DefaultChannelModelMapping: JSON.stringify(defaultChannelModelMapping),
|
||||
DefaultChannelModels: JSON.stringify(defaultChannelModels),
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
reset()
|
||||
}
|
||||
reset();
|
||||
};
|
||||
|
||||
type FieldErrorType =
|
||||
| FieldError
|
||||
| FieldErrorsImpl<{
|
||||
type: number
|
||||
defaultMode: string[]
|
||||
defaultModeMapping: Record<string, string>
|
||||
}>
|
||||
type: number;
|
||||
defaultMode: string[];
|
||||
defaultModeMapping: Record<string, string>;
|
||||
}>;
|
||||
|
||||
const getFirstErrorMessage = (errors: FieldErrors<FormData>): string => {
|
||||
// Iterate through top-level errors
|
||||
for (const index in errors) {
|
||||
const fieldError = errors[index] as FieldErrorType
|
||||
if (!fieldError) continue
|
||||
const fieldError = errors[index] as FieldErrorType;
|
||||
if (!fieldError) continue;
|
||||
|
||||
// Check if error is an object
|
||||
if (typeof fieldError === 'object') {
|
||||
// If it has a direct message property
|
||||
if ('message' in fieldError && fieldError.message) {
|
||||
return `Item ${Number(index) + 1}: ${fieldError.message}`
|
||||
return `Item ${Number(index) + 1}: ${fieldError.message}`;
|
||||
}
|
||||
|
||||
// Iterate through nested field errors
|
||||
const errorKeys = Object.keys(fieldError) as Array<keyof typeof fieldError>
|
||||
const errorKeys = Object.keys(fieldError) as Array<keyof typeof fieldError>;
|
||||
for (const fieldName of errorKeys) {
|
||||
const nestedError = fieldError[fieldName]
|
||||
const nestedError = fieldError[fieldName];
|
||||
if (nestedError && typeof nestedError === 'object' && 'message' in nestedError) {
|
||||
// Map field names to their display labels
|
||||
const fieldLabel =
|
||||
@@ -245,25 +245,25 @@ const ModelConfig = () => {
|
||||
type: 'Type',
|
||||
defaultMode: 'Default Mode',
|
||||
defaultModeMapping: 'Model Mapping',
|
||||
}[fieldName as string] || fieldName
|
||||
}[fieldName as string] || fieldName;
|
||||
|
||||
return `Item ${Number(index) + 1} ${fieldLabel}: ${nestedError.message}`
|
||||
return `Item ${Number(index) + 1} ${fieldLabel}: ${nestedError.message}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return 'Form validation failed'
|
||||
}
|
||||
return 'Form validation failed';
|
||||
};
|
||||
|
||||
const onValidate = async (data: FormData) => {
|
||||
try {
|
||||
const batchOptionData: BatchOptionData = transformFormDataToConfig(data)
|
||||
await batchOptionMutation.mutateAsync(batchOptionData)
|
||||
const batchOptionData: BatchOptionData = transformFormDataToConfig(data);
|
||||
await batchOptionMutation.mutateAsync(batchOptionData);
|
||||
|
||||
queryClient.invalidateQueries({ queryKey: [QueryKey.GetOption] })
|
||||
queryClient.invalidateQueries({ queryKey: [QueryKey.GetChannelTypeNames] })
|
||||
queryClient.invalidateQueries({ queryKey: [QueryKey.GetAllChannelModes] })
|
||||
resetForm()
|
||||
queryClient.invalidateQueries({ queryKey: [QueryKey.GetOption] });
|
||||
queryClient.invalidateQueries({ queryKey: [QueryKey.GetChannelTypeNames] });
|
||||
queryClient.invalidateQueries({ queryKey: [QueryKey.GetAllChannelModes] });
|
||||
resetForm();
|
||||
} catch (error) {
|
||||
message({
|
||||
title: t('globalConfigs.saveDefaultModelFailed'),
|
||||
@@ -273,15 +273,15 @@ const ModelConfig = () => {
|
||||
isClosable: true,
|
||||
description:
|
||||
error instanceof Error ? error.message : t('globalConfigs.saveDefaultModelFailed'),
|
||||
})
|
||||
console.error(error)
|
||||
});
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onInvalid = (errors: FieldErrors<FormData>): void => {
|
||||
console.error('errors', errors)
|
||||
console.error('errors', errors);
|
||||
|
||||
const errorMessage = getFirstErrorMessage(errors)
|
||||
const errorMessage = getFirstErrorMessage(errors);
|
||||
|
||||
message({
|
||||
title: errorMessage,
|
||||
@@ -289,10 +289,10 @@ const ModelConfig = () => {
|
||||
position: 'top',
|
||||
duration: 2000,
|
||||
isClosable: true,
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onSubmit = handleSubmit(onValidate, onInvalid)
|
||||
const onSubmit = handleSubmit(onValidate, onInvalid);
|
||||
return (
|
||||
/*
|
||||
顶级 Flex 容器的高度: calc(100vh - 16px - 24px - 12px - 32px - 36px)
|
||||
@@ -471,9 +471,9 @@ const ModelConfig = () => {
|
||||
color: '#D92D20',
|
||||
}}
|
||||
onClick={() => {
|
||||
const currentValues = watch()
|
||||
const newValues = Object.values(currentValues).filter((_, i) => i !== index)
|
||||
reset(newValues)
|
||||
const currentValues = watch();
|
||||
const newValues = Object.values(currentValues).filter((_, i) => i !== index);
|
||||
reset(newValues);
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
@@ -511,7 +511,7 @@ const ModelConfig = () => {
|
||||
(type): type is ChannelType =>
|
||||
type !== undefined && type in channelTypeNames
|
||||
)
|
||||
.map((type) => channelTypeNames[type])
|
||||
.map((type) => channelTypeNames[type]);
|
||||
|
||||
// Filter available types
|
||||
const availableTypes = allSupportChannel.filter(
|
||||
@@ -521,11 +521,11 @@ const ModelConfig = () => {
|
||||
(field.value &&
|
||||
channelTypeNames[String(field.value) as ChannelType] ===
|
||||
channelType)
|
||||
)
|
||||
);
|
||||
|
||||
const initSelectedItem = field.value
|
||||
? channelTypeNames[String(field.value) as ChannelType]
|
||||
: undefined
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<SingleSelectCombobox<string>
|
||||
@@ -535,20 +535,20 @@ const ModelConfig = () => {
|
||||
if (channelName) {
|
||||
const channelType = Object.entries(channelTypeNames).find(
|
||||
([_, name]) => name === channelName
|
||||
)?.[0]
|
||||
)?.[0];
|
||||
|
||||
if (channelType) {
|
||||
const defaultModelField =
|
||||
defaultModel[channelType as ChannelType]
|
||||
defaultModel[channelType as ChannelType];
|
||||
const defaultModelMappingField =
|
||||
defaultModelMapping[channelType as ChannelType]
|
||||
defaultModelMapping[channelType as ChannelType];
|
||||
|
||||
field.onChange(Number(channelType))
|
||||
setValue(`${index}.defaultMode`, defaultModelField || [])
|
||||
field.onChange(Number(channelType));
|
||||
setValue(`${index}.defaultMode`, defaultModelField || []);
|
||||
setValue(
|
||||
`${index}.defaultModeMapping`,
|
||||
defaultModelMappingField || {}
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}}
|
||||
@@ -556,11 +556,11 @@ const ModelConfig = () => {
|
||||
dropdownItems: string[],
|
||||
inputValue: string
|
||||
) => {
|
||||
const lowerCasedInput = inputValue.toLowerCase()
|
||||
const lowerCasedInput = inputValue.toLowerCase();
|
||||
return dropdownItems.filter(
|
||||
(item) =>
|
||||
!inputValue || item.toLowerCase().includes(lowerCasedInput)
|
||||
)
|
||||
);
|
||||
}}
|
||||
handleDropdownItemDisplay={(item: string) => (
|
||||
<Text
|
||||
@@ -576,7 +576,7 @@ const ModelConfig = () => {
|
||||
</Text>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
@@ -587,10 +587,10 @@ const ModelConfig = () => {
|
||||
control={control}
|
||||
render={({ field }) => {
|
||||
// Get the current type value from the form
|
||||
const currentType = watch(`${index}.type`)
|
||||
const currentType = watch(`${index}.type`);
|
||||
const dropdownItems = currentType
|
||||
? allSupportChannelWithMode[String(currentType) as ChannelType] || []
|
||||
: []
|
||||
: [];
|
||||
|
||||
const handleSetCustomModel = (
|
||||
selectedItems: string[],
|
||||
@@ -601,35 +601,35 @@ const ModelConfig = () => {
|
||||
if (customModeName.trim()) {
|
||||
const exists = field.value.some(
|
||||
(item) => item === customModeName.trim()
|
||||
)
|
||||
);
|
||||
|
||||
if (!exists) {
|
||||
field.onChange([...field.value, customModeName.trim()])
|
||||
setCustomModeName('')
|
||||
field.onChange([...field.value, customModeName.trim()]);
|
||||
setCustomModeName('');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleModelFilteredDropdownItems = (
|
||||
dropdownItems: string[],
|
||||
selectedItems: string[],
|
||||
inputValue: string
|
||||
) => {
|
||||
const lowerCasedInputValue = inputValue.toLowerCase()
|
||||
const lowerCasedInputValue = inputValue.toLowerCase();
|
||||
|
||||
return dropdownItems.filter(
|
||||
(item) =>
|
||||
!selectedItems.includes(item) &&
|
||||
item.toLowerCase().includes(lowerCasedInputValue)
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<MultiSelectCombobox<string>
|
||||
dropdownItems={dropdownItems || []}
|
||||
selectedItems={field.value || []} // Use field.value for selected items
|
||||
setSelectedItems={(models) => {
|
||||
field.onChange(models)
|
||||
field.onChange(models);
|
||||
}}
|
||||
handleFilteredDropdownItems={handleModelFilteredDropdownItems}
|
||||
handleDropdownItemDisplay={(item) => (
|
||||
@@ -659,7 +659,7 @@ const ModelConfig = () => {
|
||||
)}
|
||||
handleSetCustomSelectedItem={handleSetCustomModel}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
@@ -669,31 +669,31 @@ const ModelConfig = () => {
|
||||
name={`${index}.defaultModeMapping`}
|
||||
control={control}
|
||||
render={({ field }) => {
|
||||
const defaultMode = watch(`${index}.defaultMode`)
|
||||
const defaultModeMapping = watch(`${index}.defaultModeMapping`)
|
||||
const defaultMode = watch(`${index}.defaultMode`);
|
||||
const defaultModeMapping = watch(`${index}.defaultModeMapping`);
|
||||
|
||||
return (
|
||||
<ConstructMappingComponent
|
||||
mapKeys={defaultMode}
|
||||
mapData={defaultModeMapping}
|
||||
setMapData={(mapping) => {
|
||||
field.onChange(mapping)
|
||||
field.onChange(mapping);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
</VStack>
|
||||
</Flex>
|
||||
)
|
||||
);
|
||||
})
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
{/* -- config end */}
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default ModelConfig
|
||||
export default ModelConfig;
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
'use client'
|
||||
import { useRef } from 'react'
|
||||
import { Button, Divider, Flex, Text, useDisclosure } from '@chakra-ui/react'
|
||||
import { useMessage } from '@sealos/ui/src/components'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
'use client';
|
||||
import { useRef } from 'react';
|
||||
import { Button, Divider, Flex, Text, useDisclosure } from '@chakra-ui/react';
|
||||
import { useMessage } from '@sealos/ui/src/components';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { getOption, uploadOptions } from '@/api/platform'
|
||||
import { useTranslationClientSide } from '@/app/i18n/client'
|
||||
import { useI18n } from '@/providers/i18n/i18nContext'
|
||||
import { QueryKey } from '@/types/query-key'
|
||||
import { downloadJson } from '@/utils/common'
|
||||
import { getOption, uploadOptions } from '@/api/platform';
|
||||
import { useTranslationClientSide } from '@/app/i18n/client';
|
||||
import { useI18n } from '@/providers/i18n/i18nContext';
|
||||
import { QueryKey } from '@/types/query-key';
|
||||
import { downloadJson } from '@/utils/common';
|
||||
|
||||
import CommonConfig from './components/CommonConfig'
|
||||
import ModelConfig from './components/ModelConfig'
|
||||
import CommonConfig from './components/CommonConfig';
|
||||
import ModelConfig from './components/ModelConfig';
|
||||
|
||||
export default function GlobalConfigPage() {
|
||||
const { lng } = useI18n()
|
||||
const { t } = useTranslationClientSide(lng, 'common')
|
||||
const { lng } = useI18n();
|
||||
const { t } = useTranslationClientSide(lng, 'common');
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const { message } = useMessage({
|
||||
warningBoxBg: '#FFFAEB',
|
||||
@@ -27,9 +27,9 @@ export default function GlobalConfigPage() {
|
||||
successBoxBg: '#EDFBF3',
|
||||
successIconBg: '#039855',
|
||||
successIconFill: 'white',
|
||||
})
|
||||
});
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const {
|
||||
isFetching: isOptionFetching,
|
||||
@@ -40,53 +40,53 @@ export default function GlobalConfigPage() {
|
||||
queryFn: () => getOption(),
|
||||
refetchOnReconnect: false,
|
||||
enabled: false,
|
||||
})
|
||||
});
|
||||
|
||||
const uploadMutation = useMutation({
|
||||
mutationFn: uploadOptions,
|
||||
})
|
||||
});
|
||||
|
||||
const handleExport = async () => {
|
||||
const result = await refetch()
|
||||
const dataToExport = result.data || []
|
||||
downloadJson(dataToExport, 'global_configs')
|
||||
}
|
||||
const result = await refetch();
|
||||
const dataToExport = result.data || [];
|
||||
downloadJson(dataToExport, 'global_configs');
|
||||
};
|
||||
|
||||
const handleImport = () => {
|
||||
fileInputRef.current?.click()
|
||||
}
|
||||
fileInputRef.current?.click();
|
||||
};
|
||||
|
||||
const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0]
|
||||
if (!file) return
|
||||
const file = event.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
try {
|
||||
await uploadMutation.mutateAsync(formData)
|
||||
await uploadMutation.mutateAsync(formData);
|
||||
message({
|
||||
title: t('dashboard.importSuccess'),
|
||||
status: 'success',
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
})
|
||||
queryClient.invalidateQueries([QueryKey.GetOption])
|
||||
queryClient.invalidateQueries([QueryKey.GetCommonConfig])
|
||||
});
|
||||
queryClient.invalidateQueries([QueryKey.GetOption]);
|
||||
queryClient.invalidateQueries([QueryKey.GetCommonConfig]);
|
||||
} catch (error) {
|
||||
console.error('Import error:', error)
|
||||
console.error('Import error:', error);
|
||||
message({
|
||||
title: t('dashboard.importError'),
|
||||
status: 'error',
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
})
|
||||
});
|
||||
} finally {
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = ''
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex pt="4px" pb="12px" pr="12px" pl="0px" h="100vh" width="full">
|
||||
@@ -265,5 +265,5 @@ export default function GlobalConfigPage() {
|
||||
{/* -- config end */}
|
||||
</Flex>
|
||||
</Flex>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
'use client'
|
||||
'use client';
|
||||
|
||||
import { useMemo, useState } from 'react'
|
||||
import { Box, Button, Flex, Icon, Input, Text } from '@chakra-ui/react'
|
||||
import { CurrencySymbol, MyTooltip } from '@sealos/ui'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { ColumnDef, getCoreRowModel, useReactTable } from '@tanstack/react-table'
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Box, Button, Flex, Icon, Input, Text } from '@chakra-ui/react';
|
||||
import { CurrencySymbol, MyTooltip } from '@sealos/ui';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { ColumnDef, getCoreRowModel, useReactTable } from '@tanstack/react-table';
|
||||
|
||||
import { getEnabledMode, getGlobalLogs } from '@/api/platform'
|
||||
import { useTranslationClientSide } from '@/app/i18n/client'
|
||||
import SelectDateRange from '@/components/common/SelectDateRange'
|
||||
import { SingleSelectComboboxUnstyle } from '@/components/common/SingleSelectComboboxUnStyle'
|
||||
import SwitchPage from '@/components/common/SwitchPage'
|
||||
import { BaseTable } from '@/components/table/BaseTable'
|
||||
import { useI18n } from '@/providers/i18n/i18nContext'
|
||||
import { useBackendStore } from '@/store/backend'
|
||||
import { QueryKey } from '@/types/query-key'
|
||||
import { GlobalLogItem } from '@/types/user/logs'
|
||||
import { getEnabledMode, getGlobalLogs } from '@/api/platform';
|
||||
import { useTranslationClientSide } from '@/app/i18n/client';
|
||||
import SelectDateRange from '@/components/common/SelectDateRange';
|
||||
import { SingleSelectComboboxUnstyle } from '@/components/common/SingleSelectComboboxUnStyle';
|
||||
import SwitchPage from '@/components/common/SwitchPage';
|
||||
import { BaseTable } from '@/components/table/BaseTable';
|
||||
import { useI18n } from '@/providers/i18n/i18nContext';
|
||||
import { useBackendStore } from '@/store/backend';
|
||||
import { QueryKey } from '@/types/query-key';
|
||||
import { GlobalLogItem } from '@/types/user/logs';
|
||||
|
||||
export default function Home(): React.JSX.Element {
|
||||
const { lng } = useI18n()
|
||||
const { t } = useTranslationClientSide(lng, 'common')
|
||||
const { currencySymbol } = useBackendStore()
|
||||
const { lng } = useI18n();
|
||||
const { t } = useTranslationClientSide(lng, 'common');
|
||||
const { currencySymbol } = useBackendStore();
|
||||
|
||||
const [startTime, setStartTime] = useState(() => {
|
||||
const currentDate = new Date()
|
||||
currentDate.setMonth(currentDate.getMonth() - 1)
|
||||
return currentDate
|
||||
})
|
||||
const [endTime, setEndTime] = useState(new Date())
|
||||
const [groupId, setGroupId] = useState('')
|
||||
const [name, setName] = useState('')
|
||||
const [modelName, setModelName] = useState<string>('')
|
||||
const [page, setPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(10)
|
||||
const [logData, setLogData] = useState<GlobalLogItem[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const currentDate = new Date();
|
||||
currentDate.setMonth(currentDate.getMonth() - 1);
|
||||
return currentDate;
|
||||
});
|
||||
const [endTime, setEndTime] = useState(new Date());
|
||||
const [groupId, setGroupId] = useState('');
|
||||
const [name, setName] = useState('');
|
||||
const [modelName, setModelName] = useState<string>('');
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [logData, setLogData] = useState<GlobalLogItem[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
|
||||
const { data: models = [] } = useQuery([QueryKey.GetEnabledModels], () => getEnabledMode())
|
||||
const { data: models = [] } = useQuery([QueryKey.GetEnabledModels], () => getEnabledMode());
|
||||
|
||||
const { isLoading } = useQuery(
|
||||
[QueryKey.GetGlobalLogs, page, pageSize, name, modelName, startTime, endTime, groupId],
|
||||
@@ -53,15 +53,15 @@ export default function Home(): React.JSX.Element {
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
if (!data?.logs) {
|
||||
setLogData([])
|
||||
setTotal(0)
|
||||
return
|
||||
setLogData([]);
|
||||
setTotal(0);
|
||||
return;
|
||||
}
|
||||
setLogData(data?.logs || [])
|
||||
setTotal(data?.total || 0)
|
||||
setLogData(data?.logs || []);
|
||||
setTotal(data?.total || 0);
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
const columns = useMemo<ColumnDef<GlobalLogItem>[]>(() => {
|
||||
return [
|
||||
@@ -94,7 +94,7 @@ export default function Home(): React.JSX.Element {
|
||||
header: t('logs.status'),
|
||||
accessorFn: (row) => (row.code === 200 ? t('logs.success') : t('logs.failed')),
|
||||
cell: ({ getValue }) => {
|
||||
const value = getValue() as string
|
||||
const value = getValue() as string;
|
||||
return (
|
||||
<Text
|
||||
color={
|
||||
@@ -110,7 +110,7 @@ export default function Home(): React.JSX.Element {
|
||||
>
|
||||
{value}
|
||||
</Text>
|
||||
)
|
||||
);
|
||||
},
|
||||
id: 'status',
|
||||
},
|
||||
@@ -142,17 +142,17 @@ export default function Home(): React.JSX.Element {
|
||||
</Flex>
|
||||
</MyTooltip>
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
},
|
||||
},
|
||||
]
|
||||
}, [])
|
||||
];
|
||||
}, []);
|
||||
|
||||
const table = useReactTable({
|
||||
data: logData,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
})
|
||||
});
|
||||
|
||||
return (
|
||||
<Flex
|
||||
@@ -223,8 +223,8 @@ export default function Home(): React.JSX.Element {
|
||||
bg="white"
|
||||
boxShadow="0px 1px 2px 0px rgba(19, 51, 107, 0.05), 0px 0px 1px 0px rgba(19, 51, 107, 0.08)"
|
||||
onClick={() => {
|
||||
setName('')
|
||||
setModelName('')
|
||||
setName('');
|
||||
setModelName('');
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
@@ -315,16 +315,16 @@ export default function Home(): React.JSX.Element {
|
||||
dropdownItems={['all', ...models.map((item) => item.model)]}
|
||||
setSelectedItem={(value) => {
|
||||
if (value === 'all') {
|
||||
setModelName('')
|
||||
setModelName('');
|
||||
} else {
|
||||
setModelName(value)
|
||||
setModelName(value);
|
||||
}
|
||||
}}
|
||||
handleDropdownItemFilter={(dropdownItems, inputValue) => {
|
||||
const lowerCasedInput = inputValue.toLowerCase()
|
||||
const lowerCasedInput = inputValue.toLowerCase();
|
||||
return dropdownItems.filter(
|
||||
(item) => !inputValue || item.toLowerCase().includes(lowerCasedInput)
|
||||
)
|
||||
);
|
||||
}}
|
||||
handleDropdownItemDisplay={(dropdownItem) => {
|
||||
return (
|
||||
@@ -339,7 +339,7 @@ export default function Home(): React.JSX.Element {
|
||||
>
|
||||
{dropdownItem}
|
||||
</Text>
|
||||
)
|
||||
);
|
||||
}}
|
||||
flexProps={{ w: '500px' }}
|
||||
placeholder={t('GlobalLogs.selectModel')}
|
||||
@@ -447,5 +447,5 @@ export default function Home(): React.JSX.Element {
|
||||
{/* -- table end */}
|
||||
</Flex>
|
||||
</Flex>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Box, Flex } from '@chakra-ui/react'
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
|
||||
import SideBar from '@/components/admin/Sidebar'
|
||||
import SideBar from '@/components/admin/Sidebar';
|
||||
|
||||
export default function UserLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
@@ -13,5 +13,5 @@ export default function UserLayout({ children }: { children: React.ReactNode })
|
||||
{children}
|
||||
</Box>
|
||||
</Flex>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
'use client';
|
||||
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useMemo, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
@@ -12,31 +12,31 @@ import {
|
||||
MenuItem,
|
||||
MenuList,
|
||||
Text,
|
||||
} from '@chakra-ui/react'
|
||||
import { CurrencySymbol } from '@sealos/ui'
|
||||
import { useMessage } from '@sealos/ui'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
} from '@chakra-ui/react';
|
||||
import { CurrencySymbol } from '@sealos/ui';
|
||||
import { useMessage } from '@sealos/ui';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
ColumnDef,
|
||||
createColumnHelper,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
} from '@tanstack/react-table'
|
||||
} from '@tanstack/react-table';
|
||||
|
||||
import { deleteGroup, getGroups, updateGroupQpm, updateGroupStatus } from '@/api/platform'
|
||||
import { useTranslationClientSide } from '@/app/i18n/client'
|
||||
import { EditableTextNoLable } from '@/components/common/EditableTextNoLable'
|
||||
import SwitchPage from '@/components/common/SwitchPage'
|
||||
import { BaseTable } from '@/components/table/BaseTable'
|
||||
import { useI18n } from '@/providers/i18n/i18nContext'
|
||||
import { useBackendStore } from '@/store/backend'
|
||||
import { GroupInfo, GroupStatus } from '@/types/admin/group'
|
||||
import { QueryKey } from '@/types/query-key'
|
||||
import { deleteGroup, getGroups, updateGroupQpm, updateGroupStatus } from '@/api/platform';
|
||||
import { useTranslationClientSide } from '@/app/i18n/client';
|
||||
import { EditableTextNoLable } from '@/components/common/EditableTextNoLable';
|
||||
import SwitchPage from '@/components/common/SwitchPage';
|
||||
import { BaseTable } from '@/components/table/BaseTable';
|
||||
import { useI18n } from '@/providers/i18n/i18nContext';
|
||||
import { useBackendStore } from '@/store/backend';
|
||||
import { GroupInfo, GroupStatus } from '@/types/admin/group';
|
||||
import { QueryKey } from '@/types/query-key';
|
||||
|
||||
export default function Home(): React.JSX.Element {
|
||||
const { lng } = useI18n()
|
||||
const { t } = useTranslationClientSide(lng, 'common')
|
||||
const { currencySymbol } = useBackendStore()
|
||||
const { lng } = useI18n();
|
||||
const { t } = useTranslationClientSide(lng, 'common');
|
||||
const { currencySymbol } = useBackendStore();
|
||||
const { message } = useMessage({
|
||||
warningBoxBg: '#FFFAEB',
|
||||
warningIconBg: '#F79009',
|
||||
@@ -44,15 +44,15 @@ export default function Home(): React.JSX.Element {
|
||||
successBoxBg: '#EDFBF3',
|
||||
successIconBg: '#039855',
|
||||
successIconFill: 'white',
|
||||
})
|
||||
});
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [groupId, setGroupId] = useState('')
|
||||
const [page, setPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(10)
|
||||
const [groupData, setGroupData] = useState<GroupInfo[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const [groupId, setGroupId] = useState('');
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [groupData, setGroupData] = useState<GroupInfo[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
|
||||
const { isLoading } = useQuery(
|
||||
[QueryKey.GetGroups, page, pageSize, groupId],
|
||||
@@ -65,15 +65,15 @@ export default function Home(): React.JSX.Element {
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
if (!data?.groups) {
|
||||
setGroupData([])
|
||||
setTotal(0)
|
||||
return
|
||||
setGroupData([]);
|
||||
setTotal(0);
|
||||
return;
|
||||
}
|
||||
setGroupData(data?.groups || [])
|
||||
setTotal(data?.total || 0)
|
||||
setGroupData(data?.groups || []);
|
||||
setTotal(data?.total || 0);
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
const deleteGroupMutation = useMutation(({ id }: { id: string }) => deleteGroup(id), {
|
||||
onSuccess() {
|
||||
@@ -83,8 +83,8 @@ export default function Home(): React.JSX.Element {
|
||||
isClosable: true,
|
||||
duration: 2000,
|
||||
position: 'top',
|
||||
})
|
||||
queryClient.invalidateQueries([QueryKey.GetGroups])
|
||||
});
|
||||
queryClient.invalidateQueries([QueryKey.GetGroups]);
|
||||
},
|
||||
onError(err: any) {
|
||||
message({
|
||||
@@ -93,9 +93,9 @@ export default function Home(): React.JSX.Element {
|
||||
description: err?.message || t('nsManager.deleteGroupFailed'),
|
||||
isClosable: true,
|
||||
position: 'top',
|
||||
})
|
||||
});
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const updateGroupStatusMutation = useMutation(
|
||||
({ id, status }: { id: string; status: number }) => updateGroupStatus(id, status),
|
||||
@@ -107,8 +107,8 @@ export default function Home(): React.JSX.Element {
|
||||
isClosable: true,
|
||||
duration: 2000,
|
||||
position: 'top',
|
||||
})
|
||||
queryClient.invalidateQueries([QueryKey.GetGroups])
|
||||
});
|
||||
queryClient.invalidateQueries([QueryKey.GetGroups]);
|
||||
},
|
||||
onError(err: any) {
|
||||
message({
|
||||
@@ -117,10 +117,10 @@ export default function Home(): React.JSX.Element {
|
||||
description: err?.message || t('nsManager.updateGroupStatusFailed'),
|
||||
isClosable: true,
|
||||
position: 'top',
|
||||
})
|
||||
});
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
const updateGroupQpmMutation = useMutation(
|
||||
({ id, qpm }: { id: string; qpm: number }) => updateGroupQpm(id, qpm),
|
||||
@@ -132,8 +132,8 @@ export default function Home(): React.JSX.Element {
|
||||
isClosable: true,
|
||||
duration: 2000,
|
||||
position: 'top',
|
||||
})
|
||||
queryClient.invalidateQueries([QueryKey.GetGroups])
|
||||
});
|
||||
queryClient.invalidateQueries([QueryKey.GetGroups]);
|
||||
},
|
||||
onError(err: any) {
|
||||
message({
|
||||
@@ -142,18 +142,18 @@ export default function Home(): React.JSX.Element {
|
||||
description: err?.message || t('nsManager.updateGroupQpmFailed'),
|
||||
isClosable: true,
|
||||
position: 'top',
|
||||
})
|
||||
});
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Update the button click handlers in the table actions column:
|
||||
const handleStatusUpdate = (id: string, currentStatus: number) => {
|
||||
const newStatus =
|
||||
currentStatus === GroupStatus.DISABLED ? GroupStatus.ENABLED : GroupStatus.DISABLED
|
||||
updateGroupStatusMutation.mutate({ id, status: newStatus })
|
||||
}
|
||||
const columnHelper = createColumnHelper<GroupInfo>()
|
||||
currentStatus === GroupStatus.DISABLED ? GroupStatus.ENABLED : GroupStatus.DISABLED;
|
||||
updateGroupStatusMutation.mutate({ id, status: newStatus });
|
||||
};
|
||||
const columnHelper = createColumnHelper<GroupInfo>();
|
||||
|
||||
const columns = useMemo<ColumnDef<GroupInfo>[]>(() => {
|
||||
return [
|
||||
@@ -186,10 +186,10 @@ export default function Home(): React.JSX.Element {
|
||||
header: t('nsManager.accessed_at'),
|
||||
accessorFn: (row) => {
|
||||
if (row.accessed_at && row.accessed_at < 0) {
|
||||
return t('key.unused')
|
||||
return t('key.unused');
|
||||
}
|
||||
|
||||
return new Date(row.accessed_at).toLocaleString()
|
||||
return new Date(row.accessed_at).toLocaleString();
|
||||
},
|
||||
id: 'accessed_at',
|
||||
},
|
||||
@@ -202,7 +202,7 @@ export default function Home(): React.JSX.Element {
|
||||
accessorFn: (row) =>
|
||||
row.status === GroupStatus.ENABLED ? t('nsManager.enabled') : t('nsManager.disabled'),
|
||||
cell: ({ getValue }) => {
|
||||
const value = getValue() as string
|
||||
const value = getValue() as string;
|
||||
return (
|
||||
<Text
|
||||
color={
|
||||
@@ -218,7 +218,7 @@ export default function Home(): React.JSX.Element {
|
||||
>
|
||||
{value}
|
||||
</Text>
|
||||
)
|
||||
);
|
||||
},
|
||||
id: 'status',
|
||||
},
|
||||
@@ -243,7 +243,7 @@ export default function Home(): React.JSX.Element {
|
||||
<CurrencySymbol type={currencySymbol} />
|
||||
</Flex>
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
},
|
||||
},
|
||||
columnHelper.display({
|
||||
@@ -409,14 +409,14 @@ export default function Home(): React.JSX.Element {
|
||||
</Menu>
|
||||
),
|
||||
}),
|
||||
]
|
||||
}, [])
|
||||
];
|
||||
}, []);
|
||||
|
||||
const table = useReactTable({
|
||||
data: groupData,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
})
|
||||
});
|
||||
|
||||
return (
|
||||
<Flex
|
||||
@@ -487,7 +487,7 @@ export default function Home(): React.JSX.Element {
|
||||
bg="white"
|
||||
boxShadow="0px 1px 2px 0px rgba(19, 51, 107, 0.05), 0px 0px 1px 0px rgba(19, 51, 107, 0.08)"
|
||||
onClick={() => {
|
||||
setGroupId('')
|
||||
setGroupId('');
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
@@ -574,5 +574,5 @@ export default function Home(): React.JSX.Element {
|
||||
{/* -- table end */}
|
||||
</Flex>
|
||||
</Flex>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,52 +1,52 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { Box, Flex, Text } from '@chakra-ui/react'
|
||||
import * as echarts from 'echarts'
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { Box, Flex, Text } from '@chakra-ui/react';
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
import { useTranslationClientSide } from '@/app/i18n/client'
|
||||
import { useI18n } from '@/providers/i18n/i18nContext'
|
||||
import { useBackendStore } from '@/store/backend'
|
||||
import { ChartDataItem } from '@/types/user/dashboard'
|
||||
import { useTranslationClientSide } from '@/app/i18n/client';
|
||||
import { useI18n } from '@/providers/i18n/i18nContext';
|
||||
import { useBackendStore } from '@/store/backend';
|
||||
import { ChartDataItem } from '@/types/user/dashboard';
|
||||
|
||||
export default function RequestDataChart({ data }: { data: ChartDataItem[] }): React.JSX.Element {
|
||||
const costChartRef = useRef<HTMLDivElement>(null)
|
||||
const requestChartRef = useRef<HTMLDivElement>(null)
|
||||
const costChartInstance = useRef<echarts.ECharts>()
|
||||
const requestChartInstance = useRef<echarts.ECharts>()
|
||||
const { lng } = useI18n()
|
||||
const { t } = useTranslationClientSide(lng, 'common')
|
||||
const { currencySymbol } = useBackendStore()
|
||||
const costChartRef = useRef<HTMLDivElement>(null);
|
||||
const requestChartRef = useRef<HTMLDivElement>(null);
|
||||
const costChartInstance = useRef<echarts.ECharts>();
|
||||
const requestChartInstance = useRef<echarts.ECharts>();
|
||||
const { lng } = useI18n();
|
||||
const { t } = useTranslationClientSide(lng, 'common');
|
||||
const { currencySymbol } = useBackendStore();
|
||||
|
||||
// Add helper function to determine date format
|
||||
const getDateFormat = (timestamps: number[]) => {
|
||||
if (timestamps.length < 2) return 'detailed'
|
||||
if (timestamps.length < 2) return 'detailed';
|
||||
|
||||
const timeDiff = timestamps[timestamps.length - 1] - timestamps[0]
|
||||
const timeDiff = timestamps[timestamps.length - 1] - timestamps[0];
|
||||
// If time difference is more than 15 days (1296000 seconds), show daily format
|
||||
return timeDiff > 1296000 ? 'daily' : 'detailed'
|
||||
}
|
||||
return timeDiff > 1296000 ? 'daily' : 'detailed';
|
||||
};
|
||||
|
||||
// 初始化图表
|
||||
useEffect(() => {
|
||||
if (costChartRef.current && requestChartRef.current) {
|
||||
costChartInstance.current = echarts.init(costChartRef.current, undefined, {
|
||||
renderer: 'svg',
|
||||
})
|
||||
});
|
||||
requestChartInstance.current = echarts.init(requestChartRef.current, undefined, {
|
||||
renderer: 'svg',
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
costChartInstance.current?.dispose()
|
||||
requestChartInstance.current?.dispose()
|
||||
costChartInstance.current = undefined
|
||||
requestChartInstance.current = undefined
|
||||
}
|
||||
}, [])
|
||||
costChartInstance.current?.dispose();
|
||||
requestChartInstance.current?.dispose();
|
||||
costChartInstance.current = undefined;
|
||||
requestChartInstance.current = undefined;
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 配置图表选项
|
||||
useEffect(() => {
|
||||
if (!costChartInstance.current || !requestChartInstance.current) return
|
||||
if (!costChartInstance.current || !requestChartInstance.current) return;
|
||||
|
||||
const commonTooltipStyle: echarts.EChartsOption['tooltip'] = {
|
||||
trigger: 'axis',
|
||||
@@ -63,7 +63,7 @@ export default function RequestDataChart({ data }: { data: ChartDataItem[] }): R
|
||||
color: '#111824',
|
||||
fontSize: 12,
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
const commonXAxis: echarts.EChartsOption['xAxis'] = {
|
||||
type: 'time',
|
||||
@@ -94,8 +94,8 @@ export default function RequestDataChart({ data }: { data: ChartDataItem[] }): R
|
||||
show: true,
|
||||
color: '#667085',
|
||||
formatter: (value: number) => {
|
||||
const date = new Date(value * 1000)
|
||||
const format = getDateFormat(data.map((item) => item.timestamp))
|
||||
const date = new Date(value * 1000);
|
||||
const format = getDateFormat(data.map((item) => item.timestamp));
|
||||
|
||||
return date
|
||||
.toLocaleString(lng, {
|
||||
@@ -107,12 +107,12 @@ export default function RequestDataChart({ data }: { data: ChartDataItem[] }): R
|
||||
hour12: false,
|
||||
}),
|
||||
})
|
||||
.replace(/\//g, '-')
|
||||
.replace(/\//g, '-');
|
||||
},
|
||||
margin: 14,
|
||||
align: 'left',
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
// 成本图表配置
|
||||
const costOption: echarts.EChartsOption = {
|
||||
@@ -123,22 +123,22 @@ export default function RequestDataChart({ data }: { data: ChartDataItem[] }): R
|
||||
| echarts.DefaultLabelFormatterCallbackParams
|
||||
| echarts.DefaultLabelFormatterCallbackParams[]
|
||||
) {
|
||||
if (!params) return ''
|
||||
const paramArray = Array.isArray(params) ? params : [params]
|
||||
if (paramArray.length === 0) return ''
|
||||
if (!params) return '';
|
||||
const paramArray = Array.isArray(params) ? params : [params];
|
||||
if (paramArray.length === 0) return '';
|
||||
|
||||
const time = new Date((paramArray[0].value as [number, number])[0] * 1000)
|
||||
const time = new Date((paramArray[0].value as [number, number])[0] * 1000);
|
||||
const timeStr = time.toLocaleString(lng, {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
});
|
||||
|
||||
let result = `
|
||||
<div style="font-weight: 500; margin-bottom: 4px; margin-top: 4px; color: #667085; font-size: 12px">${timeStr}</div>
|
||||
<div style="height: 1px; background: #DFE2EA; margin: 8px 0;"></div>
|
||||
`
|
||||
`;
|
||||
|
||||
const currency =
|
||||
currencySymbol === 'shellCoin'
|
||||
@@ -200,14 +200,14 @@ export default function RequestDataChart({ data }: { data: ChartDataItem[] }): R
|
||||
</svg>`
|
||||
: currencySymbol === 'cny'
|
||||
? '¥'
|
||||
: '$'
|
||||
: '$';
|
||||
|
||||
paramArray.forEach((param) => {
|
||||
const value = (param.value as [number, number])[1]
|
||||
const value = (param.value as [number, number])[1];
|
||||
const formattedValue = Number(value).toLocaleString(lng, {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 4,
|
||||
})
|
||||
});
|
||||
result += `
|
||||
<div style="display: flex; align-items: center; margin: 4px 0; min-width: 150px">
|
||||
<div style="display: flex; align-items: center; flex: 1">
|
||||
@@ -217,10 +217,10 @@ export default function RequestDataChart({ data }: { data: ChartDataItem[] }): R
|
||||
</div>
|
||||
<div style="font-weight: 500; color: #667085; font-size: 12px">${formattedValue}</div>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
`;
|
||||
});
|
||||
|
||||
return result
|
||||
return result;
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
@@ -269,32 +269,32 @@ export default function RequestDataChart({ data }: { data: ChartDataItem[] }): R
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
};
|
||||
|
||||
// 请求数图表配置
|
||||
const requestOption: echarts.EChartsOption = {
|
||||
tooltip: {
|
||||
...commonTooltipStyle,
|
||||
formatter: function (params) {
|
||||
if (!params) return ''
|
||||
const paramArray = Array.isArray(params) ? params : [params]
|
||||
if (paramArray.length === 0) return ''
|
||||
if (!params) return '';
|
||||
const paramArray = Array.isArray(params) ? params : [params];
|
||||
if (paramArray.length === 0) return '';
|
||||
|
||||
const time = new Date((paramArray[0].value as [number, number])[0] * 1000)
|
||||
const time = new Date((paramArray[0].value as [number, number])[0] * 1000);
|
||||
const timeStr = time.toLocaleString(lng, {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
});
|
||||
|
||||
let result = `
|
||||
<div style="font-weight: 500; margin-bottom: 4px; margin-top: 4px; color: #667085; font-size: 12px">${timeStr}</div>
|
||||
<div style="height: 1px; background: #DFE2EA; margin: 8px 0;"></div>
|
||||
`
|
||||
`;
|
||||
|
||||
paramArray.forEach((param) => {
|
||||
const value = (param.value as [number, number])[1]
|
||||
const value = (param.value as [number, number])[1];
|
||||
result += `
|
||||
<div style="display: flex; align-items: center; margin: 4px 0; min-width: 150px">
|
||||
<div style="display: flex; align-items: center; flex: 1">
|
||||
@@ -303,10 +303,10 @@ export default function RequestDataChart({ data }: { data: ChartDataItem[] }): R
|
||||
</div>
|
||||
<div style="font-weight: 500; color: #667085; font-size: 12px">${value}</div>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
`;
|
||||
});
|
||||
|
||||
return result
|
||||
return result;
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
@@ -363,31 +363,31 @@ export default function RequestDataChart({ data }: { data: ChartDataItem[] }): R
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
};
|
||||
|
||||
// 设置图表选项
|
||||
costChartInstance.current.setOption(costOption)
|
||||
requestChartInstance.current.setOption(requestOption)
|
||||
costChartInstance.current.setOption(costOption);
|
||||
requestChartInstance.current.setOption(requestOption);
|
||||
|
||||
// 图表联动
|
||||
costChartInstance.current.group = 'request-data'
|
||||
requestChartInstance.current.group = 'request-data'
|
||||
echarts.connect('request-data')
|
||||
}, [data, t, lng])
|
||||
costChartInstance.current.group = 'request-data';
|
||||
requestChartInstance.current.group = 'request-data';
|
||||
echarts.connect('request-data');
|
||||
}, [data, t, lng]);
|
||||
|
||||
// 处理窗口大小变化
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
costChartInstance.current?.resize()
|
||||
requestChartInstance.current?.resize()
|
||||
}
|
||||
costChartInstance.current?.resize();
|
||||
requestChartInstance.current?.resize();
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize)
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
}
|
||||
}, [])
|
||||
window.removeEventListener('resize', handleResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -435,5 +435,5 @@ export default function RequestDataChart({ data }: { data: ChartDataItem[] }): R
|
||||
<Box ref={requestChartRef} w="full" h="full" position="relative" minH="160px" />
|
||||
</Flex>
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
'use client'
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Box, Button, Center, Flex, Text } from '@chakra-ui/react'
|
||||
import { CurrencySymbol, MySelect } from '@sealos/ui'
|
||||
import { useQuery, UseQueryResult } from '@tanstack/react-query'
|
||||
import { useState } from 'react';
|
||||
import { Box, Button, Center, Flex, Text } from '@chakra-ui/react';
|
||||
import { CurrencySymbol, MySelect } from '@sealos/ui';
|
||||
import { useQuery, UseQueryResult } from '@tanstack/react-query';
|
||||
|
||||
import { getDashboardData } from '@/api/platform'
|
||||
import { useTranslationClientSide } from '@/app/i18n/client'
|
||||
import { useI18n } from '@/providers/i18n/i18nContext'
|
||||
import { useBackendStore } from '@/store/backend'
|
||||
import { QueryKey } from '@/types/query-key'
|
||||
import { DashboardResponse } from '@/types/user/dashboard'
|
||||
import { getDashboardData } from '@/api/platform';
|
||||
import { useTranslationClientSide } from '@/app/i18n/client';
|
||||
import { useI18n } from '@/providers/i18n/i18nContext';
|
||||
import { useBackendStore } from '@/store/backend';
|
||||
import { QueryKey } from '@/types/query-key';
|
||||
import { DashboardResponse } from '@/types/user/dashboard';
|
||||
|
||||
import RequestDataChart from './components/RequestDataChart'
|
||||
import RequestDataChart from './components/RequestDataChart';
|
||||
|
||||
export default function Home(): React.JSX.Element {
|
||||
const { lng } = useI18n()
|
||||
const { t } = useTranslationClientSide(lng, 'common')
|
||||
const { currencySymbol } = useBackendStore()
|
||||
const { lng } = useI18n();
|
||||
const { t } = useTranslationClientSide(lng, 'common');
|
||||
const { currencySymbol } = useBackendStore();
|
||||
|
||||
const [tokenName, setTokenName] = useState<string>('')
|
||||
const [model, setModel] = useState<string>('')
|
||||
const [type, setType] = useState<'week' | 'day' | 'two_week' | 'month'>('week') // default is week
|
||||
const [tokenName, setTokenName] = useState<string>('');
|
||||
const [model, setModel] = useState<string>('');
|
||||
const [type, setType] = useState<'week' | 'day' | 'two_week' | 'month'>('week'); // default is week
|
||||
|
||||
const { data: dashboardData, isLoading }: UseQueryResult<DashboardResponse['data']> = useQuery(
|
||||
[QueryKey.GetDashboardData, type, tokenName, model],
|
||||
@@ -31,7 +31,7 @@ export default function Home(): React.JSX.Element {
|
||||
...(tokenName && { token_name: tokenName }),
|
||||
...(model && { model }),
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
@@ -98,9 +98,9 @@ export default function Home(): React.JSX.Element {
|
||||
placeholder={t('dataDashboard.selectToken')}
|
||||
onchange={(token: string) => {
|
||||
if (token === 'all') {
|
||||
setTokenName('')
|
||||
setTokenName('');
|
||||
} else {
|
||||
setTokenName(token)
|
||||
setTokenName(token);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -125,9 +125,9 @@ export default function Home(): React.JSX.Element {
|
||||
]}
|
||||
onchange={(model: string) => {
|
||||
if (model === 'all') {
|
||||
setModel('')
|
||||
setModel('');
|
||||
} else {
|
||||
setModel(model)
|
||||
setModel(model);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -734,5 +734,5 @@ export default function Home(): React.JSX.Element {
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
'use client'
|
||||
'use client';
|
||||
|
||||
import { Button, Flex, Text } from '@chakra-ui/react'
|
||||
import { Button, Flex, Text } from '@chakra-ui/react';
|
||||
|
||||
import { useTranslationClientSide } from '@/app/i18n/client'
|
||||
import { MyTooltip } from '@/components/common/MyTooltip'
|
||||
import KeyList from '@/components/user/KeyList'
|
||||
import { useI18n } from '@/providers/i18n/i18nContext'
|
||||
import { useBackendStore } from '@/store/backend'
|
||||
import { useSessionStore } from '@/store/session'
|
||||
import { useTranslationClientSide } from '@/app/i18n/client';
|
||||
import { MyTooltip } from '@/components/common/MyTooltip';
|
||||
import KeyList from '@/components/user/KeyList';
|
||||
import { useI18n } from '@/providers/i18n/i18nContext';
|
||||
import { useBackendStore } from '@/store/backend';
|
||||
import { useSessionStore } from '@/store/session';
|
||||
export default function Key(): JSX.Element {
|
||||
const { isInvitationActive } = useBackendStore()
|
||||
const { lng } = useI18n()
|
||||
const { t } = useTranslationClientSide(lng, 'common')
|
||||
const { session } = useSessionStore.getState()
|
||||
const { invitationUrl } = useBackendStore()
|
||||
let userInvitationUrl = ''
|
||||
const { isInvitationActive } = useBackendStore();
|
||||
const { lng } = useI18n();
|
||||
const { t } = useTranslationClientSide(lng, 'common');
|
||||
const { session } = useSessionStore.getState();
|
||||
const { invitationUrl } = useBackendStore();
|
||||
let userInvitationUrl = '';
|
||||
if (isInvitationActive && invitationUrl) {
|
||||
const userId = session?.user.id
|
||||
const baseUrl = new URL(invitationUrl).origin
|
||||
userInvitationUrl = `${baseUrl}/?uid=${userId}`
|
||||
const userId = session?.user.id;
|
||||
const baseUrl = new URL(invitationUrl).origin;
|
||||
userInvitationUrl = `${baseUrl}/?uid=${userId}`;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -115,7 +115,7 @@ export default function Key(): JSX.Element {
|
||||
textUnderlineOffset="auto"
|
||||
cursor="pointer"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(userInvitationUrl)
|
||||
navigator.clipboard.writeText(userInvitationUrl);
|
||||
}}
|
||||
>
|
||||
{userInvitationUrl}
|
||||
@@ -139,7 +139,7 @@ export default function Key(): JSX.Element {
|
||||
bgGradient="linear(90deg, #1058FF 0%, #838AF1 57.5%, #FF80E6 100%)"
|
||||
color="white"
|
||||
onClick={() => {
|
||||
window.open(invitationUrl, '_blank')
|
||||
window.open(invitationUrl, '_blank');
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
@@ -191,5 +191,5 @@ export default function Key(): JSX.Element {
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Box, Flex } from '@chakra-ui/react'
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
|
||||
import SideBar from '@/components/user/Sidebar'
|
||||
import SideBar from '@/components/user/Sidebar';
|
||||
|
||||
export default function UserLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
@@ -13,5 +13,5 @@ export default function UserLayout({ children }: { children: React.ReactNode })
|
||||
{children}
|
||||
</Box>
|
||||
</Flex>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
'use client';
|
||||
|
||||
import ReactJson, { OnCopyProps } from 'react-json-view'
|
||||
import ReactJson, { OnCopyProps } from 'react-json-view';
|
||||
import {
|
||||
Box,
|
||||
Center,
|
||||
@@ -14,45 +14,45 @@ import {
|
||||
ModalOverlay,
|
||||
Spinner,
|
||||
Text,
|
||||
} from '@chakra-ui/react'
|
||||
import { CurrencySymbol } from '@sealos/ui'
|
||||
import { useMessage } from '@sealos/ui'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
} from '@chakra-ui/react';
|
||||
import { CurrencySymbol } from '@sealos/ui';
|
||||
import { useMessage } from '@sealos/ui';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { getUserLogDetail } from '@/api/platform'
|
||||
import { useTranslationClientSide } from '@/app/i18n/client'
|
||||
import { MyTooltip } from '@/components/common/MyTooltip'
|
||||
import { useI18n } from '@/providers/i18n/i18nContext'
|
||||
import { useBackendStore } from '@/store/backend'
|
||||
import { QueryKey } from '@/types/query-key'
|
||||
import { LogItem } from '@/types/user/logs'
|
||||
import { getTranslationWithFallback } from '@/utils/common'
|
||||
import { getUserLogDetail } from '@/api/platform';
|
||||
import { useTranslationClientSide } from '@/app/i18n/client';
|
||||
import { MyTooltip } from '@/components/common/MyTooltip';
|
||||
import { useI18n } from '@/providers/i18n/i18nContext';
|
||||
import { useBackendStore } from '@/store/backend';
|
||||
import { QueryKey } from '@/types/query-key';
|
||||
import { LogItem } from '@/types/user/logs';
|
||||
import { getTranslationWithFallback } from '@/utils/common';
|
||||
|
||||
import { getTimeDiff } from '../tools/handleTime'
|
||||
import { getTimeDiff } from '../tools/handleTime';
|
||||
|
||||
export default function LogDetailModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
rowData,
|
||||
}: {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
rowData: LogItem | null
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
rowData: LogItem | null;
|
||||
}): React.JSX.Element {
|
||||
const { lng } = useI18n()
|
||||
const { t } = useTranslationClientSide(lng, 'common')
|
||||
const { currencySymbol } = useBackendStore()
|
||||
const { lng } = useI18n();
|
||||
const { t } = useTranslationClientSide(lng, 'common');
|
||||
const { currencySymbol } = useBackendStore();
|
||||
|
||||
const { data: logDetail, isLoading } = useQuery({
|
||||
queryKey: [QueryKey.GetUserLogDetail, rowData?.request_detail?.log_id],
|
||||
queryFn: () => {
|
||||
if (!rowData?.request_detail?.log_id) throw new Error('No log ID')
|
||||
return getUserLogDetail(rowData.request_detail.log_id)
|
||||
if (!rowData?.request_detail?.log_id) throw new Error('No log ID');
|
||||
return getUserLogDetail(rowData.request_detail.log_id);
|
||||
},
|
||||
enabled: !!rowData?.request_detail?.log_id,
|
||||
})
|
||||
});
|
||||
|
||||
const isDetailLoading = !!rowData?.request_detail?.log_id && isLoading
|
||||
const isDetailLoading = !!rowData?.request_detail?.log_id && isLoading;
|
||||
|
||||
const { message } = useMessage({
|
||||
warningBoxBg: '#FFFAEB',
|
||||
@@ -61,14 +61,14 @@ export default function LogDetailModal({
|
||||
successBoxBg: '#EDFBF3',
|
||||
successIconBg: '#039855',
|
||||
successIconFill: 'white',
|
||||
})
|
||||
});
|
||||
|
||||
// 定义默认的网格配置
|
||||
const gridConfig = {
|
||||
labelWidth: '153px',
|
||||
rowHeight: '48px',
|
||||
jsonContentHeight: '122px',
|
||||
}
|
||||
};
|
||||
|
||||
const renderDetailRow = (
|
||||
leftLabel: string | React.ReactNode | null,
|
||||
@@ -76,15 +76,15 @@ export default function LogDetailModal({
|
||||
rightLabel?: string | React.ReactNode | null,
|
||||
rightValue?: string | number | React.ReactNode | undefined,
|
||||
options?: {
|
||||
labelWidth?: string
|
||||
rowHeight?: string
|
||||
isFirst?: boolean
|
||||
isLast?: boolean
|
||||
labelWidth?: string;
|
||||
rowHeight?: string;
|
||||
isFirst?: boolean;
|
||||
isLast?: boolean;
|
||||
}
|
||||
) => {
|
||||
// 辅助函数:渲染标签
|
||||
const renderLabel = (label: string | React.ReactNode | null) => {
|
||||
if (label === null) return null
|
||||
if (label === null) return null;
|
||||
if (typeof label === 'string') {
|
||||
return (
|
||||
<Text
|
||||
@@ -96,10 +96,10 @@ export default function LogDetailModal({
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
)
|
||||
);
|
||||
}
|
||||
return label
|
||||
}
|
||||
return label;
|
||||
};
|
||||
|
||||
// 辅助函数:渲染值
|
||||
const renderValue = (value: string | number | React.ReactNode | undefined) => {
|
||||
@@ -115,17 +115,17 @@ export default function LogDetailModal({
|
||||
>
|
||||
{value}
|
||||
</Text>
|
||||
)
|
||||
);
|
||||
}
|
||||
return value
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
// 判断是否显示左侧或右侧列
|
||||
const showLeftSection = leftLabel !== null && leftLabel !== undefined
|
||||
const showRightSection = rightLabel !== null && rightLabel !== undefined
|
||||
const showLeftSection = leftLabel !== null && leftLabel !== undefined;
|
||||
const showRightSection = rightLabel !== null && rightLabel !== undefined;
|
||||
|
||||
// 根据显示的列数设置模板
|
||||
const gridTemplateColumns = showLeftSection && showRightSection ? '1fr 1fr' : '1fr'
|
||||
const gridTemplateColumns = showLeftSection && showRightSection ? '1fr 1fr' : '1fr';
|
||||
|
||||
return (
|
||||
<Grid
|
||||
@@ -206,35 +206,35 @@ export default function LogDetailModal({
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const renderJsonContent = (
|
||||
label: string,
|
||||
content: string | undefined,
|
||||
options?: {
|
||||
labelWidth?: string
|
||||
contentHeight?: string
|
||||
isFirst?: boolean
|
||||
isLast?: boolean
|
||||
labelWidth?: string;
|
||||
contentHeight?: string;
|
||||
isFirst?: boolean;
|
||||
isLast?: boolean;
|
||||
}
|
||||
) => {
|
||||
if (!content) return null
|
||||
if (!content) return null;
|
||||
const handleCopy = (copy: OnCopyProps) => {
|
||||
if (typeof window === 'undefined') return
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
const copyText =
|
||||
typeof copy.src === 'object' ? JSON.stringify(copy.src, null, 2) : String(copy.src)
|
||||
typeof copy.src === 'object' ? JSON.stringify(copy.src, null, 2) : String(copy.src);
|
||||
|
||||
navigator.clipboard.writeText(copyText)
|
||||
}
|
||||
navigator.clipboard.writeText(copyText);
|
||||
};
|
||||
|
||||
let parsed = null
|
||||
let parsed = null;
|
||||
|
||||
try {
|
||||
parsed = JSON.parse(content)
|
||||
parsed = JSON.parse(content);
|
||||
} catch {
|
||||
parsed = content
|
||||
parsed = content;
|
||||
}
|
||||
return (
|
||||
<Grid
|
||||
@@ -325,8 +325,8 @@ export default function LogDetailModal({
|
||||
)}
|
||||
</Box>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return isDetailLoading ? (
|
||||
<Modal isOpen={isOpen} onClose={onClose} isCentered>
|
||||
@@ -522,7 +522,7 @@ export default function LogDetailModal({
|
||||
isClosable: true,
|
||||
duration: 2000,
|
||||
position: 'top',
|
||||
})
|
||||
});
|
||||
},
|
||||
(err) => {
|
||||
message({
|
||||
@@ -531,9 +531,9 @@ export default function LogDetailModal({
|
||||
description: err?.message || t('copyFailed'),
|
||||
isClosable: true,
|
||||
position: 'top',
|
||||
})
|
||||
});
|
||||
}
|
||||
)
|
||||
);
|
||||
}}
|
||||
>
|
||||
{rowData.content}
|
||||
@@ -771,5 +771,5 @@ export default function LogDetailModal({
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
'use client';
|
||||
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useMemo, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
@@ -11,51 +11,51 @@ import {
|
||||
InputRightElement,
|
||||
Text,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react'
|
||||
import { CurrencySymbol, MySelect, MyTooltip } from '@sealos/ui'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { createColumnHelper, getCoreRowModel, useReactTable } from '@tanstack/react-table'
|
||||
import dynamic from 'next/dynamic'
|
||||
} from '@chakra-ui/react';
|
||||
import { CurrencySymbol, MySelect, MyTooltip } from '@sealos/ui';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { createColumnHelper, getCoreRowModel, useReactTable } from '@tanstack/react-table';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
import { getEnabledMode, getTokens, getUserLogs } from '@/api/platform'
|
||||
import { useTranslationClientSide } from '@/app/i18n/client'
|
||||
import SelectDateRange from '@/components/common/SelectDateRange'
|
||||
import { SingleSelectComboboxUnstyle } from '@/components/common/SingleSelectComboboxUnStyle'
|
||||
import SwitchPage from '@/components/common/SwitchPage'
|
||||
import { BaseTable } from '@/components/table/BaseTable'
|
||||
import { useDebounce } from '@/hooks/useDebounce'
|
||||
import { useI18n } from '@/providers/i18n/i18nContext'
|
||||
import { useBackendStore } from '@/store/backend'
|
||||
import { QueryKey } from '@/types/query-key'
|
||||
import { LogItem } from '@/types/user/logs'
|
||||
import { getEnabledMode, getTokens, getUserLogs } from '@/api/platform';
|
||||
import { useTranslationClientSide } from '@/app/i18n/client';
|
||||
import SelectDateRange from '@/components/common/SelectDateRange';
|
||||
import { SingleSelectComboboxUnstyle } from '@/components/common/SingleSelectComboboxUnStyle';
|
||||
import SwitchPage from '@/components/common/SwitchPage';
|
||||
import { BaseTable } from '@/components/table/BaseTable';
|
||||
import { useDebounce } from '@/hooks/useDebounce';
|
||||
import { useI18n } from '@/providers/i18n/i18nContext';
|
||||
import { useBackendStore } from '@/store/backend';
|
||||
import { QueryKey } from '@/types/query-key';
|
||||
import { LogItem } from '@/types/user/logs';
|
||||
|
||||
import { getTimeDiff } from './tools/handleTime'
|
||||
import { getTimeDiff } from './tools/handleTime';
|
||||
|
||||
const LogDetailModal = dynamic(
|
||||
() => import('./components/LogDetailModal'),
|
||||
{ ssr: false } // 禁用服务端渲染
|
||||
)
|
||||
);
|
||||
|
||||
export default function Logs(): React.JSX.Element {
|
||||
const { lng } = useI18n()
|
||||
const { t } = useTranslationClientSide(lng, 'common')
|
||||
const { currencySymbol } = useBackendStore()
|
||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||
const [selectedRow, setSelectedRow] = useState<LogItem | null>(null)
|
||||
const { lng } = useI18n();
|
||||
const { t } = useTranslationClientSide(lng, 'common');
|
||||
const { currencySymbol } = useBackendStore();
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const [selectedRow, setSelectedRow] = useState<LogItem | null>(null);
|
||||
|
||||
const [startTime, setStartTime] = useState(() => {
|
||||
const currentDate = new Date()
|
||||
currentDate.setDate(currentDate.getDate() - 3)
|
||||
return currentDate
|
||||
})
|
||||
const [endTime, setEndTime] = useState(new Date())
|
||||
const [keyName, setKeyName] = useState('')
|
||||
const [codeType, setCodeType] = useState('all')
|
||||
const [modelName, setModelName] = useState('')
|
||||
const [page, setPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(10)
|
||||
const [inputKeyword, setInputKeyword] = useState('')
|
||||
const debouncedKeyword = useDebounce(inputKeyword, 500) // 500ms 延迟 0.5s
|
||||
const currentDate = new Date();
|
||||
currentDate.setDate(currentDate.getDate() - 3);
|
||||
return currentDate;
|
||||
});
|
||||
const [endTime, setEndTime] = useState(new Date());
|
||||
const [keyName, setKeyName] = useState('');
|
||||
const [codeType, setCodeType] = useState('all');
|
||||
const [modelName, setModelName] = useState('');
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [inputKeyword, setInputKeyword] = useState('');
|
||||
const debouncedKeyword = useDebounce(inputKeyword, 500); // 500ms 延迟 0.5s
|
||||
|
||||
const { data: logData, isLoading } = useQuery(
|
||||
[
|
||||
@@ -80,9 +80,9 @@ export default function Logs(): React.JSX.Element {
|
||||
start_timestamp: startTime.getTime().toString(),
|
||||
end_timestamp: endTime.getTime().toString(),
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const columnHelper = createColumnHelper<LogItem>()
|
||||
const columnHelper = createColumnHelper<LogItem>();
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
@@ -189,7 +189,7 @@ export default function Logs(): React.JSX.Element {
|
||||
</Text>
|
||||
),
|
||||
cell: ({ getValue, row }) => {
|
||||
const code = getValue()
|
||||
const code = getValue();
|
||||
return (
|
||||
<Flex alignItems="center" gap="4px" w="fit-content">
|
||||
<Text
|
||||
@@ -221,7 +221,7 @@ export default function Logs(): React.JSX.Element {
|
||||
</MyTooltip>
|
||||
)}
|
||||
</Flex>
|
||||
)
|
||||
);
|
||||
},
|
||||
id: 'status',
|
||||
}),
|
||||
@@ -256,7 +256,7 @@ export default function Logs(): React.JSX.Element {
|
||||
</Flex>
|
||||
</MyTooltip>
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
},
|
||||
id: 'used_amount',
|
||||
}),
|
||||
@@ -277,8 +277,8 @@ export default function Logs(): React.JSX.Element {
|
||||
cell: ({ row }) => (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setSelectedRow(row.original)
|
||||
onOpen()
|
||||
setSelectedRow(row.original);
|
||||
onOpen();
|
||||
}}
|
||||
h="28px"
|
||||
variant="unstyled"
|
||||
@@ -338,13 +338,13 @@ export default function Logs(): React.JSX.Element {
|
||||
}),
|
||||
],
|
||||
[t, currencySymbol]
|
||||
)
|
||||
);
|
||||
|
||||
const table = useReactTable({
|
||||
data: logData?.logs || [],
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
})
|
||||
});
|
||||
|
||||
return (
|
||||
<Flex
|
||||
@@ -414,7 +414,7 @@ export default function Logs(): React.JSX.Element {
|
||||
}}
|
||||
value={inputKeyword}
|
||||
onChange={(e) => {
|
||||
setInputKeyword(e.target.value)
|
||||
setInputKeyword(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<InputRightElement w="32px" h="32px">
|
||||
@@ -472,9 +472,9 @@ export default function Logs(): React.JSX.Element {
|
||||
bg="white"
|
||||
boxShadow="0px 1px 2px 0px rgba(19, 51, 107, 0.05), 0px 0px 1px 0px rgba(19, 51, 107, 0.08)"
|
||||
onClick={() => {
|
||||
setKeyName('')
|
||||
setModelName('')
|
||||
setCodeType('all')
|
||||
setKeyName('');
|
||||
setModelName('');
|
||||
setCodeType('all');
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
@@ -524,16 +524,16 @@ export default function Logs(): React.JSX.Element {
|
||||
dropdownItems={['all', ...(logData?.token_names || [])]}
|
||||
setSelectedItem={(value) => {
|
||||
if (value === 'all') {
|
||||
setKeyName('')
|
||||
setKeyName('');
|
||||
} else {
|
||||
setKeyName(value)
|
||||
setKeyName(value);
|
||||
}
|
||||
}}
|
||||
handleDropdownItemFilter={(dropdownItems, inputValue) => {
|
||||
const lowerCasedInput = inputValue.toLowerCase()
|
||||
const lowerCasedInput = inputValue.toLowerCase();
|
||||
return dropdownItems.filter(
|
||||
(item) => !inputValue || item.toLowerCase().includes(lowerCasedInput)
|
||||
)
|
||||
);
|
||||
}}
|
||||
handleDropdownItemDisplay={(dropdownItem) => {
|
||||
return (
|
||||
@@ -548,7 +548,7 @@ export default function Logs(): React.JSX.Element {
|
||||
>
|
||||
{dropdownItem}
|
||||
</Text>
|
||||
)
|
||||
);
|
||||
}}
|
||||
flexProps={{ w: '280px' }}
|
||||
placeholder={t('logs.select_token_name')}
|
||||
@@ -572,16 +572,16 @@ export default function Logs(): React.JSX.Element {
|
||||
dropdownItems={['all', ...(logData?.models || [])]}
|
||||
setSelectedItem={(value) => {
|
||||
if (value === 'all') {
|
||||
setModelName('')
|
||||
setModelName('');
|
||||
} else {
|
||||
setModelName(value)
|
||||
setModelName(value);
|
||||
}
|
||||
}}
|
||||
handleDropdownItemFilter={(dropdownItems, inputValue) => {
|
||||
const lowerCasedInput = inputValue.toLowerCase()
|
||||
const lowerCasedInput = inputValue.toLowerCase();
|
||||
return dropdownItems.filter(
|
||||
(item) => !inputValue || item.toLowerCase().includes(lowerCasedInput)
|
||||
)
|
||||
);
|
||||
}}
|
||||
handleDropdownItemDisplay={(dropdownItem) => {
|
||||
return (
|
||||
@@ -596,7 +596,7 @@ export default function Logs(): React.JSX.Element {
|
||||
>
|
||||
{dropdownItem}
|
||||
</Text>
|
||||
)
|
||||
);
|
||||
}}
|
||||
flexProps={{ w: '280px' }}
|
||||
placeholder={t('logs.select_modal')}
|
||||
@@ -639,7 +639,7 @@ export default function Logs(): React.JSX.Element {
|
||||
},
|
||||
]}
|
||||
onchange={(val: string) => {
|
||||
setCodeType(val)
|
||||
setCodeType(val);
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
@@ -688,5 +688,5 @@ export default function Logs(): React.JSX.Element {
|
||||
</Flex>
|
||||
<LogDetailModal isOpen={isOpen} onClose={onClose} rowData={selectedRow} />
|
||||
</Flex>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
'use client'
|
||||
import { Badge, Box, Center, Flex, Icon, Skeleton, SkeletonText, Text } from '@chakra-ui/react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
'use client';
|
||||
import { Badge, Box, Center, Flex, Icon, Skeleton, SkeletonText, Text } from '@chakra-ui/react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
|
||||
import { getMcpDetail } from '@/api/platform'
|
||||
import { useTranslationClientSide } from '@/app/i18n/client'
|
||||
import { useI18n } from '@/providers/i18n/i18nContext'
|
||||
import { QueryKey } from '@/types/query-key'
|
||||
import { getMcpDetail } from '@/api/platform';
|
||||
import { useTranslationClientSide } from '@/app/i18n/client';
|
||||
import { useI18n } from '@/providers/i18n/i18nContext';
|
||||
import { QueryKey } from '@/types/query-key';
|
||||
|
||||
import McpDetailBody from '../components/McpDetailBody'
|
||||
import McpDetailHeader from '../components/McpDetailHeader'
|
||||
import McpDetailBody from '../components/McpDetailBody';
|
||||
import McpDetailHeader from '../components/McpDetailHeader';
|
||||
|
||||
export default function McpDetailPage() {
|
||||
const { lng } = useI18n()
|
||||
const { t } = useTranslationClientSide(lng, 'common')
|
||||
const params = useParams()
|
||||
const router = useRouter()
|
||||
const id = params.id as string
|
||||
const { lng } = useI18n();
|
||||
const { t } = useTranslationClientSide(lng, 'common');
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const id = params.id as string;
|
||||
|
||||
const { data: mcpDetail, isLoading } = useQuery({
|
||||
queryKey: [QueryKey.mcpDetail, id],
|
||||
queryFn: () => getMcpDetail(id),
|
||||
enabled: !!id,
|
||||
})
|
||||
});
|
||||
|
||||
const handleNavigateToHub = () => {
|
||||
router.push(`/${lng}/mcp-hub`)
|
||||
}
|
||||
router.push(`/${lng}/mcp-hub`);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
@@ -115,7 +115,7 @@ export default function McpDetailPage() {
|
||||
</Flex>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!mcpDetail) {
|
||||
@@ -127,7 +127,7 @@ export default function McpDetailPage() {
|
||||
</Text>
|
||||
</Box>
|
||||
</Center>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -205,5 +205,5 @@ export default function McpDetailPage() {
|
||||
</Flex>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
'use client'
|
||||
import { Badge, Box, Flex, Icon, Text } from '@chakra-ui/react'
|
||||
import Image from 'next/image'
|
||||
import { useRouter } from 'next/navigation'
|
||||
'use client';
|
||||
import { Badge, Box, Flex, Icon, Text } from '@chakra-ui/react';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { useTranslationClientSide } from '@/app/i18n/client'
|
||||
import { useI18n } from '@/providers/i18n/i18nContext'
|
||||
import { Mcp } from '@/types/mcp'
|
||||
import { getMcpIcon } from '@/ui/icons/mcp-icons'
|
||||
import { useTranslationClientSide } from '@/app/i18n/client';
|
||||
import { useI18n } from '@/providers/i18n/i18nContext';
|
||||
import { Mcp } from '@/types/mcp';
|
||||
import { getMcpIcon } from '@/ui/icons/mcp-icons';
|
||||
|
||||
export interface McpCardProps {
|
||||
mcp: Mcp
|
||||
mcp: Mcp;
|
||||
}
|
||||
|
||||
export default function McpCard({ mcp }: McpCardProps) {
|
||||
const { lng } = useI18n()
|
||||
const { t } = useTranslationClientSide(lng, 'common')
|
||||
const router = useRouter()
|
||||
const { lng } = useI18n();
|
||||
const { t } = useTranslationClientSide(lng, 'common');
|
||||
const router = useRouter();
|
||||
|
||||
const mcpName = lng === 'zh' ? mcp.name_cn || mcp.name : mcp.name
|
||||
const mcpDescription = lng === 'zh' ? mcp.description_cn || mcp.description : mcp.description
|
||||
const mcpName = lng === 'zh' ? mcp.name_cn || mcp.name : mcp.name;
|
||||
const mcpDescription = lng === 'zh' ? mcp.description_cn || mcp.description : mcp.description;
|
||||
|
||||
const handleCardClick = () => {
|
||||
router.push(`/${lng}/mcp-hub/${mcp.id}`)
|
||||
}
|
||||
const iconSrc = getMcpIcon(mcp.id)
|
||||
router.push(`/${lng}/mcp-hub/${mcp.id}`);
|
||||
};
|
||||
const iconSrc = getMcpIcon(mcp.id);
|
||||
return (
|
||||
<Box
|
||||
bg="white"
|
||||
@@ -132,5 +132,5 @@ export default function McpCard({ mcp }: McpCardProps) {
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
+38
-38
@@ -1,4 +1,4 @@
|
||||
'use client'
|
||||
'use client';
|
||||
import {
|
||||
Alert,
|
||||
AlertIcon,
|
||||
@@ -12,33 +12,33 @@ import {
|
||||
TabPanels,
|
||||
Tabs,
|
||||
Text,
|
||||
} from '@chakra-ui/react'
|
||||
import { useMessage } from '@sealos/ui'
|
||||
import { useRouter } from 'next/navigation'
|
||||
} from '@chakra-ui/react';
|
||||
import { useMessage } from '@sealos/ui';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { useTranslationClientSide } from '@/app/i18n/client'
|
||||
import { useI18n } from '@/providers/i18n/i18nContext'
|
||||
import { McpDetail } from '@/types/mcp'
|
||||
import { useTranslationClientSide } from '@/app/i18n/client';
|
||||
import { useI18n } from '@/providers/i18n/i18nContext';
|
||||
import { McpDetail } from '@/types/mcp';
|
||||
|
||||
export interface McpConnectionInfoProps {
|
||||
mcpDetail: McpDetail
|
||||
setViewMode?: () => void
|
||||
mcpDetail: McpDetail;
|
||||
setViewMode?: () => void;
|
||||
}
|
||||
|
||||
// 自定义配置渲染组件
|
||||
const ConfigRenderer = ({ config, lng }: { config: any; lng: string }) => {
|
||||
const router = useRouter()
|
||||
const router = useRouter();
|
||||
|
||||
const handleTokenClick = () => {
|
||||
router.push(`/${lng}/key`)
|
||||
}
|
||||
router.push(`/${lng}/key`);
|
||||
};
|
||||
|
||||
const renderConfigContent = () => {
|
||||
const configString = JSON.stringify(config, null, 2)
|
||||
const parts = configString.split('your_token')
|
||||
const configString = JSON.stringify(config, null, 2);
|
||||
const parts = configString.split('your_token');
|
||||
|
||||
if (parts.length === 1) {
|
||||
return <span>{configString}</span>
|
||||
return <span>{configString}</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -61,8 +61,8 @@ const ConfigRenderer = ({ config, lng }: { config: any; lng: string }) => {
|
||||
</span>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Code
|
||||
@@ -77,20 +77,20 @@ const ConfigRenderer = ({ config, lng }: { config: any; lng: string }) => {
|
||||
>
|
||||
{renderConfigContent()}
|
||||
</Code>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default function McpConnectionInfo({ mcpDetail, setViewMode }: McpConnectionInfoProps) {
|
||||
const { lng } = useI18n()
|
||||
const { t } = useTranslationClientSide(lng, 'common')
|
||||
const { message } = useMessage()
|
||||
const { lng } = useI18n();
|
||||
const { t } = useTranslationClientSide(lng, 'common');
|
||||
const { message } = useMessage();
|
||||
|
||||
const hasSSE = mcpDetail.endpoints?.sse
|
||||
const hasHTTP = mcpDetail.endpoints?.streamable_http
|
||||
const hasSSE = mcpDetail.endpoints?.sse;
|
||||
const hasHTTP = mcpDetail.endpoints?.streamable_http;
|
||||
|
||||
const generateConfig = (type: 'sse' | 'streamable_http') => {
|
||||
const endpoint = type === 'sse' ? mcpDetail.endpoints.sse : mcpDetail.endpoints.streamable_http
|
||||
const url = `https://${mcpDetail.endpoints.host}${endpoint}?key=your_token`
|
||||
const endpoint = type === 'sse' ? mcpDetail.endpoints.sse : mcpDetail.endpoints.streamable_http;
|
||||
const url = `https://${mcpDetail.endpoints.host}${endpoint}?key=your_token`;
|
||||
|
||||
return {
|
||||
mcpServers: {
|
||||
@@ -99,11 +99,11 @@ export default function McpConnectionInfo({ mcpDetail, setViewMode }: McpConnect
|
||||
url: url,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const copyToClipboard = (config: object) => {
|
||||
const configString = JSON.stringify(config, null, 2)
|
||||
const configString = JSON.stringify(config, null, 2);
|
||||
navigator.clipboard
|
||||
.writeText(configString)
|
||||
.then(() => {
|
||||
@@ -111,20 +111,20 @@ export default function McpConnectionInfo({ mcpDetail, setViewMode }: McpConnect
|
||||
status: 'success',
|
||||
title: t('copySuccess'),
|
||||
duration: 2000,
|
||||
})
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
message({
|
||||
status: 'error',
|
||||
title: t('copyFailed'),
|
||||
duration: 2000,
|
||||
})
|
||||
})
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 如果只有一种连接方式,就不显示Tab
|
||||
if (hasSSE && !hasHTTP) {
|
||||
const config = generateConfig('sse')
|
||||
const config = generateConfig('sse');
|
||||
return (
|
||||
<Box h="full" overflow="hidden">
|
||||
<Flex direction="column" gap="16px" h="full">
|
||||
@@ -150,11 +150,11 @@ export default function McpConnectionInfo({ mcpDetail, setViewMode }: McpConnect
|
||||
</Box>
|
||||
</Flex>
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasSSE && hasHTTP) {
|
||||
const config = generateConfig('streamable_http')
|
||||
const config = generateConfig('streamable_http');
|
||||
return (
|
||||
<Box h="full" overflow="hidden">
|
||||
<Flex direction="column" gap="16px" h="full">
|
||||
@@ -180,7 +180,7 @@ export default function McpConnectionInfo({ mcpDetail, setViewMode }: McpConnect
|
||||
</Box>
|
||||
</Flex>
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// 有两种连接方式,显示Tab
|
||||
@@ -254,5 +254,5 @@ export default function McpConnectionInfo({ mcpDetail, setViewMode }: McpConnect
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
'use client'
|
||||
import { Box, Flex } from '@chakra-ui/react'
|
||||
'use client';
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
|
||||
import { McpDetail } from '@/types/mcp'
|
||||
import { McpDetail } from '@/types/mcp';
|
||||
|
||||
import McpParamsConfig from './McpParamsConfig'
|
||||
import McpReadme from './McpReadme'
|
||||
import McpParamsConfig from './McpParamsConfig';
|
||||
import McpReadme from './McpReadme';
|
||||
|
||||
export interface McpDetailBodyProps {
|
||||
mcpDetail: McpDetail
|
||||
mcpDetail: McpDetail;
|
||||
}
|
||||
|
||||
export default function McpDetailBody({ mcpDetail }: McpDetailBodyProps) {
|
||||
@@ -27,5 +27,5 @@ export default function McpDetailBody({ mcpDetail }: McpDetailBodyProps) {
|
||||
</Box>
|
||||
)}
|
||||
</Flex>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
+14
-14
@@ -1,25 +1,25 @@
|
||||
'use client'
|
||||
import { Badge, Box, Flex, IconButton, Text } from '@chakra-ui/react'
|
||||
import Image from 'next/image'
|
||||
'use client';
|
||||
import { Badge, Box, Flex, IconButton, Text } from '@chakra-ui/react';
|
||||
import Image from 'next/image';
|
||||
|
||||
import { useTranslationClientSide } from '@/app/i18n/client'
|
||||
import { useI18n } from '@/providers/i18n/i18nContext'
|
||||
import { McpDetail } from '@/types/mcp'
|
||||
import { getMcpIcon } from '@/ui/icons/mcp-icons'
|
||||
import { useTranslationClientSide } from '@/app/i18n/client';
|
||||
import { useI18n } from '@/providers/i18n/i18nContext';
|
||||
import { McpDetail } from '@/types/mcp';
|
||||
import { getMcpIcon } from '@/ui/icons/mcp-icons';
|
||||
|
||||
export interface McpDetailHeaderProps {
|
||||
mcpDetail: McpDetail
|
||||
mcpDetail: McpDetail;
|
||||
}
|
||||
|
||||
export default function McpDetailHeader({ mcpDetail }: McpDetailHeaderProps) {
|
||||
const { lng } = useI18n()
|
||||
const { t } = useTranslationClientSide(lng, 'common')
|
||||
const { lng } = useI18n();
|
||||
const { t } = useTranslationClientSide(lng, 'common');
|
||||
|
||||
const mcpName = lng === 'zh' ? mcpDetail.name_cn || mcpDetail.name : mcpDetail.name
|
||||
const mcpName = lng === 'zh' ? mcpDetail.name_cn || mcpDetail.name : mcpDetail.name;
|
||||
const mcpDescription =
|
||||
lng === 'zh' ? mcpDetail.description_cn || mcpDetail.description : mcpDetail.description
|
||||
lng === 'zh' ? mcpDetail.description_cn || mcpDetail.description : mcpDetail.description;
|
||||
|
||||
const iconSrc = getMcpIcon(mcpDetail.id)
|
||||
const iconSrc = getMcpIcon(mcpDetail.id);
|
||||
|
||||
return (
|
||||
<Flex direction="column" gap="16px">
|
||||
@@ -100,5 +100,5 @@ export default function McpDetailHeader({ mcpDetail }: McpDetailHeaderProps) {
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
'use client'
|
||||
import { Box, Center, Grid, Skeleton, SkeletonText, Text } from '@chakra-ui/react'
|
||||
'use client';
|
||||
import { Box, Center, Grid, Skeleton, SkeletonText, Text } from '@chakra-ui/react';
|
||||
|
||||
import { useTranslationClientSide } from '@/app/i18n/client'
|
||||
import { useI18n } from '@/providers/i18n/i18nContext'
|
||||
import { Mcp } from '@/types/mcp'
|
||||
import { useTranslationClientSide } from '@/app/i18n/client';
|
||||
import { useI18n } from '@/providers/i18n/i18nContext';
|
||||
import { Mcp } from '@/types/mcp';
|
||||
|
||||
import McpCard from './McpCard'
|
||||
import McpCard from './McpCard';
|
||||
|
||||
export interface McpListProps {
|
||||
mcps: Mcp[]
|
||||
isLoading: boolean
|
||||
mcps: Mcp[];
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
// Skeleton Card Component
|
||||
@@ -39,12 +39,12 @@ function McpCardSkeleton() {
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default function McpList({ mcps, isLoading }: McpListProps) {
|
||||
const { lng } = useI18n()
|
||||
const { t } = useTranslationClientSide(lng, 'common')
|
||||
const { lng } = useI18n();
|
||||
const { t } = useTranslationClientSide(lng, 'common');
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
@@ -61,7 +61,7 @@ export default function McpList({ mcps, isLoading }: McpListProps) {
|
||||
<McpCardSkeleton key={index} />
|
||||
))}
|
||||
</Grid>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (mcps.length === 0) {
|
||||
@@ -73,7 +73,7 @@ export default function McpList({ mcps, isLoading }: McpListProps) {
|
||||
</Text>
|
||||
</Box>
|
||||
</Center>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -90,5 +90,5 @@ export default function McpList({ mcps, isLoading }: McpListProps) {
|
||||
<McpCard key={mcp.id} mcp={mcp} />
|
||||
))}
|
||||
</Grid>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
+16
-16
@@ -1,28 +1,28 @@
|
||||
'use client'
|
||||
import { useState } from 'react'
|
||||
import { Box, Flex, Text } from '@chakra-ui/react'
|
||||
'use client';
|
||||
import { useState } from 'react';
|
||||
import { Box, Flex, Text } from '@chakra-ui/react';
|
||||
|
||||
import { useTranslationClientSide } from '@/app/i18n/client'
|
||||
import { useI18n } from '@/providers/i18n/i18nContext'
|
||||
import { McpDetail } from '@/types/mcp'
|
||||
import { useTranslationClientSide } from '@/app/i18n/client';
|
||||
import { useI18n } from '@/providers/i18n/i18nContext';
|
||||
import { McpDetail } from '@/types/mcp';
|
||||
|
||||
import McpConnectionInfo from './McpConnectionInfo'
|
||||
import McpParamsForm from './McpParamsForm'
|
||||
import McpConnectionInfo from './McpConnectionInfo';
|
||||
import McpParamsForm from './McpParamsForm';
|
||||
|
||||
export interface McpParamsConfigProps {
|
||||
mcpDetail: McpDetail
|
||||
mcpDetail: McpDetail;
|
||||
}
|
||||
|
||||
type ViewMode = 'info' | 'config'
|
||||
type ViewMode = 'info' | 'config';
|
||||
|
||||
export default function McpParamsConfig({ mcpDetail }: McpParamsConfigProps) {
|
||||
const { lng } = useI18n()
|
||||
const { t } = useTranslationClientSide(lng, 'common')
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('info')
|
||||
const { lng } = useI18n();
|
||||
const { t } = useTranslationClientSide(lng, 'common');
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('info');
|
||||
|
||||
// 检查是否有连接信息
|
||||
const hasConnectionInfo = !!mcpDetail.endpoints?.host
|
||||
const hasReusing = Object.keys(mcpDetail.reusing || {}).length > 0
|
||||
const hasConnectionInfo = !!mcpDetail.endpoints?.host;
|
||||
const hasReusing = Object.keys(mcpDetail.reusing || {}).length > 0;
|
||||
|
||||
return (
|
||||
<Box minH="0px" overflow="hidden" bg="grayModern.25" borderRadius="8px" p="20px">
|
||||
@@ -56,5 +56,5 @@ export default function McpParamsConfig({ mcpDetail }: McpParamsConfigProps) {
|
||||
</Box>
|
||||
</Flex>
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import { Dispatch, SetStateAction, useState } from 'react'
|
||||
'use client';
|
||||
import { Dispatch, SetStateAction, useState } from 'react';
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
@@ -11,20 +11,20 @@ import {
|
||||
Input,
|
||||
Text,
|
||||
VStack,
|
||||
} from '@chakra-ui/react'
|
||||
import { useMessage } from '@sealos/ui'
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
} from '@chakra-ui/react';
|
||||
import { useMessage } from '@sealos/ui';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { updateMcpParams } from '@/api/platform'
|
||||
import { useTranslationClientSide } from '@/app/i18n/client'
|
||||
import { useI18n } from '@/providers/i18n/i18nContext'
|
||||
import { McpDetail } from '@/types/mcp'
|
||||
import { QueryKey } from '@/types/query-key'
|
||||
import { updateMcpParams } from '@/api/platform';
|
||||
import { useTranslationClientSide } from '@/app/i18n/client';
|
||||
import { useI18n } from '@/providers/i18n/i18nContext';
|
||||
import { McpDetail } from '@/types/mcp';
|
||||
import { QueryKey } from '@/types/query-key';
|
||||
|
||||
export interface McpParamsFormProps {
|
||||
mcpDetail: McpDetail
|
||||
showBackButton?: boolean
|
||||
setViewMode?: Dispatch<SetStateAction<'info' | 'config'>>
|
||||
mcpDetail: McpDetail;
|
||||
showBackButton?: boolean;
|
||||
setViewMode?: Dispatch<SetStateAction<'info' | 'config'>>;
|
||||
}
|
||||
|
||||
export default function McpParamsForm({
|
||||
@@ -32,21 +32,21 @@ export default function McpParamsForm({
|
||||
showBackButton,
|
||||
setViewMode,
|
||||
}: McpParamsFormProps) {
|
||||
const { lng } = useI18n()
|
||||
const { t } = useTranslationClientSide(lng, 'common')
|
||||
const { message } = useMessage()
|
||||
const queryClient = useQueryClient()
|
||||
const { lng } = useI18n();
|
||||
const { t } = useTranslationClientSide(lng, 'common');
|
||||
const { message } = useMessage();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// 初始化表单数据
|
||||
const [formData, setFormData] = useState<Record<string, string>>(() => {
|
||||
const initialData: Record<string, string> = {}
|
||||
const initialData: Record<string, string> = {};
|
||||
Object.keys(mcpDetail.reusing || {}).forEach((key) => {
|
||||
initialData[key] = mcpDetail.params?.[key] || ''
|
||||
})
|
||||
return initialData
|
||||
})
|
||||
initialData[key] = mcpDetail.params?.[key] || '';
|
||||
});
|
||||
return initialData;
|
||||
});
|
||||
|
||||
const [errors, setErrors] = useState<Record<string, string>>({})
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
|
||||
const updateParamsMutation = useMutation(
|
||||
(params: Record<string, string>) => updateMcpParams(mcpDetail.id, params),
|
||||
@@ -56,9 +56,9 @@ export default function McpParamsForm({
|
||||
status: 'success',
|
||||
title: t('mcpHub.paramsSaved'),
|
||||
duration: 2000,
|
||||
})
|
||||
queryClient.invalidateQueries([QueryKey.mcpDetail, mcpDetail.id])
|
||||
setViewMode?.('info')
|
||||
});
|
||||
queryClient.invalidateQueries([QueryKey.mcpDetail, mcpDetail.id]);
|
||||
setViewMode?.('info');
|
||||
},
|
||||
onError: (error: any) => {
|
||||
message({
|
||||
@@ -66,49 +66,49 @@ export default function McpParamsForm({
|
||||
title: t('mcpHub.paramsSaveFailed'),
|
||||
description: error?.message || t('mcpHub.paramsSaveFailed'),
|
||||
duration: 3000,
|
||||
})
|
||||
});
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
const handleInputChange = (key: string, value: string) => {
|
||||
setFormData((prev) => ({ ...prev, [key]: value }))
|
||||
setFormData((prev) => ({ ...prev, [key]: value }));
|
||||
// 清除错误
|
||||
if (errors[key]) {
|
||||
setErrors((prev) => ({ ...prev, [key]: '' }))
|
||||
setErrors((prev) => ({ ...prev, [key]: '' }));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const validateForm = () => {
|
||||
const newErrors: Record<string, string> = {}
|
||||
const newErrors: Record<string, string> = {};
|
||||
|
||||
Object.entries(mcpDetail.reusing || {}).forEach(([key, config]) => {
|
||||
if (config.required && !formData[key]?.trim()) {
|
||||
newErrors[key] = t('mcpHub.required')
|
||||
newErrors[key] = t('mcpHub.required');
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
setErrors(newErrors)
|
||||
return Object.keys(newErrors).length === 0
|
||||
}
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!validateForm()) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
// 只提交有值的参数
|
||||
const paramsToSubmit: Record<string, string> = {}
|
||||
const paramsToSubmit: Record<string, string> = {};
|
||||
Object.entries(formData).forEach(([key, value]) => {
|
||||
if (value.trim()) {
|
||||
paramsToSubmit[key] = value.trim()
|
||||
paramsToSubmit[key] = value.trim();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
updateParamsMutation.mutate(paramsToSubmit)
|
||||
}
|
||||
updateParamsMutation.mutate(paramsToSubmit);
|
||||
};
|
||||
|
||||
const reusingParams = mcpDetail.reusing || {}
|
||||
const reusingParams = mcpDetail.reusing || {};
|
||||
|
||||
return (
|
||||
<Flex flex="1" direction="column" h="full" justifyContent="space-between">
|
||||
@@ -174,5 +174,5 @@ export default function McpParamsForm({
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
'use client'
|
||||
import Markdown from 'react-markdown'
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
|
||||
import { oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism'
|
||||
import { Box, Text } from '@chakra-ui/react'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
'use client';
|
||||
import Markdown from 'react-markdown';
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||
import { Box, Text } from '@chakra-ui/react';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
|
||||
import { useI18n } from '@/providers/i18n/i18nContext'
|
||||
import { McpDetail } from '@/types/mcp'
|
||||
import { useI18n } from '@/providers/i18n/i18nContext';
|
||||
import { McpDetail } from '@/types/mcp';
|
||||
|
||||
export interface McpReadmeProps {
|
||||
mcpDetail: McpDetail
|
||||
mcpDetail: McpDetail;
|
||||
}
|
||||
|
||||
export default function McpReadme({ mcpDetail }: McpReadmeProps) {
|
||||
const { lng } = useI18n()
|
||||
const { lng } = useI18n();
|
||||
|
||||
const readmeContent = lng === 'zh' ? mcpDetail.readme_cn || mcpDetail.readme : mcpDetail.readme
|
||||
const readmeContent = lng === 'zh' ? mcpDetail.readme_cn || mcpDetail.readme : mcpDetail.readme;
|
||||
|
||||
return (
|
||||
<Box h="full" overflow="hidden" w="100%" maxW="100%" position="relative">
|
||||
@@ -276,8 +276,8 @@ export default function McpReadme({ mcpDetail }: McpReadmeProps) {
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
code(props) {
|
||||
const { children, className, node, inline, ...rest } = props
|
||||
const match = /language-(\w+)/.exec(className || '')
|
||||
const { children, className, node, inline, ...rest } = props;
|
||||
const match = /language-(\w+)/.exec(className || '');
|
||||
return !inline && match ? (
|
||||
<Box
|
||||
className="code-block-wrapper"
|
||||
@@ -329,7 +329,7 @@ export default function McpReadme({ mcpDetail }: McpReadmeProps) {
|
||||
<code {...rest} className={className}>
|
||||
{children}
|
||||
</code>
|
||||
)
|
||||
);
|
||||
},
|
||||
}}
|
||||
>
|
||||
@@ -369,5 +369,5 @@ export default function McpReadme({ mcpDetail }: McpReadmeProps) {
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
'use client'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Button, Flex, HStack, Input, InputGroup, InputLeftElement, Text } from '@chakra-ui/react'
|
||||
'use client';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Button, Flex, HStack, Input, InputGroup, InputLeftElement, Text } from '@chakra-ui/react';
|
||||
|
||||
import { useTranslationClientSide } from '@/app/i18n/client'
|
||||
import { useI18n } from '@/providers/i18n/i18nContext'
|
||||
import { useTranslationClientSide } from '@/app/i18n/client';
|
||||
import { useI18n } from '@/providers/i18n/i18nContext';
|
||||
|
||||
export interface SearchFilterProps {
|
||||
searchTerm: string
|
||||
serviceType: 'hosted' | 'local' | ''
|
||||
onSearchChange: (value: string) => void
|
||||
onServiceTypeChange: (value: string) => void
|
||||
searchTerm: string;
|
||||
serviceType: 'hosted' | 'local' | '';
|
||||
onSearchChange: (value: string) => void;
|
||||
onServiceTypeChange: (value: string) => void;
|
||||
}
|
||||
|
||||
export default function SearchFilter({
|
||||
@@ -18,58 +18,58 @@ export default function SearchFilter({
|
||||
onSearchChange,
|
||||
onServiceTypeChange,
|
||||
}: SearchFilterProps) {
|
||||
const { lng } = useI18n()
|
||||
const { t } = useTranslationClientSide(lng, 'common')
|
||||
const { lng } = useI18n();
|
||||
const { t } = useTranslationClientSide(lng, 'common');
|
||||
|
||||
// 本地输入状态
|
||||
const [inputValue, setInputValue] = useState(searchTerm)
|
||||
const [inputValue, setInputValue] = useState(searchTerm);
|
||||
|
||||
// 本地多选状态管理 - 完全独立的视觉状态
|
||||
const [selectedTypes, setSelectedTypes] = useState<Set<'hosted' | 'local'>>(() => {
|
||||
return new Set<'hosted' | 'local'>() // 默认都不选
|
||||
})
|
||||
return new Set<'hosted' | 'local'>(); // 默认都不选
|
||||
});
|
||||
|
||||
// 当外部searchTerm变化时,同步到本地状态
|
||||
useEffect(() => {
|
||||
setInputValue(searchTerm)
|
||||
}, [searchTerm])
|
||||
setInputValue(searchTerm);
|
||||
}, [searchTerm]);
|
||||
|
||||
const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
onSearchChange(inputValue)
|
||||
onSearchChange(inputValue);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputBlur = () => {
|
||||
// 当失去焦点时也触发搜索,提供更好的用户体验
|
||||
if (inputValue !== searchTerm) {
|
||||
onSearchChange(inputValue)
|
||||
onSearchChange(inputValue);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleServiceTypeToggle = (type: 'hosted' | 'local') => {
|
||||
const newSelectedTypes = new Set(selectedTypes)
|
||||
const newSelectedTypes = new Set(selectedTypes);
|
||||
|
||||
if (newSelectedTypes.has(type)) {
|
||||
// 取消选择该类型
|
||||
newSelectedTypes.delete(type)
|
||||
newSelectedTypes.delete(type);
|
||||
} else {
|
||||
// 选择该类型
|
||||
newSelectedTypes.add(type)
|
||||
newSelectedTypes.add(type);
|
||||
}
|
||||
|
||||
setSelectedTypes(newSelectedTypes)
|
||||
setSelectedTypes(newSelectedTypes);
|
||||
|
||||
// 根据选择状态决定传递给父组件的值
|
||||
if (newSelectedTypes.size === 0 || newSelectedTypes.size === 2) {
|
||||
// 没有选择任何类型或选择了所有类型,都表示显示全部
|
||||
onServiceTypeChange('')
|
||||
onServiceTypeChange('');
|
||||
} else if (newSelectedTypes.has('hosted')) {
|
||||
onServiceTypeChange('hosted')
|
||||
onServiceTypeChange('hosted');
|
||||
} else if (newSelectedTypes.has('local')) {
|
||||
onServiceTypeChange('local')
|
||||
onServiceTypeChange('local');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex gap="24px" flexWrap="wrap" alignItems="center">
|
||||
@@ -212,5 +212,5 @@ export default function SearchFilter({
|
||||
</HStack>
|
||||
</HStack>
|
||||
</Flex>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
'use client'
|
||||
import { useState } from 'react'
|
||||
import { Box, Flex, Text } from '@chakra-ui/react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
'use client';
|
||||
import { useState } from 'react';
|
||||
import { Box, Flex, Text } from '@chakra-ui/react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { getMcpList } from '@/api/platform'
|
||||
import { useTranslationClientSide } from '@/app/i18n/client'
|
||||
import SwitchPage from '@/components/common/SwitchPage'
|
||||
import { useI18n } from '@/providers/i18n/i18nContext'
|
||||
import { QueryKey } from '@/types/query-key'
|
||||
import { getMcpList } from '@/api/platform';
|
||||
import { useTranslationClientSide } from '@/app/i18n/client';
|
||||
import SwitchPage from '@/components/common/SwitchPage';
|
||||
import { useI18n } from '@/providers/i18n/i18nContext';
|
||||
import { QueryKey } from '@/types/query-key';
|
||||
|
||||
import McpList from './components/McpList'
|
||||
import SearchFilter from './components/SearchFilter'
|
||||
import McpList from './components/McpList';
|
||||
import SearchFilter from './components/SearchFilter';
|
||||
|
||||
export default function McpHubPage() {
|
||||
const { lng } = useI18n()
|
||||
const { t } = useTranslationClientSide(lng, 'common')
|
||||
const { lng } = useI18n();
|
||||
const { t } = useTranslationClientSide(lng, 'common');
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const [serviceType, setServiceType] = useState<'hosted' | 'local' | ''>('')
|
||||
const [page, setPage] = useState(1)
|
||||
const [pageSize] = useState(28) // 固定每页28个
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [serviceType, setServiceType] = useState<'hosted' | 'local' | ''>('');
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize] = useState(28); // 固定每页28个
|
||||
|
||||
const { data: mcpListData, isLoading } = useQuery({
|
||||
queryKey: [
|
||||
@@ -33,11 +33,11 @@ export default function McpHubPage() {
|
||||
type: serviceType,
|
||||
keyword: searchTerm,
|
||||
}),
|
||||
})
|
||||
});
|
||||
|
||||
const mcps = mcpListData?.mcps || []
|
||||
const total = mcpListData?.total || 0
|
||||
const totalPages = Math.ceil(total / pageSize)
|
||||
const mcps = mcpListData?.mcps || [];
|
||||
const total = mcpListData?.total || 0;
|
||||
const totalPages = Math.ceil(total / pageSize);
|
||||
|
||||
return (
|
||||
<Box w="full" h="full" display="inline-flex" pt="4px" pb="12px" pr="12px" alignItems="center">
|
||||
@@ -62,12 +62,12 @@ export default function McpHubPage() {
|
||||
searchTerm={searchTerm}
|
||||
serviceType={serviceType}
|
||||
onSearchChange={(value) => {
|
||||
setSearchTerm(value)
|
||||
setPage(1) // 搜索时重置到第一页
|
||||
setSearchTerm(value);
|
||||
setPage(1); // 搜索时重置到第一页
|
||||
}}
|
||||
onServiceTypeChange={(type) => {
|
||||
setServiceType(type as 'hosted' | 'local' | '')
|
||||
setPage(1) // 筛选类型时重置到第一页
|
||||
setServiceType(type as 'hosted' | 'local' | '');
|
||||
setPage(1); // 筛选类型时重置到第一页
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -105,5 +105,5 @@ export default function McpHubPage() {
|
||||
</Flex>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import React from 'react'
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
@@ -11,38 +11,38 @@ import {
|
||||
DrawerOverlay,
|
||||
Flex,
|
||||
Text,
|
||||
} from '@chakra-ui/react'
|
||||
import { useMessage } from '@sealos/ui'
|
||||
} from '@chakra-ui/react';
|
||||
import { useMessage } from '@sealos/ui';
|
||||
|
||||
import { useTranslationClientSide } from '@/app/i18n/client'
|
||||
import { useI18n } from '@/providers/i18n/i18nContext'
|
||||
import { useBackendStore } from '@/store/backend'
|
||||
import { ModelConfig } from '@/types/models/model'
|
||||
import { useTranslationClientSide } from '@/app/i18n/client';
|
||||
import { useI18n } from '@/providers/i18n/i18nContext';
|
||||
import { useBackendStore } from '@/store/backend';
|
||||
import { ModelConfig } from '@/types/models/model';
|
||||
|
||||
import CodeBlock from './CodeHight'
|
||||
import { ModelComponent } from './Model'
|
||||
import CodeBlock from './CodeHight';
|
||||
import { ModelComponent } from './Model';
|
||||
|
||||
interface ApiDocContent {
|
||||
title: string
|
||||
endpoint: string
|
||||
method: string
|
||||
requestExample: string
|
||||
responseExample: string
|
||||
responseFormat: string
|
||||
title: string;
|
||||
endpoint: string;
|
||||
method: string;
|
||||
requestExample: string;
|
||||
responseExample: string;
|
||||
responseFormat: string;
|
||||
requestAdditionalInfo?: {
|
||||
voices?: string[]
|
||||
formats?: string[]
|
||||
}
|
||||
voices?: string[];
|
||||
formats?: string[];
|
||||
};
|
||||
responseAdditionalInfo?: {
|
||||
voices?: string[]
|
||||
formats?: string[]
|
||||
}
|
||||
voices?: string[];
|
||||
formats?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
interface ApiDocDrawerProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
modelConfig: ModelConfig
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
modelConfig: ModelConfig;
|
||||
}
|
||||
|
||||
const getApiDocContent = (
|
||||
@@ -93,7 +93,7 @@ const getApiDocContent = (
|
||||
"total_tokens": 70
|
||||
}
|
||||
}`,
|
||||
}
|
||||
};
|
||||
case 3:
|
||||
return {
|
||||
title: t('modeType.3'),
|
||||
@@ -129,7 +129,7 @@ const getApiDocContent = (
|
||||
"total_tokens": 4
|
||||
}
|
||||
}`,
|
||||
}
|
||||
};
|
||||
case 5:
|
||||
return {
|
||||
title: t('modeType.5'),
|
||||
@@ -162,7 +162,7 @@ const getApiDocContent = (
|
||||
}
|
||||
}
|
||||
}`,
|
||||
}
|
||||
};
|
||||
case 7:
|
||||
return {
|
||||
title: t('modeType.7'),
|
||||
@@ -192,7 +192,7 @@ ${
|
||||
voices: modelConfig?.config?.support_voices,
|
||||
formats: modelConfig?.config?.support_formats,
|
||||
},
|
||||
}
|
||||
};
|
||||
case 8:
|
||||
return {
|
||||
title: t('modeType.8'),
|
||||
@@ -208,7 +208,7 @@ ${
|
||||
responseExample: `{
|
||||
"text": "<string>"
|
||||
}`,
|
||||
}
|
||||
};
|
||||
case 10:
|
||||
return {
|
||||
title: t('modeType.10'),
|
||||
@@ -258,7 +258,7 @@ ${
|
||||
}
|
||||
}
|
||||
}`,
|
||||
}
|
||||
};
|
||||
case 11:
|
||||
return {
|
||||
title: t('modeType.11'),
|
||||
@@ -275,7 +275,7 @@ ${
|
||||
"pages": 1,
|
||||
"markdown": "sf ad fda daf da \\\\( f \\\\) ds f sd fs d afdas fsd asfad f\\n\\n\\n\\n\\n\\n| sadsa | | |\\n| --- | --- | --- |\\n| | sadasdsa | sad |\\n| | | dsadsadsa |\\n| | | |\\n\\n\\n\\na fda"
|
||||
}`,
|
||||
}
|
||||
};
|
||||
default:
|
||||
return {
|
||||
title: t('modeType.0'),
|
||||
@@ -284,16 +284,16 @@ ${
|
||||
responseFormat: 'json',
|
||||
requestExample: 'unknown',
|
||||
responseExample: 'unknown',
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const ApiDocDrawer: React.FC<ApiDocDrawerProps> = ({ isOpen, onClose, modelConfig }) => {
|
||||
const { lng } = useI18n()
|
||||
const { t } = useTranslationClientSide(lng, 'common')
|
||||
const aiproxyBackend = useBackendStore((state) => state.aiproxyBackend)
|
||||
const { docUrl } = useBackendStore()
|
||||
const apiDoc = getApiDocContent(modelConfig, aiproxyBackend, t)
|
||||
const { lng } = useI18n();
|
||||
const { t } = useTranslationClientSide(lng, 'common');
|
||||
const aiproxyBackend = useBackendStore((state) => state.aiproxyBackend);
|
||||
const { docUrl } = useBackendStore();
|
||||
const apiDoc = getApiDocContent(modelConfig, aiproxyBackend, t);
|
||||
|
||||
const { message } = useMessage({
|
||||
warningBoxBg: '#FFFAEB',
|
||||
@@ -302,7 +302,7 @@ const ApiDocDrawer: React.FC<ApiDocDrawerProps> = ({ isOpen, onClose, modelConfi
|
||||
successBoxBg: '#EDFBF3',
|
||||
successIconBg: '#039855',
|
||||
successIconFill: 'white',
|
||||
})
|
||||
});
|
||||
|
||||
return (
|
||||
<Drawer isOpen={isOpen} onClose={onClose} placement="right" size="full">
|
||||
@@ -363,7 +363,7 @@ const ApiDocDrawer: React.FC<ApiDocDrawerProps> = ({ isOpen, onClose, modelConfi
|
||||
<ModelComponent modelConfig={modelConfig} displayType={true} />
|
||||
<Button
|
||||
onClick={() => {
|
||||
window.open(docUrl, '_blank')
|
||||
window.open(docUrl, '_blank');
|
||||
}}
|
||||
variant="unstyled"
|
||||
position="absolute"
|
||||
@@ -525,7 +525,7 @@ const ApiDocDrawer: React.FC<ApiDocDrawerProps> = ({ isOpen, onClose, modelConfi
|
||||
isClosable: true,
|
||||
duration: 2000,
|
||||
position: 'top',
|
||||
})
|
||||
});
|
||||
},
|
||||
(err) => {
|
||||
message({
|
||||
@@ -534,9 +534,9 @@ const ApiDocDrawer: React.FC<ApiDocDrawerProps> = ({ isOpen, onClose, modelConfi
|
||||
description: err?.message || t('copyFailed'),
|
||||
isClosable: true,
|
||||
position: 'top',
|
||||
})
|
||||
});
|
||||
}
|
||||
)
|
||||
);
|
||||
}}
|
||||
variant="unstyled"
|
||||
display="inline-flex"
|
||||
@@ -876,7 +876,7 @@ const ApiDocDrawer: React.FC<ApiDocDrawerProps> = ({ isOpen, onClose, modelConfi
|
||||
isClosable: true,
|
||||
duration: 2000,
|
||||
position: 'top',
|
||||
})
|
||||
});
|
||||
},
|
||||
(err) => {
|
||||
message({
|
||||
@@ -885,9 +885,9 @@ const ApiDocDrawer: React.FC<ApiDocDrawerProps> = ({ isOpen, onClose, modelConfi
|
||||
description: err?.message || t('copyFailed'),
|
||||
isClosable: true,
|
||||
position: 'top',
|
||||
})
|
||||
});
|
||||
}
|
||||
)
|
||||
);
|
||||
}}
|
||||
variant="unstyled"
|
||||
display="inline-flex"
|
||||
@@ -931,7 +931,7 @@ const ApiDocDrawer: React.FC<ApiDocDrawerProps> = ({ isOpen, onClose, modelConfi
|
||||
</DrawerBody>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default ApiDocDrawer
|
||||
export default ApiDocDrawer;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
|
||||
import { atomDark } from 'react-syntax-highlighter/dist/esm/styles/prism'
|
||||
import { Box } from '@chakra-ui/react'
|
||||
import React from 'react';
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { atomDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
|
||||
const CodeBlock = ({ code, language = 'bash' }: { code: string; language?: string }) => {
|
||||
const customizedStyle = {
|
||||
@@ -12,7 +12,7 @@ const CodeBlock = ({ code, language = 'bash' }: { code: string; language?: strin
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -63,7 +63,7 @@ const CodeBlock = ({ code, language = 'bash' }: { code: string; language?: strin
|
||||
{code}
|
||||
</SyntaxHighlighter>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default CodeBlock
|
||||
export default CodeBlock;
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
'use client'
|
||||
import { Badge, Flex, Text } from '@chakra-ui/react'
|
||||
import { useMessage } from '@sealos/ui'
|
||||
import Image, { StaticImageData } from 'next/image'
|
||||
'use client';
|
||||
import { Badge, Flex, Text } from '@chakra-ui/react';
|
||||
import { useMessage } from '@sealos/ui';
|
||||
import Image, { StaticImageData } from 'next/image';
|
||||
|
||||
import { useTranslationClientSide } from '@/app/i18n/client'
|
||||
import { MyTooltip } from '@/components/common/MyTooltip'
|
||||
import { useI18n } from '@/providers/i18n/i18nContext'
|
||||
import { ModelConfig } from '@/types/models/model'
|
||||
import { modelIcons } from '@/ui/icons/mode-icons'
|
||||
import { getTranslationWithFallback } from '@/utils/common'
|
||||
import { useTranslationClientSide } from '@/app/i18n/client';
|
||||
import { MyTooltip } from '@/components/common/MyTooltip';
|
||||
import { useI18n } from '@/providers/i18n/i18nContext';
|
||||
import { ModelConfig } from '@/types/models/model';
|
||||
import { modelIcons } from '@/ui/icons/mode-icons';
|
||||
import { getTranslationWithFallback } from '@/utils/common';
|
||||
|
||||
export const getModelIcon = (modelOwner: string): StaticImageData => {
|
||||
const icon = modelIcons[modelOwner as keyof typeof modelIcons] || modelIcons['default']
|
||||
return icon
|
||||
}
|
||||
const icon = modelIcons[modelOwner as keyof typeof modelIcons] || modelIcons['default'];
|
||||
return icon;
|
||||
};
|
||||
|
||||
// 在组件外部定义样式配置
|
||||
const MODEL_TYPE_STYLES = {
|
||||
@@ -61,22 +61,22 @@ const MODEL_TYPE_STYLES = {
|
||||
background: '#F4F4F7',
|
||||
color: '#383F50',
|
||||
},
|
||||
} as const
|
||||
} as const;
|
||||
|
||||
// 在组件中使用
|
||||
export const getTypeStyle = (type: number) => {
|
||||
return MODEL_TYPE_STYLES[type as keyof typeof MODEL_TYPE_STYLES] || MODEL_TYPE_STYLES.default
|
||||
}
|
||||
return MODEL_TYPE_STYLES[type as keyof typeof MODEL_TYPE_STYLES] || MODEL_TYPE_STYLES.default;
|
||||
};
|
||||
|
||||
export const ModelComponent = ({
|
||||
modelConfig,
|
||||
displayType = false,
|
||||
}: {
|
||||
modelConfig: ModelConfig
|
||||
displayType?: boolean
|
||||
modelConfig: ModelConfig;
|
||||
displayType?: boolean;
|
||||
}) => {
|
||||
const { lng } = useI18n()
|
||||
const { t } = useTranslationClientSide(lng, 'common')
|
||||
const { lng } = useI18n();
|
||||
const { t } = useTranslationClientSide(lng, 'common');
|
||||
const { message } = useMessage({
|
||||
warningBoxBg: '#FFFAEB',
|
||||
warningIconBg: '#F79009',
|
||||
@@ -84,9 +84,9 @@ export const ModelComponent = ({
|
||||
successBoxBg: '#EDFBF3',
|
||||
successIconBg: '#039855',
|
||||
successIconFill: 'white',
|
||||
})
|
||||
});
|
||||
|
||||
const iconSrc = getModelIcon(modelConfig.owner)
|
||||
const iconSrc = getModelIcon(modelConfig.owner);
|
||||
|
||||
return (
|
||||
<Flex alignItems="center" justifyContent="flex-start" gap="8px" h="42px">
|
||||
@@ -120,7 +120,7 @@ export const ModelComponent = ({
|
||||
isClosable: true,
|
||||
duration: 2000,
|
||||
position: 'top',
|
||||
})
|
||||
});
|
||||
},
|
||||
(err) => {
|
||||
message({
|
||||
@@ -129,7 +129,7 @@ export const ModelComponent = ({
|
||||
description: err?.message || t('copyFailed'),
|
||||
isClosable: true,
|
||||
position: 'top',
|
||||
})
|
||||
});
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -343,5 +343,5 @@ export const ModelComponent = ({
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import { useMemo, useState } from 'react'
|
||||
'use client';
|
||||
import { useMemo, useState } from 'react';
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
@@ -16,48 +16,48 @@ import {
|
||||
Thead,
|
||||
Tr,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react'
|
||||
import { CurrencySymbol } from '@sealos/ui'
|
||||
} from '@chakra-ui/react';
|
||||
import { CurrencySymbol } from '@sealos/ui';
|
||||
import {
|
||||
createColumnHelper,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
} from '@tanstack/react-table'
|
||||
} from '@tanstack/react-table';
|
||||
|
||||
import { useTranslationClientSide } from '@/app/i18n/client'
|
||||
import { MyTooltip } from '@/components/common/MyTooltip'
|
||||
import { useI18n } from '@/providers/i18n/i18nContext'
|
||||
import { useBackendStore } from '@/store/backend'
|
||||
import { ModelConfig } from '@/types/models/model'
|
||||
import { getTranslationWithFallback } from '@/utils/common'
|
||||
import { useTranslationClientSide } from '@/app/i18n/client';
|
||||
import { MyTooltip } from '@/components/common/MyTooltip';
|
||||
import { useI18n } from '@/providers/i18n/i18nContext';
|
||||
import { useBackendStore } from '@/store/backend';
|
||||
import { ModelConfig } from '@/types/models/model';
|
||||
import { getTranslationWithFallback } from '@/utils/common';
|
||||
|
||||
import ApiDocDrawer from './ApiDoc'
|
||||
import { ModelComponent } from './Model'
|
||||
import { getTypeStyle } from './Model'
|
||||
import ApiDocDrawer from './ApiDoc';
|
||||
import { ModelComponent } from './Model';
|
||||
import { getTypeStyle } from './Model';
|
||||
|
||||
type SortDirection = 'asc' | 'desc' | false
|
||||
type SortDirection = 'asc' | 'desc' | false;
|
||||
|
||||
export function PriceTable({
|
||||
modelConfigs,
|
||||
isLoading,
|
||||
}: {
|
||||
modelConfigs: ModelConfig[]
|
||||
isLoading: boolean
|
||||
modelConfigs: ModelConfig[];
|
||||
isLoading: boolean;
|
||||
}) {
|
||||
const { lng } = useI18n()
|
||||
const { t } = useTranslationClientSide(lng, 'common')
|
||||
const [selectedModel, setSelectedModel] = useState<ModelConfig | null>(null)
|
||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||
const { lng } = useI18n();
|
||||
const { t } = useTranslationClientSide(lng, 'common');
|
||||
const [selectedModel, setSelectedModel] = useState<ModelConfig | null>(null);
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
|
||||
const handleOpenApiDoc = (modelConfig: ModelConfig) => {
|
||||
setSelectedModel(modelConfig)
|
||||
onOpen()
|
||||
}
|
||||
setSelectedModel(modelConfig);
|
||||
onOpen();
|
||||
};
|
||||
|
||||
const { currencySymbol } = useBackendStore()
|
||||
const { currencySymbol } = useBackendStore();
|
||||
|
||||
const columnHelper = createColumnHelper<ModelConfig>()
|
||||
const columnHelper = createColumnHelper<ModelConfig>();
|
||||
const columns = [
|
||||
columnHelper.accessor((row) => row.model, {
|
||||
id: 'model',
|
||||
@@ -217,7 +217,7 @@ export function PriceTable({
|
||||
letterSpacing="0.5px"
|
||||
/>
|
||||
</Flex>
|
||||
)
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Flex direction="column" gap="8px">
|
||||
@@ -363,7 +363,7 @@ export function PriceTable({
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
)
|
||||
);
|
||||
},
|
||||
}),
|
||||
columnHelper.display({
|
||||
@@ -380,7 +380,7 @@ export function PriceTable({
|
||||
</Text>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const modelConfig = row.original
|
||||
const modelConfig = row.original;
|
||||
return (
|
||||
<Button
|
||||
onClick={() => handleOpenApiDoc(modelConfig)}
|
||||
@@ -437,21 +437,21 @@ export function PriceTable({
|
||||
{t('logs.detail')}
|
||||
</Text>
|
||||
</Button>
|
||||
)
|
||||
);
|
||||
},
|
||||
id: 'detail',
|
||||
}),
|
||||
]
|
||||
];
|
||||
|
||||
const tableData = useMemo(() => {
|
||||
return modelConfigs
|
||||
}, [modelConfigs])
|
||||
return modelConfigs;
|
||||
}, [modelConfigs]);
|
||||
|
||||
const table = useReactTable({
|
||||
data: tableData,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
})
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -514,5 +514,5 @@ export function PriceTable({
|
||||
<ApiDocDrawer isOpen={isOpen} onClose={onClose} modelConfig={selectedModel} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import { useMemo, useState } from 'react'
|
||||
'use client';
|
||||
import { useMemo, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
@@ -9,36 +9,36 @@ import {
|
||||
InputGroup,
|
||||
InputRightElement,
|
||||
Text,
|
||||
} from '@chakra-ui/react'
|
||||
import { useQuery, UseQueryResult } from '@tanstack/react-query'
|
||||
import Image from 'next/image'
|
||||
} from '@chakra-ui/react';
|
||||
import { useQuery, UseQueryResult } from '@tanstack/react-query';
|
||||
import Image from 'next/image';
|
||||
|
||||
import { getEnabledMode } from '@/api/platform'
|
||||
import { useTranslationClientSide } from '@/app/i18n/client'
|
||||
import { SingleSelectComboboxUnstyle } from '@/components/common/SingleSelectComboboxUnStyle'
|
||||
import { useDebounce } from '@/hooks/useDebounce'
|
||||
import { useI18n } from '@/providers/i18n/i18nContext'
|
||||
import { ModelConfig } from '@/types/models/model'
|
||||
import { QueryKey } from '@/types/query-key'
|
||||
import { getTranslationWithFallback } from '@/utils/common'
|
||||
import { getEnabledMode } from '@/api/platform';
|
||||
import { useTranslationClientSide } from '@/app/i18n/client';
|
||||
import { SingleSelectComboboxUnstyle } from '@/components/common/SingleSelectComboboxUnStyle';
|
||||
import { useDebounce } from '@/hooks/useDebounce';
|
||||
import { useI18n } from '@/providers/i18n/i18nContext';
|
||||
import { ModelConfig } from '@/types/models/model';
|
||||
import { QueryKey } from '@/types/query-key';
|
||||
import { getTranslationWithFallback } from '@/utils/common';
|
||||
|
||||
import { getModelIcon } from './component/Model'
|
||||
import { PriceTable } from './component/PriceTable'
|
||||
import { getModelIcon } from './component/Model';
|
||||
import { PriceTable } from './component/PriceTable';
|
||||
|
||||
export default function Price() {
|
||||
const { lng } = useI18n()
|
||||
const { t } = useTranslationClientSide(lng, 'common')
|
||||
const { lng } = useI18n();
|
||||
const { t } = useTranslationClientSide(lng, 'common');
|
||||
|
||||
const [modelOwner, setModelOwner] = useState<string>('')
|
||||
const [modelType, setModelType] = useState<string>('')
|
||||
const [modelOwner, setModelOwner] = useState<string>('');
|
||||
const [modelType, setModelType] = useState<string>('');
|
||||
|
||||
const [searchInput, setSearchInput] = useState('')
|
||||
const debouncedSearch = useDebounce(searchInput, 500)
|
||||
const [searchInput, setSearchInput] = useState('');
|
||||
const debouncedSearch = useDebounce(searchInput, 500);
|
||||
|
||||
interface FilterParams {
|
||||
owner: string
|
||||
type: string
|
||||
name: string
|
||||
owner: string;
|
||||
type: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const filterModels = (modelConfigs: ModelConfig[], filterParams: FilterParams): ModelConfig[] => {
|
||||
@@ -50,34 +50,34 @@ export default function Price() {
|
||||
`modeOwner.${String(config.owner)}`,
|
||||
'modeOwner.unknown',
|
||||
t as any
|
||||
) === filterParams.owner
|
||||
) === filterParams.owner;
|
||||
|
||||
const typeMatch =
|
||||
!filterParams.type ||
|
||||
filterParams.type === t('price.all') ||
|
||||
getTranslationWithFallback(`modeType.${String(config.type)}`, 'modeType.0', t as any) ===
|
||||
filterParams.type
|
||||
filterParams.type;
|
||||
|
||||
const nameMatch =
|
||||
!filterParams.name || config.model.toLowerCase().includes(filterParams.name.toLowerCase())
|
||||
!filterParams.name || config.model.toLowerCase().includes(filterParams.name.toLowerCase());
|
||||
|
||||
return ownerMatch && typeMatch && nameMatch
|
||||
})
|
||||
}
|
||||
return ownerMatch && typeMatch && nameMatch;
|
||||
});
|
||||
};
|
||||
|
||||
const {
|
||||
isLoading,
|
||||
data: modelConfigs = [] as ModelConfig[],
|
||||
refetch,
|
||||
}: UseQueryResult<ModelConfig[]> = useQuery([QueryKey.GetEnabledModels], () => getEnabledMode())
|
||||
}: UseQueryResult<ModelConfig[]> = useQuery([QueryKey.GetEnabledModels], () => getEnabledMode());
|
||||
|
||||
const filteredModelConfigs = useMemo(() => {
|
||||
return filterModels(modelConfigs, {
|
||||
owner: modelOwner,
|
||||
type: modelType,
|
||||
name: debouncedSearch,
|
||||
})
|
||||
}, [modelConfigs, modelOwner, modelType, debouncedSearch])
|
||||
});
|
||||
}, [modelConfigs, modelOwner, modelType, debouncedSearch]);
|
||||
|
||||
return (
|
||||
<Box w="full" h="full" display="inline-flex" pt="4px" pb="12px" pr="12px" alignItems="center">
|
||||
@@ -129,7 +129,7 @@ export default function Price() {
|
||||
bg="white"
|
||||
boxShadow="0px 1px 2px 0px rgba(19, 51, 107, 0.05), 0px 0px 1px 0px rgba(19, 51, 107, 0.08)"
|
||||
onClick={() => {
|
||||
refetch()
|
||||
refetch();
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
@@ -189,16 +189,16 @@ export default function Price() {
|
||||
),
|
||||
]}
|
||||
setSelectedItem={(modelOwner) => {
|
||||
setModelOwner(modelOwner.name)
|
||||
setModelOwner(modelOwner.name);
|
||||
}}
|
||||
handleDropdownItemFilter={(dropdownItems, inputValue) => {
|
||||
const lowerCasedInput = inputValue.toLowerCase()
|
||||
const lowerCasedInput = inputValue.toLowerCase();
|
||||
return dropdownItems.filter(
|
||||
(item) => !inputValue || item.name.toLowerCase().includes(lowerCasedInput)
|
||||
)
|
||||
);
|
||||
}}
|
||||
handleDropdownItemDisplay={(dropdownItem) => {
|
||||
const iconSrc = getModelIcon(dropdownItem.icon)
|
||||
const iconSrc = getModelIcon(dropdownItem.icon);
|
||||
if (dropdownItem.name === t('price.all')) {
|
||||
return (
|
||||
<Flex alignItems="center" gap="8px">
|
||||
@@ -215,7 +215,7 @@ export default function Price() {
|
||||
{dropdownItem.name}
|
||||
</Text>
|
||||
</Flex>
|
||||
)
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Flex alignItems="center" gap="8px">
|
||||
@@ -232,7 +232,7 @@ export default function Price() {
|
||||
{dropdownItem.name}
|
||||
</Text>
|
||||
</Flex>
|
||||
)
|
||||
);
|
||||
}}
|
||||
flexProps={{ w: '240px' }}
|
||||
initSelectedItem={{ icon: '', name: t('price.all') }}
|
||||
@@ -266,13 +266,13 @@ export default function Price() {
|
||||
),
|
||||
]}
|
||||
setSelectedItem={(modelType) => {
|
||||
setModelType(modelType)
|
||||
setModelType(modelType);
|
||||
}}
|
||||
handleDropdownItemFilter={(dropdownItems, inputValue) => {
|
||||
const lowerCasedInput = inputValue.toLowerCase()
|
||||
const lowerCasedInput = inputValue.toLowerCase();
|
||||
return dropdownItems.filter(
|
||||
(item) => !inputValue || item.toLowerCase().includes(lowerCasedInput)
|
||||
)
|
||||
);
|
||||
}}
|
||||
handleDropdownItemDisplay={(dropdownItem) => {
|
||||
return (
|
||||
@@ -287,7 +287,7 @@ export default function Price() {
|
||||
>
|
||||
{dropdownItem}
|
||||
</Text>
|
||||
)
|
||||
);
|
||||
}}
|
||||
flexProps={{ w: '240px' }}
|
||||
initSelectedItem={t('price.all')}
|
||||
@@ -320,7 +320,7 @@ export default function Price() {
|
||||
}}
|
||||
value={searchInput}
|
||||
onChange={(e) => {
|
||||
setSearchInput(e.target.value)
|
||||
setSearchInput(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<InputRightElement w="32px" h="32px">
|
||||
@@ -364,5 +364,5 @@ export default function Price() {
|
||||
</Flex>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,52 +1,52 @@
|
||||
import { dir } from 'i18next'
|
||||
import type { Metadata } from 'next'
|
||||
import Script from 'next/script'
|
||||
import { dir } from 'i18next';
|
||||
import type { Metadata } from 'next';
|
||||
import Script from 'next/script';
|
||||
|
||||
import { useTranslationServerSide } from '@/app/i18n/server'
|
||||
import { fallbackLng, languages } from '@/app/i18n/settings'
|
||||
import InitializeApp from '@/components/InitializeApp'
|
||||
import ChakraProviders from '@/providers/chakra/providers'
|
||||
import { I18nProvider } from '@/providers/i18n/i18nContext'
|
||||
import QueryProvider from '@/providers/tanstack-query/QueryProvider'
|
||||
import { useTranslationServerSide } from '@/app/i18n/server';
|
||||
import { fallbackLng, languages } from '@/app/i18n/settings';
|
||||
import InitializeApp from '@/components/InitializeApp';
|
||||
import ChakraProviders from '@/providers/chakra/providers';
|
||||
import { I18nProvider } from '@/providers/i18n/i18nContext';
|
||||
import QueryProvider from '@/providers/tanstack-query/QueryProvider';
|
||||
|
||||
import './globals.css'
|
||||
import 'react-day-picker/dist/style.css'
|
||||
import './globals.css';
|
||||
import 'react-day-picker/dist/style.css';
|
||||
|
||||
export async function generateStaticParams(): Promise<{ lng: string }[]> {
|
||||
return languages.map((lng) => ({ lng }))
|
||||
return languages.map((lng) => ({ lng }));
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: {
|
||||
lng: string
|
||||
}
|
||||
lng: string;
|
||||
};
|
||||
}): Promise<Metadata> {
|
||||
let { lng } = await params
|
||||
if (languages.indexOf(lng) < 0) lng = fallbackLng
|
||||
let { lng } = await params;
|
||||
if (languages.indexOf(lng) < 0) lng = fallbackLng;
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const { t } = await useTranslationServerSide(lng, 'common')
|
||||
const { t } = await useTranslationServerSide(lng, 'common');
|
||||
return {
|
||||
icons: {
|
||||
icon: '/favicon.svg',
|
||||
},
|
||||
title: t('title'),
|
||||
description: t('description'),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
params,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode
|
||||
params: Promise<{ lng: string }>
|
||||
children: React.ReactNode;
|
||||
params: Promise<{ lng: string }>;
|
||||
}>): Promise<React.JSX.Element> {
|
||||
const lng = (await params).lng
|
||||
const lng = (await params).lng;
|
||||
const scripts: { [key: string]: string }[] = JSON.parse(
|
||||
process.env.NEXT_PUBLIC_CUSTOM_SCRIPTS ?? '[]'
|
||||
)
|
||||
);
|
||||
return (
|
||||
<html lang={lng} dir={dir(lng)}>
|
||||
<body>
|
||||
@@ -63,5 +63,5 @@ export default async function RootLayout({
|
||||
))}
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Metadata } from 'next'
|
||||
import { Metadata } from 'next';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'AI Proxy API Doc',
|
||||
@@ -7,12 +7,12 @@ export const metadata: Metadata = {
|
||||
index: process.env.NODE_ENV === 'development',
|
||||
follow: false,
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import ReactSwagger from '@/components/SwaggerUi'
|
||||
import { getApiDocs } from '@/lib/swagger'
|
||||
import ReactSwagger from '@/components/SwaggerUi';
|
||||
import { getApiDocs } from '@/lib/swagger';
|
||||
|
||||
async function ApiDocsPage() {
|
||||
const spec = await getApiDocs()
|
||||
const spec = await getApiDocs();
|
||||
|
||||
return (
|
||||
<section className="container">
|
||||
<ReactSwagger spec={spec} />
|
||||
</section>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default ApiDocsPage
|
||||
export default ApiDocsPage;
|
||||
|
||||
@@ -1,135 +1,135 @@
|
||||
'use client'
|
||||
'use client';
|
||||
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { EVENT_NAME } from 'sealos-desktop-sdk'
|
||||
import { createSealosApp, sealosApp } from 'sealos-desktop-sdk/app'
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { EVENT_NAME } from 'sealos-desktop-sdk';
|
||||
import { createSealosApp, sealosApp } from 'sealos-desktop-sdk/app';
|
||||
|
||||
import { initAppConfig } from '@/api/platform'
|
||||
import { useTranslationClientSide } from '@/app/i18n/client'
|
||||
import { useI18n } from '@/providers/i18n/i18nContext'
|
||||
import { useBackendStore } from '@/store/backend'
|
||||
import { useSessionStore } from '@/store/session'
|
||||
import { initAppConfig } from '@/api/platform';
|
||||
import { useTranslationClientSide } from '@/app/i18n/client';
|
||||
import { useI18n } from '@/providers/i18n/i18nContext';
|
||||
import { useBackendStore } from '@/store/backend';
|
||||
import { useSessionStore } from '@/store/session';
|
||||
|
||||
export default function InitializeApp() {
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const { lng } = useI18n()
|
||||
const { i18n } = useTranslationClientSide(lng)
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const { lng } = useI18n();
|
||||
const { i18n } = useTranslationClientSide(lng);
|
||||
const {
|
||||
setAiproxyBackend,
|
||||
setCurrencySymbol,
|
||||
setDocUrl,
|
||||
setIsInvitationActive,
|
||||
setInvitationUrl,
|
||||
} = useBackendStore()
|
||||
} = useBackendStore();
|
||||
|
||||
const handleI18nChange = useCallback(
|
||||
(data: { currentLanguage: string }) => {
|
||||
const currentLng = i18n.resolvedLanguage // get the latest resolvedLanguage
|
||||
const newLng = data.currentLanguage
|
||||
const currentLng = i18n.resolvedLanguage; // get the latest resolvedLanguage
|
||||
const newLng = data.currentLanguage;
|
||||
|
||||
if (currentLng !== newLng) {
|
||||
const currentPath = window.location.pathname
|
||||
const pathWithoutLang = currentPath.split('/').slice(2).join('/')
|
||||
router.push(`/${newLng}/${pathWithoutLang}`)
|
||||
const currentPath = window.location.pathname;
|
||||
const pathWithoutLang = currentPath.split('/').slice(2).join('/');
|
||||
router.push(`/${newLng}/${pathWithoutLang}`);
|
||||
}
|
||||
},
|
||||
[i18n.resolvedLanguage]
|
||||
)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const cleanupApp = createSealosApp()
|
||||
let cleanupEventListener: (() => void) | undefined
|
||||
const cleanupApp = createSealosApp();
|
||||
let cleanupEventListener: (() => void) | undefined;
|
||||
|
||||
const initApp = async () => {
|
||||
try {
|
||||
await initLanguage()
|
||||
await initLanguage();
|
||||
|
||||
await initSession()
|
||||
await initSession();
|
||||
|
||||
await initConfig()
|
||||
await initConfig();
|
||||
|
||||
cleanupEventListener = sealosApp?.addAppEventListen(
|
||||
EVENT_NAME.CHANGE_I18N,
|
||||
handleI18nChange
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('aiproxy: init app error:', error)
|
||||
console.error('aiproxy: init app error:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
initApp()
|
||||
initApp();
|
||||
|
||||
return () => {
|
||||
if (cleanupEventListener && typeof cleanupEventListener === 'function') {
|
||||
cleanupEventListener()
|
||||
cleanupEventListener();
|
||||
}
|
||||
if (cleanupApp && typeof cleanupApp === 'function') {
|
||||
cleanupApp()
|
||||
cleanupApp();
|
||||
}
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
}, []);
|
||||
|
||||
// init language
|
||||
const initLanguage = async () => {
|
||||
const pathLng = pathname.split('/')[1]
|
||||
const pathLng = pathname.split('/')[1];
|
||||
try {
|
||||
const lang = await sealosApp.getLanguage()
|
||||
const lang = await sealosApp.getLanguage();
|
||||
if (pathLng !== lang.lng) {
|
||||
const pathParts = pathname.split('/')
|
||||
pathParts[1] = lang.lng
|
||||
router.push(pathParts.join('/'))
|
||||
router.refresh()
|
||||
const pathParts = pathname.split('/');
|
||||
pathParts[1] = lang.lng;
|
||||
router.push(pathParts.join('/'));
|
||||
router.refresh();
|
||||
}
|
||||
console.info('aiproxy: init language success')
|
||||
console.info('aiproxy: init language success');
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.debug('aiproxy: init language error:', error.message)
|
||||
console.debug('aiproxy: init language error:', error.message);
|
||||
} else {
|
||||
console.debug('aiproxy: unknown init language error:', error)
|
||||
console.debug('aiproxy: unknown init language error:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// init session
|
||||
const initSession = async () => {
|
||||
const { setSession } = useSessionStore.getState()
|
||||
const { setSession } = useSessionStore.getState();
|
||||
|
||||
try {
|
||||
const newSession = await sealosApp.getSession()
|
||||
const currentSession = useSessionStore.getState().session
|
||||
const newSession = await sealosApp.getSession();
|
||||
const currentSession = useSessionStore.getState().session;
|
||||
// Compare token from persisted session with new session token
|
||||
if (newSession?.token !== currentSession?.token) {
|
||||
setSession(newSession)
|
||||
window.location.reload()
|
||||
setSession(newSession);
|
||||
window.location.reload();
|
||||
}
|
||||
console.info('aiproxy: init session success')
|
||||
console.info('aiproxy: init session success');
|
||||
} catch (err) {
|
||||
console.info('aiproxy: app is not running in desktop')
|
||||
console.info('aiproxy: app is not running in desktop');
|
||||
if (!process.env.NEXT_PUBLIC_MOCK_USER) {
|
||||
setSession(null)
|
||||
setSession(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// init config
|
||||
const initConfig = async () => {
|
||||
try {
|
||||
const { aiproxyBackend, currencySymbol, docUrl, isInvitationActive, invitationUrl } =
|
||||
await initAppConfig()
|
||||
setAiproxyBackend(aiproxyBackend)
|
||||
setCurrencySymbol(currencySymbol)
|
||||
setDocUrl(docUrl)
|
||||
setIsInvitationActive(isInvitationActive)
|
||||
setInvitationUrl(invitationUrl)
|
||||
console.info('aiproxy: init config success')
|
||||
await initAppConfig();
|
||||
setAiproxyBackend(aiproxyBackend);
|
||||
setCurrencySymbol(currencySymbol);
|
||||
setDocUrl(docUrl);
|
||||
setIsInvitationActive(isInvitationActive);
|
||||
setInvitationUrl(invitationUrl);
|
||||
console.info('aiproxy: init config success');
|
||||
} catch (error) {
|
||||
console.error('aiproxy: init config error:', error)
|
||||
console.error('aiproxy: init config error:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
'use client'
|
||||
'use client';
|
||||
|
||||
import { memo } from 'react'
|
||||
import SwaggerUI from 'swagger-ui-react'
|
||||
import { memo } from 'react';
|
||||
import SwaggerUI from 'swagger-ui-react';
|
||||
|
||||
import 'swagger-ui-react/swagger-ui.css'
|
||||
import 'swagger-ui-react/swagger-ui.css';
|
||||
|
||||
interface ReactSwaggerProps {
|
||||
spec: object | string
|
||||
spec: object | string;
|
||||
}
|
||||
|
||||
const ReactSwagger = memo(({ spec }: ReactSwaggerProps) => {
|
||||
@@ -50,9 +50,9 @@ const ReactSwagger = memo(({ spec }: ReactSwaggerProps) => {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
ReactSwagger.displayName = 'ReactSwagger'
|
||||
ReactSwagger.displayName = 'ReactSwagger';
|
||||
|
||||
export default ReactSwagger
|
||||
export default ReactSwagger;
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
'use client'
|
||||
import { Flex, Text } from '@chakra-ui/react'
|
||||
import Image, { StaticImageData } from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
'use client';
|
||||
import { Flex, Text } from '@chakra-ui/react';
|
||||
import Image, { StaticImageData } from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
import { useTranslationClientSide } from '@/app/i18n/client'
|
||||
import { useI18n } from '@/providers/i18n/i18nContext'
|
||||
import configIcon from '@/ui/svg/icons/admin-sidebar/config.svg'
|
||||
import configIcon_a from '@/ui/svg/icons/admin-sidebar/config_a.svg'
|
||||
import homeIcon from '@/ui/svg/icons/admin-sidebar/home.svg'
|
||||
import homeIcon_a from '@/ui/svg/icons/admin-sidebar/home_a.svg'
|
||||
import logsIcon from '@/ui/svg/icons/admin-sidebar/logs.svg'
|
||||
import logsIcon_a from '@/ui/svg/icons/admin-sidebar/logs_a.svg'
|
||||
import nsManagerIcon from '@/ui/svg/icons/admin-sidebar/nsManager.svg'
|
||||
import nsManagerIcon_a from '@/ui/svg/icons/admin-sidebar/nsManager_a.svg'
|
||||
import { useTranslationClientSide } from '@/app/i18n/client';
|
||||
import { useI18n } from '@/providers/i18n/i18nContext';
|
||||
import configIcon from '@/ui/svg/icons/admin-sidebar/config.svg';
|
||||
import configIcon_a from '@/ui/svg/icons/admin-sidebar/config_a.svg';
|
||||
import homeIcon from '@/ui/svg/icons/admin-sidebar/home.svg';
|
||||
import homeIcon_a from '@/ui/svg/icons/admin-sidebar/home_a.svg';
|
||||
import logsIcon from '@/ui/svg/icons/admin-sidebar/logs.svg';
|
||||
import logsIcon_a from '@/ui/svg/icons/admin-sidebar/logs_a.svg';
|
||||
import nsManagerIcon from '@/ui/svg/icons/admin-sidebar/nsManager.svg';
|
||||
import nsManagerIcon_a from '@/ui/svg/icons/admin-sidebar/nsManager_a.svg';
|
||||
|
||||
type Menu = {
|
||||
id: string
|
||||
url: string
|
||||
value: string
|
||||
icon: StaticImageData
|
||||
activeIcon: StaticImageData
|
||||
display: boolean
|
||||
}
|
||||
id: string;
|
||||
url: string;
|
||||
value: string;
|
||||
icon: StaticImageData;
|
||||
activeIcon: StaticImageData;
|
||||
display: boolean;
|
||||
};
|
||||
|
||||
const SideBar = (): JSX.Element => {
|
||||
const pathname = usePathname()
|
||||
const { lng } = useI18n()
|
||||
const { t } = useTranslationClientSide(lng, 'common')
|
||||
const pathname = usePathname();
|
||||
const { lng } = useI18n();
|
||||
const { t } = useTranslationClientSide(lng, 'common');
|
||||
|
||||
const menus: Menu[] = [
|
||||
{
|
||||
@@ -62,7 +62,7 @@ const SideBar = (): JSX.Element => {
|
||||
activeIcon: nsManagerIcon_a,
|
||||
display: true,
|
||||
},
|
||||
]
|
||||
];
|
||||
|
||||
return (
|
||||
<Flex
|
||||
@@ -76,8 +76,8 @@ const SideBar = (): JSX.Element => {
|
||||
{menus
|
||||
.filter((menu) => menu.display)
|
||||
.map((menu) => {
|
||||
const fullUrl = `/${lng}${menu.url}`
|
||||
const isActive = pathname === fullUrl
|
||||
const fullUrl = `/${lng}${menu.url}`;
|
||||
const isActive = pathname === fullUrl;
|
||||
|
||||
return (
|
||||
<Link href={fullUrl} key={menu.id} style={{ textDecoration: 'none' }}>
|
||||
@@ -115,10 +115,10 @@ const SideBar = (): JSX.Element => {
|
||||
</Text>
|
||||
</Flex>
|
||||
</Link>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default SideBar
|
||||
export default SideBar;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import { Box, Button, Flex, FormLabel, Input, Text, VStack } from '@chakra-ui/react'
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { Box, Button, Flex, FormLabel, Input, Text, VStack } from '@chakra-ui/react';
|
||||
|
||||
import { useTranslationClientSide } from '@/app/i18n/client'
|
||||
import { useI18n } from '@/providers/i18n/i18nContext'
|
||||
import { useTranslationClientSide } from '@/app/i18n/client';
|
||||
import { useI18n } from '@/providers/i18n/i18nContext';
|
||||
|
||||
import { CustomSelect } from './Select'
|
||||
type MapKeyValuePair = { key: string; value: string }
|
||||
import { CustomSelect } from './Select';
|
||||
type MapKeyValuePair = { key: string; value: string };
|
||||
|
||||
// mapKeys determines the available selection options
|
||||
export const ConstructMappingComponent = function ({
|
||||
@@ -13,30 +13,30 @@ export const ConstructMappingComponent = function ({
|
||||
mapData,
|
||||
setMapData,
|
||||
}: {
|
||||
mapKeys: string[]
|
||||
mapData: Record<string, string>
|
||||
setMapData: (mapping: Record<string, string>) => void
|
||||
mapKeys: string[];
|
||||
mapData: Record<string, string>;
|
||||
setMapData: (mapping: Record<string, string>) => void;
|
||||
}) {
|
||||
const { lng } = useI18n()
|
||||
const { t } = useTranslationClientSide(lng, 'common')
|
||||
const { lng } = useI18n();
|
||||
const { t } = useTranslationClientSide(lng, 'common');
|
||||
|
||||
const [mapKeyValuePairs, setMapkeyValuePairs] = useState<Array<MapKeyValuePair>>([])
|
||||
const [mapKeyValuePairs, setMapkeyValuePairs] = useState<Array<MapKeyValuePair>>([]);
|
||||
|
||||
// 组件初步渲染时,isInternalUpdate 为 false,可以保证每次重新渲染时,mapData 变化时,mapKeyValuePairs 重新初始化
|
||||
const [isInternalUpdate, setIsInternalUpdate] = useState(false)
|
||||
const [isInternalUpdate, setIsInternalUpdate] = useState(false);
|
||||
|
||||
// 初始化并在 mapData 变化时更新 mapKeyValuePairs
|
||||
useEffect(() => {
|
||||
if (!isInternalUpdate) {
|
||||
const entries = Object.entries(mapData)
|
||||
const entries = Object.entries(mapData);
|
||||
setMapkeyValuePairs(
|
||||
entries.length > 0
|
||||
? entries.map(([key, value]) => ({ key, value }))
|
||||
: [{ key: '', value: '' }]
|
||||
)
|
||||
);
|
||||
}
|
||||
setIsInternalUpdate(false)
|
||||
}, [mapData])
|
||||
setIsInternalUpdate(false);
|
||||
}, [mapData]);
|
||||
|
||||
const handleDropdownItemDisplay = (dropdownItem: string) => {
|
||||
if (dropdownItem === t('channelsFormPlaceholder.modelMappingInput')) {
|
||||
@@ -52,7 +52,7 @@ export const ConstructMappingComponent = function ({
|
||||
>
|
||||
{t('channelsFormPlaceholder.modelMappingInput')}
|
||||
</Text>
|
||||
)
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Text
|
||||
@@ -66,8 +66,8 @@ export const ConstructMappingComponent = function ({
|
||||
>
|
||||
{dropdownItem}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const handleSeletedItemDisplay = (selectedItem: string) => {
|
||||
if (selectedItem === t('channelsFormPlaceholder.modelMappingInput')) {
|
||||
@@ -83,7 +83,7 @@ export const ConstructMappingComponent = function ({
|
||||
>
|
||||
{t('channelsFormPlaceholder.modelMappingInput')}
|
||||
</Text>
|
||||
)
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Box
|
||||
@@ -108,101 +108,101 @@ export const ConstructMappingComponent = function ({
|
||||
{selectedItem}
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// Handling mapData and mapKeyValuePairs cleanup when map keys change.
|
||||
useEffect(() => {
|
||||
// 1. Handle mapData cleanup
|
||||
const removedKeysFromMapData = Object.keys(mapData).filter((key) => !mapKeys.includes(key))
|
||||
const removedKeysFromMapData = Object.keys(mapData).filter((key) => !mapKeys.includes(key));
|
||||
if (removedKeysFromMapData.length > 0) {
|
||||
const newMapData = { ...mapData }
|
||||
const newMapData = { ...mapData };
|
||||
removedKeysFromMapData.forEach((key) => {
|
||||
delete newMapData[key]
|
||||
})
|
||||
setIsInternalUpdate(true)
|
||||
setMapData(newMapData)
|
||||
delete newMapData[key];
|
||||
});
|
||||
setIsInternalUpdate(true);
|
||||
setMapData(newMapData);
|
||||
}
|
||||
|
||||
// 2. Handle mapKeyValuePairs cleanup
|
||||
const removedPairs = mapKeyValuePairs.filter((pair) => pair.key && !mapKeys.includes(pair.key))
|
||||
const removedPairs = mapKeyValuePairs.filter((pair) => pair.key && !mapKeys.includes(pair.key));
|
||||
if (removedPairs.length > 0) {
|
||||
const newMapKeyValuePairs = mapKeyValuePairs.filter(
|
||||
(pair) => !pair.key || mapKeys.includes(pair.key)
|
||||
)
|
||||
setMapkeyValuePairs(newMapKeyValuePairs)
|
||||
);
|
||||
setMapkeyValuePairs(newMapKeyValuePairs);
|
||||
}
|
||||
}, [mapKeys])
|
||||
}, [mapKeys]);
|
||||
|
||||
// Get the keys that have been selected
|
||||
const getSelectedMapKeys = (currentIndex: number) => {
|
||||
const selected = new Set<string>()
|
||||
const selected = new Set<string>();
|
||||
mapKeyValuePairs.forEach((mapKeyValuePair, idx) => {
|
||||
if (idx !== currentIndex && mapKeyValuePair.key) {
|
||||
selected.add(mapKeyValuePair.key)
|
||||
selected.add(mapKeyValuePair.key);
|
||||
}
|
||||
})
|
||||
return selected
|
||||
}
|
||||
});
|
||||
return selected;
|
||||
};
|
||||
|
||||
// Handling adding a new row
|
||||
const handleAddNewMapKeyPair = () => {
|
||||
setMapkeyValuePairs([...mapKeyValuePairs, { key: '', value: '' }])
|
||||
}
|
||||
setMapkeyValuePairs([...mapKeyValuePairs, { key: '', value: '' }]);
|
||||
};
|
||||
|
||||
// Handling deleting a row
|
||||
const handleRemoveMapKeyPair = (index: number) => {
|
||||
const mapKeyValuePair = mapKeyValuePairs[index]
|
||||
const newMapData = { ...mapData }
|
||||
const mapKeyValuePair = mapKeyValuePairs[index];
|
||||
const newMapData = { ...mapData };
|
||||
if (mapKeyValuePair.key) {
|
||||
delete newMapData[mapKeyValuePair.key]
|
||||
delete newMapData[mapKeyValuePair.key];
|
||||
}
|
||||
setIsInternalUpdate(true)
|
||||
setMapData(newMapData)
|
||||
setIsInternalUpdate(true);
|
||||
setMapData(newMapData);
|
||||
|
||||
const newMapKeyValuePairs = mapKeyValuePairs.filter((_, idx) => idx !== index)
|
||||
setMapkeyValuePairs(newMapKeyValuePairs)
|
||||
}
|
||||
const newMapKeyValuePairs = mapKeyValuePairs.filter((_, idx) => idx !== index);
|
||||
setMapkeyValuePairs(newMapKeyValuePairs);
|
||||
};
|
||||
|
||||
// Handling selection/input changes
|
||||
const handleInputChange = (index: number, field: 'key' | 'value', value: string) => {
|
||||
const newMapKeyValuePairs = [...mapKeyValuePairs]
|
||||
const oldValue = newMapKeyValuePairs[index][field]
|
||||
newMapKeyValuePairs[index][field] = value
|
||||
const newMapKeyValuePairs = [...mapKeyValuePairs];
|
||||
const oldValue = newMapKeyValuePairs[index][field];
|
||||
newMapKeyValuePairs[index][field] = value;
|
||||
|
||||
// Update the mapping relationship
|
||||
const newMapData = { ...mapData }
|
||||
const newMapData = { ...mapData };
|
||||
if (field === 'key') {
|
||||
if (oldValue) delete newMapData[oldValue]
|
||||
if (oldValue) delete newMapData[oldValue];
|
||||
|
||||
if (!value) {
|
||||
newMapKeyValuePairs[index].value = ''
|
||||
newMapKeyValuePairs[index].value = '';
|
||||
}
|
||||
|
||||
if (value && newMapKeyValuePairs[index].value) {
|
||||
newMapData[value] = newMapKeyValuePairs[index].value
|
||||
newMapData[value] = newMapKeyValuePairs[index].value;
|
||||
}
|
||||
} else {
|
||||
if (newMapKeyValuePairs[index].key) {
|
||||
newMapData[newMapKeyValuePairs[index].key] = value
|
||||
newMapData[newMapKeyValuePairs[index].key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
setMapkeyValuePairs(newMapKeyValuePairs)
|
||||
setIsInternalUpdate(true)
|
||||
setMapData(newMapData)
|
||||
}
|
||||
setMapkeyValuePairs(newMapKeyValuePairs);
|
||||
setIsInternalUpdate(true);
|
||||
setMapData(newMapData);
|
||||
};
|
||||
|
||||
// Check if there are still keys that can be selected
|
||||
const hasAvailableKeys = useMemo(() => {
|
||||
const usedKeys = new Set(
|
||||
mapKeyValuePairs.map((mapKeyValuePair) => mapKeyValuePair.key).filter(Boolean)
|
||||
)
|
||||
);
|
||||
// Ensure mapKeyValuePairs length does not exceed mapKeys length
|
||||
return (
|
||||
mapKeyValuePairs.length < mapKeys.length && mapKeys.some((mapKey) => !usedKeys.has(mapKey))
|
||||
)
|
||||
}, [mapKeys, mapKeyValuePairs])
|
||||
);
|
||||
}, [mapKeys, mapKeyValuePairs]);
|
||||
|
||||
return (
|
||||
<VStack w="full" align="stretch" alignItems="flex-start" spacing="8px">
|
||||
@@ -339,6 +339,6 @@ export const ConstructMappingComponent = function ({
|
||||
</Button>
|
||||
)}
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
export default ConstructMappingComponent
|
||||
);
|
||||
};
|
||||
export default ConstructMappingComponent;
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import { Badge, Box, Button, Flex, FormLabel, Input, Text, VStack } from '@chakra-ui/react'
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { Badge, Box, Button, Flex, FormLabel, Input, Text, VStack } from '@chakra-ui/react';
|
||||
|
||||
import { useTranslationClientSide } from '@/app/i18n/client'
|
||||
import { useI18n } from '@/providers/i18n/i18nContext'
|
||||
import { useTranslationClientSide } from '@/app/i18n/client';
|
||||
import { useI18n } from '@/providers/i18n/i18nContext';
|
||||
|
||||
import { CustomSelect } from './Select'
|
||||
type MapKeyValuePair = { key: string; value: string }
|
||||
import { CustomSelect } from './Select';
|
||||
type MapKeyValuePair = { key: string; value: string };
|
||||
|
||||
type Model = {
|
||||
name: string
|
||||
isDefault: boolean
|
||||
}
|
||||
name: string;
|
||||
isDefault: boolean;
|
||||
};
|
||||
|
||||
// mapKeys determines the available selection options
|
||||
export const ConstructModeMappingComponent = function ({
|
||||
@@ -18,30 +18,30 @@ export const ConstructModeMappingComponent = function ({
|
||||
mapData,
|
||||
setMapData,
|
||||
}: {
|
||||
mapKeys: Model[]
|
||||
mapData: Record<string, string>
|
||||
setMapData: (mapping: Record<string, string>) => void
|
||||
mapKeys: Model[];
|
||||
mapData: Record<string, string>;
|
||||
setMapData: (mapping: Record<string, string>) => void;
|
||||
}) {
|
||||
const { lng } = useI18n()
|
||||
const { t } = useTranslationClientSide(lng, 'common')
|
||||
const { lng } = useI18n();
|
||||
const { t } = useTranslationClientSide(lng, 'common');
|
||||
|
||||
const [mapKeyValuePairs, setMapkeyValuePairs] = useState<Array<MapKeyValuePair>>([
|
||||
{ key: '', value: '' },
|
||||
])
|
||||
]);
|
||||
|
||||
const [isInternalUpdate, setIsInternalUpdate] = useState(false)
|
||||
const [isInternalUpdate, setIsInternalUpdate] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isInternalUpdate) {
|
||||
const entries = Object.entries(mapData)
|
||||
const entries = Object.entries(mapData);
|
||||
setMapkeyValuePairs(
|
||||
entries.length > 0
|
||||
? entries.map(([key, value]) => ({ key, value }))
|
||||
: [{ key: '', value: '' }]
|
||||
)
|
||||
);
|
||||
}
|
||||
setIsInternalUpdate(false)
|
||||
}, [mapData])
|
||||
setIsInternalUpdate(false);
|
||||
}, [mapData]);
|
||||
|
||||
const handleDropdownItemDisplay = (dropdownItem: Model | string) => {
|
||||
if (dropdownItem === t('channelsFormPlaceholder.modelMappingInput')) {
|
||||
@@ -57,7 +57,7 @@ export const ConstructModeMappingComponent = function ({
|
||||
>
|
||||
{t('channelsFormPlaceholder.modelMappingInput')}
|
||||
</Text>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if ((dropdownItem as Model).isDefault) {
|
||||
@@ -112,7 +112,7 @@ export const ConstructModeMappingComponent = function ({
|
||||
</svg>
|
||||
</Badge>
|
||||
</Flex>
|
||||
)
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Text
|
||||
@@ -126,8 +126,8 @@ export const ConstructModeMappingComponent = function ({
|
||||
>
|
||||
{(dropdownItem as Model).name}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const handleSeletedItemDisplay = (selectedItem: Model | string) => {
|
||||
if (selectedItem === t('channelsFormPlaceholder.modelMappingInput')) {
|
||||
@@ -143,7 +143,7 @@ export const ConstructModeMappingComponent = function ({
|
||||
>
|
||||
{t('channelsFormPlaceholder.modelMappingInput')}
|
||||
</Text>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if ((selectedItem as Model).isDefault) {
|
||||
@@ -215,7 +215,7 @@ export const ConstructModeMappingComponent = function ({
|
||||
</svg>
|
||||
</Badge>
|
||||
</Flex>
|
||||
)
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Box
|
||||
@@ -240,106 +240,106 @@ export const ConstructModeMappingComponent = function ({
|
||||
{(selectedItem as Model).name}
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// Handling mapData and mapKeyValuePairs cleanup when map keys change.
|
||||
useEffect(() => {
|
||||
// 1. Handle mapData cleanup
|
||||
const removedKeys = Object.keys(mapData).filter(
|
||||
(key) => !mapKeys.some((model) => model.name === key)
|
||||
)
|
||||
);
|
||||
if (removedKeys.length > 0) {
|
||||
// If there are mappings with removed keys, delete them
|
||||
const newMapData = { ...mapData }
|
||||
const newMapData = { ...mapData };
|
||||
removedKeys.forEach((key) => {
|
||||
delete newMapData[key]
|
||||
})
|
||||
setIsInternalUpdate(true)
|
||||
setMapData(newMapData)
|
||||
delete newMapData[key];
|
||||
});
|
||||
setIsInternalUpdate(true);
|
||||
setMapData(newMapData);
|
||||
}
|
||||
|
||||
// 2. Handle mapKeyValuePairs cleanup
|
||||
const removedPairs = mapKeyValuePairs.filter(
|
||||
(pair) => pair.key && !mapKeys.some((model) => model.name === pair.key)
|
||||
)
|
||||
);
|
||||
if (removedPairs.length > 0) {
|
||||
const newMapKeyValuePairs = mapKeyValuePairs.filter(
|
||||
(pair) => !pair.key || mapKeys.some((model) => model.name === pair.key)
|
||||
)
|
||||
setMapkeyValuePairs(newMapKeyValuePairs)
|
||||
);
|
||||
setMapkeyValuePairs(newMapKeyValuePairs);
|
||||
}
|
||||
}, [mapKeys])
|
||||
}, [mapKeys]);
|
||||
|
||||
// Get the keys that have been selected
|
||||
const getSelectedMapKeys = (currentIndex: number) => {
|
||||
const selected = new Set<string>()
|
||||
const selected = new Set<string>();
|
||||
mapKeyValuePairs.forEach((mapKeyValuePair, idx) => {
|
||||
if (idx !== currentIndex && mapKeyValuePair.key) {
|
||||
selected.add(mapKeyValuePair.key)
|
||||
selected.add(mapKeyValuePair.key);
|
||||
}
|
||||
})
|
||||
return selected
|
||||
}
|
||||
});
|
||||
return selected;
|
||||
};
|
||||
|
||||
// Handling adding a new row
|
||||
const handleAddNewMapKeyPair = () => {
|
||||
setMapkeyValuePairs([...mapKeyValuePairs, { key: '', value: '' }])
|
||||
}
|
||||
setMapkeyValuePairs([...mapKeyValuePairs, { key: '', value: '' }]);
|
||||
};
|
||||
|
||||
// Handling deleting a row
|
||||
const handleRemoveMapKeyPair = (index: number) => {
|
||||
const mapKeyValuePair = mapKeyValuePairs[index]
|
||||
const newMapData = { ...mapData }
|
||||
const mapKeyValuePair = mapKeyValuePairs[index];
|
||||
const newMapData = { ...mapData };
|
||||
if (mapKeyValuePair.key) {
|
||||
delete newMapData[mapKeyValuePair.key]
|
||||
delete newMapData[mapKeyValuePair.key];
|
||||
}
|
||||
setIsInternalUpdate(true)
|
||||
setMapData(newMapData)
|
||||
setIsInternalUpdate(true);
|
||||
setMapData(newMapData);
|
||||
|
||||
const newMapKeyValuePairs = mapKeyValuePairs.filter((_, idx) => idx !== index)
|
||||
setMapkeyValuePairs(newMapKeyValuePairs)
|
||||
}
|
||||
const newMapKeyValuePairs = mapKeyValuePairs.filter((_, idx) => idx !== index);
|
||||
setMapkeyValuePairs(newMapKeyValuePairs);
|
||||
};
|
||||
|
||||
// Handling selection/input changes
|
||||
const handleInputChange = (index: number, field: 'key' | 'value', value: string) => {
|
||||
const newMapKeyValuePairs = [...mapKeyValuePairs]
|
||||
const oldValue = newMapKeyValuePairs[index][field]
|
||||
newMapKeyValuePairs[index][field] = value
|
||||
const newMapKeyValuePairs = [...mapKeyValuePairs];
|
||||
const oldValue = newMapKeyValuePairs[index][field];
|
||||
newMapKeyValuePairs[index][field] = value;
|
||||
|
||||
// Update the mapping relationship
|
||||
const newMapData = { ...mapData }
|
||||
const newMapData = { ...mapData };
|
||||
if (field === 'key') {
|
||||
if (oldValue) delete newMapData[oldValue]
|
||||
if (oldValue) delete newMapData[oldValue];
|
||||
|
||||
if (!value) {
|
||||
newMapKeyValuePairs[index].value = ''
|
||||
newMapKeyValuePairs[index].value = '';
|
||||
}
|
||||
|
||||
if (value && newMapKeyValuePairs[index].value) {
|
||||
newMapData[value] = newMapKeyValuePairs[index].value
|
||||
newMapData[value] = newMapKeyValuePairs[index].value;
|
||||
}
|
||||
} else {
|
||||
if (newMapKeyValuePairs[index].key) {
|
||||
newMapData[newMapKeyValuePairs[index].key] = value
|
||||
newMapData[newMapKeyValuePairs[index].key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
setMapkeyValuePairs(newMapKeyValuePairs)
|
||||
setIsInternalUpdate(true)
|
||||
setMapData(newMapData)
|
||||
}
|
||||
setMapkeyValuePairs(newMapKeyValuePairs);
|
||||
setIsInternalUpdate(true);
|
||||
setMapData(newMapData);
|
||||
};
|
||||
|
||||
// Check if there are still keys that can be selected
|
||||
const hasAvailableKeys = useMemo(() => {
|
||||
const usedKeys = new Set(
|
||||
mapKeyValuePairs.map((mapKeyValuePair) => mapKeyValuePair.key).filter(Boolean)
|
||||
)
|
||||
);
|
||||
return (
|
||||
mapKeyValuePairs.length < mapKeys.length &&
|
||||
mapKeys.some((mapKey) => !usedKeys.has(mapKey.name))
|
||||
)
|
||||
}, [mapKeys, mapKeyValuePairs])
|
||||
);
|
||||
}, [mapKeys, mapKeyValuePairs]);
|
||||
|
||||
return (
|
||||
<VStack w="full" align="stretch" alignItems="flex-start" spacing="8px">
|
||||
@@ -370,9 +370,9 @@ export const ConstructModeMappingComponent = function ({
|
||||
// when select placeholder, the newSelectedItem is null
|
||||
handleSelectedItemChange={(newSelectedItem) => {
|
||||
if (newSelectedItem) {
|
||||
handleInputChange(index, 'key', newSelectedItem.name)
|
||||
handleInputChange(index, 'key', newSelectedItem.name);
|
||||
} else {
|
||||
handleInputChange(index, 'key', '')
|
||||
handleInputChange(index, 'key', '');
|
||||
}
|
||||
}}
|
||||
handleDropdownItemDisplay={handleDropdownItemDisplay}
|
||||
@@ -489,6 +489,6 @@ export const ConstructModeMappingComponent = function ({
|
||||
</Button>
|
||||
)}
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
export default ConstructModeMappingComponent
|
||||
);
|
||||
};
|
||||
export default ConstructModeMappingComponent;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
import React, { useState } from 'react'
|
||||
import { CheckIcon, CloseIcon } from '@chakra-ui/icons'
|
||||
'use client';
|
||||
import React, { useState } from 'react';
|
||||
import { CheckIcon, CloseIcon } from '@chakra-ui/icons';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
@@ -14,28 +14,28 @@ import {
|
||||
PopoverTrigger,
|
||||
Text,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react'
|
||||
} from '@chakra-ui/react';
|
||||
|
||||
interface EditableTextProps {
|
||||
value: string | number
|
||||
onSubmit: (value: string) => void
|
||||
flexProps?: FlexProps
|
||||
value: string | number;
|
||||
onSubmit: (value: string) => void;
|
||||
flexProps?: FlexProps;
|
||||
}
|
||||
|
||||
export const EditableTextNoLable = ({ value, onSubmit, flexProps }: EditableTextProps) => {
|
||||
const [editValue, setEditValue] = useState(String(value))
|
||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||
const [editValue, setEditValue] = useState(String(value));
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
|
||||
const handleSubmit = () => {
|
||||
onSubmit(editValue)
|
||||
onClose()
|
||||
}
|
||||
onSubmit(editValue);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
// 关闭时 恢复到传递来的初始值
|
||||
setEditValue(String(value))
|
||||
onClose()
|
||||
}
|
||||
setEditValue(String(value));
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover isOpen={isOpen} onClose={handleCancel} placement="bottom-end">
|
||||
@@ -173,5 +173,5 @@ export const EditableTextNoLable = ({ value, onSubmit, flexProps }: EditableText
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import { Dispatch, ReactNode, SetStateAction, useMemo, useState } from 'react'
|
||||
'use client';
|
||||
import { Dispatch, ReactNode, SetStateAction, useMemo, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
@@ -11,11 +11,11 @@ import {
|
||||
ListItem,
|
||||
Text,
|
||||
VStack,
|
||||
} from '@chakra-ui/react'
|
||||
import { useCombobox, useMultipleSelection } from 'downshift'
|
||||
} from '@chakra-ui/react';
|
||||
import { useCombobox, useMultipleSelection } from 'downshift';
|
||||
|
||||
import { useTranslationClientSide } from '@/app/i18n/client'
|
||||
import { useI18n } from '@/providers/i18n/i18nContext'
|
||||
import { useTranslationClientSide } from '@/app/i18n/client';
|
||||
import { useI18n } from '@/providers/i18n/i18nContext';
|
||||
|
||||
export const MultiSelectCombobox = function <T>({
|
||||
dropdownItems,
|
||||
@@ -26,30 +26,30 @@ export const MultiSelectCombobox = function <T>({
|
||||
handleSelectedItemDisplay,
|
||||
handleSetCustomSelectedItem,
|
||||
}: {
|
||||
dropdownItems: T[]
|
||||
selectedItems: T[]
|
||||
setSelectedItems: Dispatch<SetStateAction<T[]>>
|
||||
handleFilteredDropdownItems: (dropdownItems: T[], selectedItems: T[], inputValue: string) => T[]
|
||||
handleDropdownItemDisplay: (dropdownItem: T) => ReactNode
|
||||
handleSelectedItemDisplay: (selectedItem: T) => ReactNode
|
||||
dropdownItems: T[];
|
||||
selectedItems: T[];
|
||||
setSelectedItems: Dispatch<SetStateAction<T[]>>;
|
||||
handleFilteredDropdownItems: (dropdownItems: T[], selectedItems: T[], inputValue: string) => T[];
|
||||
handleDropdownItemDisplay: (dropdownItem: T) => ReactNode;
|
||||
handleSelectedItemDisplay: (selectedItem: T) => ReactNode;
|
||||
handleSetCustomSelectedItem?: (
|
||||
selectedItems: T[],
|
||||
setSelectedItems: Dispatch<SetStateAction<T[]>>,
|
||||
customSelectedItemName: string,
|
||||
setCustomSelectedItemName: Dispatch<SetStateAction<string>>
|
||||
) => void
|
||||
) => void;
|
||||
}): JSX.Element {
|
||||
const { lng } = useI18n()
|
||||
const { t } = useTranslationClientSide(lng, 'common')
|
||||
const { lng } = useI18n();
|
||||
const { t } = useTranslationClientSide(lng, 'common');
|
||||
|
||||
const [inputValue, setInputValue] = useState<string>('')
|
||||
const [customSelectedItemName, setCustomSelectedItemName] = useState('')
|
||||
const [inputValue, setInputValue] = useState<string>('');
|
||||
const [customSelectedItemName, setCustomSelectedItemName] = useState('');
|
||||
|
||||
// Dropdown list excludes already selected options and includes those matching the input.
|
||||
const items = useMemo(
|
||||
() => handleFilteredDropdownItems(dropdownItems, selectedItems, inputValue),
|
||||
[inputValue, selectedItems, dropdownItems, handleFilteredDropdownItems]
|
||||
)
|
||||
);
|
||||
|
||||
// 对已经选中的项目 添加处理事件
|
||||
const { getSelectedItemProps, getDropdownProps, removeSelectedItem } = useMultipleSelection({
|
||||
@@ -61,14 +61,14 @@ export const MultiSelectCombobox = function <T>({
|
||||
case useMultipleSelection.stateChangeTypes.DropdownKeyDownBackspace:
|
||||
case useMultipleSelection.stateChangeTypes.FunctionRemoveSelectedItem:
|
||||
if (newSelectedItems) {
|
||||
setSelectedItems(newSelectedItems)
|
||||
setSelectedItems(newSelectedItems);
|
||||
}
|
||||
break
|
||||
break;
|
||||
default:
|
||||
break
|
||||
break;
|
||||
}
|
||||
},
|
||||
})
|
||||
});
|
||||
const {
|
||||
isOpen,
|
||||
getToggleButtonProps,
|
||||
@@ -84,7 +84,7 @@ export const MultiSelectCombobox = function <T>({
|
||||
selectedItem: null,
|
||||
inputValue,
|
||||
stateReducer(state, actionAndChanges) {
|
||||
const { changes, type } = actionAndChanges
|
||||
const { changes, type } = actionAndChanges;
|
||||
|
||||
switch (type) {
|
||||
case useCombobox.stateChangeTypes.InputKeyDownEnter:
|
||||
@@ -93,9 +93,9 @@ export const MultiSelectCombobox = function <T>({
|
||||
...changes,
|
||||
isOpen: true, // keep the menu open after selection.
|
||||
highlightedIndex: 0, // with the first option highlighted.
|
||||
}
|
||||
};
|
||||
default:
|
||||
return changes
|
||||
return changes;
|
||||
}
|
||||
},
|
||||
onStateChange({ inputValue: newInputValue, type, selectedItem: newSelectedItem }) {
|
||||
@@ -104,20 +104,20 @@ export const MultiSelectCombobox = function <T>({
|
||||
case useCombobox.stateChangeTypes.ItemClick:
|
||||
case useCombobox.stateChangeTypes.InputBlur:
|
||||
if (newSelectedItem) {
|
||||
setSelectedItems([...selectedItems, newSelectedItem])
|
||||
setInputValue('')
|
||||
setSelectedItems([...selectedItems, newSelectedItem]);
|
||||
setInputValue('');
|
||||
}
|
||||
break
|
||||
break;
|
||||
|
||||
case useCombobox.stateChangeTypes.InputChange:
|
||||
setInputValue(newInputValue ?? '')
|
||||
setInputValue(newInputValue ?? '');
|
||||
|
||||
break
|
||||
break;
|
||||
default:
|
||||
break
|
||||
break;
|
||||
}
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
return (
|
||||
<Box w="full">
|
||||
@@ -215,7 +215,7 @@ export const MultiSelectCombobox = function <T>({
|
||||
setSelectedItems,
|
||||
customSelectedItemName,
|
||||
setCustomSelectedItemName
|
||||
)
|
||||
);
|
||||
}}
|
||||
_hover={{
|
||||
transform: 'scale(1.05)',
|
||||
@@ -282,8 +282,8 @@ export const MultiSelectCombobox = function <T>({
|
||||
justifyContent="center"
|
||||
cursor="pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
removeSelectedItem(selectedItemForRender)
|
||||
e.stopPropagation();
|
||||
removeSelectedItem(selectedItemForRender);
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
@@ -417,5 +417,5 @@ export const MultiSelectCombobox = function <T>({
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
'use client';
|
||||
|
||||
import { Tooltip, TooltipProps } from '@chakra-ui/react'
|
||||
import { Tooltip, TooltipProps } from '@chakra-ui/react';
|
||||
|
||||
export const MyTooltip = ({ children, ...tooltipProps }: TooltipProps) => {
|
||||
const tooltipStyles = {
|
||||
@@ -42,11 +42,11 @@ export const MyTooltip = ({ children, ...tooltipProps }: TooltipProps) => {
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip {...tooltipStyles} {...tooltipProps}>
|
||||
{children}
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
'use client'
|
||||
import { ReactNode } from 'react'
|
||||
import { Box, List, ListItem, Text } from '@chakra-ui/react'
|
||||
import { useSelect } from 'downshift'
|
||||
'use client';
|
||||
import { ReactNode } from 'react';
|
||||
import { Box, List, ListItem, Text } from '@chakra-ui/react';
|
||||
import { useSelect } from 'downshift';
|
||||
|
||||
import { useTranslationClientSide } from '@/app/i18n/client'
|
||||
import { useI18n } from '@/providers/i18n/i18nContext'
|
||||
import { useTranslationClientSide } from '@/app/i18n/client';
|
||||
import { useI18n } from '@/providers/i18n/i18nContext';
|
||||
|
||||
export const CustomSelect = function <T>({
|
||||
listItems,
|
||||
@@ -14,16 +14,16 @@ export const CustomSelect = function <T>({
|
||||
placeholder,
|
||||
initSelectedItem,
|
||||
}: {
|
||||
listItems: T[]
|
||||
handleSelectedItemChange: (selectedItem: T) => void
|
||||
handleDropdownItemDisplay: (dropdownItem: T) => ReactNode
|
||||
handleSelectedItemDisplay: (selectedItem: T) => ReactNode
|
||||
placeholder?: string
|
||||
initSelectedItem?: T
|
||||
listItems: T[];
|
||||
handleSelectedItemChange: (selectedItem: T) => void;
|
||||
handleDropdownItemDisplay: (dropdownItem: T) => ReactNode;
|
||||
handleSelectedItemDisplay: (selectedItem: T) => ReactNode;
|
||||
placeholder?: string;
|
||||
initSelectedItem?: T;
|
||||
}) {
|
||||
const { lng } = useI18n()
|
||||
const { t } = useTranslationClientSide(lng, 'common')
|
||||
const items = [placeholder, ...listItems]
|
||||
const { lng } = useI18n();
|
||||
const { t } = useTranslationClientSide(lng, 'common');
|
||||
const items = [placeholder, ...listItems];
|
||||
|
||||
const {
|
||||
isOpen,
|
||||
@@ -37,12 +37,12 @@ export const CustomSelect = function <T>({
|
||||
initialSelectedItem: initSelectedItem,
|
||||
onSelectedItemChange: ({ selectedItem: newSelectedItem }) => {
|
||||
if (newSelectedItem === placeholder) {
|
||||
handleSelectedItemChange(undefined as T)
|
||||
handleSelectedItemChange(undefined as T);
|
||||
} else {
|
||||
handleSelectedItemChange(newSelectedItem as T)
|
||||
handleSelectedItemChange(newSelectedItem as T);
|
||||
}
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
return (
|
||||
<Box w="full" position="relative" flex={1}>
|
||||
@@ -143,5 +143,5 @@ export const CustomSelect = function <T>({
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ChangeEventHandler, Dispatch, SetStateAction, useState } from 'react'
|
||||
import { DateRange, DayPicker, SelectRangeEventHandler } from 'react-day-picker'
|
||||
import { ChangeEventHandler, Dispatch, SetStateAction, useState } from 'react';
|
||||
import { DateRange, DayPicker, SelectRangeEventHandler } from 'react-day-picker';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
@@ -10,16 +10,16 @@ import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@chakra-ui/react'
|
||||
import { endOfDay, format, isAfter, isBefore, isValid, parse, startOfDay } from 'date-fns'
|
||||
} from '@chakra-ui/react';
|
||||
import { endOfDay, format, isAfter, isBefore, isValid, parse, startOfDay } from 'date-fns';
|
||||
|
||||
type SelectDateRangeProps = {
|
||||
isDisabled?: boolean
|
||||
startTime: Date
|
||||
setStartTime: Dispatch<SetStateAction<Date>>
|
||||
endTime: Date
|
||||
setEndTime: Dispatch<SetStateAction<Date>>
|
||||
}
|
||||
isDisabled?: boolean;
|
||||
startTime: Date;
|
||||
setStartTime: Dispatch<SetStateAction<Date>>;
|
||||
endTime: Date;
|
||||
setEndTime: Dispatch<SetStateAction<Date>>;
|
||||
};
|
||||
|
||||
export default function SelectDateRange({
|
||||
isDisabled,
|
||||
@@ -29,146 +29,146 @@ export default function SelectDateRange({
|
||||
setEndTime,
|
||||
...props
|
||||
}: SelectDateRangeProps & FlexProps): JSX.Element {
|
||||
const initState = { from: startTime, to: endTime }
|
||||
const initState = { from: startTime, to: endTime };
|
||||
|
||||
const [selectedRange, setSelectedRange] = useState<DateRange | undefined>(initState)
|
||||
const [fromValue, setFromValue] = useState<string>(format(initState.from, 'y-MM-dd'))
|
||||
const [toValue, setToValue] = useState<string>(format(initState.to, 'y-MM-dd'))
|
||||
const [inputState, setInputState] = useState<0 | 1>(0)
|
||||
const [selectedRange, setSelectedRange] = useState<DateRange | undefined>(initState);
|
||||
const [fromValue, setFromValue] = useState<string>(format(initState.from, 'y-MM-dd'));
|
||||
const [toValue, setToValue] = useState<string>(format(initState.to, 'y-MM-dd'));
|
||||
const [inputState, setInputState] = useState<0 | 1>(0);
|
||||
const onClose = () => {
|
||||
selectedRange?.from && setStartTime(startOfDay(selectedRange.from))
|
||||
selectedRange?.to && setEndTime(endOfDay(selectedRange.to))
|
||||
}
|
||||
selectedRange?.from && setStartTime(startOfDay(selectedRange.from));
|
||||
selectedRange?.to && setEndTime(endOfDay(selectedRange.to));
|
||||
};
|
||||
|
||||
const handleFromChange: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
setFromValue(e.target.value)
|
||||
const date = parse(e.target.value, 'y-MM-dd', new Date())
|
||||
setFromValue(e.target.value);
|
||||
const date = parse(e.target.value, 'y-MM-dd', new Date());
|
||||
if (!isValid(date)) {
|
||||
return setSelectedRange({ from: undefined, to: selectedRange?.to })
|
||||
return setSelectedRange({ from: undefined, to: selectedRange?.to });
|
||||
}
|
||||
|
||||
if (selectedRange?.to) {
|
||||
if (isAfter(date, selectedRange.to)) {
|
||||
setSelectedRange({ from: selectedRange.to, to: date })
|
||||
setSelectedRange({ from: selectedRange.to, to: date });
|
||||
} else {
|
||||
setSelectedRange({ from: date, to: selectedRange?.to })
|
||||
setSelectedRange({ from: date, to: selectedRange?.to });
|
||||
}
|
||||
} else {
|
||||
setSelectedRange({ from: date, to: date })
|
||||
setSelectedRange({ from: date, to: date });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleToChange: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
setToValue(e.target.value)
|
||||
const date = parse(e.target.value, 'y-MM-dd', new Date())
|
||||
setToValue(e.target.value);
|
||||
const date = parse(e.target.value, 'y-MM-dd', new Date());
|
||||
|
||||
if (!isValid(date)) {
|
||||
return setSelectedRange({ from: selectedRange?.from, to: undefined })
|
||||
return setSelectedRange({ from: selectedRange?.from, to: undefined });
|
||||
}
|
||||
if (selectedRange?.from) {
|
||||
if (isBefore(date, selectedRange.from)) {
|
||||
setSelectedRange({ from: date, to: selectedRange.from })
|
||||
setSelectedRange({ from: date, to: selectedRange.from });
|
||||
} else {
|
||||
setSelectedRange({ from: selectedRange?.from, to: date })
|
||||
setSelectedRange({ from: selectedRange?.from, to: date });
|
||||
}
|
||||
} else {
|
||||
setSelectedRange({ from: date, to: date })
|
||||
setSelectedRange({ from: date, to: date });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleRangeSelect: SelectRangeEventHandler = (range: DateRange | undefined) => {
|
||||
if (range) {
|
||||
let { from, to } = range
|
||||
let { from, to } = range;
|
||||
if (inputState === 0) {
|
||||
if (from === selectedRange?.from) {
|
||||
from = to
|
||||
from = to;
|
||||
} else {
|
||||
to = from
|
||||
to = from;
|
||||
}
|
||||
setInputState(1)
|
||||
setInputState(1);
|
||||
} else {
|
||||
setInputState(0)
|
||||
setInputState(0);
|
||||
}
|
||||
setSelectedRange({
|
||||
from,
|
||||
to,
|
||||
})
|
||||
});
|
||||
if (from) {
|
||||
setFromValue(format(from, 'y-MM-dd'))
|
||||
setFromValue(format(from, 'y-MM-dd'));
|
||||
} else {
|
||||
setFromValue('')
|
||||
setFromValue('');
|
||||
}
|
||||
if (to) {
|
||||
setToValue(format(to, 'y-MM-dd'))
|
||||
setToValue(format(to, 'y-MM-dd'));
|
||||
} else {
|
||||
setToValue(from ? format(from, 'y-MM-dd') : '')
|
||||
setToValue(from ? format(from, 'y-MM-dd') : '');
|
||||
}
|
||||
} else {
|
||||
if (fromValue && selectedRange?.from) {
|
||||
setToValue(fromValue)
|
||||
setToValue(fromValue);
|
||||
setSelectedRange({
|
||||
...selectedRange,
|
||||
to: selectedRange.from,
|
||||
})
|
||||
setInputState(1)
|
||||
});
|
||||
setInputState(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleRangeSelectFrom: SelectRangeEventHandler = (range: DateRange | undefined) => {
|
||||
if (range) {
|
||||
let { from, to } = range
|
||||
let { from, to } = range;
|
||||
if (selectedRange?.to) {
|
||||
if (from) {
|
||||
if (!to) {
|
||||
to = from
|
||||
to = from;
|
||||
} else if (from === selectedRange?.from) {
|
||||
from = to
|
||||
to = selectedRange.to
|
||||
from = to;
|
||||
to = selectedRange.to;
|
||||
}
|
||||
if (isBefore(from, selectedRange.to)) {
|
||||
setSelectedRange({
|
||||
...selectedRange,
|
||||
from,
|
||||
})
|
||||
setFromValue(format(from, 'y-MM-dd'))
|
||||
});
|
||||
setFromValue(format(from, 'y-MM-dd'));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleRangeSelectTo: SelectRangeEventHandler = (range: DateRange | undefined) => {
|
||||
if (range) {
|
||||
let { from, to } = range
|
||||
let { from, to } = range;
|
||||
if (selectedRange?.from) {
|
||||
if (to) {
|
||||
if (!from) {
|
||||
from = to
|
||||
from = to;
|
||||
} else if (to === selectedRange?.to) {
|
||||
to = from
|
||||
from = selectedRange.from
|
||||
to = from;
|
||||
from = selectedRange.from;
|
||||
}
|
||||
if (isAfter(to, selectedRange.from)) {
|
||||
setSelectedRange({
|
||||
...selectedRange,
|
||||
to,
|
||||
})
|
||||
setToValue(format(to, 'y-MM-dd'))
|
||||
});
|
||||
setToValue(format(to, 'y-MM-dd'));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (fromValue && selectedRange?.from) {
|
||||
setToValue(fromValue)
|
||||
setToValue(fromValue);
|
||||
setSelectedRange({
|
||||
...selectedRange,
|
||||
to: selectedRange.from,
|
||||
})
|
||||
setInputState(1)
|
||||
});
|
||||
setInputState(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex
|
||||
@@ -196,7 +196,7 @@ export default function SelectDateRange({
|
||||
minW="50px"
|
||||
onChange={handleFromChange}
|
||||
onBlur={() => {
|
||||
selectedRange?.from && setStartTime(startOfDay(selectedRange.from))
|
||||
selectedRange?.from && setStartTime(startOfDay(selectedRange.from));
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
@@ -229,7 +229,7 @@ export default function SelectDateRange({
|
||||
minW="90px"
|
||||
onChange={handleToChange}
|
||||
onBlur={() => {
|
||||
selectedRange?.to && setEndTime(endOfDay(selectedRange.to))
|
||||
selectedRange?.to && setEndTime(endOfDay(selectedRange.to));
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
@@ -251,8 +251,8 @@ export default function SelectDateRange({
|
||||
</Popover>
|
||||
<Popover
|
||||
onClose={() => {
|
||||
setInputState(0)
|
||||
onClose()
|
||||
setInputState(0);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger>
|
||||
@@ -286,5 +286,5 @@ export default function SelectDateRange({
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</Flex>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,26 @@
|
||||
'use client'
|
||||
import { ReactNode, useEffect, useState } from 'react'
|
||||
import { Box, Button, FormLabel, Input, InputGroup, List, ListItem, VStack } from '@chakra-ui/react'
|
||||
import { useCombobox, UseComboboxReturnValue } from 'downshift'
|
||||
'use client';
|
||||
import { ReactNode, useEffect, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
FormLabel,
|
||||
Input,
|
||||
InputGroup,
|
||||
List,
|
||||
ListItem,
|
||||
VStack,
|
||||
} from '@chakra-ui/react';
|
||||
import { useCombobox, UseComboboxReturnValue } from 'downshift';
|
||||
|
||||
import { useTranslationClientSide } from '@/app/i18n/client'
|
||||
import { useI18n } from '@/providers/i18n/i18nContext'
|
||||
import { useTranslationClientSide } from '@/app/i18n/client';
|
||||
import { useI18n } from '@/providers/i18n/i18nContext';
|
||||
|
||||
export const SingleSelectCombobox: <T>(props: {
|
||||
dropdownItems: T[]
|
||||
setSelectedItem: (value: T) => void
|
||||
handleDropdownItemFilter: (dropdownItems: T[], inputValue: string) => T[]
|
||||
handleDropdownItemDisplay: (dropdownItem: T) => ReactNode
|
||||
initSelectedItem?: T
|
||||
dropdownItems: T[];
|
||||
setSelectedItem: (value: T) => void;
|
||||
handleDropdownItemFilter: (dropdownItems: T[], inputValue: string) => T[];
|
||||
handleDropdownItemDisplay: (dropdownItem: T) => ReactNode;
|
||||
initSelectedItem?: T;
|
||||
}) => JSX.Element = function <T>({
|
||||
dropdownItems,
|
||||
setSelectedItem,
|
||||
@@ -19,18 +28,18 @@ export const SingleSelectCombobox: <T>(props: {
|
||||
handleDropdownItemDisplay,
|
||||
initSelectedItem,
|
||||
}: {
|
||||
dropdownItems: T[]
|
||||
setSelectedItem: (value: T) => void
|
||||
handleDropdownItemFilter: (dropdownItems: T[], inputValue: string) => T[]
|
||||
handleDropdownItemDisplay: (dropdownItem: T) => ReactNode
|
||||
initSelectedItem?: T
|
||||
dropdownItems: T[];
|
||||
setSelectedItem: (value: T) => void;
|
||||
handleDropdownItemFilter: (dropdownItems: T[], inputValue: string) => T[];
|
||||
handleDropdownItemDisplay: (dropdownItem: T) => ReactNode;
|
||||
initSelectedItem?: T;
|
||||
}) {
|
||||
const { lng } = useI18n()
|
||||
const { t } = useTranslationClientSide(lng, 'common')
|
||||
const [getFilteredDropdownItems, setGetFilteredDropdownItems] = useState<T[]>(dropdownItems)
|
||||
const { lng } = useI18n();
|
||||
const { t } = useTranslationClientSide(lng, 'common');
|
||||
const [getFilteredDropdownItems, setGetFilteredDropdownItems] = useState<T[]>(dropdownItems);
|
||||
useEffect(() => {
|
||||
setGetFilteredDropdownItems(dropdownItems)
|
||||
}, [dropdownItems])
|
||||
setGetFilteredDropdownItems(dropdownItems);
|
||||
}, [dropdownItems]);
|
||||
|
||||
const {
|
||||
isOpen: isComboboxOpen,
|
||||
@@ -44,18 +53,18 @@ export const SingleSelectCombobox: <T>(props: {
|
||||
}: UseComboboxReturnValue<T> = useCombobox({
|
||||
items: getFilteredDropdownItems,
|
||||
onInputValueChange: ({ inputValue }) => {
|
||||
setGetFilteredDropdownItems(handleDropdownItemFilter(dropdownItems, inputValue))
|
||||
setGetFilteredDropdownItems(handleDropdownItemFilter(dropdownItems, inputValue));
|
||||
},
|
||||
|
||||
initialSelectedItem: initSelectedItem || undefined,
|
||||
|
||||
onSelectedItemChange: ({ selectedItem }) => {
|
||||
const selectedDropdownItem = dropdownItems.find((item) => item === selectedItem)
|
||||
const selectedDropdownItem = dropdownItems.find((item) => item === selectedItem);
|
||||
if (selectedDropdownItem) {
|
||||
setSelectedItem(selectedDropdownItem)
|
||||
setSelectedItem(selectedDropdownItem);
|
||||
}
|
||||
},
|
||||
})
|
||||
});
|
||||
return (
|
||||
<Box w="full">
|
||||
<VStack w="full" align="stretch" alignItems="flex-start" spacing="8px">
|
||||
@@ -197,5 +206,5 @@ export const SingleSelectCombobox: <T>(props: {
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
'use client'
|
||||
import { ReactNode, useEffect, useState } from 'react'
|
||||
import { Box, Button, Flex, FlexProps, Input, InputGroup, List, ListItem } from '@chakra-ui/react'
|
||||
import { useCombobox, UseComboboxReturnValue } from 'downshift'
|
||||
'use client';
|
||||
import { ReactNode, useEffect, useState } from 'react';
|
||||
import { Box, Button, Flex, FlexProps, Input, InputGroup, List, ListItem } from '@chakra-ui/react';
|
||||
import { useCombobox, UseComboboxReturnValue } from 'downshift';
|
||||
|
||||
import { useTranslationClientSide } from '@/app/i18n/client'
|
||||
import { useI18n } from '@/providers/i18n/i18nContext'
|
||||
import { useTranslationClientSide } from '@/app/i18n/client';
|
||||
import { useI18n } from '@/providers/i18n/i18nContext';
|
||||
|
||||
export const SingleSelectComboboxUnstyle: <T>(props: {
|
||||
dropdownItems: T[]
|
||||
setSelectedItem: (value: T) => void
|
||||
handleDropdownItemFilter: (dropdownItems: T[], inputValue: string) => T[]
|
||||
handleDropdownItemDisplay: (dropdownItem: T) => ReactNode
|
||||
handleInputDisplay?: (item: T) => string
|
||||
initSelectedItem?: T
|
||||
flexProps?: FlexProps
|
||||
placeholder?: string
|
||||
dropdownItems: T[];
|
||||
setSelectedItem: (value: T) => void;
|
||||
handleDropdownItemFilter: (dropdownItems: T[], inputValue: string) => T[];
|
||||
handleDropdownItemDisplay: (dropdownItem: T) => ReactNode;
|
||||
handleInputDisplay?: (item: T) => string;
|
||||
initSelectedItem?: T;
|
||||
flexProps?: FlexProps;
|
||||
placeholder?: string;
|
||||
}) => JSX.Element = function <T>({
|
||||
dropdownItems,
|
||||
setSelectedItem,
|
||||
@@ -25,21 +25,21 @@ export const SingleSelectComboboxUnstyle: <T>(props: {
|
||||
flexProps,
|
||||
placeholder,
|
||||
}: {
|
||||
dropdownItems: T[]
|
||||
setSelectedItem: (value: T) => void
|
||||
handleDropdownItemFilter: (dropdownItems: T[], inputValue: string) => T[]
|
||||
handleDropdownItemDisplay: (dropdownItem: T) => ReactNode
|
||||
handleInputDisplay?: (item: T) => string
|
||||
initSelectedItem?: T
|
||||
flexProps?: FlexProps
|
||||
placeholder?: string
|
||||
dropdownItems: T[];
|
||||
setSelectedItem: (value: T) => void;
|
||||
handleDropdownItemFilter: (dropdownItems: T[], inputValue: string) => T[];
|
||||
handleDropdownItemDisplay: (dropdownItem: T) => ReactNode;
|
||||
handleInputDisplay?: (item: T) => string;
|
||||
initSelectedItem?: T;
|
||||
flexProps?: FlexProps;
|
||||
placeholder?: string;
|
||||
}) {
|
||||
const { lng } = useI18n()
|
||||
const { t } = useTranslationClientSide(lng, 'common')
|
||||
const [getFilteredDropdownItems, setGetFilteredDropdownItems] = useState<T[]>(dropdownItems)
|
||||
const { lng } = useI18n();
|
||||
const { t } = useTranslationClientSide(lng, 'common');
|
||||
const [getFilteredDropdownItems, setGetFilteredDropdownItems] = useState<T[]>(dropdownItems);
|
||||
useEffect(() => {
|
||||
setGetFilteredDropdownItems(dropdownItems)
|
||||
}, [dropdownItems])
|
||||
setGetFilteredDropdownItems(dropdownItems);
|
||||
}, [dropdownItems]);
|
||||
|
||||
const {
|
||||
isOpen: isComboboxOpen,
|
||||
@@ -52,22 +52,22 @@ export const SingleSelectComboboxUnstyle: <T>(props: {
|
||||
}: UseComboboxReturnValue<T> = useCombobox({
|
||||
items: getFilteredDropdownItems,
|
||||
onInputValueChange: ({ inputValue }) => {
|
||||
setGetFilteredDropdownItems(handleDropdownItemFilter(dropdownItems, inputValue))
|
||||
setGetFilteredDropdownItems(handleDropdownItemFilter(dropdownItems, inputValue));
|
||||
},
|
||||
initialSelectedItem: initSelectedItem || undefined,
|
||||
|
||||
itemToString: (item) => {
|
||||
if (!item) return ''
|
||||
return handleInputDisplay ? handleInputDisplay(item) : String(item)
|
||||
if (!item) return '';
|
||||
return handleInputDisplay ? handleInputDisplay(item) : String(item);
|
||||
},
|
||||
|
||||
onSelectedItemChange: ({ selectedItem }) => {
|
||||
const selectedDropdownItem = dropdownItems.find((item) => item === selectedItem)
|
||||
const selectedDropdownItem = dropdownItems.find((item) => item === selectedItem);
|
||||
if (selectedDropdownItem) {
|
||||
setSelectedItem(selectedDropdownItem)
|
||||
setSelectedItem(selectedDropdownItem);
|
||||
}
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
return (
|
||||
<Flex flexDirection="column" w="full" {...flexProps} position="relative">
|
||||
@@ -195,5 +195,5 @@ export const SingleSelectComboboxUnstyle: <T>(props: {
|
||||
))}
|
||||
</List>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Button, ButtonProps, Flex, FlexProps, Text } from '@chakra-ui/react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { Button, ButtonProps, Flex, FlexProps, Text } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
import { RightFirstIcon, ToLeftIcon } from '@/ui/icons'
|
||||
import { RightFirstIcon, ToLeftIcon } from '@/ui/icons';
|
||||
|
||||
export default function SwitchPage({
|
||||
totalPage,
|
||||
@@ -12,14 +12,14 @@ export default function SwitchPage({
|
||||
isPreviousData,
|
||||
...props
|
||||
}: {
|
||||
currentPage: number
|
||||
totalPage: number
|
||||
totalItem: number
|
||||
pageSize: number
|
||||
isPreviousData?: boolean
|
||||
setCurrentPage: (idx: number) => void
|
||||
currentPage: number;
|
||||
totalPage: number;
|
||||
totalItem: number;
|
||||
pageSize: number;
|
||||
isPreviousData?: boolean;
|
||||
setCurrentPage: (idx: number) => void;
|
||||
} & FlexProps) {
|
||||
const { t } = useTranslation()
|
||||
const { t } = useTranslation();
|
||||
const switchStyle: ButtonProps = {
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
@@ -38,7 +38,7 @@ export default function SwitchPage({
|
||||
cursor: 'not-allowed',
|
||||
minW: '0',
|
||||
},
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Flex minW="370px" h="32px" align={'center'} mb={'8px'} fontSize="14px" {...props}>
|
||||
<Text fontSize="14px" color={'grayModern.500'}>
|
||||
@@ -56,8 +56,8 @@ export default function SwitchPage({
|
||||
minW="0"
|
||||
boxSize="24px"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
setCurrentPage(1)
|
||||
e.preventDefault();
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
>
|
||||
<RightFirstIcon transform={'rotate(-180deg)'} />
|
||||
@@ -70,8 +70,8 @@ export default function SwitchPage({
|
||||
minW="0"
|
||||
boxSize="24px"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
setCurrentPage(currentPage - 1)
|
||||
e.preventDefault();
|
||||
setCurrentPage(currentPage - 1);
|
||||
}}
|
||||
>
|
||||
<ToLeftIcon />
|
||||
@@ -88,8 +88,8 @@ export default function SwitchPage({
|
||||
minW="0"
|
||||
borderRadius={'50%'}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
setCurrentPage(currentPage + 1)
|
||||
e.preventDefault();
|
||||
setCurrentPage(currentPage + 1);
|
||||
}}
|
||||
>
|
||||
<ToLeftIcon transform={'rotate(180deg)'} />
|
||||
@@ -104,8 +104,8 @@ export default function SwitchPage({
|
||||
borderRadius={'50%'}
|
||||
mr={'10px'}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
setCurrentPage(totalPage)
|
||||
e.preventDefault();
|
||||
setCurrentPage(totalPage);
|
||||
}}
|
||||
>
|
||||
<RightFirstIcon />
|
||||
@@ -118,5 +118,5 @@ export default function SwitchPage({
|
||||
/{t('Page')}
|
||||
</Text>
|
||||
</Flex>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ import {
|
||||
Th,
|
||||
Thead,
|
||||
Tr,
|
||||
} from '@chakra-ui/react'
|
||||
import { flexRender, Table as ReactTable } from '@tanstack/react-table'
|
||||
} from '@chakra-ui/react';
|
||||
import { flexRender, Table as ReactTable } from '@tanstack/react-table';
|
||||
|
||||
export function BaseTable<T extends unknown>({
|
||||
table,
|
||||
@@ -38,10 +38,10 @@ export function BaseTable<T extends unknown>({
|
||||
>
|
||||
{flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</Th>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</Tr>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</Thead>
|
||||
<Tbody>
|
||||
@@ -72,14 +72,14 @@ export function BaseTable<T extends unknown>({
|
||||
<Td py="10px" key={cell.id} px="24px">
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</Td>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</Tr>
|
||||
)
|
||||
);
|
||||
})
|
||||
)}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import React, { useMemo, useState } from 'react'
|
||||
'use client';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
@@ -28,33 +28,33 @@ import {
|
||||
Thead,
|
||||
Tr,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react'
|
||||
import { CurrencySymbol } from '@sealos/ui'
|
||||
import { useMessage } from '@sealos/ui'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
} from '@chakra-ui/react';
|
||||
import { CurrencySymbol } from '@sealos/ui';
|
||||
import { useMessage } from '@sealos/ui';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
Column,
|
||||
createColumnHelper,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
} from '@tanstack/react-table'
|
||||
import { TFunction } from 'i18next'
|
||||
} from '@tanstack/react-table';
|
||||
import { TFunction } from 'i18next';
|
||||
|
||||
import { createToken, deleteToken, getTokens, updateToken } from '@/api/platform'
|
||||
import { useTranslationClientSide } from '@/app/i18n/client'
|
||||
import { MyTooltip } from '@/components/common/MyTooltip'
|
||||
import SwitchPage from '@/components/common/SwitchPage'
|
||||
import { useI18n } from '@/providers/i18n/i18nContext'
|
||||
import { useBackendStore } from '@/store/backend'
|
||||
import { QueryKey } from '@/types/query-key'
|
||||
import { TokenInfo } from '@/types/user/token'
|
||||
import { ChainIcon } from '@/ui/icons/index'
|
||||
import { createToken, deleteToken, getTokens, updateToken } from '@/api/platform';
|
||||
import { useTranslationClientSide } from '@/app/i18n/client';
|
||||
import { MyTooltip } from '@/components/common/MyTooltip';
|
||||
import SwitchPage from '@/components/common/SwitchPage';
|
||||
import { useI18n } from '@/providers/i18n/i18nContext';
|
||||
import { useBackendStore } from '@/store/backend';
|
||||
import { QueryKey } from '@/types/query-key';
|
||||
import { TokenInfo } from '@/types/user/token';
|
||||
import { ChainIcon } from '@/ui/icons/index';
|
||||
|
||||
export function KeyList(): JSX.Element {
|
||||
const { lng } = useI18n()
|
||||
const { t } = useTranslationClientSide(lng, 'common')
|
||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||
const { lng } = useI18n();
|
||||
const { t } = useTranslationClientSide(lng, 'common');
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -78,7 +78,7 @@ export function KeyList(): JSX.Element {
|
||||
{/* modal */}
|
||||
<CreateKeyModal isOpen={isOpen} onClose={onClose} t={t} />
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export enum TableHeaderId {
|
||||
@@ -100,7 +100,7 @@ enum KeyStatus {
|
||||
}
|
||||
|
||||
const CustomHeader = ({ column, t }: { column: Column<TokenInfo>; t: TFunction }) => {
|
||||
const { currencySymbol } = useBackendStore()
|
||||
const { currencySymbol } = useBackendStore();
|
||||
if (column.id === TableHeaderId.USED_AMOUNT) {
|
||||
return (
|
||||
<Flex alignItems={'center'} gap={'4px'}>
|
||||
@@ -116,7 +116,7 @@ const CustomHeader = ({ column, t }: { column: Column<TokenInfo>; t: TFunction }
|
||||
</Text>
|
||||
<CurrencySymbol type={currencySymbol} />
|
||||
</Flex>
|
||||
)
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Text
|
||||
@@ -129,15 +129,15 @@ const CustomHeader = ({ column, t }: { column: Column<TokenInfo>; t: TFunction }
|
||||
>
|
||||
{t(column.id as TableHeaderId)}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const ModelKeyTable = ({ t, onOpen }: { t: TFunction; onOpen: () => void }) => {
|
||||
const aiproxyBackend = useBackendStore((state) => state.aiproxyBackend)
|
||||
const [total, setTotal] = useState(0)
|
||||
const [page, setPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(10)
|
||||
const [openPopoverId, setOpenPopoverId] = useState<number | null>(null)
|
||||
const aiproxyBackend = useBackendStore((state) => state.aiproxyBackend);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [openPopoverId, setOpenPopoverId] = useState<number | null>(null);
|
||||
|
||||
const { message } = useMessage({
|
||||
warningBoxBg: '#FFFAEB',
|
||||
@@ -146,28 +146,28 @@ const ModelKeyTable = ({ t, onOpen }: { t: TFunction; onOpen: () => void }) => {
|
||||
successBoxBg: '#EDFBF3',
|
||||
successIconBg: '#039855',
|
||||
successIconFill: 'white',
|
||||
})
|
||||
const queryClient = useQueryClient()
|
||||
});
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: [QueryKey.GetTokens, page, pageSize],
|
||||
queryFn: () => getTokens({ page, perPage: pageSize }),
|
||||
refetchOnReconnect: true,
|
||||
onSuccess(data) {
|
||||
setTotal(data?.total || 0)
|
||||
setTotal(data?.total || 0);
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const deleteKeyMutation = useMutation((id: number) => deleteToken(id), {
|
||||
onSuccess() {
|
||||
queryClient.invalidateQueries([QueryKey.GetTokens])
|
||||
queryClient.invalidateQueries([QueryKey.GetTokens]);
|
||||
message({
|
||||
status: 'success',
|
||||
title: t('key.deleteSuccess'),
|
||||
isClosable: true,
|
||||
duration: 2000,
|
||||
position: 'top',
|
||||
})
|
||||
});
|
||||
},
|
||||
onError(err: any) {
|
||||
message({
|
||||
@@ -176,22 +176,22 @@ const ModelKeyTable = ({ t, onOpen }: { t: TFunction; onOpen: () => void }) => {
|
||||
description: err?.message || t('key.deleteFailed'),
|
||||
isClosable: true,
|
||||
position: 'top',
|
||||
})
|
||||
});
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const updateKeyMutation = useMutation(
|
||||
({ id, status }: { id: number; status: number }) => updateToken(id, status),
|
||||
{
|
||||
onSuccess() {
|
||||
queryClient.invalidateQueries([QueryKey.GetTokens])
|
||||
queryClient.invalidateQueries([QueryKey.GetTokens]);
|
||||
message({
|
||||
status: 'success',
|
||||
title: t('key.updateSuccess'),
|
||||
isClosable: true,
|
||||
duration: 2000,
|
||||
position: 'top',
|
||||
})
|
||||
});
|
||||
},
|
||||
onError(err: any) {
|
||||
message({
|
||||
@@ -200,22 +200,22 @@ const ModelKeyTable = ({ t, onOpen }: { t: TFunction; onOpen: () => void }) => {
|
||||
description: err?.message ? t(err.message) : t('key.updateFailed'),
|
||||
isClosable: true,
|
||||
position: 'top',
|
||||
})
|
||||
});
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Update the button click handlers in the table actions column:
|
||||
const handleStatusUpdate = (id: number, currentStatus: number) => {
|
||||
const newStatus = currentStatus === KeyStatus.DISABLED ? KeyStatus.ENABLED : KeyStatus.DISABLED
|
||||
updateKeyMutation.mutate({ id, status: newStatus })
|
||||
}
|
||||
const newStatus = currentStatus === KeyStatus.DISABLED ? KeyStatus.ENABLED : KeyStatus.DISABLED;
|
||||
updateKeyMutation.mutate({ id, status: newStatus });
|
||||
};
|
||||
|
||||
const handleDelete = (id: number) => {
|
||||
deleteKeyMutation.mutate(id)
|
||||
}
|
||||
deleteKeyMutation.mutate(id);
|
||||
};
|
||||
|
||||
const columnHelper = createColumnHelper<TokenInfo>()
|
||||
const columnHelper = createColumnHelper<TokenInfo>();
|
||||
|
||||
const columns = [
|
||||
columnHelper.accessor((row) => row.name, {
|
||||
@@ -247,7 +247,7 @@ const ModelKeyTable = ({ t, onOpen }: { t: TFunction; onOpen: () => void }) => {
|
||||
letterSpacing="0.5px"
|
||||
cursor="pointer"
|
||||
onClick={() => {
|
||||
const key = 'sk-' + info.getValue()
|
||||
const key = 'sk-' + info.getValue();
|
||||
navigator.clipboard.writeText(key).then(
|
||||
() => {
|
||||
message({
|
||||
@@ -256,7 +256,7 @@ const ModelKeyTable = ({ t, onOpen }: { t: TFunction; onOpen: () => void }) => {
|
||||
isClosable: true,
|
||||
duration: 2000,
|
||||
position: 'top',
|
||||
})
|
||||
});
|
||||
},
|
||||
(err) => {
|
||||
message({
|
||||
@@ -265,9 +265,9 @@ const ModelKeyTable = ({ t, onOpen }: { t: TFunction; onOpen: () => void }) => {
|
||||
description: err?.message || t('copyFailed'),
|
||||
isClosable: true,
|
||||
position: 'top',
|
||||
})
|
||||
});
|
||||
}
|
||||
)
|
||||
);
|
||||
}}
|
||||
>
|
||||
<MyTooltip label={t('copy')}>
|
||||
@@ -296,7 +296,7 @@ const ModelKeyTable = ({ t, onOpen }: { t: TFunction; onOpen: () => void }) => {
|
||||
id: TableHeaderId.LAST_USED_AT,
|
||||
header: (props) => <CustomHeader column={props.column} t={t} />,
|
||||
cell: (info) => {
|
||||
const timestamp = info.getValue()
|
||||
const timestamp = info.getValue();
|
||||
|
||||
if (timestamp && timestamp < 0) {
|
||||
return (
|
||||
@@ -310,7 +310,7 @@ const ModelKeyTable = ({ t, onOpen }: { t: TFunction; onOpen: () => void }) => {
|
||||
>
|
||||
{t('key.unused')}
|
||||
</Text>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -324,37 +324,37 @@ const ModelKeyTable = ({ t, onOpen }: { t: TFunction; onOpen: () => void }) => {
|
||||
>
|
||||
{new Date(timestamp).toLocaleString()}
|
||||
</Text>
|
||||
)
|
||||
);
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor((row) => row.status, {
|
||||
id: TableHeaderId.STATUS,
|
||||
header: (props) => <CustomHeader column={props.column} t={t} />,
|
||||
cell: (info) => {
|
||||
const status = info.getValue()
|
||||
let statusText = ''
|
||||
let statusColor = ''
|
||||
const status = info.getValue();
|
||||
let statusText = '';
|
||||
let statusColor = '';
|
||||
|
||||
switch (status) {
|
||||
case KeyStatus.ENABLED:
|
||||
statusText = t('keystatus.enabled')
|
||||
statusColor = 'green.600'
|
||||
break
|
||||
statusText = t('keystatus.enabled');
|
||||
statusColor = 'green.600';
|
||||
break;
|
||||
case KeyStatus.DISABLED:
|
||||
statusText = t('keystatus.disabled')
|
||||
statusColor = 'red.600'
|
||||
break
|
||||
statusText = t('keystatus.disabled');
|
||||
statusColor = 'red.600';
|
||||
break;
|
||||
case KeyStatus.EXPIRED:
|
||||
statusText = t('keystatus.expired')
|
||||
statusColor = 'orange.500'
|
||||
break
|
||||
statusText = t('keystatus.expired');
|
||||
statusColor = 'orange.500';
|
||||
break;
|
||||
case KeyStatus.EXHAUSTED:
|
||||
statusText = t('keystatus.exhausted')
|
||||
statusColor = 'gray.500'
|
||||
break
|
||||
statusText = t('keystatus.exhausted');
|
||||
statusColor = 'gray.500';
|
||||
break;
|
||||
default:
|
||||
statusText = t('keystatus.unknown')
|
||||
statusColor = 'gray.500'
|
||||
statusText = t('keystatus.unknown');
|
||||
statusColor = 'gray.500';
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -368,7 +368,7 @@ const ModelKeyTable = ({ t, onOpen }: { t: TFunction; onOpen: () => void }) => {
|
||||
>
|
||||
{statusText}
|
||||
</Text>
|
||||
)
|
||||
);
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -393,11 +393,11 @@ const ModelKeyTable = ({ t, onOpen }: { t: TFunction; onOpen: () => void }) => {
|
||||
id: TableHeaderId.USED_AMOUNT,
|
||||
header: (props) => <CustomHeader column={props.column} t={t} />,
|
||||
cell: (info) => {
|
||||
const value = Number(info.getValue())
|
||||
const value = Number(info.getValue());
|
||||
// 获取小数部分的长度
|
||||
const decimalLength = value.toString().split('.')[1]?.length || 0
|
||||
const decimalLength = value.toString().split('.')[1]?.length || 0;
|
||||
// 如果小数位超过6位则保留6位,否则保持原样
|
||||
const formattedValue = decimalLength > 6 ? value.toFixed(6) : value
|
||||
const formattedValue = decimalLength > 6 ? value.toFixed(6) : value;
|
||||
|
||||
return (
|
||||
<Text
|
||||
@@ -410,7 +410,7 @@ const ModelKeyTable = ({ t, onOpen }: { t: TFunction; onOpen: () => void }) => {
|
||||
>
|
||||
{formattedValue}
|
||||
</Text>
|
||||
)
|
||||
);
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -485,8 +485,8 @@ const ModelKeyTable = ({ t, onOpen }: { t: TFunction; onOpen: () => void }) => {
|
||||
color: 'brightBlue.600',
|
||||
}}
|
||||
onClick={() => {
|
||||
handleStatusUpdate(info.row.original.id, info.row.original.status)
|
||||
setOpenPopoverId(null)
|
||||
handleStatusUpdate(info.row.original.id, info.row.original.status);
|
||||
setOpenPopoverId(null);
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
@@ -532,8 +532,8 @@ const ModelKeyTable = ({ t, onOpen }: { t: TFunction; onOpen: () => void }) => {
|
||||
color: 'brightBlue.600',
|
||||
}}
|
||||
onClick={() => {
|
||||
setOpenPopoverId(null)
|
||||
handleStatusUpdate(info.row.original.id, info.row.original.status)
|
||||
setOpenPopoverId(null);
|
||||
handleStatusUpdate(info.row.original.id, info.row.original.status);
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
@@ -579,8 +579,8 @@ const ModelKeyTable = ({ t, onOpen }: { t: TFunction; onOpen: () => void }) => {
|
||||
color: '#D92D20',
|
||||
}}
|
||||
onClick={() => {
|
||||
handleDelete(info.row.original.id)
|
||||
setOpenPopoverId(null)
|
||||
handleDelete(info.row.original.id);
|
||||
setOpenPopoverId(null);
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
@@ -614,19 +614,19 @@ const ModelKeyTable = ({ t, onOpen }: { t: TFunction; onOpen: () => void }) => {
|
||||
</Popover>
|
||||
),
|
||||
}),
|
||||
]
|
||||
];
|
||||
|
||||
const sortTableData = (data: TokenInfo[]) => {
|
||||
return data.sort((a, b) => b.created_at - a.created_at)
|
||||
}
|
||||
return data.sort((a, b) => b.created_at - a.created_at);
|
||||
};
|
||||
|
||||
const sortedData = useMemo(() => sortTableData(data?.tokens || []), [data])
|
||||
const sortedData = useMemo(() => sortTableData(data?.tokens || []), [data]);
|
||||
|
||||
const table = useReactTable({
|
||||
data: sortedData,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
})
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -767,7 +767,7 @@ const ModelKeyTable = ({ t, onOpen }: { t: TFunction; onOpen: () => void }) => {
|
||||
cursor="pointer"
|
||||
whiteSpace="nowrap"
|
||||
onClick={() => {
|
||||
const endpoint = aiproxyBackend
|
||||
const endpoint = aiproxyBackend;
|
||||
navigator.clipboard.writeText(endpoint).then(
|
||||
() => {
|
||||
message({
|
||||
@@ -776,7 +776,7 @@ const ModelKeyTable = ({ t, onOpen }: { t: TFunction; onOpen: () => void }) => {
|
||||
isClosable: true,
|
||||
duration: 2000,
|
||||
position: 'top',
|
||||
})
|
||||
});
|
||||
},
|
||||
(err) => {
|
||||
message({
|
||||
@@ -785,9 +785,9 @@ const ModelKeyTable = ({ t, onOpen }: { t: TFunction; onOpen: () => void }) => {
|
||||
description: err?.message || t('copyFailed'),
|
||||
isClosable: true,
|
||||
position: 'top',
|
||||
})
|
||||
});
|
||||
}
|
||||
)
|
||||
);
|
||||
}}
|
||||
>
|
||||
{aiproxyBackend}
|
||||
@@ -857,10 +857,10 @@ const ModelKeyTable = ({ t, onOpen }: { t: TFunction; onOpen: () => void }) => {
|
||||
>
|
||||
{flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</Th>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</Tr>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</Thead>
|
||||
<Tbody>
|
||||
@@ -878,10 +878,10 @@ const ModelKeyTable = ({ t, onOpen }: { t: TFunction; onOpen: () => void }) => {
|
||||
<Td key={cell.id}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</Td>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</Tr>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</Tbody>
|
||||
</Table>
|
||||
@@ -900,22 +900,22 @@ const ModelKeyTable = ({ t, onOpen }: { t: TFunction; onOpen: () => void }) => {
|
||||
</Flex>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
function CreateKeyModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
t,
|
||||
}: {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
t: TFunction
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
t: TFunction;
|
||||
}) {
|
||||
const initialRef = React.useRef(null)
|
||||
const [name, setName] = useState('')
|
||||
const [error, setError] = useState('')
|
||||
const queryClient = useQueryClient()
|
||||
const initialRef = React.useRef(null);
|
||||
const [name, setName] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const queryClient = useQueryClient();
|
||||
const { message } = useMessage({
|
||||
warningBoxBg: '#FFFAEB',
|
||||
warningIconBg: '#F79009',
|
||||
@@ -924,58 +924,58 @@ function CreateKeyModal({
|
||||
successBoxBg: '#EDFBF3',
|
||||
successIconBg: '#039855',
|
||||
successIconFill: 'white',
|
||||
})
|
||||
});
|
||||
|
||||
const createKeyMutation = useMutation((name: string) => createToken(name), {
|
||||
onSuccess(data) {
|
||||
createKeyMutation.reset()
|
||||
setName('')
|
||||
queryClient.invalidateQueries([QueryKey.GetTokens])
|
||||
createKeyMutation.reset();
|
||||
setName('');
|
||||
queryClient.invalidateQueries([QueryKey.GetTokens]);
|
||||
message({
|
||||
status: 'success',
|
||||
title: t('key.createSuccess'),
|
||||
isClosable: true,
|
||||
duration: 2000,
|
||||
position: 'top',
|
||||
})
|
||||
onClose()
|
||||
});
|
||||
onClose();
|
||||
},
|
||||
onError(err) {
|
||||
console.error(err)
|
||||
console.error(err);
|
||||
message({
|
||||
status: 'warning',
|
||||
title: t('key.createFailed'),
|
||||
description: err instanceof Error ? t(err.message as any) : t('key.createFailed'),
|
||||
isClosable: true,
|
||||
position: 'top',
|
||||
})
|
||||
});
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const validateName = (value: string) => {
|
||||
if (!value) {
|
||||
setError(t('key.nameRequired'))
|
||||
setError(t('key.nameRequired'));
|
||||
} else if (value.length >= 32) {
|
||||
setError(t('key.nameMaxLength'))
|
||||
setError(t('key.nameMaxLength'));
|
||||
} else if (!/^[A-Za-z0-9-]+$/.test(value)) {
|
||||
setError(t('key.nameOnlyLettersAndNumbers'))
|
||||
setError(t('key.nameOnlyLettersAndNumbers'));
|
||||
} else {
|
||||
setError('')
|
||||
setError('');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newName = e.target.value
|
||||
validateName(newName)
|
||||
setName(newName)
|
||||
}
|
||||
const newName = e.target.value;
|
||||
validateName(newName);
|
||||
setName(newName);
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (error === '' && name !== '') {
|
||||
createKeyMutation.mutate(name)
|
||||
return
|
||||
createKeyMutation.mutate(name);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} isCentered initialFocusRef={initialRef}>
|
||||
@@ -1098,7 +1098,7 @@ function CreateKeyModal({
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Modal>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default KeyList
|
||||
export default KeyList;
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
'use client'
|
||||
import { Button, Flex, Text } from '@chakra-ui/react'
|
||||
import Image, { StaticImageData } from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
'use client';
|
||||
import { Button, Flex, Text } from '@chakra-ui/react';
|
||||
import Image, { StaticImageData } from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
import { useTranslationClientSide } from '@/app/i18n/client'
|
||||
import { useI18n } from '@/providers/i18n/i18nContext'
|
||||
import { useBackendStore } from '@/store/backend'
|
||||
import homeIcon from '@/ui/svg/icons/sidebar/home.svg'
|
||||
import homeIcon_a from '@/ui/svg/icons/sidebar/home_a.svg'
|
||||
import keysIcon from '@/ui/svg/icons/sidebar/key.svg'
|
||||
import keysIcon_a from '@/ui/svg/icons/sidebar/key_a.svg'
|
||||
import logsIcon from '@/ui/svg/icons/sidebar/logs.svg'
|
||||
import logsIcon_a from '@/ui/svg/icons/sidebar/logs_a.svg'
|
||||
import mcpIcon from '@/ui/svg/icons/sidebar/mcp.svg'
|
||||
import mcpIcon_a from '@/ui/svg/icons/sidebar/mcp_a.svg'
|
||||
import priceIcon from '@/ui/svg/icons/sidebar/price.svg'
|
||||
import priceIcon_a from '@/ui/svg/icons/sidebar/price_a.svg'
|
||||
import { useTranslationClientSide } from '@/app/i18n/client';
|
||||
import { useI18n } from '@/providers/i18n/i18nContext';
|
||||
import { useBackendStore } from '@/store/backend';
|
||||
import homeIcon from '@/ui/svg/icons/sidebar/home.svg';
|
||||
import homeIcon_a from '@/ui/svg/icons/sidebar/home_a.svg';
|
||||
import keysIcon from '@/ui/svg/icons/sidebar/key.svg';
|
||||
import keysIcon_a from '@/ui/svg/icons/sidebar/key_a.svg';
|
||||
import logsIcon from '@/ui/svg/icons/sidebar/logs.svg';
|
||||
import logsIcon_a from '@/ui/svg/icons/sidebar/logs_a.svg';
|
||||
import mcpIcon from '@/ui/svg/icons/sidebar/mcp.svg';
|
||||
import mcpIcon_a from '@/ui/svg/icons/sidebar/mcp_a.svg';
|
||||
import priceIcon from '@/ui/svg/icons/sidebar/price.svg';
|
||||
import priceIcon_a from '@/ui/svg/icons/sidebar/price_a.svg';
|
||||
|
||||
type Menu = {
|
||||
id: string
|
||||
url: string
|
||||
value: string
|
||||
icon: StaticImageData
|
||||
activeIcon: StaticImageData
|
||||
display: boolean
|
||||
}
|
||||
id: string;
|
||||
url: string;
|
||||
value: string;
|
||||
icon: StaticImageData;
|
||||
activeIcon: StaticImageData;
|
||||
display: boolean;
|
||||
};
|
||||
|
||||
const SideBar = (): JSX.Element => {
|
||||
const pathname = usePathname()
|
||||
const { lng } = useI18n()
|
||||
const { t } = useTranslationClientSide(lng, 'common')
|
||||
const pathname = usePathname();
|
||||
const { lng } = useI18n();
|
||||
const { t } = useTranslationClientSide(lng, 'common');
|
||||
|
||||
const { invitationUrl, docUrl } = useBackendStore()
|
||||
const { invitationUrl, docUrl } = useBackendStore();
|
||||
|
||||
const menus: Menu[] = [
|
||||
{
|
||||
@@ -75,7 +75,7 @@ const SideBar = (): JSX.Element => {
|
||||
activeIcon: mcpIcon_a,
|
||||
display: true,
|
||||
},
|
||||
]
|
||||
];
|
||||
|
||||
return (
|
||||
<Flex
|
||||
@@ -92,8 +92,8 @@ const SideBar = (): JSX.Element => {
|
||||
{menus
|
||||
.filter((menu) => menu.display)
|
||||
.map((menu) => {
|
||||
const fullUrl = `/${lng}${menu.url}`
|
||||
const isActive = pathname === fullUrl || pathname.startsWith(fullUrl + '/')
|
||||
const fullUrl = `/${lng}${menu.url}`;
|
||||
const isActive = pathname === fullUrl || pathname.startsWith(fullUrl + '/');
|
||||
|
||||
return (
|
||||
<Link href={fullUrl} key={menu.id} style={{ textDecoration: 'none' }}>
|
||||
@@ -132,7 +132,7 @@ const SideBar = (): JSX.Element => {
|
||||
</Text>
|
||||
</Flex>
|
||||
</Link>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
{/* doc */}
|
||||
@@ -154,7 +154,7 @@ const SideBar = (): JSX.Element => {
|
||||
bg="rgba(150, 153, 180, 0.10)"
|
||||
_hover={{ bg: 'rgba(150, 153, 180, 0.15)' }}
|
||||
onClick={() => {
|
||||
window.open(docUrl, '_blank')
|
||||
window.open(docUrl, '_blank');
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
@@ -195,7 +195,7 @@ const SideBar = (): JSX.Element => {
|
||||
bg="rgba(150, 153, 180, 0.10)"
|
||||
_hover={{ bg: 'rgba(150, 153, 180, 0.15)' }}
|
||||
onClick={() => {
|
||||
window.open(invitationUrl, '_blank')
|
||||
window.open(invitationUrl, '_blank');
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
@@ -227,7 +227,7 @@ const SideBar = (): JSX.Element => {
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default SideBar
|
||||
export default SideBar;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
'use client'
|
||||
'use client';
|
||||
|
||||
import { ReactNode } from 'react'
|
||||
import { ChakraProvider } from '@chakra-ui/react'
|
||||
import { ReactNode } from 'react';
|
||||
import { ChakraProvider } from '@chakra-ui/react';
|
||||
|
||||
import { theme } from '@/ui/chakraTheme'
|
||||
import { theme } from '@/ui/chakraTheme';
|
||||
|
||||
export function ChakraProviders({ children }: { children: ReactNode }): JSX.Element {
|
||||
return <ChakraProvider theme={theme}>{children}</ChakraProvider>
|
||||
return <ChakraProvider theme={theme}>{children}</ChakraProvider>;
|
||||
}
|
||||
|
||||
export default ChakraProviders
|
||||
export default ChakraProviders;
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
'use client'
|
||||
import { createContext, useContext } from 'react'
|
||||
'use client';
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
const I18nContext = createContext<{ lng: string }>({ lng: 'en' })
|
||||
const I18nContext = createContext<{ lng: string }>({ lng: 'en' });
|
||||
|
||||
export const useI18n = (): { lng: string } => useContext(I18nContext)
|
||||
export const useI18n = (): { lng: string } => useContext(I18nContext);
|
||||
|
||||
export function I18nProvider({
|
||||
children,
|
||||
lng,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
lng: string
|
||||
children: React.ReactNode;
|
||||
lng: string;
|
||||
}): JSX.Element {
|
||||
return <I18nContext.Provider value={{ lng }}>{children}</I18nContext.Provider>
|
||||
return <I18nContext.Provider value={{ lng }}>{children}</I18nContext.Provider>;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { useState } from 'react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
|
||||
const QueryProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [queryClient] = useState(
|
||||
@@ -15,9 +15,9 @@ const QueryProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
}
|
||||
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
|
||||
};
|
||||
|
||||
export default QueryProvider
|
||||
export default QueryProvider;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Icon, IconProps } from '@chakra-ui/react'
|
||||
import { Icon, IconProps } from '@chakra-ui/react';
|
||||
|
||||
export function ToLeftIcon(props: IconProps) {
|
||||
return (
|
||||
@@ -15,7 +15,7 @@ export function ToLeftIcon(props: IconProps) {
|
||||
fill="#111824"
|
||||
/>
|
||||
</Icon>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function RightFirstIcon(props: IconProps) {
|
||||
@@ -33,7 +33,7 @@ export function RightFirstIcon(props: IconProps) {
|
||||
fill="#111824"
|
||||
/>
|
||||
</Icon>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export const ChainIcon = (props: IconProps) => (
|
||||
@@ -45,7 +45,7 @@ export const ChainIcon = (props: IconProps) => (
|
||||
fill="currentColor"
|
||||
/>
|
||||
</Icon>
|
||||
)
|
||||
);
|
||||
|
||||
export const ListIcon = (props: IconProps) => (
|
||||
<Icon viewBox="0 0 18 18" width="18" height="18" {...props}>
|
||||
@@ -74,4 +74,4 @@ export const ListIcon = (props: IconProps) => (
|
||||
fill="currentColor"
|
||||
/>
|
||||
</Icon>
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1,53 +1,54 @@
|
||||
import { StaticImageData } from 'next/image'
|
||||
import DefaultIcon from '@/ui/svg/icons/mcp/default.svg'
|
||||
import alipayIcon from '@/ui/svg/icons/mcp/alipay.svg'
|
||||
import baiduMapIcon from '@/ui/svg/icons/mcp/baiduMap.svg'
|
||||
import one2306Icon from '@/ui/svg/icons/mcp/12306.svg'
|
||||
import bingIcon from '@/ui/svg/icons/mcp/bing.svg'
|
||||
import amapIcon from '@/ui/svg/icons/mcp/amap.svg'
|
||||
import searchIcon from '@/ui/svg/icons/mcp/search.svg'
|
||||
import firecrawlIcon from '@/ui/svg/icons/mcp/firecrawl.svg'
|
||||
import flomoIcon from '@/ui/svg/icons/mcp/flomo.svg'
|
||||
import vis from '@/ui/svg/icons/mcp/vis.svg'
|
||||
import hefengIcon from '@/ui/svg/icons/mcp/hefeng.svg'
|
||||
import gezheIcon from '@/ui/svg/icons/mcp/gezhe.svg'
|
||||
import tavilyIcon from '@/ui/svg/icons/mcp/tavily.svg'
|
||||
import jinaIcon from '@/ui/svg/icons/mcp/jina.svg'
|
||||
import howtocookIcon from '@/ui/svg/icons/mcp/howtocook.svg'
|
||||
import timeIcon from '@/ui/svg/icons/mcp/time.svg'
|
||||
import blenderIcon from '@/ui/svg/icons/mcp/blender.svg'
|
||||
import browserbaseIcon from '@/ui/svg/icons/mcp/browserbase.svg'
|
||||
import excelIcon from '@/ui/svg/icons/mcp/excel.svg'
|
||||
import magicIcon from '@/ui/svg/icons/mcp/21dev.png'
|
||||
import alibabacloudDmsIcon from '@/ui/svg/icons/mcp/alibabacloud-dms.svg'
|
||||
import academicSearchIcon from '@/ui/svg/icons/mcp/academic-search.svg'
|
||||
import allvoicelabIcon from '@/ui/svg/icons/mcp/allvoicelab.png'
|
||||
import cfworkerIcon from '@/ui/svg/icons/mcp/cfworker.svg'
|
||||
import elasticsearchIcon from '@/ui/svg/icons/mcp/elasticsearch.svg'
|
||||
import everythingSearchIcon from '@/ui/svg/icons/mcp/everything-search.svg'
|
||||
import filesystemIcon from '@/ui/svg/icons/mcp/filesystem.svg'
|
||||
import gitIcon from '@/ui/svg/icons/mcp/git.svg'
|
||||
import knowledgeGraphMemoryIcon from '@/ui/svg/icons/mcp/knowledge-graph-memory.svg'
|
||||
import mysqlIcon from '@/ui/svg/icons/mcp/mysql.svg'
|
||||
import oceanbaseIcon from '@/ui/svg/icons/mcp/oceanbase.svg'
|
||||
import openmemoryIcon from '@/ui/svg/icons/mcp/openmemory.svg'
|
||||
import officeWordIcon from '@/ui/svg/icons/mcp/office-word.svg'
|
||||
import redbookSearchCommentIcon from '@/ui/svg/icons/mcp/redbook-search-comment.svg'
|
||||
import puppeteerIcon from '@/ui/svg/icons/mcp/puppeteer.svg'
|
||||
import playwrightIcon from '@/ui/svg/icons/mcp/playwright.svg'
|
||||
import markdownifyIcon from '@/ui/svg/icons/mcp/markdownify.svg'
|
||||
import redisIcon from '@/ui/svg/icons/mcp/redis.svg'
|
||||
import sqliteIcon from '@/ui/svg/icons/mcp/sqlite.svg'
|
||||
import stockAnalysisIcon from '@/ui/svg/icons/mcp/stock-analysis.svg'
|
||||
import queryTableIcon from '@/ui/svg/icons/mcp/query-table.svg'
|
||||
import unionpayIcon from '@/ui/svg/icons/mcp/unionpay.svg'
|
||||
import wecomBotIcon from '@/ui/svg/icons/mcp/wecom-bot.svg'
|
||||
import chatIcon from '@/ui/svg/icons/mcp/chat.svg'
|
||||
import webcontentIcon from '@/ui/svg/icons/mcp/webcontent.svg'
|
||||
import githubIcon from '@/ui/svg/icons/mcp/github.svg'
|
||||
import notionIcon from '@/ui/svg/icons/mcp/notion.svg'
|
||||
import zhipuaiIcon from '@/ui/svg/icons/mcp/zhipuai.svg'
|
||||
import weatherIcon from '@/ui/svg/icons/mcp/weather.svg'
|
||||
import { StaticImageData } from 'next/image';
|
||||
|
||||
import magicIcon from '@/ui/svg/icons/mcp/21dev.png';
|
||||
import one2306Icon from '@/ui/svg/icons/mcp/12306.svg';
|
||||
import academicSearchIcon from '@/ui/svg/icons/mcp/academic-search.svg';
|
||||
import alibabacloudDmsIcon from '@/ui/svg/icons/mcp/alibabacloud-dms.svg';
|
||||
import alipayIcon from '@/ui/svg/icons/mcp/alipay.svg';
|
||||
import allvoicelabIcon from '@/ui/svg/icons/mcp/allvoicelab.png';
|
||||
import amapIcon from '@/ui/svg/icons/mcp/amap.svg';
|
||||
import baiduMapIcon from '@/ui/svg/icons/mcp/baiduMap.svg';
|
||||
import bingIcon from '@/ui/svg/icons/mcp/bing.svg';
|
||||
import blenderIcon from '@/ui/svg/icons/mcp/blender.svg';
|
||||
import browserbaseIcon from '@/ui/svg/icons/mcp/browserbase.svg';
|
||||
import cfworkerIcon from '@/ui/svg/icons/mcp/cfworker.svg';
|
||||
import chatIcon from '@/ui/svg/icons/mcp/chat.svg';
|
||||
import DefaultIcon from '@/ui/svg/icons/mcp/default.svg';
|
||||
import elasticsearchIcon from '@/ui/svg/icons/mcp/elasticsearch.svg';
|
||||
import everythingSearchIcon from '@/ui/svg/icons/mcp/everything-search.svg';
|
||||
import excelIcon from '@/ui/svg/icons/mcp/excel.svg';
|
||||
import filesystemIcon from '@/ui/svg/icons/mcp/filesystem.svg';
|
||||
import firecrawlIcon from '@/ui/svg/icons/mcp/firecrawl.svg';
|
||||
import flomoIcon from '@/ui/svg/icons/mcp/flomo.svg';
|
||||
import gezheIcon from '@/ui/svg/icons/mcp/gezhe.svg';
|
||||
import gitIcon from '@/ui/svg/icons/mcp/git.svg';
|
||||
import githubIcon from '@/ui/svg/icons/mcp/github.svg';
|
||||
import hefengIcon from '@/ui/svg/icons/mcp/hefeng.svg';
|
||||
import howtocookIcon from '@/ui/svg/icons/mcp/howtocook.svg';
|
||||
import jinaIcon from '@/ui/svg/icons/mcp/jina.svg';
|
||||
import knowledgeGraphMemoryIcon from '@/ui/svg/icons/mcp/knowledge-graph-memory.svg';
|
||||
import markdownifyIcon from '@/ui/svg/icons/mcp/markdownify.svg';
|
||||
import mysqlIcon from '@/ui/svg/icons/mcp/mysql.svg';
|
||||
import notionIcon from '@/ui/svg/icons/mcp/notion.svg';
|
||||
import oceanbaseIcon from '@/ui/svg/icons/mcp/oceanbase.svg';
|
||||
import officeWordIcon from '@/ui/svg/icons/mcp/office-word.svg';
|
||||
import openmemoryIcon from '@/ui/svg/icons/mcp/openmemory.svg';
|
||||
import playwrightIcon from '@/ui/svg/icons/mcp/playwright.svg';
|
||||
import puppeteerIcon from '@/ui/svg/icons/mcp/puppeteer.svg';
|
||||
import queryTableIcon from '@/ui/svg/icons/mcp/query-table.svg';
|
||||
import redbookSearchCommentIcon from '@/ui/svg/icons/mcp/redbook-search-comment.svg';
|
||||
import redisIcon from '@/ui/svg/icons/mcp/redis.svg';
|
||||
import searchIcon from '@/ui/svg/icons/mcp/search.svg';
|
||||
import sqliteIcon from '@/ui/svg/icons/mcp/sqlite.svg';
|
||||
import stockAnalysisIcon from '@/ui/svg/icons/mcp/stock-analysis.svg';
|
||||
import tavilyIcon from '@/ui/svg/icons/mcp/tavily.svg';
|
||||
import timeIcon from '@/ui/svg/icons/mcp/time.svg';
|
||||
import unionpayIcon from '@/ui/svg/icons/mcp/unionpay.svg';
|
||||
import vis from '@/ui/svg/icons/mcp/vis.svg';
|
||||
import weatherIcon from '@/ui/svg/icons/mcp/weather.svg';
|
||||
import webcontentIcon from '@/ui/svg/icons/mcp/webcontent.svg';
|
||||
import wecomBotIcon from '@/ui/svg/icons/mcp/wecom-bot.svg';
|
||||
import zhipuaiIcon from '@/ui/svg/icons/mcp/zhipuai.svg';
|
||||
|
||||
export const mcpIcons = {
|
||||
default: DefaultIcon,
|
||||
@@ -100,9 +101,9 @@ export const mcpIcons = {
|
||||
notion: notionIcon,
|
||||
zhipuai: zhipuaiIcon,
|
||||
weather: weatherIcon,
|
||||
}
|
||||
};
|
||||
|
||||
export const getMcpIcon = (mcpId: string): StaticImageData => {
|
||||
const icon = mcpIcons[mcpId as keyof typeof mcpIcons] || mcpIcons['default']
|
||||
return icon
|
||||
}
|
||||
const icon = mcpIcons[mcpId as keyof typeof mcpIcons] || mcpIcons['default'];
|
||||
return icon;
|
||||
};
|
||||
|
||||
@@ -1,36 +1,35 @@
|
||||
import OpenAIIcon from '@/ui/svg/icons/modelist/openai.svg'
|
||||
import QwenIcon from '@/ui/svg/icons/modelist/qianwen.svg'
|
||||
import ChatglmIcon from '@/ui/svg/icons/modelist/chatglm.svg'
|
||||
import DeepseekIcon from '@/ui/svg/icons/modelist/deepseek.svg'
|
||||
import MoonshotIcon from '@/ui/svg/icons/modelist/moonshot.svg'
|
||||
import SparkdeskIcon from '@/ui/svg/icons/modelist/sparkdesk.svg'
|
||||
import DoubaoIcon from '@/ui/svg/icons/modelist/doubao.svg'
|
||||
import BaaiIcon from '@/ui/svg/icons/modelist/baai.svg'
|
||||
import HunyuanIcon from '@/ui/svg/icons/modelist/hunyuan.svg'
|
||||
import MiniMaxIcon from '@/ui/svg/icons/modelist/minimax.svg'
|
||||
import BaiduIcon from '@/ui/svg/icons/modelist/baidu.svg'
|
||||
import GoogleGeminiIcon from '@/ui/svg/icons/modelist/google.svg'
|
||||
import AlibabaIcon from '@/ui/svg/icons/modelist/alibaba.svg'
|
||||
import FishAudioIcon from '@/ui/svg/icons/modelist/fishaudio.svg'
|
||||
import StabilityAIIcon from '@/ui/svg/icons/modelist/stabilityai.svg'
|
||||
import NeteaseIcon from '@/ui/svg/icons/modelist/netease.svg'
|
||||
import AI360Icon from '@/ui/svg/icons/modelist/ai360.svg'
|
||||
import AnthropicIcon from '@/ui/svg/icons/modelist/anthropic.svg'
|
||||
import BaichuanIcon from '@/ui/svg/icons/modelist/baichuan.svg'
|
||||
import MetaIcon from '@/ui/svg/icons/modelist/meta.svg'
|
||||
import MistralIcon from '@/ui/svg/icons/modelist/mistral.svg'
|
||||
import OpenChatIcon from '@/ui/svg/icons/modelist/openchat.svg'
|
||||
import DefogIcon from '@/ui/svg/icons/modelist/defog.svg'
|
||||
import NexusFlowIcon from '@/ui/svg/icons/modelist/nexusflow.svg'
|
||||
import MicrosoftIcon from '@/ui/svg/icons/modelist/microsoft.svg'
|
||||
import CohereIcon from '@/ui/svg/icons/modelist/cohere.svg'
|
||||
import HuggingFaceIcon from '@/ui/svg/icons/modelist/huggingface.svg'
|
||||
import LingyiWanwuIcon from '@/ui/svg/icons/modelist/lingyiwanwu.svg'
|
||||
import StepFunIcon from '@/ui/svg/icons/modelist/stepfun.svg'
|
||||
import DefaultIcon from '@/ui/svg/icons/modelist/default.svg'
|
||||
|
||||
import AbabIcon from '@/ui/svg/icons/modelist/minimax.svg'
|
||||
import ErnieIcon from '@/ui/svg/icons/modelist/ernie.svg'
|
||||
import AI360Icon from '@/ui/svg/icons/modelist/ai360.svg';
|
||||
import AlibabaIcon from '@/ui/svg/icons/modelist/alibaba.svg';
|
||||
import AnthropicIcon from '@/ui/svg/icons/modelist/anthropic.svg';
|
||||
import BaaiIcon from '@/ui/svg/icons/modelist/baai.svg';
|
||||
import BaichuanIcon from '@/ui/svg/icons/modelist/baichuan.svg';
|
||||
import BaiduIcon from '@/ui/svg/icons/modelist/baidu.svg';
|
||||
import ChatglmIcon from '@/ui/svg/icons/modelist/chatglm.svg';
|
||||
import CohereIcon from '@/ui/svg/icons/modelist/cohere.svg';
|
||||
import DeepseekIcon from '@/ui/svg/icons/modelist/deepseek.svg';
|
||||
import DefaultIcon from '@/ui/svg/icons/modelist/default.svg';
|
||||
import DefogIcon from '@/ui/svg/icons/modelist/defog.svg';
|
||||
import DoubaoIcon from '@/ui/svg/icons/modelist/doubao.svg';
|
||||
import ErnieIcon from '@/ui/svg/icons/modelist/ernie.svg';
|
||||
import FishAudioIcon from '@/ui/svg/icons/modelist/fishaudio.svg';
|
||||
import GoogleGeminiIcon from '@/ui/svg/icons/modelist/google.svg';
|
||||
import HuggingFaceIcon from '@/ui/svg/icons/modelist/huggingface.svg';
|
||||
import HunyuanIcon from '@/ui/svg/icons/modelist/hunyuan.svg';
|
||||
import LingyiWanwuIcon from '@/ui/svg/icons/modelist/lingyiwanwu.svg';
|
||||
import MetaIcon from '@/ui/svg/icons/modelist/meta.svg';
|
||||
import MicrosoftIcon from '@/ui/svg/icons/modelist/microsoft.svg';
|
||||
import MiniMaxIcon from '@/ui/svg/icons/modelist/minimax.svg';
|
||||
import AbabIcon from '@/ui/svg/icons/modelist/minimax.svg';
|
||||
import MistralIcon from '@/ui/svg/icons/modelist/mistral.svg';
|
||||
import MoonshotIcon from '@/ui/svg/icons/modelist/moonshot.svg';
|
||||
import NeteaseIcon from '@/ui/svg/icons/modelist/netease.svg';
|
||||
import NexusFlowIcon from '@/ui/svg/icons/modelist/nexusflow.svg';
|
||||
import OpenAIIcon from '@/ui/svg/icons/modelist/openai.svg';
|
||||
import OpenChatIcon from '@/ui/svg/icons/modelist/openchat.svg';
|
||||
import QwenIcon from '@/ui/svg/icons/modelist/qianwen.svg';
|
||||
import SparkdeskIcon from '@/ui/svg/icons/modelist/sparkdesk.svg';
|
||||
import StabilityAIIcon from '@/ui/svg/icons/modelist/stabilityai.svg';
|
||||
import StepFunIcon from '@/ui/svg/icons/modelist/stepfun.svg';
|
||||
|
||||
export const modelIcons = {
|
||||
openai: OpenAIIcon,
|
||||
@@ -63,4 +62,4 @@ export const modelIcons = {
|
||||
lingyiwanwu: LingyiWanwuIcon,
|
||||
stepfun: StepFunIcon,
|
||||
default: DefaultIcon,
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user