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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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, From 035b3f35f25a3ba1482c46e3b9efce7d3f527c66 Mon Sep 17 00:00:00 2001 From: mlhiter <3076438032@qq.com> Date: Thu, 26 Dec 2024 11:30:35 +0800 Subject: [PATCH 27/29] chore: prettier modify --- frontend/providers/devbox/.prettierignore | 7 ++++--- frontend/providers/devbox/.prettierrc.js | 20 ++++++++++++++++++++ frontend/providers/devbox/.prettierrc.json | 18 ------------------ 3 files changed, 24 insertions(+), 21 deletions(-) create mode 100644 frontend/providers/devbox/.prettierrc.js delete mode 100644 frontend/providers/devbox/.prettierrc.json diff --git a/frontend/providers/devbox/.prettierignore b/frontend/providers/devbox/.prettierignore index ab9a493f2ce..83c68743957 100644 --- a/frontend/providers/devbox/.prettierignore +++ b/frontend/providers/devbox/.prettierignore @@ -1,4 +1,5 @@ -dist -.vscode +**/dist +**/.vscode **/.DS_Store -node_modules +**/node_modules +**/.next \ No newline at end of file diff --git a/frontend/providers/devbox/.prettierrc.js b/frontend/providers/devbox/.prettierrc.js new file mode 100644 index 00000000000..8bc336a3674 --- /dev/null +++ b/frontend/providers/devbox/.prettierrc.js @@ -0,0 +1,20 @@ +module.exports = { + printWidth: 100, + tabWidth: 2, + useTabs: false, + semi: true, + singleQuote: true, + quoteProps: 'as-needed', + jsxSingleQuote: false, + trailingComma: 'none', + bracketSpacing: true, + jsxBracketSameLine: false, + arrowParens: 'always', + rangeStart: 0, + rangeEnd: Infinity, + requirePragma: false, + insertPragma: false, + proseWrap: 'preserve', + htmlWhitespaceSensitivity: 'css', + endOfLine: 'lf' +}; diff --git a/frontend/providers/devbox/.prettierrc.json b/frontend/providers/devbox/.prettierrc.json deleted file mode 100644 index c3bc434d271..00000000000 --- a/frontend/providers/devbox/.prettierrc.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "printWidth": 100, - "tabWidth": 2, - "useTabs": false, - "semi": false, - "singleQuote": true, - "quoteProps": "as-needed", - "trailingComma": "none", - "bracketSpacing": true, - "jsxBracketSameLine": true, - "bracketSameLine": true, - "arrowParens": "always", - "requirePragma": false, - "insertPragma": false, - "proseWrap": "preserve", - "htmlWhitespaceSensitivity": "ignore", - "endOfLine": "auto" -} From dd047b1aab50941770167003ac98d39a8fbeb1b6 Mon Sep 17 00:00:00 2001 From: mlhiter <3076438032@qq.com> Date: Thu, 26 Dec 2024 11:35:18 +0800 Subject: [PATCH 28/29] chore: prettier every file --- frontend/providers/devbox/api/devbox.ts | 86 ++-- frontend/providers/devbox/api/platform.ts | 18 +- frontend/providers/devbox/api/template.ts | 190 +++---- .../(home)/components/DevboxHeader.tsx | 158 +++--- .../(home)/components/DevboxList.tsx | 484 +++++++++--------- .../(home)/components/DevboxListContainer.tsx | 111 ++-- .../(platform)/(home)/components/Empty.tsx | 22 +- .../app/[lang]/(platform)/(home)/page.tsx | 4 +- .../devbox/create/components/Header.tsx | 76 +-- .../devbox/create/components/Yaml.tsx | 62 ++- .../form/BasicConfiguration/CpuSelector.tsx | 54 +- .../BasicConfiguration/DevboxNameInput.tsx | 98 ++-- .../form/BasicConfiguration/GpuSelector.tsx | 75 +-- .../BasicConfiguration/MemorySelector.tsx | 48 +- .../TemplateRepositoryListNav.tsx | 48 +- .../TemplateReposistoryItem.tsx | 43 +- .../TemplateRepositorySelector/index.tsx | 204 +++++--- .../TemplateSelector/index.tsx | 146 +++--- .../form/BasicConfiguration/index.tsx | 24 +- .../components/form/ConfigurationHeader.tsx | 12 +- .../devbox/create/components/form/Label.tsx | 13 +- .../form/NetworkConfiguration/index.tsx | 430 ++++++++-------- .../devbox/create/components/form/index.tsx | 103 ++-- .../[lang]/(platform)/devbox/create/page.tsx | 259 +++++----- .../detail/[name]/components/BasicInfo.tsx | 97 ++-- .../detail/[name]/components/Header.tsx | 147 +++--- .../detail/[name]/components/MainBody.tsx | 77 +-- .../detail/[name]/components/Version.tsx | 206 ++++---- .../(platform)/devbox/detail/[name]/page.tsx | 90 ++-- .../devbox/app/[lang]/(platform)/layout.tsx | 207 ++++---- .../(platform)/template/TagCheckbox.tsx | 36 +- .../template/TemplateModal/PrivatePanel.tsx | 188 +++---- .../template/TemplateModal/PromptModal.tsx | 59 ++- .../template/TemplateModal/PublicPanel.tsx | 404 ++++++++------- .../template/TemplateModal/TemplateCard.tsx | 216 ++++---- .../template/TemplateModal/index.tsx | 119 ++--- .../updateTemplate/CreateTemplateModal.tsx | 82 ++- .../DeleteTemplateReposistoryModal.tsx | 62 ++- .../updateTemplate/EditTemplateModal.tsx | 245 ++++----- .../EditTemplateReposistoryModal.tsx | 201 ++++---- .../SelectActionModal/TemplateDropdown.tsx | 93 ++-- .../SelectActionModal/index.tsx | 71 +-- .../UpdateTemplateRepositoryModal.tsx | 418 +++++++-------- .../TemplateRepositoryDescriptionField.tsx | 22 +- .../TemplateRepositoryIsPublicField.tsx | 128 +++-- .../TemplateRepositoryNameField.tsx | 47 +- .../components/TemplateRepositoryTagField.tsx | 182 +++---- .../DeleteTemplateVersionModal.tsx | 96 ++-- .../OverviewTemplateVersionModal.tsx | 59 ++- .../providers/devbox/app/[lang]/default.tsx | 2 +- .../providers/devbox/app/[lang]/globals.css | 10 +- .../providers/devbox/app/[lang]/layout.tsx | 35 +- .../devbox/app/api/auth/init/route.ts | 66 +-- .../devbox/app/api/createDevbox/route.ts | 52 +- .../devbox/app/api/delDevbox/route.ts | 50 +- .../app/api/delDevboxVersionByName/route.ts | 26 +- .../devbox/app/api/editDevboxVersion/route.ts | 22 +- .../devbox/app/api/getAppsByDevboxId/route.ts | 30 +- .../devbox/app/api/getDevboxByName/route.ts | 64 +-- .../devbox/app/api/getDevboxList/route.ts | 59 +-- .../api/getDevboxPodsByDevboxName/route.ts | 24 +- .../app/api/getDevboxVersionList/route.ts | 32 +- .../providers/devbox/app/api/getEnv/route.ts | 22 +- .../app/api/getSSHConnectionInfo/route.ts | 62 ++- .../devbox/app/api/getSSHRuntimeInfo/route.ts | 32 +- .../app/api/monitor/getMonitorData/route.ts | 108 ++-- .../devbox/app/api/pauseDevbox/route.ts | 22 +- .../app/api/platform/authCname/route.ts | 32 +- .../devbox/app/api/platform/getQuota/route.ts | 26 +- .../app/api/platform/resourcePrice/route.ts | 126 ++--- .../devbox/app/api/releaseDevbox/route.ts | 46 +- .../devbox/app/api/restartDevbox/route.ts | 44 +- .../devbox/app/api/startDevbox/route.ts | 22 +- .../api/templateRepository/delete/route.ts | 42 +- .../app/api/templateRepository/get/route.ts | 51 +- .../app/api/templateRepository/list/route.ts | 40 +- .../templateRepository/listOfficial/route.ts | 20 +- .../templateRepository/listPrivate/route.ts | 72 +-- .../api/templateRepository/tag/list/route.ts | 19 +- .../template/delete/route.ts | 56 +- .../template/getConfig/route.ts | 49 +- .../templateRepository/template/list/route.ts | 57 ++- .../api/templateRepository/update/route.ts | 85 ++- .../withTemplate/create/route.ts | 110 ++-- .../withTemplate/update/route.ts | 162 +++--- .../devbox/app/api/updateDevbox/route.ts | 82 +-- .../app/api/v1/getDBSecretList/route.ts | 102 ++-- .../app/api/v1/getDevboxDetail/route.ts | 46 +- .../devbox/app/api/v1/getNetworkList/route.ts | 38 +- .../devbox/components/DevboxStatusTag.tsx | 36 +- .../providers/devbox/components/GPUItem.tsx | 28 +- .../providers/devbox/components/IDEButton.tsx | 88 ++-- .../devbox/components/Icon/index.tsx | 12 +- .../devbox/components/MyFormControl.tsx | 37 +- .../providers/devbox/components/MyTable.tsx | 64 ++- .../providers/devbox/components/MyTooltip.tsx | 11 +- .../devbox/components/PodLineChart.tsx | 76 +-- .../providers/devbox/components/PriceBox.tsx | 82 +-- .../providers/devbox/components/QuotaBox.tsx | 49 +- .../devbox/components/SwitchPage.tsx | 24 +- .../devbox/components/YamlCode/index.tsx | 28 +- .../components/modals/AppSelectModal.tsx | 113 ++-- .../components/modals/CustomAccessModal.tsx | 49 +- .../devbox/components/modals/DelModal.tsx | 84 +-- .../components/modals/EditVersionDesModal.tsx | 52 +- .../devbox/components/modals/ErrorModal.tsx | 23 +- .../devbox/components/modals/MonitorModal.tsx | 29 +- .../devbox/components/modals/releaseModal.tsx | 85 +-- .../components/providers/MyChakraProvider.tsx | 10 +- .../components/providers/MyIntlProvider.tsx | 14 +- .../components/providers/MyQueryProvider.tsx | 26 +- .../providers/MyRouteHandlerProvider.tsx | 54 +- frontend/providers/devbox/constants/devbox.ts | 46 +- .../providers/devbox/constants/template.ts | 6 +- frontend/providers/devbox/constants/theme.ts | 6 +- .../providers/devbox/hooks/useConfirm.tsx | 82 +-- .../providers/devbox/hooks/useLoading.tsx | 23 +- .../providers/devbox/hooks/useRequest.tsx | 32 +- frontend/providers/devbox/i18n.ts | 17 +- frontend/providers/devbox/middleware.ts | 13 +- frontend/providers/devbox/next.config.js | 16 +- .../providers/devbox/services/backend/auth.ts | 98 ++-- .../devbox/services/backend/kubernetes.ts | 198 +++---- .../devbox/services/backend/response.ts | 50 +- frontend/providers/devbox/services/db/init.ts | 4 +- frontend/providers/devbox/services/error.ts | 16 +- .../providers/devbox/services/monitorFetch.ts | 21 +- frontend/providers/devbox/services/request.ts | 118 ++--- frontend/providers/devbox/services/retag.ts | 4 +- frontend/providers/devbox/stores/devbox.ts | 165 +++--- frontend/providers/devbox/stores/env.ts | 28 +- frontend/providers/devbox/stores/global.ts | 40 +- frontend/providers/devbox/stores/ide.ts | 42 +- frontend/providers/devbox/stores/price.ts | 33 +- frontend/providers/devbox/stores/runtime.ts | 74 +-- .../providers/devbox/stores/tagSelector.ts | 34 +- frontend/providers/devbox/stores/template.ts | 46 +- frontend/providers/devbox/stores/user.ts | 56 +- frontend/providers/devbox/types/adapt.d.ts | 32 +- frontend/providers/devbox/types/app.d.ts | 8 +- frontend/providers/devbox/types/cluster.d.ts | 114 ++--- frontend/providers/devbox/types/db.d.ts | 4 +- frontend/providers/devbox/types/devbox.d.ts | 290 +++++------ frontend/providers/devbox/types/index.d.ts | 14 +- frontend/providers/devbox/types/ingress.d.ts | 18 +- frontend/providers/devbox/types/k8s.d.ts | 436 ++++++++-------- frontend/providers/devbox/types/monitor.d.ts | 74 +-- frontend/providers/devbox/types/params.ts | 2 +- frontend/providers/devbox/types/static.d.ts | 54 +- frontend/providers/devbox/types/template.d.ts | 6 +- frontend/providers/devbox/types/user.d.ts | 56 +- frontend/providers/devbox/utils/adapt.ts | 132 ++--- frontend/providers/devbox/utils/cookie.ts | 16 +- frontend/providers/devbox/utils/json2Yaml.ts | 114 ++--- frontend/providers/devbox/utils/tools.ts | 294 +++++------ frontend/providers/devbox/utils/user.ts | 91 ++-- frontend/providers/devbox/utils/vaildate.ts | 38 +- 157 files changed, 6439 insertions(+), 6061 deletions(-) diff --git a/frontend/providers/devbox/api/devbox.ts b/frontend/providers/devbox/api/devbox.ts index 540891d47d0..eb303a901c4 100644 --- a/frontend/providers/devbox/api/devbox.ts +++ b/frontend/providers/devbox/api/devbox.ts @@ -1,22 +1,22 @@ -import { V1Deployment, V1Pod, V1StatefulSet } from '@kubernetes/client-node' +import { V1Deployment, V1Pod, V1StatefulSet } from '@kubernetes/client-node'; -import { GetDevboxByNameReturn } from '@/types/adapt' +import { GetDevboxByNameReturn } from '@/types/adapt'; import { DevboxEditTypeV2, DevboxListItemTypeV2, DevboxPatchPropsType, DevboxVersionListItemType -} from '@/types/devbox' -import { KBDevboxReleaseType, KBDevboxTypeV2 } from '@/types/k8s' -import { MonitorDataResult, MonitorQueryKey } from '@/types/monitor' +} from '@/types/devbox'; +import { KBDevboxReleaseType, KBDevboxTypeV2 } from '@/types/k8s'; +import { MonitorDataResult, MonitorQueryKey } from '@/types/monitor'; import { adaptAppListItem, adaptDevboxDetailV2, adaptDevboxListItemV2, adaptDevboxVersionListItem, adaptPod -} from '@/utils/adapt' -import { GET, POST, DELETE } from '@/services/request' +} from '@/utils/adapt'; +import { GET, POST, DELETE } from '@/services/request'; export const getMyDevboxList = () => GET< @@ -24,78 +24,78 @@ export const getMyDevboxList = () => KBDevboxTypeV2, { templateRepository: { - iconId: string | null - } - uid: string + 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() + return new Date(b.createTime).getTime() - new Date(a.createTime).getTime(); }) - ) + ); export const getDevboxByName = (devboxName: string) => - GET('/api/getDevboxByName', { devboxName }).then(adaptDevboxDetailV2) + GET('/api/getDevboxByName', { devboxName }).then(adaptDevboxDetailV2); export const applyYamlList = (yamlList: string[], type: 'create' | 'replace' | 'update') => - POST('/api/applyYamlList', { yamlList, type }) + POST('/api/applyYamlList', { yamlList, type }); export const createDevbox = (payload: { devboxForm: DevboxEditTypeV2 }) => - POST(`/api/createDevbox`, payload) + POST(`/api/createDevbox`, payload); export const updateDevbox = (payload: { patch: DevboxPatchPropsType; devboxName: string }) => - POST(`/api/updateDevbox`, payload) + POST(`/api/updateDevbox`, payload); -export const delDevbox = (devboxName: string) => DELETE('/api/delDevbox', { devboxName }) +export const delDevbox = (devboxName: string) => DELETE('/api/delDevbox', { devboxName }); -export const restartDevbox = (data: { devboxName: string }) => POST('/api/restartDevbox', data) +export const restartDevbox = (data: { devboxName: string }) => POST('/api/restartDevbox', data); -export const startDevbox = (data: { devboxName: string }) => POST('/api/startDevbox', data) +export const startDevbox = (data: { devboxName: string }) => POST('/api/startDevbox', data); -export const pauseDevbox = (data: { devboxName: string }) => POST('/api/pauseDevbox', data) +export const pauseDevbox = (data: { devboxName: string }) => POST('/api/pauseDevbox', data); export const getDevboxVersionList = (devboxName: string, devboxUid: string) => GET('/api/getDevboxVersionList', { devboxName, devboxUid }).then( (data): DevboxVersionListItemType[] => data.map(adaptDevboxVersionListItem).sort((a, b) => { - return new Date(b.createTime).getTime() - new Date(a.createTime).getTime() + return new Date(b.createTime).getTime() - new Date(a.createTime).getTime(); }) - ) + ); export const releaseDevbox = (data: { - devboxName: string - tag: string - releaseDes: string - devboxUid: string -}) => POST('/api/releaseDevbox', data) + devboxName: string; + tag: string; + releaseDes: string; + devboxUid: string; +}) => POST('/api/releaseDevbox', data); export const editDevboxVersion = (data: { name: string; releaseDes: string }) => - POST('/api/editDevboxVersion', data) + POST('/api/editDevboxVersion', data); export const delDevboxVersionByName = (versionName: string) => - DELETE('/api/delDevboxVersionByName', { versionName }) + DELETE('/api/delDevboxVersionByName', { versionName }); 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)) + GET('/api/getDevboxPodsByDevboxName', { name }).then((item) => item.map(adaptPod)); export const getDevboxMonitorData = (payload: { - queryName: string - queryKey: keyof MonitorQueryKey - step: string -}) => GET(`/api/monitor/getMonitorData`, payload) + queryName: string; + queryKey: keyof MonitorQueryKey; + step: string; +}) => GET(`/api/monitor/getMonitorData`, payload); export const getAppsByDevboxId = (devboxId: string) => GET('/api/getAppsByDevboxId', { devboxId }).then((res) => res.map(adaptAppListItem) - ) + ); diff --git a/frontend/providers/devbox/api/platform.ts b/frontend/providers/devbox/api/platform.ts index 684763129e7..27ed09297e5 100644 --- a/frontend/providers/devbox/api/platform.ts +++ b/frontend/providers/devbox/api/platform.ts @@ -1,16 +1,16 @@ -import { GET, POST } from '@/services/request' -import type { UserQuotaItemType } from '@/types/user' -import type { Env } from '@/types/static' -export const getAppEnv = () => GET('/api/getEnv') +import { GET, POST } from '@/services/request'; +import type { UserQuotaItemType } from '@/types/user'; +import type { Env } from '@/types/static'; +export const getAppEnv = () => GET('/api/getEnv'); export const getUserQuota = () => GET<{ - quota: UserQuotaItemType[] - }>('/api/platform/getQuota') + quota: UserQuotaItemType[]; + }>('/api/platform/getQuota'); -export const getRuntime = () => GET('/api/platform/getRuntime') +export const getRuntime = () => GET('/api/platform/getRuntime'); -export const getResourcePrice = () => GET('/api/platform/resourcePrice') +export const getResourcePrice = () => GET('/api/platform/resourcePrice'); export const postAuthCname = (data: { publicDomain: string; customDomain: string }) => - POST('/api/platform/authCname', data) + POST('/api/platform/authCname', data); diff --git a/frontend/providers/devbox/api/template.ts b/frontend/providers/devbox/api/template.ts index cdb3e6096cc..81fc247feca 100644 --- a/frontend/providers/devbox/api/template.ts +++ b/frontend/providers/devbox/api/template.ts @@ -1,152 +1,152 @@ -import { Tag, TemplateRepositoryKind } from '@/prisma/generated/client' -import { DELETE, GET, POST } from '@/services/request' +import { Tag, TemplateRepositoryKind } from '@/prisma/generated/client'; +import { DELETE, GET, POST } from '@/services/request'; import { CreateTemplateRepositoryType, UpdateTemplateRepositoryType, UpdateTemplateType -} from '@/utils/vaildate' +} from '@/utils/vaildate'; export const listOfficialTemplateRepository = () => GET<{ templateRepositoryList: { - uid: string - name: string - kind: TemplateRepositoryKind - iconId: string - description: string | null + uid: string; + name: string; + kind: TemplateRepositoryKind; + iconId: string; + description: string | null; templateRepositoryTags: { - tag: Tag - }[] - }[] - }>(`/api/templateRepository/listOfficial`) + tag: Tag; + }[]; + }[]; + }>(`/api/templateRepository/listOfficial`); export const listTemplateRepository = ( page: { - page: number - pageSize: number + page: number; + pageSize: number; }, tags?: string[], search?: string ) => { - const searchParams = new URLSearchParams() + const searchParams = new URLSearchParams(); if (tags && tags.length > 0) { tags.forEach((tag) => { - searchParams.append('tags', tag) - }) + searchParams.append('tags', tag); + }); } - searchParams.append('page', page.page.toString()) - searchParams.append('pageSize', page.pageSize.toString()) - if (search) searchParams.append('search', search) + searchParams.append('page', page.page.toString()); + searchParams.append('pageSize', page.pageSize.toString()); + 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 - } - }>(`/api/templateRepository/list?${searchParams.toString()}`) -} + page: number; + pageSize: number; + totalItems: number; + totalPage: number; + }; + }>(`/api/templateRepository/list?${searchParams.toString()}`); +}; export const listPrivateTemplateRepository = ({ search, page, pageSize }: { - search?: string - page?: number - pageSize?: number + search?: string; + page?: number; + pageSize?: number; } = {}) => { - const searchParams = new URLSearchParams() + const searchParams = new URLSearchParams(); - if (search) searchParams.append('search', search) - if (page) searchParams.append('page', page.toString()) - if (pageSize) searchParams.append('pageSize', pageSize.toString()) + if (search) searchParams.append('search', search); + if (page) searchParams.append('page', page.toString()); + 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 - } - }>(`/api/templateRepository/listPrivate?${searchParams.toString()}`) -} + 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 + name: string; + uid: string; + }[]; + uid: string; + isPublic: true; + name: string; + description: string | null; + iconId: string | null; templateRepositoryTags: { - tag: Tag - }[] - } - }>(`/api/templateRepository/get?uid=${uid}`) + 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}`) + 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}`) + 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`) + tagList: Tag[]; + }>(`/api/templateRepository/tag/list`); export const createTemplateReposistory = (data: CreateTemplateRepositoryType) => - POST(`/api/templateRepository/withTemplate/create`, data) -export const initUser = () => POST(`/api/auth/init`) + POST(`/api/templateRepository/withTemplate/create`, data); +export const initUser = () => POST(`/api/auth/init`); export const deleteTemplateRepository = (templateRepositoryUid: string) => - DELETE(`/api/templateRepository/delete?templateRepositoryUid=${templateRepositoryUid}`) + DELETE(`/api/templateRepository/delete?templateRepositoryUid=${templateRepositoryUid}`); export const updateTemplateReposistory = (data: UpdateTemplateRepositoryType) => - POST(`/api/templateRepository/update`, data) + POST(`/api/templateRepository/update`, data); export const updateTemplate = (data: UpdateTemplateType) => - POST(`/api/templateRepository/withTemplate/update`, data) + POST(`/api/templateRepository/withTemplate/update`, data); export const deleteTemplate = (templateUid: string) => - DELETE(`/api/templateRepository/template/delete?uid=${templateUid}`) + DELETE(`/api/templateRepository/template/delete?uid=${templateUid}`); diff --git a/frontend/providers/devbox/app/[lang]/(platform)/(home)/components/DevboxHeader.tsx b/frontend/providers/devbox/app/[lang]/(platform)/(home)/components/DevboxHeader.tsx index 46ad42d6011..7c5fec29e08 100644 --- a/frontend/providers/devbox/app/[lang]/(platform)/(home)/components/DevboxHeader.tsx +++ b/frontend/providers/devbox/app/[lang]/(platform)/(home)/components/DevboxHeader.tsx @@ -1,90 +1,94 @@ -import MyIcon from "@/components/Icon"; -import { TemplateState } from "@/constants/template"; -import { usePathname, useRouter } from "@/i18n"; -import { useTemplateStore } from "@/stores/template"; -import { Box, Button, Center, Flex, Text, useTheme } from "@chakra-ui/react"; -import { useTranslations } from "next-intl"; -import { useEffect } from "react"; +import MyIcon from '@/components/Icon'; +import { TemplateState } from '@/constants/template'; +import { usePathname, useRouter } from '@/i18n'; +import { useTemplateStore } from '@/stores/template'; +import { Box, Button, Center, Flex, Text, useTheme } from '@chakra-ui/react'; +import { useTranslations } from 'next-intl'; +import { useEffect } from 'react'; export default function DevboxHeader({ listLength }: { listLength: number }) { - const { openTemplateModal, config, updateTemplateModalConfig } = useTemplateStore() - const theme = useTheme() - const router = useRouter() - const t = useTranslations() - const pathname = usePathname() - const lastRoute = '/?openTemplate=publicTemplate' + const { openTemplateModal, config, updateTemplateModalConfig } = useTemplateStore(); + const theme = useTheme(); + const router = useRouter(); + const t = useTranslations(); + const pathname = usePathname(); + const lastRoute = '/?openTemplate=publicTemplate'; useEffect(() => { - const refreshLastRoute = '/' + const refreshLastRoute = '/'; if (config.lastRoute.includes('openTemplate')) { openTemplateModal({ ...config, - lastRoute: refreshLastRoute - }) + lastRoute: refreshLastRoute + }); } else { updateTemplateModalConfig({ ...config, - lastRoute: refreshLastRoute - }) + lastRoute: refreshLastRoute + }); } - }, []) - return -
- -
- - {t('devbox_list')} - - - ( {listLength} ) - - { - // setLastRoute(pathname) - openTemplateModal({ - 'templateState': TemplateState.publicTemplate, - lastRoute, - }) - }} - > - +
+ +
+ + {t('devbox_list')} + + + ( {listLength} ) + + - { + // setLastRoute(pathname) + openTemplateModal({ + templateState: TemplateState.publicTemplate, + lastRoute + }); + }} + > + + + {t('scan_templates')} + + +
- -
; -} \ No newline at end of file + ); +} diff --git a/frontend/providers/devbox/app/[lang]/(platform)/(home)/components/DevboxList.tsx b/frontend/providers/devbox/app/[lang]/(platform)/(home)/components/DevboxList.tsx index 2a588dd5358..095abc70d52 100644 --- a/frontend/providers/devbox/app/[lang]/(platform)/(home)/components/DevboxList.tsx +++ b/frontend/providers/devbox/app/[lang]/(platform)/(home)/components/DevboxList.tsx @@ -1,114 +1,113 @@ -import { Box, Button, Flex, Image, MenuButton, Text } from '@chakra-ui/react' -import { MyTable, SealosMenu, useMessage } from '@sealos/ui' -import { useTranslations } from 'next-intl' -import dynamic from 'next/dynamic' -import { useCallback, useState } from 'react' -import { sealosApp } from 'sealos-desktop-sdk/app' +import { Box, Button, Flex, Image, MenuButton, Text } from '@chakra-ui/react'; +import { MyTable, SealosMenu, useMessage } from '@sealos/ui'; +import { useTranslations } from 'next-intl'; +import dynamic from 'next/dynamic'; +import { useCallback, useState } from 'react'; +import { sealosApp } from 'sealos-desktop-sdk/app'; -import { pauseDevbox, restartDevbox, startDevbox } from '@/api/devbox' -import { useRouter } from '@/i18n' -import { useGlobalStore } from '@/stores/global' -import { DevboxListItemTypeV2 } from '@/types/devbox' +import { pauseDevbox, restartDevbox, startDevbox } from '@/api/devbox'; +import { useRouter } from '@/i18n'; +import { useGlobalStore } from '@/stores/global'; +import { DevboxListItemTypeV2 } from '@/types/devbox'; -import DevboxStatusTag from '@/components/DevboxStatusTag' -import MyIcon from '@/components/Icon' -import IDEButton from '@/components/IDEButton' -import ReleaseModal from '@/components/modals/releaseModal' -import PodLineChart from '@/components/PodLineChart' +import DevboxStatusTag from '@/components/DevboxStatusTag'; +import MyIcon from '@/components/Icon'; +import IDEButton from '@/components/IDEButton'; +import ReleaseModal from '@/components/modals/releaseModal'; +import PodLineChart from '@/components/PodLineChart'; -const DelModal = dynamic(() => import('@/components/modals/DelModal')) +const DelModal = dynamic(() => import('@/components/modals/DelModal')); const DevboxList = ({ devboxList = [], refetchDevboxList }: { - devboxList: DevboxListItemTypeV2[] - refetchDevboxList: () => void + devboxList: DevboxListItemTypeV2[]; + refetchDevboxList: () => void; }) => { - - const router = useRouter() - const t = useTranslations() - const { message: toast } = useMessage() + const router = useRouter(); + const t = useTranslations(); + const { message: toast } = useMessage(); // TODO: Unified Loading Behavior - const { setLoading } = useGlobalStore() + const { setLoading } = useGlobalStore(); - const [onOpenRelease, setOnOpenRelease] = useState(false) - const [delDevbox, setDelDevbox] = useState(null) + const [onOpenRelease, setOnOpenRelease] = useState(false); + const [delDevbox, setDelDevbox] = useState(null); const [currentDevboxListItem, setCurrentDevboxListItem] = useState( null - ) + ); const handleOpenRelease = (devbox: DevboxListItemTypeV2) => { - setCurrentDevboxListItem(devbox) - setOnOpenRelease(true) - } + setCurrentDevboxListItem(devbox); + setOnOpenRelease(true); + }; const handlePauseDevbox = useCallback( async (devbox: DevboxListItemTypeV2) => { try { - setLoading(true) - await pauseDevbox({ devboxName: devbox.name }) + setLoading(true); + await pauseDevbox({ devboxName: devbox.name }); toast({ title: t('pause_success'), status: 'success' - }) + }); } catch (error: any) { toast({ title: typeof error === 'string' ? error : error.message || t('pause_error'), status: 'error' - }) - console.error(error) + }); + console.error(error); } - refetchDevboxList() - setLoading(false) + refetchDevboxList(); + setLoading(false); }, [refetchDevboxList, setLoading, t, toast] - ) + ); const handleRestartDevbox = useCallback( async (devbox: DevboxListItemTypeV2) => { try { - setLoading(true) - await restartDevbox({ devboxName: devbox.name }) + setLoading(true); + await restartDevbox({ devboxName: devbox.name }); toast({ title: t('restart_success'), status: 'success' - }) + }); } catch (error: any) { toast({ title: typeof error === 'string' ? error : error.message || t('restart_error'), status: 'error' - }) - console.error(error, '==') + }); + console.error(error, '=='); } - refetchDevboxList() - setLoading(false) + refetchDevboxList(); + setLoading(false); }, [refetchDevboxList, setLoading, t, toast] - ) + ); const handleStartDevbox = useCallback( async (devbox: DevboxListItemTypeV2) => { try { - setLoading(true) - await startDevbox({ devboxName: devbox.name }) + setLoading(true); + await startDevbox({ devboxName: devbox.name }); toast({ title: t('start_success'), status: 'success' - }) + }); } catch (error: any) { toast({ title: typeof error === 'string' ? error : error.message || t('start_error'), status: 'error' - }) - console.error(error, '==') + }); + console.error(error, '=='); } - refetchDevboxList() - setLoading(false) + refetchDevboxList(); + setLoading(false); }, [refetchDevboxList, setLoading, t, toast] - ) + ); const handleGoToTerminal = useCallback( async (devbox: DevboxListItemTypeV2) => { - const defaultCommand = `kubectl exec -it $(kubectl get po -l app.kubernetes.io/name=${devbox.name} -oname) -- sh -c "clear; (bash || ash || sh)"` + const defaultCommand = `kubectl exec -it $(kubectl get po -l app.kubernetes.io/name=${devbox.name} -oname) -- sh -c "clear; (bash || ash || sh)"`; try { sealosApp.runEvents('openDesktopApp', { appKey: 'system-terminal', @@ -116,170 +115,173 @@ const DevboxList = ({ defaultCommand }, messageData: { type: 'new terminal', command: defaultCommand } - }) + }); } catch (error: any) { toast({ title: typeof error === 'string' ? error : error.message || t('jump_terminal_error'), status: 'error' - }) - console.error(error) + }); + console.error(error); } }, [t, toast] - ) + ); const columns: { - title: string - dataIndex?: keyof DevboxListItemTypeV2 - key: string - render?: (item: DevboxListItemTypeV2) => JSX.Element + title: string; + dataIndex?: keyof DevboxListItemTypeV2; + key: string; + render?: (item: DevboxListItemTypeV2) => JSX.Element; }[] = [ - { - title: t('name'), - key: 'name', - render: (item) => { - return ( - - {item.id} - - {item.name} - - - ) - } - }, - { - title: t('status'), - key: 'status', - render: (item) => - }, - { - title: t('create_time'), - dataIndex: 'createTime', - key: 'createTime', - render: (item) => { - return {item.createTime} - } - }, - { - title: t('cpu'), - key: 'cpu', - render: (item) => ( - - - - - {item?.usedCpu?.yData[item?.usedCpu?.yData?.length - 1]}% - + { + title: t('name'), + key: 'name', + render: (item) => { + return ( + + {item.id} + + {item.name} + + ); + } + }, + { + title: t('status'), + key: 'status', + render: (item) => + }, + { + title: t('create_time'), + dataIndex: 'createTime', + key: 'createTime', + render: (item) => { + return {item.createTime}; + } + }, + { + title: t('cpu'), + key: 'cpu', + render: (item) => ( + + + + + {item?.usedCpu?.yData[item?.usedCpu?.yData?.length - 1]}% + - ) - }, - { - title: t('memory'), - key: 'storage', - render: (item) => ( - - - - - {item?.usedMemory?.yData[item?.usedMemory?.yData?.length - 1]}% - - + + ) + }, + { + title: t('memory'), + key: 'storage', + render: (item) => ( + + + + + {item?.usedMemory?.yData[item?.usedMemory?.yData?.length - 1]}% + - ) - }, - { - title: t('control'), - key: 'control', - render: (item) => ( - - - - - - - } - menuList={[ - { - child: ( - <> - - {t('publish')} - - ), - onClick: () => handleOpenRelease(item) - }, - { - child: ( - <> - - {t('terminal')} - - ), - onClick: () => handleGoToTerminal(item), - menuItemStyle: { - borderBottomLeftRadius: '0px', - borderBottomRightRadius: '0px', - borderBottom: '1px solid #F0F1F6' - } - }, - { - child: ( - <> - - {t('update')} - - ), - onClick: () => router.push(`/devbox/create?name=${item.name}`) - }, - ...(item.status.value === 'Stopped' - ? [ + + ) + }, + { + title: t('control'), + key: 'control', + render: (item) => ( + + + + + + + } + menuList={[ + { + child: ( + <> + + {t('publish')} + + ), + onClick: () => handleOpenRelease(item) + }, + { + child: ( + <> + + {t('terminal')} + + ), + onClick: () => handleGoToTerminal(item), + menuItemStyle: { + borderBottomLeftRadius: '0px', + borderBottomRightRadius: '0px', + borderBottom: '1px solid #F0F1F6' + } + }, + { + child: ( + <> + + {t('update')} + + ), + onClick: () => router.push(`/devbox/create?name=${item.name}`) + }, + ...(item.status.value === 'Stopped' + ? [ { child: ( <> @@ -290,10 +292,10 @@ const DevboxList = ({ onClick: () => handleStartDevbox(item) } ] - : []), - // maybe Error or other status,all can restart - ...(item.status.value !== 'Stopped' - ? [ + : []), + // maybe Error or other status,all can restart + ...(item.status.value !== 'Stopped' + ? [ { child: ( <> @@ -304,9 +306,9 @@ const DevboxList = ({ onClick: () => handleRestartDevbox(item) } ] - : []), - ...(item.status.value === 'Running' - ? [ + : []), + ...(item.status.value === 'Running' + ? [ { child: ( <> @@ -317,28 +319,28 @@ const DevboxList = ({ onClick: () => handlePauseDevbox(item) } ] - : []), - { - child: ( - <> - - {t('delete')} - - ), - menuItemStyle: { - _hover: { - color: 'red.600', - bg: 'rgba(17, 24, 36, 0.05)' - } - }, - onClick: () => setDelDevbox(item) - } - ]} - /> - - ) - } - ] + : []), + { + child: ( + <> + + {t('delete')} + + ), + menuItemStyle: { + _hover: { + color: 'red.600', + bg: 'rgba(17, 24, 36, 0.05)' + } + }, + onClick: () => setDelDevbox(item) + } + ]} + /> +
+ ) + } + ]; return ( <> @@ -353,17 +355,17 @@ const DevboxList = ({ {!!onOpenRelease && !!currentDevboxListItem && ( { - router.push(`/devbox/detail/${currentDevboxListItem?.name}`) + router.push(`/devbox/detail/${currentDevboxListItem?.name}`); }} onClose={() => { - setOnOpenRelease(false) - setCurrentDevboxListItem(null) + setOnOpenRelease(false); + setCurrentDevboxListItem(null); }} devbox={currentDevboxListItem} /> )} - ) -} + ); +}; -export default DevboxList +export default DevboxList; diff --git a/frontend/providers/devbox/app/[lang]/(platform)/(home)/components/DevboxListContainer.tsx b/frontend/providers/devbox/app/[lang]/(platform)/(home)/components/DevboxListContainer.tsx index de65b83c3a5..6e629c2f529 100644 --- a/frontend/providers/devbox/app/[lang]/(platform)/(home)/components/DevboxListContainer.tsx +++ b/frontend/providers/devbox/app/[lang]/(platform)/(home)/components/DevboxListContainer.tsx @@ -1,113 +1,100 @@ // components/DevboxListContainer.tsx -'use client' -import { useDevboxStore } from '@/stores/devbox' -import { useTemplateStore } from '@/stores/template' -import { DevboxListItemTypeV2 } from '@/types/devbox' -import { isElementInViewport } from '@/utils/tools' -import { Flex, FlexProps } from '@chakra-ui/react' -import { useQuery, useQueryClient } from '@tanstack/react-query' -import { useRouter } from 'next/navigation' -import { useCallback, useEffect, useRef } from 'react' -import DevboxHeader from './DevboxHeader' -import DevboxList from './DevboxList' -import Empty from './Empty' +'use client'; +import { useDevboxStore } from '@/stores/devbox'; +import { useTemplateStore } from '@/stores/template'; +import { DevboxListItemTypeV2 } from '@/types/devbox'; +import { isElementInViewport } from '@/utils/tools'; +import { Flex, FlexProps } from '@chakra-ui/react'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { useRouter } from 'next/navigation'; +import { useCallback, useEffect, useRef } from 'react'; +import DevboxHeader from './DevboxHeader'; +import DevboxList from './DevboxList'; +import Empty from './Empty'; function useDevboxList() { - const queryClient = useQueryClient() - const router = useRouter() - const { devboxList, setDevboxList, loadAvgMonitorData, intervalLoadPods } = useDevboxStore() - const { isOpen: templateIsOpen } = useTemplateStore() - const list = useRef(devboxList) + const queryClient = useQueryClient(); + const router = useRouter(); + const { devboxList, setDevboxList, loadAvgMonitorData, intervalLoadPods } = useDevboxStore(); + const { isOpen: templateIsOpen } = useTemplateStore(); + const list = useRef(devboxList); - const { isLoading, refetch: refetchDevboxList } = useQuery( - ['devboxListQuery'], - setDevboxList, - { - onSettled(res) { - if (!res) return - // refreshList(res) - list.current = res - }, - refetchInterval: !templateIsOpen ? 3000 : false, - staleTime: 3000, - enabled:!templateIsOpen, - } - ) + const { isLoading, refetch: refetchDevboxList } = useQuery(['devboxListQuery'], setDevboxList, { + onSettled(res) { + if (!res) return; + // refreshList(res) + list.current = res; + }, + refetchInterval: !templateIsOpen ? 3000 : false, + staleTime: 3000, + enabled: !templateIsOpen + }); const getViewportDevboxes = (minCount = 3) => { - const doms = document.querySelectorAll('.devboxListItem') + const doms = document.querySelectorAll('.devboxListItem'); const viewportDomIds = Array.from(doms) .filter(isElementInViewport) - .map(item => item.getAttribute('data-id')) + .map((item) => item.getAttribute('data-id')); return viewportDomIds.length < minCount ? devboxList - : devboxList.filter((devbox) => viewportDomIds.includes(devbox.id)) - } + : devboxList.filter((devbox) => viewportDomIds.includes(devbox.id)); + }; useQuery( ['intervalLoadPods', devboxList.length], () => { - - const viewportDevboxList = getViewportDevboxes() + const viewportDevboxList = getViewportDevboxes(); return viewportDevboxList .filter((devbox) => devbox.status.value !== 'Stopped') - .map((devbox) => intervalLoadPods(devbox.name, false)) + .map((devbox) => intervalLoadPods(devbox.name, false)); }, { refetchOnMount: true, refetchInterval: !templateIsOpen ? 3000 : false, staleTime: 3000, - enabled: !isLoading &&!templateIsOpen, + enabled: !isLoading && !templateIsOpen } - ) + ); useQuery( ['loadAvgMonitorData', devboxList.length], () => { - const viewportDevboxList = getViewportDevboxes() + const viewportDevboxList = getViewportDevboxes(); return viewportDevboxList .filter((devbox) => devbox.status.value === 'Running') - .map((devbox) => loadAvgMonitorData(devbox.name)) + .map((devbox) => loadAvgMonitorData(devbox.name)); }, { refetchInterval: !templateIsOpen ? 2 * 60 * 1000 : false, staleTime: 2 * 60 * 1000, - enabled:!isLoading &&!templateIsOpen, + enabled: !isLoading && !templateIsOpen } - ) + ); // 路由预加载 useEffect(() => { - router.prefetch('/devbox/detail') - router.prefetch('/devbox/create') - }, [router]) + router.prefetch('/devbox/detail'); + router.prefetch('/devbox/create'); + }, [router]); return { list: list.current, isLoading, refetchList: useCallback(() => { - queryClient.invalidateQueries(['devboxListQuery']) + queryClient.invalidateQueries(['devboxListQuery']); }, [queryClient]) - } + }; } export default function DevboxListContainer({ ...props }: FlexProps) { - const { list, isLoading, refetchList } = useDevboxList() + const { list, isLoading, refetchList } = useDevboxList(); return ( - + {list.length === 0 && !isLoading ? ( ) : ( - + )} {/* */} - ) -} \ No newline at end of file + ); +} diff --git a/frontend/providers/devbox/app/[lang]/(platform)/(home)/components/Empty.tsx b/frontend/providers/devbox/app/[lang]/(platform)/(home)/components/Empty.tsx index b322cf15d3b..63bac28b38d 100644 --- a/frontend/providers/devbox/app/[lang]/(platform)/(home)/components/Empty.tsx +++ b/frontend/providers/devbox/app/[lang]/(platform)/(home)/components/Empty.tsx @@ -1,13 +1,12 @@ -import { Box, Flex } from '@chakra-ui/react' -import { useTranslations } from 'next-intl' - -import MyIcon from '@/components/Icon' -import { useRouter } from '@/i18n' +import { Box, Flex } from '@chakra-ui/react'; +import { useTranslations } from 'next-intl'; +import MyIcon from '@/components/Icon'; +import { useRouter } from '@/i18n'; const Empty = () => { - const router = useRouter() - const t = useTranslations() + const router = useRouter(); + const t = useTranslations(); return ( { flexDirection="column" alignItems="center" justifyContent="center" - bg={'#F3F4F5'}> + bg={'#F3F4F5'} + > {t('devbox_empty')} - ) -} + ); +}; -export default Empty +export default Empty; diff --git a/frontend/providers/devbox/app/[lang]/(platform)/(home)/page.tsx b/frontend/providers/devbox/app/[lang]/(platform)/(home)/page.tsx index 12608f29ce2..8aa0ee23c91 100644 --- a/frontend/providers/devbox/app/[lang]/(platform)/(home)/page.tsx +++ b/frontend/providers/devbox/app/[lang]/(platform)/(home)/page.tsx @@ -1,5 +1,5 @@ -import DevboxListContainer from "./components/DevboxListContainer" +import DevboxListContainer from './components/DevboxListContainer'; export default function EmptyPage() { - return + return ; } diff --git a/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/Header.tsx b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/Header.tsx index d58728a90e8..c47fb4a7270 100644 --- a/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/Header.tsx +++ b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/Header.tsx @@ -1,16 +1,16 @@ -import { Box, Button, Flex } from '@chakra-ui/react' -import dayjs from 'dayjs' -import JSZip from 'jszip' -import { useTranslations } from 'next-intl' -import { useCallback } from 'react' +import { Box, Button, Flex } from '@chakra-ui/react'; +import dayjs from 'dayjs'; +import JSZip from 'jszip'; +import { useTranslations } from 'next-intl'; +import { useCallback } from 'react'; -import MyIcon from '@/components/Icon' -import { useRouter } from '@/i18n' -import { useEnvStore } from '@/stores/env' -import { useGlobalStore } from '@/stores/global' -import { useTemplateStore } from '@/stores/template' -import type { YamlItemType } from '@/types/index' -import { downLoadBlob } from '@/utils/tools' +import MyIcon from '@/components/Icon'; +import { useRouter } from '@/i18n'; +import { useEnvStore } from '@/stores/env'; +import { useGlobalStore } from '@/stores/global'; +import { useTemplateStore } from '@/stores/template'; +import type { YamlItemType } from '@/types/index'; +import { downLoadBlob } from '@/utils/tools'; const Header = ({ title, @@ -18,33 +18,37 @@ const Header = ({ applyCb, applyBtnText }: { - yamlList: YamlItemType[] - applyCb: () => void - title: string - applyBtnText: string + yamlList: YamlItemType[]; + applyCb: () => void; + title: string; + applyBtnText: string; }) => { - const router = useRouter() - const { lastRoute } = useGlobalStore() - const t = useTranslations() - const { config } = useTemplateStore() - const { env } = useEnvStore() + const router = useRouter(); + const { lastRoute } = useGlobalStore(); + const t = useTranslations(); + const { config } = useTemplateStore(); + const { env } = useEnvStore(); const handleExportYaml = useCallback(async () => { - const zip = new JSZip() + const zip = new JSZip(); yamlList.forEach((item) => { - zip.file(item.filename, item.value) - }) - const res = await zip.generateAsync({ type: 'blob' }) - downLoadBlob(res, 'application/zip', `yaml${dayjs().format('YYYYMMDDHHmmss')}.zip`) - }, [yamlList]) + zip.file(item.filename, item.value); + }); + const res = await zip.generateAsync({ type: 'blob' }); + downLoadBlob(res, 'application/zip', `yaml${dayjs().format('YYYYMMDDHHmmss')}.zip`); + }, [yamlList]); return ( - { - if (config.lastRoute) { - router.replace(lastRoute) - } else { - router.replace(lastRoute) + { + if (config.lastRoute) { + router.replace(lastRoute); + } else { + router.replace(lastRoute); + } }} - }> + > {t(title)} @@ -58,7 +62,7 @@ const Header = ({ {t(applyBtnText)} - ) -} + ); +}; -export default Header +export default Header; diff --git a/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/Yaml.tsx b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/Yaml.tsx index 059147e1500..ac7ea7aaf55 100644 --- a/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/Yaml.tsx +++ b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/Yaml.tsx @@ -1,27 +1,27 @@ -import { useState } from 'react' -import { Tabs } from '@sealos/ui' -import { useTranslations } from 'next-intl' -import { useSearchParams } from 'next/navigation' -import { Box, Center, Flex, Grid, useTheme } from '@chakra-ui/react' +import { useState } from 'react'; +import { Tabs } from '@sealos/ui'; +import { useTranslations } from 'next-intl'; +import { useSearchParams } from 'next/navigation'; +import { Box, Center, Flex, Grid, useTheme } from '@chakra-ui/react'; -import { useRouter } from '@/i18n' -import MyIcon from '@/components/Icon' -import { obj2Query } from '@/utils/tools' -import type { YamlItemType } from '@/types' -import { useCopyData } from '@/utils/tools' -import YamlCode from '@/components/YamlCode/index' +import { useRouter } from '@/i18n'; +import MyIcon from '@/components/Icon'; +import { obj2Query } from '@/utils/tools'; +import type { YamlItemType } from '@/types'; +import { useCopyData } from '@/utils/tools'; +import YamlCode from '@/components/YamlCode/index'; -import styles from './index.module.scss' +import styles from './index.module.scss'; const Yaml = ({ yamlList = [], pxVal }: { yamlList: YamlItemType[]; pxVal: number }) => { - const theme = useTheme() - const router = useRouter() - const t = useTranslations() - const { copyData } = useCopyData() - const searchParams = useSearchParams() - const [selectedIndex, setSelectedIndex] = useState(0) + const theme = useTheme(); + const router = useRouter(); + const t = useTranslations(); + const { copyData } = useCopyData(); + const searchParams = useSearchParams(); + const [selectedIndex, setSelectedIndex] = useState(0); - const devboxName = searchParams.get('name') as string + const devboxName = searchParams.get('name') as string; return ( + px={`${pxVal}px`} + > + border={theme.borders.base} + > {yamlList.map((file, index) => ( setSelectedIndex(index)}> + onClick={() => setSelectedIndex(index)} + > + opacity={selectedIndex === index ? 1 : 0} + > {file.filename} ))} @@ -97,7 +101,8 @@ const Yaml = ({ yamlList = [], pxVal }: { yamlList: YamlItemType[]; pxVal: numbe overflow={'hidden'} border={theme.borders.base} borderRadius={'md'} - position={'relative'}> + position={'relative'} + > {yamlList[selectedIndex].filename} @@ -106,7 +111,8 @@ const Yaml = ({ yamlList = [], pxVal }: { yamlList: YamlItemType[]; pxVal: numbe cursor={'pointer'} color={'grayModern.600'} _hover={{ color: '#219BF4' }} - onClick={() => copyData(yamlList[selectedIndex].value)}> + onClick={() => copyData(yamlList[selectedIndex].value)} + > @@ -116,7 +122,7 @@ const Yaml = ({ yamlList = [], pxVal }: { yamlList: YamlItemType[]; pxVal: numbe )} - ) -} + ); +}; -export default Yaml +export default Yaml; diff --git a/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/form/BasicConfiguration/CpuSelector.tsx b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/form/BasicConfiguration/CpuSelector.tsx index fa527cb89c2..4617ba56f17 100644 --- a/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/form/BasicConfiguration/CpuSelector.tsx +++ b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/form/BasicConfiguration/CpuSelector.tsx @@ -1,28 +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" +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 CpuSelector(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')} - - -} \ No newline at end of file + 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/DevboxNameInput.tsx b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/form/BasicConfiguration/DevboxNameInput.tsx index a4b7084dc81..902313b6a2b 100644 --- a/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/form/BasicConfiguration/DevboxNameInput.tsx +++ b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/form/BasicConfiguration/DevboxNameInput.tsx @@ -1,56 +1,58 @@ -import { DevboxEditTypeV2 } from "@/types/devbox" -import { nanoid } from "@/utils/tools" -import { devboxNameSchema } from "@/utils/vaildate" -import { Flex, FormControl, FormControlProps, Input } from "@chakra-ui/react" -import { useTranslations } from "next-intl" -import { useFieldArray, useFormContext } from "react-hook-form" -import Label from "../Label" +import { DevboxEditTypeV2 } from '@/types/devbox'; +import { nanoid } from '@/utils/tools'; +import { devboxNameSchema } from '@/utils/vaildate'; +import { Flex, FormControl, FormControlProps, Input } from '@chakra-ui/react'; +import { useTranslations } from 'next-intl'; +import { useFieldArray, useFormContext } from 'react-hook-form'; +import Label from '../Label'; -export default function DevboxNameInput({isEdit, ...props}: {isEdit: boolean} & FormControlProps) { - const t = useTranslations() +export default function DevboxNameInput({ + isEdit, + ...props +}: { isEdit: boolean } & FormControlProps) { + const t = useTranslations(); const { register, setValue, formState: { errors }, control - } = useFormContext() - const { - fields: networks, - update: updateNetworks - } = useFieldArray({ + } = useFormContext(); + const { fields: networks, update: updateNetworks } = useFieldArray({ control, name: 'networks' - }) - return - - - - devboxNameSchema.safeParse(value).success || t('devbox_name_invalid') - } - })} - onBlur={(e) => { - const lowercaseValue = e.target.value.toLowerCase() - setValue('name', lowercaseValue) - networks.forEach((network, i) => { - updateNetworks(i, { - ...network, - networkName: `${lowercaseValue}-${nanoid()}` - }) - }) - }} - /> - - -} \ No newline at end of file + }); + return ( + + + + + devboxNameSchema.safeParse(value).success || t('devbox_name_invalid') + } + })} + onBlur={(e) => { + const lowercaseValue = e.target.value.toLowerCase(); + setValue('name', lowercaseValue); + networks.forEach((network, i) => { + updateNetworks(i, { + ...network, + networkName: `${lowercaseValue}-${nanoid()}` + }); + }); + }} + /> + + + ); +} 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 edb5792a989..777c1018891 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,48 +1,48 @@ -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 { 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' +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 +const labelWidth = 100; export default function GpuSelector({ countGpuInventory }: { - countGpuInventory: (type: string) => number + countGpuInventory: (type: string) => number; }) { - const t = useTranslations() - const { sourcePrice } = usePriceStore() - const { watch, setValue, getValues } = useFormContext() + const t = useTranslations(); + const { sourcePrice } = usePriceStore(); + const { watch, setValue, getValues } = useFormContext(); const templateRepositoryQuery = useQuery( ['list-official-template-repository'], listOfficialTemplateRepository - ) + ); const templateData = useMemo( () => templateRepositoryQuery.data?.templateRepositoryList || [], [templateRepositoryQuery.data] - ) - const templateRepositoryUid = getValues('templateRepositoryUid') + ); + const 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 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 + 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( @@ -78,10 +78,10 @@ export default function GpuSelector({ ] : [], [countGpuInventory, t, sourcePrice?.gpu] - ) + ); if (!isGpuTemplate || !sourcePrice?.gpu) { - return null + return null; } return ( @@ -94,10 +94,10 @@ export default function GpuSelector({ value={getValues('gpu.type')} list={gpuSelectList} onchange={(type: any) => { - const selected = sourcePrice?.gpu?.find((item) => item.type === type) - const inventory = countGpuInventory(type) + const selected = sourcePrice?.gpu?.find((item) => item.type === type); + const inventory = countGpuInventory(type); if (type === '' || (selected && inventory > 0)) { - setValue('gpu.type', type) + setValue('gpu.type', type); } }} /> @@ -107,9 +107,9 @@ export default function GpuSelector({ {t('Amount')} {GpuAmountMarkList.map((item) => { - const inventory = selectedGpu()?.inventory || 0 + const inventory = selectedGpu()?.inventory || 0; - const hasInventory = item.value <= inventory + const hasInventory = item.value <= inventory; return ( @@ -133,17 +133,18 @@ export default function GpuSelector({ ? { cursor: 'pointer', onClick: () => { - setValue('gpu.amount', item.value) + setValue('gpu.amount', item.value); } } : { cursor: 'default', opacity: 0.5 - })}> + })} + > {item.label} - ) + ); })} / {t('Card')} @@ -152,5 +153,5 @@ export default function GpuSelector({ )}
- ) + ); } diff --git a/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/form/BasicConfiguration/MemorySelector.tsx b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/form/BasicConfiguration/MemorySelector.tsx index 3af3ced3037..7adb6893caf 100644 --- a/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/form/BasicConfiguration/MemorySelector.tsx +++ b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/form/BasicConfiguration/MemorySelector.tsx @@ -1,25 +1,27 @@ -import { MemorySlideMarkList } from "@/constants/devbox" -import { DevboxEditTypeV2 } from "@/types/devbox" -import { 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" +import { MemorySlideMarkList } from '@/constants/devbox'; +import { DevboxEditTypeV2 } from '@/types/devbox'; +import { 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 MemorySelector(props: FlexProps) { - const t = useTranslations() - const { watch, setValue } = useFormContext() - return - - { - setValue('memory', MemorySlideMarkList[e].value) - }} - max={MemorySlideMarkList.length - 1} - min={0} - step={1} - /> - -} \ No newline at end of file + const t = useTranslations(); + const { watch, setValue } = useFormContext(); + return ( + + + { + setValue('memory', MemorySlideMarkList[e].value); + }} + max={MemorySlideMarkList.length - 1} + min={0} + step={1} + /> + + ); +} diff --git a/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/form/BasicConfiguration/TemplateRepositoryListNav.tsx b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/form/BasicConfiguration/TemplateRepositoryListNav.tsx index b301d0bdb72..3af4119ca7e 100644 --- a/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/form/BasicConfiguration/TemplateRepositoryListNav.tsx +++ b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/form/BasicConfiguration/TemplateRepositoryListNav.tsx @@ -1,14 +1,14 @@ -import MyIcon from "@/components/Icon"; -import { TemplateState } from "@/constants/template"; -import { useTemplateStore } from "@/stores/template"; -import { Box, Flex, Text } from "@chakra-ui/react"; -import { useTranslations } from "next-intl"; -import { usePathname } from "next/navigation"; +import MyIcon from '@/components/Icon'; +import { TemplateState } from '@/constants/template'; +import { useTemplateStore } from '@/stores/template'; +import { Box, Flex, Text } from '@chakra-ui/react'; +import { useTranslations } from 'next-intl'; +import { usePathname } from 'next/navigation'; const TemplateRepositoryListNav = () => { - const t = useTranslations() - const { openTemplateModal, config, isOpen } = useTemplateStore() - const lastRoute = usePathname() + const t = useTranslations(); + const { openTemplateModal, config, isOpen } = useTemplateStore(); + const lastRoute = usePathname(); return ( { cursor="pointer" onClick={() => { openTemplateModal({ - 'templateState': TemplateState.publicTemplate, + templateState: TemplateState.publicTemplate, lastRoute - }) + }); }} > { width="18px" height="18px" color="#0884DD" - fill={"#0884DD"} + fill={'#0884DD'} /> { letterSpacing="0.5px" color="#485264" > - {t("all_templates")} + {t('all_templates')} {/* Divider */} - + {/* My Templates Tab */} { cursor="pointer" onClick={() => { openTemplateModal({ - 'templateState': TemplateState.privateTemplate, + templateState: TemplateState.privateTemplate, lastRoute - }) + }); }} > - + { letterSpacing="0.5px" color="#485264" > - {t("my_templates")} + {t('my_templates')} ); }; -export default TemplateRepositoryListNav; \ No newline at end of file +export default TemplateRepositoryListNav; 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 94bd9be484b..8ed87ce12d4 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,21 +1,21 @@ -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 + 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() + 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 (isEdit) return; + const devboxName = getValues('name'); if (!devboxName) { toast({ title: t('Please enter the devbox name first'), status: 'warning' - }) - return + }); + return; } - setValue('gpu.type', '') + setValue('gpu.type', ''); if (startedTemplate && startedTemplate.uid !== item.uid) { - setStartedTemplate(undefined) + setStartedTemplate(undefined); } - setValue('templateRepositoryUid', item.uid) - }}> + setValue('templateRepositoryUid', item.uid); + }} + >
- ) + ); } diff --git a/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/form/BasicConfiguration/TemplateRepositorySelector/index.tsx b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/form/BasicConfiguration/TemplateRepositorySelector/index.tsx index b0e7ebaa212..587dbd48aa1 100644 --- a/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/form/BasicConfiguration/TemplateRepositorySelector/index.tsx +++ b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/form/BasicConfiguration/TemplateRepositorySelector/index.tsx @@ -1,113 +1,158 @@ -import { getTemplateRepository, listOfficialTemplateRepository } from '@/api/template' -import { TemplateRepositoryKind } from '@/prisma/generated/client' -import { useDevboxStore } from '@/stores/devbox' -import { DevboxEditTypeV2 } from '@/types/devbox' -import { TemplateRepository } from '@/types/template' -import { Box, Flex, VStack } from '@chakra-ui/react' -import { useQuery } from '@tanstack/react-query' -import { useTranslations } from 'next-intl' -import { useEffect, useMemo } from 'react' -import { useFormContext } from 'react-hook-form' -import Label from '../../Label' -import TemplateRepositoryListNav from '../TemplateRepositoryListNav' -import TemplateRepositoryItem from './TemplateReposistoryItem' +import { getTemplateRepository, listOfficialTemplateRepository } from '@/api/template'; +import { TemplateRepositoryKind } from '@/prisma/generated/client'; +import { useDevboxStore } from '@/stores/devbox'; +import { DevboxEditTypeV2 } from '@/types/devbox'; +import { TemplateRepository } from '@/types/template'; +import { Box, Flex, VStack } from '@chakra-ui/react'; +import { useQuery } from '@tanstack/react-query'; +import { useTranslations } from 'next-intl'; +import { useEffect, useMemo } from 'react'; +import { useFormContext } from 'react-hook-form'; +import Label from '../../Label'; +import TemplateRepositoryListNav from '../TemplateRepositoryListNav'; +import TemplateRepositoryItem from './TemplateReposistoryItem'; interface TemplateRepositorySelectorProps { - isEdit: boolean + isEdit: boolean; } -export default function TemplateRepositorySelector({ - isEdit, -}: TemplateRepositorySelectorProps) { - const { startedTemplate, setStartedTemplate } = useDevboxStore() - const { setValue, getValues, watch } = useFormContext() - const t = useTranslations() - const templateRepositoryQuery = useQuery(['list-official-template-repository'], listOfficialTemplateRepository, { - staleTime: Infinity, - cacheTime: 1000 * 60 * 30 - }) - const curTemplateRepositoryUid = watch('templateRepositoryUid') - const curTemplateRepositoryDetail = useQuery(['get-template-repository', curTemplateRepositoryUid], () => { - return getTemplateRepository(curTemplateRepositoryUid) - }, { - enabled: !!isEdit && !!curTemplateRepositoryUid, - }) +export default function TemplateRepositorySelector({ isEdit }: TemplateRepositorySelectorProps) { + const { startedTemplate, setStartedTemplate } = useDevboxStore(); + const { setValue, getValues, watch } = useFormContext(); + const t = useTranslations(); + const templateRepositoryQuery = useQuery( + ['list-official-template-repository'], + listOfficialTemplateRepository, + { + staleTime: Infinity, + cacheTime: 1000 * 60 * 30 + } + ); + const curTemplateRepositoryUid = watch('templateRepositoryUid'); + const curTemplateRepositoryDetail = useQuery( + ['get-template-repository', curTemplateRepositoryUid], + () => { + return getTemplateRepository(curTemplateRepositoryUid); + }, + { + enabled: !!isEdit && !!curTemplateRepositoryUid + } + ); - const templateData = useMemo(() => templateRepositoryQuery.data?.templateRepositoryList || [], [templateRepositoryQuery.data]) + const templateData = useMemo( + () => templateRepositoryQuery.data?.templateRepositoryList || [], + [templateRepositoryQuery.data] + ); const categorizedData = useMemo(() => { - return templateData.reduce((acc, item) => { - acc[item.kind] = [...(acc[item.kind] || []), item] - return acc - }, { - 'LANGUAGE': [], - 'FRAMEWORK': [], - 'OS': [], - 'CUSTOM': [] - } as Record) - }, [templateData]) + return templateData.reduce( + (acc, item) => { + acc[item.kind] = [...(acc[item.kind] || []), item]; + return acc; + }, + { + LANGUAGE: [], + FRAMEWORK: [], + OS: [], + CUSTOM: [] + } as Record + ); + }, [templateData]); useEffect(() => { if (!startedTemplate || isEdit) { - return + return; } - const templateUid = startedTemplate.uid - if (templateData.findIndex((item) => { - return item.uid === templateUid - }) > -1) { - setStartedTemplate(undefined) + const templateUid = startedTemplate.uid; + if ( + templateData.findIndex((item) => { + return item.uid === templateUid; + }) > -1 + ) { + setStartedTemplate(undefined); } - setValue('templateRepositoryUid', templateUid) - }, [startedTemplate, isEdit]) + setValue('templateRepositoryUid', templateUid); + }, [startedTemplate, isEdit]); useEffect(() => { if (startedTemplate || isEdit) { - return + return; } - if (!(templateRepositoryQuery.isSuccess - && templateData.length > 0 - && templateRepositoryQuery.isFetched)) return - const curTemplateRepositoryUid = getValues('templateRepositoryUid') + if ( + !( + templateRepositoryQuery.isSuccess && + templateData.length > 0 && + templateRepositoryQuery.isFetched + ) + ) + return; + const curTemplateRepositoryUid = getValues('templateRepositoryUid'); const curTemplateRepository = templateData.find((item) => { - return item.uid === curTemplateRepositoryUid - }) + return item.uid === curTemplateRepositoryUid; + }); if (!curTemplateRepository) { - const defaultTemplateRepositoryUid = templateData[0].uid - setValue('templateRepositoryUid', defaultTemplateRepositoryUid) + const defaultTemplateRepositoryUid = templateData[0].uid; + setValue('templateRepositoryUid', defaultTemplateRepositoryUid); } - }, [templateRepositoryQuery.isSuccess, startedTemplate, templateData, templateRepositoryQuery.isFetched, isEdit]) + }, [ + templateRepositoryQuery.isSuccess, + startedTemplate, + templateData, + templateRepositoryQuery.isFetched, + isEdit + ]); useEffect(() => { - if (!isEdit || !templateRepositoryQuery.isSuccess || !templateData || !curTemplateRepositoryDetail.isSuccess || !curTemplateRepositoryDetail.data) { - return + if ( + !isEdit || + !templateRepositoryQuery.isSuccess || + !templateData || + !curTemplateRepositoryDetail.isSuccess || + !curTemplateRepositoryDetail.data + ) { + return; } - const templateRepository = curTemplateRepositoryDetail.data.templateRepository + const templateRepository = curTemplateRepositoryDetail.data.templateRepository; // setStartedTemplate(templateRepository) - setValue('templateRepositoryUid', templateRepository.uid) + setValue('templateRepositoryUid', templateRepository.uid); - if (templateData.findIndex((item) => { - return item.uid === templateRepository.uid - }) === -1) { - setStartedTemplate(templateRepository) + if ( + templateData.findIndex((item) => { + return item.uid === templateRepository.uid; + }) === -1 + ) { + setStartedTemplate(templateRepository); } - }, [curTemplateRepositoryDetail.isSuccess, curTemplateRepositoryDetail.data, curTemplateRepositoryDetail.isFetched, isEdit, templateData, templateRepositoryQuery.isSuccess]) + }, [ + curTemplateRepositoryDetail.isSuccess, + curTemplateRepositoryDetail.data, + curTemplateRepositoryDetail.isFetched, + isEdit, + templateData, + templateRepositoryQuery.isSuccess + ]); return ( - + - {!!startedTemplate && - {t('current')} - - + {!!startedTemplate && ( + + {t('current')} + + + - } + )} {/* Language */} {categorizedData['LANGUAGE'].length !== 0 && {t('language')}} @@ -136,6 +181,5 @@ export default function TemplateRepositorySelector({ - ) + ); } - diff --git a/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/form/BasicConfiguration/TemplateSelector/index.tsx b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/form/BasicConfiguration/TemplateSelector/index.tsx index 1ac11c76f91..5402ca67793 100644 --- a/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/form/BasicConfiguration/TemplateSelector/index.tsx +++ b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/form/BasicConfiguration/TemplateSelector/index.tsx @@ -1,36 +1,38 @@ // RuntimeVersionSelector.tsx -import { listTemplate } from '@/api/template' -import { useEnvStore } from '@/stores/env' -import { DevboxEditTypeV2 } from '@/types/devbox' -import { nanoid } from '@/utils/tools' -import { Flex, Input } from '@chakra-ui/react' -import { MySelect, useMessage } from '@sealos/ui' -import { useQuery } from '@tanstack/react-query' -import { useTranslations } from 'next-intl' -import { useEffect } from 'react' -import { useController, useFormContext } from 'react-hook-form' -import { z } from 'zod' -import Label from '../../Label' +import { listTemplate } from '@/api/template'; +import { useEnvStore } from '@/stores/env'; +import { DevboxEditTypeV2 } from '@/types/devbox'; +import { nanoid } from '@/utils/tools'; +import { Flex, Input } from '@chakra-ui/react'; +import { MySelect, useMessage } from '@sealos/ui'; +import { useQuery } from '@tanstack/react-query'; +import { useTranslations } from 'next-intl'; +import { useEffect } from 'react'; +import { useController, useFormContext } from 'react-hook-form'; +import { z } from 'zod'; +import Label from '../../Label'; interface TemplateSelectorProps { - isEdit: boolean + isEdit: boolean; } -export default function TemplateSelector({ - isEdit, -}: TemplateSelectorProps) { - const { getValues, setValue, watch, control } = useFormContext() - const { env } = useEnvStore() - const { message: toast } = useMessage() - const templateRepositoryUid = watch('templateRepositoryUid') - const isVaildTemplateRepositoryUid = z.string().uuid().safeParse(templateRepositoryUid).success - const templateListQuery = useQuery(['templateList', templateRepositoryUid], () => listTemplate(templateRepositoryUid), { - enabled: isVaildTemplateRepositoryUid, - }) - const templateList = (templateListQuery.data?.templateList || []) - const t = useTranslations() +export default function TemplateSelector({ isEdit }: TemplateSelectorProps) { + const { getValues, setValue, watch, control } = useFormContext(); + const { env } = useEnvStore(); + const { message: toast } = useMessage(); + const templateRepositoryUid = watch('templateRepositoryUid'); + const isVaildTemplateRepositoryUid = z.string().uuid().safeParse(templateRepositoryUid).success; + const templateListQuery = useQuery( + ['templateList', templateRepositoryUid], + () => listTemplate(templateRepositoryUid), + { + enabled: isVaildTemplateRepositoryUid + } + ); + const templateList = templateListQuery.data?.templateList || []; + const t = useTranslations(); // const defaultTemplateUid = watch('templateUid') - const menuList = templateList.map(v => ({ label: v.name, value: v.uid })) + const menuList = templateList.map((v) => ({ label: v.name, value: v.uid })); // const defaultTemplate = defaultTemplateUid ? templateList.find(t => t.uid === defaultTemplateUid) : templateList[0] const { field } = useController({ @@ -39,54 +41,50 @@ export default function TemplateSelector({ rules: { required: t('This runtime field is required') } - }) + }); const afterUpdateTemplate = (uid: string) => { - const template = templateList.find(v => v.uid === uid)! - setValue( - 'templateConfig', - template.config as string - ) - setValue( - 'image', - template.image - ) - - } + const template = templateList.find((v) => v.uid === uid)!; + setValue('templateConfig', template.config as string); + setValue('image', template.image); + }; const resetNetwork = () => { - const devboxName = getValues('name') - const config = getValues('templateConfig') - const parsedConfig = - JSON.parse(config as string) as { appPorts: [{ port: number, name: string, protocol: string }] } + const devboxName = getValues('name'); + const config = getValues('templateConfig'); + const parsedConfig = JSON.parse(config as string) as { + appPorts: [{ port: number; name: string; protocol: string }]; + }; setValue( 'networks', parsedConfig.appPorts.map( - ({ port }) => ({ - networkName: `${devboxName}-${nanoid()}`, - portName: nanoid(), - port: port, - protocol: 'HTTP', - openPublicDomain: true, - publicDomain: `${nanoid()}.${env.ingressDomain}`, - customDomain: '' - } as const) + ({ port }) => + ({ + networkName: `${devboxName}-${nanoid()}`, + portName: nanoid(), + port: port, + protocol: 'HTTP', + openPublicDomain: true, + publicDomain: `${nanoid()}.${env.ingressDomain}`, + customDomain: '' + }) as const ) - ) - } + ); + }; useEffect(() => { - if (!templateListQuery.isSuccess || !templateList.length || !templateListQuery.isFetched) return + if (!templateListQuery.isSuccess || !templateList.length || !templateListQuery.isFetched) + return; - const curTemplate = templateList.find(t => t.uid === field.value) - const isExist = !!curTemplate + const curTemplate = templateList.find((t) => t.uid === field.value); + const isExist = !!curTemplate; if (!isExist) { - const defaultTemplate = templateList[0] - setValue('templateUid', defaultTemplate.uid) - afterUpdateTemplate(defaultTemplate.uid) - resetNetwork() + const defaultTemplate = templateList[0]; + setValue('templateUid', defaultTemplate.uid); + afterUpdateTemplate(defaultTemplate.uid); + resetNetwork(); } else { - setValue('templateUid', curTemplate.uid) - afterUpdateTemplate(curTemplate.uid) + setValue('templateUid', curTemplate.uid); + afterUpdateTemplate(curTemplate.uid); } - }, [templateListQuery.isSuccess, templateList, templateListQuery.isFetched, isEdit]) + }, [templateListQuery.isSuccess, templateList, templateListQuery.isFetched, isEdit]); return ( @@ -94,7 +92,7 @@ export default function TemplateSelector({ t.uid === field.value)?.name} + defaultValue={templateList.find((t) => t.uid === field.value)?.name} disabled /> ) : ( @@ -108,21 +106,21 @@ export default function TemplateSelector({ name={field.name} onchange={(val) => { // if (isEdit) return - const devboxName = getValues('name') + const devboxName = getValues('name'); if (!devboxName) { toast({ title: t('Please enter the devbox name first'), status: 'warning' - }) - return + }); + return; } - const oldTemplateUid = getValues('templateUid') - field.onChange(val) - afterUpdateTemplate(val) - if(oldTemplateUid !== val) resetNetwork() + const oldTemplateUid = getValues('templateUid'); + field.onChange(val); + afterUpdateTemplate(val); + if (oldTemplateUid !== val) resetNetwork(); }} /> )} - ) -} \ No newline at end of file + ); +} 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 3f642d92a56..43b344b596f 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,21 +1,21 @@ -import MyIcon from '@/components/Icon' -import { Box, BoxProps } from '@chakra-ui/react' -import { useTranslations } from 'next-intl' +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' +import CpuSelector from './CpuSelector'; +import GpuSelector from './GpuSelector'; +import MemorySelector from './MemorySelector'; +import DevboxNameInput from './DevboxNameInput'; +import TemplateSelector from './TemplateSelector'; +import ConfigurationHeader from '../ConfigurationHeader'; +import TemplateRepositorySelector from './TemplateRepositorySelector'; export default function BasicConfiguration({ isEdit, countGpuInventory, ...props }: BoxProps & { isEdit: boolean; countGpuInventory: (type: string) => number }) { - const t = useTranslations() + const t = useTranslations(); return ( @@ -37,5 +37,5 @@ export default function BasicConfiguration({
- ) + ); } diff --git a/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/form/ConfigurationHeader.tsx b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/form/ConfigurationHeader.tsx index d38d46c5e1b..7782c5c1ec3 100644 --- a/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/form/ConfigurationHeader.tsx +++ b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/form/ConfigurationHeader.tsx @@ -1,5 +1,5 @@ -import { Box, BoxProps } from "@chakra-ui/react"; -import { ReactNode } from "react"; +import { Box, BoxProps } from '@chakra-ui/react'; +import { ReactNode } from 'react'; export default function ConfigurationHeader({ children }: { children: ReactNode }) { const headerStyles: BoxProps = { @@ -12,8 +12,6 @@ export default function ConfigurationHeader({ children }: { children: ReactNode display: 'flex', alignItems: 'center', backgroundColor: 'grayModern.50' - } - return - {children} - -} \ No newline at end of file + }; + return {children}; +} diff --git a/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/form/Label.tsx b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/form/Label.tsx index 1804caecf39..ee8f45d9f9b 100644 --- a/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/form/Label.tsx +++ b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/form/Label.tsx @@ -1,20 +1,21 @@ -import { Box, BoxProps } from "@chakra-ui/react" +import { Box, BoxProps } from '@chakra-ui/react'; const Label = ({ children, w = 'auto', ...props }: { - children: string - w?: number | 'auto' + children: string; + w?: number | 'auto'; } & BoxProps) => ( + {...props} + > {children} -) -export default Label \ No newline at end of file +); +export default Label; diff --git a/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/form/NetworkConfiguration/index.tsx b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/form/NetworkConfiguration/index.tsx index 72e699922eb..364c949cb3b 100644 --- a/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/form/NetworkConfiguration/index.tsx +++ b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/components/form/NetworkConfiguration/index.tsx @@ -1,43 +1,51 @@ -import MyIcon from "@/components/Icon" -import { ProtocolList } from "@/constants/devbox" -import { useEnvStore } from "@/stores/env" -import { DevboxEditTypeV2 } from "@/types/devbox" -import { nanoid } from "@/utils/tools" -import { Box, BoxProps, Button, ButtonProps, Flex, IconButton, Input, Switch, useTheme } from "@chakra-ui/react" -import { MySelect, useMessage } from "@sealos/ui" -import { useTranslations } from "next-intl" -import dynamic from "next/dynamic" -import { useState } from "react" -import { useFieldArray, useFormContext } from "react-hook-form" -import ConfigurationHeader from "../ConfigurationHeader" +import MyIcon from '@/components/Icon'; +import { ProtocolList } from '@/constants/devbox'; +import { useEnvStore } from '@/stores/env'; +import { DevboxEditTypeV2 } from '@/types/devbox'; +import { nanoid } from '@/utils/tools'; +import { + Box, + BoxProps, + Button, + ButtonProps, + Flex, + IconButton, + Input, + Switch, + useTheme +} from '@chakra-ui/react'; +import { MySelect, useMessage } from '@sealos/ui'; +import { useTranslations } from 'next-intl'; +import dynamic from 'next/dynamic'; +import { useState } from 'react'; +import { useFieldArray, useFormContext } from 'react-hook-form'; +import ConfigurationHeader from '../ConfigurationHeader'; // const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 12) export type CustomAccessModalParams = { - publicDomain: string - customDomain: string -} + publicDomain: string; + customDomain: string; +}; -const CustomAccessModal = dynamic(() => import('@/components/modals/CustomAccessModal')) +const CustomAccessModal = dynamic(() => import('@/components/modals/CustomAccessModal')); const AppendNetworksButton = (props: ButtonProps) => { - const t = useTranslations() - return () -} + const t = useTranslations(); + return ( + + ); +}; export default function NetworkConfiguration({ isEdit, ...props }: BoxProps & { isEdit: boolean }) { - const { - register, - getValues, - control - } = useFormContext() - const theme = useTheme() - const [customAccessModalData, setCustomAccessModalData] = useState() - const { env } = useEnvStore() + const { register, getValues, control } = useFormContext(); + const theme = useTheme(); + const [customAccessModalData, setCustomAccessModalData] = useState(); + const { env } = useEnvStore(); const { fields: networks, update: updateNetworks, @@ -46,9 +54,9 @@ export default function NetworkConfiguration({ isEdit, ...props }: BoxProps & { } = useFieldArray({ control, name: 'networks' - }) - const t = useTranslations() - const { message: toast } = useMessage() + }); + const t = useTranslations(); + const { message: toast } = useMessage(); const appendNetworks = () => { _appendNetworks({ networkName: '', @@ -58,184 +66,188 @@ export default function NetworkConfiguration({ isEdit, ...props }: BoxProps & { openPublicDomain: false, publicDomain: '', customDomain: '' - }) - } + }); + }; // const networks = watch('networks') - return <> - - - {t('Network Configuration')} - - - {networks.length === 0 && ( - appendNetworks()} /> - )} - {networks.map((network, i) => ( - - - - {t('Container Port')} - - { - const ports = getValues('networks').map((network, index) => ({ - port: network.port, - index - })); - // 排除当前正在编辑的端口 - const isDuplicate = ports.some( - (item) => item.port === value && item.index !== i - ); - return !isDuplicate || t('The port number cannot be repeated'); - } - } - })} - /> - {i === networks.length - 1 && networks.length < 5 && ( - - appendNetworks()} /> + return ( + <> + + + + {t('Network Configuration')} + + + {networks.length === 0 && appendNetworks()} />} + {networks.map((network, i) => ( + + + + {t('Container Port')} + + { + const ports = getValues('networks').map((network, index) => ({ + port: network.port, + index + })); + // 排除当前正在编辑的端口 + const isDuplicate = ports.some( + (item) => item.port === value && item.index !== i + ); + return !isDuplicate || t('The port number cannot be repeated'); + } + } + })} + /> + {i === networks.length - 1 && networks.length < 5 && ( + + appendNetworks()} /> + + )} - )} - - - - {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 && ( - <> - - + + + {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], - protocol: val - }) + networkName: network.networkName || `${devboxName}-${nanoid()}`, + protocol: network.protocol || 'HTTP', + openPublicDomain: e.target.checked, + publicDomain: network.publicDomain || `${nanoid()}.${env.ingressDomain}` + }); }} /> - - - {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) - }} - /> - )} -} \ No newline at end of file + {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); + }} + /> + )} + + ); +} 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 8717db72d5c..e613a510f84 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,37 +1,37 @@ -'use client' +'use client'; -import { Box, Flex, Grid, useTheme } from '@chakra-ui/react' -import { Tabs } from '@sealos/ui' -import { throttle } from 'lodash' -import { useTranslations } from 'next-intl' -import { useEffect, useState } from 'react' -import { useFormContext } from 'react-hook-form' +import { Box, Flex, Grid, useTheme } from '@chakra-ui/react'; +import { Tabs } from '@sealos/ui'; +import { throttle } from 'lodash'; +import { useTranslations } from 'next-intl'; +import { useEffect, useState } from 'react'; +import { useFormContext } from 'react-hook-form'; -import MyIcon from '@/components/Icon' -import PriceBox from '@/components/PriceBox' -import QuotaBox from '@/components/QuotaBox' -import { useRouter } from '@/i18n' +import MyIcon from '@/components/Icon'; +import PriceBox from '@/components/PriceBox'; +import QuotaBox from '@/components/QuotaBox'; +import { useRouter } from '@/i18n'; -import { useDevboxStore } from '@/stores/devbox' +import { useDevboxStore } from '@/stores/devbox'; -import type { DevboxEditTypeV2 } from '@/types/devbox' -import { obj2Query } from '@/utils/tools' -import BasicConfiguration from './BasicConfiguration' -import NetworkConfiguration from './NetworkConfiguration' +import type { DevboxEditTypeV2 } from '@/types/devbox'; +import { obj2Query } from '@/utils/tools'; +import BasicConfiguration from './BasicConfiguration'; +import NetworkConfiguration from './NetworkConfiguration'; const Form = ({ pxVal, isEdit, countGpuInventory }: { - pxVal: number - isEdit: boolean - countGpuInventory: (type: string) => number + pxVal: number; + isEdit: boolean; + countGpuInventory: (type: string) => number; }) => { - const theme = useTheme() - const router = useRouter() - const t = useTranslations() - const { watch } = useFormContext() + const theme = useTheme(); + const router = useRouter(); + const t = useTranslations(); + const { watch } = useFormContext(); const navList: { id: string; label: string; icon: string }[] = [ { id: 'baseInfo', @@ -43,43 +43,43 @@ const Form = ({ label: t('Network Configuration'), icon: 'network' } - ] - const [activeNav, setActiveNav] = useState(navList[0].id) - const { devboxList } = useDevboxStore() + ]; + const [activeNav, setActiveNav] = useState(navList[0].id); + const { devboxList } = useDevboxStore(); // listen scroll and set activeNav useEffect(() => { const scrollFn = throttle((e: Event) => { - if (!e.target) return + 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 + 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 + const offsetTop = doms[i].dom?.offsetTop || 0; if (scrollTop + 500 >= offsetTop) { - setActiveNav(doms[i].id) - break + setActiveNav(doms[i].id); + break; } } - }, 200) - document.getElementById('form-container')?.addEventListener('scroll', scrollFn) + }, 200); + document.getElementById('form-container')?.addEventListener('scroll', scrollFn); return () => { - document.getElementById('form-container')?.removeEventListener('scroll', scrollFn) - } + document.getElementById('form-container')?.removeEventListener('scroll', scrollFn); + }; // eslint-disable-next-line - }, []) + }, []); const boxStyles = { border: theme.borders.base, borderRadius: 'lg', mb: 4, bg: 'white' - } + }; return ( + pl={`${pxVal}px`} + > {/* left sidebar */} + p={'4px'} + > {navList.map((item) => ( { - setActiveNav(item.id) - window.location.hash = item.id - }}> + setActiveNav(item.id); + window.location.hash = item.id; + }} + > + backgroundColor={activeNav === item.id ? 'grayModern.100' : 'transparent'} + > + overflowY={'scroll'} + > {/* base info */} - ) -} + ); +}; -export default Form +export default Form; 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 c95316d063e..5703c2586cd 100644 --- a/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/page.tsx +++ b/frontend/providers/devbox/app/[lang]/(platform)/devbox/create/page.tsx @@ -1,284 +1,284 @@ -'use client' +'use client'; -import { useRouter } from '@/i18n' -import { Box, Flex } from '@chakra-ui/react' -import { useMessage } from '@sealos/ui' -import { useQuery } from '@tanstack/react-query' -import { useTranslations } from 'next-intl' -import dynamic from 'next/dynamic' -import { useSearchParams } from 'next/navigation' -import { useCallback, useEffect, useMemo, useRef, useState } from 'react' -import { FormProvider, useForm } from 'react-hook-form' +import { useRouter } from '@/i18n'; +import { Box, Flex } from '@chakra-ui/react'; +import { useMessage } from '@sealos/ui'; +import { useQuery } from '@tanstack/react-query'; +import { useTranslations } from 'next-intl'; +import dynamic from 'next/dynamic'; +import { useSearchParams } from 'next/navigation'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { FormProvider, useForm } from 'react-hook-form'; -import Form from './components/form' -import Header from './components/Header' -import Yaml from './components/Yaml' +import Form from './components/form'; +import Header from './components/Header'; +import Yaml from './components/Yaml'; -import type { YamlItemType } from '@/types' -import type { DevboxEditType, DevboxEditTypeV2, DevboxKindsType } from '@/types/devbox' +import type { YamlItemType } from '@/types'; +import type { DevboxEditType, DevboxEditTypeV2, DevboxKindsType } from '@/types/devbox'; -import { useConfirm } from '@/hooks/useConfirm' -import { useLoading } from '@/hooks/useLoading' +import { useConfirm } from '@/hooks/useConfirm'; +import { useLoading } from '@/hooks/useLoading'; -import { useDevboxStore } from '@/stores/devbox' -import { useEnvStore } from '@/stores/env' -import { useGlobalStore } from '@/stores/global' -import { useIDEStore } from '@/stores/ide' -import { useUserStore } from '@/stores/user' -import { usePriceStore } from '@/stores/price' +import { useDevboxStore } from '@/stores/devbox'; +import { useEnvStore } from '@/stores/env'; +import { useGlobalStore } from '@/stores/global'; +import { useIDEStore } from '@/stores/ide'; +import { useUserStore } from '@/stores/user'; +import { usePriceStore } from '@/stores/price'; -import { createDevbox, updateDevbox } from '@/api/devbox' -import { defaultDevboxEditValueV2, editModeMap } from '@/constants/devbox' -import { useTemplateStore } from '@/stores/template' -import { generateYamlList } from '@/utils/json2Yaml' -import { patchYamlList } from '@/utils/tools' -import { debounce } from 'lodash' +import { createDevbox, updateDevbox } from '@/api/devbox'; +import { defaultDevboxEditValueV2, editModeMap } from '@/constants/devbox'; +import { useTemplateStore } from '@/stores/template'; +import { generateYamlList } from '@/utils/json2Yaml'; +import { patchYamlList } from '@/utils/tools'; +import { debounce } from 'lodash'; -const ErrorModal = dynamic(() => import('@/components/modals/ErrorModal')) +const ErrorModal = dynamic(() => import('@/components/modals/ErrorModal')); const DevboxCreatePage = () => { - const { env } = useEnvStore() - const generateDefaultYamlList = () => generateYamlList(defaultDevboxEditValueV2, env) - const router = useRouter() - const t = useTranslations() - const { Loading, setIsLoading } = useLoading() + const { env } = useEnvStore(); + const generateDefaultYamlList = () => generateYamlList(defaultDevboxEditValueV2, env); + const router = useRouter(); + const t = useTranslations(); + const { Loading, setIsLoading } = useLoading(); - const searchParams = useSearchParams() - const { message: toast } = useMessage() - const { addDevboxIDE } = useIDEStore() - const { sourcePrice, setSourcePrice } = usePriceStore() - const { checkQuotaAllow } = useUserStore() - const { setDevboxDetail, devboxList } = useDevboxStore() + const searchParams = useSearchParams(); + const { message: toast } = useMessage(); + const { addDevboxIDE } = useIDEStore(); + const { sourcePrice, setSourcePrice } = usePriceStore(); + const { checkQuotaAllow } = useUserStore(); + const { setDevboxDetail, devboxList } = useDevboxStore(); - const crOldYamls = useRef([]) - const formOldYamls = useRef([]) - const oldDevboxEditData = useRef() + const crOldYamls = useRef([]); + const formOldYamls = useRef([]); + const oldDevboxEditData = useRef(); - const [errorMessage, setErrorMessage] = useState('') - const [yamlList, setYamlList] = useState([]) + const [errorMessage, setErrorMessage] = useState(''); + const [yamlList, setYamlList] = useState([]); - const tabType = searchParams.get('type') || 'form' - const devboxName = searchParams.get('name') || '' + const tabType = searchParams.get('type') || 'form'; + const devboxName = searchParams.get('name') || ''; // NOTE: need to explain why this is needed // fix a bug: searchParams will disappear when go into this page - const [captureDevboxName, setCaptureDevboxName] = useState('') - const { updateTemplateModalConfig, config: templateConfig } = useTemplateStore() + const [captureDevboxName, setCaptureDevboxName] = useState(''); + const { updateTemplateModalConfig, config: templateConfig } = useTemplateStore(); useEffect(() => { - const name = searchParams.get('name') + const name = searchParams.get('name'); if (name) { - setCaptureDevboxName(name) - router.replace(`/devbox/create?name=${captureDevboxName}`, undefined) + setCaptureDevboxName(name); + router.replace(`/devbox/create?name=${captureDevboxName}`, undefined); } - }, [searchParams, router, captureDevboxName]) + }, [searchParams, router, captureDevboxName]); // eslint-disable-next-line react-hooks/exhaustive-deps - const isEdit = useMemo(() => !!devboxName, []) + const isEdit = useMemo(() => !!devboxName, []); - const { title, applyBtnText, applyMessage, applySuccess, applyError } = editModeMap(isEdit) + const { title, applyBtnText, applyMessage, applySuccess, applyError } = editModeMap(isEdit); const { openConfirm, ConfirmChild } = useConfirm({ content: applyMessage - }) + }); // compute container width - const { screenWidth, lastRoute } = useGlobalStore() + const { screenWidth, lastRoute } = useGlobalStore(); const pxVal = useMemo(() => { - const val = Math.floor((screenWidth - 1050) / 2) + const val = Math.floor((screenWidth - 1050) / 2); if (val < 20) { - return 20 + return 20; } - return val - }, [screenWidth]) + return val; + }, [screenWidth]); const formHook = useForm({ defaultValues: defaultDevboxEditValueV2 - }) + }); // updateyamlList every time yamlList change const debouncedUpdateYaml = useMemo( () => debounce((data: DevboxEditTypeV2, env) => { try { - const newYamlList = generateYamlList(data, env) - setYamlList(newYamlList) + const newYamlList = generateYamlList(data, env); + setYamlList(newYamlList); } catch (error) { - console.error('Failed to generate yaml:', error) + console.error('Failed to generate yaml:', error); } }, 300), [] - ) + ); const countGpuInventory = useCallback( (type?: string) => { - const inventory = sourcePrice?.gpu?.find((item) => item.type === type)?.inventory || 0 + const inventory = sourcePrice?.gpu?.find((item) => item.type === type)?.inventory || 0; - return inventory + return inventory; }, [sourcePrice?.gpu] - ) + ); // 监听表单变化 useEffect(() => { const subscription = formHook.watch((value) => { if (value) { - debouncedUpdateYaml(value as DevboxEditTypeV2, env) + debouncedUpdateYaml(value as DevboxEditTypeV2, env); } - }) + }); return () => { - subscription.unsubscribe() - debouncedUpdateYaml.cancel() - } - }, [formHook, debouncedUpdateYaml, env]) + subscription.unsubscribe(); + debouncedUpdateYaml.cancel(); + }; + }, [formHook, debouncedUpdateYaml, env]); const { refetch: refetchPrice } = useQuery(['init-price'], setSourcePrice, { enabled: !!sourcePrice?.gpu, refetchInterval: 6000 - }) + }); useQuery( ['initDevboxCreateData'], () => { if (!devboxName) { - setYamlList(generateDefaultYamlList()) - return null + setYamlList(generateDefaultYamlList()); + return null; } - setIsLoading(true) - return setDevboxDetail(devboxName, env.sealosDomain) + setIsLoading(true); + return setDevboxDetail(devboxName, env.sealosDomain); }, { onSuccess(res) { if (!res) { - return + return; } - oldDevboxEditData.current = res - formOldYamls.current = generateYamlList(res, env) - crOldYamls.current = generateYamlList(res, env) as DevboxKindsType[] - formHook.reset(res) + oldDevboxEditData.current = res; + formOldYamls.current = generateYamlList(res, env); + crOldYamls.current = generateYamlList(res, env) as DevboxKindsType[]; + formHook.reset(res); }, onError(err) { toast({ title: String(err), status: 'error' - }) + }); }, onSettled() { - setIsLoading(false) + setIsLoading(false); } } - ) + ); const submitSuccess = async (formData: DevboxEditTypeV2) => { - setIsLoading(true) + setIsLoading(true); try { // gpu inventory check if (formData.gpu?.type) { - const inventory = countGpuInventory(formData.gpu?.type) + const inventory = countGpuInventory(formData.gpu?.type); if (formData.gpu?.amount > inventory) { return toast({ status: 'warning', title: t('Gpu under inventory Tip', { gputype: formData.gpu.type }) - }) + }); } } // quote check const quoteCheckRes = checkQuotaAllow( { ...formData, nodeports: devboxList.length + 1 } as DevboxEditTypeV2 & { - nodeports: number + nodeports: number; }, { ...oldDevboxEditData.current, nodeports: devboxList.length } as DevboxEditType & { - nodeports: number + nodeports: number; } - ) + ); if (quoteCheckRes) { - setIsLoading(false) + setIsLoading(false); return toast({ status: 'warning', title: t(quoteCheckRes), duration: 5000, isClosable: true - }) + }); } // update if (isEdit) { - const yamlList = generateYamlList(formData, env) - setYamlList(yamlList) - const parsedNewYamlList = yamlList.map((item) => item.value) - const parsedOldYamlList = formOldYamls.current.map((item) => item.value) + const yamlList = generateYamlList(formData, env); + setYamlList(yamlList); + const parsedNewYamlList = yamlList.map((item) => item.value); + const parsedOldYamlList = formOldYamls.current.map((item) => item.value); const areYamlListsEqual = new Set(parsedNewYamlList).size === new Set(parsedOldYamlList).size && - [...new Set(parsedNewYamlList)].every((item) => new Set(parsedOldYamlList).has(item)) + [...new Set(parsedNewYamlList)].every((item) => new Set(parsedOldYamlList).has(item)); if (areYamlListsEqual) { - setIsLoading(false) + setIsLoading(false); return toast({ status: 'info', title: t('No changes detected'), duration: 5000, isClosable: true - }) + }); } if (!parsedNewYamlList) { // prevent empty yamlList - return setErrorMessage(t('submit_form_error')) + return setErrorMessage(t('submit_form_error')); } const patch = patchYamlList({ parsedOldYamlList: parsedOldYamlList, parsedNewYamlList: parsedNewYamlList, originalYamlList: crOldYamls.current - }) + }); await updateDevbox({ patch, devboxName: formData.name - }) + }); } else { - await createDevbox({ devboxForm: formData }) + await createDevbox({ devboxForm: formData }); } - addDevboxIDE('vscode', formData.name) - addDevboxIDE('vscode', formData.name) + addDevboxIDE('vscode', formData.name); + addDevboxIDE('vscode', formData.name); toast({ title: t(applySuccess), status: 'success' - }) + }); updateTemplateModalConfig({ ...templateConfig, lastRoute - }) + }); if (sourcePrice?.gpu) { - refetchPrice() + refetchPrice(); } - router.push(lastRoute) + router.push(lastRoute); } catch (error) { - console.log('error', error) + console.log('error', error); if (error instanceof String && error.includes('402')) { - setErrorMessage(t('outstanding_tips')) - } else setErrorMessage(JSON.stringify(error)) + setErrorMessage(t('outstanding_tips')); + } else setErrorMessage(JSON.stringify(error)); } - setIsLoading(false) - } + setIsLoading(false); + }; const submitError = useCallback(() => { // deep search message const deepSearch = (obj: any): string => { if (!obj || typeof obj !== 'object') { - return t('submit_form_error') + return t('submit_form_error'); } if (!!obj.message) { - return obj.message + return obj.message; } - return deepSearch(Object.values(obj)[0]) - } + return deepSearch(Object.values(obj)[0]); + }; toast({ title: deepSearch(formHook.formState.errors), status: 'error', position: 'top', duration: 3000, isClosable: true - }) - }, [formHook.formState.errors, toast, t]) + }); + }, [formHook.formState.errors, toast, t]); return ( <> @@ -288,7 +288,8 @@ const DevboxCreatePage = () => { alignItems={'center'} h={'100vh'} minWidth={'1024px'} - backgroundColor={'grayModern.100'}> + backgroundColor={'grayModern.100'} + >
{ setErrorMessage('')} /> )} - ) -} + ); +}; -export default DevboxCreatePage +export default DevboxCreatePage; 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 9ebc8d4f60a..7a8754e3fdc 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 @@ -1,60 +1,60 @@ -import { Box, Flex, Image, Spinner, Text, Tooltip } from '@chakra-ui/react' -import { useMessage } from '@sealos/ui' -import { useTranslations } from 'next-intl' -import { useCallback, useState } from 'react' +import { Box, Flex, Image, Spinner, Text, Tooltip } from '@chakra-ui/react'; +import { useMessage } from '@sealos/ui'; +import { useTranslations } from 'next-intl'; +import { useCallback, useState } from 'react'; -import MyIcon from '@/components/Icon' -import GPUItem from '@/components/GPUItem' -import { DevboxDetailType } from '@/types/devbox' +import MyIcon from '@/components/Icon'; +import GPUItem from '@/components/GPUItem'; +import { DevboxDetailType } from '@/types/devbox'; -import { useEnvStore } from '@/stores/env' -import { usePriceStore } from '@/stores/price' -import { useDevboxStore } from '@/stores/devbox' +import { useEnvStore } from '@/stores/env'; +import { usePriceStore } from '@/stores/price'; +import { useDevboxStore } from '@/stores/devbox'; const BasicInfo = () => { - const t = useTranslations() - const { message: toast } = useMessage() + const t = useTranslations(); + const { message: toast } = useMessage(); - const { env } = useEnvStore() - const { sourcePrice } = usePriceStore() - const { devboxDetail } = useDevboxStore() + const { env } = useEnvStore(); + const { sourcePrice } = usePriceStore(); + const { devboxDetail } = useDevboxStore(); // const { getRuntimeDetailLabel } = useRuntimeStore() - const [loading, setLoading] = useState(false) + const [loading, setLoading] = useState(false); const handleCopySSHCommand = useCallback(() => { - const sshCommand = `ssh -i yourPrivateKeyPath ${devboxDetail?.sshConfig?.sshUser}@${env.sealosDomain} -p ${devboxDetail?.sshPort}` + const sshCommand = `ssh -i yourPrivateKeyPath ${devboxDetail?.sshConfig?.sshUser}@${env.sealosDomain} -p ${devboxDetail?.sshPort}`; navigator.clipboard.writeText(sshCommand).then(() => { toast({ title: t('copy_success'), status: 'success', duration: 2000, isClosable: true - }) - }) - }, [devboxDetail?.sshConfig?.sshUser, devboxDetail?.sshPort, env.sealosDomain, toast, t]) + }); + }); + }, [devboxDetail?.sshConfig?.sshUser, devboxDetail?.sshPort, env.sealosDomain, toast, t]); const handleDownloadConfig = useCallback( async (config: DevboxDetailType['sshConfig']) => { - setLoading(true) + setLoading(true); - const privateKey = config?.sshPrivateKey as string + const privateKey = config?.sshPrivateKey as string; - const blob = new Blob([privateKey], { type: 'application/octet-stream' }) - const url = window.URL.createObjectURL(blob) - const a = document.createElement('a') - a.style.display = 'none' - a.href = url - a.download = devboxDetail?.name || '' - document.body.appendChild(a) - a.click() - window.URL.revokeObjectURL(url) - document.body.removeChild(a) + const blob = new Blob([privateKey], { type: 'application/octet-stream' }); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.style.display = 'none'; + a.href = url; + a.download = devboxDetail?.name || ''; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); - setLoading(false) + setLoading(false); }, [devboxDetail?.name] - ) + ); return ( @@ -77,7 +77,7 @@ const BasicInfo = () => { width={'20px'} height={'20px'} onError={(e) => { - e.currentTarget.src = '/images/custom.svg' + e.currentTarget.src = '/images/custom.svg'; }} alt={devboxDetail?.iconId} src={`/images/${devboxDetail?.iconId}.svg`} @@ -91,7 +91,8 @@ const BasicInfo = () => { {`${env.registryAddr}/${env.namespace}/${devboxDetail?.name}`} + w={'full'} + >{`${env.registryAddr}/${env.namespace}/${devboxDetail?.name}`} @@ -181,13 +182,15 @@ const BasicInfo = () => { fontSize={'12px'} fontWeight={400} py={2} - borderRadius={'md'}> + borderRadius={'md'} + > + w={'full'} + > {`ssh -i yourPrivateKeyPath ${devboxDetail?.sshConfig?.sshUser}@${env.sealosDomain} -p ${devboxDetail?.sshPort}`} @@ -209,13 +212,15 @@ const BasicInfo = () => { fontSize={'12px'} fontWeight={400} py={2} - borderRadius={'md'}> + borderRadius={'md'} + > + }} + > { fontSize={'12px'} fontWeight={400} py={2} - borderRadius={'md'}> + borderRadius={'md'} + > + }} + > { - ) -} + ); +}; -export default BasicInfo +export default BasicInfo; diff --git a/frontend/providers/devbox/app/[lang]/(platform)/devbox/detail/[name]/components/Header.tsx b/frontend/providers/devbox/app/[lang]/(platform)/devbox/detail/[name]/components/Header.tsx index ec53313842d..6c0979a93b0 100644 --- a/frontend/providers/devbox/app/[lang]/(platform)/devbox/detail/[name]/components/Header.tsx +++ b/frontend/providers/devbox/app/[lang]/(platform)/devbox/detail/[name]/components/Header.tsx @@ -1,113 +1,113 @@ -import { Box, Button, Flex } from '@chakra-ui/react' -import { useMessage } from '@sealos/ui' -import { useTranslations } from 'next-intl' -import { Dispatch, useCallback, useMemo, useState } from 'react' +import { Box, Button, Flex } from '@chakra-ui/react'; +import { useMessage } from '@sealos/ui'; +import { useTranslations } from 'next-intl'; +import { Dispatch, useCallback, useMemo, useState } from 'react'; -import { pauseDevbox, restartDevbox, startDevbox } from '@/api/devbox' -import { useRouter } from '@/i18n' -import { useDevboxStore } from '@/stores/devbox' -import { useGlobalStore } from '@/stores/global' +import { pauseDevbox, restartDevbox, startDevbox } from '@/api/devbox'; +import { useRouter } from '@/i18n'; +import { useDevboxStore } from '@/stores/devbox'; +import { useGlobalStore } from '@/stores/global'; -import { DevboxDetailTypeV2 } from '@/types/devbox' +import { DevboxDetailTypeV2 } from '@/types/devbox'; -import DevboxStatusTag from '@/components/DevboxStatusTag' -import MyIcon from '@/components/Icon' -import IDEButton from '@/components/IDEButton' -import DelModal from '@/components/modals/DelModal' -import { sealosApp } from 'sealos-desktop-sdk/app' -import { useQuery } from '@tanstack/react-query' +import DevboxStatusTag from '@/components/DevboxStatusTag'; +import MyIcon from '@/components/Icon'; +import IDEButton from '@/components/IDEButton'; +import DelModal from '@/components/modals/DelModal'; +import { sealosApp } from 'sealos-desktop-sdk/app'; +import { useQuery } from '@tanstack/react-query'; const Header = ({ refetchDevboxDetail, setShowSlider, isLargeScreen = true }: { - refetchDevboxDetail: () => void - setShowSlider: Dispatch - isLargeScreen: boolean + refetchDevboxDetail: () => void; + setShowSlider: Dispatch; + isLargeScreen: boolean; }) => { - const router = useRouter() - const t = useTranslations() - const { message: toast } = useMessage() + const router = useRouter(); + const t = useTranslations(); + const { message: toast } = useMessage(); - const { devboxDetail, setDevboxList } = useDevboxStore() - const { screenWidth, setLoading } = useGlobalStore() + const { devboxDetail, setDevboxList } = useDevboxStore(); + const { screenWidth, setLoading } = useGlobalStore(); - const [delDevbox, setDelDevbox] = useState(null) - const isBigButton = useMemo(() => screenWidth > 1000, [screenWidth]) + const [delDevbox, setDelDevbox] = useState(null); + const isBigButton = useMemo(() => screenWidth > 1000, [screenWidth]); const { refetch: refetchDevboxList } = useQuery(['devboxListQuery'], setDevboxList, { onSettled(res) { - if (!res) return + if (!res) return; } - }) + }); const handlePauseDevbox = useCallback( async (devbox: DevboxDetailTypeV2) => { try { - setLoading(true) - await pauseDevbox({ devboxName: devbox.name }) + setLoading(true); + await pauseDevbox({ devboxName: devbox.name }); toast({ title: t('pause_success'), status: 'success' - }) + }); } catch (error: any) { toast({ title: typeof error === 'string' ? error : error.message || t('pause_error'), status: 'error' - }) - console.error(error) + }); + console.error(error); } - refetchDevboxDetail() - setLoading(false) + refetchDevboxDetail(); + setLoading(false); }, [refetchDevboxDetail, setLoading, t, toast] - ) + ); const handleRestartDevbox = useCallback( async (devbox: DevboxDetailTypeV2) => { try { - setLoading(true) - await restartDevbox({ devboxName: devbox.name }) + setLoading(true); + await restartDevbox({ devboxName: devbox.name }); toast({ title: t('restart_success'), status: 'success' - }) + }); } catch (error: any) { toast({ title: typeof error === 'string' ? error : error.message || t('restart_error'), status: 'error' - }) - console.error(error, '==') + }); + console.error(error, '=='); } - refetchDevboxDetail() - setLoading(false) + refetchDevboxDetail(); + setLoading(false); }, [setLoading, t, toast, refetchDevboxDetail] - ) + ); const handleStartDevbox = useCallback( async (devbox: DevboxDetailTypeV2) => { try { - setLoading(true) - await startDevbox({ devboxName: devbox.name }) + setLoading(true); + await startDevbox({ devboxName: devbox.name }); toast({ title: t('start_success'), status: 'success' - }) + }); } catch (error: any) { toast({ title: typeof error === 'string' ? error : error.message || t('start_error'), status: 'error' - }) - console.error(error, '==') + }); + console.error(error, '=='); } - refetchDevboxDetail() - setLoading(false) + refetchDevboxDetail(); + setLoading(false); }, [setLoading, t, toast, refetchDevboxDetail] - ) + ); const handleGoToTerminal = useCallback( async (devbox: DevboxDetailTypeV2) => { - const defaultCommand = `kubectl exec -it $(kubectl get po -l app.kubernetes.io/name=${devbox.name} -oname) -- sh -c "clear; (bash || ash || sh)"` + const defaultCommand = `kubectl exec -it $(kubectl get po -l app.kubernetes.io/name=${devbox.name} -oname) -- sh -c "clear; (bash || ash || sh)"`; try { sealosApp.runEvents('openDesktopApp', { appKey: 'system-terminal', @@ -115,18 +115,18 @@ const Header = ({ defaultCommand }, messageData: { type: 'new terminal', command: defaultCommand } - }) + }); } catch (error: any) { toast({ title: typeof error === 'string' ? error : error.message || t('jump_terminal_error'), status: 'error' - }) - console.error(error) + }); + console.error(error); } }, [t, toast] - ) - if (!devboxDetail) return null + ); + if (!devboxDetail) return null; return ( {/* left back button and title */} @@ -144,7 +144,7 @@ const Header = ({ {/* detail button */} - + {!isLargeScreen && ( @@ -200,7 +201,8 @@ const Header = ({ }} borderWidth={1} leftIcon={isBigButton ? : undefined} - onClick={() => handleGoToTerminal(devboxDetail)}> + onClick={() => handleGoToTerminal(devboxDetail)} + > {isBigButton ? t('terminal') : } {devboxDetail.status.value === 'Running' && ( @@ -214,7 +216,8 @@ const Header = ({ }} borderWidth={1} leftIcon={isBigButton ? : undefined} - onClick={() => handlePauseDevbox(devboxDetail)}> + onClick={() => handlePauseDevbox(devboxDetail)} + > {isBigButton ? t('pause') : } )} @@ -229,7 +232,8 @@ const Header = ({ }} borderWidth={1} leftIcon={isBigButton ? : undefined} - onClick={() => handleStartDevbox(devboxDetail)}> + onClick={() => handleStartDevbox(devboxDetail)} + > {isBigButton ? t('start') : } )} @@ -243,7 +247,8 @@ const Header = ({ }} borderWidth={1} leftIcon={isBigButton ? : undefined} - onClick={() => router.push(`/devbox/create?name=${devboxDetail.name}`)}> + onClick={() => router.push(`/devbox/create?name=${devboxDetail.name}`)} + > {!isBigButton ? : t('update')} {devboxDetail.status.value !== 'Stopped' && ( @@ -257,7 +262,8 @@ const Header = ({ }} borderWidth={1} leftIcon={isBigButton ? : undefined} - onClick={() => handleRestartDevbox(devboxDetail)}> + onClick={() => handleRestartDevbox(devboxDetail)} + > {isBigButton ? t('restart') : } )} @@ -271,7 +277,8 @@ const Header = ({ }} borderWidth={1} leftIcon={isBigButton ? : undefined} - onClick={() => setDelDevbox(devboxDetail)}> + onClick={() => setDelDevbox(devboxDetail)} + > {isBigButton ? t('delete') : } @@ -280,14 +287,14 @@ const Header = ({ devbox={delDevbox} onClose={() => setDelDevbox(null)} onSuccess={() => { - setDelDevbox(null) - router.push('/') + setDelDevbox(null); + router.push('/'); }} refetchDevboxList={refetchDevboxList} /> )} - ) -} + ); +}; -export default Header +export default Header; diff --git a/frontend/providers/devbox/app/[lang]/(platform)/devbox/detail/[name]/components/MainBody.tsx b/frontend/providers/devbox/app/[lang]/(platform)/devbox/detail/[name]/components/MainBody.tsx index 9a2b2bd1002..a3dfd72b903 100644 --- a/frontend/providers/devbox/app/[lang]/(platform)/devbox/detail/[name]/components/MainBody.tsx +++ b/frontend/providers/devbox/app/[lang]/(platform)/devbox/detail/[name]/components/MainBody.tsx @@ -1,33 +1,33 @@ -import { Box, Button, Flex, Text, Tooltip, useDisclosure } from '@chakra-ui/react' -import dayjs from 'dayjs' -import { useTranslations } from 'next-intl' -import dynamic from 'next/dynamic' +import { Box, Button, Flex, Text, Tooltip, useDisclosure } from '@chakra-ui/react'; +import dayjs from 'dayjs'; +import { useTranslations } from 'next-intl'; +import dynamic from 'next/dynamic'; -import MyIcon from '@/components/Icon' -import MyTable from '@/components/MyTable' -import PodLineChart from '@/components/PodLineChart' +import MyIcon from '@/components/Icon'; +import MyTable from '@/components/MyTable'; +import PodLineChart from '@/components/PodLineChart'; -import { NetworkType } from '@/types/devbox' -import { useCopyData } from '@/utils/tools' +import { NetworkType } from '@/types/devbox'; +import { useCopyData } from '@/utils/tools'; -import { useDevboxStore } from '@/stores/devbox' -import { useEnvStore } from '@/stores/env' +import { useDevboxStore } from '@/stores/devbox'; +import { useEnvStore } from '@/stores/env'; -const MonitorModal = dynamic(() => import('@/components/modals/MonitorModal')) +const MonitorModal = dynamic(() => import('@/components/modals/MonitorModal')); const MainBody = () => { - const t = useTranslations() - const { copyData } = useCopyData() - const { devboxDetail } = useDevboxStore() - const { env } = useEnvStore() - const { isOpen, onOpen, onClose } = useDisclosure() + const t = useTranslations(); + const { copyData } = useCopyData(); + const { devboxDetail } = useDevboxStore(); + const { env } = useEnvStore(); + const { isOpen, onOpen, onClose } = useDisclosure(); const networkColumn: { - title: string - dataIndex?: keyof NetworkType - key: string - render?: (item: NetworkType) => JSX.Element - width?: string + title: string; + dataIndex?: keyof NetworkType; + key: string; + render?: (item: NetworkType) => JSX.Element; + width?: string; }[] = [ { title: t('port'), @@ -37,7 +37,7 @@ const MainBody = () => { {item.port} - ) + ); }, width: '0.5fr' }, @@ -54,7 +54,8 @@ const MainBody = () => { fontSize={'12px'} fontWeight={400} py={2} - borderRadius={'md'}> + borderRadius={'md'} + > { copyData( `http://${devboxDetail?.name}.${env.namespace}.svc.cluster.local:${item.port}` ) - }>{`http://${devboxDetail?.name}.${env.namespace}.svc.cluster.local:${item.port}`} + } + >{`http://${devboxDetail?.name}.${env.namespace}.svc.cluster.local:${item.port}`} - ) + ); } }, { @@ -75,7 +77,7 @@ const MainBody = () => { key: 'externalAddress', render: (item: NetworkType) => { if (item.openPublicDomain) { - const address = item.customDomain || item.publicDomain + const address = item.customDomain || item.publicDomain; return ( { fontSize={'12px'} fontWeight={400} py={2} - borderRadius={'md'}> + borderRadius={'md'} + > window.open(`https://${address}`, '_blank')}> + onClick={() => window.open(`https://${address}`, '_blank')} + > https://{address} - ) + ); } - return - + return -; } } - ] + ]; return ( {/* monitor */} @@ -131,7 +135,8 @@ const MainBody = () => { position={'absolute'} right={'2px'} top={'-6px'} - onClick={onOpen}> + onClick={onOpen} + > @@ -168,7 +173,7 @@ const MainBody = () => { - ) -} + ); +}; -export default MainBody +export default MainBody; 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 cc9490ab797..53bb4a93ada 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 @@ -1,57 +1,57 @@ -import { Box, Button, Flex, MenuButton, Text, useDisclosure } from '@chakra-ui/react' -import { SealosMenu, useMessage } from '@sealos/ui' -import { useQuery } from '@tanstack/react-query' -import { useTranslations } from 'next-intl' -import { useCallback, useState } from 'react' -import { sealosApp } from 'sealos-desktop-sdk/app' +import { Box, Button, Flex, MenuButton, Text, useDisclosure } from '@chakra-ui/react'; +import { SealosMenu, useMessage } from '@sealos/ui'; +import { useQuery } from '@tanstack/react-query'; +import { useTranslations } from 'next-intl'; +import { useCallback, useState } from 'react'; +import { sealosApp } from 'sealos-desktop-sdk/app'; -import { delDevboxVersionByName, getAppsByDevboxId } from '@/api/devbox' -import DevboxStatusTag from '@/components/DevboxStatusTag' -import MyIcon from '@/components/Icon' -import EditVersionDesModal from '@/components/modals/EditVersionDesModal' -import ReleaseModal from '@/components/modals/releaseModal' -import MyTable from '@/components/MyTable' -import { DevboxReleaseStatusEnum, devboxIdKey } from '@/constants/devbox' -import { DevboxVersionListItemType } from '@/types/devbox' +import { delDevboxVersionByName, getAppsByDevboxId } from '@/api/devbox'; +import DevboxStatusTag from '@/components/DevboxStatusTag'; +import MyIcon from '@/components/Icon'; +import EditVersionDesModal from '@/components/modals/EditVersionDesModal'; +import ReleaseModal from '@/components/modals/releaseModal'; +import MyTable from '@/components/MyTable'; +import { DevboxReleaseStatusEnum, devboxIdKey } from '@/constants/devbox'; +import { DevboxVersionListItemType } from '@/types/devbox'; -import { useConfirm } from '@/hooks/useConfirm' -import { useLoading } from '@/hooks/useLoading' +import { useConfirm } from '@/hooks/useConfirm'; +import { useLoading } from '@/hooks/useLoading'; -import { getTemplateConfig, listPrivateTemplateRepository } from '@/api/template' -import CreateTemplateModal from '@/app/[lang]/(platform)/template/updateTemplate/CreateTemplateModal' -import SelectTemplateModal from '@/app/[lang]/(platform)/template/updateTemplate/SelectActionModal' -import UpdateTemplateRepositoryModal from '@/app/[lang]/(platform)/template/updateTemplate/UpdateTemplateRepositoryModal' -import AppSelectModal from '@/components/modals/AppSelectModal' -import { useDevboxStore } from '@/stores/devbox' -import { useEnvStore } from '@/stores/env' -import { AppListItemType } from '@/types/app' -import { parseTemplateConfig } from '@/utils/tools' +import { getTemplateConfig, listPrivateTemplateRepository } from '@/api/template'; +import CreateTemplateModal from '@/app/[lang]/(platform)/template/updateTemplate/CreateTemplateModal'; +import SelectTemplateModal from '@/app/[lang]/(platform)/template/updateTemplate/SelectActionModal'; +import UpdateTemplateRepositoryModal from '@/app/[lang]/(platform)/template/updateTemplate/UpdateTemplateRepositoryModal'; +import AppSelectModal from '@/components/modals/AppSelectModal'; +import { useDevboxStore } from '@/stores/devbox'; +import { useEnvStore } from '@/stores/env'; +import { AppListItemType } from '@/types/app'; +import { parseTemplateConfig } from '@/utils/tools'; const Version = () => { - const t = useTranslations() - const { message: toast } = useMessage() - const { Loading, setIsLoading } = useLoading() - const { isOpen: isOpenEdit, onOpen: onOpenEdit, onClose: onCloseEdit } = useDisclosure() + const t = useTranslations(); + const { message: toast } = useMessage(); + const { Loading, setIsLoading } = useLoading(); + const { isOpen: isOpenEdit, onOpen: onOpenEdit, onClose: onCloseEdit } = useDisclosure(); - const { env } = useEnvStore() - const { devboxDetail: devbox, devboxVersionList, setDevboxVersionList } = useDevboxStore() + const { env } = useEnvStore(); + const { devboxDetail: devbox, devboxVersionList, setDevboxVersionList } = useDevboxStore(); - const [initialized, setInitialized] = useState(false) - const [onOpenRelease, setOnOpenRelease] = useState(false) - const [onOpenSelectApp, setOnOpenSelectApp] = useState(false) - const [apps, setApps] = useState([]) - const [deployData, setDeployData] = useState(null) - const [currentVersion, setCurrentVersion] = useState(null) + const [initialized, setInitialized] = useState(false); + const [onOpenRelease, setOnOpenRelease] = useState(false); + const [onOpenSelectApp, setOnOpenSelectApp] = useState(false); + const [apps, setApps] = useState([]); + const [deployData, setDeployData] = useState(null); + const [currentVersion, setCurrentVersion] = useState(null); const [updateTemplateRepo, setUpdateTemplateRepo] = useState< | null | Awaited>['templateRepositoryList'][number] - >(null) - const createTemplateModalHandler = useDisclosure() - const selectTemplalteModalHandler = useDisclosure() - const updateTemplateModalHandler = useDisclosure() + >(null); + const createTemplateModalHandler = useDisclosure(); + const selectTemplalteModalHandler = useDisclosure(); + const updateTemplateModalHandler = useDisclosure(); const { openConfirm, ConfirmChild } = useConfirm({ content: 'delete_version_confirm_info' - }) + }); const { refetch } = useQuery( ['initDevboxVersionList'], () => setDevboxVersionList(devbox!.name, devbox!.id), @@ -65,40 +65,40 @@ const Version = () => { ? 3000 : false, onSettled() { - setInitialized(true) + setInitialized(true); }, enabled: !!devbox } - ) + ); const listPrivateTemplateRepositoryQuery = useQuery( ['template-repository-list', 'template-repository-private'], () => { return listPrivateTemplateRepository({ page: 1, pageSize: 100 - }) + }); } - ) + ); const templateRepositoryList = - listPrivateTemplateRepositoryQuery.data?.templateRepositoryList || [] + listPrivateTemplateRepositoryQuery.data?.templateRepositoryList || []; const handleDeploy = useCallback( async (version: DevboxVersionListItemType) => { // const { releaseCommand, releaseArgs } = await getSSHRuntimeInfo(devbox.runtimeVersion) - if (!devbox) return - const result = await getTemplateConfig(devbox.templateUid) - const config = parseTemplateConfig(result.template.config) - const releaseArgs = config.releaseArgs.join(' ') - const releaseCommand = config.releaseCommand.join(' ') - const { cpu, memory, networks, name } = devbox + if (!devbox) return; + const result = await getTemplateConfig(devbox.templateUid); + const config = parseTemplateConfig(result.template.config); + const releaseArgs = config.releaseArgs.join(' '); + const releaseCommand = config.releaseCommand.join(' '); + const { cpu, memory, networks, name } = devbox; const newNetworks = networks.map((network) => { return { port: network.port, protocol: network.protocol, openPublicDomain: network.openPublicDomain, domain: env.ingressDomain - } - }) - const imageName = `${env.registryAddr}/${env.namespace}/${devbox.name}:${version.tag}` + }; + }); + const imageName = `${env.registryAddr}/${env.namespace}/${devbox.name}:${version.tag}`; const transformData = { appName: `${name}-release`, @@ -121,13 +121,13 @@ const Version = () => { labels: { [devboxIdKey]: devbox.id } - } - setDeployData(transformData) - const apps = await getAppsByDevboxId(devbox.id) + }; + setDeployData(transformData); + const apps = await getAppsByDevboxId(devbox.id); // when: there is no app,create a new app if (apps.length === 0) { - const tempFormDataStr = encodeURIComponent(JSON.stringify(transformData)) + const tempFormDataStr = encodeURIComponent(JSON.stringify(transformData)); sealosApp.runEvents('openDesktopApp', { appKey: 'system-applaunchpad', pathname: '/redirect', @@ -136,55 +136,55 @@ const Version = () => { type: 'InternalAppCall', formData: tempFormDataStr } - }) + }); } // when: there have apps,show the app select modal if (apps.length >= 1) { - setApps(apps) - setOnOpenSelectApp(true) + setApps(apps); + setOnOpenSelectApp(true); } }, [devbox, env.ingressDomain, env.namespace, env.registryAddr] - ) + ); const handleDelDevboxVersion = useCallback( async (versionName: string) => { try { - setIsLoading(true) - await delDevboxVersionByName(versionName) + setIsLoading(true); + await delDevboxVersionByName(versionName); toast({ title: t('delete_successful'), status: 'success' - }) - let retryCount = 0 - const maxRetries = 3 - const retryInterval = 3000 + }); + let retryCount = 0; + const maxRetries = 3; + const retryInterval = 3000; const retry = async () => { if (retryCount < maxRetries) { - await new Promise((resolve) => setTimeout(resolve, retryInterval)) - await refetch() - retryCount++ + await new Promise((resolve) => setTimeout(resolve, retryInterval)); + await refetch(); + retryCount++; } - } - retry() + }; + retry(); } catch (error: any) { toast({ title: typeof error === 'string' ? error : error.message || t('delete_failed'), status: 'error' - }) - console.error(error) + }); + console.error(error); } - setIsLoading(false) + setIsLoading(false); }, [setIsLoading, toast, t, refetch] - ) + ); const columns: { - title: string - dataIndex?: keyof DevboxVersionListItemType - key: string - render?: (item: DevboxVersionListItemType) => JSX.Element + title: string; + dataIndex?: keyof DevboxVersionListItemType; + key: string; + render?: (item: DevboxVersionListItemType) => JSX.Element; }[] = [ { title: t('version_number'), @@ -207,7 +207,7 @@ const Version = () => { dataIndex: 'createTime', key: 'createTime', render: (item: DevboxVersionListItemType) => { - return {item.createTime} + return {item.createTime}; } }, { @@ -239,7 +239,8 @@ const Version = () => { color: 'brightBlue.600' }} isDisabled={item.status.value !== DevboxReleaseStatusEnum.Success} - onClick={() => handleDeploy(item)}> + onClick={() => handleDeploy(item)} + > {t('deploy')} { ), onClick: () => { - setCurrentVersion(item) - onOpenEdit() + setCurrentVersion(item); + onOpenEdit(); } }, { @@ -277,13 +278,13 @@ const Version = () => { ), onClick: () => { - setCurrentVersion(item) + setCurrentVersion(item); // onOpenEdit() // openTemplateModal({templateState: }) if (templateRepositoryList.length > 0) { - selectTemplalteModalHandler.onOpen() + selectTemplalteModalHandler.onOpen(); } else { - createTemplateModalHandler.onOpen() + createTemplateModalHandler.onOpen(); } } }, @@ -307,7 +308,7 @@ const Version = () => { ) } - ] + ]; return ( { pr={6} bg={'white'} h={'full'} - position={'relative'}> + position={'relative'} + > @@ -334,7 +336,8 @@ const Version = () => { leftIcon={} _hover={{ color: 'brightBlue.600' - }}> + }} + > {t('release_version')} @@ -345,7 +348,8 @@ const Version = () => { alignItems={'center'} mt={10} flexDirection={'column'} - gap={4}> + gap={4} + > {t('no_versions')} @@ -371,7 +375,7 @@ const Version = () => { { - setOnOpenRelease(false) + setOnOpenRelease(false); }} devbox={{ ...devbox, sshPort: devbox.sshPort || 0 }} /> @@ -395,9 +399,9 @@ const Version = () => { { - const repo = templateRepositoryList.find((item) => item.uid === uid) - setUpdateTemplateRepo(repo || null) - updateTemplateModalHandler.onOpen() + const repo = templateRepositoryList.find((item) => item.uid === uid); + setUpdateTemplateRepo(repo || null); + updateTemplateModalHandler.onOpen(); }} templateRepositoryList={templateRepositoryList} isOpen={selectTemplalteModalHandler.isOpen} @@ -413,7 +417,7 @@ const Version = () => { /> )} - ) -} + ); +}; -export default Version +export default Version; diff --git a/frontend/providers/devbox/app/[lang]/(platform)/devbox/detail/[name]/page.tsx b/frontend/providers/devbox/app/[lang]/(platform)/devbox/detail/[name]/page.tsx index 7f64bf8a4bd..ed090260da6 100644 --- a/frontend/providers/devbox/app/[lang]/(platform)/devbox/detail/[name]/page.tsx +++ b/frontend/providers/devbox/app/[lang]/(platform)/devbox/detail/[name]/page.tsx @@ -1,69 +1,69 @@ -'use client' +'use client'; -import { Box, Flex } from '@chakra-ui/react' -import { useQuery } from '@tanstack/react-query' -import { useMemo, useState } from 'react' +import { Box, Flex } from '@chakra-ui/react'; +import { useQuery } from '@tanstack/react-query'; +import { useMemo, useState } from 'react'; -import { useLoading } from '@/hooks/useLoading' -import BasicInfo from './components/BasicInfo' -import Header from './components/Header' -import MainBody from './components/MainBody' -import Version from './components/Version' +import { useLoading } from '@/hooks/useLoading'; +import BasicInfo from './components/BasicInfo'; +import Header from './components/Header'; +import MainBody from './components/MainBody'; +import Version from './components/Version'; -import { useDevboxStore } from '@/stores/devbox' -import { useEnvStore } from '@/stores/env' -import { useGlobalStore } from '@/stores/global' +import { useDevboxStore } from '@/stores/devbox'; +import { useEnvStore } from '@/stores/env'; +import { useGlobalStore } from '@/stores/global'; const DevboxDetailPage = ({ params }: { params: { name: string } }) => { - const devboxName = params.name - const { Loading } = useLoading() + const devboxName = params.name; + const { Loading } = useLoading(); - const { env } = useEnvStore() - const { screenWidth } = useGlobalStore() + const { env } = useEnvStore(); + const { screenWidth } = useGlobalStore(); const { devboxDetail, setDevboxDetail, loadDetailMonitorData, intervalLoadPods } = - useDevboxStore() + useDevboxStore(); - const [showSlider, setShowSlider] = useState(false) - const [initialized, setInitialized] = useState(false) - const isLargeScreen = useMemo(() => screenWidth > 1280, [screenWidth]) + const [showSlider, setShowSlider] = useState(false); + const [initialized, setInitialized] = useState(false); + const isLargeScreen = useMemo(() => screenWidth > 1280, [screenWidth]); const { refetch, data } = useQuery( - ['initDevboxDetail',], + ['initDevboxDetail'], () => setDevboxDetail(devboxName, env.sealosDomain), { onSettled() { - setInitialized(true) + setInitialized(true); } } - ) + ); useQuery( ['devbox-detail-pod'], () => { - if (devboxDetail?.isPause) return null - return intervalLoadPods(devboxName, true) + if (devboxDetail?.isPause) return null; + return intervalLoadPods(devboxName, true); }, { enabled: !devboxDetail?.isPause, refetchOnMount: true, refetchInterval: 3000 } - ) + ); useQuery( ['loadDetailMonitorData', devboxName, devboxDetail?.isPause], () => { - if (devboxDetail?.isPause) return null - return loadDetailMonitorData(devboxName) + if (devboxDetail?.isPause) return null; + return loadDetailMonitorData(devboxName); }, { refetchOnMount: true, - refetchInterval: 2 * 60 * 1000, + refetchInterval: 2 * 60 * 1000 } - ) + ); return ( - - - {devboxDetail && initialized && ( + + + {devboxDetail && initialized && ( <>
{ {...(isLargeScreen ? {} : { - position: 'absolute', - left: 0, - boxShadow: '7px 4px 12px rgba(165, 172, 185, 0.25)', - transform: `translateX(${showSlider ? '0' : '-500'}px)` - })}> + position: 'absolute', + left: 0, + boxShadow: '7px 4px 12px rgba(165, 172, 185, 0.25)', + transform: `translateX(${showSlider ? '0' : '-500'}px)` + })} + > { }, msOverflowStyle: 'none', // IE and Edge scrollbarWidth: 'none' // Firefox - }}> + }} + > @@ -127,9 +129,9 @@ const DevboxDetailPage = ({ params }: { params: { name: string } }) => { /> )} - )} - - ) -} + )} + + ); +}; -export default DevboxDetailPage +export default DevboxDetailPage; diff --git a/frontend/providers/devbox/app/[lang]/(platform)/layout.tsx b/frontend/providers/devbox/app/[lang]/(platform)/layout.tsx index cbac0a74a99..2fe412c08db 100644 --- a/frontend/providers/devbox/app/[lang]/(platform)/layout.tsx +++ b/frontend/providers/devbox/app/[lang]/(platform)/layout.tsx @@ -1,147 +1,146 @@ -'use client' +'use client'; -import { usePathname, useRouter } from '@/i18n' -import throttle from 'lodash/throttle' -import { useSearchParams } from 'next/navigation' -import { useEffect, useState } from 'react' -import { EVENT_NAME } from 'sealos-desktop-sdk' -import { createSealosApp, sealosApp } from 'sealos-desktop-sdk/app' +import { usePathname, useRouter } from '@/i18n'; +import throttle from 'lodash/throttle'; +import { useSearchParams } from 'next/navigation'; +import { useEffect, useState } from 'react'; +import { EVENT_NAME } from 'sealos-desktop-sdk'; +import { createSealosApp, sealosApp } from 'sealos-desktop-sdk/app'; // import { useConfirm } from '@/hooks/useConfirm' -import { useLoading } from '@/hooks/useLoading' +import { useLoading } from '@/hooks/useLoading'; -import { useEnvStore } from '@/stores/env' -import { useGlobalStore } from '@/stores/global' -import { usePriceStore } from '@/stores/price' +import { useEnvStore } from '@/stores/env'; +import { useGlobalStore } from '@/stores/global'; +import { usePriceStore } from '@/stores/price'; -import { initUser } from '@/api/template' -import ChakraProvider from '@/components/providers/MyChakraProvider' -import RouteHandlerProvider from '@/components/providers/MyRouteHandlerProvider' -import { useConfirm } from '@/hooks/useConfirm' -import { getLangStore, setLangStore } from '@/utils/cookie' -import { cleanSession, setSessionToSessionStorage } from '@/utils/user' -import { useQueryClient } from '@tanstack/react-query' -import TemplateModal from './template/TemplateModal' +import { initUser } from '@/api/template'; +import ChakraProvider from '@/components/providers/MyChakraProvider'; +import RouteHandlerProvider from '@/components/providers/MyRouteHandlerProvider'; +import { useConfirm } from '@/hooks/useConfirm'; +import { getLangStore, setLangStore } from '@/utils/cookie'; +import { cleanSession, setSessionToSessionStorage } from '@/utils/user'; +import { useQueryClient } from '@tanstack/react-query'; +import TemplateModal from './template/TemplateModal'; export default function PlatformLayout({ children }: { children: React.ReactNode }) { - const router = useRouter() - const pathname = usePathname() - const { Loading } = useLoading() - const { setEnv, env } = useEnvStore() - const searchParams = useSearchParams() - const { setSourcePrice } = usePriceStore() - const [refresh, setRefresh] = useState(false) - const { setScreenWidth, loading, setLastRoute } = useGlobalStore() + const router = useRouter(); + const pathname = usePathname(); + const { Loading } = useLoading(); + const { setEnv, env } = useEnvStore(); + const searchParams = useSearchParams(); + const { setSourcePrice } = usePriceStore(); + const [refresh, setRefresh] = useState(false); + const { setScreenWidth, loading, setLastRoute } = useGlobalStore(); const { openConfirm, ConfirmChild } = useConfirm({ title: 'jump_prompt', content: 'not_allow_standalone_use' - }) - const queryClient = useQueryClient() - const [init, setInit] = useState(false) + }); + const queryClient = useQueryClient(); + const [init, setInit] = useState(false); // init session useEffect(() => { - const response = createSealosApp() - ; (async () => { - try { - - const newSession = JSON.stringify(await sealosApp.getSession()) - const oldSession = sessionStorage.getItem('session') - if(newSession && newSession !== oldSession) { - sessionStorage.setItem('session', newSession) - return window.location.reload() - } - // init user - console.log('devbox: app init success') - const token = (await initUser()) - if (!!token) { - setSessionToSessionStorage(token) - setInit(true) - } - queryClient.clear() - } catch (err) { - console.log('devbox: app is not running in desktop') - if (!process.env.NEXT_PUBLIC_MOCK_USER) { - cleanSession() - openConfirm(() => { - window.open(`https://${env.sealosDomain}`, '_self') - })() - } + const response = createSealosApp(); + (async () => { + try { + const newSession = JSON.stringify(await sealosApp.getSession()); + const oldSession = sessionStorage.getItem('session'); + if (newSession && newSession !== oldSession) { + sessionStorage.setItem('session', newSession); + return window.location.reload(); } - })() - return response + // init user + console.log('devbox: app init success'); + const token = await initUser(); + if (!!token) { + setSessionToSessionStorage(token); + setInit(true); + } + queryClient.clear(); + } catch (err) { + console.log('devbox: app is not running in desktop'); + if (!process.env.NEXT_PUBLIC_MOCK_USER) { + cleanSession(); + openConfirm(() => { + window.open(`https://${env.sealosDomain}`, '_self'); + })(); + } + } + })(); + return response; // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) + }, []); useEffect(() => { - if (!init) return - setSourcePrice() + if (!init) return; + setSourcePrice(); // setRuntime() - setEnv() + setEnv(); const changeI18n = async (data: any) => { - const lastLang = getLangStore() - const newLang = data.currentLanguage + const lastLang = getLangStore(); + const newLang = data.currentLanguage; if (lastLang !== newLang) { - router.push(pathname, { locale: newLang }) - setLangStore(newLang) - setRefresh((state) => !state) + router.push(pathname, { locale: newLang }); + setLangStore(newLang); + setRefresh((state) => !state); } - } - - ; (async () => { - try { - const lang = await sealosApp.getLanguage() - changeI18n({ - currentLanguage: lang.lng - }) - } catch (error) { - changeI18n({ - currentLanguage: 'zh' - }) - } - })() + }; + + (async () => { + try { + const lang = await sealosApp.getLanguage(); + changeI18n({ + currentLanguage: lang.lng + }); + } catch (error) { + changeI18n({ + currentLanguage: 'zh' + }); + } + })(); - return sealosApp?.addAppEventListen(EVENT_NAME.CHANGE_I18N, changeI18n) + return sealosApp?.addAppEventListen(EVENT_NAME.CHANGE_I18N, changeI18n); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [init]) + }, [init]); // add resize event useEffect(() => { const resize = throttle((e: Event) => { - const documentWidth = document.documentElement.clientWidth || document.body.clientWidth - setScreenWidth(documentWidth) - }, 200) - window.addEventListener('resize', resize) - const documentWidth = document.documentElement.clientWidth || document.body.clientWidth - setScreenWidth(documentWidth) + const documentWidth = document.documentElement.clientWidth || document.body.clientWidth; + setScreenWidth(documentWidth); + }, 200); + window.addEventListener('resize', resize); + const documentWidth = document.documentElement.clientWidth || document.body.clientWidth; + setScreenWidth(documentWidth); return () => { - window.removeEventListener('resize', resize) - } - }, [setScreenWidth]) + window.removeEventListener('resize', resize); + }; + }, [setScreenWidth]); // record route useEffect(() => { return () => { - setLastRoute(pathname) - } + setLastRoute(pathname); + }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [pathname]) + }, [pathname]); useEffect(() => { - const lang = getLangStore() || 'zh' - router.push(pathname, { locale: lang }) + const lang = getLangStore() || 'zh'; + router.push(pathname, { locale: lang }); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [refresh, pathname]) + }, [refresh, pathname]); useEffect(() => { - const page = searchParams.get('page') - const runtime = searchParams.get('runtime') + const page = searchParams.get('page'); + const runtime = searchParams.get('runtime'); - const path = `${page ? `/devbox/${page}` : ''}${runtime ? `?runtime=${runtime}` : ''}` + const path = `${page ? `/devbox/${page}` : ''}${runtime ? `?runtime=${runtime}` : ''}`; - router.push(path) - }, [router, searchParams]) + router.push(path); + }, [router, searchParams]); return ( @@ -152,5 +151,5 @@ export default function PlatformLayout({ children }: { children: React.ReactNode - ) + ); } diff --git a/frontend/providers/devbox/app/[lang]/(platform)/template/TagCheckbox.tsx b/frontend/providers/devbox/app/[lang]/(platform)/template/TagCheckbox.tsx index b9e182097b9..fe22f46f08a 100644 --- a/frontend/providers/devbox/app/[lang]/(platform)/template/TagCheckbox.tsx +++ b/frontend/providers/devbox/app/[lang]/(platform)/template/TagCheckbox.tsx @@ -2,16 +2,10 @@ import { CheckIcon } from '@chakra-ui/icons'; import { Box, chakra, CheckboxProps, useCheckbox } from '@chakra-ui/react'; export const TagCheckbox = (props: CheckboxProps) => { - const { state, getCheckboxProps, getInputProps, getLabelProps, htmlProps } = - useCheckbox(props); + const { state, getCheckboxProps, getInputProps, getLabelProps, htmlProps } = useCheckbox(props); return ( - + { borderRadius="4px" border="1px solid" cursor={'pointer'} - { - ...state.isChecked ? { - bg: "blue.50", - borderColor: "brightBlue.500", - boxShadow: "0px 0px 0px 2.4px rgba(33, 155, 244, 0.15)" - }: { - borderColor: "grayModern.300", - } - } + {...(state.isChecked + ? { + bg: 'blue.50', + borderColor: 'brightBlue.500', + boxShadow: '0px 0px 0px 2.4px rgba(33, 155, 244, 0.15)' + } + : { + borderColor: 'grayModern.300' + })} transition="all 0.2s" > - {state.isChecked && ( - - )} + {state.isChecked && } {props.children} diff --git a/frontend/providers/devbox/app/[lang]/(platform)/template/TemplateModal/PrivatePanel.tsx b/frontend/providers/devbox/app/[lang]/(platform)/template/TemplateModal/PrivatePanel.tsx index f5aacfce782..2952f548d01 100644 --- a/frontend/providers/devbox/app/[lang]/(platform)/template/TemplateModal/PrivatePanel.tsx +++ b/frontend/providers/devbox/app/[lang]/(platform)/template/TemplateModal/PrivatePanel.tsx @@ -1,117 +1,129 @@ -import { listPrivateTemplateRepository } from "@/api/template"; -import MyIcon from "@/components/Icon"; -import SwitchPage from "@/components/SwitchPage"; -import { Box, Flex, Grid, TabPanel, Text } from "@chakra-ui/react"; -import { useQuery } from "@tanstack/react-query"; -import { useTranslations } from "next-intl"; -import { useEffect, useState } from "react"; -import TemplateCard from "./TemplateCard"; +import { listPrivateTemplateRepository } from '@/api/template'; +import MyIcon from '@/components/Icon'; +import SwitchPage from '@/components/SwitchPage'; +import { Box, Flex, Grid, TabPanel, Text } from '@chakra-ui/react'; +import { useQuery } from '@tanstack/react-query'; +import { useTranslations } from 'next-intl'; +import { useEffect, useState } from 'react'; +import TemplateCard from './TemplateCard'; -export default function PrivatePanel({ - search, }: { - search: string, - }) { +export default function PrivatePanel({ search }: { search: string }) { const [pageQueryBody, setPageQueryBody] = useState({ page: 1, pageSize: 30, totalItems: 0, - totalPage: 0, - }) + totalPage: 0 + }); // reset query useEffect(() => { - if (!search) return - setPageQueryBody(prev => ({ + if (!search) return; + setPageQueryBody((prev) => ({ ...prev, page: 1, totalItems: 0, - totalPage: 0, - })) - }, [search]) + totalPage: 0 + })); + }, [search]); // reset query const queryBody = { page: pageQueryBody.page, pageSize: pageQueryBody.pageSize, - search, - } + search + }; const listPrivateTemplateReposistory = useQuery( ['template-repository-list', 'template-repository-private', queryBody], () => { - return listPrivateTemplateRepository(queryBody) + return listPrivateTemplateRepository(queryBody); } - ) + ); useEffect(() => { - if (listPrivateTemplateReposistory.isFetched && listPrivateTemplateReposistory.isSuccess && listPrivateTemplateReposistory.data) { - const data = listPrivateTemplateReposistory.data.page - setPageQueryBody(prev => ({ + if ( + listPrivateTemplateReposistory.isFetched && + listPrivateTemplateReposistory.isSuccess && + listPrivateTemplateReposistory.data + ) { + const data = listPrivateTemplateReposistory.data.page; + setPageQueryBody((prev) => ({ ...prev, totalItems: data.totalItems || 0, totalPage: data.totalPage || 0, page: data.page || 1 - })) + })); } - }, [listPrivateTemplateReposistory.data, listPrivateTemplateReposistory.isFetched, listPrivateTemplateReposistory.isSuccess]) + }, [ + listPrivateTemplateReposistory.data, + listPrivateTemplateReposistory.isFetched, + listPrivateTemplateReposistory.isSuccess + ]); - const t = useTranslations() - const privateTempalteReposistoryList = listPrivateTemplateReposistory.data?.templateRepositoryList || [] - return - - - {t('my_templates')} - - - - {privateTempalteReposistoryList.map((tr) => ( - t.tag)} /> - ))} - - {privateTempalteReposistoryList.length === 0 && + + + {t('my_templates')} + + + - - - {t('no_template_repository_versions')} - - } - - - { - setPageQueryBody(page => { - return { - ...page, - page: currentPage, - } - }) - }} - /> + {privateTempalteReposistoryList.map((tr) => ( + t.tag)} + /> + ))} + + {privateTempalteReposistoryList.length === 0 && ( + + + + {t('no_template_repository_versions')} + + + )} + + + { + setPageQueryBody((page) => { + return { + ...page, + page: currentPage + }; + }); + }} + /> + - - -} \ No newline at end of file + + ); +} diff --git a/frontend/providers/devbox/app/[lang]/(platform)/template/TemplateModal/PromptModal.tsx b/frontend/providers/devbox/app/[lang]/(platform)/template/TemplateModal/PromptModal.tsx index aa76acb2d50..b15a5bb483d 100644 --- a/frontend/providers/devbox/app/[lang]/(platform)/template/TemplateModal/PromptModal.tsx +++ b/frontend/providers/devbox/app/[lang]/(platform)/template/TemplateModal/PromptModal.tsx @@ -1,39 +1,66 @@ -import MyIcon from "@/components/Icon" -import { Button, ButtonGroup, Flex, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, ModalProps, Text } from "@chakra-ui/react" -import { useTranslations } from "next-intl" +import MyIcon from '@/components/Icon'; +import { + Button, + ButtonGroup, + Flex, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + ModalProps, + Text +} from '@chakra-ui/react'; +import { useTranslations } from 'next-intl'; -const OverviewTemplateVersionModal = ({ onSubmit, version, template, ...props }: Omit & { onSubmit: () => void, version: string, template: string }) => { - const t = useTranslations() +const OverviewTemplateVersionModal = ({ + onSubmit, + version, + template, + ...props +}: Omit & { onSubmit: () => void; version: string; template: string }) => { + const t = useTranslations(); return ( - + - + {t('prompt')} {t.rich('overview_template_version_prompt', { - version: {version} as any, - name: {template} as any + version: ( + + {version} + + ) as any, + name: ( + + {template} + + ) as any })} - - {!inPublicStore && + {!inPublicStore && ( ), onClick: deleteTemplateHandle.onOpen - }, + } ]} /> - } + )} {/* Tags */} - {tags.filter(tag => tag.name !== 'official').map(tag => - {tag[lastLang === 'zh' ? 'zhName' : 'enName'] || tag.name} - )} + {tags + .filter((tag) => tag.name !== 'official') + .map((tag) => ( + + {tag[lastLang === 'zh' ? 'zhName' : 'enName'] || tag.name} + + ))} - + { - const t = useTranslations() - const { isOpen, config, closeTemplateModal, openTemplateModal, updateTemplateModalConfig } = useTemplateStore() - const [search, setsearch] = useState('') + const t = useTranslations(); + const { isOpen, config, closeTemplateModal, openTemplateModal, updateTemplateModalConfig } = + useTemplateStore(); + const [search, setsearch] = useState(''); const updateSearchVal = useCallback( debounce((val: string) => { - setsearch(val) + setsearch(val); }, 500), [] - ) - const lastRoute = usePathname() + ); + const lastRoute = usePathname(); return ( - { - closeTemplateModal() - updateTemplateModalConfig({ - templateState: TemplateState.publicTemplate, - lastRoute - }) - }} lockFocusAcrossFrames={false}> + { + closeTemplateModal(); + updateTemplateModalConfig({ + templateState: TemplateState.publicTemplate, + lastRoute + }); + }} + lockFocusAcrossFrames={false} + > - + {t('devbox_template')} {/* */} - { - if (idx === 0) openTemplateModal({ - templateState: TemplateState.publicTemplate, - lastRoute - }) - else openTemplateModal({ - templateState: TemplateState.privateTemplate, - lastRoute - }) - }}> + if (idx === 0) + openTemplateModal({ + templateState: TemplateState.publicTemplate, + lastRoute + }); + else + openTemplateModal({ + templateState: TemplateState.privateTemplate, + lastRoute + }); + }} + > {/* TabList must be direct child of Tabs */} { borderRadius="6px" color="grayModern.500" _selected={{ - bg: "grayModern.100", - color: "brightBlue.600", + bg: 'grayModern.100', + color: 'brightBlue.600' }} > - + { borderRadius="6px" color="grayModern.500" _selected={{ - bg: "grayModern.100", - color: "brightBlue.600", + bg: 'grayModern.100', + color: 'brightBlue.600' }} > - + { - + { rounded="md" placeholder={t('template_search')} onChange={(e) => { - updateSearchVal(e.target.value) + updateSearchVal(e.target.value); }} /> @@ -159,7 +162,7 @@ const TemplateModal = () => { - ) -} + ); +}; -export default TemplateModal \ No newline at end of file +export default TemplateModal; diff --git a/frontend/providers/devbox/app/[lang]/(platform)/template/updateTemplate/CreateTemplateModal.tsx b/frontend/providers/devbox/app/[lang]/(platform)/template/updateTemplate/CreateTemplateModal.tsx index 9ed10002b9f..4a67bd8e601 100644 --- a/frontend/providers/devbox/app/[lang]/(platform)/template/updateTemplate/CreateTemplateModal.tsx +++ b/frontend/providers/devbox/app/[lang]/(platform)/template/updateTemplate/CreateTemplateModal.tsx @@ -30,7 +30,7 @@ import TemplateRepositoryIsPublicField from './components/TemplateRepositoryIsPu import TemplateRepositoryNameField from './components/TemplateRepositoryNameField'; import TemplateRepositoryTagField from './components/TemplateRepositoryTagField'; const tagSchema = z.object({ - value: z.string(), + value: z.string() }); // 定义表单数据类型和验证规则 @@ -47,14 +47,17 @@ const CreateTemplateModal: FC = ({ // onSubmit devboxReleaseName }) => { - const t = useTranslations() + const t = useTranslations(); const formSchema = z.object({ name: z.string().min(1, t('input_template_name_placeholder')).pipe(templateNameSchema), version: z.string().min(1, t('input_template_version_placeholder')).pipe(versionSchema), isPublic: z.boolean().default(false), agreeTerms: z.boolean().refine((val) => val === true, t('privacy_and_security_agreement_tips')), - tags: z.array(tagSchema).min(1, t('select_at_least_1_tag')).max(3, t('select_lest_than_3_tags')), - description: z.string(), + tags: z + .array(tagSchema) + .min(1, t('select_at_least_1_tag')) + .max(3, t('select_lest_than_3_tags')), + description: z.string() }); type FormData = z.infer; @@ -66,7 +69,7 @@ const CreateTemplateModal: FC = ({ isPublic: false, agreeTerms: false, tags: [], - description: '', + description: '' }, mode: 'onSubmit' }); @@ -74,44 +77,44 @@ const CreateTemplateModal: FC = ({ control, handleSubmit, formState: { errors, isSubmitting }, - reset, - } = methods - const { openTemplateModal, config } = useTemplateStore() - const queryClient = useQueryClient() + reset + } = methods; + const { openTemplateModal, config } = useTemplateStore(); + const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: createTemplateReposistory // return await createTemplate(data) - }) - const { message: toast } = useMessage() - const lastRoute = usePathname() + }); + const { message: toast } = useMessage(); + const lastRoute = usePathname(); const onSubmitHandler: SubmitHandler = async (_data) => { try { - const result = formSchema.safeParse(_data) + const result = formSchema.safeParse(_data); if (!result.success) { // const title = result.error.errors[0] - const error = result.error.errors[0] - if(error.path[0] === 'name' && error.code === 'invalid_string') { + const error = result.error.errors[0]; + if (error.path[0] === 'name' && error.code === 'invalid_string') { toast({ title: t('invalide_template_name'), - status: 'error', + status: 'error' }); return; } - if(error.path[0] === 'version' && error.code === 'invalid_string') { + if (error.path[0] === 'version' && error.code === 'invalid_string') { toast({ title: t('invalide_template_version'), - status: 'error', + status: 'error' }); return; } - const title = error.message + const title = error.message; toast({ title, - status: 'error', + status: 'error' }); return; } - const data = result.data + const data = result.data; await mutation.mutateAsync({ templateRepositoryName: data.name, version: data.version, @@ -120,40 +123,34 @@ const CreateTemplateModal: FC = ({ tagUidList: data.tags.map((tag) => tag.value), devboxReleaseName }); - queryClient.invalidateQueries(['template-repository-list']) - queryClient.invalidateQueries(['template-repository-detail']) + queryClient.invalidateQueries(['template-repository-list']); + queryClient.invalidateQueries(['template-repository-detail']); reset(); onClose(); openTemplateModal({ templateState: TemplateState.privateTemplate, lastRoute - }) + }); toast({ title: t('create_template_success'), - status: 'success', + status: 'success' }); } catch (error) { - - if(error == '409:templateRepository name already exists') { + if (error == '409:templateRepository name already exists') { return toast({ title: t('template_repository_name_already_exists'), - status: 'error', + status: 'error' }); } toast({ title: error as string, - status: 'error', + status: 'error' }); } }; return ( - reset()} - > + reset()}> @@ -170,14 +167,16 @@ const CreateTemplateModal: FC = ({ {/* 版本号 */} - {t('version')} + + {t('version')} + ( + render={({ field }) => ( = ({ /> - {/* 公开 */} {/* */} @@ -195,13 +193,11 @@ const CreateTemplateModal: FC = ({ {/* 标签 */} - {/* 简介 */} - - + {/* 按钮组 */} - ) -} + ); +}; -export default DeleteTemplateReposistoryModal \ No newline at end of file +export default DeleteTemplateReposistoryModal; diff --git a/frontend/providers/devbox/app/[lang]/(platform)/template/updateTemplate/EditTemplateModal.tsx b/frontend/providers/devbox/app/[lang]/(platform)/template/updateTemplate/EditTemplateModal.tsx index 7638112cb03..18342024c5e 100644 --- a/frontend/providers/devbox/app/[lang]/(platform)/template/updateTemplate/EditTemplateModal.tsx +++ b/frontend/providers/devbox/app/[lang]/(platform)/template/updateTemplate/EditTemplateModal.tsx @@ -21,11 +21,11 @@ import { FC, useState } from 'react'; import { z } from 'zod'; import DeleteTemplateVersionModal from '../updateTemplateVersion/DeleteTemplateVersionModal'; const tagSchema = z.object({ - value: z.string().min(1), + value: z.string().min(1) }); const versionSchema = z.object({ name: z.string(), - uid: z.string(), + uid: z.string() }); type VersionType = z.infer; interface CreateTemplateModalProps { @@ -42,18 +42,14 @@ const EditTemplateModal: FC = ({ uid, templateRepositoryName }) => { - const t = useTranslations() - const DeleteTemplateVersionHandle = useDisclosure() - const [deletedTemplateVersion, setDeletedTemplateVersion] = useState() - const templateRepositoryQuery = useQuery( - ['templateList', uid], - () => listTemplate(uid), - { - enabled: isOpen - } - ) + const t = useTranslations(); + const DeleteTemplateVersionHandle = useDisclosure(); + const [deletedTemplateVersion, setDeletedTemplateVersion] = useState(); + const templateRepositoryQuery = useQuery(['templateList', uid], () => listTemplate(uid), { + enabled: isOpen + }); const columns: { - title: string + title: string; dataIndex?: keyof { uid: string; name: string; @@ -61,9 +57,9 @@ const EditTemplateModal: FC = ({ image: string; createAt: Date; updateAt: Date; - } - minWidth?: string - key: string + }; + minWidth?: string; + key: string; render?: (item: { uid: string; name: string; @@ -71,68 +67,72 @@ const EditTemplateModal: FC = ({ image: string; createAt: Date; updateAt: Date; - }) => JSX.Element + }) => JSX.Element; }[] = [ - { - title: t('version'), - key: 'name', - render: (item) => { - return ( - - {item.name} - - ) - } - }, - { - title: t('creation_time'), - dataIndex: 'createAt', - key: 'createAt', - render: (item) => { - return {dayjs().format('YYYY-MM-DD mm:ss')} - } - }, { - title: t('update_time'), - dataIndex: 'createAt', - key: 'createAt', - render: (item) => { - return {dayjs().format('YYYY-MM-DD mm:ss')} - } - }, - { - title: t('control'), - key: 'control', - minWidth: 'unset', - render: (item) => ( - // - } - minW={'unset'} - onClick={() => { - setDeletedTemplateVersion({ - name: item.name, - uid: item.uid, - }) - DeleteTemplateVersionHandle.onOpen() - }} - /> - // - ) + { + title: t('version'), + key: 'name', + render: (item) => { + return ( + + + {item.name} + + + ); + } + }, + { + title: t('creation_time'), + dataIndex: 'createAt', + key: 'createAt', + render: (item) => { + return {dayjs().format('YYYY-MM-DD mm:ss')}; + } + }, + { + title: t('update_time'), + dataIndex: 'createAt', + key: 'createAt', + render: (item) => { + return {dayjs().format('YYYY-MM-DD mm:ss')}; } - ] + }, + { + title: t('control'), + key: 'control', + minWidth: 'unset', + render: (item) => ( + // + } + minW={'unset'} + onClick={() => { + setDeletedTemplateVersion({ + name: item.name, + uid: item.uid + }); + DeleteTemplateVersionHandle.onOpen(); + }} + /> + // + ) + } + ]; // const mock = [ // { // "uid": "741bc275-107a-43a7-ac01-2d26a0f64297", @@ -143,50 +143,57 @@ const EditTemplateModal: FC = ({ // "updatedAt": "2024-12-13T08:29:26.171Z" // }, // ] - const templateList = - templateRepositoryQuery.data?.templateList || [] - return (<> - - - - - {t('version_manage')} - - - - - - - - {templateList.length === 0 && - - - {t('no_template_versions')} - - } + const templateList = templateRepositoryQuery.data?.templateList || []; + return ( + <> + + + + + {t('version_manage')} + + + + + + + {templateList.length === 0 && ( + + + + {t('no_template_versions')} + + + )} + - - - - - {!!deletedTemplateVersion && } - + + + + {!!deletedTemplateVersion && ( + + )} + ); }; -export default EditTemplateModal \ No newline at end of file +export default EditTemplateModal; diff --git a/frontend/providers/devbox/app/[lang]/(platform)/template/updateTemplate/EditTemplateReposistoryModal.tsx b/frontend/providers/devbox/app/[lang]/(platform)/template/updateTemplate/EditTemplateReposistoryModal.tsx index dcef3b9da3e..94a95e06ebd 100644 --- a/frontend/providers/devbox/app/[lang]/(platform)/template/updateTemplate/EditTemplateReposistoryModal.tsx +++ b/frontend/providers/devbox/app/[lang]/(platform)/template/updateTemplate/EditTemplateReposistoryModal.tsx @@ -25,7 +25,7 @@ import TemplateRepositoryIsPublicField from './components/TemplateRepositoryIsPu import TemplateRepositoryNameField from './components/TemplateRepositoryNameField'; import TemplateRepositoryTagField from './components/TemplateRepositoryTagField'; const tagSchema = z.object({ - value: z.string().min(1), + value: z.string().min(1) }); const versionSchema = z.object({ name: z.string(), @@ -40,18 +40,17 @@ interface CreateTemplateModalProps { uid: string; } -const EditTemplateRepositoryModal: FC = ({ - isOpen, - onClose, - uid -}) => { - const t = useTranslations() +const EditTemplateRepositoryModal: FC = ({ isOpen, onClose, uid }) => { + const t = useTranslations(); const formSchema = z.object({ name: z.string().min(1, t('input_template_name_placeholder')), isPublic: z.boolean().default(false), agreeTerms: z.boolean().refine((val) => val === true, t('privacy_and_security_agreement_tips')), - tags: z.array(tagSchema).min(1, t('select_at_least_1_tag')).max(3, t('select_lest_than_3_tags')), - description: z.string(), + tags: z + .array(tagSchema) + .min(1, t('select_at_least_1_tag')) + .max(3, t('select_lest_than_3_tags')), + description: z.string() }); type FormData = z.infer; const methods = useForm({ @@ -60,66 +59,66 @@ const EditTemplateRepositoryModal: FC = ({ isPublic: false, agreeTerms: false, tags: [], - description: '', - }, + description: '' + } }); const { control, handleSubmit, formState: { errors, isSubmitting }, reset, - setValue, - } = methods + setValue + } = methods; - const DeleteTemplateVersionHandle = useDisclosure() - const [deletedTemplateVersion, setDeletedTemplateVersion] = useState() + const DeleteTemplateVersionHandle = useDisclosure(); + const [deletedTemplateVersion, setDeletedTemplateVersion] = useState(); const templateRepositoryQuery = useQuery( ['template-repository-detail', uid], () => getTemplateRepository(uid), { enabled: isOpen } - ) - const updateMutation = useMutation( - updateTemplateReposistory, - { - onSuccess() { - queryClient.invalidateQueries(['template-repository-list']) - queryClient.invalidateQueries(['template-repository-detail']) - } + ); + const updateMutation = useMutation(updateTemplateReposistory, { + onSuccess() { + queryClient.invalidateQueries(['template-repository-list']); + queryClient.invalidateQueries(['template-repository-detail']); } - ) - const templateRepository = templateRepositoryQuery.data?.templateRepository - const [publicIsDisabled, setPublicIsDisabled] = useState(false) + }); + const templateRepository = templateRepositoryQuery.data?.templateRepository; + const [publicIsDisabled, setPublicIsDisabled] = useState(false); useEffect(() => { if (isOpen && templateRepository && templateRepositoryQuery.isSuccess) { - setValue('tags', templateRepository.templateRepositoryTags.map(({ tag }) => ({ - value: tag.uid, - }))) + setValue( + 'tags', + templateRepository.templateRepositoryTags.map(({ tag }) => ({ + value: tag.uid + })) + ); - setValue('name', templateRepository.name) - setValue('description', templateRepository.description || '') - setValue('isPublic', templateRepository.isPublic) + setValue('name', templateRepository.name); + setValue('description', templateRepository.description || ''); + setValue('isPublic', templateRepository.isPublic); if (templateRepository.isPublic) { - setPublicIsDisabled(true) - setValue('agreeTerms', true) + setPublicIsDisabled(true); + setValue('agreeTerms', true); } } - }, [templateRepository, isOpen]) - const { message: toast } = useMessage() - const queryClient = useQueryClient() + }, [templateRepository, isOpen]); + const { message: toast } = useMessage(); + const queryClient = useQueryClient(); const onSubmitHandler = async (_data: FormData) => { try { - const result = formSchema.safeParse(_data) + const result = formSchema.safeParse(_data); if (!result.success) { - const error = result.error.errors[0] + const error = result.error.errors[0]; toast({ title: error.message, - status: 'error', + status: 'error' }); return; } - const data = result.data + const data = result.data; await updateMutation.mutateAsync({ uid, templateRepositoryName: data.name, @@ -127,84 +126,78 @@ const EditTemplateRepositoryModal: FC = ({ description: data.description, tagUidList: data.tags.map(({ value }) => value) }); - queryClient.invalidateQueries(['template-repository-list']) + queryClient.invalidateQueries(['template-repository-list']); reset(); onClose(); toast({ title: t('template_ssaved_successfully'), - status: 'success', + status: 'success' }); } catch (error) { toast({ title: error as string, - status: 'error', + status: 'error' }); } }; - return (<> - reset()} - > - - - - - - {t('edit_template')} - - - - - - {/* 名称 */} - - - {/* 公开 */} - + return ( + <> + reset()}> + + + + + + {t('edit_template')} + + + - {/* 标签 */} - + + {/* 名称 */} + - {/* 简介 */} - - + {/* 公开 */} + - - - {/* 按钮组 */} - - - - - - - + {/* 标签 */} + - - - + {/* 简介 */} + + + + + {/* 按钮组 */} + + + + + + + + + + ); }; -export default EditTemplateRepositoryModal \ No newline at end of file +export default EditTemplateRepositoryModal; diff --git a/frontend/providers/devbox/app/[lang]/(platform)/template/updateTemplate/SelectActionModal/TemplateDropdown.tsx b/frontend/providers/devbox/app/[lang]/(platform)/template/updateTemplate/SelectActionModal/TemplateDropdown.tsx index 34c6518c38d..7ab584287e7 100644 --- a/frontend/providers/devbox/app/[lang]/(platform)/template/updateTemplate/SelectActionModal/TemplateDropdown.tsx +++ b/frontend/providers/devbox/app/[lang]/(platform)/template/updateTemplate/SelectActionModal/TemplateDropdown.tsx @@ -1,13 +1,34 @@ -import MyIcon from "@/components/Icon"; -import { ChevronDownIcon } from "@chakra-ui/icons"; -import { Box, Button, ButtonProps, HStack, Img, Popover, PopoverBody, PopoverContent, PopoverTrigger, Text, useDisclosure, VStack } from "@chakra-ui/react"; +import MyIcon from '@/components/Icon'; +import { ChevronDownIcon } from '@chakra-ui/icons'; +import { + Box, + Button, + ButtonProps, + HStack, + Img, + Popover, + PopoverBody, + PopoverContent, + PopoverTrigger, + Text, + useDisclosure, + VStack +} from '@chakra-ui/react'; -const TemplateButton = ({ isActive = false, icon, title, description, onClick, isInMenu = false, ...props }: ButtonProps & { +const TemplateButton = ({ + isActive = false, + icon, + title, + description, + onClick, + isInMenu = false, + ...props +}: ButtonProps & { icon: React.ReactNode; title: string; isActive?: boolean; - isInMenu?: boolean - description: string + isInMenu?: boolean; + description: string; }) => { return ( - @@ -80,4 +93,4 @@ const SelectTemplateModal = ({ ); }; -export default SelectTemplateModal \ No newline at end of file +export default SelectTemplateModal; diff --git a/frontend/providers/devbox/app/[lang]/(platform)/template/updateTemplate/UpdateTemplateRepositoryModal.tsx b/frontend/providers/devbox/app/[lang]/(platform)/template/updateTemplate/UpdateTemplateRepositoryModal.tsx index 5bb60056ea6..24061c60ec6 100644 --- a/frontend/providers/devbox/app/[lang]/(platform)/template/updateTemplate/UpdateTemplateRepositoryModal.tsx +++ b/frontend/providers/devbox/app/[lang]/(platform)/template/updateTemplate/UpdateTemplateRepositoryModal.tsx @@ -35,11 +35,10 @@ import TemplateRepositoryDescriptionField from './components/TemplateRepositoryD import TemplateRepositoryNameField from './components/TemplateRepositoryNameField'; import TemplateRepositoryTagField from './components/TemplateRepositoryTagField'; - interface CreateTemplateModalProps { isOpen: boolean; onClose: () => void; - devboxReleaseName: string + devboxReleaseName: string; templateRepository: { uid: string; name: string; @@ -59,147 +58,150 @@ interface CreateTemplateModalProps { }; } - -const VersionSelect = ({ templateList }: { templateList: { uid: string, name: string }[] }) => { - const { - watch, - setValue, - } = useFormContext(); - const [inputValue, setInputValue] = useState("") +const VersionSelect = ({ templateList }: { templateList: { uid: string; name: string }[] }) => { + const { watch, setValue } = useFormContext(); + const [inputValue, setInputValue] = useState(''); const handleVersionSelect = (version: string) => { - setInputValue(version) - setValue('version', inputValue) - handler.onClose() - } - const handler = useDisclosure() + setInputValue(version); + setValue('version', inputValue); + handler.onClose(); + }; + const handler = useDisclosure(); const handleCreateVersion = () => { // 处理创建新版本的逻辑 - setValue('version', inputValue) - handler.onClose() - } - const t = useTranslations() - return (<> - - - - {watch('version')} - - - - + - - { - setInputValue(e.target.value) - }} - // border="1px solid #219BF4" - // boxShadow="0px 0px 0px 2.4px rgba(51, 112, 255, 0.15)" - borderRadius="4px" - fontSize="12px" - placeholder={t('search_or_add_version')} - _focus={{ - border: "1px solid #219BF4", - boxShadow: "0px 0px 0px 2.4px rgba(51, 112, 255, 0.15)", - }} - /> - - {/* 已有版本列表 */} - {templateList - .filter(v => v.name.toLowerCase().includes(inputValue.toLowerCase())) - .map((v) => ( - + + + {watch('version')} + + + + + + + { + setInputValue(e.target.value); + }} + // border="1px solid #219BF4" + // boxShadow="0px 0px 0px 2.4px rgba(51, 112, 255, 0.15)" + borderRadius="4px" + fontSize="12px" + placeholder={t('search_or_add_version')} + _focus={{ + border: '1px solid #219BF4', + boxShadow: '0px 0px 0px 2.4px rgba(51, 112, 255, 0.15)' + }} + /> + + {/* 已有版本列表 */} + {templateList + .filter((v) => v.name.toLowerCase().includes(inputValue.toLowerCase())) + .map((v) => ( + handleVersionSelect(v.name)} + > + {v.name} + + ))} + + {/* 创建新版本选项 */} + {inputValue && !templateList.find((v) => v.name === inputValue) && ( + handleVersionSelect(v.name)} + _hover={{ bg: 'rgba(17, 24, 36, 0.05)' }} + onClick={handleCreateVersion} > - {v.name} - - ))} - - {/* 创建新版本选项 */} - {inputValue && !templateList.find(v => v.name === inputValue) && ( - - - - {t('create_template_version', { - version: inputValue, - })} - - - )} - - - - - - ) -} + + + {t('create_template_version', { + version: inputValue + })} + + + )} + + + + + + ); +}; const UpdateTemplateRepositoryModal: FC = ({ isOpen, onClose, templateRepository, devboxReleaseName }) => { - const t = useTranslations() + const t = useTranslations(); const tagSchema = z.object({ - value: z.string(), + value: z.string() }); const formSchema = z.object({ name: z.string().min(1, t('input_template_name_placeholder')), version: z.string().min(1, t('input_template_version_placeholder')).pipe(versionSchema), - tags: z.array(tagSchema).min(1, t('select_at_least_1_tag')).max(3, t('select_lest_than_3_tags')), - description: z.string(), + tags: z + .array(tagSchema) + .min(1, t('select_at_least_1_tag')) + .max(3, t('select_lest_than_3_tags')), + description: z.string() }); - const queryClient = useQueryClient() + const queryClient = useQueryClient(); type FormData = z.infer; const mutation = useMutation(updateTemplate, { onSuccess() { - queryClient.invalidateQueries(['template-repository-list']) - queryClient.invalidateQueries(['template-repository-detail']) + queryClient.invalidateQueries(['template-repository-list']); + queryClient.invalidateQueries(['template-repository-detail']); } - }) + }); const methods = useForm({ defaultValues: { name: '', version: '', // agreeTerms: false, tags: [], - description: '', - }, + description: '' + } }); const { handleSubmit, @@ -207,141 +209,143 @@ const UpdateTemplateRepositoryModal: FC = ({ reset, setValue, watch - } = methods + } = methods; useEffect(() => { if (templateRepository && isOpen) { - setValue('tags', templateRepository.templateRepositoryTags.map(({ tag }) => ({ - value: tag.uid, - }))) - setValue('version', templateRepository.templates[0]?.name || '') - setValue('name', templateRepository.name) - setValue('description', templateRepository.description || '') + setValue( + 'tags', + templateRepository.templateRepositoryTags.map(({ tag }) => ({ + value: tag.uid + })) + ); + setValue('version', templateRepository.templates[0]?.name || ''); + setValue('name', templateRepository.name); + setValue('description', templateRepository.description || ''); } - }, [templateRepository, isOpen]) - const { message: toast } = useMessage() - const overviewHandler = useDisclosure() + }, [templateRepository, isOpen]); + const { message: toast } = useMessage(); + const overviewHandler = useDisclosure(); const submit = async (_data: FormData) => { try { - const result = formSchema.safeParse(_data) + const result = formSchema.safeParse(_data); if (!result.success) { - const error = result.error.errors[0] - if(error.path[0] === 'version' && error.code === 'invalid_string') { + const error = result.error.errors[0]; + if (error.path[0] === 'version' && error.code === 'invalid_string') { toast({ title: t('invalide_template_version'), - status: 'error', + status: 'error' }); return; } toast({ title: error.message, - status: 'error', + status: 'error' }); return; } - const data = result.data + const data = result.data; await mutation.mutateAsync({ templateRepositoryUid: templateRepository?.uid || '', version: data.version, devboxReleaseName, description: data.description, - tagUidList: data.tags.map(({ value }) => value), - }) + tagUidList: data.tags.map(({ value }) => value) + }); - queryClient.invalidateQueries(['template-repository-list']) + queryClient.invalidateQueries(['template-repository-list']); reset(); onClose(); toast({ title: t('update_template_success'), - status: 'success', + status: 'success' }); } catch (error) { toast({ title: error as string, - status: 'error', + status: 'error' }); } }; const onSubmitHandler = (data: FormData) => { - if (templateRepository.templates.findIndex(d => data.version === d.name) > -1) { - overviewHandler.onOpen() - return + if (templateRepository.templates.findIndex((d) => data.version === d.name) > -1) { + overviewHandler.onOpen(); + return; } - return submit(data) + return submit(data); }; - return (<> - reset()} - > - - - -
- - {t('update_template')} - - - - - {/* 名称 */} - + return ( + <> + reset()}> + + + + + + {t('update_template')} + + + + + {/* 名称 */} + - {/* 版本号 */} - - {t('version')} - - - - + {/* 版本号 */} + + + {t('version')} + + + + + - {/* 标签 */} - - - {/* 简介 */} - - - - - - - {/* 按钮组 */} - - - - - - - + {/* 标签 */} + - - - { - submit(methods.getValues()) - }} version={watch('version')} template={templateRepository.name} /> - + {/* 简介 */} + + +
+ + {/* 按钮组 */} + + + + + + +
+
+
+ { + submit(methods.getValues()); + }} + version={watch('version')} + template={templateRepository.name} + /> + ); }; -export default UpdateTemplateRepositoryModal \ No newline at end of file +export default UpdateTemplateRepositoryModal; diff --git a/frontend/providers/devbox/app/[lang]/(platform)/template/updateTemplate/components/TemplateRepositoryDescriptionField.tsx b/frontend/providers/devbox/app/[lang]/(platform)/template/updateTemplate/components/TemplateRepositoryDescriptionField.tsx index a9f978b66af..f62470da530 100644 --- a/frontend/providers/devbox/app/[lang]/(platform)/template/updateTemplate/components/TemplateRepositoryDescriptionField.tsx +++ b/frontend/providers/devbox/app/[lang]/(platform)/template/updateTemplate/components/TemplateRepositoryDescriptionField.tsx @@ -1,19 +1,22 @@ -import MyFormLabel from "@/components/MyFormControl"; -import { Flex, Textarea } from "@chakra-ui/react"; -import { useTranslations } from "next-intl"; -import { Controller, useFormContext } from "react-hook-form"; +import MyFormLabel from '@/components/MyFormControl'; +import { Flex, Textarea } from '@chakra-ui/react'; +import { useTranslations } from 'next-intl'; +import { Controller, useFormContext } from 'react-hook-form'; export default function TemplateRepositoryDescriptionField() { const { control } = useFormContext<{ description: string }>(); - const t = useTranslations() - return - {t('template_description')} + const t = useTranslations(); + return ( + + + {t('template_description')} + (