From e722796eebcdc948269724fb433a8145468163da Mon Sep 17 00:00:00 2001 From: mlhiter <3076438032@qq.com> Date: Mon, 9 Dec 2024 20:02:44 +0800 Subject: [PATCH 01/26] feat: basic logic --- .../devbox/create/components/Form.tsx | 175 +++++++++++++++++- .../[lang]/(platform)/devbox/create/page.tsx | 41 +++- .../devbox/components/Icon/icons/nvidia.svg | 1 + .../devbox/components/Icon/index.tsx | 3 +- frontend/providers/devbox/constants/devbox.ts | 12 ++ frontend/providers/devbox/message/en.json | 10 +- frontend/providers/devbox/message/zh.json | 10 +- frontend/providers/devbox/stores/global.ts | 9 + frontend/providers/devbox/stores/price.ts | 7 +- frontend/providers/devbox/stores/user.ts | 13 +- frontend/providers/devbox/types/devbox.d.ts | 7 + frontend/providers/devbox/types/index.d.ts | 8 + frontend/providers/devbox/types/static.d.ts | 7 + frontend/providers/devbox/utils/adapt.ts | 17 ++ 14 files changed, 300 insertions(+), 20 deletions(-) create mode 100644 frontend/providers/devbox/components/Icon/icons/nvidia.svg diff --git a/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/Form.tsx b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/Form.tsx index 15142e8adcf..3bd689c947a 100644 --- a/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/Form.tsx +++ b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/Form.tsx @@ -17,10 +17,10 @@ import { import { throttle } from 'lodash' import dynamic from 'next/dynamic' import { customAlphabet } from 'nanoid' -import { useEffect, useState } from 'react' +import { useCallback, useEffect, useMemo, useState } from 'react' import { useTranslations } from 'next-intl' import { UseFormReturn, useFieldArray } from 'react-hook-form' -import { MySelect, MySlider, Tabs, useMessage } from '@sealos/ui' +import { MySelect, MySlider, MyTooltip, Tabs, useMessage } from '@sealos/ui' import { useRouter } from '@/i18n' import MyIcon from '@/components/Icon' @@ -28,16 +28,21 @@ import PriceBox from '@/components/PriceBox' import QuotaBox from '@/components/QuotaBox' import { useEnvStore } from '@/stores/env' +import { usePriceStore } from '@/stores/price' import { useDevboxStore } from '@/stores/devbox' import { useRuntimeStore } from '@/stores/runtime' -import { ProtocolList } from '@/constants/devbox' -import type { DevboxEditType } from '@/types/devbox' import { obj2Query } from '@/utils/tools' +import { defaultSliderKey, GpuAmountMarkList, ProtocolList } from '@/constants/devbox' +import type { DevboxEditType } from '@/types/devbox' import { CpuSlideMarkList, MemorySlideMarkList } from '@/constants/devbox' +import { useGlobalStore } from '@/stores/global' +import { sliderNumber2MarkList } from '@/utils/adapt' const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 12) +const labelWidth = 120 + export type CustomAccessModalParams = { publicDomain: string customDomain: string @@ -48,11 +53,15 @@ const CustomAccessModal = dynamic(() => import('@/components/modals/CustomAccess const Form = ({ formHook, pxVal, - isEdit + isEdit, + already, + countGpuInventory }: { formHook: UseFormReturn pxVal: number + already: boolean isEdit: boolean + countGpuInventory: (type: string) => number }) => { const theme = useTheme() const router = useRouter() @@ -86,6 +95,8 @@ const Form = ({ getRuntimeDetailLabel } = useRuntimeStore() const { env } = useEnvStore() + const { sourcePrice } = usePriceStore() + const { formSliderListConfig } = useGlobalStore() const [customAccessModalData, setCustomAccessModalData] = useState() const navList: { id: string; label: string; icon: string }[] = [ @@ -131,6 +142,83 @@ const Form = ({ // eslint-disable-next-line }, []) + // 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] + ) + const selectedGpu = useMemo(() => { + const selected = sourcePrice?.gpu?.find((item) => item.type === getValues('gpu.type')) + if (!selected) return + return { + ...selected, + inventory: countGpuInventory(selected.type) + } + }, [sourcePrice?.gpu, countGpuInventory, getValues]) + + // cpu, memory have different sliderValue + const countSliderList = useCallback(() => { + const gpuType = getValues('gpu.type') + const key = gpuType && formSliderListConfig[gpuType] ? gpuType : defaultSliderKey + + const cpu = getValues('cpu') + const memory = getValues('memory') + + const cpuList = formSliderListConfig[key].cpu + const memoryList = formSliderListConfig[key].memory + + const sortedCpuList = + cpu !== undefined ? [...new Set([...cpuList, cpu])].sort((a, b) => a - b) : cpuList + + const sortedMemoryList = + memory !== undefined + ? [...new Set([...memoryList, memory])].sort((a, b) => a - b) + : memoryList + + return { + cpu: sliderNumber2MarkList({ + val: sortedCpuList, + type: 'cpu', + gpuAmount: getValues('gpu.amount') + }), + memory: sliderNumber2MarkList({ + val: sortedMemoryList, + type: 'memory', + gpuAmount: getValues('gpu.amount') + }) + } + }, [formSliderListConfig, getValues]) + if (!formHook) return null const Label = ({ @@ -576,7 +664,7 @@ const Form = ({ {...register('runtimeVersion', { required: t('This runtime field is required') })} - width={'200px'} + width={'300px'} placeholder={`${t('runtime')} ${t('version')}`} defaultValue={ getValues('runtimeVersion') || @@ -613,6 +701,81 @@ const Form = ({ /> )} + + {/* GPU */} + {!sourcePrice?.gpu && ( + + + + { + 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) + const sliderList = countSliderList() + setValue('cpu', sliderList.cpu[1].value) + setValue('memory', sliderList.memory[1].value) + } + } + : { + cursor: 'default', + opacity: 0.5 + })}> + {item.label} +
+
+ ) + })} + + / {t('Card')} + +
+
+ )} +
+ )} {/* CPU */} 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 f005fa3e8c5..e3a573be1fe 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 { useLoading } from '@/hooks/useLoading' import { useEnvStore } from '@/stores/env' import { useIDEStore } from '@/stores/ide' import { useUserStore } from '@/stores/user' +import { usePriceStore } from '@/stores/price' import { useDevboxStore } from '@/stores/devbox' import { useGlobalStore } from '@/stores/global' import { useRuntimeStore } from '@/stores/runtime' @@ -47,12 +48,14 @@ const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 12) const DevboxCreatePage = () => { const router = useRouter() const t = useTranslations() + const { Loading, setIsLoading } = useLoading() const searchParams = useSearchParams() const { message: toast } = useMessage() const { env } = useEnvStore() const { addDevboxIDE } = useIDEStore() + const { sourcePrice } = usePriceStore() const { checkQuotaAllow } = useUserStore() const { setDevboxDetail, devboxList } = useDevboxStore() const { runtimeNamespaceMap, languageVersionMap, frameworkVersionMap, osVersionMap } = @@ -62,10 +65,15 @@ const DevboxCreatePage = () => { const formOldYamls = useRef([]) const oldDevboxEditData = useRef() - const { Loading, setIsLoading } = useLoading() + const [already, setAlready] = useState(false) const [errorMessage, setErrorMessage] = useState('') const [forceUpdate, setForceUpdate] = useState(false) const [yamlList, setYamlList] = useState([]) + const [defaultGpuSource, setDefaultGpuSource] = useState({ + type: '', + amount: 0, + manufacturers: '' + }) const tabType = searchParams.get('type') || 'form' const devboxName = searchParams.get('name') || '' @@ -191,6 +199,15 @@ const DevboxCreatePage = () => { [] ) + const countGpuInventory = useCallback( + (type?: string) => { + const inventory = sourcePrice?.gpu?.find((item) => item.type === type)?.inventory || 0 + const defaultInventory = type === defaultGpuSource?.type ? defaultGpuSource?.amount || 0 : 0 + return inventory + defaultInventory + }, + [defaultGpuSource?.amount, defaultGpuSource?.type, sourcePrice?.gpu] + ) + // watch form change, compute new yaml formHook.watch((data) => { data && formOnchangeDebounce(data as DevboxEditType) @@ -241,6 +258,8 @@ const DevboxCreatePage = () => { oldDevboxEditData.current = res formOldYamls.current = formData2Yamls(res) crOldYamls.current = generateYamlList(res) as DevboxKindsType[] + setDefaultGpuSource(res.gpu) + setAlready(true) formHook.reset(res) }, onError(err) { @@ -259,6 +278,18 @@ const DevboxCreatePage = () => { 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 DevboxEditType & { @@ -361,7 +392,13 @@ const DevboxCreatePage = () => { /> {tabType === 'form' ? ( -
+ ) : ( )} 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 530ff784ed0..0d32bc79e77 100644 --- a/frontend/providers/devbox/components/Icon/index.tsx +++ b/frontend/providers/devbox/components/Icon/index.tsx @@ -64,7 +64,8 @@ const map = { empty: require('./icons/empty.svg').default, shutdown: require('./icons/shutdown.svg').default, windsurf: require('./icons/windsurf.svg').default, - rocket: require('./icons/rocket.svg').default + rocket: require('./icons/rocket.svg').default, + nvidia: require('./icons/nvidia.svg').default } const MyIcon = ({ diff --git a/frontend/providers/devbox/constants/devbox.ts b/frontend/providers/devbox/constants/devbox.ts index 9d9f86e4841..937e8221cd3 100644 --- a/frontend/providers/devbox/constants/devbox.ts +++ b/frontend/providers/devbox/constants/devbox.ts @@ -1,5 +1,6 @@ import { DevboxEditType, DevboxDetailType } from '@/types/devbox' +export const defaultSliderKey = 'default' export const crLabelKey = 'sealos-devbox-cr' export const devboxKey = 'cloud.sealos.io/devbox-manager' export const devboxIdKey = 'cloud.sealos.io/app-devbox-id' @@ -263,3 +264,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/message/en.json b/frontend/providers/devbox/message/en.json index 9fe45270117..d092f6d28b4 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", @@ -20,6 +25,7 @@ "The maximum number of exposed ports is 65535": "Maximum port number is 65535", "The minimum exposed port is 1": "Minimum port number is 1", "This runtime field is required": "The runtime version field is required", + "Under Stock": "Out of Stock", "app_name": "App Name", "basic_configuration": "Basic", "basic_info": "Basic", @@ -162,7 +168,9 @@ "version_info": "Version List", "version_list": "Version List", "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." } diff --git a/frontend/providers/devbox/message/zh.json b/frontend/providers/devbox/message/zh.json index e85d7a73f73..dbb311e6580 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": "已关机", @@ -20,6 +25,7 @@ "The maximum number of exposed ports is 65535": "暴露端口最大为 65535", "The minimum exposed port is 1": "暴露端口最小为 1", "This runtime field is required": "运行时版本是必填项", + "Under Stock": "库存不足", "app_name": "应用名称", "basic_configuration": "基础配置", "basic_info": "基础信息", @@ -164,8 +170,10 @@ "version_info": "版本列表", "version_list": "版本列表", "version_number": "版本号", + "vm": "显存", "vscode": "VS Code", "vscodeInsider": "VSCode Insider", "vscode_tooltip": "点击在 VSCode 中开发", - "yaml_file": "YAML 文件" + "yaml_file": "YAML 文件", + "gpu_exceeds_quota": "申请的 GPU 超出配额限制,请联系管理员" } 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/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/user.ts b/frontend/providers/devbox/stores/user.ts index ca9468cfa47..583737965fc 100644 --- a/frontend/providers/devbox/stores/user.ts +++ b/frontend/providers/devbox/stores/user.ts @@ -23,34 +23,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 96df965daf2..f7ee1ab1b32 100644 --- a/frontend/providers/devbox/types/devbox.d.ts +++ b/frontend/providers/devbox/types/devbox.d.ts @@ -28,12 +28,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 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/static.d.ts b/frontend/providers/devbox/types/static.d.ts index e6d15571be7..5fbf17f480c 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 { diff --git a/frontend/providers/devbox/utils/adapt.ts b/frontend/providers/devbox/utils/adapt.ts index 979727d7084..b932f5b7bdb 100644 --- a/frontend/providers/devbox/utils/adapt.ts +++ b/frontend/providers/devbox/utils/adapt.ts @@ -208,3 +208,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 + })) +} From a7a588da0c81e28212324f45317ce53cd2d45965 Mon Sep 17 00:00:00 2001 From: mlhiter <3076438032@qq.com> Date: Tue, 10 Dec 2024 14:11:27 +0800 Subject: [PATCH 02/26] feat: basic logic gpu runtime --- frontend/providers/devbox/.env.template | 1 + .../devbox/create/components/Form.tsx | 54 ++--------- .../[lang]/(platform)/devbox/create/page.tsx | 3 - .../app/api/platform/getRuntime/route.ts | 77 ++++++++++----- .../app/api/platform/resourcePrice/route.ts | 93 ++++++++++++++++++- frontend/providers/devbox/constants/devbox.ts | 2 + frontend/providers/devbox/stores/runtime.ts | 15 +++ frontend/providers/devbox/types/devbox.d.ts | 17 ---- frontend/providers/devbox/types/static.d.ts | 5 + frontend/providers/devbox/types/user.d.ts | 1 + frontend/providers/devbox/utils/json2Yaml.ts | 22 ++++- 11 files changed, 191 insertions(+), 99 deletions(-) diff --git a/frontend/providers/devbox/.env.template b/frontend/providers/devbox/.env.template index c0bc5d8b6a4..2ae187c400f 100644 --- a/frontend/providers/devbox/.env.template +++ b/frontend/providers/devbox/.env.template @@ -6,3 +6,4 @@ DEVBOX_AFFINITY_ENABLE= SQUASH_ENABLE= NODE_TLS_REJECT_UNAUTHORIZED= ROOT_RUNTIME_NAMESPACE= +GPU_ENABLE= \ No newline at end of file diff --git a/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/Form.tsx b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/Form.tsx index 3bd689c947a..d0aa1d0c4a0 100644 --- a/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/Form.tsx +++ b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/Form.tsx @@ -17,9 +17,9 @@ import { import { throttle } from 'lodash' import dynamic from 'next/dynamic' import { customAlphabet } from 'nanoid' -import { useCallback, useEffect, useMemo, useState } from 'react' import { useTranslations } from 'next-intl' import { UseFormReturn, useFieldArray } from 'react-hook-form' +import { useEffect, useMemo, useState } from 'react' import { MySelect, MySlider, MyTooltip, Tabs, useMessage } from '@sealos/ui' import { useRouter } from '@/i18n' @@ -33,15 +33,14 @@ import { useDevboxStore } from '@/stores/devbox' import { useRuntimeStore } from '@/stores/runtime' import { obj2Query } from '@/utils/tools' -import { defaultSliderKey, GpuAmountMarkList, ProtocolList } from '@/constants/devbox' +import { useGlobalStore } from '@/stores/global' import type { DevboxEditType } from '@/types/devbox' import { CpuSlideMarkList, MemorySlideMarkList } from '@/constants/devbox' -import { useGlobalStore } from '@/stores/global' -import { sliderNumber2MarkList } from '@/utils/adapt' +import { GpuAmountMarkList, ProtocolList } from '@/constants/devbox' const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 12) -const labelWidth = 120 +const labelWidth = 100 export type CustomAccessModalParams = { publicDomain: string @@ -54,12 +53,10 @@ const Form = ({ formHook, pxVal, isEdit, - already, countGpuInventory }: { formHook: UseFormReturn pxVal: number - already: boolean isEdit: boolean countGpuInventory: (type: string) => number }) => { @@ -92,10 +89,12 @@ const Form = ({ osTypeList, getRuntimeVersionList, getRuntimeVersionDefault, - getRuntimeDetailLabel + getRuntimeDetailLabel, + isGPURuntimeType } = useRuntimeStore() const { env } = useEnvStore() const { sourcePrice } = usePriceStore() + const { devboxList } = useDevboxStore() const { formSliderListConfig } = useGlobalStore() const [customAccessModalData, setCustomAccessModalData] = useState() @@ -113,7 +112,6 @@ const Form = ({ ] const { message: toast } = useMessage() const [activeNav, setActiveNav] = useState(navList[0].id) - const { devboxList } = useDevboxStore() // listen scroll and set activeNav useEffect(() => { @@ -186,39 +184,6 @@ const Form = ({ } }, [sourcePrice?.gpu, countGpuInventory, getValues]) - // cpu, memory have different sliderValue - const countSliderList = useCallback(() => { - const gpuType = getValues('gpu.type') - const key = gpuType && formSliderListConfig[gpuType] ? gpuType : defaultSliderKey - - const cpu = getValues('cpu') - const memory = getValues('memory') - - const cpuList = formSliderListConfig[key].cpu - const memoryList = formSliderListConfig[key].memory - - const sortedCpuList = - cpu !== undefined ? [...new Set([...cpuList, cpu])].sort((a, b) => a - b) : cpuList - - const sortedMemoryList = - memory !== undefined - ? [...new Set([...memoryList, memory])].sort((a, b) => a - b) - : memoryList - - return { - cpu: sliderNumber2MarkList({ - val: sortedCpuList, - type: 'cpu', - gpuAmount: getValues('gpu.amount') - }), - memory: sliderNumber2MarkList({ - val: sortedMemoryList, - type: 'memory', - gpuAmount: getValues('gpu.amount') - }) - } - }, [formSliderListConfig, getValues]) - if (!formHook) return null const Label = ({ @@ -703,7 +668,7 @@ const Form = ({ {/* GPU */} - {!sourcePrice?.gpu && ( + {sourcePrice?.gpu && isGPURuntimeType(getValues('runtimeType')) && ( @@ -754,9 +719,6 @@ const Form = ({ cursor: 'pointer', onClick: () => { setValue('gpu.amount', item.value) - const sliderList = countSliderList() - setValue('cpu', sliderList.cpu[1].value) - setValue('memory', sliderList.memory[1].value) } } : { 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 e3a573be1fe..77043c3bc0f 100644 --- a/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/page.tsx +++ b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/page.tsx @@ -65,7 +65,6 @@ const DevboxCreatePage = () => { const formOldYamls = useRef([]) const oldDevboxEditData = useRef() - const [already, setAlready] = useState(false) const [errorMessage, setErrorMessage] = useState('') const [forceUpdate, setForceUpdate] = useState(false) const [yamlList, setYamlList] = useState([]) @@ -259,7 +258,6 @@ const DevboxCreatePage = () => { formOldYamls.current = formData2Yamls(res) crOldYamls.current = generateYamlList(res) as DevboxKindsType[] setDefaultGpuSource(res.gpu) - setAlready(true) formHook.reset(res) }, onError(err) { @@ -393,7 +391,6 @@ const DevboxCreatePage = () => { {tabType === 'form' ? ( item.spec.kind === 'Language') - languageTypeList.push( - ...languageList.map((item: any) => { + const dealtLanguageList = languageList.map((item: any) => { + const aRuntime = runtimes.find((runtime: any) => runtime.spec.classRef === item.metadata.name) + if (aRuntime?.spec.category?.includes('gpu')) { return { id: item.metadata.name, - label: item.spec.title + label: item.spec.title, + gpu: true } - }) - ) + } + return { + id: item.metadata.name, + label: item.spec.title, + gpu: false + } + }) + languageTypeList.push(...dealtLanguageList) + const frameworkList = runtimeClasses?.items.filter( (item: any) => item.spec.kind === 'Framework' ) - frameworkTypeList.push( - ...frameworkList.map((item: any) => { + const dealtFrameworkList = frameworkList.map((item: any) => { + const aRuntime = runtimes.find((runtime: any) => runtime.spec.classRef === item.metadata.name) + if (aRuntime?.spec.category?.includes('gpu')) { return { id: item.metadata.name, - label: item.spec.title + label: item.spec.title, + gpu: true } - }) - ) + } + return { + id: item.metadata.name, + label: item.spec.title, + gpu: false + } + }) + frameworkTypeList.push(...dealtFrameworkList) + const osList = runtimeClasses?.items.filter((item: any) => item.spec.kind === 'OS') - osTypeList.push( - ...osList.map((item: any) => { + const dealtOsList = osList.map((item: any) => { + const aRuntime = runtimes.find((runtime: any) => runtime.spec.classRef === item.metadata.name) + if (aRuntime?.spec.category?.includes('gpu')) { return { id: item.metadata.name, - label: item.spec.title + label: item.spec.title, + gpu: true } - }) - ) + } + return { + id: item.metadata.name, + label: item.spec.title, + gpu: false + } + }) + osTypeList.push(...dealtOsList) // runtimeVersions and runtimeNamespaceMap languageList.forEach((item: any) => { @@ -166,9 +195,11 @@ export async function GET(req: NextRequest) { languageVersionMap, frameworkVersionMap, osVersionMap, + languageTypeList, frameworkTypeList, osTypeList, + runtimeNamespaceMap } }) diff --git a/frontend/providers/devbox/app/api/platform/resourcePrice/route.ts b/frontend/providers/devbox/app/api/platform/resourcePrice/route.ts index 2c791b5f300..257bed5ad49 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 { userPriceType } from '@/types/user' +import { CoreV1Api } from '@kubernetes/client-node' import { jsonRes } from '@/services/backend/response' +import { K8sApiDefault } from '@/services/backend/kubernetes' export const dynamic = 'force-dynamic' @@ -27,6 +29,13 @@ 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 = { @@ -38,8 +47,9 @@ const valuationMap: Record = { 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 +64,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 +84,75 @@ export async function GET(req: NextRequest) { } } +/* get gpu nodes by configmap. */ +export 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/constants/devbox.ts b/frontend/providers/devbox/constants/devbox.ts index 937e8221cd3..2c1006d335d 100644 --- a/frontend/providers/devbox/constants/devbox.ts +++ b/frontend/providers/devbox/constants/devbox.ts @@ -2,7 +2,9 @@ import { DevboxEditType, DevboxDetailType } 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` diff --git a/frontend/providers/devbox/stores/runtime.ts b/frontend/providers/devbox/stores/runtime.ts index 5654fb4f97c..fffa7f06c44 100644 --- a/frontend/providers/devbox/stores/runtime.ts +++ b/frontend/providers/devbox/stores/runtime.ts @@ -9,9 +9,11 @@ type State = { languageTypeList: RuntimeTypeMap[] frameworkTypeList: RuntimeTypeMap[] osTypeList: RuntimeTypeMap[] + runtimeNamespaceMap: { [key: string]: string } + languageVersionMap: RuntimeVersionMap frameworkVersionMap: RuntimeVersionMap osVersionMap: RuntimeVersionMap @@ -24,6 +26,7 @@ type State = { }[] getRuntimeDetailLabel: (runtimeType: string, runtimeVersion: string) => string getRuntimeVersionDefault: (runtimeType: string) => string + isGPURuntimeType: (runtimeType: string) => boolean } export const useRuntimeStore = create()( @@ -33,10 +36,13 @@ export const useRuntimeStore = create()( languageTypeList: [], frameworkTypeList: [], osTypeList: [], + runtimeNamespaceMap: {}, + languageVersionMap: {}, frameworkVersionMap: {}, osVersionMap: {}, + async setRuntime() { const res = await getRuntime() set((state) => { @@ -73,6 +79,15 @@ export const useRuntimeStore = create()( 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 + ) } })), { diff --git a/frontend/providers/devbox/types/devbox.d.ts b/frontend/providers/devbox/types/devbox.d.ts index f7ee1ab1b32..d0bedc2be32 100644 --- a/frontend/providers/devbox/types/devbox.d.ts +++ b/frontend/providers/devbox/types/devbox.d.ts @@ -144,23 +144,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}` diff --git a/frontend/providers/devbox/types/static.d.ts b/frontend/providers/devbox/types/static.d.ts index 5fbf17f480c..36be283767f 100644 --- a/frontend/providers/devbox/types/static.d.ts +++ b/frontend/providers/devbox/types/static.d.ts @@ -26,6 +26,7 @@ export interface Env { export interface RuntimeTypeMap { id: string label: string + gpu: boolean } // RuntimeTypeMap @@ -52,3 +53,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..cc50af27a0b 100644 --- a/frontend/providers/devbox/types/user.d.ts +++ b/frontend/providers/devbox/types/user.d.ts @@ -25,6 +25,7 @@ export type userPriceType = { cpu: number memory: number nodeports: number + gpu?: { alias: string; type: string; price: number; inventory: number; vm: number }[] } export type UserQuotaItemType = { diff --git a/frontend/providers/devbox/utils/json2Yaml.ts b/frontend/providers/devbox/utils/json2Yaml.ts index 40dd83e677b..b9dc3ec008b 100644 --- a/frontend/providers/devbox/utils/json2Yaml.ts +++ b/frontend/providers/devbox/utils/json2Yaml.ts @@ -2,18 +2,28 @@ import yaml from 'js-yaml' import { str2Num } from './tools' import { getUserNamespace } from './user' -import { DevboxEditType, runtimeNamespaceMapType } from '@/types/devbox' -import { devboxKey, publicDomainKey } from '@/constants/devbox' +import { DevboxEditType } from '@/types/devbox' +import { RuntimeNamespaceMap } from '@/types/static' +import { devboxKey, gpuNodeSelectorKey, gpuResourceKey, publicDomainKey } from '@/constants/devbox' 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', @@ -30,13 +40,15 @@ 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 } : {}) }, runtimeRef: { name: data.runtimeVersion, namespace: runtimeNamespace }, - state: 'Running' + state: 'Running', + ...gpuMap } } if (devboxAffinityEnable === 'true') { From c56ade596a0f4b337c54447c5acab6e8de74e34c Mon Sep 17 00:00:00 2001 From: mlhiter <3076438032@qq.com> Date: Tue, 10 Dec 2024 15:19:36 +0800 Subject: [PATCH 03/26] feat: pricebox and quota box --- .../devbox/create/components/Form.tsx | 8 +++++++- .../devbox/app/api/platform/getQuota/route.ts | 6 +++++- .../providers/devbox/components/PriceBox.tsx | 20 ++++++++++++++++--- .../providers/devbox/components/QuotaBox.tsx | 4 ++++ frontend/providers/devbox/message/en.json | 4 +++- frontend/providers/devbox/message/zh.json | 4 +++- .../devbox/services/backend/kubernetes.ts | 5 +++++ frontend/providers/devbox/types/user.d.ts | 2 +- 8 files changed, 45 insertions(+), 8 deletions(-) diff --git a/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/Form.tsx b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/Form.tsx index d0aa1d0c4a0..048976c0b08 100644 --- a/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/Form.tsx +++ b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/Form.tsx @@ -301,7 +301,13 @@ const Form = ({ { cpu: getValues('cpu'), memory: getValues('memory'), - nodeports: devboxList.length + nodeports: devboxList.length, + gpu: !!getValues('gpu.type') + ? { + type: getValues('gpu.type'), + amount: getValues('gpu.amount') + } + : undefined } ]} /> 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/components/PriceBox.tsx b/frontend/providers/devbox/components/PriceBox.tsx index 6151a9b68c1..d09f65fc448 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) 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/message/en.json b/frontend/providers/devbox/message/en.json index d092f6d28b4..240b113fa7b 100644 --- a/frontend/providers/devbox/message/en.json +++ b/frontend/providers/devbox/message/en.json @@ -172,5 +172,7 @@ "vscode": "VS Code", "vscode_tooltip": "Click to develop in VSCode", "yaml_file": "YAML", - "gpu_exceeds_quota": "Requested GPU exceeds your quota. Please contact admin." + "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 dbb311e6580..22549d87374 100644 --- a/frontend/providers/devbox/message/zh.json +++ b/frontend/providers/devbox/message/zh.json @@ -175,5 +175,7 @@ "vscodeInsider": "VSCode Insider", "vscode_tooltip": "点击在 VSCode 中开发", "yaml_file": "YAML 文件", - "gpu_exceeds_quota": "申请的 GPU 超出配额限制,请联系管理员" + "gpu_exceeds_quota": "申请的 GPU 超出配额限制,请联系管理员", + "GPU": "GPU", + "gpu": "GPU" } 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/types/user.d.ts b/frontend/providers/devbox/types/user.d.ts index cc50af27a0b..e78a9cb5477 100644 --- a/frontend/providers/devbox/types/user.d.ts +++ b/frontend/providers/devbox/types/user.d.ts @@ -29,7 +29,7 @@ export type userPriceType = { } export type UserQuotaItemType = { - type: 'cpu' | 'memory' | 'nodeports' + type: 'cpu' | 'memory' | 'nodeports' | 'gpu' used: number limit: number } From 4b7f7a7f587f760c7aa13c90063ba52c342880e4 Mon Sep 17 00:00:00 2001 From: mlhiter <3076438032@qq.com> Date: Tue, 10 Dec 2024 15:40:14 +0800 Subject: [PATCH 04/26] fix: bug --- .../[lang]/(platform)/devbox/create/components/Form.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/Form.tsx b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/Form.tsx index 048976c0b08..7be6c55166c 100644 --- a/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/Form.tsx +++ b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/Form.tsx @@ -175,14 +175,14 @@ const Form = ({ : [], [countGpuInventory, t, sourcePrice?.gpu] ) - const selectedGpu = useMemo(() => { + const selectedGpu = () => { const selected = sourcePrice?.gpu?.find((item) => item.type === getValues('gpu.type')) if (!selected) return return { ...selected, inventory: countGpuInventory(selected.type) } - }, [sourcePrice?.gpu, countGpuInventory, getValues]) + } if (!formHook) return null @@ -697,7 +697,8 @@ const Form = ({ {t('Amount')} {GpuAmountMarkList.map((item) => { - const inventory = selectedGpu?.inventory || 0 + const inventory = selectedGpu()?.inventory || 0 + const hasInventory = item.value <= inventory return ( From 1dbbea8b538c84fda289503170d89fca08fea155 Mon Sep 17 00:00:00 2001 From: mlhiter <3076438032@qq.com> Date: Tue, 10 Dec 2024 15:41:25 +0800 Subject: [PATCH 05/26] chore: ubuntu-cuda svg --- frontend/providers/devbox/public/images/ubuntu-cuda.svg | 1 + 1 file changed, 1 insertion(+) create mode 100644 frontend/providers/devbox/public/images/ubuntu-cuda.svg 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 From cb6d891e29957fd0d0a83af80a4f5ec1334aa1de Mon Sep 17 00:00:00 2001 From: mlhiter <3076438032@qq.com> Date: Tue, 10 Dec 2024 16:01:12 +0800 Subject: [PATCH 06/26] fix: price bug --- .../providers/devbox/app/api/platform/resourcePrice/route.ts | 2 ++ frontend/providers/devbox/components/PriceBox.tsx | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/providers/devbox/app/api/platform/resourcePrice/route.ts b/frontend/providers/devbox/app/api/platform/resourcePrice/route.ts index 257bed5ad49..d24c1eada98 100644 --- a/frontend/providers/devbox/app/api/platform/resourcePrice/route.ts +++ b/frontend/providers/devbox/app/api/platform/resourcePrice/route.ts @@ -42,6 +42,7 @@ const valuationMap: Record = { cpu: 1000, memory: 1024, storage: 1024, + gpu: 1000, ['services.nodeports']: 1000 } @@ -141,6 +142,7 @@ function countGpuSource(rawData: ResourcePriceType['data']['properties'], gpuNod 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'], diff --git a/frontend/providers/devbox/components/PriceBox.tsx b/frontend/providers/devbox/components/PriceBox.tsx index d09f65fc448..c1b40dfb9ed 100644 --- a/frontend/providers/devbox/components/PriceBox.tsx +++ b/frontend/providers/devbox/components/PriceBox.tsx @@ -48,7 +48,7 @@ const PriceBox = ({ pp = sourcePrice.nodeports * nodeports * 24 gp = (() => { - if (!gpu) return 0 + 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) From 23bf2dc937dcbad53b046ff453dff4a0e4bf40c9 Mon Sep 17 00:00:00 2001 From: mlhiter <3076438032@qq.com> Date: Tue, 10 Dec 2024 16:15:49 +0800 Subject: [PATCH 07/26] fix: build bug --- .../providers/devbox/app/api/platform/resourcePrice/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/providers/devbox/app/api/platform/resourcePrice/route.ts b/frontend/providers/devbox/app/api/platform/resourcePrice/route.ts index d24c1eada98..98313bddba8 100644 --- a/frontend/providers/devbox/app/api/platform/resourcePrice/route.ts +++ b/frontend/providers/devbox/app/api/platform/resourcePrice/route.ts @@ -86,7 +86,7 @@ export async function GET(req: NextRequest) { } /* get gpu nodes by configmap. */ -export async function getGpuNode() { +async function getGpuNode() { const gpuCrName = 'node-gpu-info' const gpuCrNS = 'node-system' From 2c5dbe0bae1d43c9a00715ae9b2a8b0ef1677aa8 Mon Sep 17 00:00:00 2001 From: mlhiter <3076438032@qq.com> Date: Tue, 10 Dec 2024 16:27:13 +0800 Subject: [PATCH 08/26] fix: gpu bug --- .../app/[lang]/(platform)/devbox/create/components/Form.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/Form.tsx b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/Form.tsx index 7be6c55166c..7a8cd1a2b69 100644 --- a/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/Form.tsx +++ b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/Form.tsx @@ -415,6 +415,7 @@ const Form = ({ 'runtimeVersion', languageVersionMap[getValues('runtimeType')][0].id ) + setValue('gpu.type', '') setValue( 'networks', languageVersionMap[getValues('runtimeType')][0].defaultPorts.map( @@ -496,6 +497,7 @@ const Form = ({ 'runtimeVersion', frameworkVersionMap[getValues('runtimeType')][0].id ) + setValue('gpu.type', '') setValue( 'networks', frameworkVersionMap[getValues('runtimeType')][0].defaultPorts.map( @@ -577,6 +579,7 @@ const Form = ({ 'runtimeVersion', osVersionMap[getValues('runtimeType')][0].id ) + setValue('gpu.type', '') setValue( 'networks', osVersionMap[getValues('runtimeType')][0].defaultPorts.map( From c0d66c59302ad3f4e54451d7c7ce7bf0ad0be58a Mon Sep 17 00:00:00 2001 From: mlhiter <3076438032@qq.com> Date: Tue, 10 Dec 2024 16:33:38 +0800 Subject: [PATCH 09/26] fix: build bug --- frontend/providers/devbox/api/devbox.ts | 6 +++--- frontend/providers/devbox/app/api/createDevbox/route.ts | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/frontend/providers/devbox/api/devbox.ts b/frontend/providers/devbox/api/devbox.ts index 6cec0b4fc2e..9456f66bc15 100644 --- a/frontend/providers/devbox/api/devbox.ts +++ b/frontend/providers/devbox/api/devbox.ts @@ -4,8 +4,7 @@ import { DevboxEditType, DevboxListItemType, DevboxPatchPropsType, - DevboxVersionListItemType, - runtimeNamespaceMapType + DevboxVersionListItemType } from '@/types/devbox' import { adaptAppListItem, @@ -14,6 +13,7 @@ import { adaptDevboxVersionListItem, adaptPod } from '@/utils/adapt' +import { RuntimeNamespaceMap } from '@/types/static' import { GET, POST, DELETE } from '@/services/request' import { KBDevboxType, KBDevboxReleaseType } from '@/types/k8s' import { MonitorDataResult, MonitorQueryKey } from '@/types/monitor' @@ -34,7 +34,7 @@ export const applyYamlList = (yamlList: string[], type: 'create' | 'replace' | ' export const createDevbox = (payload: { devboxForm: DevboxEditType - runtimeNamespaceMap: runtimeNamespaceMapType + runtimeNamespaceMap: RuntimeNamespaceMap }) => POST(`/api/createDevbox`, payload) export const updateDevbox = (payload: { patch: DevboxPatchPropsType; devboxName: string }) => diff --git a/frontend/providers/devbox/app/api/createDevbox/route.ts b/frontend/providers/devbox/app/api/createDevbox/route.ts index 147481be6d6..9e24de93102 100644 --- a/frontend/providers/devbox/app/api/createDevbox/route.ts +++ b/frontend/providers/devbox/app/api/createDevbox/route.ts @@ -1,9 +1,10 @@ import { NextRequest } from 'next/server' +import { DevboxEditType } from '@/types/devbox' +import { RuntimeNamespaceMap } from '@/types/static' import { jsonRes } from '@/services/backend/response' import { authSession } from '@/services/backend/auth' import { getK8s } from '@/services/backend/kubernetes' -import { DevboxEditType, runtimeNamespaceMapType } from '@/types/devbox' import { json2Devbox, json2Ingress, json2Service } from '@/utils/json2Yaml' export const dynamic = 'force-dynamic' @@ -13,7 +14,7 @@ export async function POST(req: NextRequest) { // NOTE: runtimeNamespaceMap will be too big? const { devboxForm, runtimeNamespaceMap } = (await req.json()) as { devboxForm: DevboxEditType - runtimeNamespaceMap: runtimeNamespaceMapType + runtimeNamespaceMap: RuntimeNamespaceMap } const headerList = req.headers From c23b7a8c37ace47139d63c35ef73e840eda705ec Mon Sep 17 00:00:00 2001 From: mlhiter <3076438032@qq.com> Date: Tue, 10 Dec 2024 16:45:34 +0800 Subject: [PATCH 10/26] chore: pytorch svg --- frontend/providers/devbox/public/images/pytorch.svg | 1 + 1 file changed, 1 insertion(+) create mode 100644 frontend/providers/devbox/public/images/pytorch.svg 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 From 0fd2f8df94255271198bac5e74148ed035a79b20 Mon Sep 17 00:00:00 2001 From: mlhiter <3076438032@qq.com> Date: Tue, 10 Dec 2024 18:14:55 +0800 Subject: [PATCH 11/26] fix: delete will not be effective --- frontend/providers/devbox/components/modals/DelModal.tsx | 9 ++++++++- frontend/providers/devbox/stores/devbox.ts | 7 +++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/frontend/providers/devbox/components/modals/DelModal.tsx b/frontend/providers/devbox/components/modals/DelModal.tsx index 31029de0e32..86f717c61bf 100644 --- a/frontend/providers/devbox/components/modals/DelModal.tsx +++ b/frontend/providers/devbox/components/modals/DelModal.tsx @@ -18,7 +18,9 @@ import { useCallback, useState } from 'react' import MyIcon from '@/components/Icon' import { delDevbox } from '@/api/devbox' + import { useIDEStore } from '@/stores/ide' +import { useDevboxStore } from '@/stores/devbox' import { DevboxDetailType, DevboxListItemType } from '@/types/devbox' const DelModal = ({ @@ -33,6 +35,7 @@ const DelModal = ({ const t = useTranslations() const { message: toast } = useMessage() const { removeDevboxIDE } = useIDEStore() + const { deleteDevbox } = useDevboxStore() const [loading, setLoading] = useState(false) const [inputValue, setInputValue] = useState('') @@ -42,10 +45,14 @@ const DelModal = ({ setLoading(true) await delDevbox(devbox.name) removeDevboxIDE(devbox.name) + // NOTE: there delete item from devboxList + // why I do that? devboxLIst can not be updated immediately, so I need to delete it from devboxList,otherwise it will be not deleted in surface + deleteDevbox(devbox.name) toast({ title: t('delete_successful'), status: 'success' }) + onSuccess() onClose() } catch (error: any) { @@ -56,7 +63,7 @@ const DelModal = ({ console.error(error) } setLoading(false) - }, [devbox.name, removeDevboxIDE, toast, t, onSuccess, onClose]) + }, [devbox.name, removeDevboxIDE, toast, t, onSuccess, onClose, deleteDevbox]) return ( diff --git a/frontend/providers/devbox/stores/devbox.ts b/frontend/providers/devbox/stores/devbox.ts index 249c6857cc2..7ba9e3f22eb 100644 --- a/frontend/providers/devbox/stores/devbox.ts +++ b/frontend/providers/devbox/stores/devbox.ts @@ -30,6 +30,7 @@ type State = { setDevboxDetail: (devboxName: string, sealosDomain: string) => Promise intervalLoadPods: (devboxName: string, updateDetail: boolean) => Promise loadDetailMonitorData: (devboxName: string) => Promise + deleteDevbox: (devboxName: string) => Promise } export const useDevboxStore = create()( @@ -43,6 +44,12 @@ export const useDevboxStore = create()( }) return res }, + deleteDevbox: async (devboxName: string) => { + set((state) => { + state.devboxList = state.devboxList.filter((item) => item.name !== devboxName) + }) + return 'success' + }, loadAvgMonitorData: async (devboxName) => { const pods = await getDevboxPodsByDevboxName(devboxName) const queryName = pods.length > 0 ? pods[0].podName : devboxName From ea3d1a88dd9732e4132edc6ce0a1527496bf7008 Mon Sep 17 00:00:00 2001 From: mlhiter <3076438032@qq.com> Date: Tue, 10 Dec 2024 18:16:20 +0800 Subject: [PATCH 12/26] chore: cursor->vscode --- .../devbox/app/[lang]/(platform)/devbox/create/page.tsx | 2 +- frontend/providers/devbox/components/IDEButton.tsx | 2 +- frontend/providers/devbox/stores/ide.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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 77043c3bc0f..14d7a2b7c8d 100644 --- a/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/page.tsx +++ b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/page.tsx @@ -339,7 +339,7 @@ const DevboxCreatePage = () => { } else { await createDevbox({ devboxForm: formData, runtimeNamespaceMap }) } - addDevboxIDE('cursor', formData.name) + addDevboxIDE('vscode', formData.name) toast({ title: t(applySuccess), status: 'success' diff --git a/frontend/providers/devbox/components/IDEButton.tsx b/frontend/providers/devbox/components/IDEButton.tsx index e946fb42024..cd9e9f24928 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/stores/ide.ts b/frontend/providers/devbox/stores/ide.ts index 553a2ca02e2..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 || 'cursor' + return get().devboxIDEList.find((item) => item.devboxName === devboxName)?.ide || 'vscode' }, addDevboxIDE(ide: IDEType, devboxName: string) { set((state) => { From a122ce7c1d11e97cd4682e85683366e48d135669 Mon Sep 17 00:00:00 2001 From: mlhiter <3076438032@qq.com> Date: Wed, 11 Dec 2024 16:32:08 +0800 Subject: [PATCH 13/26] feat: detail support gpu --- .../detail/[name]/components/BasicInfo.tsx | 14 ++++++- .../providers/devbox/components/gpuItem.tsx | 38 +++++++++++++++++++ frontend/providers/devbox/types/k8s.d.ts | 12 +++++- frontend/providers/devbox/types/user.d.ts | 6 +++ frontend/providers/devbox/utils/adapt.ts | 7 ++++ 5 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 frontend/providers/devbox/components/gpuItem.tsx 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 31e33b8597c..4334d8c3cd5 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,18 +4,20 @@ import React, { useCallback, useState } from 'react' import { Box, Text, Flex, Image, Spinner, Tooltip } from '@chakra-ui/react' import MyIcon from '@/components/Icon' - +import GPUItem from '@/components/gpuItem' import { DevboxDetailType } from '@/types/devbox' import { useEnvStore } from '@/stores/env' import { useDevboxStore } from '@/stores/devbox' import { useRuntimeStore } from '@/stores/runtime' +import { usePriceStore } from '@/stores/price' const BasicInfo = () => { const t = useTranslations() const { message: toast } = useMessage() const { env } = useEnvStore() + const { sourcePrice } = usePriceStore() const { devboxDetail } = useDevboxStore() const { getRuntimeDetailLabel } = useRuntimeStore() @@ -132,6 +134,16 @@ const BasicInfo = () => { {devboxDetail?.memory / 1024} G + {sourcePrice?.gpu && ( + + + GPU + + + + + + )} {/* ssh config */} diff --git a/frontend/providers/devbox/components/gpuItem.tsx b/frontend/providers/devbox/components/gpuItem.tsx new file mode 100644 index 00000000000..78830293413 --- /dev/null +++ b/frontend/providers/devbox/components/gpuItem.tsx @@ -0,0 +1,38 @@ +import { Box, Flex } from '@chakra-ui/react' +import React, { useMemo } from 'react' +import MyIcon from './Icon' +import { GpuType } from '@/types/user' +import { useTranslation } from 'next-i18next' +import { usePriceStore } from '@/stores/price' + +const GPUItem = ({ gpu }: { gpu?: GpuType }) => { + const { t } = useTranslation() + 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 React.memo(GPUItem) diff --git a/frontend/providers/devbox/types/k8s.d.ts b/frontend/providers/devbox/types/k8s.d.ts index 481131ee99d..b3d3d80addc 100644 --- a/frontend/providers/devbox/types/k8s.d.ts +++ b/frontend/providers/devbox/types/k8s.d.ts @@ -1,4 +1,10 @@ -import { DevboxStatusEnum, PodStatusEnum, ReconfigStatus } from '@/constants/devbox' +import { + DevboxStatusEnum, + gpuNodeSelectorKey, + PodStatusEnum, + ReconfigStatus, + gpuResourceKey +} from '@/constants/devbox' export type KBDevboxType = { apiVersion: 'devbox.sealos.io/v1alpha1' @@ -77,11 +83,15 @@ export interface KBDevboxSpec { resource: { cpu: string memory: string + [gpuResourceKey]?: string } runtimeRef: { name: string namespace: string } + nodeSelector?: { + [gpuNodeSelectorKey]: string + } state: DevboxStatusEnum tolerations?: { key: string diff --git a/frontend/providers/devbox/types/user.d.ts b/frontend/providers/devbox/types/user.d.ts index e78a9cb5477..a5e52ec5cfb 100644 --- a/frontend/providers/devbox/types/user.d.ts +++ b/frontend/providers/devbox/types/user.d.ts @@ -33,3 +33,9 @@ export type UserQuotaItemType = { 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 b932f5b7bdb..e420b02a046 100644 --- a/frontend/providers/devbox/utils/adapt.ts +++ b/frontend/providers/devbox/utils/adapt.ts @@ -18,6 +18,7 @@ import { V1Deployment, V1Ingress, V1Pod, V1StatefulSet } from '@kubernetes/clien import { DBListItemType, KbPgClusterType } from '@/types/cluster' import { IngressListItemType } from '@/types/ingress' import { AppListItemType } from '@/types/app' +import { gpuNodeSelectorKey, gpuResourceKey } from '../constants/devbox' export const adaptDevboxListItem = (devbox: KBDevboxType): DevboxListItemType => { return { @@ -57,6 +58,7 @@ export const adaptDevboxListItem = (devbox: KBDevboxType): DevboxListItemType => export const adaptDevboxDetail = ( devbox: KBDevboxType & { portInfos: any[] } ): DevboxDetailType => { + console.log('devbox', devbox) return { id: devbox.metadata?.uid || ``, name: devbox.metadata.name || 'devbox', @@ -71,6 +73,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), From 9aa727fe910c4509e9f758946955e79a288d372c Mon Sep 17 00:00:00 2001 From: mlhiter <3076438032@qq.com> Date: Wed, 11 Dec 2024 16:32:57 +0800 Subject: [PATCH 14/26] chore: name change --- .../(platform)/devbox/detail/[name]/components/BasicInfo.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 4334d8c3cd5..b95b80220dc 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,7 +4,7 @@ import React, { useCallback, useState } from 'react' import { Box, Text, Flex, Image, Spinner, Tooltip } from '@chakra-ui/react' import MyIcon from '@/components/Icon' -import GPUItem from '@/components/gpuItem' +import GPUItem from '@/components/GpuItem' import { DevboxDetailType } from '@/types/devbox' import { useEnvStore } from '@/stores/env' From 6d968ca9378f93187f91a3c4483d47af30c62935 Mon Sep 17 00:00:00 2001 From: mlhiter <3076438032@qq.com> Date: Wed, 11 Dec 2024 17:16:44 +0800 Subject: [PATCH 15/26] fix: gpu inventory bug --- .../devbox/create/components/Form.tsx | 1 - .../[lang]/(platform)/devbox/create/page.tsx | 22 ++++++++++--------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/Form.tsx b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/Form.tsx index 7a8cd1a2b69..05d45936be0 100644 --- a/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/Form.tsx +++ b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/Form.tsx @@ -95,7 +95,6 @@ const Form = ({ const { env } = useEnvStore() const { sourcePrice } = usePriceStore() const { devboxList } = useDevboxStore() - const { formSliderListConfig } = useGlobalStore() const [customAccessModalData, setCustomAccessModalData] = useState() const navList: { id: string; label: string; icon: string }[] = [ 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 14d7a2b7c8d..4ec8d0fcee8 100644 --- a/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/page.tsx +++ b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/page.tsx @@ -55,7 +55,7 @@ const DevboxCreatePage = () => { const { env } = useEnvStore() const { addDevboxIDE } = useIDEStore() - const { sourcePrice } = usePriceStore() + const { sourcePrice, setSourcePrice } = usePriceStore() const { checkQuotaAllow } = useUserStore() const { setDevboxDetail, devboxList } = useDevboxStore() const { runtimeNamespaceMap, languageVersionMap, frameworkVersionMap, osVersionMap } = @@ -68,11 +68,6 @@ const DevboxCreatePage = () => { const [errorMessage, setErrorMessage] = useState('') const [forceUpdate, setForceUpdate] = useState(false) const [yamlList, setYamlList] = useState([]) - const [defaultGpuSource, setDefaultGpuSource] = useState({ - type: '', - amount: 0, - manufacturers: '' - }) const tabType = searchParams.get('type') || 'form' const devboxName = searchParams.get('name') || '' @@ -201,10 +196,10 @@ const DevboxCreatePage = () => { const countGpuInventory = useCallback( (type?: string) => { const inventory = sourcePrice?.gpu?.find((item) => item.type === type)?.inventory || 0 - const defaultInventory = type === defaultGpuSource?.type ? defaultGpuSource?.amount || 0 : 0 - return inventory + defaultInventory + + return inventory }, - [defaultGpuSource?.amount, defaultGpuSource?.type, sourcePrice?.gpu] + [sourcePrice?.gpu] ) // watch form change, compute new yaml @@ -213,6 +208,11 @@ const DevboxCreatePage = () => { setForceUpdate(!forceUpdate) }) + const { refetch: refetchPrice } = useQuery(['init-price'], setSourcePrice, { + enabled: !!sourcePrice?.gpu, + refetchInterval: 6000 + }) + useQuery( ['initDevboxCreateData'], () => { @@ -257,7 +257,6 @@ const DevboxCreatePage = () => { oldDevboxEditData.current = res formOldYamls.current = formData2Yamls(res) crOldYamls.current = generateYamlList(res) as DevboxKindsType[] - setDefaultGpuSource(res.gpu) formHook.reset(res) }, onError(err) { @@ -344,6 +343,9 @@ const DevboxCreatePage = () => { title: t(applySuccess), status: 'success' }) + if (sourcePrice?.gpu) { + refetchPrice() + } router.push(lastRoute) } catch (error) { console.error(error) From 4132c9b11efb8647419cf1b1df314159394eec55 Mon Sep 17 00:00:00 2001 From: mlhiter <3076438032@qq.com> Date: Wed, 11 Dec 2024 17:38:39 +0800 Subject: [PATCH 16/26] fix: delete devbox --- frontend/providers/devbox/components/modals/DelModal.tsx | 7 ++----- frontend/providers/devbox/stores/devbox.ts | 7 ------- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/frontend/providers/devbox/components/modals/DelModal.tsx b/frontend/providers/devbox/components/modals/DelModal.tsx index 86f717c61bf..bce5fa049cc 100644 --- a/frontend/providers/devbox/components/modals/DelModal.tsx +++ b/frontend/providers/devbox/components/modals/DelModal.tsx @@ -35,7 +35,6 @@ const DelModal = ({ const t = useTranslations() const { message: toast } = useMessage() const { removeDevboxIDE } = useIDEStore() - const { deleteDevbox } = useDevboxStore() const [loading, setLoading] = useState(false) const [inputValue, setInputValue] = useState('') @@ -45,9 +44,7 @@ const DelModal = ({ setLoading(true) await delDevbox(devbox.name) removeDevboxIDE(devbox.name) - // NOTE: there delete item from devboxList - // why I do that? devboxLIst can not be updated immediately, so I need to delete it from devboxList,otherwise it will be not deleted in surface - deleteDevbox(devbox.name) + toast({ title: t('delete_successful'), status: 'success' @@ -63,7 +60,7 @@ const DelModal = ({ console.error(error) } setLoading(false) - }, [devbox.name, removeDevboxIDE, toast, t, onSuccess, onClose, deleteDevbox]) + }, [devbox.name, removeDevboxIDE, toast, t, onSuccess, onClose]) return ( diff --git a/frontend/providers/devbox/stores/devbox.ts b/frontend/providers/devbox/stores/devbox.ts index 7ba9e3f22eb..249c6857cc2 100644 --- a/frontend/providers/devbox/stores/devbox.ts +++ b/frontend/providers/devbox/stores/devbox.ts @@ -30,7 +30,6 @@ type State = { setDevboxDetail: (devboxName: string, sealosDomain: string) => Promise intervalLoadPods: (devboxName: string, updateDetail: boolean) => Promise loadDetailMonitorData: (devboxName: string) => Promise - deleteDevbox: (devboxName: string) => Promise } export const useDevboxStore = create()( @@ -44,12 +43,6 @@ export const useDevboxStore = create()( }) return res }, - deleteDevbox: async (devboxName: string) => { - set((state) => { - state.devboxList = state.devboxList.filter((item) => item.name !== devboxName) - }) - return 'success' - }, loadAvgMonitorData: async (devboxName) => { const pods = await getDevboxPodsByDevboxName(devboxName) const queryName = pods.length > 0 ? pods[0].podName : devboxName From a66dc6015d445212c83bab0477ea6951c6d5a304 Mon Sep 17 00:00:00 2001 From: mlhiter <3076438032@qq.com> Date: Wed, 11 Dec 2024 18:27:11 +0800 Subject: [PATCH 17/26] fix: bug --- .../detail/[name]/components/BasicInfo.tsx | 2 +- frontend/providers/devbox/components/gpuItem.tsx | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) 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 b95b80220dc..8bff3c29080 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,7 +4,7 @@ import React, { useCallback, useState } from 'react' import { Box, Text, Flex, Image, Spinner, Tooltip } from '@chakra-ui/react' import MyIcon from '@/components/Icon' -import GPUItem from '@/components/GpuItem' +import GPUItem from '@/components/GPUItem' import { DevboxDetailType } from '@/types/devbox' import { useEnvStore } from '@/stores/env' diff --git a/frontend/providers/devbox/components/gpuItem.tsx b/frontend/providers/devbox/components/gpuItem.tsx index 78830293413..fb3947e2bc2 100644 --- a/frontend/providers/devbox/components/gpuItem.tsx +++ b/frontend/providers/devbox/components/gpuItem.tsx @@ -1,12 +1,13 @@ -import { Box, Flex } from '@chakra-ui/react' 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 { useTranslation } from 'next-i18next' import { usePriceStore } from '@/stores/price' const GPUItem = ({ gpu }: { gpu?: GpuType }) => { - const { t } = useTranslation() + const t = useTranslations() const { sourcePrice } = usePriceStore() const gpuAlias = useMemo(() => { @@ -16,23 +17,22 @@ const GPUItem = ({ gpu }: { gpu?: GpuType }) => { }, [gpu?.type, sourcePrice?.gpu]) return ( - + {gpuAlias && ( <> {gpuAlias} - + / - + )} {!!gpuAlias ? gpu?.amount : 0} - {t('Card')} ) } -export default React.memo(GPUItem) +export default GPUItem From ac0e1a4bdadbda2265c4d441b41abce7b465c6f5 Mon Sep 17 00:00:00 2001 From: mlhiter <3076438032@qq.com> Date: Fri, 13 Dec 2024 16:31:49 +0800 Subject: [PATCH 18/26] fix: next build bug --- .../providers/devbox/components/{gpuItem.tsx => tempItem.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename frontend/providers/devbox/components/{gpuItem.tsx => tempItem.tsx} (100%) diff --git a/frontend/providers/devbox/components/gpuItem.tsx b/frontend/providers/devbox/components/tempItem.tsx similarity index 100% rename from frontend/providers/devbox/components/gpuItem.tsx rename to frontend/providers/devbox/components/tempItem.tsx From 7d8a39135326f1da1c1d972bc66f7923f6bbcb35 Mon Sep 17 00:00:00 2001 From: mlhiter <3076438032@qq.com> Date: Fri, 13 Dec 2024 16:32:30 +0800 Subject: [PATCH 19/26] fix: next build bug --- .../providers/devbox/components/{tempItem.tsx => GPUItem.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename frontend/providers/devbox/components/{tempItem.tsx => GPUItem.tsx} (100%) diff --git a/frontend/providers/devbox/components/tempItem.tsx b/frontend/providers/devbox/components/GPUItem.tsx similarity index 100% rename from frontend/providers/devbox/components/tempItem.tsx rename to frontend/providers/devbox/components/GPUItem.tsx From 454d4e026d914cab1976c92fa1d9c1b9b262c534 Mon Sep 17 00:00:00 2001 From: mlhiter <3076438032@qq.com> Date: Tue, 17 Dec 2024 10:03:40 +0800 Subject: [PATCH 20/26] chore:delete coonsole.log --- frontend/providers/devbox/app/api/v1/getDBSecretList/route.ts | 2 -- frontend/providers/devbox/utils/adapt.ts | 1 - 2 files changed, 3 deletions(-) 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/utils/adapt.ts b/frontend/providers/devbox/utils/adapt.ts index e420b02a046..ff1f2c5f624 100644 --- a/frontend/providers/devbox/utils/adapt.ts +++ b/frontend/providers/devbox/utils/adapt.ts @@ -58,7 +58,6 @@ export const adaptDevboxListItem = (devbox: KBDevboxType): DevboxListItemType => export const adaptDevboxDetail = ( devbox: KBDevboxType & { portInfos: any[] } ): DevboxDetailType => { - console.log('devbox', devbox) return { id: devbox.metadata?.uid || ``, name: devbox.metadata.name || 'devbox', From 0e117098f74d9a4f94021835777c55c44dc3844e Mon Sep 17 00:00:00 2001 From: mlhiter <3076438032@qq.com> Date: Tue, 17 Dec 2024 10:06:58 +0800 Subject: [PATCH 21/26] perf: code shorten --- .../app/api/platform/getRuntime/route.ts | 27 +++---------------- 1 file changed, 3 insertions(+), 24 deletions(-) diff --git a/frontend/providers/devbox/app/api/platform/getRuntime/route.ts b/frontend/providers/devbox/app/api/platform/getRuntime/route.ts index c3dc8e0a43f..6ef4af60373 100644 --- a/frontend/providers/devbox/app/api/platform/getRuntime/route.ts +++ b/frontend/providers/devbox/app/api/platform/getRuntime/route.ts @@ -49,17 +49,10 @@ export async function GET(req: NextRequest) { const languageList = runtimeClasses?.items.filter((item: any) => item.spec.kind === 'Language') const dealtLanguageList = languageList.map((item: any) => { const aRuntime = runtimes.find((runtime: any) => runtime.spec.classRef === item.metadata.name) - if (aRuntime?.spec.category?.includes('gpu')) { - return { - id: item.metadata.name, - label: item.spec.title, - gpu: true - } - } return { id: item.metadata.name, label: item.spec.title, - gpu: false + gpu: !!aRuntime?.spec.category?.includes('gpu') } }) languageTypeList.push(...dealtLanguageList) @@ -69,17 +62,10 @@ export async function GET(req: NextRequest) { ) const dealtFrameworkList = frameworkList.map((item: any) => { const aRuntime = runtimes.find((runtime: any) => runtime.spec.classRef === item.metadata.name) - if (aRuntime?.spec.category?.includes('gpu')) { - return { - id: item.metadata.name, - label: item.spec.title, - gpu: true - } - } return { id: item.metadata.name, label: item.spec.title, - gpu: false + gpu: !!aRuntime?.spec.category?.includes('gpu') } }) frameworkTypeList.push(...dealtFrameworkList) @@ -87,17 +73,10 @@ export async function GET(req: NextRequest) { const osList = runtimeClasses?.items.filter((item: any) => item.spec.kind === 'OS') const dealtOsList = osList.map((item: any) => { const aRuntime = runtimes.find((runtime: any) => runtime.spec.classRef === item.metadata.name) - if (aRuntime?.spec.category?.includes('gpu')) { - return { - id: item.metadata.name, - label: item.spec.title, - gpu: true - } - } return { id: item.metadata.name, label: item.spec.title, - gpu: false + gpu: !!aRuntime?.spec.category?.includes('gpu') } }) osTypeList.push(...dealtOsList) From 6ee5e1d6b5474425abe52013abc2afc7b6c6f42b Mon Sep 17 00:00:00 2001 From: mlhiter <3076438032@qq.com> Date: Mon, 23 Dec 2024 16:00:34 +0800 Subject: [PATCH 22/26] chore: deploy update --- frontend/providers/devbox/deploy/manifests/deploy.yaml.tmpl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 From 9c87d5ece85db061bbe064b83156da9dc66d51a2 Mon Sep 17 00:00:00 2001 From: mlhiter <3076438032@qq.com> Date: Mon, 23 Dec 2024 18:09:55 +0800 Subject: [PATCH 23/26] fix: add runtimeClassName --- frontend/providers/devbox/utils/json2Yaml.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/providers/devbox/utils/json2Yaml.ts b/frontend/providers/devbox/utils/json2Yaml.ts index b9dc3ec008b..dd0dc2391d1 100644 --- a/frontend/providers/devbox/utils/json2Yaml.ts +++ b/frontend/providers/devbox/utils/json2Yaml.ts @@ -43,6 +43,7 @@ export const json2Devbox = ( memory: `${str2Num(data.memory)}Mi`, ...(!!data.gpu?.type ? { [gpuResourceKey]: data.gpu.amount } : {}) }, + ...(!!data.gpu?.type ? { runtimeClassName: 'nvidia' } : {}), runtimeRef: { name: data.runtimeVersion, namespace: runtimeNamespace From 706afc11e6a9dea693c8b7533803cbf4104d6586 Mon Sep 17 00:00:00 2001 From: mlhiter <3076438032@qq.com> Date: Wed, 25 Dec 2024 16:52:54 +0800 Subject: [PATCH 24/26] fix: basic fix --- .../devbox/create/components/Form.tsx | 4 +- .../form/BasicConfiguration/GpuSelector.tsx | 30 ++ .../form/BasicConfiguration/index.tsx | 60 ++-- .../[lang]/(platform)/devbox/create/page.tsx | 12 - .../detail/[name]/components/BasicInfo.tsx | 4 +- .../detail/[name]/components/Version.tsx | 318 +++++++++--------- .../devbox/app/api/getDevboxByName/route.ts | 31 +- .../app/api/platform/getRuntime/route.ts | 190 ----------- .../app/api/platform/resourcePrice/route.ts | 1 - .../app/api/templateRepository/list/route.ts | 57 ++-- .../templateRepository/listOfficial/route.ts | 11 +- frontend/providers/devbox/types/devbox.d.ts | 14 +- 12 files changed, 289 insertions(+), 443 deletions(-) create mode 100644 frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/form/BasicConfiguration/GpuSelector.tsx delete mode 100644 frontend/providers/devbox/app/api/platform/getRuntime/route.ts diff --git a/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/Form.tsx b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/Form.tsx index 05d45936be0..f6c2b0dd84b 100644 --- a/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/Form.tsx +++ b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/Form.tsx @@ -34,7 +34,7 @@ import { useRuntimeStore } from '@/stores/runtime' import { obj2Query } from '@/utils/tools' import { useGlobalStore } from '@/stores/global' -import type { DevboxEditType } from '@/types/devbox' +import type { DevboxEditType, DevboxEditTypeV2 } from '@/types/devbox' import { CpuSlideMarkList, MemorySlideMarkList } from '@/constants/devbox' import { GpuAmountMarkList, ProtocolList } from '@/constants/devbox' @@ -55,7 +55,7 @@ const Form = ({ isEdit, countGpuInventory }: { - formHook: UseFormReturn + formHook: UseFormReturn pxVal: number isEdit: boolean countGpuInventory: (type: string) => number 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..9289ad75195 --- /dev/null +++ b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/form/BasicConfiguration/GpuSelector.tsx @@ -0,0 +1,30 @@ +import { CpuSlideMarkList } from '@/constants/devbox' +import { DevboxEditTypeV2 } from '@/types/devbox' +import { Box, Flex, FlexProps } from '@chakra-ui/react' +import { MySlider } from '@sealos/ui' +import { useTranslations } from 'next-intl' +import { useFormContext } from 'react-hook-form' +import Label from '../Label' + +export default function GpuSelector(props: FlexProps) { + const t = useTranslations() + const { watch, setValue } = useFormContext() + return ( + + + { + setValue('cpu', CpuSlideMarkList[e].value) + }} + max={CpuSlideMarkList.length - 1} + min={0} + step={1} + /> + + {t('core')} + + + ) +} 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..b1cb08b83c3 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,37 @@ -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' + +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, ...props }: BoxProps & { isEdit: boolean }) { 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/page.tsx b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/page.tsx index 5a42e2a8aec..bced5b987b3 100644 --- a/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/page.tsx +++ b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/page.tsx @@ -26,9 +26,6 @@ import { useGlobalStore } from '@/stores/global' import { useIDEStore } from '@/stores/ide' import { useUserStore } from '@/stores/user' import { usePriceStore } from '@/stores/price' -import { useDevboxStore } from '@/stores/devbox' -import { useGlobalStore } from '@/stores/global' -import { useRuntimeStore } from '@/stores/runtime' import { createDevbox, updateDevbox } from '@/api/devbox' import { defaultDevboxEditValueV2, editModeMap } from '@/constants/devbox' @@ -121,15 +118,6 @@ const DevboxCreatePage = () => { [sourcePrice?.gpu] ) - 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) => { 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 fb2fa75acbb..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 @@ -7,9 +7,9 @@ import MyIcon from '@/components/Icon' import GPUItem from '@/components/GPUItem' import { DevboxDetailType } from '@/types/devbox' -import { useDevboxStore } from '@/stores/devbox' -import { useRuntimeStore } from '@/stores/runtime' +import { useEnvStore } from '@/stores/env' import { usePriceStore } from '@/stores/price' +import { useDevboxStore } from '@/stores/devbox' const BasicInfo = () => { const t = useTranslations() 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/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/getRuntime/route.ts b/frontend/providers/devbox/app/api/platform/getRuntime/route.ts deleted file mode 100644 index e7b27234c77..00000000000 --- a/frontend/providers/devbox/app/api/platform/getRuntime/route.ts +++ /dev/null @@ -1,190 +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: RuntimeTypeMap[] = [] - const frameworkTypeList: RuntimeTypeMap[] = [] - const osTypeList: RuntimeTypeMap[] = [] - - const osVersionMap: RuntimeVersionMap = {} - const languageVersionMap: RuntimeVersionMap = {} - const frameworkVersionMap: RuntimeVersionMap = {} - - const runtimeNamespaceMap: RuntimeNamespaceMap = {} - - 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') - const dealtLanguageList = languageList.map((item: any) => { - const aRuntime = runtimes.find((runtime: any) => runtime.spec.classRef === item.metadata.name) - return { - id: item.metadata.name, - label: item.spec.title, - gpu: !!aRuntime?.spec.category?.includes('gpu') - } - }) - languageTypeList.push(...dealtLanguageList) - - const frameworkList = runtimeClasses?.items.filter( - (item: any) => item.spec.kind === 'Framework' - ) - const dealtFrameworkList = frameworkList.map((item: any) => { - const aRuntime = runtimes.find((runtime: any) => runtime.spec.classRef === item.metadata.name) - return { - id: item.metadata.name, - label: item.spec.title, - gpu: !!aRuntime?.spec.category?.includes('gpu') - } - }) - frameworkTypeList.push(...dealtFrameworkList) - - const osList = runtimeClasses?.items.filter((item: any) => item.spec.kind === 'OS') - const dealtOsList = osList.map((item: any) => { - const aRuntime = runtimes.find((runtime: any) => runtime.spec.classRef === item.metadata.name) - return { - id: item.metadata.name, - label: item.spec.title, - gpu: !!aRuntime?.spec.category?.includes('gpu') - } - }) - osTypeList.push(...dealtOsList) - - // 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 - }) - } -} diff --git a/frontend/providers/devbox/app/api/platform/resourcePrice/route.ts b/frontend/providers/devbox/app/api/platform/resourcePrice/route.ts index b2b997ce5f7..4c4907078a6 100644 --- a/frontend/providers/devbox/app/api/platform/resourcePrice/route.ts +++ b/frontend/providers/devbox/app/api/platform/resourcePrice/route.ts @@ -1,6 +1,5 @@ import { NextRequest } from 'next/server' -import { userPriceType } from '@/types/user' import { CoreV1Api } from '@kubernetes/client-node' import { jsonRes } from '@/services/backend/response' import { userPriceType } from '@/types/user' 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/types/devbox.d.ts b/frontend/providers/devbox/types/devbox.d.ts index 54d7b31b55d..d796560b72f 100644 --- a/frontend/providers/devbox/types/devbox.d.ts +++ b/frontend/providers/devbox/types/devbox.d.ts @@ -57,6 +57,7 @@ export interface DevboxEditTypeV2 { image: string cpu: number memory: number + gpu?: GpuType networks: PortInfos } export interface DevboxStatusMapType { @@ -116,7 +117,7 @@ export interface DevboxDetailTypeV2 extends json2DevboxV2Data { sshDomain: string sshPort: number sshPrivateKey: string - }, + } sshPort?: number lastTerminatedReason?: string } @@ -150,9 +151,9 @@ export interface DevboxListItemTypeV2 { // templateRepository: object template: { templateRepository: { - iconId: string | null; - }; - uid: string; + iconId: string | null + } + uid: string } status: DevboxStatusMapType createTime: string @@ -214,7 +215,6 @@ export interface PodDetailType extends V1Pod { } export interface json2DevboxV2Data extends DevboxEditTypeV2 { - templateConfig: string, - image: string, + templateConfig: string + image: string } - From 6cde1f95c267f1aff3da05748d52585a1447b3a2 Mon Sep 17 00:00:00 2001 From: mlhiter <3076438032@qq.com> Date: Wed, 25 Dec 2024 17:31:16 +0800 Subject: [PATCH 25/26] fix: gpu bug --- frontend/providers/devbox/api/template.ts | 202 ++-- .../devbox/create/components/Form.tsx | 988 ------------------ .../form/BasicConfiguration/GpuSelector.tsx | 170 ++- .../TemplateReposistoryItem.tsx | 127 +-- .../form/BasicConfiguration/index.tsx | 8 +- .../devbox/create/components/form/index.tsx | 203 ++-- 6 files changed, 431 insertions(+), 1267 deletions(-) delete mode 100644 frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/Form.tsx 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.tsx b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/Form.tsx deleted file mode 100644 index f6c2b0dd84b..00000000000 --- a/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/Form.tsx +++ /dev/null @@ -1,988 +0,0 @@ -'use client' - -import { - Box, - Button, - Center, - Flex, - FormControl, - Grid, - IconButton, - Image, - Input, - Switch, - Text, - useTheme -} from '@chakra-ui/react' -import { throttle } from 'lodash' -import dynamic from 'next/dynamic' -import { customAlphabet } from 'nanoid' -import { useTranslations } from 'next-intl' -import { UseFormReturn, useFieldArray } from 'react-hook-form' -import { useEffect, useMemo, useState } from 'react' -import { MySelect, MySlider, MyTooltip, Tabs, useMessage } from '@sealos/ui' - -import { useRouter } from '@/i18n' -import MyIcon from '@/components/Icon' -import PriceBox from '@/components/PriceBox' -import QuotaBox from '@/components/QuotaBox' - -import { useEnvStore } from '@/stores/env' -import { usePriceStore } from '@/stores/price' -import { useDevboxStore } from '@/stores/devbox' -import { useRuntimeStore } from '@/stores/runtime' - -import { obj2Query } from '@/utils/tools' -import { useGlobalStore } from '@/stores/global' -import type { DevboxEditType, DevboxEditTypeV2 } from '@/types/devbox' -import { CpuSlideMarkList, MemorySlideMarkList } from '@/constants/devbox' -import { GpuAmountMarkList, ProtocolList } from '@/constants/devbox' - -const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 12) - -const labelWidth = 100 - -export type CustomAccessModalParams = { - publicDomain: string - customDomain: string -} - -const CustomAccessModal = dynamic(() => import('@/components/modals/CustomAccessModal')) - -const Form = ({ - formHook, - pxVal, - isEdit, - countGpuInventory -}: { - formHook: UseFormReturn - pxVal: number - isEdit: boolean - countGpuInventory: (type: string) => number -}) => { - const theme = useTheme() - const router = useRouter() - const t = useTranslations() - const { - control, - register, - setValue, - getValues, - formState: { errors } - } = formHook - const { - fields: networks, - append: appendNetworks, - remove: removeNetworks, - update: updateNetworks - } = useFieldArray({ - control, - name: 'networks' - }) - - const { - languageVersionMap, - frameworkVersionMap, - osVersionMap, - languageTypeList, - frameworkTypeList, - osTypeList, - getRuntimeVersionList, - getRuntimeVersionDefault, - getRuntimeDetailLabel, - isGPURuntimeType - } = useRuntimeStore() - const { env } = useEnvStore() - const { sourcePrice } = usePriceStore() - const { devboxList } = useDevboxStore() - - const [customAccessModalData, setCustomAccessModalData] = useState() - const navList: { id: string; label: string; icon: string }[] = [ - { - id: 'baseInfo', - label: t('basic_configuration'), - icon: 'formInfo' - }, - { - id: 'network', - label: t('Network Configuration'), - icon: 'network' - } - ] - const { message: toast } = useMessage() - const [activeNav, setActiveNav] = useState(navList[0].id) - - // listen scroll and set activeNav - useEffect(() => { - const scrollFn = throttle((e: Event) => { - if (!e.target) return - const doms = navList.map((item) => ({ - dom: document.getElementById(item.id), - id: item.id - })) - - const dom = e.target as HTMLDivElement - const scrollTop = dom.scrollTop - - for (let i = doms.length - 1; i >= 0; i--) { - const offsetTop = doms[i].dom?.offsetTop || 0 - if (scrollTop + 500 >= offsetTop) { - setActiveNav(doms[i].id) - break - } - } - }, 200) - document.getElementById('form-container')?.addEventListener('scroll', scrollFn) - return () => { - document.getElementById('form-container')?.removeEventListener('scroll', scrollFn) - } - // eslint-disable-next-line - }, []) - - // 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] - ) - const selectedGpu = () => { - const selected = sourcePrice?.gpu?.find((item) => item.type === getValues('gpu.type')) - if (!selected) return - return { - ...selected, - inventory: countGpuInventory(selected.type) - } - } - - if (!formHook) return null - - const Label = ({ - children, - w = 'auto', - ...props - }: { - children: string - w?: number | 'auto' - [key: string]: any - }) => ( - - {children} - - ) - - const boxStyles = { - border: theme.borders.base, - borderRadius: 'lg', - mb: 4, - bg: 'white' - } - - const headerStyles = { - py: 4, - pl: '42px', - borderTopRadius: 'lg', - fontSize: 'xl', - color: 'grayModern.900', - fontWeight: 'bold', - display: 'flex', - alignItems: 'center', - backgroundColor: 'grayModern.50' - } - - return ( - <> - - {/* left sidebar */} - - - router.replace( - `/devbox/create?${obj2Query({ - type: 'yaml' - })}` - ) - } - /> - - {navList.map((item) => ( - { - setActiveNav(item.id) - window.location.hash = item.id - }}> - - - - {item?.label} - - - ))} - - - - - - - - - {/* right content */} - - {/* base info */} - - - - {t('basic_configuration')} - - - {/* Devbox Name */} - - - - - /^[a-zA-Z0-9]([-a-zA-Z0-9]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([-a-zA-Z0-9]*[a-zA-Z0-9])?)*$/.test( - value - ) || t('devbox_name_invalid') - } - })} - onBlur={(e) => { - const lowercaseValue = e.target.value.toLowerCase() - - setValue('name', lowercaseValue) - const networks = getValues('networks') - networks.forEach((network, i) => { - updateNetworks(i, { - ...network, - networkName: `${lowercaseValue}-${nanoid()}` - }) - }) - }} - /> - - - {/* Runtime Type */} - - - - {/* Language */} - {languageTypeList.length !== 0 && {t('language')}} - - {languageTypeList && - languageTypeList?.map((item) => { - return ( -
{ - if (isEdit) return - const devboxName = getValues('name') - if (!devboxName) { - toast({ - title: t('Please enter the devbox name first'), - status: 'warning' - }) - return - } - setValue('runtimeType', item.id) - setValue( - 'runtimeVersion', - languageVersionMap[getValues('runtimeType')][0].id - ) - setValue('gpu.type', '') - setValue( - 'networks', - languageVersionMap[getValues('runtimeType')][0].defaultPorts.map( - (port) => ({ - networkName: `${devboxName}-${nanoid()}`, - portName: nanoid(), - port: port, - protocol: 'HTTP', - openPublicDomain: true, - publicDomain: `${nanoid()}.${env.ingressDomain}`, - customDomain: '' - }) - ) - ) - }}> - {item.id} { - e.currentTarget.src = '/images/custom.svg' - }} - /> - - {item?.label} - -
- ) - })} -
- {/* framework */} - {frameworkTypeList.length !== 0 && {t('framework')}} - - {frameworkTypeList && - frameworkTypeList?.map((item) => { - return ( -
{ - if (isEdit) return - const devboxName = getValues('name') - if (!devboxName) { - toast({ - title: t('Please enter the devbox name first'), - status: 'warning' - }) - return - } - setValue('runtimeType', item.id) - setValue( - 'runtimeVersion', - frameworkVersionMap[getValues('runtimeType')][0].id - ) - setValue('gpu.type', '') - setValue( - 'networks', - frameworkVersionMap[getValues('runtimeType')][0].defaultPorts.map( - (port) => ({ - networkName: `${devboxName}-${nanoid()}`, - portName: nanoid(), - port: port, - protocol: 'HTTP', - openPublicDomain: true, - publicDomain: `${nanoid()}.${env.ingressDomain}`, - customDomain: '' - }) - ) - ) - }}> - {item.id} { - e.currentTarget.src = '/images/custom.svg' - }} - /> - - {item?.label} - -
- ) - })} -
- {/* os */} - {osTypeList.length !== 0 && {t('os')}} - - {osTypeList && - osTypeList?.map((item) => { - return ( -
{ - if (isEdit) return - const devboxName = getValues('name') - if (!devboxName) { - toast({ - title: t('Please enter the devbox name first'), - status: 'warning' - }) - return - } - setValue('runtimeType', item.id) - setValue( - 'runtimeVersion', - osVersionMap[getValues('runtimeType')][0].id - ) - setValue('gpu.type', '') - setValue( - 'networks', - osVersionMap[getValues('runtimeType')][0].defaultPorts.map( - (port) => ({ - networkName: `${devboxName}-${nanoid()}`, - portName: nanoid(), - port: port, - protocol: 'HTTP', - openPublicDomain: true, - publicDomain: `${nanoid()}.${env.ingressDomain}`, - customDomain: '' - }) - ) - ) - }}> - {item.id} { - e.currentTarget.src = '/images/custom.svg' - }} - /> - - {item?.label} - -
- ) - })} -
-
-
- {/* Runtime Version */} - - - {isEdit ? ( - - ) : ( - { - if (isEdit) return - const devboxName = getValues('name') - if (!devboxName) { - toast({ - title: t('Please enter the devbox name first'), - status: 'warning' - }) - return - } - setValue('runtimeVersion', val) - setValue( - 'networks', - getRuntimeVersionList(getValues('runtimeType'))[0].defaultPorts.map( - (port) => ({ - networkName: `${devboxName}-${nanoid()}`, - portName: nanoid(), - port: port, - protocol: 'HTTP', - openPublicDomain: true, - publicDomain: `${nanoid()}.${env.ingressDomain}`, - customDomain: '' - }) - ) - ) - }} - /> - )} - - - {/* GPU */} - {sourcePrice?.gpu && isGPURuntimeType(getValues('runtimeType')) && ( - - - - { - 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')} - -
-
- )} -
- )} - {/* CPU */} - - - { - setValue('cpu', CpuSlideMarkList[e].value) - }} - max={CpuSlideMarkList.length - 1} - min={0} - step={1} - /> - - {t('core')} - - - {/* Memory */} - - - { - setValue('memory', MemorySlideMarkList[e].value) - }} - max={MemorySlideMarkList.length - 1} - min={0} - step={1} - /> - -
-
- {/* network */} - - - - {t('Network Configuration')} - - - {networks.length === 0 && ( - - )} - {networks.map((network, i) => ( - - - - {t('Container Port')} - - - {i === networks.length - 1 && networks.length < 5 && ( - - - - )} - - - - {t('Open Public Access')} - - - { - const devboxName = getValues('name') - if (!devboxName) { - toast({ - title: t('Please enter the devbox name first'), - status: 'warning' - }) - return - } - - updateNetworks(i, { - ...getValues('networks')[i], - networkName: network.networkName || `${devboxName}-${nanoid()}`, - protocol: network.protocol || 'HTTP', - openPublicDomain: e.target.checked, - publicDomain: network.publicDomain || `${nanoid()}.${env.ingressDomain}` - }) - }} - /> - - - {network.openPublicDomain && ( - <> - - - - { - updateNetworks(i, { - ...getValues('networks')[i], - protocol: val - }) - }} - /> - - - {network.customDomain ? network.customDomain : network.publicDomain} - - - setCustomAccessModalData({ - publicDomain: network.publicDomain, - customDomain: network.customDomain - }) - }> - {t('Custom Domain')} - - - - - - )} - {networks.length >= 1 && ( - - - } - onClick={() => removeNetworks(i)} - /> - - )} - - ))} - - - {!!customAccessModalData && ( - setCustomAccessModalData(undefined)} - onSuccess={(e) => { - const i = networks.findIndex( - (item) => item.publicDomain === customAccessModalData.publicDomain - ) - if (i === -1) return - updateNetworks(i, { - ...networks[i], - customDomain: e - }) - - setCustomAccessModalData(undefined) - }} - /> - )} -
-
- - ) -} - -export default Form 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 index 9289ad75195..edb5792a989 100644 --- 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 @@ -1,30 +1,156 @@ -import { CpuSlideMarkList } from '@/constants/devbox' -import { DevboxEditTypeV2 } from '@/types/devbox' -import { Box, Flex, FlexProps } from '@chakra-ui/react' -import { MySlider } from '@sealos/ui' +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(props: FlexProps) { +export default function GpuSelector({ + countGpuInventory +}: { + countGpuInventory: (type: string) => number +}) { const t = useTranslations() - const { watch, setValue } = useFormContext() + 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 ( - - - { - setValue('cpu', CpuSlideMarkList[e].value) - }} - max={CpuSlideMarkList.length - 1} - min={0} - step={1} - /> - - {t('core')} - - + + + + { + 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 b1cb08b83c3..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 @@ -10,7 +10,11 @@ import TemplateSelector from './TemplateSelector' import ConfigurationHeader from '../ConfigurationHeader' import TemplateRepositorySelector from './TemplateRepositorySelector' -export default function BasicConfiguration({ isEdit, ...props }: BoxProps & { isEdit: boolean }) { +export default function BasicConfiguration({ + isEdit, + countGpuInventory, + ...props +}: BoxProps & { isEdit: boolean; countGpuInventory: (type: string) => number }) { const t = useTranslations() return ( @@ -26,7 +30,7 @@ export default function BasicConfiguration({ isEdit, ...props }: BoxProps & { is {/* Runtime Version */} {/* GPU */} - + {/* CPU */} {/* Memory */} 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 */} + + + ) } From dc40a9f79178f52ecd9bb8e7067360df46d72c6f Mon Sep 17 00:00:00 2001 From: mlhiter <3076438032@qq.com> Date: Wed, 25 Dec 2024 18:04:02 +0800 Subject: [PATCH 26/26] fix: type bug --- frontend/providers/devbox/api/devbox.ts | 43 +++++++++---------- .../[lang]/(platform)/devbox/create/page.tsx | 7 +-- frontend/providers/devbox/stores/user.ts | 7 +-- frontend/providers/devbox/utils/json2Yaml.ts | 11 ++--- 4 files changed, 27 insertions(+), 41 deletions(-) diff --git a/frontend/providers/devbox/api/devbox.ts b/frontend/providers/devbox/api/devbox.ts index feaf0afe0af..540891d47d0 100644 --- a/frontend/providers/devbox/api/devbox.ts +++ b/frontend/providers/devbox/api/devbox.ts @@ -1,13 +1,11 @@ import { V1Deployment, V1Pod, V1StatefulSet } from '@kubernetes/client-node' -import { DELETE, GET, POST } from '@/services/request' import { GetDevboxByNameReturn } from '@/types/adapt' import { DevboxEditTypeV2, DevboxListItemTypeV2, DevboxPatchPropsType, DevboxVersionListItemType - DevboxVersionListItemType } from '@/types/devbox' import { KBDevboxReleaseType, KBDevboxTypeV2 } from '@/types/k8s' import { MonitorDataResult, MonitorQueryKey } from '@/types/monitor' @@ -18,18 +16,20 @@ import { adaptDevboxVersionListItem, adaptPod } from '@/utils/adapt' -import { RuntimeNamespaceMap } from '@/types/static' import { GET, POST, DELETE } from '@/services/request' -import { KBDevboxType, KBDevboxReleaseType } from '@/types/k8s' -import { MonitorDataResult, MonitorQueryKey } from '@/types/monitor' 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() }) @@ -40,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) @@ -78,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/app/[lang]/(platform)/devbox/create/page.tsx b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/page.tsx index bced5b987b3..c95316d063e 100644 --- a/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/page.tsx +++ b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/page.tsx @@ -302,12 +302,7 @@ const DevboxCreatePage = () => { /> {tabType === 'form' ? ( - + ) : ( )} diff --git a/frontend/providers/devbox/stores/user.ts b/frontend/providers/devbox/stores/user.ts index 47c5cacce96..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()( diff --git a/frontend/providers/devbox/utils/json2Yaml.ts b/frontend/providers/devbox/utils/json2Yaml.ts index d2d7f73bbd5..dda49c90bcb 100644 --- a/frontend/providers/devbox/utils/json2Yaml.ts +++ b/frontend/providers/devbox/utils/json2Yaml.ts @@ -1,16 +1,11 @@ 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,