diff --git a/frontend/providers/devbox/.env.template b/frontend/providers/devbox/.env.template index 3dd1c90bc8a..9763c991061 100644 --- a/frontend/providers/devbox/.env.template +++ b/frontend/providers/devbox/.env.template @@ -6,6 +6,7 @@ DEVBOX_AFFINITY_ENABLE= SQUASH_ENABLE= NODE_TLS_REJECT_UNAUTHORIZED= ROOT_RUNTIME_NAMESPACE= +GPU_ENABLE= DATABASE_URL= RETAG_SVC_URL= -PRIVACY_URL= \ No newline at end of file +PRIVACY_URL= diff --git a/frontend/providers/devbox/api/devbox.ts b/frontend/providers/devbox/api/devbox.ts index 9f94747da1e..540891d47d0 100644 --- a/frontend/providers/devbox/api/devbox.ts +++ b/frontend/providers/devbox/api/devbox.ts @@ -1,6 +1,5 @@ import { V1Deployment, V1Pod, V1StatefulSet } from '@kubernetes/client-node' -import { DELETE, GET, POST } from '@/services/request' import { GetDevboxByNameReturn } from '@/types/adapt' import { DevboxEditTypeV2, @@ -17,14 +16,20 @@ import { adaptDevboxVersionListItem, adaptPod } from '@/utils/adapt' +import { GET, POST, DELETE } from '@/services/request' export const getMyDevboxList = () => - GET<[KBDevboxTypeV2, { - templateRepository: { - iconId: string | null; - }; - uid: string; - }][]>('/api/getDevboxList').then((data): DevboxListItemTypeV2[] => + GET< + [ + KBDevboxTypeV2, + { + templateRepository: { + iconId: string | null + } + uid: string + } + ][] + >('/api/getDevboxList').then((data): DevboxListItemTypeV2[] => data.map(adaptDevboxListItemV2).sort((a, b) => { return new Date(b.createTime).getTime() - new Date(a.createTime).getTime() }) @@ -35,9 +40,8 @@ export const getDevboxByName = (devboxName: string) => export const applyYamlList = (yamlList: string[], type: 'create' | 'replace' | 'update') => POST('/api/applyYamlList', { yamlList, type }) -export const createDevbox = (payload: { - devboxForm: DevboxEditTypeV2 -}) => POST(`/api/createDevbox`, payload) +export const createDevbox = (payload: { devboxForm: DevboxEditTypeV2 }) => + POST(`/api/createDevbox`, payload) export const updateDevbox = (payload: { patch: DevboxPatchPropsType; devboxName: string }) => POST(`/api/updateDevbox`, payload) @@ -73,14 +77,14 @@ export const delDevboxVersionByName = (versionName: string) => export const getSSHConnectionInfo = (data: { devboxName: string }) => GET<{ - base64PublicKey: string; - base64PrivateKey: string; - token: string; - userName: string; - workingDir: string; - releaseCommand: string; - releaseArgs: string; -}>('/api/getSSHConnectionInfo', data) + base64PublicKey: string + base64PrivateKey: string + token: string + userName: string + workingDir: string + releaseCommand: string + releaseArgs: string + }>('/api/getSSHConnectionInfo', data) export const getDevboxPodsByDevboxName = (name: string) => GET('/api/getDevboxPodsByDevboxName', { name }).then((item) => item.map(adaptPod)) diff --git a/frontend/providers/devbox/api/template.ts b/frontend/providers/devbox/api/template.ts index 454568a3fc2..cdb3e6096cc 100644 --- a/frontend/providers/devbox/api/template.ts +++ b/frontend/providers/devbox/api/template.ts @@ -1,20 +1,32 @@ -import { Tag, TemplateRepositoryKind } from "@/prisma/generated/client"; -import { DELETE, GET, POST } from "@/services/request"; -import { CreateTemplateRepositoryType, UpdateTemplateRepositoryType, UpdateTemplateType } from "@/utils/vaildate"; +import { Tag, TemplateRepositoryKind } from '@/prisma/generated/client' +import { DELETE, GET, POST } from '@/services/request' +import { + CreateTemplateRepositoryType, + UpdateTemplateRepositoryType, + UpdateTemplateType +} from '@/utils/vaildate' -export const listOfficialTemplateRepository = () => GET<{ - templateRepositoryList: { - uid: string; - name: string; - kind: TemplateRepositoryKind; - iconId: string; - description: string | null; - }[] -}>(`/api/templateRepository/listOfficial`) -export const listTemplateRepository = (page: { - page: number, - pageSize: number, -}, tags?: string[], search?: string) => { +export const listOfficialTemplateRepository = () => + GET<{ + templateRepositoryList: { + uid: string + name: string + kind: TemplateRepositoryKind + iconId: string + description: string | null + templateRepositoryTags: { + tag: Tag + }[] + }[] + }>(`/api/templateRepository/listOfficial`) +export const listTemplateRepository = ( + page: { + page: number + pageSize: number + }, + tags?: string[], + search?: string +) => { const searchParams = new URLSearchParams() if (tags && tags.length > 0) { tags.forEach((tag) => { @@ -26,35 +38,34 @@ export const listTemplateRepository = (page: { if (search) searchParams.append('search', search) return GET<{ templateRepositoryList: { - uid: string; - name: string; - description: string | null; - iconId: string | null; + uid: string + name: string + description: string | null + iconId: string | null templates: { - uid: string; - name: string; - }[]; + uid: string + name: string + }[] templateRepositoryTags: { - tag: Tag; - }[]; - }[], + tag: Tag + }[] + }[] page: { - page: number, - pageSize: number, - totalItems: number, - totalPage: number, + page: number + pageSize: number + totalItems: number + totalPage: number } }>(`/api/templateRepository/list?${searchParams.toString()}`) - } export const listPrivateTemplateRepository = ({ search, page, - pageSize, + pageSize }: { - search?: string, - page?: number, - pageSize?: number, + search?: string + page?: number + pageSize?: number } = {}) => { const searchParams = new URLSearchParams() @@ -63,70 +74,79 @@ export const listPrivateTemplateRepository = ({ if (pageSize) searchParams.append('pageSize', pageSize.toString()) return GET<{ templateRepositoryList: { - uid: string; - name: string; - description: string | null; - iconId: string | null; + uid: string + name: string + description: string | null + iconId: string | null templates: { - uid: string; - name: string; - }[]; - isPublic: boolean; + uid: string + name: string + }[] + isPublic: boolean templateRepositoryTags: { - tag: Tag; - }[]; - }[], + tag: Tag + }[] + }[] page: { - page: number, - pageSize: number, - totalItems: number, - totalPage: number, + page: number + pageSize: number + totalItems: number + totalPage: number } }>(`/api/templateRepository/listPrivate?${searchParams.toString()}`) } -export const getTemplateRepository = (uid: string) => GET<{ - templateRepository: { - templates: { - name: string; - uid: string; - }[]; - uid: string; - isPublic: true; - name: string; - description: string | null; - iconId: string | null; - templateRepositoryTags: { - tag: Tag; - }[]; - } -}>(`/api/templateRepository/get?uid=${uid}`) -export const getTemplateConfig = (uid: string) => GET<{ - template: { - name: string; - uid: string; - config: string; - } -}>(`/api/templateRepository/template/getConfig?uid=${uid}`) -export const listTemplate = (templateRepositoryUid: string) => GET<{ - templateList: { - uid: string; - name: string; - config: string; - image: string; - createAt: Date; - updateAt: Date; - }[] -}>(`/api/templateRepository/template/list?templateRepositoryUid=${templateRepositoryUid}`) -export const listTag = () => GET<{ - tagList: Tag[] -}>(`/api/templateRepository/tag/list`) +export const getTemplateRepository = (uid: string) => + GET<{ + templateRepository: { + templates: { + name: string + uid: string + }[] + uid: string + isPublic: true + name: string + description: string | null + iconId: string | null + templateRepositoryTags: { + tag: Tag + }[] + } + }>(`/api/templateRepository/get?uid=${uid}`) +export const getTemplateConfig = (uid: string) => + GET<{ + template: { + name: string + uid: string + config: string + } + }>(`/api/templateRepository/template/getConfig?uid=${uid}`) +export const listTemplate = (templateRepositoryUid: string) => + GET<{ + templateList: { + uid: string + name: string + config: string + image: string + createAt: Date + updateAt: Date + }[] + }>(`/api/templateRepository/template/list?templateRepositoryUid=${templateRepositoryUid}`) +export const listTag = () => + GET<{ + tagList: Tag[] + }>(`/api/templateRepository/tag/list`) -export const createTemplateReposistory = (data: CreateTemplateRepositoryType) => POST(`/api/templateRepository/withTemplate/create`, data) +export const createTemplateReposistory = (data: CreateTemplateRepositoryType) => + POST(`/api/templateRepository/withTemplate/create`, data) export const initUser = () => POST(`/api/auth/init`) -export const deleteTemplateRepository = (templateRepositoryUid: string) => DELETE(`/api/templateRepository/delete?templateRepositoryUid=${templateRepositoryUid}`) +export const deleteTemplateRepository = (templateRepositoryUid: string) => + DELETE(`/api/templateRepository/delete?templateRepositoryUid=${templateRepositoryUid}`) -export const updateTemplateReposistory = (data: UpdateTemplateRepositoryType) => POST(`/api/templateRepository/update`, data) -export const updateTemplate = (data: UpdateTemplateType) => POST(`/api/templateRepository/withTemplate/update`, data) -export const deleteTemplate = (templateUid: string) => DELETE(`/api/templateRepository/template/delete?uid=${templateUid}`) \ No newline at end of file +export const updateTemplateReposistory = (data: UpdateTemplateRepositoryType) => + POST(`/api/templateRepository/update`, data) +export const updateTemplate = (data: UpdateTemplateType) => + POST(`/api/templateRepository/withTemplate/update`, data) +export const deleteTemplate = (templateUid: string) => + DELETE(`/api/templateRepository/template/delete?uid=${templateUid}`) diff --git a/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/form/BasicConfiguration/GpuSelector.tsx b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/form/BasicConfiguration/GpuSelector.tsx new file mode 100644 index 00000000000..edb5792a989 --- /dev/null +++ b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/form/BasicConfiguration/GpuSelector.tsx @@ -0,0 +1,156 @@ +import { useMemo } from 'react' +import { useTranslations } from 'next-intl' +import { useFormContext } from 'react-hook-form' +import { useQuery } from '@tanstack/react-query' +import { Box, Center, Flex } from '@chakra-ui/react' +import { MySelect, MyTooltip } from '@sealos/ui' + +import Label from '../Label' +import { usePriceStore } from '@/stores/price' +import { DevboxEditTypeV2 } from '@/types/devbox' +import { GpuAmountMarkList } from '@/constants/devbox' +import { listOfficialTemplateRepository } from '@/api/template' + +const labelWidth = 100 + +export default function GpuSelector({ + countGpuInventory +}: { + countGpuInventory: (type: string) => number +}) { + const t = useTranslations() + const { sourcePrice } = usePriceStore() + const { watch, setValue, getValues } = useFormContext() + const templateRepositoryQuery = useQuery( + ['list-official-template-repository'], + listOfficialTemplateRepository + ) + const templateData = useMemo( + () => templateRepositoryQuery.data?.templateRepositoryList || [], + [templateRepositoryQuery.data] + ) + const templateRepositoryUid = getValues('templateRepositoryUid') + const isGpuTemplate = useMemo(() => { + const template = templateData.find((item) => item.uid === templateRepositoryUid) + return template?.templateRepositoryTags.some((item) => item.tag.name === 'gpu') + }, [templateData, templateRepositoryUid]) + + const selectedGpu = () => { + const selected = sourcePrice?.gpu?.find((item) => item.type === getValues('gpu.type')) + if (!selected) return + return { + ...selected, + inventory: countGpuInventory(selected.type) + } + } + + // add NoGPU select item + const gpuSelectList = useMemo( + () => + sourcePrice?.gpu + ? [ + { + label: t('No GPU'), + value: '' + }, + ...sourcePrice.gpu.map((item) => ({ + icon: 'nvidia', + label: ( + + {item.alias} + + | + + + {t('vm')} : {Math.round(item.vm)}G + + + | + + + {t('Inventory')} :  + {countGpuInventory(item.type)} + + + ), + value: item.type + })) + ] + : [], + [countGpuInventory, t, sourcePrice?.gpu] + ) + + if (!isGpuTemplate || !sourcePrice?.gpu) { + return null + } + + return ( + + + + { + const selected = sourcePrice?.gpu?.find((item) => item.type === type) + const inventory = countGpuInventory(type) + if (type === '' || (selected && inventory > 0)) { + setValue('gpu.type', type) + } + }} + /> + + {!!getValues('gpu.type') && ( + + {t('Amount')} + + {GpuAmountMarkList.map((item) => { + const inventory = selectedGpu()?.inventory || 0 + + const hasInventory = item.value <= inventory + + return ( + +
{ + setValue('gpu.amount', item.value) + } + } + : { + cursor: 'default', + opacity: 0.5 + })}> + {item.label} +
+
+ ) + })} + + / {t('Card')} + +
+
+ )} +
+ ) +} diff --git a/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/form/BasicConfiguration/TemplateRepositorySelector/TemplateReposistoryItem.tsx b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/form/BasicConfiguration/TemplateRepositorySelector/TemplateReposistoryItem.tsx index 05ef402d9c6..94bd9be484b 100644 --- a/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/form/BasicConfiguration/TemplateRepositorySelector/TemplateReposistoryItem.tsx +++ b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/form/BasicConfiguration/TemplateRepositorySelector/TemplateReposistoryItem.tsx @@ -1,68 +1,71 @@ -import { useDevboxStore } from "@/stores/devbox"; -import { DevboxEditTypeV2 } from "@/types/devbox"; -import { Center, Img, Text } from "@chakra-ui/react"; -import { useMessage } from "@sealos/ui"; -import { useTranslations } from "next-intl"; -import { useFormContext } from "react-hook-form"; +import { useDevboxStore } from '@/stores/devbox' +import { DevboxEditTypeV2 } from '@/types/devbox' +import { Center, Img, Text } from '@chakra-ui/react' +import { useMessage } from '@sealos/ui' +import { useTranslations } from 'next-intl' +import { useFormContext } from 'react-hook-form' -export default function TemplateRepositoryItem({ item, isEdit }: { item: { uid: string, iconId: string, name: string }; isEdit: boolean}) { +export default function TemplateRepositoryItem({ + item, + isEdit +}: { + item: { uid: string; iconId: string; name: string } + isEdit: boolean +}) { const { message: toast } = useMessage() const t = useTranslations() const { getValues, setValue, watch } = useFormContext() const { startedTemplate, setStartedTemplate } = useDevboxStore() - return
{ + if (isEdit) return + const devboxName = getValues('name') + if (!devboxName) { + toast({ + title: t('Please enter the devbox name first'), + status: 'warning' + }) + return } - })} - onClick={() =>{ - if (isEdit) return - const devboxName = getValues('name') - if (!devboxName) { - toast({ - title: t('Please enter the devbox name first'), - status: 'warning' - }) - return - } - if (startedTemplate && startedTemplate.uid !== item.uid) { - setStartedTemplate(undefined) - } - setValue('templateRepositoryUid', item.uid) - }} - > - {item.uid} - - {item.name} - -
-} \ No newline at end of file + setValue('gpu.type', '') + if (startedTemplate && startedTemplate.uid !== item.uid) { + setStartedTemplate(undefined) + } + setValue('templateRepositoryUid', item.uid) + }}> + {item.uid} + + {item.name} + + + ) +} diff --git a/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/form/BasicConfiguration/index.tsx b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/form/BasicConfiguration/index.tsx index 6c5c712c3bb..3f642d92a56 100644 --- a/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/form/BasicConfiguration/index.tsx +++ b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/form/BasicConfiguration/index.tsx @@ -1,31 +1,41 @@ -import MyIcon from "@/components/Icon" -import { Box, BoxProps } from "@chakra-ui/react" -import { useTranslations } from "next-intl" -import ConfigurationHeader from "../ConfigurationHeader" -import CpuSelector from "./CpuSelector" -import DevboxNameInput from "./DevboxNameInput" -import MemorySelector from "./MemorySelector" -import TemplateRepositorySelector from "./TemplateRepositorySelector" -import TemplateSelector from "./TemplateSelector" +import MyIcon from '@/components/Icon' +import { Box, BoxProps } from '@chakra-ui/react' +import { useTranslations } from 'next-intl' -export default function BasicConfiguration({ isEdit, ...props }: BoxProps & { isEdit: boolean }) { +import CpuSelector from './CpuSelector' +import GpuSelector from './GpuSelector' +import MemorySelector from './MemorySelector' +import DevboxNameInput from './DevboxNameInput' +import TemplateSelector from './TemplateSelector' +import ConfigurationHeader from '../ConfigurationHeader' +import TemplateRepositorySelector from './TemplateRepositorySelector' + +export default function BasicConfiguration({ + isEdit, + countGpuInventory, + ...props +}: BoxProps & { isEdit: boolean; countGpuInventory: (type: string) => number }) { const t = useTranslations() - return - - - {t('basic_configuration')} - - - {/* Devbox Name */} - - {/* Template Repository */} - - {/* Runtime Version */} - - {/* CPU */} - - {/* Memory */} - + return ( + + + + {t('basic_configuration')} + + + {/* Devbox Name */} + + {/* Template Repository */} + + {/* Runtime Version */} + + {/* GPU */} + + {/* CPU */} + + {/* Memory */} + + - -} \ No newline at end of file + ) +} diff --git a/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/form/index.tsx b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/form/index.tsx index edfcd924487..8717db72d5c 100644 --- a/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/form/index.tsx +++ b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/form/index.tsx @@ -1,11 +1,6 @@ 'use client' -import { - Box, - Flex, - Grid, - useTheme -} from '@chakra-ui/react' +import { Box, Flex, Grid, useTheme } from '@chakra-ui/react' import { Tabs } from '@sealos/ui' import { throttle } from 'lodash' import { useTranslations } from 'next-intl' @@ -26,17 +21,17 @@ import NetworkConfiguration from './NetworkConfiguration' const Form = ({ pxVal, - isEdit + isEdit, + countGpuInventory }: { pxVal: number isEdit: boolean + countGpuInventory: (type: string) => number }) => { const theme = useTheme() const router = useRouter() const t = useTranslations() - const { - watch - } = useFormContext() + const { watch } = useFormContext() const navList: { id: string; label: string; icon: string }[] = [ { id: 'baseInfo', @@ -79,7 +74,6 @@ const Form = ({ // eslint-disable-next-line }, []) - const boxStyles = { border: theme.borders.base, borderRadius: 'lg', @@ -88,100 +82,105 @@ const Form = ({ } return ( - - {/* left sidebar */} - - + {/* left sidebar */} + + + router.replace( + `/devbox/create?${obj2Query({ + type: 'yaml' + })}` + ) + } + /> + + {navList.map((item) => ( + { + setActiveNav(item.id) + window.location.hash = item.id + }}> + + + + {item.label} + + + ))} + + + + + + - router.replace( - `/devbox/create?${obj2Query({ - type: 'yaml' - })}` - ) - } /> - - {navList.map((item) => ( - { - setActiveNav(item.id) - window.location.hash = item.id - }}> - - - - {item.label} - - - ))} - - - - - - - - - {/* right content */} - - {/* base info */} - - {/* network */} - - + + {/* right content */} + + {/* base info */} + + {/* network */} + + + ) } diff --git a/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/page.tsx b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/page.tsx index e0c6940625b..c95316d063e 100644 --- a/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/page.tsx +++ b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/page.tsx @@ -25,6 +25,7 @@ import { useEnvStore } from '@/stores/env' import { useGlobalStore } from '@/stores/global' import { useIDEStore } from '@/stores/ide' import { useUserStore } from '@/stores/user' +import { usePriceStore } from '@/stores/price' import { createDevbox, updateDevbox } from '@/api/devbox' import { defaultDevboxEditValueV2, editModeMap } from '@/constants/devbox' @@ -35,13 +36,16 @@ import { debounce } from 'lodash' const ErrorModal = dynamic(() => import('@/components/modals/ErrorModal')) const DevboxCreatePage = () => { - const {env } = useEnvStore() + const { env } = useEnvStore() const generateDefaultYamlList = () => generateYamlList(defaultDevboxEditValueV2, env) const router = useRouter() const t = useTranslations() + const { Loading, setIsLoading } = useLoading() + const searchParams = useSearchParams() const { message: toast } = useMessage() const { addDevboxIDE } = useIDEStore() + const { sourcePrice, setSourcePrice } = usePriceStore() const { checkQuotaAllow } = useUserStore() const { setDevboxDetail, devboxList } = useDevboxStore() @@ -49,7 +53,6 @@ const DevboxCreatePage = () => { const formOldYamls = useRef([]) const oldDevboxEditData = useRef() - const { Loading, setIsLoading } = useLoading() const [errorMessage, setErrorMessage] = useState('') const [yamlList, setYamlList] = useState([]) @@ -94,7 +97,7 @@ const DevboxCreatePage = () => { // updateyamlList every time yamlList change const debouncedUpdateYaml = useMemo( - () => + () => debounce((data: DevboxEditTypeV2, env) => { try { const newYamlList = generateYamlList(data, env) @@ -106,6 +109,15 @@ const DevboxCreatePage = () => { [] ) + const countGpuInventory = useCallback( + (type?: string) => { + const inventory = sourcePrice?.gpu?.find((item) => item.type === type)?.inventory || 0 + + return inventory + }, + [sourcePrice?.gpu] + ) + // 监听表单变化 useEffect(() => { const subscription = formHook.watch((value) => { @@ -119,6 +131,10 @@ const DevboxCreatePage = () => { } }, [formHook, debouncedUpdateYaml, env]) + const { refetch: refetchPrice } = useQuery(['init-price'], setSourcePrice, { + enabled: !!sourcePrice?.gpu, + refetchInterval: 6000 + }) useQuery( ['initDevboxCreateData'], @@ -154,6 +170,18 @@ const DevboxCreatePage = () => { const submitSuccess = async (formData: DevboxEditTypeV2) => { setIsLoading(true) try { + // gpu inventory check + if (formData.gpu?.type) { + const inventory = countGpuInventory(formData.gpu?.type) + if (formData.gpu?.amount > inventory) { + return toast({ + status: 'warning', + title: t('Gpu under inventory Tip', { + gputype: formData.gpu.type + }) + }) + } + } // quote check const quoteCheckRes = checkQuotaAllow( { ...formData, nodeports: devboxList.length + 1 } as DevboxEditTypeV2 & { @@ -193,8 +221,8 @@ const DevboxCreatePage = () => { isClosable: true }) } - if(!parsedNewYamlList) { - // prevent empty yamlList + if (!parsedNewYamlList) { + // prevent empty yamlList return setErrorMessage(t('submit_form_error')) } const patch = patchYamlList({ @@ -210,6 +238,7 @@ const DevboxCreatePage = () => { await createDevbox({ devboxForm: formData }) } addDevboxIDE('vscode', formData.name) + addDevboxIDE('vscode', formData.name) toast({ title: t(applySuccess), status: 'success' @@ -218,14 +247,15 @@ const DevboxCreatePage = () => { ...templateConfig, lastRoute }) + if (sourcePrice?.gpu) { + refetchPrice() + } router.push(lastRoute) } catch (error) { console.log('error', error) if (error instanceof String && error.includes('402')) { setErrorMessage(t('outstanding_tips')) - } - else - setErrorMessage(JSON.stringify(error)) + } else setErrorMessage(JSON.stringify(error)) } setIsLoading(false) } @@ -250,38 +280,42 @@ const DevboxCreatePage = () => { }) }, [formHook.formState.errors, toast, t]) - return (<> - - -
- formHook.handleSubmit((data) => openConfirm(() => submitSuccess(data))(), submitError)() - } - /> - - {tabType === 'form' ? ( -
- ) : ( - - )} - - - - - + return ( + <> + + +
+ formHook.handleSubmit( + (data) => openConfirm(() => submitSuccess(data))(), + submitError + )() + } + /> + + {tabType === 'form' ? ( + + ) : ( + + )} + + + + + - {!!errorMessage && ( - setErrorMessage('')} /> - )} - + {!!errorMessage && ( + setErrorMessage('')} /> + )} + ) } diff --git a/frontend/providers/devbox/app/[lang]/(platform)/devbox/detail/[name]/components/BasicInfo.tsx b/frontend/providers/devbox/app/[lang]/(platform)/devbox/detail/[name]/components/BasicInfo.tsx index c24a8755923..9ebc8d4f60a 100644 --- a/frontend/providers/devbox/app/[lang]/(platform)/devbox/detail/[name]/components/BasicInfo.tsx +++ b/frontend/providers/devbox/app/[lang]/(platform)/devbox/detail/[name]/components/BasicInfo.tsx @@ -4,22 +4,24 @@ import { useTranslations } from 'next-intl' import { useCallback, useState } from 'react' import MyIcon from '@/components/Icon' - +import GPUItem from '@/components/GPUItem' import { DevboxDetailType } from '@/types/devbox' -import { useDevboxStore } from '@/stores/devbox' import { useEnvStore } from '@/stores/env' +import { usePriceStore } from '@/stores/price' +import { useDevboxStore } from '@/stores/devbox' const BasicInfo = () => { const t = useTranslations() const { message: toast } = useMessage() const { env } = useEnvStore() + const { sourcePrice } = usePriceStore() const { devboxDetail } = useDevboxStore() // const { getRuntimeDetailLabel } = useRuntimeStore() const [loading, setLoading] = useState(false) - + const handleCopySSHCommand = useCallback(() => { const sshCommand = `ssh -i yourPrivateKeyPath ${devboxDetail?.sshConfig?.sshUser}@${env.sealosDomain} -p ${devboxDetail?.sshPort}` navigator.clipboard.writeText(sshCommand).then(() => { @@ -105,14 +107,10 @@ const BasicInfo = () => { {t('start_runtime')} - + { - // getRuntimeDetailLabel(devboxDetail?., devboxDetail?.runtimeVersion) - `${devboxDetail?.templateRepositoryName}-${devboxDetail?.templateName}` + // getRuntimeDetailLabel(devboxDetail?., devboxDetail?.runtimeVersion) + `${devboxDetail?.templateRepositoryName}-${devboxDetail?.templateName}` } @@ -141,6 +139,16 @@ const BasicInfo = () => { {(devboxDetail?.memory || 0) / 1024} G + {sourcePrice?.gpu && ( + + + GPU + + + + + + )} {/* ssh config */} diff --git a/frontend/providers/devbox/app/[lang]/(platform)/devbox/detail/[name]/components/Version.tsx b/frontend/providers/devbox/app/[lang]/(platform)/devbox/detail/[name]/components/Version.tsx index ef511e4885b..cc9490ab797 100644 --- a/frontend/providers/devbox/app/[lang]/(platform)/devbox/detail/[name]/components/Version.tsx +++ b/frontend/providers/devbox/app/[lang]/(platform)/devbox/detail/[name]/components/Version.tsx @@ -42,8 +42,10 @@ const Version = () => { const [apps, setApps] = useState([]) const [deployData, setDeployData] = useState(null) const [currentVersion, setCurrentVersion] = useState(null) - const [updateTemplateRepo, setUpdateTemplateRepo] = useState>['templateRepositoryList'][number]>(null) + const [updateTemplateRepo, setUpdateTemplateRepo] = useState< + | null + | Awaited>['templateRepositoryList'][number] + >(null) const createTemplateModalHandler = useDisclosure() const selectTemplalteModalHandler = useDisclosure() const updateTemplateModalHandler = useDisclosure() @@ -56,10 +58,10 @@ const Version = () => { { refetchInterval: devboxVersionList.length > 0 && - !createTemplateModalHandler.isOpen && - !updateTemplateModalHandler.isOpen && - !selectTemplalteModalHandler.isOpen && - devboxVersionList[0].status.value === DevboxReleaseStatusEnum.Pending + !createTemplateModalHandler.isOpen && + !updateTemplateModalHandler.isOpen && + !selectTemplalteModalHandler.isOpen && + devboxVersionList[0].status.value === DevboxReleaseStatusEnum.Pending ? 3000 : false, onSettled() { @@ -73,11 +75,12 @@ const Version = () => { () => { return listPrivateTemplateRepository({ page: 1, - pageSize: 100, + pageSize: 100 }) } ) - const templateRepositoryList = listPrivateTemplateRepositoryQuery.data?.templateRepositoryList || [] + const templateRepositoryList = + listPrivateTemplateRepositoryQuery.data?.templateRepositoryList || [] const handleDeploy = useCallback( async (version: DevboxVersionListItemType) => { // const { releaseCommand, releaseArgs } = await getSSHRuntimeInfo(devbox.runtimeVersion) @@ -106,13 +109,13 @@ const Version = () => { newNetworks.length > 0 ? newNetworks : [ - { - port: 80, - protocol: 'http', - openPublicDomain: false, - domain: env.ingressDomain - } - ], + { + port: 80, + protocol: 'http', + openPublicDomain: false, + domain: env.ingressDomain + } + ], runCMD: releaseCommand, cmdParam: releaseArgs, labels: { @@ -183,133 +186,128 @@ const Version = () => { key: string render?: (item: DevboxVersionListItemType) => JSX.Element }[] = [ - { - title: t('version_number'), - key: 'tag', - render: (item: DevboxVersionListItemType) => ( - - {item.tag} + { + title: t('version_number'), + key: 'tag', + render: (item: DevboxVersionListItemType) => ( + + {item.tag} + + ) + }, + { + title: t('status'), + key: 'status', + render: (item: DevboxVersionListItemType) => ( + + ) + }, + { + title: t('create_time'), + dataIndex: 'createTime', + key: 'createTime', + render: (item: DevboxVersionListItemType) => { + return {item.createTime} + } + }, + { + title: t('version_description'), + key: 'description', + render: (item: DevboxVersionListItemType) => ( + + + {item.description} - ) - }, - { - title: t('status'), - key: 'status', - render: (item: DevboxVersionListItemType) => ( - - ) - }, - { - title: t('create_time'), - dataIndex: 'createTime', - key: 'createTime', - render: (item: DevboxVersionListItemType) => { - return {item.createTime} - } - }, - { - title: t('version_description'), - key: 'description', - render: (item: DevboxVersionListItemType) => ( - - - {item.description} - - - ) - }, - { - title: t('control'), - key: 'control', - render: (item: DevboxVersionListItemType) => ( - - - - + ) + }, + { + title: t('control'), + key: 'control', + render: (item: DevboxVersionListItemType) => ( + + + + - - } - menuList={[ - { - child: ( - <> - - {t('edit')} - - ), - onClick: () => { - setCurrentVersion(item) - onOpenEdit() + /> + + } + menuList={[ + { + child: ( + <> + + {t('edit')} + + ), + onClick: () => { + setCurrentVersion(item) + onOpenEdit() + } + }, + { + child: ( + <> + + {t('convert_to_runtime')} + + ), + onClick: () => { + setCurrentVersion(item) + // onOpenEdit() + // openTemplateModal({templateState: }) + if (templateRepositoryList.length > 0) { + selectTemplalteModalHandler.onOpen() + } else { + createTemplateModalHandler.onOpen() } - }, - { - child: ( - <> - - {t('convert_to_runtime')} - - ), - onClick: () => { - setCurrentVersion(item) - // onOpenEdit() - // openTemplateModal({templateState: }) - if (templateRepositoryList.length > 0) { - selectTemplalteModalHandler.onOpen() - } else { - createTemplateModalHandler.onOpen() - } + } + }, + { + child: ( + <> + + {t('delete')} + + ), + menuItemStyle: { + _hover: { + color: 'red.600', + bg: 'rgba(17, 24, 36, 0.05)' } }, - { - child: ( - <> - - {t('delete')} - - ), - menuItemStyle: { - _hover: { - color: 'red.600', - bg: 'rgba(17, 24, 36, 0.05)' - } - }, - onClick: () => openConfirm(() => handleDelDevboxVersion(item.name))() - } - ] + onClick: () => openConfirm(() => handleDelDevboxVersion(item.name))() } - /> - - ) - } - ] + ]} + /> + + ) + } + ] return ( { ) : ( - + )} {!!currentVersion && ( { /> )} - - {templateRepositoryList.length > 0 && { - const repo = templateRepositoryList.find((item) => item.uid === uid) - setUpdateTemplateRepo(repo || null) - updateTemplateModalHandler.onOpen() - }} - templateRepositoryList={templateRepositoryList} - isOpen={selectTemplalteModalHandler.isOpen} onClose={ - selectTemplalteModalHandler.onClose - } />} - {!!updateTemplateRepo && } - + /> + {templateRepositoryList.length > 0 && ( + { + const repo = templateRepositoryList.find((item) => item.uid === uid) + setUpdateTemplateRepo(repo || null) + updateTemplateModalHandler.onOpen() + }} + templateRepositoryList={templateRepositoryList} + isOpen={selectTemplalteModalHandler.isOpen} + onClose={selectTemplalteModalHandler.onClose} + /> + )} + {!!updateTemplateRepo && ( + + )} ) } diff --git a/frontend/providers/devbox/app/api/createDevbox/route.ts b/frontend/providers/devbox/app/api/createDevbox/route.ts index a28e5c95d53..c5943225e1f 100644 --- a/frontend/providers/devbox/app/api/createDevbox/route.ts +++ b/frontend/providers/devbox/app/api/createDevbox/route.ts @@ -27,11 +27,17 @@ export async function POST(req: NextRequest) { 'devbox.sealos.io', 'v1alpha1', namespace, - 'devboxes', - )) as { body: { - items: KBDevboxTypeV2[] - } } - if(!!devboxListBody && devboxListBody.items.length > 0 && devboxListBody.items.find((item) => item.metadata.name === devboxForm.name)) { + 'devboxes' + )) as { + body: { + items: KBDevboxTypeV2[] + } + } + if ( + !!devboxListBody && + devboxListBody.items.length > 0 && + devboxListBody.items.find((item) => item.metadata.name === devboxForm.name) + ) { return jsonRes({ code: 409, error: 'Devbox already exists' @@ -40,8 +46,8 @@ export async function POST(req: NextRequest) { const template = await devboxDB.template.findUnique({ where: { uid: devboxForm.templateUid, - isDeleted: false, - }, + isDeleted: false + } }) if (!template) { return jsonRes({ @@ -50,11 +56,7 @@ export async function POST(req: NextRequest) { }) } const { INGRESS_SECRET, DEVBOX_AFFINITY_ENABLE, SQUASH_ENABLE } = process.env - const devbox = json2DevboxV2( - devboxForm, - DEVBOX_AFFINITY_ENABLE, - SQUASH_ENABLE - ) + const devbox = json2DevboxV2(devboxForm, DEVBOX_AFFINITY_ENABLE, SQUASH_ENABLE) const service = json2Service(devboxForm) const ingress = json2Ingress(devboxForm, INGRESS_SECRET as string) await applyYamlList([devbox, service, ingress], 'create') @@ -68,4 +70,4 @@ export async function POST(req: NextRequest) { error: err }) } -} \ No newline at end of file +} diff --git a/frontend/providers/devbox/app/api/getDevboxByName/route.ts b/frontend/providers/devbox/app/api/getDevboxByName/route.ts index dc5fc7f7855..4c9aad744a2 100644 --- a/frontend/providers/devbox/app/api/getDevboxByName/route.ts +++ b/frontend/providers/devbox/app/api/getDevboxByName/route.ts @@ -13,7 +13,6 @@ export const dynamic = 'force-dynamic' export async function GET(req: NextRequest) { try { const headerList = req.headers - const { ROOT_RUNTIME_NAMESPACE } = process.env const { searchParams } = req.nextUrl const devboxName = searchParams.get('devboxName') as string @@ -38,7 +37,7 @@ export async function GET(req: NextRequest) { )) as { body: KBDevboxTypeV2 } const template = await devboxDB.template.findUnique({ where: { - uid: devboxBody.spec.templateID, + uid: devboxBody.spec.templateID }, select: { templateRepository: { @@ -46,12 +45,12 @@ export async function GET(req: NextRequest) { uid: true, iconId: true, name: true, - kind: true, + kind: true } }, uid: true, image: true, - name: true, + name: true } }) if (!template) { @@ -63,14 +62,9 @@ export async function GET(req: NextRequest) { const label = `${devboxKey}=${devboxName}` // get ingresses and service const [ingressesResponse, serviceResponse] = await Promise.all([ - k8sNetworkingApp.listNamespacedIngress( - namespace, - undefined, - undefined, - undefined, - undefined, - label - ).catch(() => null), + k8sNetworkingApp + .listNamespacedIngress(namespace, undefined, undefined, undefined, undefined, label) + .catch(() => null), k8sCore.readNamespacedService(devboxName, namespace, undefined).catch(() => null) ]) const ingresses = ingressesResponse?.body.items || [] @@ -89,8 +83,9 @@ export async function GET(req: NextRequest) { } }) - const portInfos: PortInfos = service?.spec?.ports?.map((svcport) => { - const ingressInfo = ingressList.find((ingress) => ingress.port === svcport.port) + const portInfos: PortInfos = + service?.spec?.ports?.map((svcport) => { + const ingressInfo = ingressList.find((ingress) => ingress.port === svcport.port) return { portName: svcport.name!, port: svcport.port, @@ -100,12 +95,8 @@ export async function GET(req: NextRequest) { publicDomain: ingressInfo?.publicDomain, customDomain: ingressInfo?.customDomain } - }) || [] - const resp = [ - devboxBody, - portInfos, - template - ] as const + }) || [] + const resp = [devboxBody, portInfos, template] as const return jsonRes({ data: resp }) } catch (err: any) { return jsonRes({ diff --git a/frontend/providers/devbox/app/api/platform/getQuota/route.ts b/frontend/providers/devbox/app/api/platform/getQuota/route.ts index eedfd8e6c31..5f0bee7c97b 100644 --- a/frontend/providers/devbox/app/api/platform/getQuota/route.ts +++ b/frontend/providers/devbox/app/api/platform/getQuota/route.ts @@ -14,11 +14,15 @@ export async function GET(req: NextRequest) { kubeconfig: await authSession(headerList) }) + const { GPU_ENABLE } = process.env + const quota = await getUserQuota() + const filteredQuota = GPU_ENABLE ? quota : quota.filter((item) => item.type !== 'gpu') + return jsonRes({ data: { - quota + quota: filteredQuota } }) } catch (error) { diff --git a/frontend/providers/devbox/app/api/platform/getRuntime/route.ts b/frontend/providers/devbox/app/api/platform/getRuntime/route.ts deleted file mode 100644 index a5f77c5c890..00000000000 --- a/frontend/providers/devbox/app/api/platform/getRuntime/route.ts +++ /dev/null @@ -1,181 +0,0 @@ -import { NextRequest } from 'next/server' - -import { authSession } from '@/services/backend/auth' -import { getK8s } from '@/services/backend/kubernetes' -import { jsonRes } from '@/services/backend/response' -import { defaultEnv } from '@/stores/env' -import { runtimeNamespaceMapType, ValueType, VersionMapType } from '@/types/devbox' -import { KBRuntimeClassType, KBRuntimeType } from '@/types/k8s' - -export const dynamic = 'force-dynamic' - -export async function GET(req: NextRequest) { - try { - const languageTypeList: ValueType[] = [] - const frameworkTypeList: ValueType[] = [] - const osTypeList: ValueType[] = [] - const languageVersionMap: VersionMapType = {} - const frameworkVersionMap: VersionMapType = {} - const osVersionMap: VersionMapType = {} - const runtimeNamespaceMap: runtimeNamespaceMapType = {} - - const { ROOT_RUNTIME_NAMESPACE } = process.env - - const headerList = req.headers - - const { k8sCustomObjects } = await getK8s({ - kubeconfig: await authSession(headerList) - }) - - const { body: runtimeClasses } = (await k8sCustomObjects.listNamespacedCustomObject( - 'devbox.sealos.io', - 'v1alpha1', - ROOT_RUNTIME_NAMESPACE || defaultEnv.rootRuntimeNamespace, - 'runtimeclasses' - )) as { body: { items: KBRuntimeClassType[] } } - const { body: _runtimes } = (await k8sCustomObjects.listNamespacedCustomObject( - 'devbox.sealos.io', - 'v1alpha1', - ROOT_RUNTIME_NAMESPACE || defaultEnv.rootRuntimeNamespace, - 'runtimes' - )) as { body: { items: KBRuntimeType[] } } - - let runtimes = _runtimes?.items?.filter((item) => item.spec.state === 'active') - - // runtimeClasses - const languageList = runtimeClasses?.items.filter((item: any) => item.spec.kind === 'Language') - languageTypeList.push( - ...languageList.map((item: any) => { - return { - id: item.metadata.name, - label: item.spec.title - } - }) - ) - const frameworkList = runtimeClasses?.items.filter( - (item: any) => item.spec.kind === 'Framework' - ) - frameworkTypeList.push( - ...frameworkList.map((item: any) => { - return { - id: item.metadata.name, - label: item.spec.title - } - }) - ) - const osList = runtimeClasses?.items.filter((item: any) => item.spec.kind === 'OS') - osTypeList.push( - ...osList.map((item: any) => { - return { - id: item.metadata.name, - label: item.spec.title - } - }) - ) - - // runtimeVersions and runtimeNamespaceMap - languageList.forEach((item: any) => { - const language = item.metadata.name - const versions = runtimes.filter((runtime: any) => runtime.spec.classRef === language) - const defaultVersion = versions.find( - (v: any) => v.metadata.annotations?.['devbox.sealos.io/defaultVersion'] === 'true' - ) - const otherVersions = versions.filter( - (v: any) => v.metadata.annotations?.['devbox.sealos.io/defaultVersion'] !== 'true' - ) - const sortedVersions = defaultVersion ? [defaultVersion, ...otherVersions] : versions - - sortedVersions.forEach((version: any) => { - runtimeNamespaceMap[version.metadata.name] = item.metadata.namespace - }) - - languageVersionMap[language] = sortedVersions.map((version: any) => ({ - id: version.metadata.name, - label: version.spec.version, - defaultPorts: version.spec.config.appPorts.map((item: any) => item.port) - })) - if (languageVersionMap[language].length === 0) { - delete languageVersionMap[language] - const index = languageTypeList.findIndex((item) => item.id === language) - if (index !== -1) { - languageTypeList.splice(index, 1) - } - } - }) - - frameworkList.forEach((item: any) => { - const framework = item.metadata.name - const versions = runtimes.filter((runtime: any) => runtime.spec.classRef === framework) - const defaultVersion = versions.find( - (v: any) => v.metadata.annotations?.['devbox.sealos.io/defaultVersion'] === 'true' - ) - const otherVersions = versions.filter( - (v: any) => v.metadata.annotations?.['devbox.sealos.io/defaultVersion'] !== 'true' - ) - const sortedVersions = defaultVersion ? [defaultVersion, ...otherVersions] : versions - - sortedVersions.forEach((version: any) => { - runtimeNamespaceMap[version.metadata.name] = item.metadata.namespace - }) - - frameworkVersionMap[framework] = sortedVersions.map((version: any) => ({ - id: version.metadata.name, - label: version.spec.version, - defaultPorts: version.spec.config.appPorts.map((item: any) => item.port) - })) - if (frameworkVersionMap[framework].length === 0) { - delete frameworkVersionMap[framework] - const index = frameworkTypeList.findIndex((item) => item.id === framework) - if (index !== -1) { - frameworkTypeList.splice(index, 1) - } - } - }) - - osList.forEach((item: any) => { - const os = item.metadata.name - const versions = runtimes.filter((runtime: any) => runtime.spec.classRef === os) - const defaultVersion = versions.find( - (v: any) => v.metadata.annotations?.['devbox.sealos.io/defaultVersion'] === 'true' - ) - const otherVersions = versions.filter( - (v: any) => v.metadata.annotations?.['devbox.sealos.io/defaultVersion'] !== 'true' - ) - const sortedVersions = defaultVersion ? [defaultVersion, ...otherVersions] : versions - - sortedVersions.forEach((version: any) => { - runtimeNamespaceMap[version.metadata.name] = item.metadata.namespace - }) - - osVersionMap[os] = sortedVersions.map((version: any) => ({ - id: version.metadata.name, - label: version.spec.version, - defaultPorts: version.spec.config.appPorts.map((item: any) => item.port) - })) - if (osVersionMap[os].length === 0) { - delete osVersionMap[os] - const index = osTypeList.findIndex((item) => item.id === os) - if (index !== -1) { - osTypeList.splice(index, 1) - } - } - }) - - return jsonRes({ - data: { - languageVersionMap, - frameworkVersionMap, - osVersionMap, - languageTypeList, - frameworkTypeList, - osTypeList, - runtimeNamespaceMap - } - }) - } catch (error) { - return jsonRes({ - code: 500, - error: error - }) - } -} \ No newline at end of file diff --git a/frontend/providers/devbox/app/api/platform/resourcePrice/route.ts b/frontend/providers/devbox/app/api/platform/resourcePrice/route.ts index b0268eac3fe..4c4907078a6 100644 --- a/frontend/providers/devbox/app/api/platform/resourcePrice/route.ts +++ b/frontend/providers/devbox/app/api/platform/resourcePrice/route.ts @@ -1,7 +1,9 @@ import { NextRequest } from 'next/server' +import { CoreV1Api } from '@kubernetes/client-node' import { jsonRes } from '@/services/backend/response' import { userPriceType } from '@/types/user' +import { K8sApiDefault } from '@/services/backend/kubernetes' export const dynamic = 'force-dynamic' @@ -27,19 +29,28 @@ type ResourceType = | 'infra-disk' | 'services.nodeports' +type GpuNodeType = { + 'gpu.count': number + 'gpu.memory': number + 'gpu.product': string + 'gpu.alias': string +} + const PRICE_SCALE = 1000000 const valuationMap: Record = { cpu: 1000, memory: 1024, storage: 1024, + gpu: 1000, ['services.nodeports']: 1000 } export async function GET(req: NextRequest) { try { - const { ACCOUNT_URL, SEALOS_DOMAIN } = process.env + const { ACCOUNT_URL, SEALOS_DOMAIN, GPU_ENABLE } = process.env const baseUrl = ACCOUNT_URL ? ACCOUNT_URL : `https://account-api.${SEALOS_DOMAIN}` + const getResourcePrice = async () => { try { const res = await fetch(`${baseUrl}/account/v1alpha1/properties`, { @@ -54,12 +65,16 @@ export async function GET(req: NextRequest) { } } - const resp = (await getResourcePrice()) as ResourcePriceType['data']['properties'] + const [priceResponse, gpuNodes] = await Promise.all([ + getResourcePrice() as Promise, + GPU_ENABLE ? getGpuNode() : Promise.resolve([]) + ]) const data: userPriceType = { - cpu: countSourcePrice(resp, 'cpu'), - memory: countSourcePrice(resp, 'memory'), - nodeports: countSourcePrice(resp, 'services.nodeports') + cpu: countSourcePrice(priceResponse, 'cpu'), + memory: countSourcePrice(priceResponse, 'memory'), + nodeports: countSourcePrice(priceResponse, 'services.nodeports'), + gpu: GPU_ENABLE ? countGpuSource(priceResponse, gpuNodes) : undefined } return jsonRes({ @@ -70,6 +85,76 @@ export async function GET(req: NextRequest) { } } +/* get gpu nodes by configmap. */ +async function getGpuNode() { + const gpuCrName = 'node-gpu-info' + const gpuCrNS = 'node-system' + + try { + const kc = K8sApiDefault() + const { body } = await kc.makeApiClient(CoreV1Api).readNamespacedConfigMap(gpuCrName, gpuCrNS) + const gpuMap = body?.data?.gpu + + if (!gpuMap || !body?.data?.alias) return [] + const alias = (JSON.parse(body?.data?.alias) || {}) as Record + + const parseGpuMap = JSON.parse(gpuMap) as Record< + string, + { + 'gpu.count': string + 'gpu.memory': string + 'gpu.product': string + } + > + + const gpuValues = Object.values(parseGpuMap).filter((item) => item['gpu.product']) + + const gpuList: GpuNodeType[] = [] + + // merge same type gpu + gpuValues.forEach((item) => { + const index = gpuList.findIndex((gpu) => gpu['gpu.product'] === item['gpu.product']) + if (index > -1) { + gpuList[index]['gpu.count'] += Number(item['gpu.count']) + } else { + gpuList.push({ + ['gpu.count']: +item['gpu.count'], + ['gpu.memory']: +item['gpu.memory'], + ['gpu.product']: item['gpu.product'], + ['gpu.alias']: alias[item['gpu.product']] || item['gpu.product'] + }) + } + }) + + return gpuList + } catch (error) { + console.log('error', error) + return [] + } +} + +function countGpuSource(rawData: ResourcePriceType['data']['properties'], gpuNodes: GpuNodeType[]) { + const gpuList: userPriceType['gpu'] = [] + + // count gpu price by gpuNode and accountPriceConfig + rawData?.forEach((item) => { + if (!item.name.startsWith('gpu')) return + const gpuType = item.name.replace('gpu-', '') + const gpuNode = gpuNodes.find((item) => item['gpu.product'] === gpuType) + if (!gpuNode) return + + gpuList.push({ + alias: gpuNode['gpu.alias'], + type: gpuNode['gpu.product'], + price: (item.unit_price * valuationMap.gpu) / PRICE_SCALE, + inventory: +gpuNode['gpu.count'], + vm: +gpuNode['gpu.memory'] / 1024 + }) + }) + + return gpuList.length === 0 ? undefined : gpuList +} + function countSourcePrice(rawData: ResourcePriceType['data']['properties'], type: ResourceType) { const rawPrice = rawData.find((item) => item.name === type)?.unit_price || 1 const sourceScale = rawPrice * (valuationMap[type] || 1) diff --git a/frontend/providers/devbox/app/api/templateRepository/list/route.ts b/frontend/providers/devbox/app/api/templateRepository/list/route.ts index 3dcd8c999c3..603a09e5f16 100644 --- a/frontend/providers/devbox/app/api/templateRepository/list/route.ts +++ b/frontend/providers/devbox/app/api/templateRepository/list/route.ts @@ -9,33 +9,42 @@ export async function GET(req: NextRequest) { const searchParams = req.nextUrl.searchParams const tags = searchParams.getAll('tags') || [] const search = searchParams.get('search') || '' - const page = z.number().int().positive().safeParse(Number(searchParams.get('page'))).data || 1 - const pageSize = z.number().int().min(1).safeParse(Number(searchParams.get('pageSize'))).data || 30 + const page = + z + .number() + .int() + .positive() + .safeParse(Number(searchParams.get('page'))).data || 1 + const pageSize = + z + .number() + .int() + .min(1) + .safeParse(Number(searchParams.get('pageSize'))).data || 30 const dbquery: Prisma.TemplateRepositoryWhereInput = { - ...(tags && tags.length > 0 ? { - AND: tags.map((tag) => ({ - templateRepositoryTags: { - some: { - tagUid: tag + AND: tags.map((tag) => ({ + templateRepositoryTags: { + some: { + tagUid: tag + } } - } - })) - } + })) + } : {}), ...(search && search.length > 0 ? { - name: { - contains: search + name: { + contains: search + } } - } : {}) } - const [templateRepositoryList, totalItems] = await devboxDB.$transaction(async tx => { + const [templateRepositoryList, totalItems] = await devboxDB.$transaction(async (tx) => { const validRepoIds = await tx.template.findMany({ where: { - isDeleted: false, + isDeleted: false }, select: { templateRepositoryUid: true @@ -45,7 +54,7 @@ export async function GET(req: NextRequest) { const where: Prisma.TemplateRepositoryWhereInput = { uid: { - in: validRepoIds.map(r => r.templateRepositoryUid) + in: validRepoIds.map((r) => r.templateRepositoryUid) }, isPublic: true, isDeleted: false, @@ -57,27 +66,27 @@ export async function GET(req: NextRequest) { select: { organization: { select: { - name: true, + name: true } }, templateRepositoryTags: { select: { - tag: true, - }, + tag: true + } }, templates: { where: { - isDeleted: false, + isDeleted: false }, select: { name: true, - uid: true, + uid: true } }, name: true, uid: true, description: true, - iconId: true, + iconId: true }, skip: (page - 1) * pageSize, take: pageSize, @@ -88,7 +97,7 @@ export async function GET(req: NextRequest) { ] }), tx.templateRepository.count({ - where: dbquery, + where: dbquery }) ]) return [templateRepositoryList, totalItems] @@ -117,4 +126,4 @@ export async function GET(req: NextRequest) { error: err }) } -} \ No newline at end of file +} diff --git a/frontend/providers/devbox/app/api/templateRepository/listOfficial/route.ts b/frontend/providers/devbox/app/api/templateRepository/listOfficial/route.ts index 9318d393e3e..335cea2d6f0 100644 --- a/frontend/providers/devbox/app/api/templateRepository/listOfficial/route.ts +++ b/frontend/providers/devbox/app/api/templateRepository/listOfficial/route.ts @@ -10,7 +10,7 @@ export const GET = async function GET(req: NextRequest) { id: 'labring' } }) - if(!organization) throw Error('organization not found') + if (!organization) throw Error('organization not found') const templateRepositoryList = await devboxDB.templateRepository.findMany({ where: { isPublic: true, @@ -23,7 +23,12 @@ export const GET = async function GET(req: NextRequest) { name: true, uid: true, description: true, - }, + templateRepositoryTags: { + select: { + tag: true + } + } + } }) return jsonRes({ data: { @@ -36,4 +41,4 @@ export const GET = async function GET(req: NextRequest) { error: err }) } -} \ No newline at end of file +} diff --git a/frontend/providers/devbox/app/api/v1/getDBSecretList/route.ts b/frontend/providers/devbox/app/api/v1/getDBSecretList/route.ts index 04fa4bad6e5..ef0f4f39495 100644 --- a/frontend/providers/devbox/app/api/v1/getDBSecretList/route.ts +++ b/frontend/providers/devbox/app/api/v1/getDBSecretList/route.ts @@ -191,8 +191,6 @@ export async function GET(req: NextRequest) { const dbListResult = await Promise.all(dbList) - console.log('dbListResult', dbListResult) - return jsonRes({ data: { dbList: dbListResult diff --git a/frontend/providers/devbox/components/GPUItem.tsx b/frontend/providers/devbox/components/GPUItem.tsx new file mode 100644 index 00000000000..fb3947e2bc2 --- /dev/null +++ b/frontend/providers/devbox/components/GPUItem.tsx @@ -0,0 +1,38 @@ +import React, { useMemo } from 'react' +import { useTranslations } from 'next-intl' +import { Box, Flex } from '@chakra-ui/react' + +import MyIcon from './Icon' +import { GpuType } from '@/types/user' +import { usePriceStore } from '@/stores/price' + +const GPUItem = ({ gpu }: { gpu?: GpuType }) => { + const t = useTranslations() + const { sourcePrice } = usePriceStore() + + const gpuAlias = useMemo(() => { + const gpuItem = sourcePrice?.gpu?.find((item) => item.type === gpu?.type) + + return gpuItem?.alias || gpu?.type || '' + }, [gpu?.type, sourcePrice?.gpu]) + + return ( + + + {gpuAlias && ( + <> + {gpuAlias} + + / + + + )} + + {!!gpuAlias ? gpu?.amount : 0} + {t('Card')} + + + ) +} + +export default GPUItem diff --git a/frontend/providers/devbox/components/IDEButton.tsx b/frontend/providers/devbox/components/IDEButton.tsx index 74be2f5da87..88022291b86 100644 --- a/frontend/providers/devbox/components/IDEButton.tsx +++ b/frontend/providers/devbox/components/IDEButton.tsx @@ -48,7 +48,7 @@ const IDEButton = ({ const currentIDE = getDevboxIDEByDevboxName(devboxName) as IDEType const handleGotoIDE = useCallback( - async (currentIDE: IDEType = 'cursor') => { + async (currentIDE: IDEType = 'vscode') => { setLoading(true) toast({ diff --git a/frontend/providers/devbox/components/Icon/icons/nvidia.svg b/frontend/providers/devbox/components/Icon/icons/nvidia.svg new file mode 100644 index 00000000000..5eefafabedd --- /dev/null +++ b/frontend/providers/devbox/components/Icon/icons/nvidia.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/providers/devbox/components/Icon/index.tsx b/frontend/providers/devbox/components/Icon/index.tsx index 2e893f8f8e8..d5a0191d728 100644 --- a/frontend/providers/devbox/components/Icon/index.tsx +++ b/frontend/providers/devbox/components/Icon/index.tsx @@ -65,11 +65,12 @@ const map = { shutdown: require('./icons/shutdown.svg').default, windsurf: require('./icons/windsurf.svg').default, rocket: require('./icons/rocket.svg').default, + nvidia: require('./icons/nvidia.svg').default, user: require('./icons/user.svg').default, templateTitle: require('./icons/templateTitle.svg').default, official: require('./icons/official.svg').default, firstPage: require('./icons/firstPage.svg').default, - prePage: require('./icons/prePage.svg').default, + prePage: require('./icons/prePage.svg').default } const MyIcon = ({ diff --git a/frontend/providers/devbox/components/PriceBox.tsx b/frontend/providers/devbox/components/PriceBox.tsx index 6151a9b68c1..c1b40dfb9ed 100644 --- a/frontend/providers/devbox/components/PriceBox.tsx +++ b/frontend/providers/devbox/components/PriceBox.tsx @@ -19,6 +19,10 @@ const PriceBox = ({ cpu: number memory: number nodeports: number + gpu?: { + type: string + amount: number + } }[] }) => { const theme = useTheme() @@ -36,12 +40,21 @@ const PriceBox = ({ let mp = 0 let pp = 0 let tp = 0 + let gp = 0 - components.forEach(({ cpu, memory, nodeports }) => { + components.forEach(({ cpu, memory, nodeports, gpu }) => { cp = (sourcePrice.cpu * cpu * 24) / 1000 mp = (sourcePrice.memory * memory * 24) / 1024 pp = sourcePrice.nodeports * nodeports * 24 - tp = cp + mp + pp + + gp = (() => { + if (!gpu || !gpu.amount) return 0 + const item = sourcePrice?.gpu?.find((item) => item.type === gpu.type) + if (!item) return 0 + return +(item.price * gpu.amount * 24) + })() + + tp = cp + mp + pp + gp }) return [ @@ -56,9 +69,10 @@ const PriceBox = ({ color: '#8172D8', value: pp.toFixed(2) }, + ...(sourcePrice?.gpu ? [{ label: 'GPU', color: '#89CD11', value: gp.toFixed(2) }] : []), { label: 'total_price', color: '#485058', value: tp.toFixed(2) } ] - }, [components, sourcePrice.cpu, sourcePrice.memory, sourcePrice.nodeports]) + }, [components, sourcePrice.cpu, sourcePrice.memory, sourcePrice.nodeports, sourcePrice.gpu]) return ( diff --git a/frontend/providers/devbox/components/QuotaBox.tsx b/frontend/providers/devbox/components/QuotaBox.tsx index 57d5f603522..bf61e9f637c 100644 --- a/frontend/providers/devbox/components/QuotaBox.tsx +++ b/frontend/providers/devbox/components/QuotaBox.tsx @@ -20,6 +20,10 @@ const sourceMap = { nodeports: { color: '#FFA500', unit: '' + }, + gpu: { + color: '#89CD11', + unit: 'Card' } } diff --git a/frontend/providers/devbox/components/modals/DelModal.tsx b/frontend/providers/devbox/components/modals/DelModal.tsx index c0a4fafa15b..53738a1b742 100644 --- a/frontend/providers/devbox/components/modals/DelModal.tsx +++ b/frontend/providers/devbox/components/modals/DelModal.tsx @@ -19,6 +19,7 @@ import { useCallback, useState } from 'react' import { delDevbox } from '@/api/devbox' import MyIcon from '@/components/Icon' import { useIDEStore } from '@/stores/ide' +import { useDevboxStore } from '@/stores/devbox' import { DevboxDetailTypeV2, DevboxListItemTypeV2 } from '@/types/devbox' const DelModal = ({ @@ -44,10 +45,12 @@ const DelModal = ({ setLoading(true) await delDevbox(devbox.name) removeDevboxIDE(devbox.name) + toast({ title: t('delete_successful'), status: 'success' }) + onSuccess() onClose() diff --git a/frontend/providers/devbox/constants/devbox.ts b/frontend/providers/devbox/constants/devbox.ts index 12c3890a4f2..1e56c4e5ee9 100644 --- a/frontend/providers/devbox/constants/devbox.ts +++ b/frontend/providers/devbox/constants/devbox.ts @@ -1,7 +1,10 @@ import { DevboxDetailType, DevboxEditType, DevboxEditTypeV2 } from '@/types/devbox' +export const defaultSliderKey = 'default' export const crLabelKey = 'sealos-devbox-cr' +export const gpuResourceKey = 'nvidia.com/gpu' export const devboxKey = 'cloud.sealos.io/devbox-manager' +export const gpuNodeSelectorKey = 'nvidia.com/gpu.product' export const devboxIdKey = 'cloud.sealos.io/app-devbox-id' export const ingressProtocolKey = 'nginx.ingress.kubernetes.io/backend-protocol' export const publicDomainKey = `cloud.sealos.io/app-deploy-manager-domain` @@ -272,3 +275,14 @@ export const podStatusMap = { message: '' } } + +export const GpuAmountMarkList = [ + { label: '1', value: 1 }, + { label: '2', value: 2 }, + { label: '3', value: 3 }, + { label: '4', value: 4 }, + { label: '5', value: 5 }, + { label: '6', value: 6 }, + { label: '7', value: 7 }, + { label: '8', value: 8 } +] diff --git a/frontend/providers/devbox/deploy/manifests/deploy.yaml.tmpl b/frontend/providers/devbox/deploy/manifests/deploy.yaml.tmpl index feef4308e72..da863a7b8e1 100644 --- a/frontend/providers/devbox/deploy/manifests/deploy.yaml.tmpl +++ b/frontend/providers/devbox/deploy/manifests/deploy.yaml.tmpl @@ -38,11 +38,11 @@ spec: - name: devbox-frontend env: - name: SEALOS_DOMAIN - value: {{ .cloudDomain }} + value: { { .cloudDomain } } - name: INGRESS_SECRET value: wildcard-cert - name: REGISTRY_ADDR - value: {{ .registryAddr }} + value: { { .registryAddr } } - name: DEVBOX_AFFINITY_ENABLE value: 'true' - name: MONITOR_URL @@ -57,6 +57,8 @@ spec: value: sealosusw.site - name: CURRENCY_SYMBOL value: usd # 'shellCoin' | 'cny' | 'usd' + - name: GPU_ENABLE + value: 'true' securityContext: runAsNonRoot: true runAsUser: 1001 diff --git a/frontend/providers/devbox/message/en.json b/frontend/providers/devbox/message/en.json index ef446a204cc..5ab7eb4fc2c 100644 --- a/frontend/providers/devbox/message/en.json +++ b/frontend/providers/devbox/message/en.json @@ -1,13 +1,18 @@ { "Add Port": "Add Port", + "Amount": "Amount", "CNAME Tips": "To set up a custom domain, please add a `CNAME` record for your domain pointing to {domain} at your domain registrar. You can bind your custom domain once the DNS resolution takes effect.", + "Card": "cards", "Container Port": "Container Port", "Custom Domain": "Custom Domain", "Delete": "Deleting", "Error": "Error", "Failed": "Failed", + "Gpu under inventory Tip": "{gputype} is out of stock. Please select a different type or reduce the quantity", "Input your custom domain": "Enter your custom domain", + "Inventory": "Stock", "Network Configuration": "Network", + "No GPU": "No GPU", "No changes detected": "No change detected", "Open Public Access": "Enable Internet Access", "Paused": "Paused", @@ -21,6 +26,7 @@ "The minimum exposed port is 1": "Minimum port number is 1", "The port number cannot be repeated": "The port number cannot be repeated", "This runtime field is required": "The runtime version field is required", + "Under Stock": "Out of Stock", "all_templates": "All Templates", "app_name": "App Name", "basic_configuration": "Basic", @@ -221,7 +227,11 @@ "version_list": "Version List", "version_manage": "Version Control", "version_number": "Tag", + "vm": "vm", "vscode": "VS Code", "vscode_tooltip": "Click to develop in VSCode", - "yaml_file": "YAML" + "yaml_file": "YAML", + "gpu_exceeds_quota": "Requested GPU exceeds your quota. Please contact admin.", + "GPU": "GPU", + "gpu": "GPU" } diff --git a/frontend/providers/devbox/message/zh.json b/frontend/providers/devbox/message/zh.json index 850ee999e89..a3747620b18 100644 --- a/frontend/providers/devbox/message/zh.json +++ b/frontend/providers/devbox/message/zh.json @@ -1,13 +1,18 @@ { "Add Port": "添加端口", + "Amount": "数量", "CNAME Tips": "请到您的域名服务商处,添加该域名的 `CNAME` 解析到 {domain},解析生效后即可绑定自定义域名。", + "Card": "张", "Container Port": "容器暴露端口", "Custom Domain": "自定义域名", "Delete": "删除中", "Error": "错误", "Failed": "失败", + "Gpu under inventory Tip": "{gputype} 库存不足,请更换型号或减少数量", "Input your custom domain": "输入您的自定义域名", + "Inventory": "库存", "Network Configuration": "网络配置", + "No GPU": "不使用GPU", "No changes detected": "并没有变更任何项", "Open Public Access": "开启公网访问", "Paused": "已关机", @@ -21,6 +26,7 @@ "The minimum exposed port is 1": "暴露端口最小为 1", "The port number cannot be repeated": "不能重复暴露相同的端口", "This runtime field is required": "运行时版本是必填项", + "Under Stock": "库存不足", "all_templates": "所有模板", "app_name": "应用名称", "basic_configuration": "基础配置", @@ -222,8 +228,12 @@ "version_list": "版本列表", "version_manage": "版本管理", "version_number": "版本号", + "vm": "显存", "vscode": "VS Code", "vscodeInsider": "VSCode Insider", "vscode_tooltip": "点击在 VSCode 中开发", - "yaml_file": "YAML 文件" + "yaml_file": "YAML 文件", + "gpu_exceeds_quota": "申请的 GPU 超出配额限制,请联系管理员", + "GPU": "GPU", + "gpu": "GPU" } diff --git a/frontend/providers/devbox/public/images/pytorch.svg b/frontend/providers/devbox/public/images/pytorch.svg new file mode 100644 index 00000000000..49be044e977 --- /dev/null +++ b/frontend/providers/devbox/public/images/pytorch.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/providers/devbox/public/images/ubuntu-cuda.svg b/frontend/providers/devbox/public/images/ubuntu-cuda.svg new file mode 100644 index 00000000000..6de966bc2c8 --- /dev/null +++ b/frontend/providers/devbox/public/images/ubuntu-cuda.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/providers/devbox/services/backend/kubernetes.ts b/frontend/providers/devbox/services/backend/kubernetes.ts index d7e0f481adc..6752bc477d4 100644 --- a/frontend/providers/devbox/services/backend/kubernetes.ts +++ b/frontend/providers/devbox/services/backend/kubernetes.ts @@ -228,6 +228,11 @@ export async function getUserQuota( type: 'nodeports', limit: Number(status?.hard?.['services.nodeports']) || 0, used: Number(status?.used?.['services.nodeports']) || 0 + }, + { + type: 'gpu', + limit: Number(status?.hard?.['requests.nvidia.com/gpu'] || 0), + used: Number(status?.used?.['requests.nvidia.com/gpu'] || 0) } ] } diff --git a/frontend/providers/devbox/stores/global.ts b/frontend/providers/devbox/stores/global.ts index 5a3492af779..3da1b63a2dd 100644 --- a/frontend/providers/devbox/stores/global.ts +++ b/frontend/providers/devbox/stores/global.ts @@ -1,3 +1,5 @@ +import { defaultSliderKey } from '@/constants/devbox' +import { FormSliderListType } from '@/types' import { create } from 'zustand' import { devtools } from 'zustand/middleware' import { immer } from 'zustand/middleware/immer' @@ -9,6 +11,7 @@ type State = { setLoading: (val: boolean) => void lastRoute: string setLastRoute: (val: string) => void + formSliderListConfig: FormSliderListType } export const useGlobalStore = create()( @@ -31,6 +34,12 @@ export const useGlobalStore = create()( set((state) => { state.lastRoute = val }) + }, + formSliderListConfig: { + [defaultSliderKey]: { + cpu: [100, 200, 500, 1000, 2000, 3000, 4000, 8000], + memory: [64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384] + } } })) ) diff --git a/frontend/providers/devbox/stores/ide.ts b/frontend/providers/devbox/stores/ide.ts index a12839d38e5..bb853a39c71 100644 --- a/frontend/providers/devbox/stores/ide.ts +++ b/frontend/providers/devbox/stores/ide.ts @@ -18,7 +18,7 @@ export const useIDEStore = create()( immer((set, get) => ({ devboxIDEList: [], getDevboxIDEByDevboxName(devboxName: string) { - return get().devboxIDEList.find((item) => item.devboxName === devboxName)?.ide ||'vscode' + return get().devboxIDEList.find((item) => item.devboxName === devboxName)?.ide || 'vscode' }, addDevboxIDE(ide: IDEType, devboxName: string) { set((state) => { diff --git a/frontend/providers/devbox/stores/price.ts b/frontend/providers/devbox/stores/price.ts index 9830575b1a0..80e533b3322 100644 --- a/frontend/providers/devbox/stores/price.ts +++ b/frontend/providers/devbox/stores/price.ts @@ -1,10 +1,11 @@ -import { getResourcePrice } from '@/api/platform' -import { SourcePrice } from '@/types/static' import { create } from 'zustand' import { devtools } from 'zustand/middleware' import { immer } from 'zustand/middleware/immer' -const defaultSourcePrice: SourcePrice = { cpu: 0.067, memory: 0.033792, nodeports: 0.0001 } +import { SourcePrice } from '@/types/static' +import { getResourcePrice } from '@/api/platform' + +const defaultSourcePrice: SourcePrice = { cpu: 0.067, memory: 0.033792, nodeports: 0.0001, gpu: [] } type State = { sourcePrice: SourcePrice diff --git a/frontend/providers/devbox/stores/runtime.ts b/frontend/providers/devbox/stores/runtime.ts new file mode 100644 index 00000000000..fffa7f06c44 --- /dev/null +++ b/frontend/providers/devbox/stores/runtime.ts @@ -0,0 +1,98 @@ +import { create } from 'zustand' +import { immer } from 'zustand/middleware/immer' +import { devtools, persist } from 'zustand/middleware' + +import { getRuntime } from '@/api/platform' +import { RuntimeTypeMap, RuntimeVersionMap } from '@/types/static' + +type State = { + languageTypeList: RuntimeTypeMap[] + frameworkTypeList: RuntimeTypeMap[] + osTypeList: RuntimeTypeMap[] + + runtimeNamespaceMap: { + [key: string]: string + } + + languageVersionMap: RuntimeVersionMap + frameworkVersionMap: RuntimeVersionMap + osVersionMap: RuntimeVersionMap + + setRuntime: () => Promise + getRuntimeVersionList: (runtimeType: string) => { + value: string + label: string + defaultPorts: number[] + }[] + getRuntimeDetailLabel: (runtimeType: string, runtimeVersion: string) => string + getRuntimeVersionDefault: (runtimeType: string) => string + isGPURuntimeType: (runtimeType: string) => boolean +} + +export const useRuntimeStore = create()( + devtools( + persist( + immer((set, get) => ({ + languageTypeList: [], + frameworkTypeList: [], + osTypeList: [], + + runtimeNamespaceMap: {}, + + languageVersionMap: {}, + frameworkVersionMap: {}, + osVersionMap: {}, + + async setRuntime() { + const res = await getRuntime() + set((state) => { + Object.assign(state, res) + }) + }, + getRuntimeVersionList(runtimeType: string) { + const { languageVersionMap, frameworkVersionMap, osVersionMap } = get() + const versions = + languageVersionMap[runtimeType] || + frameworkVersionMap[runtimeType] || + osVersionMap[runtimeType] || + [] + return versions.map((i) => ({ + value: i.id, + label: i.label, + defaultPorts: i.defaultPorts + })) + }, + getRuntimeDetailLabel(runtimeType: string, runtimeVersion: string) { + const { languageVersionMap, frameworkVersionMap, osVersionMap } = get() + const versions = + languageVersionMap[runtimeType] || + frameworkVersionMap[runtimeType] || + osVersionMap[runtimeType] + + const version = versions.find((i) => i.id === runtimeVersion) + return `${runtimeType}-${version?.label}` + }, + getRuntimeVersionDefault(runtimeType: string) { + const { languageVersionMap, frameworkVersionMap, osVersionMap } = get() + return ( + languageVersionMap[runtimeType]?.[0]?.id || + frameworkVersionMap[runtimeType]?.[0]?.id || + osVersionMap[runtimeType]?.[0]?.id + ) + }, + isGPURuntimeType(runtimeType: string) { + const { languageTypeList, frameworkTypeList, osTypeList } = get() + return ( + languageTypeList.find((i) => i.id === runtimeType)?.gpu || + frameworkTypeList.find((i) => i.id === runtimeType)?.gpu || + osTypeList.find((i) => i.id === runtimeType)?.gpu || + false + ) + } + })), + { + name: 'runtime-storage' + } + ) + ) +) diff --git a/frontend/providers/devbox/stores/user.ts b/frontend/providers/devbox/stores/user.ts index 1c87fcdbe72..df6b64e4a28 100644 --- a/frontend/providers/devbox/stores/user.ts +++ b/frontend/providers/devbox/stores/user.ts @@ -5,15 +5,12 @@ import { immer } from 'zustand/middleware/immer' import { getUserQuota } from '@/api/platform' import { DevboxEditType } from '@/types/devbox' import { UserQuotaItemType } from '@/types/user' -type TQuota = Pick & { nodeports: number } +type TQuota = Pick & { nodeports: number } type State = { balance: number userQuota: UserQuotaItemType[] loadUserQuota: () => Promise - checkQuotaAllow: ( - request: TQuota, - usedData?: TQuota - ) => string | undefined + checkQuotaAllow: (request: TQuota, usedData?: TQuota) => string | undefined } export const useUserStore = create()( @@ -23,34 +20,35 @@ export const useUserStore = create()( userQuota: [], loadUserQuota: async () => { const response = await getUserQuota() - set((state) => { state.userQuota = response.quota }) return null }, - checkQuotaAllow: ({ cpu, memory, nodeports }, usedData): string | undefined => { + checkQuotaAllow: ({ cpu, memory, nodeports, gpu }, usedData): string | undefined => { const quote = get().userQuota - console.log(cpu, memory, nodeports) const request = { cpu: cpu / 1000, memory: memory / 1024, - nodeports: nodeports + nodeports: nodeports, + gpu: gpu?.type ? gpu.amount : 0 } if (usedData) { - const { cpu = 0, memory = 0, nodeports = 0 } = usedData + const { cpu = 0, memory = 0, nodeports = 0, gpu } = usedData request.cpu -= cpu / 1000 request.memory -= memory / 1024 request.nodeports -= nodeports + request.gpu -= gpu?.type ? gpu.amount : 0 } const overLimitTip: { [key: string]: string } = { cpu: 'cpu_exceeds_quota', memory: 'memory_exceeds_quota', - nodeports: 'nodeports_exceeds_quota' + nodeports: 'nodeports_exceeds_quota', + gpu: 'gpu_exceeds_quota' } const exceedQuota = quote.find((item) => { diff --git a/frontend/providers/devbox/types/devbox.d.ts b/frontend/providers/devbox/types/devbox.d.ts index ea9b3b44783..d796560b72f 100644 --- a/frontend/providers/devbox/types/devbox.d.ts +++ b/frontend/providers/devbox/types/devbox.d.ts @@ -26,12 +26,19 @@ export type DevboxReleaseStatusValueType = `${DevboxReleaseStatusEnum}` export type RuntimeType = `${FrameworkTypeEnum}` | `${LanguageTypeEnum}` | `${OSTypeEnum}` export type ProtocolType = 'HTTP' | 'GRPC' | 'WS' +export type GpuType = { + manufacturers: string + type: string + amount: number +} + export interface DevboxEditType { name: string runtimeType: string runtimeVersion: string cpu: number memory: number + gpu?: GpuType networks: { networkName: string portName: string @@ -50,6 +57,7 @@ export interface DevboxEditTypeV2 { image: string cpu: number memory: number + gpu?: GpuType networks: PortInfos } export interface DevboxStatusMapType { @@ -109,7 +117,7 @@ export interface DevboxDetailTypeV2 extends json2DevboxV2Data { sshDomain: string sshPort: number sshPrivateKey: string - }, + } sshPort?: number lastTerminatedReason?: string } @@ -143,9 +151,9 @@ export interface DevboxListItemTypeV2 { // templateRepository: object template: { templateRepository: { - iconId: string | null; - }; - uid: string; + iconId: string | null + } + uid: string } status: DevboxStatusMapType createTime: string @@ -181,23 +189,6 @@ export type DevboxKindsType = | V1Secret | V1HorizontalPodAutoscaler -export interface ValueType { - id: string - label: string -} - -export interface VersionMapType { - [key: string]: ValueTypeWithPorts[] -} - -export interface ValueTypeWithPorts extends ValueType { - defaultPorts: number[] -} - -export interface runtimeNamespaceMapType { - [key: string]: string -} - export interface PodStatusMapType { label: string value: `${PodStatusEnum}` @@ -224,7 +215,6 @@ export interface PodDetailType extends V1Pod { } export interface json2DevboxV2Data extends DevboxEditTypeV2 { - templateConfig: string, - image: string, + templateConfig: string + image: string } - diff --git a/frontend/providers/devbox/types/index.d.ts b/frontend/providers/devbox/types/index.d.ts index 0ed3a6d8cf0..90fac19207e 100644 --- a/frontend/providers/devbox/types/index.d.ts +++ b/frontend/providers/devbox/types/index.d.ts @@ -6,3 +6,11 @@ export interface YamlItemType { filename: string value: string } + +export type FormSliderListType = Record< + string, + { + cpu: number[] + memory: number[] + } +> diff --git a/frontend/providers/devbox/types/k8s.d.ts b/frontend/providers/devbox/types/k8s.d.ts index f76768ed883..999425d13f4 100644 --- a/frontend/providers/devbox/types/k8s.d.ts +++ b/frontend/providers/devbox/types/k8s.d.ts @@ -1,4 +1,10 @@ -import { DevboxStatusEnum } from '@/constants/devbox' +import { + DevboxStatusEnum, + gpuNodeSelectorKey, + PodStatusEnum, + ReconfigStatus, + gpuResourceKey +} from '@/constants/devbox' export type KBDevboxType = { apiVersion: 'devbox.sealos.io/v1alpha1' @@ -126,11 +132,15 @@ export interface KBDevboxSpec { resource: { cpu: string memory: string + [gpuResourceKey]?: string } runtimeRef: { name: string namespace: string } + nodeSelector?: { + [gpuNodeSelectorKey]: string + } state: DevboxStatusEnum tolerations?: { key: string @@ -201,7 +211,7 @@ export type KBDevboxReleaseType = { metadata: { name: string uid: string - creationTimestamp: string, + creationTimestamp: string ownerReferences: { apiVersion: string controller: boolean @@ -213,7 +223,7 @@ export type KBDevboxReleaseType = { spec: { devboxName: string newTag: string - notes?:string + notes?: string } status: { originalImage?: string diff --git a/frontend/providers/devbox/types/static.d.ts b/frontend/providers/devbox/types/static.d.ts index d0c42464020..1ccf8bff2df 100644 --- a/frontend/providers/devbox/types/static.d.ts +++ b/frontend/providers/devbox/types/static.d.ts @@ -2,6 +2,13 @@ export interface SourcePrice { cpu: number memory: number nodeports: number + gpu: { + alias: string + type: string + price: number + inventory: number + vm: number + }[] } export interface Env { @@ -20,6 +27,7 @@ export interface Env { export interface RuntimeTypeMap { id: string label: string + gpu: boolean } // RuntimeTypeMap @@ -46,3 +54,7 @@ export interface RuntimeVersionMap { // } // ] // } + +export interface RuntimeNamespaceMap { + [key: string]: string +} diff --git a/frontend/providers/devbox/types/user.d.ts b/frontend/providers/devbox/types/user.d.ts index 8e9b1add0c5..a5e52ec5cfb 100644 --- a/frontend/providers/devbox/types/user.d.ts +++ b/frontend/providers/devbox/types/user.d.ts @@ -25,10 +25,17 @@ export type userPriceType = { cpu: number memory: number nodeports: number + gpu?: { alias: string; type: string; price: number; inventory: number; vm: number }[] } export type UserQuotaItemType = { - type: 'cpu' | 'memory' | 'nodeports' + type: 'cpu' | 'memory' | 'nodeports' | 'gpu' used: number limit: number } + +export type GpuType = { + manufacturers: string + type: string + amount: number +} diff --git a/frontend/providers/devbox/utils/adapt.ts b/frontend/providers/devbox/utils/adapt.ts index e4c1b0ef556..83c80717407 100644 --- a/frontend/providers/devbox/utils/adapt.ts +++ b/frontend/providers/devbox/utils/adapt.ts @@ -23,6 +23,7 @@ import { V1Deployment, V1Ingress, V1Pod, V1StatefulSet } from '@kubernetes/clien import { KBDevboxReleaseType, KBDevboxType, KBDevboxTypeV2 } from '@/types/k8s' import { calculateUptime, cpuFormatToM, formatPodTime, memoryFormatToMi } from '@/utils/tools' +import { gpuNodeSelectorKey, gpuResourceKey } from '../constants/devbox' export const adaptDevboxListItem = (devbox: KBDevboxType): DevboxListItemType => { return { @@ -53,17 +54,19 @@ export const adaptDevboxListItem = (devbox: KBDevboxType): DevboxListItemType => ? devbox.status.state.waiting ? devbox.status.state.waiting.reason : devbox.status.state.terminated - ? devbox.status.state.terminated.reason - : '' + ? devbox.status.state.terminated.reason + : '' : '' } } -export const adaptDevboxListItemV2 = ([devbox, template]: [KBDevboxTypeV2, { - templateRepository: { - iconId: string | null; - }; - uid: string; -} +export const adaptDevboxListItemV2 = ([devbox, template]: [ + KBDevboxTypeV2, + { + templateRepository: { + iconId: string | null + } + uid: string + } ]): DevboxListItemTypeV2 => { return { id: devbox.metadata?.uid || ``, @@ -92,8 +95,8 @@ export const adaptDevboxListItemV2 = ([devbox, template]: [KBDevboxTypeV2, { ? devbox.status.state.waiting ? devbox.status.state.waiting.reason : devbox.status.state.terminated - ? devbox.status.state.terminated.reason - : '' + ? devbox.status.state.terminated.reason + : '' : '' } } @@ -114,6 +117,11 @@ export const adaptDevboxDetail = ( createTime: dayjs(devbox.metadata.creationTimestamp).format('YYYY-MM-DD HH:mm'), cpu: cpuFormatToM(devbox.spec.resource.cpu), memory: memoryFormatToMi(devbox.spec.resource.memory), + gpu: { + type: devbox.spec.nodeSelector?.[gpuNodeSelectorKey] || '', + amount: Number(devbox.spec.resource[gpuResourceKey] || 1), + manufacturers: 'nvidia' + }, usedCpu: { name: '', xData: new Array(30).fill(0), @@ -130,18 +138,21 @@ export const adaptDevboxDetail = ( ? devbox.status.state.waiting ? devbox.status.state.waiting.reason : devbox.status.state.terminated - ? devbox.status.state.terminated.reason - : '' + ? devbox.status.state.terminated.reason + : '' : '' } } -export const adaptDevboxDetailV2 = ( - [devbox, portInfos, template]: GetDevboxByNameReturn -): DevboxDetailTypeV2 => { +export const adaptDevboxDetailV2 = ([ + devbox, + portInfos, + template +]: GetDevboxByNameReturn): DevboxDetailTypeV2 => { console.log('adaptDevboxDetailV2') - const status = devbox.status.phase && devboxStatusMap[devbox.status.phase] - ? devboxStatusMap[devbox.status.phase] - : devboxStatusMap.Error + const status = + devbox.status.phase && devboxStatusMap[devbox.status.phase] + ? devboxStatusMap[devbox.status.phase] + : devboxStatusMap.Error return { id: devbox.metadata?.uid || ``, name: devbox.metadata.name || 'devbox', @@ -174,8 +185,8 @@ export const adaptDevboxDetailV2 = ( ? devbox.status.state.waiting ? devbox.status.state.waiting.reason : devbox.status.state.terminated - ? devbox.status.state.terminated.reason - : '' + ? devbox.status.state.terminated.reason + : '' : '' } } @@ -311,3 +322,20 @@ export const adaptAppListItem = (app: V1Deployment & V1StatefulSet): AppListItem '' } } + +export const sliderNumber2MarkList = ({ + val, + type, + gpuAmount = 1 +}: { + val: number[] + type: 'cpu' | 'memory' + gpuAmount?: number +}) => { + const newVal = val.map((item) => item * gpuAmount) + + return newVal.map((item) => ({ + label: type === 'memory' ? (item >= 1024 ? `${item / 1024} G` : `${item} M`) : `${item / 1000}`, + value: item + })) +} diff --git a/frontend/providers/devbox/utils/json2Yaml.ts b/frontend/providers/devbox/utils/json2Yaml.ts index 73a68ec242c..dda49c90bcb 100644 --- a/frontend/providers/devbox/utils/json2Yaml.ts +++ b/frontend/providers/devbox/utils/json2Yaml.ts @@ -1,20 +1,28 @@ import yaml from 'js-yaml' -import { devboxKey, publicDomainKey } from '@/constants/devbox' -import { DevboxEditType, DevboxEditTypeV2, json2DevboxV2Data, ProtocolType, runtimeNamespaceMapType } from '@/types/devbox' +import { devboxKey, gpuNodeSelectorKey, gpuResourceKey, publicDomainKey } from '@/constants/devbox' +import { DevboxEditType, DevboxEditTypeV2, json2DevboxV2Data, ProtocolType } from '@/types/devbox' import { produce } from 'immer' import { parseTemplateConfig, str2Num } from './tools' import { getUserNamespace } from './user' +import { RuntimeNamespaceMap } from '@/types/static' export const json2Devbox = ( data: DevboxEditType, - runtimeNamespaceMap: runtimeNamespaceMapType, + runtimeNamespaceMap: RuntimeNamespaceMap, devboxAffinityEnable: string = 'true', squashEnable: string = 'false' ) => { // runtimeNamespace inject const runtimeNamespace = runtimeNamespaceMap[data.runtimeVersion] - + // gpu node selector + const gpuMap = !!data.gpu?.type + ? { + nodeSelector: { + [gpuNodeSelectorKey]: data.gpu.type + } + } + : {} let json: any = { apiVersion: 'devbox.sealos.io/v1alpha1', kind: 'Devbox', @@ -31,13 +39,16 @@ export const json2Devbox = ( }, resource: { cpu: `${str2Num(Math.floor(data.cpu))}m`, - memory: `${str2Num(data.memory)}Mi` + memory: `${str2Num(data.memory)}Mi`, + ...(!!data.gpu?.type ? { [gpuResourceKey]: data.gpu.amount } : {}) }, + ...(!!data.gpu?.type ? { runtimeClassName: 'nvidia' } : {}), runtimeRef: { name: data.runtimeVersion, namespace: runtimeNamespace }, - state: 'Running' + state: 'Running', + ...gpuMap } } if (devboxAffinityEnable === 'true') { @@ -92,7 +103,7 @@ export const json2DevboxV2 = ( }, templateID: data.templateUid, image: data.image, - config: produce(parseTemplateConfig(data.templateConfig), draft=>{ + config: produce(parseTemplateConfig(data.templateConfig), (draft) => { draft.appPorts = data.networks.map((item) => ({ port: str2Num(item.port), name: item.portName, @@ -183,7 +194,10 @@ export const json2DevboxRelease = (data: { return yaml.dump(json) } -export const json2Ingress = (data: Pick, ingressSecret: string) => { +export const json2Ingress = ( + data: Pick, + ingressSecret: string +) => { // different protocol annotations const map = { HTTP: { @@ -226,7 +240,7 @@ export const json2Ingress = (data: Pick, in annotations: { 'kubernetes.io/ingress.class': 'nginx', 'nginx.ingress.kubernetes.io/proxy-body-size': '32m', - ...map[network.protocol as ProtocolType || 'HTTP'] + ...map[(network.protocol as ProtocolType) || 'HTTP'] } }, spec: { @@ -316,7 +330,7 @@ export const json2Ingress = (data: Pick, in return result.join('\n---\n') } -export const json2Service = (data: Pick) => { +export const json2Service = (data: Pick) => { if (data.networks.length === 0) { return '' } @@ -356,11 +370,14 @@ spec: memory: 64Mi type: Container ` -export const generateYamlList = (data: json2DevboxV2Data, env: { - devboxAffinityEnable?: string, - squashEnable?: string, - ingressSecret: string -}) => { +export const generateYamlList = ( + data: json2DevboxV2Data, + env: { + devboxAffinityEnable?: string + squashEnable?: string + ingressSecret: string + } +) => { return [ { filename: 'devbox.yaml', @@ -368,19 +385,19 @@ export const generateYamlList = (data: json2DevboxV2Data, env: { }, ...(data.networks.length > 0 ? [ - { - filename: 'service.yaml', - value: json2Service(data) - } - ] + { + filename: 'service.yaml', + value: json2Service(data) + } + ] : []), ...(data.networks.find((item) => item.openPublicDomain) ? [ - { - filename: 'ingress.yaml', - value: json2Ingress(data, env.ingressSecret) - } - ] + { + filename: 'ingress.yaml', + value: json2Ingress(data, env.ingressSecret) + } + ] : []) ] -} \ No newline at end of file +}