feat: Home Page AI Service Deployment

This commit is contained in:
ningyv
2025-02-08 18:47:08 +08:00
parent 2c205921d6
commit 0b2928eb3c
13 changed files with 1071 additions and 265 deletions
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100" height="100" viewBox="0 0 100 100">
<image id="icons8-server-100" y="11" width="99" height="78" xlink:href="data:img/png;base64,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"/>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100" height="100" viewBox="0 0 100 100">
<image id="icons8-ai-cloud-100" y="16" width="100" height="68" xlink:href="data:img/png;base64,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"/>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100" height="100" viewBox="0 0 100 100">
<defs>
<style>
.cls-1 {
font-size: 40px;
fill: #339af0;
font-family: Montserrat;
font-weight: 600;
}
</style>
</defs>
<image id="icons8-api-100" width="100" height="100" xlink:href="data:img/png;base64,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"/>
<text id="REST" class="cls-1" transform="translate(20.904 48.641) scale(0.553)">REST</text>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

@@ -182,7 +182,7 @@ function BasicLayout({ project = 'core' }: { project: string }) {
</Button>,
...((pluginSlotHub.getSlot('basicLayoutAfterBtns') as unknown[]) || [])
]
}, [pluginSlotHub.getSlot('basicLayoutAfterBtns')])
}, [state.language, pluginSlotHub.getSlot('basicLayoutAfterBtns')])
return (
<div
@@ -63,6 +63,12 @@ export type SimpleTeamItem = {
description: string
appNum: number
}
export type LocalModelItem = {
id: string
is_popular: boolean
name: string
size: string
}
export type MatchItem = {
position: typeof MatchPositionEnum
@@ -108,6 +108,13 @@ export const SYSTEM_TABLE_COLUMNS: PageProColumns<SystemTableListItem>[] = [
dataIndex: ['team', 'name'],
ellipsis: true
},
{
title: '状态',
width: 140,
dataIndex: 'update_time',
// dataIndex: 'state',
ellipsis: true
},
{
title: 'API 数量',
dataIndex: 'apiNum',
@@ -4,31 +4,50 @@ import { BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from '@common/
import { useFetch } from '@common/hooks/http'
import { $t } from '@common/locales'
import { App, Form, InputNumber, Select, Switch, Tag, Tooltip } from 'antd'
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react'
import { AiProviderLlmsItems, ModelDetailData } from './types'
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react'
import { AiProviderLlmsItems, ModelDetailData, AiSettingListItem } from './types'
import { MemberItem, SimpleTeamItem } from '@common/const/type'
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
export type AiSettingModalContentProps = {
entity: ModelDetailData & { defaultLlm: string }
readOnly: boolean
modelMode?: 'auto' | 'manual'
/** 如果是手动选择 AI 模型,那么需要更新 footer 底部的内容,所以需要这个方法去更新外部的 footer */
updateEntityData: (entity: ModelDetailData & { defaultLlm: string }) => void
}
export type AiSettingModalContentHandle = {
save: () => Promise<boolean | string>
deployAIServer: () => Promise<boolean | string>
}
const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingModalContentProps>((props, ref) => {
const [form] = Form.useForm()
const { message } = App.useApp()
const { entity, readOnly } = props
const { entity, readOnly, modelMode = 'auto', updateEntityData } = props
const { fetchData } = useFetch()
const [llmList, setLlmList] = useState<AiProviderLlmsItems[]>()
const [loading, setLoading] = useState<boolean>(false)
const [enableState, setEnableState] = useState<boolean>(entity.status === 'enabled')
const getLlmList = () => {
// AI 模型配置
const [localEntity, setLocalEntity] = useState(entity)
const [teamList, setTeamList] = useState<SimpleTeamItem[]>([])
// AI 模型提供商列表
const modelProviderListRef = useRef<AiSettingListItem[]>([])
// 模型模式加载
const [modelModeLoading, setModelModeLoading] = useState<boolean>(false)
const [enableState, setEnableState] = useState<boolean>(localEntity?.status === 'enabled')
const { checkPermission } = useGlobalContext()
/**
* 获取 llm 列表
* @param id
*/
const getLlmList = (id?: string) => {
setLoading(true)
fetchData<BasicResponse<{ llms: AiProviderLlmsItems[] }>>(`ai/provider/llms`, {
method: 'GET',
eoParams: { provider: entity.id }
eoParams: { provider: id || localEntity.id }
})
.then((response) => {
const { code, data, msg } = response
@@ -43,25 +62,159 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
})
}
useEffect(() => {
getLlmList()
/**
* 获取团队选项列表
* @returns
*/
const getTeamOptionList = async (): any[] => {
const response = await fetchData<BasicResponse<{ teams: SimpleTeamItem[] }>>(
!checkPermission('system.workspace.team.view_all') ? 'simple/teams/mine' : 'simple/teams',
{ method: 'GET', eoTransformKeys: [] }
)
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
const teamOptionList = data.teams?.map((x: MemberItem) => {
return { ...x, label: x.name, value: x.id }
})
setTeamList(teamOptionList)
if (form.getFieldValue('team') === undefined && data.teams?.length) {
form.setFieldValue('team', data.teams[0].id)
}
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
return []
}
}
/**
* 获取未配置模型提供者列表
*/
const getModelProviderList = () => {
setModelModeLoading(true)
fetchData<BasicResponse<{ providers: AiSettingListItem[] }>>(`ai/providers/unconfigured`, {
method: 'GET',
eoTransformKeys: ['default_llm', 'default_llm_logo']
})
.then((response) => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
const providers = data.providers || []
modelProviderListRef.current = providers
if (providers.length) {
const id = providers[0].id
form.setFieldValue('modelMode', id)
getModelConfig(id)
}
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
.finally(() => {
setModelModeLoading(false)
})
}
/**
* 获取模型配置
* @param id
*/
const getModelConfig = (id: string) => {
getLlmList(id)
fetchData<BasicResponse<{ providers: ModelDetailData[] }>>(`ai/provider/config`, {
method: 'GET',
eoParams: { provider: id },
eoTransformKeys: ['get_apikey_url']
})
.then((response) => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
const modelEntity = {
...data.provider,
defaultLlm: modelProviderListRef.current.find((x) => x.id === id)?.defaultLlm
}
setLocalEntity(modelEntity)
setFormFieldsValue(modelEntity)
updateEntityData(modelEntity)
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
.finally(() => {
setModelModeLoading(false)
})
}
/**
* 设置表单字段值
* @param fieldsValue
*/
const setFormFieldsValue = (fieldsValue: any) => {
try {
form.setFieldsValue({
defaultLlm: entity.defaultLlm,
config: entity!.config ? JSON.stringify(JSON.parse(entity!.config), null, 2) : '',
priority: entity.priority || 1,
enable: entity.status === 'enabled'
defaultLlm: fieldsValue.defaultLlm,
config: fieldsValue!.config ? JSON.stringify(JSON.parse(fieldsValue!.config), null, 2) : '',
priority: fieldsValue.priority || 1,
enable: fieldsValue.status === 'enabled'
})
} catch (e) {
form.setFieldsValue({
defaultLlm: entity.defaultLlm,
defaultLlm: localEntity.defaultLlm,
config: '',
priority: 1,
enable: true
})
}
}
useEffect(() => {
// 如果是直接在 AI 模型配置,则获取默认模型列表和团队列表
if (modelMode === 'auto') {
getLlmList()
setFormFieldsValue(localEntity)
} else {
getModelProviderList()
getTeamOptionList()
}
}, [])
/**
* 部署 AI 服务
*/
const deployAIServer: () => Promise<boolean | string> = () => {
return new Promise((resolve, reject) => {
form
.validateFields()
.then((value) => {
const finalValue = {
config: value.config,
model: value.defaultLlm,
team: value.team,
provider: localEntity?.id
}
console.log(finalValue)
fetchData<BasicResponse<null>>('quick/service/ai', {
method: 'POST',
eoBody: finalValue
})
.then((response) => {
const { code, msg } = response
if (code === STATUS_CODE.SUCCESS) {
message.success(msg || $t(RESPONSE_TIPS.success))
resolve(true)
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
reject(msg || $t(RESPONSE_TIPS.error))
}
})
.catch((errorInfo) => reject(errorInfo))
})
.catch((errorInfo) => reject(errorInfo))
})
}
/**
* 保存
* @returns
*/
const save: () => Promise<boolean | string> = () => {
return new Promise((resolve, reject) => {
form
@@ -74,7 +227,7 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
fetchData<BasicResponse<null>>('ai/provider/config', {
method: 'PUT',
eoParams: { provider: entity?.id },
eoParams: { provider: localEntity?.id },
eoBody: finalValue,
eoTransformKeys: ['defaultLlm']
// eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/'
@@ -103,7 +256,8 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
}
useImperativeHandle(ref, () => ({
save
save,
deployAIServer
}))
return (
@@ -117,6 +271,26 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
autoComplete="off"
disabled={readOnly}
>
{modelMode === 'manual' && (
<Form.Item<ModelDetailData> label={$t('模型来源')} name="modelMode" rules={[{ required: true }]}>
<Select
className="w-INPUT_NORMAL"
placeholder={$t(PLACEHOLDER.select)}
loading={modelModeLoading}
options={modelProviderListRef.current?.map((x) => ({
value: x.id,
label: (
<div className="flex items-center gap-[10px]">
<span>{x.name}</span>
</div>
)
}))}
onChange={(e) => {
getModelConfig(e)
}}
></Select>
</Form.Item>
)}
<Form.Item<ModelDetailData> label={$t('默认模型')} name="defaultLlm" rules={[{ required: true }]}>
<Select
className="w-INPUT_NORMAL"
@@ -133,34 +307,42 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
}))}
></Select>
</Form.Item>
<Form.Item<ModelDetailData>
label={
<span className="flex items-center">
{$t('负载优先级')}
<Tooltip
title={$t('负载优先级决定在原供应商异常或停用后,优先使用哪一个供应商。优先级数字越小,优先级越高。')}
>
<QuestionCircleOutlined className="ml-1 text-gray-500" />
</Tooltip>
</span>
}
name="priority"
rules={[
{ required: true },
{
validator: async (_, value) => {
if (value <= 0) {
throw new Error($t('优先级必须大于 0'))
}
return Promise.resolve()
}
{modelMode === 'auto' && (
<Form.Item<ModelDetailData>
label={
<span className="flex items-center">
{$t('负载优先级')}
<Tooltip
title={$t('负载优先级决定在原供应商异常或停用后,优先使用哪一个供应商。优先级数字越小,优先级越高。')}
>
<QuestionCircleOutlined className="ml-1 text-gray-500" />
</Tooltip>
</span>
}
]}
initialValue={1}
>
<InputNumber className="w-INPUT_NORMAL" min={1} placeholder={$t('请输入优先级')} />
</Form.Item>
name="priority"
rules={[
{ required: true },
{
validator: async (_, value) => {
if (value <= 0) {
throw new Error($t('优先级必须大于 0'))
}
return Promise.resolve()
}
}
]}
initialValue={1}
>
<InputNumber className="w-INPUT_NORMAL" min={1} placeholder={$t('请输入优先级')} />
</Form.Item>
)}
{modelMode === 'manual' && (
<Form.Item label={$t('所属团队')} name="team" className="mt-[16px]" rules={[{ required: true }]}>
<Select className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)} options={teamList} onChange={(value) => {
form.setFieldValue('team', value)
}}></Select>
</Form.Item>
)}
<Form.Item<ModelDetailData> label={$t('API Key(默认 Key')} name="config">
<Codebox
@@ -173,14 +355,14 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
/>
</Form.Item>
{entity.configured && (
{localEntity?.configured && (
<Form.Item className="p-4 bg-white rounded-lg" label={$t('LLM 状态管理')}>
<div className="flex justify-between items-center">
<div>
<span className="text-gray-600">{$t('当前调用状态:')}</span>
{entity.status === 'enabled' && <Tag color="success">{$t('正常')}</Tag>}
{entity.status === 'disabled' && <Tag color="warning">{$t('停用')}</Tag>}
{entity.status === 'abnormal' && <Tag color="error">{$t('异常')}</Tag>}
{localEntity.status === 'enabled' && <Tag color="success">{$t('正常')}</Tag>}
{localEntity.status === 'disabled' && <Tag color="warning">{$t('停用')}</Tag>}
{localEntity.status === 'abnormal' && <Tag color="error">{$t('异常')}</Tag>}
</div>
<Form.Item name="enable" valuePropName="checked" noStyle>
<Switch
@@ -193,7 +375,7 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
/>
</Form.Item>
</div>
{(entity.status === 'enabled' && !enableState) || (entity.status !== 'enabled' && enableState) ? (
{(localEntity.status === 'enabled' && !enableState) || (localEntity.status !== 'enabled' && enableState) ? (
<div className="mt-2 text-sm text-gray-500">* {getTooltipText(enableState)}</div>
) : null}
</Form.Item>
@@ -12,6 +12,7 @@ export interface ModelListData {
name: string
logo: string
defaultLlm: string
modelMode?: string
status: ModelStatus
api_count: number
key_count: number
@@ -0,0 +1,427 @@
import restAPIPic from '@common/assets/restAPI.svg'
import onlineAIPic from '@common/assets/onlineAI.svg'
import localAIPic from '@common/assets/localAI.svg'
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
import { $t } from '@common/locales'
import { Icon } from '@iconify/react/dist/iconify.js'
import { App, Upload, UploadProps, Form, message, Select } from 'antd'
import { Card } from 'antd'
import { useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import WithPermission from '@common/components/aoplatform/WithPermission'
import { BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
import { useFetch } from '@common/hooks/http'
import { LocalModelItem, MemberItem, SimpleTeamItem } from '@common/const/type'
import AiSettingModalContent, { AiSettingModalContentHandle } from '../aiSetting/AiSettingModal'
import { checkAccess } from '@common/utils/permission'
const { Dragger } = Upload
export const AIModelGuide = () => {
const { modal } = App.useApp()
const [, forceUpdate] = useState<unknown>(null)
const [form] = Form.useForm()
const entityData = useRef<any>(null)
const { fetchData } = useFetch()
const navigateTo = useNavigate()
const { checkPermission, accessData } = useGlobalContext()
const modalRef = useRef<AiSettingModalContentHandle>()
/**
* 获取 team 选项列表
* @returns
*/
const getTeamOptionList = async (): any[] => {
const response = await fetchData<BasicResponse<{ teams: SimpleTeamItem[] }>>(
!checkPermission('system.workspace.team.view_all') ? 'simple/teams/mine' : 'simple/teams',
{ method: 'GET', eoTransformKeys: [] }
)
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
const teamOptionList = data.teams?.map((x: MemberItem) => {
return { ...x, label: x.name, value: x.id }
})
if (form.getFieldValue('team') === undefined && data.teams?.length) {
form.setFieldValue('team', data.teams[0].id)
}
return teamOptionList
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
return []
}
}
/**
* 部署 rest 服务
* @param file
* @returns
*/
const deployRestServer = async (file: File) => {
return new Promise((resolve, reject) => {
const formData = new FormData()
formData.append('file', file)
formData.append('type', file.type)
formData.append('team', form.getFieldValue('team'))
fetchData<BasicResponse<{ teams: SimpleTeamItem[] }>>('quick/service/rest', {
method: 'POST',
body: formData
}).then((response) => {
const { code, msg } = response
if (code === STATUS_CODE.SUCCESS) {
message.success(msg || $t(RESPONSE_TIPS.success))
resolve(true)
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
reject(false)
}
})
})
}
const deployPopularModel = async (id: string, modalInstance: any) => {
await deployLocalModel({
modelID: id,
team: form.getFieldValue('team')
})
modalInstance.destroy()
navigateTo(`/service/list`)
}
/**
* 部署本地模型
* @param value
* @returns
*/
const deployLocalModel = (value: { modelID: string; team?: number }) => {
return new Promise((resolve, reject) => {
const finalValue = {
model: value.modelID,
team: value?.team
}
console.log(finalValue)
fetchData<BasicResponse<null>>('model/local/deploy', {
method: 'POST',
eoBody: finalValue
})
.then((response) => {
const { code, msg } = response
if (code === STATUS_CODE.SUCCESS) {
message.success(msg || $t(RESPONSE_TIPS.success))
resolve(true)
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
reject(false)
}
})
.catch((errorInfo) => reject(errorInfo))
})
}
/**
* 获取本地模型列表
* @returns 本地模型列表
*/
const getLocalModelList = async (): any[] => {
const response = await fetchData<BasicResponse<{ models: LocalModelItem[] }>>(
'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/model/local/can_deploy',
// 'model/local/can_deploy'
{ method: 'GET', custom: true, eoTransformKeys: ['is_popular'] }
)
// TODO_数据模拟
if (response.ok) {
const datas = await response.json()
const { code, data, msg } = datas
if (code === STATUS_CODE.SUCCESS) {
const modelList = data.models?.map((x: LocalModelItem) => {
return { ...x, label: x.name, value: x.id }
})
return modelList
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
return []
}
} else {
console.error('HTTP error', response.status)
}
// const { code, data, msg } = response
// if (code === STATUS_CODE.SUCCESS) {
// const modelList = data.models?.map((x: LocalModelItem) => {
// return { ...x, label: x.name, value: x.id }
// })
// console.log('modelList===', modelList);
// return modelList
// } else {
// message.error(msg || $t(RESPONSE_TIPS.error))
// return []
// }
}
/**
* rest 服务卡片点击事件
*/
const restCardClick = async () => {
form.resetFields()
const teamList = await getTeamOptionList()
const props: UploadProps = {
name: 'file',
multiple: false,
maxCount: 1,
beforeUpload: (file) => {
form.setFieldsValue({ key: file })
forceUpdate({})
return false
}
}
modal.confirm({
title: $t('添加 Rest 服务'),
content: (
<WithPermission access="">
<Form
layout="vertical"
labelAlign="left"
scrollToFirstError
form={form}
className="mx-auto "
name="partitionInsideCert"
autoComplete="off"
>
<Form.Item
name="key"
className="mb-0 bg-transparent p-0 border-none rounded-none"
rules={[{ required: true }]}
>
<Dragger {...props}>
<p className="ant-upload-drag-icon">
<Icon className="text-[#ccc]" icon="tdesign:upload" width="50" height="50" />
</p>
<p className="ant-upload-text">{$t('选择 OpenAPI 文件 (.json / .yaml)')}</p>
</Dragger>
</Form.Item>
<Form.Item label={$t('所属团队')} name="team" className="mt-[16px]" rules={[{ required: true }]}>
<Select
className="w-INPUT_NORMAL"
placeholder={$t(PLACEHOLDER.input)}
options={teamList}
onChange={(value) => {
form.setFieldValue('team', value)
}}
></Select>
</Form.Item>
</Form>
</WithPermission>
),
onOk: () => {
return new Promise((resolve, reject) => {
form
.validateFields()
.then(async (value) => {
await deployRestServer(value.key.file)
resolve(true)
navigateTo(`/service/list`)
})
.catch((errorInfo) => reject(errorInfo))
})
},
width: 600,
okText: $t('确认'),
cancelText: $t('取消'),
closable: true,
icon: <></>
})
}
/**
* AI 模型配置弹窗
*/
const aiCardClick = () => {
// 更新弹窗
const updateEntityData = (data: any) => {
entityData.current = data
// 更新弹窗
modalInstance.update({})
}
const modalInstance = modal.confirm({
title: $t('模型配置'),
content: (
<AiSettingModalContent
ref={modalRef}
modelMode="manual"
updateEntityData={updateEntityData}
readOnly={!checkAccess('system.devops.ai_provider.edit', accessData)}
/>
),
onOk: () => {
return modalRef.current?.deployAIServer().then((res) => {
if (res === true) {
navigateTo(`/service/list`)
}
})
},
width: 600,
okText: $t('确认'),
footer: (_, { OkBtn, CancelBtn }) => {
return (
<div className="flex justify-between items-center">
<a
target="_blank"
rel="noopener noreferrer"
href={entityData.current?.getApikeyUrl}
className="flex items-center gap-[8px]"
>
<span>{$t('从 (0) 获取 API KEY', [entityData.current?.name])}</span>
<Icon icon="ic:baseline-open-in-new" width={16} height={16} />
</a>
<div>
<CancelBtn />
{checkAccess('system.devops.ai_provider.edit', accessData) ? <OkBtn /> : null}
</div>
</div>
)
},
cancelText: $t('取消'),
closable: true,
icon: <></>
})
}
/**
* 本地部署 AI 并生成 API
*/
const localModelCardClick = async () => {
form.resetFields()
const teamList = await getTeamOptionList()
const modelList = await getLocalModelList()
const modalInstance = modal.confirm({
title: $t('部署 AI 模型'),
content: (
<WithPermission access="">
<Form
layout="vertical"
labelAlign="left"
scrollToFirstError
form={form}
className="mx-auto "
name="partitionInsideCert"
autoComplete="off"
>
<Form.Item label={$t('模型名称')} name="modelID" rules={[{ required: true }]}>
<Select
showSearch
className="w-INPUT_NORMAL"
filterOption={(input, option) =>
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
}
placeholder={$t(PLACEHOLDER.input)}
options={modelList}
onChange={(value) => {
form.setFieldValue('modelID', value)
}}
></Select>
<div className="mt-[10px] mb-[5px]">
<Icon className="align-text-top" icon="noto-v1:fire" width="17" height="17" />
{$t('热点模型')}
</div>
<div className="pl-[5px] flex flex-wrap">
{modelList.length &&
modelList
.filter((item) => item.is_popular)
.map((item) => (
<span
key={item.id}
className="text-[#2196f3] text-[15px] hover:text-[#1976d2] mr-[20px] cursor-pointer
"
onClick={() => {
deployPopularModel(item.id, modalInstance)
}}
>
{item.name}
</span>
))}
</div>
</Form.Item>
<Form.Item label={$t('所属团队')} name="team" className="mt-[16px]" rules={[{ required: true }]}>
<Select
className="w-INPUT_NORMAL"
placeholder={$t(PLACEHOLDER.input)}
options={teamList}
onChange={(value) => {
form.setFieldValue('team', value)
}}
></Select>
</Form.Item>
</Form>
</WithPermission>
),
onOk: () => {
return new Promise((resolve, reject) => {
form
.validateFields()
.then(async (value) => {
await deployLocalModel(value)
resolve(true)
navigateTo(`/service/list`)
})
.catch((errorInfo) => reject(errorInfo))
})
},
width: 600,
okText: $t('确认'),
cancelText: $t('取消'),
closable: true,
icon: <></>
})
}
const deployDeepSeek = (e: any) => {
e.stopPropagation()
deployLocalModel({
modelID: 'deepseek-r1'
})
}
const cardList = [
{
imgSrc: restAPIPic,
title: $t('添加 Rest 服务'),
description: $t('支持批量添加现有 API 文档以实现统一的外部访问。'),
click: restCardClick
},
{
imgSrc: onlineAIPic,
title: $t('添加在线 AI API'),
description: $t('快速调用 AI 模型的云服务 API,方便管理提示词和统一计费。'),
click: aiCardClick
},
{
imgSrc: localAIPic,
title: $t('本地部署 AI 并生成 API'),
description: $t('快速在本地部署开源模型并自动生成 API。'),
click: localModelCardClick,
bottomRender: (
<span className="text-[#2196f3] text-[13px] hover:text-[#1976d2]" onClick={deployDeepSeek}>
<Icon className="align-sub mr-[5px]" icon="lsicon:lightning-filled" width="15" height="15" />
{$t('部署')} Deepseek-R1
</span>
)
}
]
return (
<div className="mb-[20px] flex justify-center">
{cardList.map((item, itemIndex) => (
<Card
key={itemIndex}
className="shadow-[0_5px_10px_0_rgba(0,0,0,0.05)] bg-[linear-gradient(153.41deg,rgba(244,245,255,1)_0.23%,rgba(255,255,255,1)_83.32%)] mr-[30px] rounded-[10px] overflow-visible cursor-pointer w-[250px] transition duration-500 hover:shadow-[0_5px_20px_0_rgba(0,0,0,0.15)] hover:scale-[1.05]"
classNames={{
header: 'border-b-[0px] p-[20px] pb-[10px] text-[14px] font-normal',
body: 'p-[20px] pt-[50px] pb-[50px] text-[12px] text-[#666] text-center'
}}
onClick={item.click}
>
<img src={item.imgSrc} alt="" width={60} height={60} />
<p className="text-[13px] font-bold text-black mt-[10px] mb-[10px]">{item.title}</p>
<p className="break-words mb-[10px]">{item.description}</p>
{item.bottomRender ? item.bottomRender : null}
</Card>
))}
</div>
)
}
+304 -215
View File
@@ -1,232 +1,321 @@
import InsidePage from "@common/components/aoplatform/InsidePage"
import { useGlobalContext } from "@common/contexts/GlobalStateContext"
import { $t } from "@common/locales"
import { Icon } from "@iconify/react/dist/iconify.js"
import { Button, Card, Collapse } from "antd"
import { Dispatch, SetStateAction, useEffect, useState } from "react"
import { useLocation, useNavigate } from "react-router-dom"
import InsidePage from '@common/components/aoplatform/InsidePage'
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
import { $t } from '@common/locales'
import { Icon } from '@iconify/react/dist/iconify.js'
import { Button, Card, Collapse } from 'antd'
import { Dispatch, SetStateAction, useEffect, useState } from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
import { AIModelGuide } from './AIModelGuide'
export default function Guide(){
const [showGuide, setShowGuide] = useState(localStorage.getItem('showGuide') !== 'false' )
const [showAdvancedGuide, setShowAdvancedGuide] = useState(localStorage.getItem('showAdvancedGuide') !== 'false' )
const [, forceUpdate] = useState<unknown>(null);
const {state} = useGlobalContext()
const location = useLocation()
const currentUrl = location.pathname
const navigator = useNavigate()
const guideSections = [
export default function Guide() {
const [showGuide, setShowGuide] = useState(localStorage.getItem('showGuide') !== 'false')
const [showAdvancedGuide, setShowAdvancedGuide] = useState(localStorage.getItem('showAdvancedGuide') !== 'false')
const [, forceUpdate] = useState<unknown>(null)
const { state } = useGlobalContext()
const location = useLocation()
const currentUrl = location.pathname
const navigator = useNavigate()
const guideSections = [
{
title: $t('快速接入 AI'),
items: [
{
title: $t('快速接入 AI'),
items: [
{
title: $t("配置你的 AI 模型"),
description: $t('通过 APIPark 快速接入各种 AI 模型,使用统一的格式来调用API,并且可以随意切换模型。'),
link: 'https://docs.apipark.com/docs/system_setting/ai_model_providers'
},
{
title: $t("创建 AI 服务和 API"),
description: $t('创建 AI 类型的服务,并且你可以将 Prompt 提示词设置为一个 API,简化使用 AI 的流程。'),
link: 'https://docs.apipark.com/docs/services/ai_services'
},
{
title: $t("创建调用 Token"),
description: $t('为了安全地调用 API,你需要创建一个消费者以及Token。'),
link: 'https://docs.apipark.com/docs/consumers'
},
{
title: $t("调用"),
description: $t('现在你可以通过 Token 来调用这些 API。'),
link: 'https://docs.apipark.com/docs/call_api'
}
]
title: $t('配置你的 AI 模型'),
description: $t('通过 APIPark 快速接入各种 AI 模型,使用统一的格式来调用API,并且可以随意切换模型。'),
link: 'https://docs.apipark.com/docs/system_setting/ai_model_providers'
},
{
title: $t('快速接入 REST API'),
items: [
{
title: $t("创建 REST 服务和 API"),
description: $t('创建 AI 类型的服务,并且你可以将 Prompt 提示词设置为一个 API,简化使用 AI 的流程。'),
link: 'https://docs.apipark.com/docs/services/rest_services'
},
{
title: $t("创建调用 Token"),
description: $t('为了安全地调用 API,你需要创建一个消费者以及Token。'),
link: 'https://docs.apipark.com/docs/consumers'
},
{
title: $t("调用"),
description: $t('现在你可以通过 Token 来调用这些 API。'),
link: 'https://docs.apipark.com/docs/call_api'
}
]
title: $t('创建 AI 服务和 API'),
description: $t('创建 AI 类型的服务,并且你可以将 Prompt 提示词设置为一个 API,简化使用 AI 的流程。'),
link: 'https://docs.apipark.com/docs/services/ai_services'
},
{
title: $t('仪表盘'),
items: [
{
title: $t("统计 API 调用情况"),
description: $t('仪表盘中提供了多种统计图表,帮助我们了解 API 的运行情况。'),
link: 'https://docs.apipark.com/docs/analysis'
}
]
title: $t('创建调用 Token'),
description: $t('为了安全地调用 API,你需要创建一个消费者以及Token。'),
link: 'https://docs.apipark.com/docs/consumers'
},
{
title: $t('调用'),
description: $t('现在你可以通过 Token 来调用这些 API。'),
link: 'https://docs.apipark.com/docs/call_api'
}
];
const advanceGuideSections = [
]
},
{
title: $t('快速接入 REST API'),
items: [
{
title: $t('核心功能'),
items: [
{
title: $t("账号与角色"),
description: $t('邀请你的团队成员加入 APIPark,共同管理和调用 API。'),
link: 'https://docs.apipark.com/docs/system_setting/account_role'
},
{
title: $t("团队"),
description: $t('团队中包含了人员、消费者和服务,不同团队之间的消费者和服务数据是隔离的,可用于管理企业内部不同的部门/项目组/团队。'),
link: 'https://docs.apipark.com/docs/teams'
},
{
title: $t("服务"),
description: $t('服务内包含一组 API,并且可以发布到 API 市场被其他团队使用。'),
link: 'https://docs.apipark.com/docs/category/-%E6%9C%8D%E5%8A%A1'
}
]
title: $t('创建 REST 服务和 API'),
description: $t('创建 AI 类型的服务,并且你可以将 Prompt 提示词设置为一个 API,简化使用 AI 的流程。'),
link: 'https://docs.apipark.com/docs/services/rest_services'
},
{
title: $t('权限管理'),
items: [
{
title: $t("订阅服务"),
description: $t('如果需要调用某个服务的 API,需要先订阅该服务,并且等待提供服务的团队审核后才可发起 API 请求。'),
link: 'https://docs.apipark.com/docs/developer_portal'
},
{
title: $t("审核订阅申请"),
description: $t('提供服务的团队可以审核来自其他团队的订阅申请,审核通过后的消费者才可发起 API 请求。'),
link: 'https://docs.apipark.com/docs/services/review_consumers'
}
]
title: $t('创建调用 Token'),
description: $t('为了安全地调用 API,你需要创建一个消费者以及Token。'),
link: 'https://docs.apipark.com/docs/consumers'
},
{
title: $t('集成'),
items: [
{
title: $t("日志"),
description: $t('APIPark 提供详尽的 API 调用日志,帮助企业监控、分析和审计 API 的运行状况。'),
link: 'https://docs.apipark.com/docs/system_setting/log/'
}
]
title: $t('调用'),
description: $t('现在你可以通过 Token 来调用这些 API。'),
link: 'https://docs.apipark.com/docs/call_api'
}
];
useEffect(()=>{
localStorage.setItem('showGuide', showGuide.toString())
},[showGuide])
useEffect(()=>{
localStorage.setItem('showAdvancedGuide', showAdvancedGuide.toString())
},[showAdvancedGuide])
useEffect(()=>{
if(currentUrl === '/guide'){
setTimeout(()=>{
navigator('/guide/page')
},0)
]
},
{
title: $t('仪表盘'),
items: [
{
title: $t('统计 API 调用情况'),
description: $t('仪表盘中提供了多种统计图表,帮助我们了解 API 的运行情况。'),
link: 'https://docs.apipark.com/docs/analysis'
}
},[])
useEffect(()=>{forceUpdate({})},[state.language])
return (
<InsidePage
pageTitle={<div className="flex items-center gap-[8px]">
<span>👋</span>
<span>{$t('Hello!欢迎使用 APIPark')}</span>
<a className="" href="https://github.com/APIParkLab/APIPark" target="_blank"><img src="https://img.shields.io/github/stars/APIParkLab/APIPark?style=social"alt="" /></a>
</div>}
description={<div className="flex flex-col gap-[8px]">
<p>{$t("你能通过 APIPark 快速在企业内部构建 API 开放门户/市场,享受极致的转发性能、API 可观测、服务治理、多租户管理、订阅审核流程等诸多好处。")}</p>
<p>{$t("如果你喜欢我们的产品,欢迎给我们 Star 或提供产品反馈意见。")}</p>
</div>}
showBorder={false}
scrollPage={false}
contentClassName=" w-full pr-PAGE_INSIDE_X pb-PAGE_INSIDE_B"
>
<div className="flex flex-col gap-[15px]">
{showGuide &&
<Collapse
size="large"
expandIconPosition='end'
defaultActiveKey={['1']}
className="bg-[linear-gradient(153.41deg,rgba(244,245,255,1)_0.23%,rgba(255,255,255,1)_83.32%)] rounded-[10px] [&>.ant-collapse-item>.ant-collapse-content]:bg-transparent "
items={[{ key: '1',
label:
<div className="">
<p className="text-[14px] mb-[10px] flex gap-[8px] items-center font-bold">
<span>🚀</span><span>{`${$t('快速入门')}`}</span> </p>
<p className="text-[12px]" >{$t("我们提供了一些任务来帮你快速了解 APIPark")}</p></div>,
children:<QuickGuideContent changeGuideShow={setShowGuide} guideSections={guideSections} /> }]}
/>}
{showAdvancedGuide &&
<Collapse
size="large"
expandIconPosition='end'
defaultActiveKey={['1']}
className="bg-[linear-gradient(153.41deg,rgba(244,245,255,1)_0.23%,rgba(255,255,255,1)_83.32%)] rounded-[10px] [&>.ant-collapse-item>.ant-collapse-content]:bg-transparent "
items={[{ key: '1',
label:
<div className="">
<p className="text-[14px] mb-[10px] flex gap-[8px] items-center font-bold">
<span>🏍</span><span>{`${$t('进阶教程')}`}</span> </p>
<p className="text-[12px]" >{$t("了解 APIPark 如何更好地管理 API 和 AI")}</p></div>,
children:<QuickGuideContent changeGuideShow={setShowAdvancedGuide} guideSections={advanceGuideSections} /> }]}
/>}
</div>
</InsidePage>)
]
}
]
const advanceGuideSections = [
{
title: $t('核心功能'),
items: [
{
title: $t('账号与角色'),
description: $t('邀请你的团队成员加入 APIPark,共同管理和调用 API。'),
link: 'https://docs.apipark.com/docs/system_setting/account_role'
},
{
title: $t('团队'),
description: $t(
'团队中包含了人员、消费者和服务,不同团队之间的消费者和服务数据是隔离的,可用于管理企业内部不同的部门/项目组/团队。'
),
link: 'https://docs.apipark.com/docs/teams'
},
{
title: $t('服务'),
description: $t('服务内包含一组 API,并且可以发布到 API 市场被其他团队使用。'),
link: 'https://docs.apipark.com/docs/category/-%E6%9C%8D%E5%8A%A1'
}
]
},
{
title: $t('权限管理'),
items: [
{
title: $t('订阅服务'),
description: $t(
'如果需要调用某个服务的 API,需要先订阅该服务,并且等待提供服务的团队审核后才可发起 API 请求。'
),
link: 'https://docs.apipark.com/docs/developer_portal'
},
{
title: $t('审核订阅申请'),
description: $t('提供服务的团队可以审核来自其他团队的订阅申请,审核通过后的消费者才可发起 API 请求。'),
link: 'https://docs.apipark.com/docs/services/review_consumers'
}
]
},
{
title: $t('集成'),
items: [
{
title: $t('日志'),
description: $t('APIPark 提供详尽的 API 调用日志,帮助企业监控、分析和审计 API 的运行状况。'),
link: 'https://docs.apipark.com/docs/system_setting/log/'
}
]
}
]
useEffect(() => {
localStorage.setItem('showGuide', showGuide.toString())
}, [showGuide])
useEffect(() => {
localStorage.setItem('showAdvancedGuide', showAdvancedGuide.toString())
}, [showAdvancedGuide])
useEffect(() => {
if (currentUrl === '/guide') {
setTimeout(() => {
navigator('/guide/page')
}, 0)
}
}, [])
useEffect(() => {
forceUpdate({})
}, [state.language])
return (
<InsidePage
pageTitle={
<div className="flex items-center gap-[8px]">
<span>👋</span>
<span>{$t('Hello!欢迎使用 APIPark')}</span>
</div>
}
description={
<div className="flex flex-col gap-[8px]">
<p>
{$t(
'你能通过 APIPark 快速在企业内部构建 API 开放门户/市场,享受极致的转发性能、API 可观测、服务治理、多租户管理、订阅审核流程等诸多好处。'
)}
</p>
<p>
{$t('如果你喜欢我们的产品,欢迎给我们 Star 或提供产品反馈意见。')}
{$t('点击这里')}
<span className="align-middle leading-[16px]">
&nbsp;
<Icon icon="pajamas:arrow-right" width="16" height="16" />
&nbsp;
</span>
<a className="align-text-top" href="https://github.com/APIParkLab/APIPark" target="_blank">
<img src="https://img.shields.io/github/stars/APIParkLab/APIPark?style=social" alt="" />
</a>
<span className="align-middle leading-[16px]">
&nbsp;
<Icon icon="pajamas:arrow-right" width="16" height="16" />
&nbsp;
</span>
{$t('点击')}
&nbsp;
<span className="align-middle leading-[16px]">
<Icon icon="emojione:star" width="16" height="16" />
</span>
Star
</p>
</div>
}
showBorder={false}
scrollPage={false}
contentClassName=" w-full pr-PAGE_INSIDE_X pb-PAGE_INSIDE_B"
>
<AIModelGuide></AIModelGuide>
<div className="flex flex-col gap-[15px]">
{showGuide && (
<Collapse
size="large"
expandIconPosition="end"
defaultActiveKey={['1']}
className="bg-[linear-gradient(153.41deg,rgba(244,245,255,1)_0.23%,rgba(255,255,255,1)_83.32%)] rounded-[10px] [&>.ant-collapse-item>.ant-collapse-content]:bg-transparent "
items={[
{
key: '1',
label: (
<div className="">
<p className="text-[14px] mb-[10px] flex gap-[8px] items-center font-bold">
<span>🚀</span>
<span>{`${$t('快速入门')}`}</span>{' '}
</p>
<p className="text-[12px]">{$t('我们提供了一些任务来帮你快速了解 APIPark')}</p>
</div>
),
children: <QuickGuideContent changeGuideShow={setShowGuide} guideSections={guideSections} />
}
]}
/>
)}
{showAdvancedGuide && (
<Collapse
size="large"
expandIconPosition="end"
defaultActiveKey={['1']}
className="bg-[linear-gradient(153.41deg,rgba(244,245,255,1)_0.23%,rgba(255,255,255,1)_83.32%)] rounded-[10px] [&>.ant-collapse-item>.ant-collapse-content]:bg-transparent "
items={[
{
key: '1',
label: (
<div className="">
<p className="text-[14px] mb-[10px] flex gap-[8px] items-center font-bold">
<span>🏍</span>
<span>{`${$t('进阶教程')}`}</span>{' '}
</p>
<p className="text-[12px]">{$t('了解 APIPark 如何更好地管理 API 和 AI')}</p>
</div>
),
children: (
<QuickGuideContent changeGuideShow={setShowAdvancedGuide} guideSections={advanceGuideSections} />
)
}
]}
/>
)}
</div>
</InsidePage>
)
}
const QuickGuideContent = ({changeGuideShow,guideSections}:{changeGuideShow:Dispatch<SetStateAction<boolean>>,guideSections: {
title: string;
const QuickGuideContent = ({
changeGuideShow,
guideSections
}: {
changeGuideShow: Dispatch<SetStateAction<boolean>>
guideSections: {
title: string
items: {
title: string;
description: string;
link: string;
}[];
}[]})=>{
return (<>
<div className="">
{guideSections.map((section, index) => (
<div key={index}>
<p className="flex gap-[8px] items-center text-[14px] font-bold">
<Icon icon="ic:baseline-info" width="18" height="18" className="text-theme" />
{section.title}
</p>
<div className="ml-[9px] border-[0px] border-l-[1px] my-[10px] border-dashed border-BORDER">
<div className="grid gap-[20px] px-[20px] py-[10px] justify-start content-start" style={{
gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 0fr))',
gridAutoRows: '1fr'
}}>
{section.items.map((item, itemIndex) => (
<Card
key={itemIndex}
title={item.title}
className="shadow-[0_5px_10px_0_rgba(0,0,0,0.05)] rounded-[10px] overflow-visible cursor-pointer w-[300px] transition duration-500 hover:shadow-[0_5px_20px_0_rgba(0,0,0,0.15)] hover:scale-[1.05]"
classNames={{ header: 'border-b-[0px] p-[20px] pb-[10px] text-[14px] font-normal', body: "p-[20px] pt-0 text-[12px] text-[#666]" }}
onClick={() => { window.open(item.link, '_blank') }}
>
<span>{item.description}</span>
</Card>
))}
</div>
</div>
</div>
))}
<p className="flex gap-[8px] items-center">
<Icon icon="ic:baseline-info" width="18" height="18" className="text-theme"/>
<div className="flex items-center w-full gap-4">
<Button type="link" icon={<Icon icon="ic:baseline-open-in-new" width="18" height="18" />} iconPosition="end" classNames={{icon:'h-[22px] flex items-center'}} href="https://docs.apipark.com" target="_blank" className="text-[14px] font-bold px-0">{$t('了解更多功能')}</Button>
<Button type="text" icon={<Icon icon="ic:baseline-visibility-off" width="18" height="18" />} onClick={()=>changeGuideShow((prev)=>!prev)} classNames={{icon:'h-[22px] flex items-center'}} className="text-[14px] font-bold">{$t('隐藏该教程')}</Button>
</div>
</p>
</div>
</>)
}
title: string
description: string
link: string
}[]
}[]
}) => {
return (
<>
<div className="">
{guideSections.map((section, index) => (
<div key={index}>
<p className="flex gap-[8px] items-center text-[14px] font-bold">
<Icon icon="ic:baseline-info" width="18" height="18" className="text-theme" />
{section.title}
</p>
<div className="ml-[9px] border-[0px] border-l-[1px] my-[10px] border-dashed border-BORDER">
<div
className="grid gap-[20px] px-[20px] py-[10px] justify-start content-start"
style={{
gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 0fr))',
gridAutoRows: '1fr'
}}
>
{section.items.map((item, itemIndex) => (
<Card
key={itemIndex}
title={item.title}
className="shadow-[0_5px_10px_0_rgba(0,0,0,0.05)] rounded-[10px] overflow-visible cursor-pointer w-[300px] transition duration-500 hover:shadow-[0_5px_20px_0_rgba(0,0,0,0.15)] hover:scale-[1.05]"
classNames={{
header: 'border-b-[0px] p-[20px] pb-[10px] text-[14px] font-normal',
body: 'p-[20px] pt-0 text-[12px] text-[#666]'
}}
onClick={() => {
window.open(item.link, '_blank')
}}
>
<span>{item.description}</span>
</Card>
))}
</div>
</div>
</div>
))}
<div className="flex gap-[8px] items-center">
<Icon icon="ic:baseline-info" width="18" height="18" className="text-theme" />
<div className="flex items-center w-full gap-4">
<Button
type="link"
icon={<Icon icon="ic:baseline-open-in-new" width="18" height="18" />}
iconPosition="end"
classNames={{ icon: 'h-[22px] flex items-center' }}
href="https://docs.apipark.com"
target="_blank"
className="text-[14px] font-bold px-0"
>
{$t('了解更多功能')}
</Button>
<Button
type="text"
icon={<Icon icon="ic:baseline-visibility-off" width="18" height="18" />}
onClick={() => changeGuideShow((prev) => !prev)}
classNames={{ icon: 'h-[22px] flex items-center' }}
className="text-[14px] font-bold"
>
{$t('隐藏该教程')}
</Button>
</div>
</div>
</div>
</>
)
}
@@ -440,7 +440,6 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
<Input
prefix={onEdit ? '' : '/'}
className="w-INPUT_NORMAL"
disabled={onEdit}
placeholder={$t(PLACEHOLDER.input)}
/>
</Form.Item>
@@ -14,6 +14,7 @@ import { useNavigate } from 'react-router-dom'
import { SERVICE_KIND_OPTIONS, SYSTEM_TABLE_COLUMNS } from '../../const/system/const.tsx'
import { SystemConfigHandle, SystemTableListItem } from '../../const/system/type.ts'
import SystemConfig from './SystemConfig.tsx'
import { ServiceDeployment } from './serviceDeployment/ServiceDeployment.tsx'
const SystemList: FC = () => {
const navigate = useNavigate()
@@ -23,7 +24,7 @@ const SystemList: FC = () => {
const { fetchData } = useFetch()
const [tableListDataSource, setTableListDataSource] = useState<SystemTableListItem[]>([])
const [tableHttpReload, setTableHttpReload] = useState(true)
const { message } = App.useApp()
const { message, modal } = App.useApp()
const pageListRef = useRef<ActionType>(null)
const [memberValueEnum, setMemberValueEnum] = useState<{ [k: string]: { text: string } }>({})
const [open, setOpen] = useState(false)
@@ -128,7 +129,22 @@ const SystemList: FC = () => {
const onClose = () => {
setOpen(false)
}
const openLogsModal = (record: any) => {
console.log('record', record)
modal.confirm({
title: $t('部署过程'),
content: <ServiceDeployment record={record} />,
onOk: () => {
console.log('ok')
},
width: 600,
okText: $t('确认'),
cancelText: $t('取消'),
closable: true,
icon: <></>
})
}
const columns = useMemo(() => {
const res = SYSTEM_TABLE_COLUMNS.map((x) => {
const dataIndex = x.dataIndex as string[]
@@ -145,6 +161,21 @@ const SystemList: FC = () => {
;(x.valueEnum as any)[option.value] = { text: $t(option.label) }
})
}
if ((x.dataIndex as string) === 'update_time') {
x.render = (text: any, record: any) => (
<span
className={`text-[13px] ${record.can_delete ? '[&>.ant-typography]:text-[#2196f3]' : ''}`}
onClick={(e) => {
if (record.can_delete) {
e?.stopPropagation();
openLogsModal(record)
}
}}
>
{text}
</span>
)
}
return { ...x, title: typeof x.title === 'string' ? $t(x.title as string) : x.title }
})
@@ -0,0 +1,44 @@
import { SystemTableListItem } from '@core/const/system/type'
import type { StepsProps } from 'antd'
import { Popover, Steps } from 'antd'
import { CheckCircleOutlined, LoadingOutlined } from '@ant-design/icons'
const customDot: StepsProps['progressDot'] = (dot, { status, index }) => (
<Popover
content={
<span>
step {index} status: {status}
</span>
}
>
{dot}
</Popover>
)
export const ServiceDeployment = (props: { record: SystemTableListItem }) => {
const { record } = props
console.log('record', record)
const items = [
{
title: 'Download',
description: '4.7 GB / 4.7 GB'
},
{
title: 'Deploy',
},
{
title: 'Initializing',
}
]
return (
<div className="flex justify-center items-center">
{/* <Steps items={items} /> */}
<Steps
current={0}
labelPlacement="vertical"
items={items}
/>
</div>
)
}