diff --git a/controllers/account/go.mod b/controllers/account/go.mod index ea8c2580a8e..f7362efa431 100644 --- a/controllers/account/go.mod +++ b/controllers/account/go.mod @@ -50,7 +50,6 @@ require ( github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/clbanning/mxj/v2 v2.5.7 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/dinoallo/sealos-networkmanager-protoapi v0.0.0-20230928031328-cf9649d6af49 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/evanphx/json-patch/v5 v5.8.0 // indirect diff --git a/controllers/devbox/cmd/main.go b/controllers/devbox/cmd/main.go index fba7e1dd261..62416e9441d 100644 --- a/controllers/devbox/cmd/main.go +++ b/controllers/devbox/cmd/main.go @@ -69,6 +69,8 @@ func main() { var registryUser string var registryPassword string var authAddr string + var requestCPURate float64 + var requestMemoryRate float64 var requestEphemeralStorage string var limitEphemeralStorage string var debugMode bool @@ -87,6 +89,8 @@ func main() { flag.BoolVar(&enableHTTP2, "enable-http2", false, "If set, HTTP/2 will be enabled for the metrics and webhook servers") flag.BoolVar(&debugMode, "debug", false, "If set, debug mode will be enabled") + flag.Float64Var(&requestCPURate, "request-cpu-rate", 10, "The request rate of cpu limit in devbox.") + flag.Float64Var(&requestMemoryRate, "request-memory-rate", 10, "The request rate of memory limit in devbox.") flag.StringVar(&requestEphemeralStorage, "request-ephemeral-storage", "500Mi", "The request value of ephemeral storage in devbox.") flag.StringVar(&limitEphemeralStorage, "limit-ephemeral-storage", "10Gi", "The limit value of ephemeral storage in devbox.") opts := zap.Options{ @@ -183,6 +187,8 @@ func main() { Scheme: mgr.GetScheme(), CommitImageRegistry: registryAddr, Recorder: mgr.GetEventRecorderFor("devbox-controller"), + RequestCPURate: requestCPURate, + RequestMemoryRate: requestMemoryRate, RequestEphemeralStorage: requestEphemeralStorage, LimitEphemeralStorage: limitEphemeralStorage, DebugMode: debugMode, diff --git a/controllers/devbox/internal/controller/devbox_controller.go b/controllers/devbox/internal/controller/devbox_controller.go index 0de5776e765..b8d76504e6c 100644 --- a/controllers/devbox/internal/controller/devbox_controller.go +++ b/controllers/devbox/internal/controller/devbox_controller.go @@ -45,6 +45,8 @@ import ( // DevboxReconciler reconciles a Devbox object type DevboxReconciler struct { CommitImageRegistry string + RequestCPURate float64 + RequestMemoryRate float64 RequestEphemeralStorage string LimitEphemeralStorage string @@ -547,7 +549,7 @@ func (r *DevboxReconciler) generateDevboxPod(devbox *devboxv1alpha1.Devbox, runt WorkingDir: helper.GenerateWorkingDir(devbox, runtime), Command: helper.GenerateCommand(devbox, runtime), Args: helper.GenerateDevboxArgs(devbox, runtime), - Resources: helper.GenerateResourceRequirements(devbox, r.RequestEphemeralStorage, r.LimitEphemeralStorage), + Resources: helper.GenerateResourceRequirements(devbox, r.RequestCPURate, r.RequestMemoryRate, r.RequestEphemeralStorage, r.LimitEphemeralStorage), }, } diff --git a/controllers/devbox/internal/controller/helper/devbox.go b/controllers/devbox/internal/controller/helper/devbox.go index eb969bc56f0..64e8fbd98b4 100644 --- a/controllers/devbox/internal/controller/helper/devbox.go +++ b/controllers/devbox/internal/controller/helper/devbox.go @@ -34,7 +34,6 @@ import ( ) const ( - rate = 10 DevBoxPartOf = "devbox" ) @@ -210,10 +209,18 @@ func PodMatchExpectations(expectPod *corev1.Pod, pod *corev1.Pod) bool { expectContainer := expectPod.Spec.Containers[0] // Check CPU and memory limits + if container.Resources.Requests.Cpu().Cmp(*expectContainer.Resources.Requests.Cpu()) != 0 { + slog.Info("CPU requests are not equal") + return false + } if container.Resources.Limits.Cpu().Cmp(*expectContainer.Resources.Limits.Cpu()) != 0 { slog.Info("CPU limits are not equal") return false } + if container.Resources.Requests.Memory().Cmp(*expectContainer.Resources.Requests.Memory()) != 0 { + slog.Info("Memory requests are not equal") + return false + } if container.Resources.Limits.Memory().Cmp(*expectContainer.Resources.Limits.Memory()) != 0 { slog.Info("Memory limits are not equal") return false @@ -381,7 +388,10 @@ func GenerateSSHVolume(devbox *devboxv1alpha1.Devbox) corev1.Volume { } } -func GenerateResourceRequirements(devbox *devboxv1alpha1.Devbox, requestEphemeralStorage, limitEphemeralStorage string) corev1.ResourceRequirements { +func GenerateResourceRequirements(devbox *devboxv1alpha1.Devbox, + requestCPURate, requestMemoryRate float64, + requestEphemeralStorage, limitEphemeralStorage string, +) corev1.ResourceRequirements { return corev1.ResourceRequirements{ Requests: calculateResourceRequest( corev1.ResourceList{ @@ -389,6 +399,7 @@ func GenerateResourceRequirements(devbox *devboxv1alpha1.Devbox, requestEphemera corev1.ResourceMemory: devbox.Spec.Resource["memory"], corev1.ResourceEphemeralStorage: resource.MustParse(requestEphemeralStorage), }, + requestCPURate, requestMemoryRate, ), Limits: corev1.ResourceList{ corev1.ResourceCPU: devbox.Spec.Resource["cpu"], @@ -402,7 +413,7 @@ func IsExceededQuotaError(err error) bool { return strings.Contains(err.Error(), "exceeded quota") } -func calculateResourceRequest(limit corev1.ResourceList) corev1.ResourceList { +func calculateResourceRequest(limit corev1.ResourceList, requestCPURate, requestMemoryRate float64) corev1.ResourceList { if limit == nil { return nil } @@ -410,13 +421,13 @@ func calculateResourceRequest(limit corev1.ResourceList) corev1.ResourceList { // Calculate CPU request if cpu, ok := limit[corev1.ResourceCPU]; ok { cpuValue := cpu.AsApproximateFloat64() - cpuRequest := cpuValue / rate + cpuRequest := cpuValue / requestCPURate request[corev1.ResourceCPU] = *resource.NewMilliQuantity(int64(cpuRequest*1000), resource.DecimalSI) } // Calculate memory request if memory, ok := limit[corev1.ResourceMemory]; ok { memoryValue := memory.AsApproximateFloat64() - memoryRequest := memoryValue / rate + memoryRequest := memoryValue / requestMemoryRate request[corev1.ResourceMemory] = *resource.NewQuantity(int64(memoryRequest), resource.BinarySI) } diff --git a/controllers/job/init/go.mod b/controllers/job/init/go.mod index 7a3c5f8810e..482dcf1042c 100644 --- a/controllers/job/init/go.mod +++ b/controllers/job/init/go.mod @@ -17,7 +17,6 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/dinoallo/sealos-networkmanager-protoapi v0.0.0-20230928031328-cf9649d6af49 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/evanphx/json-patch v5.6.0+incompatible // indirect github.com/evanphx/json-patch/v5 v5.8.0 // indirect diff --git a/controllers/license/go.mod b/controllers/license/go.mod index 562ed596360..52f23c676a7 100644 --- a/controllers/license/go.mod +++ b/controllers/license/go.mod @@ -23,7 +23,6 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/dinoallo/sealos-networkmanager-protoapi v0.0.0-20230928031328-cf9649d6af49 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/evanphx/json-patch v5.6.0+incompatible // indirect github.com/evanphx/json-patch/v5 v5.8.0 // indirect diff --git a/controllers/objectstorage/controllers/objectstorageuser_controller.go b/controllers/objectstorage/controllers/objectstorageuser_controller.go index 0b85da99b4d..3f37abe11c3 100644 --- a/controllers/objectstorage/controllers/objectstorageuser_controller.go +++ b/controllers/objectstorage/controllers/objectstorageuser_controller.go @@ -20,7 +20,6 @@ import ( "bytes" "context" "fmt" - "strconv" "strings" "time" @@ -219,8 +218,10 @@ func (r *ObjectStorageUserReconciler) Reconcile(ctx context.Context, req ctrl.Re updated = true } - if used.Value() != size { - resourceQuota.Status.Used[ResourceObjectStorageSize] = resource.MustParse(strconv.FormatInt(size, 10)) + stringSize := ConvertBytesToString(size) + + if used.String() != stringSize { + resourceQuota.Status.Used[ResourceObjectStorageSize] = resource.MustParse(stringSize) if err := r.Status().Update(ctx, resourceQuota); err != nil { r.Logger.Error(err, "failed to update status", "name", resourceQuota.Name, "namespace", userNamespace) return ctrl.Result{}, err @@ -433,6 +434,30 @@ func (r *ObjectStorageUserReconciler) initObjectStorageKeySecret(secret *corev1. return updated } +func ConvertBytesToString(bytes int64) string { + var unit string + var value float64 + + switch { + case bytes >= 1<<40: // 1 TiB + value = float64(bytes) / (1 << 40) + unit = "Ti" + case bytes >= 1<<30: // 1 GiB + value = float64(bytes) / (1 << 30) + unit = "Gi" + case bytes >= 1<<20: // 1 MiB + value = float64(bytes) / (1 << 20) + unit = "Mi" + case bytes >= 1<<10: // 1 KiB + value = float64(bytes) / (1 << 10) + unit = "Ki" + default: + return fmt.Sprintf("%d", bytes) + } + + return fmt.Sprintf("%.0f%s", value, unit) +} + // SetupWithManager sets up the controller with the Manager. func (r *ObjectStorageUserReconciler) SetupWithManager(mgr ctrl.Manager) error { r.Logger = ctrl.Log.WithName("object-storage-user-controller") diff --git a/controllers/pkg/go.mod b/controllers/pkg/go.mod index 4151613e102..d826f798637 100644 --- a/controllers/pkg/go.mod +++ b/controllers/pkg/go.mod @@ -20,7 +20,6 @@ replace ( require ( github.com/containers/storage v1.50.2 - github.com/dinoallo/sealos-networkmanager-protoapi v0.0.0-20230928031328-cf9649d6af49 github.com/dustin/go-humanize v1.0.1 github.com/go-logr/logr v1.4.1 github.com/golang-jwt/jwt/v4 v4.5.0 diff --git a/controllers/pkg/go.sum b/controllers/pkg/go.sum index 5af680fa90a..568738cb47f 100644 --- a/controllers/pkg/go.sum +++ b/controllers/pkg/go.sum @@ -10,8 +10,6 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dinoallo/sealos-networkmanager-protoapi v0.0.0-20230928031328-cf9649d6af49 h1:4GI5eviCwbPxDE311KryyyPUTO7IDVyHGp3Iyl+fEZY= -github.com/dinoallo/sealos-networkmanager-protoapi v0.0.0-20230928031328-cf9649d6af49/go.mod h1:sbm1DAsayX+XsXCOC2CFAAU9JZhX0SPKwnybDjSd0Ls= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= diff --git a/controllers/resources/go.mod b/controllers/resources/go.mod index 8dfe62ca9e6..6f938e03aa9 100644 --- a/controllers/resources/go.mod +++ b/controllers/resources/go.mod @@ -32,7 +32,6 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/dinoallo/sealos-networkmanager-protoapi v0.0.0-20230928031328-cf9649d6af49 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/evanphx/json-patch/v5 v5.8.0 // indirect @@ -61,7 +60,6 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.17.7 // indirect github.com/klauspost/cpuid/v2 v2.2.5 // indirect - github.com/labring/sealos/controllers/account v0.0.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/matoous/go-nanoid/v2 v2.0.0 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect @@ -98,8 +96,6 @@ require ( golang.org/x/time v0.5.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe // indirect - google.golang.org/grpc v1.61.0 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect @@ -120,7 +116,6 @@ require ( ) replace ( - github.com/labring/sealos/controllers/account => ../account github.com/labring/sealos/controllers/pkg => ../pkg github.com/labring/sealos/controllers/user => ../user ) diff --git a/docs/website/src/pages/components/Footer/index.scss b/docs/website/src/pages/components/Footer/index.scss index d81b89c0b61..cffaa82909c 100644 --- a/docs/website/src/pages/components/Footer/index.scss +++ b/docs/website/src/pages/components/Footer/index.scss @@ -130,7 +130,10 @@ font-style: normal; font-weight: 600; line-height: 140%; - a{ + display: flex; + align-items: center; + gap: 4px; + a { color: rgba(255, 255, 255, 0.8); text-decoration: none; } diff --git a/docs/website/src/pages/components/Footer/index.tsx b/docs/website/src/pages/components/Footer/index.tsx index 40facb0b23a..f876078b117 100644 --- a/docs/website/src/pages/components/Footer/index.tsx +++ b/docs/website/src/pages/components/Footer/index.tsx @@ -76,9 +76,15 @@ const Footer = ({ isPc }: { isPc: boolean }) => {
-
- Made by Sealos Team.{' '} - 粤ICP备2023048773号  +
+ Made by Sealos Team. + beian + 浙公网安备33011002017870号 + 粤ICP备2023048773号 珠海环界云计算有限公司版权所有
@@ -130,8 +136,15 @@ const Footer = ({ isPc }: { isPc: boolean }) => {
- Made by Sealos Team. 粤ICP备2023048773号 -   珠海环界云计算有限公司版权所有 + Made by Sealos Team. + beian + 浙公网安备33011002017870号 + 粤ICP备2023048773号 + 珠海环界云计算有限公司版权所有
{FooterLinks.map((item) => { diff --git a/docs/website/static/img/beian.png b/docs/website/static/img/beian.png new file mode 100644 index 00000000000..6fe667f73fa Binary files /dev/null and b/docs/website/static/img/beian.png differ diff --git a/frontend/desktop/README.md b/frontend/desktop/README.md index a284205c4b5..e5a3dc4a6f8 100644 --- a/frontend/desktop/README.md +++ b/frontend/desktop/README.md @@ -164,7 +164,7 @@ Open [http://localhost:3000](http://localhost:3000) with your browser to see the 1. 获取登录凭证: 由于 login 页面不是在 desktop 项目里,所以需要从线上 sealos 获取登录凭证到本地开发: 。复制 storage 里的 session 到 localhost 环境实现 mock 登录。 -2. Chakra ui +2. Chakra ui 3. TanStack Query 用法: diff --git a/frontend/desktop/public/locales/en/common.json b/frontend/desktop/public/locales/en/common.json index c99e8045966..af112933f82 100644 --- a/frontend/desktop/public/locales/en/common.json +++ b/frontend/desktop/public/locales/en/common.json @@ -6,7 +6,7 @@ "account_settings": "Account Settings", "added": "Added", "agree_policy": "I have read and agree to the", - "alarm_pod": "Alarm Pod: {{count}}", + "alarm_pod": "Unhealthy Pods: {{count}}", "alerts": "Alerts", "all_apps": "All Apps", "already_sent_code": "Already sent code", @@ -19,7 +19,7 @@ "attachment": "appendix", "avatar": "Avatar", "balance": "Balance", - "bind": "Bind", + "bind": "Link", "bind_success": "Binding successful", "bonus": "Bonus", "bound": "Bound", @@ -70,13 +70,13 @@ "enterprise_name": "Company name", "enterprise_name_required": "Please enter the correct company name", "enterprise_verification": "Enterprise real name", - "expected_to_use_next_month": "Expected to use next month", - "expected_used": "Expected used", + "expected_to_use_next_month": "Estimated Next Invoice", + "expected_used": "Estimated Runaway", "face_recognition_failed": "Personal real name failed", "face_recognition_success": "Personal real-name success", "failed_to_generate_invitation_link": "Failed to generate invitation link", "failed_to_get_qr_code": "Failed to obtain real name QR code", - "flow": "Traffic", + "flow": " Network", "force_delete_keywords": "All resources cannot be recovered after account deletion.", "force_delete_tips": "There are still undeleted resources in your account. Once deleted, all resources will be unrecoverable. Please ensure you have backed up or transferred all important data.", "from": "From", @@ -91,7 +91,7 @@ "guide_objectstorage": "Massive storage space, almost bare metal speed experience", "handle": "Handle", "have_read": "Have read", - "healthy_pod": "Healthy Pod: {{count}}", + "healthy_pod": "Healthy Pods: {{count}}", "hello_welcome": "Hello, welcome to", "help_you_enable_high_availability_database": "Help you enable high availability database", "home": "home", @@ -125,7 +125,7 @@ "login_with_google": "Login with Google", "login_with_oauth2": "login with OAuth2.0", "login_with_wechat": "Login with Wechat", - "manage_team": "Manage Workspace", + "manage_team": "Manage Workspaces", "member_list": "Member List", "memory": "Memory", "merge": "merge", @@ -133,7 +133,7 @@ "merge_account_tips2": "The account you are trying to bind has been used by another user. You can choose to merge accounts to manage your information and settings in a unified way. After the merge is completed, your private workspace may be converted into a regular workspace. Do you want to merge accounts now?", "merge_account_title": "Account has been bound", "message_center": "Message Center", - "modify_member": "Modify Member", + "modify_member": "Modify Member", "monitor": "Monitor", "more_apps": "More Apps", "name": "Name", @@ -147,7 +147,7 @@ "nickname": "Nickname", "no_apps_found": "No Apps Found", "no_realname_auth": "NO REALNAME AUTH", - "notification": "notify", + "notification": "Notification", "noworkspacecreated": "You haven't created a workspace yet", "official_account_login": "Official account login", "old_email": "Old email", @@ -233,12 +233,12 @@ "total_amount": "Total Amount", "unbind": "Unbind", "unbind_success": "Unbinding successfully", - "unbound": "Unbound", + "unbound": "Not Linked", "under_active_development": "Under active development 🚧", "unread": "Unread", "upload_success": "Upload successful", - "used_last_month": "Used last month", - "used_resources": "Used Resources", + "used_last_month": " Last Invoice", + "used_resources": "Resources Used", "user_name": "User Name", "username": "Username", "username_tips": "Username must be 3-16 characters, including letters, numbers", @@ -267,4 +267,4 @@ "you_can_view_fees_through_the_fee_center": "You can view fees through the fee center", "you_have_not_purchased_the_license": "You have not purchased the License", "yuan": "Yuan" -} +} \ No newline at end of file diff --git a/frontend/desktop/public/locales/zh/common.json b/frontend/desktop/public/locales/zh/common.json index da5db1fdae7..5676ca4bc82 100644 --- a/frontend/desktop/public/locales/zh/common.json +++ b/frontend/desktop/public/locales/zh/common.json @@ -6,7 +6,7 @@ "account_settings": "账户设置", "added": "已加入", "agree_policy": "我已阅读并同意", - "alarm_pod": "告警 Pod: {{count}}", + "alarm_pod": "告警 Pods: {{count}}", "alerts": "告警", "all_apps": "所有应用", "already_sent_code": "已经发送验证码", @@ -88,7 +88,7 @@ "guide_objectstorage": "海量存储空间,近乎裸机的速度体验", "handle": "操作", "have_read": "已读", - "healthy_pod": "健康 Pod: {{count}}", + "healthy_pod": "健康 Pods: {{count}}", "hello_welcome": "您好, 欢迎来到", "help_you_enable_high_availability_database": "帮您启用高可用数据库", "home": "首页", @@ -260,4 +260,4 @@ "you_can_view_fees_through_the_fee_center": "您可通过费用中心查看费用", "you_have_not_purchased_the_license": "您还没有购买 License", "yuan": "元" -} +} \ No newline at end of file diff --git a/frontend/desktop/src/components/account/index.tsx b/frontend/desktop/src/components/account/index.tsx index fa907229490..2311e53d981 100644 --- a/frontend/desktop/src/components/account/index.tsx +++ b/frontend/desktop/src/components/account/index.tsx @@ -200,7 +200,7 @@ export default function Account() { py={'12px'} px={'16px'} > - kubeconfig + Kubeconfig )} - + - {t('common:healthy_pod', { count: runningPodCount })} + {t('common:healthy_pod', { count: runningPodCount })} - {t('common:alarm_pod', { count: totalPodCount - runningPodCount })} + + {t('common:alarm_pod', { count: totalPodCount - runningPodCount })} + diff --git a/frontend/desktop/src/pages/api/auth/email/bind/verify.ts b/frontend/desktop/src/pages/api/auth/email/bind/verify.ts index 5c10e97502f..0417788c92f 100644 --- a/frontend/desktop/src/pages/api/auth/email/bind/verify.ts +++ b/frontend/desktop/src/pages/api/auth/email/bind/verify.ts @@ -3,11 +3,11 @@ import { ErrorHandler } from '@/services/backend/middleware/error'; import { bindEmailGuard } from '@/services/backend/middleware/oauth'; import { filterEmailVerifyParams, verifyEmailCodeGuard } from '@/services/backend/middleware/sms'; import { bindEmailSvc } from '@/services/backend/svc/bindProvider'; -import { enablePhoneSms } from '@/services/enable'; +import { enableEmailSms } from '@/services/enable'; import { NextApiRequest, NextApiResponse } from 'next'; export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { - if (!enablePhoneSms()) { + if (!enableEmailSms()) { throw new Error('SMS is not enabled'); } await filterAccessToken( diff --git a/frontend/desktop/src/pages/api/auth/email/changeBinding/newSms.ts b/frontend/desktop/src/pages/api/auth/email/changeBinding/newSms.ts index 0413e4c4e73..911ee217ff9 100644 --- a/frontend/desktop/src/pages/api/auth/email/changeBinding/newSms.ts +++ b/frontend/desktop/src/pages/api/auth/email/changeBinding/newSms.ts @@ -6,10 +6,10 @@ import { sendNewEmailCodeGuard } from '@/services/backend/middleware/sms'; import { sendEmailCodeSvc } from '@/services/backend/svc/sms'; -import { enablePhoneSms } from '@/services/enable'; +import { enableEmailSms } from '@/services/enable'; import { NextApiRequest, NextApiResponse } from 'next'; export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { - if (!enablePhoneSms()) { + if (!enableEmailSms()) { throw new Error('SMS is not enabled'); } await filterAccessToken(req, res, ({ userUid }) => diff --git a/frontend/desktop/src/pages/api/auth/email/changeBinding/oldSms.ts b/frontend/desktop/src/pages/api/auth/email/changeBinding/oldSms.ts index d9c3dc24ddf..adb279d0341 100644 --- a/frontend/desktop/src/pages/api/auth/email/changeBinding/oldSms.ts +++ b/frontend/desktop/src/pages/api/auth/email/changeBinding/oldSms.ts @@ -3,10 +3,10 @@ import { ErrorHandler } from '@/services/backend/middleware/error'; import { unbindEmailGuard } from '@/services/backend/middleware/oauth'; import { filterEmailParams, sendEmailCodeGuard } from '@/services/backend/middleware/sms'; import { sendEmailCodeSvc } from '@/services/backend/svc/sms'; -import { enablePhoneSms } from '@/services/enable'; +import { enableEmailSms } from '@/services/enable'; import { NextApiRequest, NextApiResponse } from 'next'; export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { - if (!enablePhoneSms()) { + if (!enableEmailSms()) { throw new Error('SMS is not enabled'); } await filterAccessToken(req, res, ({ userUid }) => diff --git a/frontend/desktop/src/pages/api/auth/email/changeBinding/verifyNew.ts b/frontend/desktop/src/pages/api/auth/email/changeBinding/verifyNew.ts index b39eeba9cc5..2940f93e1b8 100644 --- a/frontend/desktop/src/pages/api/auth/email/changeBinding/verifyNew.ts +++ b/frontend/desktop/src/pages/api/auth/email/changeBinding/verifyNew.ts @@ -8,11 +8,11 @@ import { verifyEmailCodeGuard } from '@/services/backend/middleware/sms'; import { changeEmailBindingSvc } from '@/services/backend/svc/bindProvider'; -import { enablePhoneSms } from '@/services/enable'; +import { enableEmailSms } from '@/services/enable'; import { NextApiRequest, NextApiResponse } from 'next'; export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { - if (!enablePhoneSms()) { + if (!enableEmailSms()) { throw new Error('SMS is not enabled'); } await filterAccessToken(req, res, ({ userUid }) => diff --git a/frontend/desktop/src/pages/api/auth/email/changeBinding/verifyOld.ts b/frontend/desktop/src/pages/api/auth/email/changeBinding/verifyOld.ts index 0a27b76d207..e2e34b3daf2 100644 --- a/frontend/desktop/src/pages/api/auth/email/changeBinding/verifyOld.ts +++ b/frontend/desktop/src/pages/api/auth/email/changeBinding/verifyOld.ts @@ -3,11 +3,11 @@ import { ErrorHandler } from '@/services/backend/middleware/error'; import { unbindEmailGuard } from '@/services/backend/middleware/oauth'; import { filterEmailVerifyParams, verifyEmailCodeGuard } from '@/services/backend/middleware/sms'; import { jsonRes } from '@/services/backend/response'; -import { enablePhoneSms } from '@/services/enable'; +import { enableEmailSms } from '@/services/enable'; import { NextApiRequest, NextApiResponse } from 'next'; export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { - if (!enablePhoneSms()) { + if (!enableEmailSms()) { throw new Error('SMS is not enabled'); } await filterAccessToken( diff --git a/frontend/desktop/src/pages/api/auth/email/unbind/sms.ts b/frontend/desktop/src/pages/api/auth/email/unbind/sms.ts index dd7650de720..23aec48d60b 100644 --- a/frontend/desktop/src/pages/api/auth/email/unbind/sms.ts +++ b/frontend/desktop/src/pages/api/auth/email/unbind/sms.ts @@ -3,11 +3,11 @@ import { ErrorHandler } from '@/services/backend/middleware/error'; import { unbindEmailGuard } from '@/services/backend/middleware/oauth'; import { filterCf, filterEmailParams, sendEmailCodeGuard } from '@/services/backend/middleware/sms'; import { sendEmailCodeSvc } from '@/services/backend/svc/sms'; -import { enablePhoneSms } from '@/services/enable'; +import { enableEmailSms } from '@/services/enable'; import { NextApiRequest, NextApiResponse } from 'next'; export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { - if (!enablePhoneSms()) { + if (!enableEmailSms()) { throw new Error('SMS is not enabled'); } await filterAccessToken(req, res, ({ userUid }) => diff --git a/frontend/desktop/src/pages/api/auth/email/unbind/verify.ts b/frontend/desktop/src/pages/api/auth/email/unbind/verify.ts index 3ded4697279..e3f84dbf812 100644 --- a/frontend/desktop/src/pages/api/auth/email/unbind/verify.ts +++ b/frontend/desktop/src/pages/api/auth/email/unbind/verify.ts @@ -2,11 +2,11 @@ import { filterAccessToken } from '@/services/backend/middleware/access'; import { ErrorHandler } from '@/services/backend/middleware/error'; import { filterEmailVerifyParams, verifyEmailCodeGuard } from '@/services/backend/middleware/sms'; import { unbindEmailSvc } from '@/services/backend/svc/bindProvider'; -import { enablePhoneSms } from '@/services/enable'; +import { enableEmailSms } from '@/services/enable'; import { NextApiRequest, NextApiResponse } from 'next'; export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { - if (!enablePhoneSms()) { + if (!enableEmailSms()) { throw new Error('SMS is not enabled'); } await filterAccessToken(req, res, ({ userUid }) => diff --git a/frontend/desktop/src/pages/api/auth/info.ts b/frontend/desktop/src/pages/api/auth/info.ts index 3390bf44440..cdb11082c91 100644 --- a/frontend/desktop/src/pages/api/auth/info.ts +++ b/frontend/desktop/src/pages/api/auth/info.ts @@ -6,7 +6,8 @@ import { enableGithub, enableGoogle, enablePassword, - enablePhoneSms + enablePhoneSms, + enableWechat } from '@/services/enable'; import { NextApiRequest, NextApiResponse } from 'next'; import { ProviderType } from 'prisma/global/generated/client'; @@ -89,6 +90,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) return enableEmailSms(); } else if (o.providerType === ProviderType.PASSWORD) { return enablePassword(); + } else if (o.providerType === ProviderType.WECHAT) { + return enableWechat(); } return true; }) diff --git a/frontend/desktop/src/services/enable.ts b/frontend/desktop/src/services/enable.ts index 91f700d9a00..4cd4da93813 100644 --- a/frontend/desktop/src/services/enable.ts +++ b/frontend/desktop/src/services/enable.ts @@ -4,9 +4,11 @@ export const enableEnterpriseRealNameAuth = () => global.AppConfig.common.enterpriseRealNameAuthEnabled || false; export const enablePassword = () => global.AppConfig.desktop.auth.idp.password?.enabled || false; export const enableGithub = () => global.AppConfig.desktop.auth.idp.github?.enabled || false; -export const enablePhoneSms = () => global.AppConfig.desktop.auth.idp.sms?.ali?.enabled || false; export const enableSms = () => global.AppConfig.desktop.auth.idp.sms?.enabled || false; -export const enableEmailSms = () => global.AppConfig.desktop.auth.idp.sms?.email?.enabled || false; +export const enablePhoneSms = () => + enableSms() && !!global.AppConfig.desktop.auth.idp.sms?.ali?.enabled; +export const enableEmailSms = () => + enableSms() && !!global.AppConfig.desktop.auth.idp.sms?.email?.enabled; export const enableWechat = () => global.AppConfig.desktop.auth.idp.wechat?.enabled || false; export const enableGoogle = () => global.AppConfig.desktop.auth.idp.google?.enabled || false; export const enableSignUp = () => global.AppConfig.desktop.auth.signUpEnabled || false; diff --git a/frontend/package.json b/frontend/package.json index 49018a21306..b336edae8b7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,7 +16,8 @@ "dev-workorder": "pnpm -r --filter ./providers/workorder run dev", "gen:theme-typings": "pnpm chakra-cli tokens packages/ui/src/theme/theme.ts --out node_modules/.pnpm/node_modules/@chakra-ui/styled-system/dist/theming.types.d.ts", "postinstall": "pnpm run gen:theme-typings", - "prepare": "cd .. && husky frontend/.husky" + "prepare": "cd .. && husky frontend/.husky", + "build-packages": "pnpm -r --filter ./packages/client-sdk run build" }, "workspaces": [ "./packages/*", diff --git a/frontend/packages/ui/src/components/Menu/index.tsx b/frontend/packages/ui/src/components/Menu/index.tsx index eaad5283d09..d607f1ff834 100644 --- a/frontend/packages/ui/src/components/Menu/index.tsx +++ b/frontend/packages/ui/src/components/Menu/index.tsx @@ -28,6 +28,8 @@ export const SealosMenu = ({ width, Button, menuList }: Props) => { {Button} =12'} dev: false + /@tanstack/virtual-core@3.10.8: + resolution: {integrity: sha512-PBu00mtt95jbKFi6Llk9aik8bnR3tR/oQP1o3TSi+iG//+Q2RTIzCEgKkHG8BB86kxMNW6O8wku+Lmi+QFR6jA==} + dev: false + /@testing-library/dom@9.3.3: resolution: {integrity: sha512-fB0R+fa3AUqbLHWyxXa2kGVtf1Fe1ZZFr0Zp6AIbIAzXb2mKbEXl+PCQNUOaq5lbTab5tfctfXRNsWXxa2f7Aw==} engines: {node: '>=14'} @@ -10371,6 +10398,12 @@ packages: resolution: {integrity: sha512-k7kRA033QNtC+gLc4VPlfnue58CM1iQLgn1IMAU8VPHGOj7oIHPp9UlhedEnD/Gl8evoCjwkZjlBORtZ3JByUA==} dev: true + /@types/papaparse@5.3.15: + resolution: {integrity: sha512-JHe6vF6x/8Z85nCX4yFdDslN11d+1pr12E526X8WAfhadOeaOTx5AuIkvDKIBopfvlzpzkdMx4YyvSKCM9oqtw==} + dependencies: + '@types/node': 20.10.0 + dev: true + /@types/parse-json@4.0.2: resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} @@ -19822,6 +19855,10 @@ packages: resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} dev: false + /papaparse@5.4.1: + resolution: {integrity: sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw==} + dev: false + /parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} diff --git a/frontend/providers/applaunchpad/public/locales/en/common.json b/frontend/providers/applaunchpad/public/locales/en/common.json index bafd64012a5..f29baeaeb26 100644 --- a/frontend/providers/applaunchpad/public/locales/en/common.json +++ b/frontend/providers/applaunchpad/public/locales/en/common.json @@ -3,8 +3,8 @@ "Active": "Active", "Add": "Add", "Add Port": "Add Port", - "Add volume": "Add Storage", - "Advanced Configuration": "Advanced Config", + "Add volume": "Add Volumes", + "Advanced Configuration": "Advanced Configuration", "Age": "Age", "Amount": "Amount", "AnticipatedPrice": "The Estimated Cost", @@ -30,11 +30,11 @@ "Auto scaling": "Scaling", "Balance": "Balance", "Basic Config": "Basic", - "Basic Information": "Basic", + "Basic Information": "Basic Infomation", "Can help you deploy any Docker image": "Rich image warehouse, supporting any Docker image", "Can not change storage path": "Storage mount path cannot be modified", "Cancel": "Cancel", - "capacity": "capacity", + "capacity": "Capacity", "Card": "cards", "Click on any shadow to skip": "Click on any shadow to skip", "Click the Deploy Application button": "Click \\\"Deploy Application\\\"", @@ -72,14 +72,14 @@ "Creation Time": "Created At", "Custom Domain": "Custom Domain", "Custom Domain Error": "Failed to add custom domain. Please verify CNAME configuration.", - "Data cannot be communicated between multiple instances": "Data is not shared between instances", + "Data cannot be communicated between multiple instances": "Data cannot be shared between instances", "Day": "Day", "Delete": "Delete", "Deletion warning": "Deletion warning", "Deploy": "Deploy", "Deploy Application": "Deploy Application", "Deployment Failed": "Deployment Failed", - "Deployment Mode": "Deployment Mode", + "Deployment Mode": "Deployment Infomation", "Deployment Successful": "Deployment succeeded", "Details": "Details", "download": "Download", @@ -107,7 +107,7 @@ "Group": "Group", "grpcs": "gRPCS", "https": "HTTPS", - "If no, the default command is used": "Leave blank to use default command", + "If no, the default command is used": "Leave blank to use the default command", "Image": "Image", "Image Address": "Image Registry", "Image Name": "Image Name", @@ -129,7 +129,7 @@ "Memory": "Memory", "Memory target value": "Target Memory Usage", "Min Storage Value": "Minimum size is", - "mount path": "Mount path", + "mount path": "Mount Path", "Mount Path Auth": "Mount path must match: /^[0-9a-zA-Z_/][0-9a-zA-Z_/.-]*[0-9a-zA-Z_/]$/", "Name": "Name", "Network Configuration": "Network", @@ -272,5 +272,9 @@ "first_charge_tip": "For some specifications, you can enjoy double the gift amount when you recharge for the first time.", "gift": "gift", "balance": "balance", - "guide_deploy_button": "Complete creation and get it now" + "guide_deploy_button": "Complete creation and get it now", + "form": { + "add_configmap": "Add Configmaps", + "storage_path_placeholder": "For Example: /data" + } } diff --git a/frontend/providers/applaunchpad/public/locales/zh/common.json b/frontend/providers/applaunchpad/public/locales/zh/common.json index 56d12456d25..e86de277adf 100644 --- a/frontend/providers/applaunchpad/public/locales/zh/common.json +++ b/frontend/providers/applaunchpad/public/locales/zh/common.json @@ -273,5 +273,9 @@ "first_charge_tip": "部分规格首次充值可享双倍赠送金额", "gift": "赠", "balance": "余额", - "guide_deploy_button": "完成创建,立即获得" + "guide_deploy_button": "完成创建,立即获得", + "form": { + "add_configmap": "新增配置文件", + "storage_path_placeholder": "如:/data" + } } diff --git a/frontend/providers/applaunchpad/src/constants/editApp.ts b/frontend/providers/applaunchpad/src/constants/editApp.ts index 979485e4397..3f904094475 100644 --- a/frontend/providers/applaunchpad/src/constants/editApp.ts +++ b/frontend/providers/applaunchpad/src/constants/editApp.ts @@ -28,8 +28,8 @@ export const defaultEditVal: AppEditType = { runCMD: '', cmdParam: '', replicas: 1, - cpu: 100, - memory: 128, + cpu: 200, + memory: 256, networks: [ { networkName: '', diff --git a/frontend/providers/applaunchpad/src/pages/_app.tsx b/frontend/providers/applaunchpad/src/pages/_app.tsx index 0ed476750a8..d8a4479dfa8 100644 --- a/frontend/providers/applaunchpad/src/pages/_app.tsx +++ b/frontend/providers/applaunchpad/src/pages/_app.tsx @@ -184,7 +184,7 @@ const App = ({ Component, pageProps }: AppProps) => { - {/* */} + diff --git a/frontend/providers/applaunchpad/src/pages/app/edit/components/Form.tsx b/frontend/providers/applaunchpad/src/pages/app/edit/components/Form.tsx index ab45455e271..7b708171e24 100644 --- a/frontend/providers/applaunchpad/src/pages/app/edit/components/Form.tsx +++ b/frontend/providers/applaunchpad/src/pages/app/edit/components/Form.tsx @@ -949,7 +949,8 @@ const Form = ({ {t('Advanced Configuration')}
setConfigEdit({ mountPath: '', value: '' })} leftIcon={} > - {t('Add')} - {t('Configuration File')} + {t('form.add_configmap')} diff --git a/frontend/providers/applaunchpad/src/pages/app/edit/components/StoreModal.tsx b/frontend/providers/applaunchpad/src/pages/app/edit/components/StoreModal.tsx index 53a07a89706..9a26406738f 100644 --- a/frontend/providers/applaunchpad/src/pages/app/edit/components/StoreModal.tsx +++ b/frontend/providers/applaunchpad/src/pages/app/edit/components/StoreModal.tsx @@ -131,7 +131,7 @@ const StoreModal = ({ GET('/api/getDBList').then((data) => data.map(adaptDBListItem)); @@ -111,3 +115,42 @@ export const getOpsRequest = ({ label, dbType }); + +export const getLogFiles = ({ + podName, + dbType, + logType +}: { + podName: string; + dbType: SupportReconfigureDBType; + logType: LogTypeEnum; +}) => + POST(`/api/logs/getFiles`, { + podName, + dbType, + logType + }); + +export const getLogContent = ({ + logPath, + page, + pageSize, + dbType, + logType, + podName +}: { + logPath: string; + page: number; + pageSize: number; + dbType: SupportReconfigureDBType; + logType: LogTypeEnum; + podName: string; +}) => + POST(`/api/logs/get`, { + logPath, + page, + pageSize, + dbType, + logType, + podName + }); diff --git a/frontend/providers/dbprovider/src/components/BaseTable/SwitchPage.tsx b/frontend/providers/dbprovider/src/components/BaseTable/SwitchPage.tsx new file mode 100644 index 00000000000..fe1c44dd4e2 --- /dev/null +++ b/frontend/providers/dbprovider/src/components/BaseTable/SwitchPage.tsx @@ -0,0 +1,158 @@ +import { Button, ButtonProps, Flex, FlexProps, Text } from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; + +import { Icon, IconProps } from '@chakra-ui/react'; + +export function ToLeftIcon(props: IconProps) { + return ( + + + + ); +} + +export function RightFirstIcon(props: IconProps) { + return ( + + + + ); +} + +export function SwitchPage({ + totalPage, + totalItem, + pageSize, + currentPage, + setCurrentPage, + isPreviousData, + ...props +}: { + currentPage: number; + totalPage: number; + totalItem: number; + pageSize: number; + isPreviousData?: boolean; + setCurrentPage: (idx: number) => void; +} & FlexProps) { + const { t } = useTranslation(); + const switchStyle: ButtonProps = { + width: '24px', + height: '24px', + minW: '0', + background: 'grayModern.250', + flexGrow: '0', + borderRadius: 'full', + // variant:'unstyled', + _hover: { + background: 'grayModern.150', + minW: '0' + }, + _disabled: { + borderRadius: 'full', + background: 'grayModern.150', + cursor: 'not-allowed', + minW: '0' + } + }; + return ( + + + {t('Total')}: + + + {totalItem} + + + + + {currentPage} + / + {totalPage} + + + + + {pageSize} + + + /{t('Page')} + + + ); +} diff --git a/frontend/providers/dbprovider/src/components/BaseTable/baseTable.tsx b/frontend/providers/dbprovider/src/components/BaseTable/baseTable.tsx new file mode 100644 index 00000000000..fc3832a73f4 --- /dev/null +++ b/frontend/providers/dbprovider/src/components/BaseTable/baseTable.tsx @@ -0,0 +1,76 @@ +import { + Spinner, + Table, + TableContainer, + TableContainerProps, + Tbody, + Td, + Th, + Thead, + Tr +} from '@chakra-ui/react'; +import { Table as ReactTable, flexRender } from '@tanstack/react-table'; + +export function BaseTable({ + table, + isLoading, + ...props +}: { table: ReactTable; isLoading: boolean } & TableContainerProps) { + return ( + + + + {table.getHeaderGroups().map((headers) => { + return ( + + {headers.headers.map((header, i) => { + return ( + + ); + })} + + ); + })} + + + {isLoading ? ( + + + + ) : ( + table.getRowModel().rows.map((item) => { + return ( + + {item.getAllCells().map((cell, i) => { + return ( + + ); + })} + + ); + }) + )} + +
+ {flexRender(header.column.columnDef.header, header.getContext())} +
+ +
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+
+ ); +} diff --git a/frontend/providers/dbprovider/src/components/Icon/icons/backup.svg b/frontend/providers/dbprovider/src/components/Icon/icons/backup.svg new file mode 100644 index 00000000000..6bc2be8a53e --- /dev/null +++ b/frontend/providers/dbprovider/src/components/Icon/icons/backup.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/providers/dbprovider/src/components/Icon/icons/config.svg b/frontend/providers/dbprovider/src/components/Icon/icons/config.svg new file mode 100644 index 00000000000..aff15fbd251 --- /dev/null +++ b/frontend/providers/dbprovider/src/components/Icon/icons/config.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/providers/dbprovider/src/components/Icon/icons/file.svg b/frontend/providers/dbprovider/src/components/Icon/icons/file.svg new file mode 100644 index 00000000000..e24054c2dfc --- /dev/null +++ b/frontend/providers/dbprovider/src/components/Icon/icons/file.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/providers/dbprovider/src/components/Icon/icons/import.svg b/frontend/providers/dbprovider/src/components/Icon/icons/import.svg new file mode 100644 index 00000000000..d57c59dccd2 --- /dev/null +++ b/frontend/providers/dbprovider/src/components/Icon/icons/import.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/frontend/providers/dbprovider/src/components/Icon/icons/instance.svg b/frontend/providers/dbprovider/src/components/Icon/icons/instance.svg new file mode 100644 index 00000000000..864e27c4cc4 --- /dev/null +++ b/frontend/providers/dbprovider/src/components/Icon/icons/instance.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/frontend/providers/dbprovider/src/components/Icon/icons/monitor.svg b/frontend/providers/dbprovider/src/components/Icon/icons/monitor.svg new file mode 100644 index 00000000000..185a68a8b10 --- /dev/null +++ b/frontend/providers/dbprovider/src/components/Icon/icons/monitor.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/providers/dbprovider/src/components/Icon/icons/time.svg b/frontend/providers/dbprovider/src/components/Icon/icons/time.svg new file mode 100644 index 00000000000..061580378a0 --- /dev/null +++ b/frontend/providers/dbprovider/src/components/Icon/icons/time.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/providers/dbprovider/src/components/Icon/index.tsx b/frontend/providers/dbprovider/src/components/Icon/index.tsx index 1ceaa31a029..3e03b06897d 100644 --- a/frontend/providers/dbprovider/src/components/Icon/index.tsx +++ b/frontend/providers/dbprovider/src/components/Icon/index.tsx @@ -46,7 +46,14 @@ const map = { upload: require('./icons/upload.svg').default, target: require('./icons/target.svg').default, gift: require('./icons/gift.svg').default, + time: require('./icons/time.svg').default, help: require('./icons/help.svg').default, + backup: require('./icons/backup.svg').default, + instance: require('./icons/instance.svg').default, + import: require('./icons/import.svg').default, + file: require('./icons/file.svg').default, + config: require('./icons/config.svg').default, + monitor: require('./icons/monitor.svg').default, arrowDown: require('./icons/arrowDown.svg').default, docs: require('./icons/docs.svg').default }; diff --git a/frontend/providers/dbprovider/src/constants/log.ts b/frontend/providers/dbprovider/src/constants/log.ts new file mode 100644 index 00000000000..1a8f694d855 --- /dev/null +++ b/frontend/providers/dbprovider/src/constants/log.ts @@ -0,0 +1,72 @@ +import { SupportReconfigureDBType } from '@/types/db'; +import { TFile } from '@/utils/kubeFileSystem'; + +export enum LogTypeEnum { + RuntimeLog = 'runtimeLog', + SlowQuery = 'slowQuery', + ErrorLog = 'errorLog' +} + +export type LogConfig = { + path: string; + containerNames: string[]; + filter: (files: TFile[]) => TFile[]; +}; + +export type LoggingConfiguration = { + [LogTypeEnum.RuntimeLog]?: LogConfig; + [LogTypeEnum.SlowQuery]?: LogConfig; + [LogTypeEnum.ErrorLog]?: LogConfig; +}; + +export const ServiceLogConfigs: Record = { + redis: { + [LogTypeEnum.RuntimeLog]: { + path: '/data/running.log', + containerNames: ['redis', 'lorry'], + filter: (files: TFile[]) => + files + .filter((f) => f.size > 0 && f.name.toLowerCase().endsWith('.log')) + .sort((a, b) => b.updateTime.getTime() - a.updateTime.getTime()) + } + }, + postgresql: { + [LogTypeEnum.RuntimeLog]: { + path: '/home/postgres/pgdata/pgroot/pg_log', + containerNames: ['postgresql', 'lorry'], + filter: (files: TFile[]) => + files + .filter((f) => f.size > 0 && f.name.toLowerCase().endsWith('.csv')) + .sort((a, b) => b.updateTime.getTime() - a.updateTime.getTime()) + } + }, + mongodb: { + [LogTypeEnum.RuntimeLog]: { + path: '/data/mongodb/mongodb.log', + containerNames: ['mongodb'], + filter: (files: TFile[]) => { + return files; + } + } + }, + 'apecloud-mysql': { + [LogTypeEnum.ErrorLog]: { + path: '/data/mysql/log', + containerNames: ['mysql', 'lorry'], + filter: (files: TFile[]) => + files + .filter((f) => f.size > 0 && f.name.toLowerCase().includes('mysqld-error')) + .sort((a, b) => b.updateTime.getTime() - a.updateTime.getTime()) + }, + [LogTypeEnum.SlowQuery]: { + path: '/data/mysql/log', + containerNames: ['mysql', 'lorry'], + filter: (files: TFile[]) => { + console.log('slow query files:', files); + return files + .filter((f) => f.size > 1024 && f.name.toLowerCase().includes('slow-query')) + .sort((a, b) => b.updateTime.getTime() - a.updateTime.getTime()); + } + } + } +}; diff --git a/frontend/providers/dbprovider/src/pages/api/getEnv.ts b/frontend/providers/dbprovider/src/pages/api/getEnv.ts index f7efd0c6ec5..02701531213 100644 --- a/frontend/providers/dbprovider/src/pages/api/getEnv.ts +++ b/frontend/providers/dbprovider/src/pages/api/getEnv.ts @@ -12,6 +12,14 @@ export type SystemEnvResponse = { SHOW_DOCUMENT: boolean; }; +process.on('unhandledRejection', (reason, promise) => { + console.error(`Caught unhandledRejection:`, reason, promise); +}); + +process.on('uncaughtException', (err) => { + console.error(`Caught uncaughtException:`, err); +}); + export default async function handler(req: NextApiRequest, res: NextApiResponse) { jsonRes(res, { data: { diff --git a/frontend/providers/dbprovider/src/pages/api/guide/getBonus.ts b/frontend/providers/dbprovider/src/pages/api/guide/getBonus.ts index c52a23b5700..bc75fd355e6 100644 --- a/frontend/providers/dbprovider/src/pages/api/guide/getBonus.ts +++ b/frontend/providers/dbprovider/src/pages/api/guide/getBonus.ts @@ -32,7 +32,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< }; } = await response.json(); - const rechargeOptions = Object.entries(result.discount.firstRechargeDiscount).map( + const rechargeOptions = Object.entries(result?.discount?.firstRechargeDiscount ?? {}).map( ([amount, rate]) => ({ amount: Number(amount), gift: Math.floor((Number(amount) * Number(rate)) / 100) @@ -44,10 +44,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< data: rechargeOptions }); } catch (err: any) { - console.log(err); jsonRes(res, { code: 500, - error: err + error: '/api/guide/getBonus error' }); } } diff --git a/frontend/providers/dbprovider/src/pages/api/logs/get.ts b/frontend/providers/dbprovider/src/pages/api/logs/get.ts new file mode 100644 index 00000000000..3e557396c51 --- /dev/null +++ b/frontend/providers/dbprovider/src/pages/api/logs/get.ts @@ -0,0 +1,65 @@ +import { DBTypeEnum } from '@/constants/db'; +import { ServiceLogConfigs } from '@/constants/log'; +import { authSession } from '@/services/backend/auth'; +import { getK8s } from '@/services/backend/kubernetes'; +import { jsonRes } from '@/services/backend/response'; +import { ApiResp } from '@/services/kubernet'; +import { SupportReconfigureDBType } from '@/types/db'; +import { LogTypeEnum } from '@/constants/log'; +import { DatabaseLogService } from '@/utils/logParsers/LogParser'; +import type { NextApiRequest, NextApiResponse } from 'next'; + +export default async function handsler(req: NextApiRequest, res: NextApiResponse) { + try { + const { namespace, k8sExec, k8sCore } = await getK8s({ + kubeconfig: await authSession(req) + }); + + const { + podName, + dbType, + logType, + logPath, + page = 1, + pageSize = 100 + } = req.body as { + podName: string; + dbType: SupportReconfigureDBType; + logType: LogTypeEnum; + logPath: string; + page?: number; + pageSize?: number; + }; + + if (!podName || !dbType || !logType || !logPath) { + throw new Error('Missing required parameters: podName, dbType, logType or logPath'); + } + + const logConfig = ServiceLogConfigs[dbType][logType]; + + if (!logConfig) { + throw new Error('Invalid log type'); + } + + const logService = new DatabaseLogService(k8sExec, k8sCore, namespace); + + const result = await logService.readLogs({ + podName, + containerNames: logConfig.containerNames, + logPath, + page, + pageSize, + dbType: dbType as DBTypeEnum, + logType + }); + + console.log(result.metadata, 'result'); + + jsonRes(res, { data: result }); + } catch (err: any) { + jsonRes(res, { + code: 500, + error: err + }); + } +} diff --git a/frontend/providers/dbprovider/src/pages/api/logs/getFiles.ts b/frontend/providers/dbprovider/src/pages/api/logs/getFiles.ts new file mode 100644 index 00000000000..b480ab6ae73 --- /dev/null +++ b/frontend/providers/dbprovider/src/pages/api/logs/getFiles.ts @@ -0,0 +1,75 @@ +import { ServiceLogConfigs } from '@/constants/log'; +import { authSession } from '@/services/backend/auth'; +import { getK8s } from '@/services/backend/kubernetes'; +import { jsonRes } from '@/services/backend/response'; +import { ApiResp } from '@/services/kubernet'; +import { SupportReconfigureDBType } from '@/types/db'; +import { LogTypeEnum } from '@/constants/log'; +import { KubeFileSystem } from '@/utils/kubeFileSystem'; +import type { NextApiRequest, NextApiResponse } from 'next'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + const { namespace, k8sExec } = await getK8s({ + kubeconfig: await authSession(req) + }); + + const { podName, dbType, logType } = req.body as { + podName: string; + dbType: SupportReconfigureDBType; + logType: LogTypeEnum; + }; + + if (!podName || !dbType) { + throw new Error('Missing required parameters: podName, containerName or logPath'); + } + + const kubefs = new KubeFileSystem(k8sExec); + + const logConfig = ServiceLogConfigs[dbType][logType]; + + console.log('/api/logs/getFiles', { podName, dbType, logType, logConfig }); + + if (!logConfig) { + throw new Error('Invalid log type'); + } + + let files, directories; + let lastError: any; + for (const container of logConfig.containerNames) { + try { + const result = await kubefs.ls({ + namespace, + podName, + containerName: container, + path: logConfig.path, + showHidden: false + }); + files = result.files; + directories = result.directories; + break; // 成功后退出循环 + } catch (error) { + lastError = error; + console.error('/api/logs/getFiles error', error); + continue; + } + } + + if (!files) { + throw new Error(lastError?.message || 'No valid log files found in any container'); + } + + const validFiles = logConfig.filter(files); + + if (!validFiles || validFiles.length === 0) { + throw new Error('No valid log files found'); + } + + jsonRes(res, { data: validFiles }); + } catch (err: any) { + jsonRes(res, { + code: 500, + error: err + }); + } +} diff --git a/frontend/providers/dbprovider/src/pages/db/detail/components/AppBaseInfo.tsx b/frontend/providers/dbprovider/src/pages/db/detail/components/AppBaseInfo.tsx index 3dd01fb4f25..5061fd41595 100644 --- a/frontend/providers/dbprovider/src/pages/db/detail/components/AppBaseInfo.tsx +++ b/frontend/providers/dbprovider/src/pages/db/detail/components/AppBaseInfo.tsx @@ -54,6 +54,7 @@ const AppBaseInfo = ({ db = defaultDBDetail }: { db: DBDetailType }) => { ['getDBStatefulSetByName', db.dbName, db.dbType], () => getDBStatefulSetByName(db.dbName, db.dbType), { + retry: 2, enabled: !!db.dbName && !!db.dbType } ); @@ -73,6 +74,7 @@ const AppBaseInfo = ({ db = defaultDBDetail }: { db: DBDetailType }) => { enabled: supportConnectDB, retry: 3, onSuccess(data) { + console.log(data, !!data, 'service'); setIsChecked(!!data); }, onError(error) { @@ -147,7 +149,7 @@ const AppBaseInfo = ({ db = defaultDBDetail }: { db: DBDetailType }) => { [DBTypeEnum.postgresql]: `psql '${secret.connection}'`, [DBTypeEnum.mongodb]: `mongosh '${secret.connection}'`, [DBTypeEnum.mysql]: `mysql -h ${secret.host} -P ${secret.port} -u ${secret.username} -p${secret.password}`, - [DBTypeEnum.redis]: `redis-cli -h ${secret.host} -p ${secret.port}`, + [DBTypeEnum.redis]: `redis-cli -u redis://${secret.username}:${secret.password}@${secret.host}:${secret.port}`, [DBTypeEnum.kafka]: ``, [DBTypeEnum.qdrant]: ``, [DBTypeEnum.nebula]: ``, @@ -168,6 +170,7 @@ const AppBaseInfo = ({ db = defaultDBDetail }: { db: DBDetailType }) => { const openNetWorkService = async () => { try { + console.log('openNetWorkService', dbStatefulSet, db); if (!dbStatefulSet || !db) { return toast({ title: 'Missing Parameters', @@ -175,7 +178,6 @@ const AppBaseInfo = ({ db = defaultDBDetail }: { db: DBDetailType }) => { }); } const yaml = json2NetworkService({ dbDetail: db, dbStatefulSet: dbStatefulSet }); - console.log(yaml); await applyYamlList([yaml], 'create'); onClose(); setIsChecked(true); diff --git a/frontend/providers/dbprovider/src/pages/db/detail/components/ErrorLog/RunTimeLog.tsx b/frontend/providers/dbprovider/src/pages/db/detail/components/ErrorLog/RunTimeLog.tsx new file mode 100644 index 00000000000..ae191a09c69 --- /dev/null +++ b/frontend/providers/dbprovider/src/pages/db/detail/components/ErrorLog/RunTimeLog.tsx @@ -0,0 +1,295 @@ +import { getLogContent, getLogFiles } from '@/api/db'; +import { BaseTable } from '@/components/BaseTable/baseTable'; +import { SwitchPage } from '@/components/BaseTable/SwitchPage'; +import MyIcon from '@/components/Icon'; +import { useDBStore } from '@/store/db'; +import { DBDetailType, SupportReconfigureDBType } from '@/types/db'; +import { LogTypeEnum } from '@/constants/log'; +import { TFile } from '@/utils/kubeFileSystem'; +import { formatTime } from '@/utils/tools'; +import { ChevronDownIcon } from '@chakra-ui/icons'; +import { + Box, + Button, + Flex, + MenuButton, + Input, + InputGroup, + InputLeftElement +} from '@chakra-ui/react'; +import { SealosMenu } from '@sealos/ui'; +import { useQuery } from '@tanstack/react-query'; +import { + ColumnDef, + getCoreRowModel, + getFilteredRowModel, + useReactTable +} from '@tanstack/react-table'; +import { useTranslation } from 'next-i18next'; +import { useMemo, useState } from 'react'; +import { I18nCommonKey } from '@/types/i18next'; + +type LogContent = { + timestamp: string; + content: string; +}; + +const getEmptyLogResult = (page = 0, pageSize = 0) => ({ + logs: [] as LogContent[], + metadata: { + total: 0, + page, + pageSize, + processingTime: '', + hasMore: false + } +}); + +export default function RunTimeLog({ + db, + logType, + filteredSubNavList, + updateSubMenu +}: { + db: DBDetailType; + logType: LogTypeEnum; + updateSubMenu: (value: LogTypeEnum) => void; + filteredSubNavList?: { + label: string; + value: LogTypeEnum; + }[]; +}) { + const { t } = useTranslation(); + const { intervalLoadPods, dbPods } = useDBStore(); + const [podName, setPodName] = useState(''); + const [logFile, setLogFile] = useState(); + const [data, setData] = useState([]); + + const [page, setPage] = useState(1); + const [pageSize, setPageSize] = useState(100); + + const [globalFilter, setGlobalFilter] = useState(''); + + useQuery(['intervalLoadPods', db?.dbName], () => db?.dbName && intervalLoadPods(db?.dbName), { + onSuccess: () => { + !podName && setPodName(dbPods[0]?.podName); + } + }); + + const { data: logFiles = [] } = useQuery( + ['getLogFiles', podName, db?.dbType], + async () => { + if (!podName || !db?.dbType) return []; + return await getLogFiles({ + podName, + dbType: db.dbType as SupportReconfigureDBType, + logType + }); + }, + { + enabled: !!podName && db?.dbType !== 'mongodb', + onSuccess: (data) => { + !logFile && setLogFile(data[0]); + } + } + ); + + const { data: logData, isLoading } = useQuery( + ['getLogContent', logFile?.path, podName, db?.dbType, page, pageSize], + async () => { + if (!podName || !db?.dbType) return getEmptyLogResult(); + + const params = { + page, + pageSize, + podName, + dbType: db.dbType as SupportReconfigureDBType, + logType, + logPath: 'default' + } as const; + + if (db.dbType === 'mongodb') { + return await getLogContent(params); + } + + if (!logFile?.path) { + return getEmptyLogResult(); + } + + return await getLogContent({ ...params, logPath: logFile.path }); + }, + { + onSuccess(data) { + setData(data.logs); + } + } + ); + + const columns = useMemo>>( + () => [ + { + accessorKey: 'timestamp', + cell: ({ row }) => { + return ( + + {formatTime(row.original.timestamp, 'YYYY-MM-DD HH:mm:ss.SSS')} + + ); + }, + header: () => { + return ( + + {t('error_log.collection_time')} + + ); + } + }, + { + accessorKey: 'content', + header: () => { + return ( + + {t('error_log.content')} + + ); + }, + cell: ({ row }) => { + return ( + + {row.original.content} + + ); + } + } + ], + [] + ); + + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + state: { + globalFilter + }, + onGlobalFilterChange: setGlobalFilter, + globalFilterFn: (row, columnId, filterValue) => { + const timestamp = formatTime(row.original.timestamp, 'YYYY-MM-DD HH:mm:ss.SSS') + .toLowerCase() + .includes(filterValue.toLowerCase()); + const content = row.original.content.toLowerCase().includes(filterValue.toLowerCase()); + return timestamp || content; + } + }); + + return ( + + + {filteredSubNavList?.map((item) => ( + item.value !== logType && updateSubMenu(item.value)} + > + {t(item.label as I18nCommonKey)} + + ))} + + } + w={'200px'} + h={'32px'} + textAlign={'start'} + bg={'grayModern.100'} + borderRadius={'md'} + border={'1px solid #E8EBF0'} + > + + + {podName} + + + + + } + menuList={dbPods.map((item) => ({ + isActive: item.podName === podName, + child: {item.podName}, + onClick: () => setPodName(item.podName) + }))} + /> + + {db?.dbType !== 'mongodb' && ( + } + w={'200px'} + h={'32px'} + textAlign={'start'} + bg={'grayModern.100'} + borderRadius={'md'} + border={'1px solid #E8EBF0'} + > + + + {logFile?.name} + + + + + } + menuList={logFiles.map((item) => ({ + isActive: item.name === logFile?.name, + child: {item.name}, + onClick: () => setLogFile(item) + }))} + /> + )} + + + + + + table.setGlobalFilter(e.target.value)} + /> + + + + setPage(idx)} + /> + + ); +} diff --git a/frontend/providers/dbprovider/src/pages/db/detail/components/ErrorLog/index.tsx b/frontend/providers/dbprovider/src/pages/db/detail/components/ErrorLog/index.tsx new file mode 100644 index 00000000000..6e397d46174 --- /dev/null +++ b/frontend/providers/dbprovider/src/pages/db/detail/components/ErrorLog/index.tsx @@ -0,0 +1,90 @@ +import { LogTypeEnum } from '@/constants/log'; +import { DBDetailType, SupportReconfigureDBType } from '@/types/db'; +import { I18nCommonKey } from '@/types/i18next'; +import { Box, Flex } from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; +import { useRouter } from 'next/router'; +import React, { ForwardedRef, forwardRef, useEffect, useMemo, useState } from 'react'; +import RunTimeLog from './RunTimeLog'; + +export type ComponentRef = { + openBackup: () => void; +}; + +const DB_LOG_TYPES: Record = { + postgresql: [LogTypeEnum.RuntimeLog], + mongodb: [LogTypeEnum.RuntimeLog], + 'apecloud-mysql': [LogTypeEnum.ErrorLog, LogTypeEnum.SlowQuery], + redis: [LogTypeEnum.RuntimeLog] +}; + +const ErrorLog = ({ db }: { db?: DBDetailType }, ref: ForwardedRef) => { + if (!db) return <>; + + const { t } = useTranslation(); + + const router = useRouter(); + const [subMenu, setSubMenu] = useState(LogTypeEnum.RuntimeLog); + + const parsedSubMenu = useMemo(() => { + const parseSubMenu = (subMenu: string): LogTypeEnum => { + if (Object.values(LogTypeEnum).includes(subMenu as LogTypeEnum)) { + return subMenu as LogTypeEnum; + } + + const dbType = db?.dbType as SupportReconfigureDBType; + const availableMenus = DB_LOG_TYPES[dbType] || []; + + if (availableMenus.includes(LogTypeEnum.ErrorLog)) { + return LogTypeEnum.ErrorLog; + } + + return LogTypeEnum.RuntimeLog; + }; + + return parseSubMenu(router.query.subMenu as string); + }, [router.query.subMenu, db?.dbType]); + + useEffect(() => { + setSubMenu(parsedSubMenu); + }, [parsedSubMenu]); + + const updateSubMenu = (newSubMenu: LogTypeEnum) => { + setSubMenu(newSubMenu); + router.push({ + query: { ...router.query, subMenu: newSubMenu } + }); + }; + + const { filteredSubNavList } = useMemo(() => { + const SubNavList = [ + { label: t('error_log.runtime_log'), value: LogTypeEnum.RuntimeLog }, + { label: t('error_log.error_log'), value: LogTypeEnum.ErrorLog }, + { label: t('error_log.slow_query'), value: LogTypeEnum.SlowQuery } + ]; + + const availableSubMenus = DB_LOG_TYPES[db.dbType as SupportReconfigureDBType] || []; + const filteredSubNavList = SubNavList.filter((item) => availableSubMenus.includes(item.value)); + + return { + availableSubMenus, + filteredSubNavList + }; + }, [t, db.dbType]); + + return ( + + {db && ( + + )} + + ); +}; + +export default React.memo(forwardRef(ErrorLog)); diff --git a/frontend/providers/dbprovider/src/pages/db/detail/index.tsx b/frontend/providers/dbprovider/src/pages/db/detail/index.tsx index d04336682ea..ff545e2f854 100644 --- a/frontend/providers/dbprovider/src/pages/db/detail/index.tsx +++ b/frontend/providers/dbprovider/src/pages/db/detail/index.tsx @@ -20,6 +20,8 @@ import Pods from './components/Pods'; import { I18nCommonKey } from '@/types/i18next'; import ReconfigureTable from './components/Reconfigure/index'; import useDetailDriver from '@/hooks/useDetailDriver'; +import ErrorLog from '@/pages/db/detail/components/ErrorLog'; +import MyIcon from '@/components/Icon'; enum TabEnum { pod = 'pod', @@ -27,7 +29,8 @@ enum TabEnum { monitor = 'monitor', InternetMigration = 'InternetMigration', DumpImport = 'DumpImport', - Reconfigure = 'reconfigure' + Reconfigure = 'reconfigure', + ErrorLog = 'errorLog' } const AppDetail = ({ @@ -54,13 +57,60 @@ const AppDetail = ({ SystemEnv.BACKUP_ENABLED; const listNavValue = [ - { label: 'monitor_list', value: TabEnum.monitor }, - { label: 'replicas_list', value: TabEnum.pod }, - ...(PublicNetMigration ? [{ label: 'dbconfig.parameter', value: TabEnum.Reconfigure }] : []), - ...(BackupSupported ? [{ label: 'backup_list', value: TabEnum.backup }] : []), - ...(PublicNetMigration ? [{ label: 'online_import', value: TabEnum.InternetMigration }] : []), + { + label: 'monitor_list', + value: TabEnum.monitor, + icon: + }, + { + label: 'replicas_list', + value: TabEnum.pod, + icon: + }, + ...(PublicNetMigration + ? [ + { + label: 'dbconfig.parameter', + value: TabEnum.Reconfigure, + icon: + } + ] + : []), + ...(BackupSupported + ? [ + { + label: 'backup_list', + value: TabEnum.backup, + icon: + } + ] + : []), + ...(PublicNetMigration + ? [ + { + label: 'online_import', + value: TabEnum.InternetMigration, + icon: + } + ] + : []), ...(PublicNetMigration && !!SystemEnv.minio_url - ? [{ label: 'import_through_file', value: TabEnum.DumpImport }] + ? [ + { + label: 'import_through_file', + value: TabEnum.DumpImport, + icon: + } + ] + : []), + ...(BackupSupported + ? [ + { + label: 'error_log.analysis', + value: TabEnum.ErrorLog, + icon: + } + ] : []) ]; @@ -80,7 +130,7 @@ const AppDetail = ({ const { dbDetail, loadDBDetail, dbPods } = useDBStore(); const [showSlider, setShowSlider] = useState(false); - useQuery([dbName, 'loadDBDetail', 'intervalLoadPods'], () => loadDBDetail(dbName), { + useQuery(['loadDBDetail', 'intervalLoadPods', dbName], () => loadDBDetail(dbName), { refetchInterval: 3000, onError(err) { router.replace('/dbs'); @@ -128,9 +178,11 @@ const AppDetail = ({ border={theme.borders.base} borderRadius={'lg'} > - + {listNav.map((item) => ( - + {item.icon} + {t(item.label as I18nCommonKey)} - + ))} {listType === TabEnum.pod && {dbPods.length} Items} @@ -196,6 +250,7 @@ const AppDetail = ({ {listType === TabEnum.Reconfigure && ( )} + {listType === TabEnum.ErrorLog && } diff --git a/frontend/providers/dbprovider/src/services/backend/kubernetes.ts b/frontend/providers/dbprovider/src/services/backend/kubernetes.ts index f7d54fbe142..9d055e34d56 100644 --- a/frontend/providers/dbprovider/src/services/backend/kubernetes.ts +++ b/frontend/providers/dbprovider/src/services/backend/kubernetes.ts @@ -331,6 +331,7 @@ export async function getK8s({ kubeconfig }: { kubeconfig: string }) { applyYamlList, delYamlList, getUserQuota: () => getUserQuota(kc, namespace), - getUserBalance: () => getUserBalance(kc) + getUserBalance: () => getUserBalance(kc), + k8sExec: new k8s.Exec(kc) }); } diff --git a/frontend/providers/dbprovider/src/types/log.ts b/frontend/providers/dbprovider/src/types/log.ts new file mode 100644 index 00000000000..d2bb696fcd2 --- /dev/null +++ b/frontend/providers/dbprovider/src/types/log.ts @@ -0,0 +1,49 @@ +import { DBTypeEnum } from '@/constants/db'; +import { LogTypeEnum } from '@/constants/log'; + +export interface BaseLogEntry { + timestamp: string; + level: string; + content: string; +} + +export interface PgLogEntry extends BaseLogEntry {} + +export interface MysqlLogEntry extends BaseLogEntry {} + +export interface RedisLogEntry extends BaseLogEntry { + processId: string; + role: string; +} + +export interface MongoLogEntry extends BaseLogEntry { + component: string; + context: string; + connectionId?: string; +} + +export interface LogResult { + logs: BaseLogEntry[]; + metadata: { + total: number; + page: number; + pageSize: number; + processingTime: string; + hasMore: boolean; + }; +} + +export type LogParserParams = { + podName: string; + containerNames: string[]; + logPath: string; + page: number; + pageSize: number; + dbType: DBTypeEnum; + keyword?: string; + logType?: LogTypeEnum; +}; + +export interface ILogParser { + readLogs(params: LogParserParams): Promise; +} diff --git a/frontend/providers/dbprovider/src/utils/kubeFileSystem.ts b/frontend/providers/dbprovider/src/utils/kubeFileSystem.ts new file mode 100644 index 00000000000..fa4fddd3213 --- /dev/null +++ b/frontend/providers/dbprovider/src/utils/kubeFileSystem.ts @@ -0,0 +1,424 @@ +import { PassThrough, Readable, Writable } from 'stream'; +import * as k8s from '@kubernetes/client-node'; +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; +import timezone from 'dayjs/plugin/timezone'; + +dayjs.extend(utc); +dayjs.extend(timezone); + +export type TFile = { + name: string; + path: string; + dir: string; + kind: string; + attr: string; + hardLinks: number; + owner: string; + group: string; + size: number; + updateTime: Date; + linkTo?: string; + processed?: boolean; +}; + +export class KubeFileSystem { + private readonly k8sExec: k8s.Exec; + + constructor(k8sExec: k8s.Exec) { + this.k8sExec = k8sExec; + } + + async execCommand( + namespace: string, + podName: string, + containerName: string, + command: string[], + stdin: Readable | null = null, + stdout: Writable | null = null + ): Promise { + const stderr = new PassThrough(); + let chunks = Buffer.alloc(0); + + if (!stdout) { + stdout = new PassThrough(); + stdout.on('data', (chunk) => { + chunks = Buffer.concat([chunks, chunk]); + }); + } + + const free = () => { + stderr.removeAllListeners(); + stdout?.removeAllListeners(); + stdin?.removeAllListeners(); + }; + + try { + const ws = await this.k8sExec.exec( + namespace, + podName, + containerName, + command, + stdout, + stderr, + stdin, + false + ); + + return await new Promise((resolve, reject) => { + // Add WebSocket error handling + ws?.on('error', (error: any) => { + free(); + const errorMessage = error?.message || error?.toString() || 'Unknown error'; + reject(new Error(`WebSocket error: ${errorMessage}`)); + }); + + stderr?.on('data', (error) => { + free(); + reject(new Error(`Command execution error: ${error.toString()}`)); + }); + + stdout?.on('end', () => { + free(); + resolve(chunks.toString()); + }); + + stdout?.on('error', (error) => { + free(); + reject(new Error(`Output stream error: ${error.message}`)); + }); + + if (stdin) { + stdin.on('end', () => { + free(); + }); + } + }).catch((error) => { + // Ensure all Promise-related errors are caught + free(); + throw error; + }); + } catch (error: any) { + free(); + if (error?.type === 'error' && error?.target instanceof WebSocket) { + throw new Error(`WebSocket error: ${error.message || 'Unknown error'}`); + } + throw new Error(`Command execution failed: ${error.message || 'Unknown error'}`); + } + } + + async getPodTimezone(namespace: string, podName: string, containerName: string): Promise { + try { + const dateOutput = await this.execCommand(namespace, podName, containerName, ['date', '+%z']); + const offset = dateOutput.trim(); + if (offset) { + return this.getTimezoneFromOffset(offset); + } + } catch (error) { + console.log('Failed to get timezone offset using date command:', error); + } + + try { + const timezoneFile = await this.execCommand(namespace, podName, containerName, [ + 'cat', + '/etc/timezone' + ]); + if (timezoneFile.trim()) { + return timezoneFile.trim(); + } + } catch (error) {} + + try { + const localtimeLink = await this.execCommand(namespace, podName, containerName, [ + 'readlink', + '-f', + '/etc/localtime' + ]); + const match = localtimeLink.match(/zoneinfo\/(.+)$/); + if (match) { + return match[1]; + } + } catch (error) {} + + return 'Etc/UTC'; + } + + async ls({ + namespace, + podName, + containerName, + path, + showHidden = false + }: { + namespace: string; + podName: string; + containerName: string; + path: string; + showHidden: boolean; + }) { + let output: string; + let isCompatibleMode = false; + try { + output = await this.execCommand(namespace, podName, containerName, [ + 'ls', + showHidden ? '-laQ' : '-lQ', + '--color=never', + '--full-time', + path + ]); + } catch (error) { + if (typeof error === 'string' && error.includes('ls: unrecognized option: full-time')) { + isCompatibleMode = true; + output = await this.execCommand(namespace, podName, containerName, [ + 'ls', + showHidden ? '-laQ' : '-lQ', + '--color=never', + '-e', + path + ]); + } else { + throw error; + } + } + const lines: string[] = output.split('\n').filter((v) => v.length > 3); + + const directories: TFile[] = []; + const files: TFile[] = []; + const symlinks: TFile[] = []; + + const podTimezone = await this.getPodTimezone(namespace, podName, containerName); + + lines.forEach((line) => { + const parts = line.split('"'); + const name = parts[1]; + + if (!name || name === '.' || name === '..') return; + + const attrs = parts[0].split(' ').filter((v) => !!v); + + const file: TFile = { + name: name, + path: (name.startsWith('/') ? '' : path + '/') + name, + dir: path, + kind: line[0], + attr: attrs[0], + hardLinks: parseInt(attrs[1]), + owner: attrs[2], + group: attrs[3], + size: parseInt(attrs[4]), + updateTime: this.convertToUTC(attrs.slice(5, 7).join(' '), podTimezone) + }; + + if (isCompatibleMode) { + file.updateTime = this.convertToUTC(attrs.slice(5, 10).join(' '), podTimezone); + } + + if (file.kind === 'c') { + if (isCompatibleMode) { + file.updateTime = this.convertToUTC(attrs.slice(7, 11).join(' '), podTimezone); + } else { + file.updateTime = this.convertToUTC(attrs.slice(6, 8).join(' '), podTimezone); + } + file.size = parseInt(attrs[5]); + } + if (file.kind === 'l') { + file.linkTo = parts[3]; + symlinks.push(file); + } + if (file.kind === 'd') { + directories.push(file); + } else { + if (file.kind !== 't' && file.kind !== '') { + files.push(file); + } + } + }); + + if (symlinks.length > 0) { + const command = ['ls', '-ldQ', '--color=never']; + + try { + symlinks.forEach((symlink) => { + let linkTo = symlink.linkTo!; + if (linkTo[0] !== '/') { + linkTo = (symlink.dir === '/' ? '' : symlink.dir) + '/' + linkTo; + } + symlink.linkTo = linkTo; + command.push(linkTo); + }); + } catch (error) { + } finally { + const output = await this.execCommand(namespace, podName, containerName, command); + const lines = output.split('\n').filter((v) => !!v); + + try { + for (const line of lines) { + if (line && line.includes('command terminated with non-zero exit code')) { + const parts = line.split('"'); + try { + symlinks.map((symlink) => { + if (symlink.linkTo === parts[1] && !symlink.processed) { + symlink.processed = true; + symlink.kind = line[0]; + if (symlink.kind === 'd') { + directories.push(symlink); + } + } + }); + } catch (error) {} + } + } + } catch (error) {} + } + } + + directories.sort((a, b) => (a.name > b.name ? 1 : -1)); + + return { + directories: directories, + files: files + }; + } + + async mv({ + namespace, + podName, + containerName, + from, + to + }: { + namespace: string; + podName: string; + containerName: string; + from: string; + to: string; + }) { + return await this.execCommand(namespace, podName, containerName, ['mv', from, to]); + } + + async rm({ + namespace, + podName, + containerName, + path + }: { + namespace: string; + podName: string; + containerName: string; + path: string; + }) { + return await this.execCommand(namespace, podName, containerName, ['rm', '-rf', path]); + } + + async download({ + namespace, + podName, + containerName, + path, + stdout + }: { + namespace: string; + podName: string; + containerName: string; + path: string; + stdout: Writable; + }) { + return await this.execCommand( + namespace, + podName, + containerName, + ['dd', `if=${path}`, 'status=none'], + null, + stdout + ); + } + + async mkdir({ + namespace, + podName, + containerName, + path + }: { + namespace: string; + podName: string; + containerName: string; + path: string; + }) { + return await this.execCommand(namespace, podName, containerName, ['mkdir', path]); + } + + async touch({ + namespace, + podName, + containerName, + path + }: { + namespace: string; + podName: string; + containerName: string; + path: string; + }) { + return await this.execCommand(namespace, podName, containerName, ['touch', path]); + } + + async upload({ + namespace, + podName, + containerName, + path, + file + }: { + namespace: string; + podName: string; + containerName: string; + path: string; + file: PassThrough; + }): Promise { + const result = await this.execCommand( + namespace, + podName, + containerName, + ['sh', '-c', `dd of=${path} status=none bs=32767`], + file + ); + return result; + } + + async md5sum({ + namespace, + podName, + containerName, + path + }: { + namespace: string; + podName: string; + containerName: string; + path: string; + }) { + return await this.execCommand(namespace, podName, containerName, ['md5sum', path]); + } + + getTimezoneFromOffset(offset: string): string { + const hours = parseInt(offset.slice(1, 3)); + const sign = offset.startsWith('-') ? '+' : '-'; + return `Etc/GMT${sign}${hours}`; + } + + convertToUTC(dateString: string, timezone: string): Date { + dateString = dateString.trim(); + timezone = timezone.trim(); + + if (timezone === 'Etc/UTC') { + timezone = 'UTC'; + } + + const dt = dayjs.tz(dateString, timezone).utc(); + + if (!dt.isValid()) { + console.error(`Failed to parse date: "${dateString}" with timezone "${timezone}"`); + return new Date(); + } + + return dt.toDate(); + } +} diff --git a/frontend/providers/dbprovider/src/utils/logParsers/LogParser.ts b/frontend/providers/dbprovider/src/utils/logParsers/LogParser.ts new file mode 100644 index 00000000000..859fb1f7309 --- /dev/null +++ b/frontend/providers/dbprovider/src/utils/logParsers/LogParser.ts @@ -0,0 +1,58 @@ +import { DBTypeEnum } from '@/constants/db'; +import { LogTypeEnum } from '@/constants/log'; +import { + BaseLogEntry, + ILogParser, + LogResult, + MongoLogEntry, + PgLogEntry, + RedisLogEntry +} from '@/types/log'; +import * as k8s from '@kubernetes/client-node'; +import { MongoLogParser } from './MongoLogParser'; +import { PostgresLogParser } from './PostgresLogParser'; +import { RedisLogParser } from './RedisLogParser'; +import { MysqlLogParser } from './MysqlLogParser'; + +class DatabaseLogService { + private parsers: Map; + + constructor( + private k8sExec: k8s.Exec, + private k8sCore: k8s.CoreV1Api, + private namespace: string + ) { + this.parsers = new Map([ + [DBTypeEnum.postgresql, new PostgresLogParser(k8sExec, namespace)], + [DBTypeEnum.redis, new RedisLogParser(k8sExec, namespace)], + [DBTypeEnum.mongodb, new MongoLogParser(k8sExec, k8sCore, namespace)], + [DBTypeEnum.mysql, new MysqlLogParser(k8sExec, namespace)] + ]); + } + + async readLogs(params: { + podName: string; + containerNames: string[]; + logPath: string; + page: number; + pageSize: number; + keyword?: string; + dbType: DBTypeEnum; + logType?: LogTypeEnum; + }): Promise { + const parser = this.parsers.get(params.dbType); + if (!parser) { + throw new Error(`Unsupported database type: ${params.dbType}`); + } + return parser.readLogs(params); + } +} + +export { + DatabaseLogService, + type BaseLogEntry, + type LogResult, + type MongoLogEntry, + type PgLogEntry, + type RedisLogEntry +}; diff --git a/frontend/providers/dbprovider/src/utils/logParsers/MongoLogParser.ts b/frontend/providers/dbprovider/src/utils/logParsers/MongoLogParser.ts new file mode 100644 index 00000000000..f11546c11ff --- /dev/null +++ b/frontend/providers/dbprovider/src/utils/logParsers/MongoLogParser.ts @@ -0,0 +1,101 @@ +import * as k8s from '@kubernetes/client-node'; +import { ILogParser, LogParserParams, LogResult, MongoLogEntry } from '@/types/log'; + +export class MongoLogParser implements ILogParser { + private static readonly MONGO_LOG_PATTERN = + /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}[+-]\d{4})\s+(\w+)\s+(\w+)\s+\[(\w+)]\s+(.+?)(?:\s+(?:connection:\s*(\d+)))?$/; + + constructor( + private k8sExec: k8s.Exec, + private k8sCore: k8s.CoreV1Api, + private namespace: string + ) {} + + async readLogs(params: LogParserParams): Promise { + const { podName, containerNames, page, pageSize } = params; + const start = performance.now(); + + try { + let logs: MongoLogEntry[] = []; + let totalCount = 0; + + const oneDayInSeconds = 1 * 60 * 60; + + for (const containerName of containerNames) { + try { + const { body: logData } = await this.k8sCore.readNamespacedPodLog( + podName, + this.namespace, + containerName, + false, + undefined, + undefined, + undefined, + false, + oneDayInSeconds, + undefined, + false + ); + + if (!logData) continue; + + const allLogs = this.parseMongoLogs(logData); + totalCount = allLogs.length; + + const startIndex = (page - 1) * pageSize; + const endIndex = startIndex + pageSize; + logs = allLogs.slice(startIndex, endIndex); + + break; + } catch (error) { + continue; + } + } + + const end = performance.now(); + + return { + logs, + metadata: { + total: totalCount, + page, + pageSize, + processingTime: `${(end - start).toFixed(2)}ms`, + hasMore: page * pageSize < totalCount + } + }; + } catch (error) { + console.error('Error reading MongoDB logs:', error); + throw error; + } + } + + private parseMongoLogs(logString: string): MongoLogEntry[] { + return logString + .split('\n') + .filter((line) => line.trim()) + .map((line) => { + const match = line.match(MongoLogParser.MONGO_LOG_PATTERN); + if (!match) { + return { + timestamp: new Date().toISOString(), + level: 'INFO', + component: 'unknown', + context: 'unknown', + content: line.trim() + }; + } + + const [, timestamp, level, component, context, content, connectionId] = match; + return { + timestamp, + level, + component, + context, + content: content.trim(), + connectionId + }; + }) + .filter((entry): entry is MongoLogEntry => entry !== null); + } +} diff --git a/frontend/providers/dbprovider/src/utils/logParsers/MysqlLogParser.ts b/frontend/providers/dbprovider/src/utils/logParsers/MysqlLogParser.ts new file mode 100644 index 00000000000..47c6f50b2c8 --- /dev/null +++ b/frontend/providers/dbprovider/src/utils/logParsers/MysqlLogParser.ts @@ -0,0 +1,293 @@ +import { LogTypeEnum } from '@/constants/log'; +import { ILogParser, LogParserParams, LogResult, MysqlLogEntry } from '@/types/log'; +import { KubeFileSystem } from '@/utils/kubeFileSystem'; +import * as k8s from '@kubernetes/client-node'; + +export class MysqlLogParser implements ILogParser { + constructor(private k8sExec: k8s.Exec, private namespace: string) {} + + async readLogs(params: LogParserParams): Promise { + const { + podName, + containerNames, + logPath, + page, + pageSize, + logType = LogTypeEnum.SlowQuery + } = params; + + const start = performance.now(); + + try { + if (logType === LogTypeEnum.SlowQuery) { + const totalCount = await this.getKeywordCount(podName, containerNames, logPath, 'Time'); + if (totalCount === 0) { + return this.emptyResult(page, pageSize); + } + + const startIndex = (page - 1) * pageSize + 1; + const endIndex = Math.min(page * pageSize, totalCount); + + const { startLine, endLine } = await this.getLineNumbersForRange( + podName, + containerNames, + logPath, + 'Time', + startIndex, + endIndex + ); + + if (!startLine || !endLine) { + return this.emptyResult(page, pageSize); + } + + const data = await this.readLogsFromContainers( + podName, + containerNames, + logPath, + startLine, + endLine + ); + + const logs = this.parseSlowQueryLogs(data); + const end = performance.now(); + + return { + logs, + metadata: { + total: totalCount, + page, + pageSize, + processingTime: `${(end - start).toFixed(2)}ms`, + hasMore: endIndex < totalCount + } + }; + } else { + const totalCount = await this.getMysqlLogCount(podName, containerNames, logPath); + if (totalCount === 0) { + return this.emptyResult(page, pageSize); + } + + const startLine = (page - 1) * pageSize + 1; + const endLine = Math.min(page * pageSize, totalCount); + + const data = await this.readLogsFromContainers( + podName, + containerNames, + logPath, + startLine, + endLine + ); + + const logs = this.parseErrorLogs(data); + const end = performance.now(); + + return { + logs, + metadata: { + total: totalCount, + page, + pageSize, + processingTime: `${(end - start).toFixed(2)}ms`, + hasMore: endLine < totalCount + } + }; + } + } catch (error) { + console.error('Error reading MySQL logs:', error); + throw error; + } + } + + private async getKeywordCount( + podName: string, + containerNames: string[], + logPath: string, + keyword: string + ): Promise { + const kubefs = new KubeFileSystem(this.k8sExec); + + for (const containerName of containerNames) { + try { + const result = await kubefs.execCommand(this.namespace, podName, containerName, [ + 'grep', + '-c', + keyword, + logPath + ]); + + if (result) { + return parseInt(result.trim(), 10); + } + } catch (error) { + continue; + } + } + return 0; + } + + private async getLineNumbersForRange( + podName: string, + containerNames: string[], + logPath: string, + keyword: string, + startIndex: number, + endIndex: number + ): Promise<{ startLine?: number; endLine?: number }> { + const kubefs = new KubeFileSystem(this.k8sExec); + + for (const containerName of containerNames) { + try { + const awkCommand = ` + /${keyword}/ { + count++; + if (count == ${startIndex}) print NR; + if (count == ${endIndex}) { print NR; exit; } + } + `; + + const result = await kubefs.execCommand(this.namespace, podName, containerName, [ + 'awk', + awkCommand, + logPath + ]); + + if (result) { + const lines = result.trim().split('\n'); + return { + startLine: parseInt(lines[0], 10), + endLine: lines[1] ? parseInt(lines[1], 10) : parseInt(lines[0], 10) + }; + } + } catch (error) { + continue; + } + } + return {}; + } + + private async readLogsFromContainers( + podName: string, + containerNames: string[], + logPath: string, + startLine: number, + endLine: number + ): Promise { + const kubefs = new KubeFileSystem(this.k8sExec); + + for (const containerName of containerNames) { + try { + const data = await kubefs.execCommand(this.namespace, podName, containerName, [ + 'sed', + '-n', + `${startLine},${endLine}p`, + logPath + ]); + if (data) return data; + } catch (error) { + continue; + } + } + return ''; + } + + private async getMysqlLogCount( + podName: string, + containerNames: string[], + logPath: string + ): Promise { + const kubefs = new KubeFileSystem(this.k8sExec); + + for (const containerName of containerNames) { + try { + const result = await kubefs.execCommand(this.namespace, podName, containerName, [ + 'wc', + '-l', + logPath + ]); + + if (result) { + return parseInt(result.trim().split(' ')[0], 10); + } + } catch (error) { + continue; + } + } + return 0; + } + + private parseSlowQueryLogs(logString: string): MysqlLogEntry[] { + if (!logString.trim()) { + return []; + } + + const entries: MysqlLogEntry[] = []; + const logs = logString.split('# Time:').filter(Boolean); + + for (const log of logs) { + const lines = log.trim().split('\n'); + + const timestampMatch = lines[0].trim().match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/); + + if (timestampMatch) { + const timestamp = lines[0].trim(); + const content = lines.slice(1).join('\n'); + + entries.push({ + timestamp, + level: 'INFO', + content: content.trim() + }); + } + } + + return entries; + } + + private parseErrorLogs(logString: string): MysqlLogEntry[] { + return logString + .split('\n') + .filter((line) => line.trim()) + .map((line) => { + const match = line.match( + /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z)\s+(\d+)\s+\[([^\]]+)\]/ + ); + if (!match) { + return { + timestamp: new Date().toISOString(), + level: 'INFO', + content: line.trim() + }; + } + + const [, timestamp, processId, level] = match; + let logLevel = 'INFO'; + if (level.includes('Warning')) logLevel = 'WARNING'; + if (level.includes('ERROR')) logLevel = 'ERROR'; + + const content = line + .substring(line.indexOf(']', line.indexOf(']', line.indexOf(']') + 1) + 1) + 1) + .trim(); + + return { + timestamp, + level: logLevel, + content + }; + }) + .filter((entry): entry is MysqlLogEntry => entry !== null); + } + + private emptyResult(page: number, pageSize: number): LogResult { + return { + logs: [], + metadata: { + total: 0, + page, + pageSize, + processingTime: '0ms', + hasMore: false + } + }; + } +} diff --git a/frontend/providers/dbprovider/src/utils/logParsers/PostgresLogParser.ts b/frontend/providers/dbprovider/src/utils/logParsers/PostgresLogParser.ts new file mode 100644 index 00000000000..0689fae0d34 --- /dev/null +++ b/frontend/providers/dbprovider/src/utils/logParsers/PostgresLogParser.ts @@ -0,0 +1,185 @@ +import * as k8s from '@kubernetes/client-node'; +import { ILogParser, LogParserParams, LogResult, PgLogEntry } from '@/types/log'; +import { KubeFileSystem } from '@/utils/kubeFileSystem'; +import Papa from 'papaparse'; + +export class PostgresLogParser implements ILogParser { + constructor(private k8sExec: k8s.Exec, private namespace: string) {} + + async readLogs(params: LogParserParams): Promise { + const { podName, containerNames, logPath, page, pageSize, keyword = 'CST' } = params; + const start = performance.now(); + + const totalCount = await this.getKeywordCount(podName, containerNames, logPath, keyword); + + if (totalCount === 0) { + return this.emptyResult(page, pageSize); + } + + const startIndex = (page - 1) * pageSize + 1; + const endIndex = Math.min(page * pageSize, totalCount); + + const { startLine, endLine } = await this.getLineNumbersForRange( + podName, + containerNames, + logPath, + keyword, + startIndex, + endIndex + ); + + if (!startLine || !endLine) { + return this.emptyResult(page, pageSize); + } + + const data = await this.readLogsFromContainers( + podName, + containerNames, + logPath, + startLine, + endLine + ); + + const logs = this.parsePostgresLogs(data); + const end = performance.now(); + + return { + logs, + metadata: { + total: totalCount, + page, + pageSize, + processingTime: `${(end - start).toFixed(2)}ms`, + hasMore: endIndex < totalCount + } + }; + } + + private async getKeywordCount( + podName: string, + containerNames: string[], + logPath: string, + keyword: string + ): Promise { + const kubefs = new KubeFileSystem(this.k8sExec); + + for (const containerName of containerNames) { + try { + const result = await kubefs.execCommand(this.namespace, podName, containerName, [ + 'grep', + '-c', + keyword, + logPath + ]); + + if (result) { + return parseInt(result.trim(), 10); + } + } catch (error) { + continue; + } + } + return 0; + } + + private async getLineNumbersForRange( + podName: string, + containerNames: string[], + logPath: string, + keyword: string, + startIndex: number, + endIndex: number + ): Promise<{ startLine?: number; endLine?: number }> { + const kubefs = new KubeFileSystem(this.k8sExec); + + for (const containerName of containerNames) { + try { + const awkCommand = ` + /${keyword}/ { + count++; + if (count == ${startIndex}) print NR; + if (count == ${endIndex}) { print NR; exit; } + } + `; + + const result = await kubefs.execCommand(this.namespace, podName, containerName, [ + 'awk', + awkCommand, + logPath + ]); + + if (result) { + const lines = result.trim().split('\n'); + return { + startLine: parseInt(lines[0], 10), + endLine: lines[1] ? parseInt(lines[1], 10) : parseInt(lines[0], 10) + }; + } + } catch (error) { + continue; + } + } + return {}; + } + + private async readLogsFromContainers( + podName: string, + containerNames: string[], + logPath: string, + startLine: number, + endLine: number + ): Promise { + const kubefs = new KubeFileSystem(this.k8sExec); + + for (const containerName of containerNames) { + try { + const data = await kubefs.execCommand(this.namespace, podName, containerName, [ + 'sed', + '-n', + `${startLine},${endLine}p`, + logPath + ]); + if (data) return data; + } catch (error) { + continue; + } + } + return ''; + } + + private parsePostgresLogs(logString: string): PgLogEntry[] { + if (!logString.trim()) { + return []; + } + + const parsed = Papa.parse(logString, { + skipEmptyLines: true, + header: false + }); + + // parsed.data.forEach((row) => { + // console.log(row, 'row'); + // }); + + return parsed.data + .filter((row) => row[0]) + .map((row) => ({ + timestamp: row[0].replace(/^"|"$/g, ''), + level: 'INFO', + content: row.slice(1).join(',') + })); + } + + private emptyResult(page: number, pageSize: number): LogResult { + return { + logs: [], + metadata: { + total: 0, + page, + pageSize, + processingTime: '0ms', + hasMore: false + } + }; + } +} diff --git a/frontend/providers/dbprovider/src/utils/logParsers/RedisLogParser.ts b/frontend/providers/dbprovider/src/utils/logParsers/RedisLogParser.ts new file mode 100644 index 00000000000..d6e39301bd6 --- /dev/null +++ b/frontend/providers/dbprovider/src/utils/logParsers/RedisLogParser.ts @@ -0,0 +1,145 @@ +import * as k8s from '@kubernetes/client-node'; +import { ILogParser, LogParserParams, LogResult, RedisLogEntry } from '@/types/log'; +import { KubeFileSystem } from '@/utils/kubeFileSystem'; +import dayjs from 'dayjs'; + +export class RedisLogParser implements ILogParser { + private static readonly REDIS_LOG_PATTERN = + /^(\d+):([A-Z])\s+(\d{1,2}\s+[A-Za-z]+\s+\d{4}\s+\d{2}:\d{2}:\d{2}\.\d{3})\s+([*#])\s+(.+)$/; + + constructor(private k8sExec: k8s.Exec, private namespace: string) {} + + async readLogs(params: LogParserParams): Promise { + const { podName, containerNames, logPath, page, pageSize } = params; + const start = performance.now(); + + try { + const totalCount = await this.getRedisLogCount(podName, containerNames, logPath); + + if (totalCount === 0) { + return this.emptyResult(page, pageSize); + } + + const startLine = (page - 1) * pageSize + 1; + const endLine = Math.min(page * pageSize, totalCount); + + const data = await this.readLogsFromContainers( + podName, + containerNames, + logPath, + startLine, + endLine + ); + + const logs = this.parseRedisLogs(data); + const end = performance.now(); + + return { + logs, + metadata: { + total: totalCount, + page, + pageSize, + processingTime: `${(end - start).toFixed(2)}ms`, + hasMore: endLine < totalCount + } + }; + } catch (error) { + console.error('Error reading Redis logs:', error); + throw error; + } + } + + private async getRedisLogCount( + podName: string, + containerNames: string[], + logPath: string + ): Promise { + const kubefs = new KubeFileSystem(this.k8sExec); + + for (const containerName of containerNames) { + try { + const result = await kubefs.execCommand(this.namespace, podName, containerName, [ + 'wc', + '-l', + logPath + ]); + + if (result) { + const count = parseInt(result.trim().split(' ')[0], 10); + return count; + } + } catch (error) { + continue; + } + } + return 0; + } + + private async readLogsFromContainers( + podName: string, + containerNames: string[], + logPath: string, + startLine: number, + endLine: number + ): Promise { + const kubefs = new KubeFileSystem(this.k8sExec); + + for (const containerName of containerNames) { + try { + const data = await kubefs.execCommand(this.namespace, podName, containerName, [ + 'sed', + '-n', + `${startLine},${endLine}p`, + logPath + ]); + if (data) return data; + } catch (error) { + continue; + } + } + return ''; + } + + private parseRedisLogs(logString: string): RedisLogEntry[] { + return logString + .split('\n') + .filter((line) => line.trim()) + .map((line) => { + // console.log(line, 'line'); + const match = line.match(RedisLogParser.REDIS_LOG_PATTERN); + if (!match) { + return { + timestamp: new Date().toISOString(), + level: 'INFO', + content: line.trim(), + processId: '', + role: '' + }; + } + + const [, processId, role, timestamp, level, content] = match; + return { + processId, + role, + timestamp: dayjs(timestamp).add(8, 'hour').format('DD MMM YYYY HH:mm:ss.SSS'), + level: level === '#' ? 'WARNING' : 'INFO', + content: content.trim() + }; + }) + .filter((entry): entry is RedisLogEntry => entry !== null); + } + + private emptyResult(page: number, pageSize: number): LogResult { + return { + logs: [], + metadata: { + total: 0, + page, + pageSize, + processingTime: '0ms', + hasMore: false + } + }; + } +} diff --git a/frontend/providers/devbox/.env.template b/frontend/providers/devbox/.env.template index 679741e5964..c0bc5d8b6a4 100644 --- a/frontend/providers/devbox/.env.template +++ b/frontend/providers/devbox/.env.template @@ -5,3 +5,4 @@ REGISTRY_ADDR= DEVBOX_AFFINITY_ENABLE= SQUASH_ENABLE= NODE_TLS_REJECT_UNAUTHORIZED= +ROOT_RUNTIME_NAMESPACE= diff --git a/frontend/providers/devbox/.vscode/settings.json b/frontend/providers/devbox/.vscode/settings.json index a553a498618..722591366fc 100644 --- a/frontend/providers/devbox/.vscode/settings.json +++ b/frontend/providers/devbox/.vscode/settings.json @@ -19,63 +19,5 @@ "deepl", "google", ], - "cSpell.words": [ - "apecloud", - "applaunchpad", - "Autoscaler", - "backuprepos", - "bdstatic", - "chakra", - "clusterdefinition", - "clusterversion", - "clusterversions", - "dataprotection", - "devbox", - "devboxes", - "devboxreleases", - "echarts", - "formatjs", - "grpcs", - "hljs", - "immer", - "jsonpatch", - "kbcli", - "kubeblocks", - "kubeconfig", - "kubernet", - "letsencrypt", - "localematcher", - "milvus", - "minio", - "mlhiter", - "nextjs", - "nextline", - "nodeports", - "nprogress", - "oname", - "openai", - "Parens", - "pitr", - "pricequeries", - "qdrant", - "Reconfig", - "rolebinding", - "runtimeclasses", - "runtimes", - "sailos", - "sealos", - "SSHDOMAIN", - "statefulset", - "svgr", - "Tailnet", - "tanstack", - "tolerations", - "weaviate", - "weixin", - "yalc", - "Yamls", - "zustand" - ], "typescript.tsdk": "node_modules/typescript/lib", - "svg.preview.background": "transparent", } diff --git a/frontend/providers/devbox/api/devbox.ts b/frontend/providers/devbox/api/devbox.ts index c11afc5dedf..0e3296a42c4 100644 --- a/frontend/providers/devbox/api/devbox.ts +++ b/frontend/providers/devbox/api/devbox.ts @@ -7,14 +7,25 @@ import { DevboxVersionListItemType, runtimeNamespaceMapType } from '@/types/devbox' +import { + adaptDevboxDetail, + adaptDevboxListItem, + adaptDevboxVersionListItem, + adaptPod +} from '@/utils/adapt' import { GET, POST, DELETE } from '@/services/request' import { KBDevboxType, KBDevboxReleaseType } from '@/types/k8s' import { MonitorDataResult, MonitorQueryKey } from '@/types/monitor' -import { adaptDevboxListItem, adaptDevboxVersionListItem, adaptPod } from '@/utils/adapt' export const getMyDevboxList = () => GET('/api/getDevboxList').then((data): DevboxListItemType[] => - data.map(adaptDevboxListItem) + data.map(adaptDevboxListItem).sort((a, b) => { + return new Date(b.createTime).getTime() - new Date(a.createTime).getTime() + }) + ) +export const getDevboxByName = (devboxName: string) => + GET('/api/getDevboxByName', { devboxName }).then((data) => + adaptDevboxDetail(data) ) export const applyYamlList = (yamlList: string[], type: 'create' | 'replace' | 'update') => @@ -28,8 +39,7 @@ export const createDevbox = (payload: { export const updateDevbox = (payload: { patch: DevboxPatchPropsType; devboxName: string }) => POST(`/api/updateDevbox`, payload) -export const delDevbox = (devboxName: string, networks: string[]) => - DELETE('/api/delDevbox', { devboxName, networks: JSON.stringify(networks) }) +export const delDevbox = (devboxName: string) => DELETE('/api/delDevbox', { devboxName }) export const restartDevbox = (data: { devboxName: string }) => POST('/api/restartDevbox', data) @@ -39,7 +49,10 @@ export const pauseDevbox = (data: { devboxName: string }) => POST('/api/pauseDev export const getDevboxVersionList = (devboxName: string, devboxUid: string) => GET('/api/getDevboxVersionList', { devboxName, devboxUid }).then( - (data): DevboxVersionListItemType[] => data.map(adaptDevboxVersionListItem) + (data): DevboxVersionListItemType[] => + data.map(adaptDevboxVersionListItem).sort((a, b) => { + return new Date(b.createTime).getTime() - new Date(a.createTime).getTime() + }) ) export const releaseDevbox = (data: { diff --git a/frontend/providers/devbox/api/platform.ts b/frontend/providers/devbox/api/platform.ts index 1d1e6bd0bf7..684763129e7 100644 --- a/frontend/providers/devbox/api/platform.ts +++ b/frontend/providers/devbox/api/platform.ts @@ -1,9 +1,7 @@ import { GET, POST } from '@/services/request' import type { UserQuotaItemType } from '@/types/user' -import { SystemEnvResponse } from '@/app/api/getEnv/route' -import type { Response as resourcePriceResponse } from '@/app/api/platform/resourcePrice/route' - -export const getAppEnv = () => GET('/api/getEnv') +import type { Env } from '@/types/static' +export const getAppEnv = () => GET('/api/getEnv') export const getUserQuota = () => GET<{ @@ -12,9 +10,7 @@ export const getUserQuota = () => 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) - -export const getNamespace = () => GET('/api/platform/getNamespace') 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 648d4f44ab6..0be248a0071 100644 --- a/frontend/providers/devbox/app/[lang]/(platform)/(home)/components/DevboxList.tsx +++ b/frontend/providers/devbox/app/[lang]/(platform)/(home)/components/DevboxList.tsx @@ -1,38 +1,19 @@ -import { - Box, - Button, - Center, - Flex, - Image, - MenuButton, - useTheme, - Text, - Tooltip, - Menu, - MenuList, - MenuItem, - IconButton -} from '@chakra-ui/react' import dynamic from 'next/dynamic' import { useTranslations } from 'next-intl' import { useCallback, useState } from 'react' import { sealosApp } from 'sealos-desktop-sdk/app' import { SealosMenu, MyTable, useMessage } from '@sealos/ui' +import { Box, Button, Center, Flex, Image, MenuButton, useTheme, Text } from '@chakra-ui/react' -import { - getSSHConnectionInfo, - getSSHRuntimeInfo, - pauseDevbox, - restartDevbox, - startDevbox -} from '@/api/devbox' import { useRouter } from '@/i18n' -import MyIcon from '@/components/Icon' -import { IDEType, useGlobalStore } from '@/stores/global' +import { useGlobalStore } from '@/stores/global' import { DevboxListItemType } from '@/types/devbox' +import { pauseDevbox, restartDevbox, startDevbox } from '@/api/devbox' + +import MyIcon from '@/components/Icon' +import IDEButton from '@/components/IDEButton' import PodLineChart from '@/components/PodLineChart' import DevboxStatusTag from '@/components/DevboxStatusTag' -import { NAMESPACE, SEALOS_DOMAIN } from '@/stores/static' import ReleaseModal from '@/components/modals/releaseModal' const DelModal = dynamic(() => import('@/components/modals/DelModal')) @@ -48,7 +29,10 @@ const DevboxList = ({ const router = useRouter() const t = useTranslations() const { message: toast } = useMessage() - const { setLoading, setCurrentIDE, currentIDE } = useGlobalStore() + + // TODO: Unified Loading Behavior + const { setLoading } = useGlobalStore() + const [onOpenRelease, setOnOpenRelease] = useState(false) const [delDevbox, setDelDevbox] = useState(null) const [currentDevboxListItem, setCurrentDevboxListItem] = useState( @@ -59,7 +43,6 @@ const DevboxList = ({ setCurrentDevboxListItem(devbox) setOnOpenRelease(true) } - const handlePauseDevbox = useCallback( async (devbox: DevboxListItemType) => { try { @@ -76,8 +59,8 @@ const DevboxList = ({ }) console.error(error) } - setLoading(false) refetchDevboxList() + setLoading(false) }, [refetchDevboxList, setLoading, t, toast] ) @@ -97,9 +80,10 @@ const DevboxList = ({ }) console.error(error, '==') } + refetchDevboxList() setLoading(false) }, - [setLoading, t, toast] + [refetchDevboxList, setLoading, t, toast] ) const handleStartDevbox = useCallback( async (devbox: DevboxListItemType) => { @@ -117,44 +101,11 @@ const DevboxList = ({ }) console.error(error, '==') } + refetchDevboxList() setLoading(false) }, - [setLoading, t, toast] - ) - - const getCurrentIDELabelAndIcon = useCallback( - ( - currentIDE: IDEType - ): { - label: string - icon: IDEType - } => { - switch (currentIDE) { - case 'vscode': - return { - label: 'VSCode', - icon: 'vscode' - } - case 'cursor': - return { - label: 'Cursor', - icon: 'cursor' - } - case 'vscodeInsider': - return { - label: 'VSCode Insider', - icon: 'vscodeInsider' - } - default: - return { - label: 'VSCode', - icon: 'vscode' - } - } - }, - [] + [refetchDevboxList, setLoading, t, toast] ) - const handleGoToTerminal = useCallback( async (devbox: DevboxListItemType) => { const defaultCommand = `kubectl exec -it $(kubectl get po -l app.kubernetes.io/name=${devbox.name} -oname) -- sh -c "clear; (bash || ash || sh)"` @@ -173,49 +124,8 @@ const DevboxList = ({ }) console.error(error) } - refetchDevboxList() - }, - [refetchDevboxList, t, toast] - ) - - const handleGotoIDE = useCallback( - async (devbox: DevboxListItemType, currentIDE: string = 'vscode') => { - try { - const { base64PrivateKey, userName } = await getSSHConnectionInfo({ - devboxName: devbox.name, - runtimeName: devbox.runtimeVersion - }) - const { workingDir } = await getSSHRuntimeInfo(devbox.runtimeVersion) - - let editorUri = '' - switch (currentIDE) { - case 'cursor': - editorUri = `cursor://` - break - case 'vscodeInsider': - editorUri = `vscode-insiders://` - break - case 'vscode': - editorUri = `vscode://` - break - default: - editorUri = `vscode://` - } - - const fullUri = `${editorUri}mlhiter.devbox-sealos?sshDomain=${encodeURIComponent( - `${userName}@${SEALOS_DOMAIN}` - )}&sshPort=${encodeURIComponent(devbox.sshPort)}&base64PrivateKey=${encodeURIComponent( - base64PrivateKey - )}&sshHostLabel=${encodeURIComponent( - `${SEALOS_DOMAIN}/${NAMESPACE}/${devbox.name}` - )}&workingDir=${encodeURIComponent(workingDir)}` - - window.location.href = fullUri - } catch (error: any) { - console.error(error, '==') - } }, - [] + [t, toast] ) const columns: { @@ -229,7 +139,7 @@ const DevboxList = ({ key: 'name', render: (item: DevboxListItemType) => { return ( - + ( - + ( - + ( - - - - - } - _before={{ - content: '""', - position: 'absolute', - left: 0, - top: '50%', - transform: 'translateY(-50%)', - width: '1px', - height: '20px', - backgroundColor: 'grayModern.250' - }} - /> - - {[ - { value: 'vscode' as IDEType, label: 'VSCode' }, - { value: 'cursor' as IDEType, label: 'Cursor' }, - { value: 'vscodeInsider' as IDEType, label: 'VSCode Insider' } - ].map((item) => ( - setCurrentIDE(item.value)} - icon={} - _hover={{ - bg: '#1118240D', - borderRadius: 4 - }} - _focus={{ - bg: '#1118240D', - borderRadius: 4 - }}> - - {item.label} - {currentIDE === item.value && } - - - ))} - - + - + {!!delDevbox && ( { + const router = useRouter() const { Loading } = useLoading() - const { devboxList, setDevboxList } = useDevboxStore() - const [initialized, setInitialized] = useState(false) - const { refetch } = useQuery(['initDevboxData'], setDevboxList, { - refetchInterval: 3000, - onSettled() { - setInitialized(true) + const { devboxList, setDevboxList, loadAvgMonitorData, intervalLoadPods } = useDevboxStore() + + const [_, setFresh] = useState(false) + const list = useRef(devboxList) + + const refreshList = useCallback( + (res = devboxList) => { + list.current = res + setFresh((state) => !state) + return null + }, + [devboxList] + ) + + const { isLoading, refetch: refetchDevboxList } = useQuery(['devboxListQuery'], setDevboxList, { + onSettled(res) { + if (!res) return + refreshList(res) } }) + + useQuery( + ['intervalLoadPods', devboxList.length], + () => { + const doms = document.querySelectorAll(`.devboxListItem`) + const viewportDomIds = Array.from(doms) + .filter((item) => isElementInViewport(item)) + .map((item) => item.getAttribute('data-id')) + + const viewportDevboxList = + viewportDomIds.length < 3 + ? devboxList + : devboxList.filter((devbox) => viewportDomIds.includes(devbox.id)) + + return viewportDevboxList + .filter((devbox) => devbox.status.value !== 'Stopped') + .map((devbox) => intervalLoadPods(devbox.name, false)) + }, + { + refetchOnMount: true, + refetchInterval: 3000, + onSettled() { + refreshList() + } + } + ) + + useQuery( + ['refresh'], + () => { + refreshList() + return null + }, + { + refetchInterval: 3000 + } + ) + + const { refetch: refetchAvgMonitorData } = useQuery( + ['loadAvgMonitorData', devboxList.length], + () => { + const doms = document.querySelectorAll('.devboxListItem') + const viewportDomIds = Array.from(doms) + .filter((dom) => isElementInViewport(dom)) + .map((dom) => dom.getAttribute('data-id')) + + const viewportDevboxList = + viewportDomIds.length < 3 + ? devboxList + : devboxList.filter((devbox) => viewportDomIds.includes(devbox.id)) + + // TODO: reference applaunchpad to request rhythmically + return viewportDevboxList + .filter((devbox) => devbox.status.value === 'Running') + .map((devbox) => loadAvgMonitorData(devbox.name)) + }, + { + refetchOnMount: true, + refetchInterval: 2 * 60 * 1000, + onError(err) { + console.log(err) + }, + onSettled() { + refreshList() + } + } + ) + + useEffect(() => { + router.prefetch('/devbox/detail') + router.prefetch('/devbox/create') + }, [router]) + return ( <> - {devboxList.length === 0 && initialized ? ( + {devboxList.length === 0 && !isLoading ? ( ) : ( <> - + { + refetchDevboxList() + refetchAvgMonitorData() + }} + /> )} - + ) } 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 8a06992cb7a..7d59eecce2c 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 @@ -23,25 +23,18 @@ import { useTranslations } from 'next-intl' import { UseFormReturn, useFieldArray } from 'react-hook-form' import { MySelect, MySlider, Tabs, useMessage } from '@sealos/ui' -import { - INSTALL_ACCOUNT, - SEALOS_DOMAIN, - frameworkTypeList, - frameworkVersionMap, - languageTypeList, - languageVersionMap, - osTypeList, - osVersionMap, - getRuntimeVersionList -} from '@/stores/static' 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 { useDevboxStore } from '@/stores/devbox' +import { useRuntimeStore } from '@/stores/runtime' + import { ProtocolList } from '@/constants/devbox' import type { DevboxEditType } from '@/types/devbox' -import { getValueDefault, obj2Query } from '@/utils/tools' +import { obj2Query } from '@/utils/tools' import { CpuSlideMarkList, MemorySlideMarkList } from '@/constants/devbox' const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 12) @@ -82,6 +75,19 @@ const Form = ({ name: 'networks' }) + const { + languageVersionMap, + frameworkVersionMap, + osVersionMap, + languageTypeList, + frameworkTypeList, + osTypeList, + getRuntimeVersionList, + getRuntimeVersionDefault, + getRuntimeDetailLabel + } = useRuntimeStore() + const { env } = useEnvStore() + const [customAccessModalData, setCustomAccessModalData] = useState() const navList: { id: string; label: string; icon: string }[] = [ { @@ -237,19 +243,17 @@ const Form = ({ - {INSTALL_ACCOUNT && ( - - - - )} + + + {/* right content */} - /^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$/.test( + /^[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) => { - setValue('name', e.target.value) - setValue( - 'networks', - getRuntimeVersionList(getValues('runtimeType'))[0].defaultPorts.map( - (port) => ({ - networkName: `${e.target.value}-${nanoid()}`, - portName: nanoid(), - port: port, - protocol: 'HTTP', - openPublicDomain: true, - publicDomain: nanoid(), - customDomain: '' - }) - ) - ) + const lowercaseValue = e.target.value.toLowerCase() + + setValue('name', lowercaseValue) + const networks = getValues('networks') + networks.forEach((network, i) => { + updateNetworks(i, { + ...network, + networkName: `${lowercaseValue}-${nanoid()}` + }) + }) }} /> - - - {errors.name && errors.name.message} - - {/* Runtime Type */} @@ -372,7 +366,7 @@ const Form = ({ port: port, protocol: 'HTTP', openPublicDomain: true, - publicDomain: nanoid(), + publicDomain: `${nanoid()}.${env.ingressDomain}`, customDomain: '' }) ) @@ -450,7 +444,7 @@ const Form = ({ port: port, protocol: 'HTTP', openPublicDomain: true, - publicDomain: nanoid(), + publicDomain: `${nanoid()}.${env.ingressDomain}`, customDomain: '' }) ) @@ -528,7 +522,7 @@ const Form = ({ port: port, protocol: 'HTTP', openPublicDomain: true, - publicDomain: nanoid(), + publicDomain: `${nanoid()}.${env.ingressDomain}`, customDomain: '' }) ) @@ -558,7 +552,15 @@ const Form = ({ {isEdit ? ( - {getValues('runtimeVersion')} + ) : ( { const devboxName = getValues('name') @@ -726,12 +730,13 @@ const Form = ({ }) return } + updateNetworks(i, { ...getValues('networks')[i], networkName: network.networkName || `${devboxName}-${nanoid()}`, protocol: network.protocol || 'HTTP', openPublicDomain: e.target.checked, - publicDomain: network.publicDomain || nanoid() + publicDomain: network.publicDomain || `${nanoid()}.${env.ingressDomain}` }) }} /> @@ -769,9 +774,7 @@ const Form = ({ borderTopRightRadius={'md'} borderBottomRightRadius={'md'}> - {network.customDomain - ? network.customDomain - : `${network.publicDomain}.${SEALOS_DOMAIN}`} + {network.customDomain ? network.customDomain : network.publicDomain} import('@/components/modals/ErrorModal')) const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 12) -const formData2Yamls = (data: DevboxEditType) => [ - { - filename: 'service.yaml', - value: json2Service(data) - }, - { - filename: 'devbox.yaml', - value: json2Devbox(data, runtimeNamespaceMap, DEVBOX_AFFINITY_ENABLE, SQUASH_ENABLE) - }, - ...(data.networks.find((item) => item.openPublicDomain) - ? [ - { - filename: 'ingress.yaml', - value: json2Ingress(data) - } - ] - : []) -] - const DevboxCreatePage = () => { const router = useRouter() const t = useTranslations() const searchParams = useSearchParams() const { message: toast } = useMessage() + + const { env } = useEnvStore() const { checkQuotaAllow } = useUserStore() + const { runtimeNamespaceMap, languageVersionMap } = useRuntimeStore() const { setDevboxDetail, devboxList } = useDevboxStore() const crOldYamls = useRef([]) const formOldYamls = useRef([]) const oldDevboxEditData = useRef() + const { Loading, setIsLoading } = useLoading() const [errorMessage, setErrorMessage] = useState('') const [forceUpdate, setForceUpdate] = useState(false) @@ -76,6 +61,29 @@ const DevboxCreatePage = () => { const tabType = searchParams.get('type') || 'form' const devboxName = searchParams.get('name') || '' + const formData2Yamls = (data: DevboxEditType) => [ + { + filename: 'devbox.yaml', + value: json2Devbox(data, runtimeNamespaceMap, env.devboxAffinityEnable, env.squashEnable) + }, + ...(data.networks.length > 0 + ? [ + { + filename: 'service.yaml', + value: json2Service(data) + } + ] + : []), + ...(data.networks.find((item) => item.openPublicDomain) + ? [ + { + filename: 'ingress.yaml', + value: json2Ingress(data, env.ingressSecret) + } + ] + : []) + ] + const defaultEdit = { ...defaultDevboxEditValue, runtimeVersion: languageVersionMap[LanguageTypeEnum.go][0].id, @@ -85,7 +93,7 @@ const DevboxCreatePage = () => { port: port, protocol: 'HTTP' as ProtocolType, openPublicDomain: true, - publicDomain: nanoid(), + publicDomain: `${nanoid()}.${env.ingressDomain}`, customDomain: '' })) } @@ -126,16 +134,24 @@ const DevboxCreatePage = () => { return [ { filename: 'devbox.yaml', - value: json2Devbox(data, runtimeNamespaceMap, DEVBOX_AFFINITY_ENABLE, SQUASH_ENABLE) - }, - { - filename: 'service.yaml', - value: json2Service(data) + value: json2Devbox(data, runtimeNamespaceMap, env.devboxAffinityEnable, env.squashEnable) }, - { - filename: 'ingress.yaml', - value: json2Ingress(data) - } + ...(data.networks.length > 0 + ? [ + { + filename: 'service.yaml', + value: json2Service(data) + } + ] + : []), + ...(data.networks.find((item) => item.openPublicDomain) + ? [ + { + filename: 'ingress.yaml', + value: json2Ingress(data, env.ingressSecret) + } + ] + : []) ] } @@ -171,23 +187,31 @@ const DevboxCreatePage = () => { value: json2Devbox( defaultEdit, runtimeNamespaceMap, - DEVBOX_AFFINITY_ENABLE, - SQUASH_ENABLE + env.devboxAffinityEnable, + env.squashEnable ) }, - { - filename: 'service.yaml', - value: json2Service(defaultEdit) - }, - { - filename: 'ingress.yaml', - value: json2Ingress(defaultEdit) - } + ...(defaultEdit.networks.length > 0 + ? [ + { + filename: 'service.yaml', + value: json2Service(defaultEdit) + } + ] + : []), + ...(defaultEdit.networks.find((item) => item.openPublicDomain) + ? [ + { + filename: 'ingress.yaml', + value: json2Ingress(defaultEdit, env.ingressSecret) + } + ] + : []) ]) return null } setIsLoading(true) - return setDevboxDetail(devboxName) + return setDevboxDetail(devboxName, env.sealosDomain) }, { onSuccess(res) { 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 8669dbd3b2e..31e33b8597c 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,25 @@ import React, { useCallback, useState } from 'react' import { Box, Text, Flex, Image, Spinner, Tooltip } from '@chakra-ui/react' import MyIcon from '@/components/Icon' -import { useDevboxStore } from '@/stores/devbox' + import { DevboxDetailType } from '@/types/devbox' -import { getRuntimeVersionItem, NAMESPACE, REGISTRY_ADDR, SEALOS_DOMAIN } from '@/stores/static' + +import { useEnvStore } from '@/stores/env' +import { useDevboxStore } from '@/stores/devbox' +import { useRuntimeStore } from '@/stores/runtime' const BasicInfo = () => { - const { devboxDetail } = useDevboxStore() - const [loading, setLoading] = useState(false) const t = useTranslations() const { message: toast } = useMessage() + const { env } = useEnvStore() + const { devboxDetail } = useDevboxStore() + const { getRuntimeDetailLabel } = useRuntimeStore() + + const [loading, setLoading] = useState(false) + const handleCopySSHCommand = useCallback(() => { - const sshCommand = `ssh -i yourPrivateKeyPath ${devboxDetail?.sshConfig?.sshUser}@${SEALOS_DOMAIN} -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'), @@ -24,7 +31,7 @@ const BasicInfo = () => { isClosable: true }) }) - }, [devboxDetail, toast, t]) + }, [devboxDetail?.sshConfig?.sshUser, devboxDetail.sshPort, env.sealosDomain, toast, t]) const handleDownloadConfig = useCallback( async (config: DevboxDetailType['sshConfig']) => { @@ -49,7 +56,7 @@ const BasicInfo = () => { ) return ( - + {/* basic info */} @@ -80,7 +87,7 @@ const BasicInfo = () => { {`${REGISTRY_ADDR}/${NAMESPACE}/${devboxDetail?.name}`} + w={'full'}>{`${env.registryAddr}/${env.namespace}/${devboxDetail?.name}`} @@ -97,7 +104,7 @@ const BasicInfo = () => { - {getRuntimeVersionItem(devboxDetail?.runtimeType, devboxDetail?.runtimeVersion)} + {getRuntimeDetailLabel(devboxDetail?.runtimeType, devboxDetail?.runtimeVersion)} @@ -111,7 +118,7 @@ const BasicInfo = () => { - Limit CPU + CPU Limit {devboxDetail?.cpu / 1000} Core @@ -119,7 +126,7 @@ const BasicInfo = () => { - Limit Memory + Memory Limit {devboxDetail?.memory / 1024} G @@ -164,7 +171,7 @@ const BasicInfo = () => { _hover={{ color: 'blue.500' }} onClick={handleCopySSHCommand} w={'full'}> - {`ssh -i yourPrivateKeyPath ${devboxDetail?.sshConfig?.sshUser}@${SEALOS_DOMAIN} -p ${devboxDetail.sshPort}`} + {`ssh -i yourPrivateKeyPath ${devboxDetail?.sshConfig?.sshUser}@${env.sealosDomain} -p ${devboxDetail.sshPort}`} @@ -225,8 +232,10 @@ const BasicInfo = () => { {t('recent_error')} - {devboxDetail?.lastTerminatedState?.reason ? ( - {devboxDetail?.lastTerminatedState?.reason} + {devboxDetail?.lastTerminatedReason ? ( + + {devboxDetail?.lastTerminatedReason} + ) : ( {t('none')} )} 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 e9b69b28262..55b9b4e56fe 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,32 +1,19 @@ import { useMessage } from '@sealos/ui' import { useTranslations } from 'next-intl' -import { Dispatch, useCallback, useState } from 'react' -import { - Flex, - Button, - Box, - Menu, - MenuButton, - IconButton, - MenuList, - MenuItem -} from '@chakra-ui/react' +import { Flex, Button, Box } from '@chakra-ui/react' +import { Dispatch, useCallback, useMemo, useState } from 'react' import { useRouter } from '@/i18n' -import MyIcon from '@/components/Icon' import { useDevboxStore } from '@/stores/devbox' import { IDEType, useGlobalStore } from '@/stores/global' +import { pauseDevbox, restartDevbox, startDevbox } from '@/api/devbox' + import { DevboxDetailType } from '@/types/devbox' + +import MyIcon from '@/components/Icon' +import IDEButton from '@/components/IDEButton' import DelModal from '@/components/modals/DelModal' import DevboxStatusTag from '@/components/DevboxStatusTag' -import { NAMESPACE, SEALOS_DOMAIN } from '@/stores/static' -import { - getSSHConnectionInfo, - getSSHRuntimeInfo, - pauseDevbox, - restartDevbox, - startDevbox -} from '@/api/devbox' const Header = ({ refetchDevboxDetail, @@ -40,83 +27,13 @@ const Header = ({ const router = useRouter() const t = useTranslations() const { message: toast } = useMessage() - const { setLoading, setCurrentIDE, currentIDE } = useGlobalStore() - const { devboxDetail } = useDevboxStore() - const [delDevbox, setDelDevbox] = useState(null) - const getCurrentIDELabelAndIcon = useCallback( - ( - currentIDE: IDEType - ): { - label: string - icon: IDEType - } => { - switch (currentIDE) { - case 'vscode': - return { - label: 'VSCode', - icon: 'vscode' - } - case 'cursor': - return { - label: 'Cursor', - icon: 'cursor' - } - case 'vscodeInsider': - return { - label: 'VSCode Insider', - icon: 'vscodeInsider' - } - default: - return { - label: 'VSCode', - icon: 'vscode' - } - } - }, - [] - ) - const handleGotoIDE = useCallback( - async (devbox: DevboxDetailType, currentIDE: string = 'vscode') => { - try { - const { base64PrivateKey, userName } = await getSSHConnectionInfo({ - devboxName: devbox.name, - runtimeName: devbox.runtimeVersion - }) - const { workingDir } = await getSSHRuntimeInfo(devbox.runtimeVersion) - - let editorUri = '' - switch (currentIDE) { - case 'cursor': - editorUri = `cursor://` - break - case 'vscodeInsider': - editorUri = `vscode-insiders://` - break - case 'vscode': - editorUri = `vscode://` - break - default: - editorUri = `vscode://` - } - - const fullUri = `${editorUri}mlhiter.devbox-sealos?sshDomain=${encodeURIComponent( - `${userName}@${SEALOS_DOMAIN}` - )}&sshPort=${encodeURIComponent( - devbox.sshPort as number - )}&base64PrivateKey=${encodeURIComponent( - base64PrivateKey - )}&sshHostLabel=${encodeURIComponent( - `${SEALOS_DOMAIN}/${NAMESPACE}/${devbox.name}` - )}&workingDir=${encodeURIComponent(workingDir)}` + const { screenWidth } = useGlobalStore() + const { devboxDetail } = useDevboxStore() + const { setLoading } = useGlobalStore() - window.location.href = fullUri - } catch (error: any) { - console.error(error, '==') - } - }, - [] - ) + const [delDevbox, setDelDevbox] = useState(null) + const isBigButton = useMemo(() => screenWidth > 1000, [screenWidth]) const handlePauseDevbox = useCallback( async (devbox: DevboxDetailType) => { @@ -182,7 +99,8 @@ const Header = ({ [setLoading, t, toast, refetchDevboxDetail] ) return ( - + + {/* left back button and title */} {devboxDetail.name} + {/* detail button */} {!isLargeScreen && ( - + - - + + } - _before={{ - content: '""', - position: 'absolute', - left: 0, - top: '50%', - transform: 'translateY(-50%)', - width: '1px', - height: '20px', - backgroundColor: 'grayModern.250' + rightButtonProps={{ + height: '40px', + borderWidth: '1 1 1 0', + bg: 'white', + color: 'grayModern.600', + mr: 0, + boxShadow: + '2px 1px 2px 0px rgba(19, 51, 107, 0.05),0px 0px 1px 0px rgba(19, 51, 107, 0.08)' }} /> - - {[ - { value: 'vscode' as IDEType, label: 'VSCode' }, - { value: 'cursor' as IDEType, label: 'Cursor' }, - { value: 'vscodeInsider' as IDEType, label: 'VSCode Insider' } - ].map((item) => ( - setCurrentIDE(item.value)} - icon={} - _hover={{ - bg: '#1118240D', - borderRadius: 4 - }} - _focus={{ - bg: '#1118240D', - borderRadius: 4 - }}> - - {item.label} - {currentIDE === item.value && } - - - ))} - - + {devboxDetail.status.value === 'Running' && ( )} {devboxDetail.status.value === 'Stopped' && ( )} - + {devboxDetail.status.value !== 'Stopped' && ( + + )} {delDevbox && ( @@ -380,7 +239,6 @@ const Header = ({ onClose={() => setDelDevbox(null)} onSuccess={() => { setDelDevbox(null) - refetchDevboxDetail() router.push('/') }} /> 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 44fd348bd6f..8cf38bb7f81 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 @@ -5,11 +5,13 @@ import { Box, Button, Flex, Text, Tooltip, useDisclosure } from '@chakra-ui/reac import MyIcon from '@/components/Icon' import MyTable from '@/components/MyTable' +import PodLineChart from '@/components/PodLineChart' + import { useCopyData } from '@/utils/tools' import { NetworkType } from '@/types/devbox' + +import { useEnvStore } from '@/stores/env' import { useDevboxStore } from '@/stores/devbox' -import PodLineChart from '@/components/PodLineChart' -import { NAMESPACE, SEALOS_DOMAIN } from '@/stores/static' const MonitorModal = dynamic(() => import('@/components/modals/MonitorModal')) @@ -17,6 +19,7 @@ const MainBody = () => { const t = useTranslations() const { copyData } = useCopyData() const { devboxDetail } = useDevboxStore() + const { env } = useEnvStore() const { isOpen, onOpen, onClose } = useDisclosure() const networkColumn: { @@ -47,8 +50,10 @@ const MainBody = () => { ml={4} color={'grayModern.600'} onClick={() => - copyData(`http://${devboxDetail?.name}.${NAMESPACE}.svc.cluster.local:${item.port}`) - }>{`http://${devboxDetail?.name}.${NAMESPACE}.svc.cluster.local:${item.port}`} + copyData( + `http://${devboxDetail?.name}.${env.namespace}.svc.cluster.local:${item.port}` + ) + }>{`http://${devboxDetail?.name}.${env.namespace}.svc.cluster.local:${item.port}`} ) } @@ -58,7 +63,7 @@ const MainBody = () => { key: 'externalAddress', render: (item: NetworkType) => { if (item.openPublicDomain) { - const address = item.customDomain || `${item.publicDomain}.${SEALOS_DOMAIN}` + const address = item.customDomain || item.publicDomain return ( { {t('cpu')} {devboxDetail?.usedCpu?.yData[devboxDetail?.usedCpu?.yData?.length - 1]}% - - + + 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 ab9ed497060..20b250b4f7a 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 @@ -7,26 +7,31 @@ import { Box, Button, Flex, MenuButton, Text, useDisclosure } from '@chakra-ui/r import MyIcon from '@/components/Icon' import MyTable from '@/components/MyTable' -import { useLoading } from '@/hooks/useLoading' -import { useDevboxStore } from '@/stores/devbox' import DevboxStatusTag from '@/components/DevboxStatusTag' -import { DevboxVersionListItemType } from '@/types/devbox' import ReleaseModal from '@/components/modals/releaseModal' -import { delDevboxVersionByName, getSSHRuntimeInfo } from '@/api/devbox' import EditVersionDesModal from '@/components/modals/EditVersionDesModal' -import { NAMESPACE, REGISTRY_ADDR, SEALOS_DOMAIN } from '@/stores/static' + +import { DevboxVersionListItemType } from '@/types/devbox' import { DevboxReleaseStatusEnum } from '@/constants/devbox' +import { delDevboxVersionByName, getSSHRuntimeInfo } from '@/api/devbox' + import { useConfirm } from '@/hooks/useConfirm' +import { useLoading } from '@/hooks/useLoading' + +import { useEnvStore } from '@/stores/env' +import { useDevboxStore } from '@/stores/devbox' const Version = () => { const t = useTranslations() - const { devboxDetail: devbox } = useDevboxStore() 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 [initialized, setInitialized] = useState(false) const [onOpenRelease, setOnOpenRelease] = useState(false) - const { devboxVersionList, setDevboxVersionList } = useDevboxStore() - const { isOpen: isOpenEdit, onOpen: onOpenEdit, onClose: onCloseEdit } = useDisclosure() const [currentVersion, setCurrentVersion] = useState(null) const { openConfirm, ConfirmChild } = useConfirm({ @@ -37,7 +42,11 @@ const Version = () => { ['initDevboxVersionList'], () => setDevboxVersionList(devbox.name, devbox.id), { - refetchInterval: 3000, + refetchInterval: + devboxVersionList.length > 0 && + devboxVersionList[0].status.value !== DevboxReleaseStatusEnum.Success + ? 3000 + : false, onSettled() { setInitialized(true) } @@ -53,7 +62,7 @@ const Version = () => { port: network.port, protocol: network.protocol, openPublicDomain: network.openPublicDomain, - domain: SEALOS_DOMAIN + domain: env.ingressDomain } }) @@ -61,7 +70,7 @@ const Version = () => { appName: `${name}-release`, cpu: cpu, memory: memory, - imageName: `${REGISTRY_ADDR}/${NAMESPACE}/${devbox.name}:${version.tag}`, + imageName: `${env.registryAddr}/${env.namespace}/${devbox.name}:${version.tag}`, networks: newNetworks.length > 0 ? newNetworks @@ -70,13 +79,12 @@ const Version = () => { port: 80, protocol: 'http', openPublicDomain: false, - domain: SEALOS_DOMAIN + domain: env.ingressDomain } ], runCMD: releaseCommand, cmdParam: releaseArgs } - console.log('transformData', transformData) const formData = encodeURIComponent(JSON.stringify(transformData)) @@ -90,7 +98,7 @@ const Version = () => { } }) }, - [devbox] + [devbox, env.ingressDomain, env.namespace, env.registryAddr] ) const handleDelDevboxVersion = useCallback( @@ -221,8 +229,16 @@ const Version = () => { } ] return ( - - + + @@ -242,7 +258,7 @@ const Version = () => { {t('release_version')} - + {devboxVersionList.length === 0 && initialized ? ( { const devboxName = params.name const { Loading } = useLoading() - const [initialized, setInitialized] = useState(false) - const { devboxDetail, setDevboxDetail, loadDetailMonitorData } = useDevboxStore() + + const { env } = useEnvStore() const { screenWidth } = useGlobalStore() + const { devboxDetail, setDevboxDetail, loadDetailMonitorData, intervalLoadPods } = + useDevboxStore() + const [showSlider, setShowSlider] = useState(false) + const [initialized, setInitialized] = useState(false) const isLargeScreen = useMemo(() => screenWidth > 1280, [screenWidth]) - const { refetch } = useQuery(['initDevboxDetail'], () => setDevboxDetail(devboxName), { - refetchOnMount: true, - refetchInterval: 1 * 60 * 1000, - onSettled() { - setInitialized(true) - }, - onSuccess: (data) => { - if (data) { - loadDetailMonitorData(data.name) + const { refetch } = useQuery( + ['initDevboxDetail'], + () => setDevboxDetail(devboxName, env.sealosDomain), + { + onSettled() { + setInitialized(true) } } - }) + ) + + useQuery( + ['devbox-detail-pod'], + () => { + if (devboxDetail?.isPause) return null + return intervalLoadPods(devboxName, true) + }, + { + refetchOnMount: true, + refetchInterval: 3000 + } + ) useQuery( ['loadDetailMonitorData', devboxName, devboxDetail?.isPause], @@ -68,6 +83,7 @@ const DevboxDetailPage = ({ params }: { params: { name: string } }) => { zIndex={1} transition={'0.4s'} bg={'white'} + borderWidth={1} borderRadius={'lg'} {...(isLargeScreen ? {} @@ -79,11 +95,23 @@ const DevboxDetailPage = ({ params }: { params: { name: string } }) => { })}> - + - + diff --git a/frontend/providers/devbox/app/[lang]/(platform)/layout.tsx b/frontend/providers/devbox/app/[lang]/(platform)/layout.tsx index c1190e16c94..b8c92913113 100644 --- a/frontend/providers/devbox/app/[lang]/(platform)/layout.tsx +++ b/frontend/providers/devbox/app/[lang]/(platform)/layout.tsx @@ -2,20 +2,18 @@ import throttle from 'lodash/throttle' import { useEffect, useState } from 'react' -import { usePathname, useRouter } from '@/i18n' import { EVENT_NAME } from 'sealos-desktop-sdk' +import { usePathname, useRouter } from '@/i18n' import { createSealosApp, sealosApp } from 'sealos-desktop-sdk/app' -import { - getEnv, - getGlobalNamespace, - getRuntime, - getUserPrice, - SEALOS_DOMAIN -} from '@/stores/static' -import { useGlobalStore } from '@/stores/global' import { useLoading } from '@/hooks/useLoading' import { useConfirm } from '@/hooks/useConfirm' + +import { useEnvStore } from '@/stores/env' +import { useGlobalStore } from '@/stores/global' +import { usePriceStore } from '@/stores/price' +import { useRuntimeStore } from '@/stores/runtime' + import { getLangStore, setLangStore } from '@/utils/cookie' import QueryProvider from '@/components/providers/MyQueryProvider' import ChakraProvider from '@/components/providers/MyChakraProvider' @@ -25,6 +23,9 @@ export default function PlatformLayout({ children }: { children: React.ReactNode const router = useRouter() const pathname = usePathname() const { Loading } = useLoading() + const { setEnv, env } = useEnvStore() + const { setRuntime } = useRuntimeStore() + const { setSourcePrice } = usePriceStore() const [refresh, setRefresh] = useState(false) const { setScreenWidth, loading, setLastRoute } = useGlobalStore() const { openConfirm, ConfirmChild } = useConfirm({ @@ -49,7 +50,7 @@ export default function PlatformLayout({ children }: { children: React.ReactNode if (!process.env.NEXT_PUBLIC_MOCK_USER) { localStorage.removeItem('session') openConfirm(() => { - window.open(`https://${SEALOS_DOMAIN}`, '_self') + window.open(`https://${env.sealosDomain}`, '_self') })() } } @@ -59,10 +60,9 @@ export default function PlatformLayout({ children }: { children: React.ReactNode }, []) useEffect(() => { - getUserPrice() - getRuntime() - getEnv() - getGlobalNamespace() + setSourcePrice() + setRuntime() + setEnv() const changeI18n = async (data: any) => { const lastLang = getLangStore() const newLang = data.currentLanguage diff --git a/frontend/providers/devbox/app/api/createDevbox/route.ts b/frontend/providers/devbox/app/api/createDevbox/route.ts index 6a52b87a3a5..147481be6d6 100644 --- a/frontend/providers/devbox/app/api/createDevbox/route.ts +++ b/frontend/providers/devbox/app/api/createDevbox/route.ts @@ -10,6 +10,7 @@ export const dynamic = 'force-dynamic' export async function POST(req: NextRequest) { try { + // NOTE: runtimeNamespaceMap will be too big? const { devboxForm, runtimeNamespaceMap } = (await req.json()) as { devboxForm: DevboxEditType runtimeNamespaceMap: runtimeNamespaceMapType @@ -21,7 +22,7 @@ export async function POST(req: NextRequest) { kubeconfig: await authSession(headerList) }) - const { SEALOS_DOMAIN, INGRESS_SECRET, DEVBOX_AFFINITY_ENABLE, SQUASH_ENABLE } = process.env + const { INGRESS_SECRET, DEVBOX_AFFINITY_ENABLE, SQUASH_ENABLE } = process.env const devbox = json2Devbox( devboxForm, runtimeNamespaceMap, @@ -29,7 +30,7 @@ export async function POST(req: NextRequest) { SQUASH_ENABLE ) const service = json2Service(devboxForm) - const ingress = json2Ingress(devboxForm, SEALOS_DOMAIN as string, INGRESS_SECRET as string) + const ingress = json2Ingress(devboxForm, INGRESS_SECRET as string) await applyYamlList([devbox, service, ingress], 'create') diff --git a/frontend/providers/devbox/app/api/delDevbox/route.ts b/frontend/providers/devbox/app/api/delDevbox/route.ts index 445a822f7f3..4045ba672e5 100644 --- a/frontend/providers/devbox/app/api/delDevbox/route.ts +++ b/frontend/providers/devbox/app/api/delDevbox/route.ts @@ -1,5 +1,6 @@ import { NextRequest } from 'next/server' +import { devboxKey } from '@/constants/devbox' import { jsonRes } from '@/services/backend/response' import { authSession } from '@/services/backend/auth' import { getK8s } from '@/services/backend/kubernetes' @@ -10,7 +11,6 @@ export async function DELETE(req: NextRequest) { try { const { searchParams } = req.nextUrl const devboxName = searchParams.get('devboxName') as string - const networks = JSON.parse(searchParams.get('networks') as string) as string[] const headerList = req.headers const { k8sCustomObjects, k8sCore, namespace } = await getK8s({ @@ -25,33 +25,54 @@ export async function DELETE(req: NextRequest) { devboxName ) + const ingressResponse = (await k8sCustomObjects.listNamespacedCustomObject( + 'networking.k8s.io', + 'v1', + namespace, + 'ingresses', + undefined, + undefined, + undefined, + undefined, + `${devboxKey}=${devboxName}` + )) as { + body: { + items: any[] + } + } + + const ingressList = ingressResponse.body.items + // delete service and ingress at the same time - if (networks.length > 0) { - await k8sCore.deleteNamespacedService(devboxName, namespace) - networks.forEach(async (networkName: string) => { - await k8sCustomObjects.deleteNamespacedCustomObject( - 'networking.k8s.io', - 'v1', - namespace, - 'ingresses', - networkName - ) - // delete issuer and certificate at the same time - await k8sCustomObjects.deleteNamespacedCustomObject( - 'cert-manager.io', - 'v1', - namespace, - 'issuers', - networkName - ) - await k8sCustomObjects.deleteNamespacedCustomObject( - 'cert-manager.io', - 'v1', - namespace, - 'certificates', - networkName - ) + if (ingressList.length > 0) { + const deleteServicePromise = k8sCore.deleteNamespacedService(devboxName, namespace) + + const deletePromises = ingressList.map(async (ingress: any) => { + const networkName = ingress.metadata.name + + const safeDelete = async (group: string, version: string, plural: string, name: string) => { + try { + await k8sCustomObjects.deleteNamespacedCustomObject( + group, + version, + namespace, + plural, + name + ) + } catch (err) { + console.warn('Failed to delete an item, ignoring:', plural, name, err) + } + } + + return Promise.all([ + safeDelete('networking.k8s.io', 'v1', 'ingresses', networkName), + // this two muse have customDomain + safeDelete('cert-manager.io', 'v1', 'issuers', networkName), + safeDelete('cert-manager.io', 'v1', 'certificates', networkName) + ]) }) + + await Promise.all([deleteServicePromise, ...deletePromises]) } return jsonRes({ diff --git a/frontend/providers/devbox/app/api/getDevboxByName/route.ts b/frontend/providers/devbox/app/api/getDevboxByName/route.ts new file mode 100644 index 00000000000..2f466a556fa --- /dev/null +++ b/frontend/providers/devbox/app/api/getDevboxByName/route.ts @@ -0,0 +1,131 @@ +import { NextRequest } from 'next/server' + +import { defaultEnv } from '@/stores/env' +import { authSession } from '@/services/backend/auth' +import { jsonRes } from '@/services/backend/response' +import { getK8s } from '@/services/backend/kubernetes' +import { KBDevboxType, KBRuntimeType } from '@/types/k8s' +import { devboxKey, publicDomainKey } from '@/constants/devbox' + +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 + + if (!devboxName) { + return jsonRes({ + code: 400, + error: 'devboxName is required' + }) + } + + const { k8sCustomObjects, namespace, k8sCore } = await getK8s({ + kubeconfig: await authSession(headerList) + }) + + const { body: devboxBody } = (await k8sCustomObjects.getNamespacedCustomObject( + 'devbox.sealos.io', + 'v1alpha1', + namespace, + 'devboxes', + devboxName + )) as { body: KBDevboxType } + const { body: runtimeBody } = (await k8sCustomObjects.getNamespacedCustomObject( + 'devbox.sealos.io', + 'v1alpha1', + ROOT_RUNTIME_NAMESPACE || defaultEnv.rootRuntimeNamespace, + 'runtimes', + devboxBody.spec.runtimeRef.name + )) as { body: KBRuntimeType } + + // add runtimeType, runtimeVersion, runtimeNamespace, networks to devbox yaml + let resp = { + ...devboxBody, + spec: { + ...devboxBody.spec, + runtimeType: runtimeBody.spec.classRef + // NOTE: where use runtimeNamespace + }, + portInfos: [] + } as KBDevboxType & { portInfos: any[] } + + if (devboxBody.spec.network.extraPorts.length === 0) { + return jsonRes({ data: resp }) + } + + // get ingresses and certificates and service + const [ingressesResponse, certificatesResponse, serviceResponse] = await Promise.all([ + k8sCustomObjects.listNamespacedCustomObject( + 'networking.k8s.io', + 'v1', + namespace, + 'ingresses', + undefined, + undefined, + undefined, + undefined, + `${devboxKey}=${devboxName}` + ), + k8sCustomObjects.listNamespacedCustomObject( + 'cert-manager.io', + 'v1', + namespace, + 'certificates', + undefined, + undefined, + undefined, + undefined, + `${devboxKey}=${devboxName}` + ), + k8sCore.readNamespacedService(devboxName, namespace).catch(() => null) + ]) + const ingresses: any = ingressesResponse.body + const certificates: any = certificatesResponse.body + const service = serviceResponse?.body + + const customDomain = certificates.items[0]?.spec.dnsNames[0] + const ingressList = ingresses.items.map((item: any) => ({ + networkName: item.metadata.name, + port: item.spec.rules[0].http.paths[0].backend.service.port.number, + protocol: item.metadata.annotations['nginx.ingress.kubernetes.io/backend-protocol'], + openPublicDomain: !!item.metadata.labels[publicDomainKey], + publicDomain: item.spec.tls[0].hosts[0], + customDomain: customDomain || '' + })) + + resp.portInfos = devboxBody.spec.network.extraPorts.map((network: any) => { + const matchingIngress = ingressList.find( + (ingress: any) => ingress.port === network.containerPort + ) + + const servicePort = service?.spec?.ports?.find( + (port: any) => port.port === network.containerPort + ) + const servicePortName = servicePort?.name + + if (matchingIngress) { + return { + ...matchingIngress, + portName: servicePortName + } + } + + return { + ...network, + port: network.containerPort + } + }) + + return jsonRes({ data: resp }) + } catch (err: any) { + return jsonRes({ + code: 500, + error: err + }) + } +} diff --git a/frontend/providers/devbox/app/api/getDevboxList/route.ts b/frontend/providers/devbox/app/api/getDevboxList/route.ts index 4c928532edc..4cfec70a4d1 100644 --- a/frontend/providers/devbox/app/api/getDevboxList/route.ts +++ b/frontend/providers/devbox/app/api/getDevboxList/route.ts @@ -1,119 +1,50 @@ import { NextRequest } from 'next/server' -import { runtimeNamespace } from '@/stores/static' +import { defaultEnv } from '@/stores/env' import { authSession } from '@/services/backend/auth' import { jsonRes } from '@/services/backend/response' import { getK8s } from '@/services/backend/kubernetes' import { KBDevboxType, KBRuntimeType } from '@/types/k8s' -import { devboxKey, publicDomainKey } from '@/constants/devbox' export const dynamic = 'force-dynamic' export async function GET(req: NextRequest) { try { const headerList = req.headers + const { ROOT_RUNTIME_NAMESPACE } = process.env - const { k8sCustomObjects, namespace, k8sCore } = await getK8s({ + const { k8sCustomObjects, namespace } = await getK8s({ kubeconfig: await authSession(headerList) }) - const { body: devboxBody } = (await k8sCustomObjects.listNamespacedCustomObject( - 'devbox.sealos.io', - 'v1alpha1', - namespace, - 'devboxes' - )) as { body: { items: KBDevboxType[] } } - - const { body: runtimeBody } = (await k8sCustomObjects.listNamespacedCustomObject( - 'devbox.sealos.io', - 'v1alpha1', - runtimeNamespace, - 'runtimes' - )) as { body: { items: KBRuntimeType[] } } - - // add runtimeType, runtimeVersion, runtimeNamespace, networks to devbox yaml - const res = devboxBody.items.map(async (item: any) => { - const devboxName = item.metadata.name - const runtimeName = item.spec.runtimeRef.name - const runtime = runtimeBody.items.find((item: any) => item.metadata.name === runtimeName) - - item.spec.runtimeType = runtime?.spec.classRef - item.spec.runtimeVersion = runtime?.metadata.name - item.spec.runtimeNamespace = runtime?.metadata.namespace - - const { body: ingresses }: any = await k8sCustomObjects.listNamespacedCustomObject( - 'networking.k8s.io', - 'v1', + const [devboxResponse, runtimeResponse] = await Promise.all([ + k8sCustomObjects.listNamespacedCustomObject( + 'devbox.sealos.io', + 'v1alpha1', namespace, - 'ingresses', - undefined, - undefined, - undefined, - undefined, - `${devboxKey}=${devboxName}` + 'devboxes' + ), + k8sCustomObjects.listNamespacedCustomObject( + 'devbox.sealos.io', + 'v1alpha1', + ROOT_RUNTIME_NAMESPACE || defaultEnv.rootRuntimeNamespace, + 'runtimes' ) - const { body: certificates }: any = await k8sCustomObjects.listNamespacedCustomObject( - 'cert-manager.io', - 'v1', - namespace, - 'certificates', - undefined, - undefined, - undefined, - undefined, - `${devboxKey}=${devboxName}` - ) - const customDomain = certificates.items[0]?.spec.dnsNames[0] - const ingressList = ingresses.items.map((item: any) => { - return { - networkName: item.metadata.name, - port: item.spec.rules[0].http.paths[0].backend.service.port.number, - protocol: item.metadata.annotations['nginx.ingress.kubernetes.io/backend-protocol'], - openPublicDomain: !!item.metadata.labels[publicDomainKey], - publicDomain: item.metadata.labels[publicDomainKey], - customDomain: customDomain || '' - } - }) - if (item.spec.network.extraPorts.length !== 0) { - try { - const { body: service } = await k8sCore.readNamespacedService(devboxName, namespace) - const portInfos = item.spec.network.extraPorts.map(async (network: any) => { - const matchingIngress = ingressList.find( - (ingress: any) => ingress.port === network.containerPort - ) + ]) - const servicePort = service.spec?.ports?.find( - (port: any) => port.port === network.containerPort - ) - const servicePortName = servicePort?.name + const devboxBody = devboxResponse.body as { items: KBDevboxType[] } + const runtimeBody = runtimeResponse.body as { items: KBRuntimeType[] } - if (matchingIngress) { - return { - networkName: matchingIngress.networkName, - port: matchingIngress.port, - portName: servicePortName, - protocol: matchingIngress.protocol, - openPublicDomain: matchingIngress.openPublicDomain, - publicDomain: matchingIngress.publicDomain, - customDomain: matchingIngress.customDomain - } - } + // add runtimeType to devbox yaml + const resp = devboxBody.items.map((item) => { + const runtimeName = item.spec.runtimeRef.name + const runtime = runtimeBody.items.find((item) => item.metadata.name === runtimeName) - return { - ...network, - port: network.containerPort - } - }) - item.portInfos = await Promise.all(portInfos) - } catch (error) { - // no service just null array - item.portInfos = [] - } - } + item.spec.runtimeType = runtime?.spec.classRef return item }) - const resp = await Promise.all(res) + return jsonRes({ data: resp }) } catch (err: any) { return jsonRes({ diff --git a/frontend/providers/devbox/app/api/getEnv/route.ts b/frontend/providers/devbox/app/api/getEnv/route.ts index ae527a037ba..6b3d1a8dc41 100644 --- a/frontend/providers/devbox/app/api/getEnv/route.ts +++ b/frontend/providers/devbox/app/api/getEnv/route.ts @@ -1,23 +1,37 @@ -import { jsonRes } from '@/services/backend/response' +import { NextRequest } from 'next/server' -export type SystemEnvResponse = { - domain: string - ingressSecret: string - registryAddr: string - devboxAffinityEnable: string - squashEnable: string -} +import type { Env } from '@/types/static' +import { defaultEnv } from '@/stores/env' +import { jsonRes } from '@/services/backend/response' +import { getK8s } from '@/services/backend/kubernetes' +import { authSession } from '@/services/backend/auth' export const dynamic = 'force-dynamic' -export async function GET() { - return jsonRes({ - data: { - domain: process.env.SEALOS_DOMAIN || 'dev.sealos.plus', - ingressSecret: process.env.INGRESS_SECRET || 'wildcard-cert', - registryAddr: process.env.REGISTRY_ADDR || 'hub.dev.sealos.plus', - devboxAffinityEnable: process.env.DEVBOX_AFFINITY_ENABLE || 'true', - squashEnable: process.env.SQUASH_ENABLE || 'true' - } - }) +export async function GET(req: NextRequest) { + try { + const headerList = req.headers + + const { namespace } = await getK8s({ + kubeconfig: await authSession(headerList) + }) + + return jsonRes({ + data: { + sealosDomain: process.env.SEALOS_DOMAIN || defaultEnv.sealosDomain, + ingressSecret: process.env.INGRESS_SECRET || defaultEnv.ingressSecret, + registryAddr: process.env.REGISTRY_ADDR || defaultEnv.registryAddr, + devboxAffinityEnable: process.env.DEVBOX_AFFINITY_ENABLE || defaultEnv.devboxAffinityEnable, + squashEnable: process.env.SQUASH_ENABLE || defaultEnv.squashEnable, + namespace: namespace || defaultEnv.namespace, + rootRuntimeNamespace: process.env.ROOT_RUNTIME_NAMESPACE || defaultEnv.rootRuntimeNamespace, + ingressDomain: process.env.INGRESS_DOMAIN || defaultEnv.ingressDomain + } + }) + } catch (err: any) { + return jsonRes({ + code: 500, + error: err + }) + } } diff --git a/frontend/providers/devbox/app/api/getSSHConnectionInfo/route.ts b/frontend/providers/devbox/app/api/getSSHConnectionInfo/route.ts index 780d9a78567..444873e1cad 100644 --- a/frontend/providers/devbox/app/api/getSSHConnectionInfo/route.ts +++ b/frontend/providers/devbox/app/api/getSSHConnectionInfo/route.ts @@ -1,10 +1,10 @@ import { NextRequest } from 'next/server' +import { defaultEnv } from '@/stores/env' import { KBRuntimeType } from '@/types/k8s' -import { runtimeNamespace } from '@/stores/static' -import { authSession, generateAccessToken } from '@/services/backend/auth' import { jsonRes } from '@/services/backend/response' import { getK8s } from '@/services/backend/kubernetes' +import { authSession, generateAccessToken } from '@/services/backend/auth' export const dynamic = 'force-dynamic' @@ -13,8 +13,11 @@ export async function GET(req: NextRequest) { const { searchParams } = req.nextUrl const devboxName = searchParams.get('devboxName') as string const runtimeName = searchParams.get('runtimeName') as string + const headerList = req.headers + const { ROOT_RUNTIME_NAMESPACE } = process.env + const { k8sCore, namespace, k8sCustomObjects } = await getK8s({ kubeconfig: await authSession(headerList) }) @@ -33,7 +36,7 @@ export async function GET(req: NextRequest) { const { body: runtime } = (await k8sCustomObjects.getNamespacedCustomObject( 'devbox.sealos.io', 'v1alpha1', - runtimeNamespace, + ROOT_RUNTIME_NAMESPACE || defaultEnv.rootRuntimeNamespace, 'runtimes', runtimeName )) as { body: KBRuntimeType } diff --git a/frontend/providers/devbox/app/api/getSSHRuntimeInfo/route.ts b/frontend/providers/devbox/app/api/getSSHRuntimeInfo/route.ts index a6d6b4b8aaa..69fb62cc63b 100644 --- a/frontend/providers/devbox/app/api/getSSHRuntimeInfo/route.ts +++ b/frontend/providers/devbox/app/api/getSSHRuntimeInfo/route.ts @@ -1,7 +1,7 @@ import { NextRequest } from 'next/server' import { KBRuntimeType } from '@/types/k8s' -import { runtimeNamespace } from '@/stores/static' +import { defaultEnv } from '@/stores/env' import { authSession } from '@/services/backend/auth' import { jsonRes } from '@/services/backend/response' import { getK8s } from '@/services/backend/kubernetes' @@ -13,6 +13,7 @@ export async function GET(req: NextRequest) { const { searchParams } = req.nextUrl const runtimeName = searchParams.get('runtimeName') as string const headerList = req.headers + const { ROOT_RUNTIME_NAMESPACE } = process.env const { k8sCustomObjects } = await getK8s({ kubeconfig: await authSession(headerList) @@ -21,7 +22,7 @@ export async function GET(req: NextRequest) { const { body: runtime } = (await k8sCustomObjects.getNamespacedCustomObject( 'devbox.sealos.io', 'v1alpha1', - runtimeNamespace, + ROOT_RUNTIME_NAMESPACE || defaultEnv.rootRuntimeNamespace, 'runtimes', runtimeName )) as { body: KBRuntimeType } diff --git a/frontend/providers/devbox/app/api/platform/getNamespace/route.ts b/frontend/providers/devbox/app/api/platform/getNamespace/route.ts deleted file mode 100644 index f30c460cb0a..00000000000 --- a/frontend/providers/devbox/app/api/platform/getNamespace/route.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { NextRequest } from 'next/server' - -import { authSession } from '@/services/backend/auth' -import { jsonRes } from '@/services/backend/response' -import { getK8s } from '@/services/backend/kubernetes' - -export const dynamic = 'force-dynamic' - -export async function GET(req: NextRequest) { - try { - const headerList = req.headers - - const { namespace } = await getK8s({ - kubeconfig: await authSession(headerList) - }) - - return jsonRes({ data: namespace }) - } catch (err: any) { - return jsonRes({ - code: 500, - error: err - }) - } -} diff --git a/frontend/providers/devbox/app/api/platform/getRuntime/route.ts b/frontend/providers/devbox/app/api/platform/getRuntime/route.ts index e87040863aa..8742092a3ac 100644 --- a/frontend/providers/devbox/app/api/platform/getRuntime/route.ts +++ b/frontend/providers/devbox/app/api/platform/getRuntime/route.ts @@ -1,6 +1,6 @@ import { NextRequest } from 'next/server' -import { runtimeNamespace } from '@/stores/static' +import { defaultEnv } from '@/stores/env' import { authSession } from '@/services/backend/auth' import { jsonRes } from '@/services/backend/response' import { getK8s } from '@/services/backend/kubernetes' @@ -19,6 +19,8 @@ export async function GET(req: NextRequest) { const osVersionMap: VersionMapType = {} const runtimeNamespaceMap: runtimeNamespaceMapType = {} + const { ROOT_RUNTIME_NAMESPACE } = process.env + const headerList = req.headers const { k8sCustomObjects } = await getK8s({ @@ -28,13 +30,13 @@ export async function GET(req: NextRequest) { const { body: runtimeClasses } = (await k8sCustomObjects.listNamespacedCustomObject( 'devbox.sealos.io', 'v1alpha1', - runtimeNamespace, + ROOT_RUNTIME_NAMESPACE || defaultEnv.rootRuntimeNamespace, 'runtimeclasses' )) as { body: { items: KBRuntimeClassType[] } } const { body: _runtimes } = (await k8sCustomObjects.listNamespacedCustomObject( 'devbox.sealos.io', 'v1alpha1', - runtimeNamespace, + ROOT_RUNTIME_NAMESPACE || defaultEnv.rootRuntimeNamespace, 'runtimes' )) as { body: { items: KBRuntimeType[] } } @@ -133,7 +135,6 @@ export async function GET(req: NextRequest) { } } }) - console.log('languageVersionMap', languageVersionMap) return jsonRes({ data: { diff --git a/frontend/providers/devbox/app/api/platform/resourcePrice/route.ts b/frontend/providers/devbox/app/api/platform/resourcePrice/route.ts index 6f14b6e4076..2c791b5f300 100644 --- a/frontend/providers/devbox/app/api/platform/resourcePrice/route.ts +++ b/frontend/providers/devbox/app/api/platform/resourcePrice/route.ts @@ -5,12 +5,6 @@ import { jsonRes } from '@/services/backend/response' export const dynamic = 'force-dynamic' -export type Response = { - cpu: number - memory: number - nodeports: number -} - type ResourcePriceType = { data: { properties: { @@ -45,17 +39,12 @@ const valuationMap: Record = { export async function GET(req: NextRequest) { try { const { ACCOUNT_URL, SEALOS_DOMAIN } = process.env - const baseUrl = ACCOUNT_URL - ? ACCOUNT_URL - : `https://account-api.${SEALOS_DOMAIN}`; + const baseUrl = ACCOUNT_URL ? ACCOUNT_URL : `https://account-api.${SEALOS_DOMAIN}` const getResourcePrice = async () => { try { - const res = await fetch( - `${baseUrl}/account/v1alpha1/properties`, - { - method: 'POST' - } - ) + const res = await fetch(`${baseUrl}/account/v1alpha1/properties`, { + method: 'POST' + }) const data: ResourcePriceType = await res.json() diff --git a/frontend/providers/devbox/app/api/v1/getDevboxDetail/route.ts b/frontend/providers/devbox/app/api/v1/getDevboxDetail/route.ts index 9d3d62f2b65..e49e5527581 100644 --- a/frontend/providers/devbox/app/api/v1/getDevboxDetail/route.ts +++ b/frontend/providers/devbox/app/api/v1/getDevboxDetail/route.ts @@ -1,13 +1,12 @@ import { NextRequest } from 'next/server' +import { KBDevboxType } from '@/types/k8s' import { devboxKey } from '@/constants/devbox' -import { getPayloadWithoutVerification, verifyToken } from '@/services/backend/auth' -import { getK8s } from '@/services/backend/kubernetes' +import { KbPgClusterType } from '@/types/cluster' import { jsonRes } from '@/services/backend/response' -import { KBDevboxType } from '@/types/k8s' +import { getK8s } from '@/services/backend/kubernetes' +import { getPayloadWithoutVerification, verifyToken } from '@/services/backend/auth' import { adaptDBListItem, adaptDevboxListItem, adaptIngressListItem } from '@/utils/adapt' -import { KbPgClusterType } from '@/types/cluster' -import { V1Ingress } from '@kubernetes/client-node' export const dynamic = 'force-dynamic' diff --git a/frontend/providers/devbox/components/IDEButton.tsx b/frontend/providers/devbox/components/IDEButton.tsx new file mode 100644 index 00000000000..057d2d600ec --- /dev/null +++ b/frontend/providers/devbox/components/IDEButton.tsx @@ -0,0 +1,222 @@ +import { + Button, + Flex, + Menu, + MenuButton, + MenuList, + Tooltip, + IconButton, + MenuItem, + ButtonProps +} from '@chakra-ui/react' +import { useMessage } from '@sealos/ui' +import { useTranslations } from 'next-intl' +import { useCallback, useState } from 'react' + +import MyIcon from './Icon' +import { useEnvStore } from '@/stores/env' +import { DevboxStatusMapType } from '@/types/devbox' +import { IDEType, useGlobalStore } from '@/stores/global' +import { getSSHConnectionInfo, getSSHRuntimeInfo } from '@/api/devbox' + +interface Props { + devboxName: string + runtimeVersion: string + sshPort: number + status: DevboxStatusMapType + isBigButton?: boolean + leftButtonProps?: ButtonProps + rightButtonProps?: ButtonProps +} + +const IDEButton = ({ + devboxName, + runtimeVersion, + sshPort, + status, + isBigButton = true, + leftButtonProps = {}, + rightButtonProps = {} +}: Props) => { + const t = useTranslations() + + const { env } = useEnvStore() + const { message: toast } = useMessage() + const [loading, setLoading] = useState(false) + const { setCurrentIDE, currentIDE } = useGlobalStore() + + const handleGotoIDE = useCallback( + async (currentIDE: string = 'vscode') => { + setLoading(true) + + toast({ + title: t('opening_ide'), + status: 'info' + }) + + try { + const { base64PrivateKey, userName } = await getSSHConnectionInfo({ + devboxName, + runtimeName: runtimeVersion + }) + const { workingDir } = await getSSHRuntimeInfo(runtimeVersion) + + let editorUri = '' + switch (currentIDE) { + case 'cursor': + editorUri = `cursor://` + break + case 'vscodeInsider': + editorUri = `vscode-insiders://` + break + case 'vscode': + editorUri = `vscode://` + break + default: + editorUri = `vscode://` + } + + const fullUri = `${editorUri}labring.devbox-aio?sshDomain=${encodeURIComponent( + `${userName}@${env.sealosDomain}` + )}&sshPort=${encodeURIComponent(sshPort)}&base64PrivateKey=${encodeURIComponent( + base64PrivateKey + )}&sshHostLabel=${encodeURIComponent( + `${env.sealosDomain}/${env.namespace}/${devboxName}` + )}&workingDir=${encodeURIComponent(workingDir)}` + + window.location.href = fullUri + } catch (error: any) { + console.error(error, '==') + } finally { + setLoading(false) + } + }, + [devboxName, env.namespace, env.sealosDomain, runtimeVersion, setLoading, sshPort, toast, t] + ) + + return ( + + + + + + } + _before={{ + content: '""', + position: 'absolute', + left: 0, + top: '50%', + transform: 'translateY(-50%)', + width: '1px', + height: '20px', + backgroundColor: 'grayModern.250' + }} + {...rightButtonProps} + /> + + {[ + { value: 'vscode' as IDEType, label: 'VSCode' }, + { value: 'cursor' as IDEType, label: 'Cursor' }, + { value: 'vscodeInsider' as IDEType, label: 'VSCode Insider' } + ].map((item) => ( + setCurrentIDE(item.value)} + icon={} + _hover={{ + bg: '#1118240D', + borderRadius: 4 + }} + _focus={{ + bg: '#1118240D', + borderRadius: 4 + }}> + + {item.label} + {currentIDE === item.value && } + + + ))} + + + + ) +} + +const getCurrentIDELabelAndIcon = ( + currentIDE: IDEType +): { + label: string + icon: IDEType +} => { + switch (currentIDE) { + case 'vscode': + return { + label: 'VSCode', + icon: 'vscode' + } + case 'cursor': + return { + label: 'Cursor', + icon: 'cursor' + } + case 'vscodeInsider': + return { + label: 'VSCode Insider', + icon: 'vscodeInsider' + } + default: + return { + label: 'VSCode', + icon: 'vscode' + } + } +} + +export default IDEButton diff --git a/frontend/providers/devbox/components/PriceBox.tsx b/frontend/providers/devbox/components/PriceBox.tsx index 82e82f57b49..5851c6b2372 100644 --- a/frontend/providers/devbox/components/PriceBox.tsx +++ b/frontend/providers/devbox/components/PriceBox.tsx @@ -1,9 +1,9 @@ import { useMemo } from 'react' +import { SealosCoin } from '@sealos/ui' import { useTranslations } from 'next-intl' import { Box, Flex, useTheme, Text } from '@chakra-ui/react' -import { SOURCE_PRICE } from '@/stores/static' -import { SealosCoin } from '@sealos/ui' +import { usePriceStore } from '@/stores/price' export const colorMap = { cpu: '#33BABB', @@ -22,6 +22,9 @@ const PriceBox = ({ }) => { const theme = useTheme() const t = useTranslations() + + const { sourcePrice } = usePriceStore() + const priceList: { label: string color: string @@ -33,9 +36,9 @@ const PriceBox = ({ let tp = 0 components.forEach(({ cpu, memory, nodeports }) => { - cp = (SOURCE_PRICE.cpu * cpu * 24) / 1000 - mp = (SOURCE_PRICE.memory * memory * 24) / 1024 - pp = SOURCE_PRICE.nodeports * nodeports * 24 + cp = (sourcePrice.cpu * cpu * 24) / 1000 + mp = (sourcePrice.memory * memory * 24) / 1024 + pp = sourcePrice.nodeports * nodeports * 24 tp = cp + mp + pp }) @@ -53,7 +56,7 @@ const PriceBox = ({ }, { label: 'total_price', color: '#485058', value: tp.toFixed(2) } ] - }, [components]) + }, [components, sourcePrice.cpu, sourcePrice.memory, sourcePrice.nodeports]) return ( diff --git a/frontend/providers/devbox/components/modals/CustomAccessModal.tsx b/frontend/providers/devbox/components/modals/CustomAccessModal.tsx index 063192325e6..e96539550f0 100644 --- a/frontend/providers/devbox/components/modals/CustomAccessModal.tsx +++ b/frontend/providers/devbox/components/modals/CustomAccessModal.tsx @@ -14,13 +14,12 @@ import { ModalHeader } from '@chakra-ui/react' import { Tip } from '@sealos/ui' +import React, { useRef } from 'react' import { useTranslations } from 'next-intl' -import React, { useMemo, useRef } from 'react' import { InfoOutlineIcon } from '@chakra-ui/icons' import { postAuthCname } from '@/api/platform' import { useRequest } from '@/hooks/useRequest' -import { SEALOS_DOMAIN } from '@/stores/static' export type CustomAccessModalParams = { publicDomain: string @@ -42,8 +41,6 @@ const CustomAccessModal = ({ mb: 2 } - const completePublicDomain = useMemo(() => `${publicDomain}.${SEALOS_DOMAIN}`, [publicDomain]) - const { mutate: authCNAME, isLoading } = useRequest({ mutationFn: async () => { const val = ref.current?.value || '' @@ -51,7 +48,7 @@ const CustomAccessModal = ({ return onSuccess('') } await postAuthCname({ - publicDomain: completePublicDomain, + publicDomain: publicDomain, customDomain: val }) return val @@ -77,7 +74,7 @@ const CustomAccessModal = ({ borderRadius={'md'} border={theme.borders.base} userSelect={'all'}> - {completePublicDomain} + {publicDomain} {t('Custom Domain')} @@ -95,7 +92,7 @@ const CustomAccessModal = ({ size={'sm'} whiteSpace={'pre-wrap'} icon={} - text={t('CNAME Tips', { domain: completePublicDomain })} + text={t('CNAME Tips', { domain: publicDomain })} /> diff --git a/frontend/providers/devbox/components/modals/DelModal.tsx b/frontend/providers/devbox/components/modals/DelModal.tsx index 61d28f09b40..08fd2990509 100644 --- a/frontend/providers/devbox/components/modals/DelModal.tsx +++ b/frontend/providers/devbox/components/modals/DelModal.tsx @@ -37,8 +37,7 @@ const DelModal = ({ const handleDelDevbox = useCallback(async () => { try { setLoading(true) - const networks = devbox.networks.map((item) => item.networkName) - await delDevbox(devbox.name, networks) + await delDevbox(devbox.name) toast({ title: t('delete_successful'), status: 'success' @@ -53,7 +52,7 @@ const DelModal = ({ console.error(error) } setLoading(false) - }, [devbox.networks, devbox.name, toast, t, onSuccess, onClose]) + }, [devbox.name, toast, t, onSuccess, onClose]) return ( @@ -81,7 +80,7 @@ const DelModal = ({ {t.rich('please_enter_devbox_name_confirm', { name: devbox.name, strong: (chunks) => ( - + {chunks} ) diff --git a/frontend/providers/devbox/components/modals/releaseModal.tsx b/frontend/providers/devbox/components/modals/releaseModal.tsx index f52dad2d41a..79154448ede 100644 --- a/frontend/providers/devbox/components/modals/releaseModal.tsx +++ b/frontend/providers/devbox/components/modals/releaseModal.tsx @@ -16,9 +16,9 @@ import { useMessage } from '@sealos/ui' import { useTranslations } from 'next-intl' import { useCallback, useState } from 'react' +import { useEnvStore } from '@/stores/env' import { useConfirm } from '@/hooks/useConfirm' import { DevboxListItemType } from '@/types/devbox' -import { NAMESPACE, REGISTRY_ADDR } from '@/stores/static' import { pauseDevbox, releaseDevbox, restartDevbox } from '@/api/devbox' const ReleaseModal = ({ @@ -31,11 +31,14 @@ const ReleaseModal = ({ onSuccess: () => void }) => { const t = useTranslations() - const [tag, setTag] = useState('') const { message: toast } = useMessage() + + const { env } = useEnvStore() + + const [tag, setTag] = useState('') const [loading, setLoading] = useState(false) - const [releaseDes, setReleaseDes] = useState('') const [tagError, setTagError] = useState(false) + const [releaseDes, setReleaseDes] = useState('') const { openConfirm, ConfirmChild } = useConfirm({ content: 'release_confirm_info', @@ -70,6 +73,7 @@ const ReleaseModal = ({ releaseDes, devboxUid: devbox.id }) + if (enableRestartMachine) { await restartDevbox({ devboxName: devbox.name }) } @@ -88,7 +92,7 @@ const ReleaseModal = ({ } setLoading(false) }, - [devbox.status.value, devbox.name, toast, t, tag, releaseDes, onSuccess, onClose] + [devbox.status.value, devbox.name, devbox.id, tag, releaseDes, toast, t, onSuccess, onClose] ) return ( @@ -107,7 +111,10 @@ const ReleaseModal = ({ {t('image_name')} - + @@ -144,7 +151,7 @@ const ReleaseModal = ({ mr={'11px'} width={'80px'} isLoading={loading}> - {t('release')} + {t('publish')} diff --git a/frontend/providers/devbox/constants/devbox.ts b/frontend/providers/devbox/constants/devbox.ts index a9acc5ef23b..bd8917eee49 100644 --- a/frontend/providers/devbox/constants/devbox.ts +++ b/frontend/providers/devbox/constants/devbox.ts @@ -48,7 +48,8 @@ export enum DevboxStatusEnum { Running = 'Running', Pending = 'Pending', Error = 'Error', - Delete = 'Delete' + Delete = 'Delete', + Unknown = 'Unknown' } export enum DevboxReleaseStatusEnum { Success = 'Success', @@ -126,26 +127,33 @@ export const devboxStatusMap = { color: '#DC6803', backgroundColor: '#FFFAEB', dotColor: '#DC6803' + }, + [DevboxStatusEnum.Unknown]: { + label: 'Unknown', + value: DevboxStatusEnum.Unknown, + color: '#787A90', + backgroundColor: '#F5F5F8', + dotColor: '#787A90' } } export const devboxReleaseStatusMap = { [DevboxReleaseStatusEnum.Success]: { - label: 'Success', + label: 'release_success', value: DevboxReleaseStatusEnum.Success, color: '#039855', backgroundColor: '#EDFBF3', dotColor: '#039855' }, [DevboxReleaseStatusEnum.Pending]: { - label: 'Pending', + label: 'release_pending', value: DevboxReleaseStatusEnum.Pending, color: '#787A90', backgroundColor: '#F5F5F8', dotColor: '#787A90' }, [DevboxReleaseStatusEnum.Failed]: { - label: 'Failed', + label: 'release_failed', value: DevboxReleaseStatusEnum.Failed, color: '#F04438', backgroundColor: '#FEF3F2', diff --git a/frontend/providers/devbox/deploy/manifests/deploy.yaml.tmpl b/frontend/providers/devbox/deploy/manifests/deploy.yaml.tmpl index 4790c8b5b86..a9c9e5343d2 100644 --- a/frontend/providers/devbox/deploy/manifests/deploy.yaml.tmpl +++ b/frontend/providers/devbox/deploy/manifests/deploy.yaml.tmpl @@ -51,6 +51,10 @@ spec: value: 'false' - name: ACCOUNT_URL value: http://account-service.account-system.svc.cluster.local:2333 + - name: ROOT_RUNTIME_NAMESPACE + value: devbox-system + - name: INGRESS_DOMAIN + value: sealosusw.site securityContext: runAsNonRoot: true runAsUser: 1001 diff --git a/frontend/providers/devbox/hooks/useConfirm.tsx b/frontend/providers/devbox/hooks/useConfirm.tsx index d07cddaa76f..63fda915543 100644 --- a/frontend/providers/devbox/hooks/useConfirm.tsx +++ b/frontend/providers/devbox/hooks/useConfirm.tsx @@ -11,7 +11,30 @@ import { Box } from '@chakra-ui/react' import { useTranslations } from 'next-intl' -import { useCallback, useRef } from 'react' +import { useCallback, useRef, useState } from 'react' + +const ConfirmCheckbox = ({ + checkboxLabel, + onCheckedChange +}: { + checkboxLabel: string + onCheckedChange: (checked: boolean) => void +}) => { + const [isChecked, setIsChecked] = useState(true) + const t = useTranslations() + + const handleChange = (e: React.ChangeEvent) => { + const newValue = e.target.checked + setIsChecked(newValue) + onCheckedChange(newValue) + } + + return ( + + {t(checkboxLabel)} + + ) +} export const useConfirm = ({ title = 'prompt', @@ -60,12 +83,12 @@ export const useConfirm = ({ {t(content)} {showCheckbox && ( - (isCheckedRef.current = e.target.checked)}> - {t(checkboxLabel)} - + { + isCheckedRef.current = checked + }} + /> )} diff --git a/frontend/providers/devbox/hooks/useLoading.tsx b/frontend/providers/devbox/hooks/useLoading.tsx index 0dd228558eb..1f34dacd5da 100644 --- a/frontend/providers/devbox/hooks/useLoading.tsx +++ b/frontend/providers/devbox/hooks/useLoading.tsx @@ -5,7 +5,15 @@ export const useLoading = (props?: { defaultLoading: boolean }) => { const [isLoading, setIsLoading] = useState(props?.defaultLoading || false) const Loading = useCallback( - ({ loading, fixed = true }: { loading?: boolean; fixed?: boolean }): JSX.Element | null => { + ({ + loading, + fixed = true, + size = 'xl' + }: { + loading?: boolean + fixed?: boolean + size?: string + }): JSX.Element | null => { return ( { speed="0.65s" emptyColor="gray.200" color="brightBlue.600" - size="xl" + size={size} /> ) diff --git a/frontend/providers/devbox/message/en.json b/frontend/providers/devbox/message/en.json index 097ccb35006..b3c814eb1a3 100644 --- a/frontend/providers/devbox/message/en.json +++ b/frontend/providers/devbox/message/en.json @@ -22,14 +22,14 @@ "This runtime field is required": "The runtime version field is required", "basic_configuration": "Basic", "basic_info": "Basic", - "boot": "Boot", + "start": "Start", "cancel": "Cancel", "code_server": "CodeServer", "config_form": "Form", "confirm": "Confirm", "confirm_create_devbox": "Confirm to create the devbox?", "confirm_delete": "Confirm", - "confirm_update_devbox": "The change action will cause the devbox to restart and VSCode to reconnect. Do you want to continue?", + "confirm_update_devbox": "Change the CPU and memory will cause the project to restart and VSCode reconnect (change the network configuration is not). Whether to continue?", "control": "Operation", "copy": "Copy", "copy_failed": "Copy failed", @@ -56,7 +56,7 @@ "devbox_empty": "You have not created a new devbox yet.", "devbox_list": "Devbox List", "devbox_name": "Name", - "devbox_name_invalid": "Name is invalid", + "devbox_name_invalid": "The Devbox name can only contain letters, numbers, and hyphens (-), and must start with a letter.", "devbox_name_max_length": "Exceeds the maximum name length", "devbox_name_required": "Devbox name cannot be empty", "download_config": "Download config", @@ -87,7 +87,7 @@ "name": "Name", "network": "Network", "no_network": "No network configuration is available", - "no_versions": "There are no versions yet", + "no_versions": "There are no releases yet", "nodeports": "NodePorts", "nodeports_exceeds_quota": "The number of devbox created exceeds the limit, please contact the administrator", "none": "None", @@ -103,12 +103,11 @@ "port": "Port", "private_key": "Private Key", "prompt": "Prompt", - "publish": "Release", - "read_event_detail": "Read event detail", - "recent_error": "Recent Error", + "publish": "Publish", + "read_event_detail": "Toggle event detail", + "recent_error": "Recent Errors", "release": "Release", "release_confirm_info": "During the release process, the machine will be temporarily shut down and the release will be in the current state. Please save the running project.", - "release_failed": "Release failed", "release_prompt": "The release process will Shutdown the machine and release in the current state.", "release_successful": "Release succeeded", "release_version": "Release", @@ -121,11 +120,11 @@ "runtime_environment": "Runtime", "save": "Save", "shutdown": "Shutdown", - "ssh_config": "SSH Config", - "ssh_connect_info": "SSH Connect String", - "start_error": "Boot error", + "ssh_config": "SSH Configuration", + "ssh_connect_info": "SSH Connection String", + "start_error": "Start error", "start_runtime": "Runtime", - "start_success": "Boot succeeded", + "start_success": "Start succeeded", "start_time": "Start Time", "status": "Status", "submit_form_error": "Form submission error", @@ -137,7 +136,7 @@ "total": "Total", "total_price": "Total", "update": "Update", - "update Time": "Update At", + "update Time": "Updated At", "update_devbox": "Update devbox", "update_failed": "Update failed", "update_success": "Update succeeded", @@ -145,12 +144,16 @@ "version": "Release", "version_config": "Configuration", "version_description": "Description", - "version_history": "Version", + "version_history": "Release", "version_info": "Version List", "version_list": "Version List", "version_number": "Tag", + "release_success": "Release success", + "release_pending": "Releasing...", + "release_failed": "Release failed", "vscode": "VS Code", "vscode_tooltip": "Click to develop in VSCode", "yaml_file": "YAML", - "delete_version_confirm_info": "Are you sure you want to delete this version?" + "delete_version_confirm_info": "Are you sure you want to delete this version?", + "opening_ide": "Opening IDE..." } diff --git a/frontend/providers/devbox/message/zh.json b/frontend/providers/devbox/message/zh.json index eafe3b66cd4..8718f816394 100644 --- a/frontend/providers/devbox/message/zh.json +++ b/frontend/providers/devbox/message/zh.json @@ -11,7 +11,7 @@ "No changes detected": "并没有变更任何项", "Open Public Access": "开启公网访问", "Paused": "已关机", - "Pending": "进行中", + "Pending": "执行中", "Please enter the devbox name first": "请先填写项目名称", "Running": "运行中", "Stopped": "已停止", @@ -22,14 +22,14 @@ "This runtime field is required": "运行时版本是必填项", "basic_configuration": "基础配置", "basic_info": "基础信息", - "boot": "开机", + "start": "开机", "cancel": "取消", "code_server": "CodeServer", "config_form": "配置表单", "confirm": "确认", "confirm_create_devbox": "确认创建项目?", "confirm_delete": "确认删除", - "confirm_update_devbox": "变更操作将导致项目重启和VSCode重新连接。是否继续?", + "confirm_update_devbox": "变更 CPU 和内存将导致项目重启和 VSCode 的重新连接(变更网络配置并不会)。是否继续?", "control": "操作", "copy": "复制", "copy_failed": "复制失败", @@ -57,7 +57,7 @@ "devbox_empty": "您还没有新建项目", "devbox_list": "项目列表", "devbox_name": "项目名称", - "devbox_name_invalid": "名称非法", + "devbox_name_invalid": "Devbox 名称只能包含字母、数字和连字符(-),且必须以字母开头。", "devbox_name_max_length": "超出最大名字长度", "devbox_name_required": "项目名称不能为空", "download_config": "下载配置", @@ -71,7 +71,7 @@ "estimated_price": "预估价格", "event": "事件", "export_privateKey": "导出私钥", - "export_yaml": "导出YAML", + "export_yaml": "导出 YAML", "external_address": "外网地址", "framework": "框架", "ide_tooltip": "点击在 IDE 中开发", @@ -91,7 +91,7 @@ "no_network": "暂无网络配置", "no_versions": "暂无版本", "nodeports": "公网端口", - "nodeports_exceeds_quota": "创建的 devbox 数量超出限制,请联系管理员", + "nodeports_exceeds_quota": "创建的 Devbox 数量超出限制,请联系管理员", "none": "无", "not_allow_standalone_use": "不允许独立使用", "open_link": "打开链接", @@ -110,7 +110,6 @@ "recent_error": "最近错误", "release": "发布", "release_confirm_info": "发版过程中将暂时关闭机器,且以当前状态发版,请保存好正在运行的项目。", - "release_failed": "发版失败", "release_prompt": "发版过程将关机,以当前状态发版。", "release_successful": "发版成功", "release_version": "发布版本", @@ -151,9 +150,13 @@ "version_info": "版本列表", "version_list": "版本列表", "version_number": "版本号", + "release_success": "发版成功", + "release_pending": "发版中", + "release_failed": "发版失败", "vscode": "VS Code", "vscodeInsider": "VSCode Insider", "vscode_tooltip": "点击在 VSCode 中开发", - "yaml_file": "YAML文件", - "delete_version_confirm_info": "你确定要删除该版本吗?" + "yaml_file": "YAML 文件", + "delete_version_confirm_info": "你确定要删除该版本吗?", + "opening_ide": "正在打开 IDE..." } diff --git a/frontend/providers/devbox/package.json b/frontend/providers/devbox/package.json index 16c93a7e8e3..73ce44d3b29 100644 --- a/frontend/providers/devbox/package.json +++ b/frontend/providers/devbox/package.json @@ -18,7 +18,6 @@ "@kubernetes/client-node": "^0.21.0", "@sealos/ui": "workspace:^", "@tanstack/react-query": "^4.35.3", - "@types/jsonwebtoken": "^9.0.3", "axios": "^1.7.3", "date-fns": "^2.30.0", "dayjs": "^1.11.10", @@ -52,6 +51,7 @@ "@types/ini": "^4.1.1", "@types/js-cookie": "^3.0.4", "@types/js-yaml": "^4.0.9", + "@types/jsonwebtoken": "^9.0.3", "@types/lodash": "^4.14.199", "@types/node": "^20", "@types/nprogress": "^0.2.3", diff --git a/frontend/providers/devbox/public/images/angular.svg b/frontend/providers/devbox/public/images/angular.svg new file mode 100644 index 00000000000..57fb4361064 --- /dev/null +++ b/frontend/providers/devbox/public/images/angular.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/providers/devbox/public/images/ant-design.svg b/frontend/providers/devbox/public/images/ant-design.svg new file mode 100644 index 00000000000..1878f791c8f --- /dev/null +++ b/frontend/providers/devbox/public/images/ant-design.svg @@ -0,0 +1,83 @@ + + + + + Group 28 Copy 5 + + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/providers/devbox/public/images/astro.svg b/frontend/providers/devbox/public/images/astro.svg new file mode 100644 index 00000000000..4060f134178 --- /dev/null +++ b/frontend/providers/devbox/public/images/astro.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/providers/devbox/public/images/c.svg b/frontend/providers/devbox/public/images/c.svg new file mode 100644 index 00000000000..4833929f42f --- /dev/null +++ b/frontend/providers/devbox/public/images/c.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/providers/devbox/public/images/chakra-ui.svg b/frontend/providers/devbox/public/images/chakra-ui.svg new file mode 100644 index 00000000000..0360858e573 --- /dev/null +++ b/frontend/providers/devbox/public/images/chakra-ui.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/providers/devbox/public/images/chi.svg b/frontend/providers/devbox/public/images/chi.svg new file mode 100644 index 00000000000..5b2a619ad78 --- /dev/null +++ b/frontend/providers/devbox/public/images/chi.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/providers/devbox/public/images/cpp.svg b/frontend/providers/devbox/public/images/cpp.svg index 4833929f42f..a177a90ee95 100644 --- a/frontend/providers/devbox/public/images/cpp.svg +++ b/frontend/providers/devbox/public/images/cpp.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/frontend/providers/devbox/public/images/django.svg b/frontend/providers/devbox/public/images/django.svg new file mode 100644 index 00000000000..4f8c6b892f2 --- /dev/null +++ b/frontend/providers/devbox/public/images/django.svg @@ -0,0 +1,19 @@ + + + logo-django + + Created with Sketch (http://www.bohemiancoding.com/sketch) + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/providers/devbox/public/images/docusaurus.svg b/frontend/providers/devbox/public/images/docusaurus.svg new file mode 100644 index 00000000000..250f2fce73a --- /dev/null +++ b/frontend/providers/devbox/public/images/docusaurus.svg @@ -0,0 +1 @@ + diff --git a/frontend/providers/devbox/public/images/echo.svg b/frontend/providers/devbox/public/images/echo.svg new file mode 100644 index 00000000000..7752172989b --- /dev/null +++ b/frontend/providers/devbox/public/images/echo.svg @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/providers/devbox/public/images/express.js.svg b/frontend/providers/devbox/public/images/express.js.svg new file mode 100644 index 00000000000..36eda1f96e0 --- /dev/null +++ b/frontend/providers/devbox/public/images/express.js.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/frontend/providers/devbox/public/images/hexo.svg b/frontend/providers/devbox/public/images/hexo.svg new file mode 100644 index 00000000000..48d709f8b46 --- /dev/null +++ b/frontend/providers/devbox/public/images/hexo.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/providers/devbox/public/images/hugo.svg b/frontend/providers/devbox/public/images/hugo.svg new file mode 100644 index 00000000000..1d0b1a821ee --- /dev/null +++ b/frontend/providers/devbox/public/images/hugo.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/providers/devbox/public/images/iris.svg b/frontend/providers/devbox/public/images/iris.svg new file mode 100644 index 00000000000..71b8493a516 --- /dev/null +++ b/frontend/providers/devbox/public/images/iris.svg @@ -0,0 +1,14 @@ + + + + diff --git a/frontend/providers/devbox/public/images/laravel.svg b/frontend/providers/devbox/public/images/laravel.svg new file mode 100644 index 00000000000..e1309dd21e8 --- /dev/null +++ b/frontend/providers/devbox/public/images/laravel.svg @@ -0,0 +1 @@ +Logomark \ No newline at end of file diff --git a/frontend/providers/devbox/public/images/nest.js.svg b/frontend/providers/devbox/public/images/nest.js.svg new file mode 100644 index 00000000000..85f87141740 --- /dev/null +++ b/frontend/providers/devbox/public/images/nest.js.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/providers/devbox/public/images/net.svg b/frontend/providers/devbox/public/images/net.svg new file mode 100644 index 00000000000..867718fa16f --- /dev/null +++ b/frontend/providers/devbox/public/images/net.svg @@ -0,0 +1,6 @@ + + + +dotnet + + \ No newline at end of file diff --git a/frontend/providers/devbox/public/images/nginx.svg b/frontend/providers/devbox/public/images/nginx.svg new file mode 100644 index 00000000000..a77eefec7e5 --- /dev/null +++ b/frontend/providers/devbox/public/images/nginx.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/providers/devbox/public/images/nuxt3.svg b/frontend/providers/devbox/public/images/nuxt3.svg new file mode 100644 index 00000000000..dbba0974c65 --- /dev/null +++ b/frontend/providers/devbox/public/images/nuxt3.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/providers/devbox/public/images/quarkus.svg b/frontend/providers/devbox/public/images/quarkus.svg new file mode 100644 index 00000000000..7f70663dfac --- /dev/null +++ b/frontend/providers/devbox/public/images/quarkus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/providers/devbox/public/images/rocket.svg b/frontend/providers/devbox/public/images/rocket.svg new file mode 100644 index 00000000000..3f2733e4ea3 --- /dev/null +++ b/frontend/providers/devbox/public/images/rocket.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/frontend/providers/devbox/public/images/sealaf.svg b/frontend/providers/devbox/public/images/sealaf.svg new file mode 100644 index 00000000000..f363e04eecc --- /dev/null +++ b/frontend/providers/devbox/public/images/sealaf.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/providers/devbox/public/images/svelte.svg b/frontend/providers/devbox/public/images/svelte.svg new file mode 100644 index 00000000000..9b0188c378f --- /dev/null +++ b/frontend/providers/devbox/public/images/svelte.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/providers/devbox/public/images/ubuntu.svg b/frontend/providers/devbox/public/images/ubuntu.svg new file mode 100644 index 00000000000..6de966bc2c8 --- /dev/null +++ b/frontend/providers/devbox/public/images/ubuntu.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/providers/devbox/public/images/umi.svg b/frontend/providers/devbox/public/images/umi.svg new file mode 100644 index 00000000000..6949a075ea9 --- /dev/null +++ b/frontend/providers/devbox/public/images/umi.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/providers/devbox/public/images/vert.x.svg b/frontend/providers/devbox/public/images/vert.x.svg new file mode 100644 index 00000000000..6a012954b9c --- /dev/null +++ b/frontend/providers/devbox/public/images/vert.x.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/providers/devbox/public/images/vitepress.svg b/frontend/providers/devbox/public/images/vitepress.svg new file mode 100644 index 00000000000..8df5c42701a --- /dev/null +++ b/frontend/providers/devbox/public/images/vitepress.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/providers/devbox/stores/devbox.ts b/frontend/providers/devbox/stores/devbox.ts index 807704e3e39..249c6857cc2 100644 --- a/frontend/providers/devbox/stores/devbox.ts +++ b/frontend/providers/devbox/stores/devbox.ts @@ -5,132 +5,90 @@ import { immer } from 'zustand/middleware/immer' import type { DevboxDetailType, DevboxListItemType, - DevboxVersionListItemType, - PodDetailType + DevboxVersionListItemType } from '@/types/devbox' import { + getDevboxByName, getDevboxMonitorData, getDevboxPodsByDevboxName, getDevboxVersionList, getMyDevboxList, getSSHConnectionInfo } from '@/api/devbox' -import { SEALOS_DOMAIN } from './static' +import { devboxStatusMap, PodStatusEnum } from '@/constants/devbox' type State = { devboxList: DevboxListItemType[] setDevboxList: () => Promise + loadAvgMonitorData: (devboxName: string) => Promise devboxVersionList: DevboxVersionListItemType[] setDevboxVersionList: ( devboxName: string, devboxUid: string ) => Promise devboxDetail: DevboxDetailType - setDevboxDetail: (devboxName: string) => Promise + setDevboxDetail: (devboxName: string, sealosDomain: string) => Promise + intervalLoadPods: (devboxName: string, updateDetail: boolean) => Promise loadDetailMonitorData: (devboxName: string) => Promise - devboxDetailPods: PodDetailType[] } export const useDevboxStore = create()( devtools( - immer((set, get) => ({ - devboxList: [] as DevboxListItemType[], + immer((set) => ({ + devboxList: [], setDevboxList: async () => { const res = await getMyDevboxList() - - // order by createTime - res.sort((a, b) => { - return new Date(b.createTime).getTime() - new Date(a.createTime).getTime() + set((state) => { + state.devboxList = res }) + return res + }, + loadAvgMonitorData: async (devboxName) => { + const pods = await getDevboxPodsByDevboxName(devboxName) + const queryName = pods.length > 0 ? pods[0].podName : devboxName - // load monitor data for each devbox - const updatedRes = await Promise.all( - res.map(async (devbox) => { - if (devbox.status.value !== 'Running') { - return devbox - } - - const pods = await getDevboxPodsByDevboxName(devbox.name) - const queryName = pods[0]?.podName || devbox.name - - let averageCpuData, averageMemoryData - try { - ;[averageCpuData, averageMemoryData] = await Promise.all([ - getDevboxMonitorData({ - queryKey: 'average_cpu', - queryName: queryName, - step: '2m' - }), - getDevboxMonitorData({ - queryKey: 'average_memory', - queryName: queryName, - step: '2m' - }) - ]) - } catch (error) { - console.error('获取监控数据失败:', error) - averageCpuData = [ - { - xData: new Array(30).fill(0), - yData: new Array(30).fill('0'), - name: '' - } - ] - averageMemoryData = [ - { - xData: new Array(30).fill(0), - yData: new Array(30).fill('0'), - name: '' - } - ] - } - - return { - ...devbox, - usedCpu: averageCpuData[0], - usedMemory: averageMemoryData[0] - } + const [averageCpuData, averageMemoryData] = await Promise.all([ + getDevboxMonitorData({ + queryKey: 'average_cpu', + queryName: queryName, + step: '2m' + }), + getDevboxMonitorData({ + queryKey: 'average_memory', + queryName: queryName, + step: '2m' }) - ) + ]) set((state) => { - state.devboxList = updatedRes + state.devboxList = state.devboxList.map((item) => ({ + ...item, + usedCpu: + item.name === devboxName && averageCpuData[0] ? averageCpuData[0] : item.usedCpu, + usedMemory: + item.name === devboxName && averageMemoryData[0] + ? averageMemoryData[0] + : item.usedMemory + })) }) - return updatedRes }, devboxVersionList: [], - setDevboxVersionList: async (devboxName: string, devboxUid: string) => { + setDevboxVersionList: async (devboxName, devboxUid) => { const res = await getDevboxVersionList(devboxName, devboxUid) - // order by createTime - res.sort((a, b) => { - return new Date(b.createTime).getTime() - new Date(a.createTime).getTime() - }) - - // createTime:2024/09/11 17:37-> 2024-09-11 - res.forEach((item) => { - item.createTime = item.createTime.replace(/\//g, '-') - }) - set((state) => { state.devboxVersionList = res }) return res }, devboxDetail: {} as DevboxDetailType, - setDevboxDetail: async (devboxName: string) => { - const res = await getMyDevboxList() - - const detail = res.find((item) => item.name === devboxName) as DevboxDetailType - - // isPause - detail.isPause = detail.status.value === 'Stopped' + setDevboxDetail: async (devboxName: string, sealosDomain: string) => { + const detail = await getDevboxByName(devboxName) if (detail.status.value !== 'Running') { set((state) => { state.devboxDetail = detail }) - return detail } @@ -144,7 +102,7 @@ export const useDevboxStore = create()( const sshPrivateKey = Buffer.from(base64PrivateKey, 'base64').toString('utf-8') const sshConfig = { sshUser: userName, - sshDomain: SEALOS_DOMAIN, + sshDomain: sealosDomain, sshPort: detail.sshPort, sshPrivateKey } @@ -152,9 +110,6 @@ export const useDevboxStore = create()( // add sshConfig detail.sshConfig = sshConfig as DevboxDetailType['sshConfig'] - // convert startTime to YYYY-MM-DD HH:mm - detail.createTime = detail.createTime.replace(/\//g, '-') - // add upTime by Pod detail.upTime = pods[0].upTime @@ -164,34 +119,36 @@ export const useDevboxStore = create()( return detail }, - devboxDetailPods: [], - loadDetailMonitorData: async (devboxName) => { + intervalLoadPods: async (devboxName, updateDetail) => { + if (!devboxName) return Promise.reject('devbox name is empty') const pods = await getDevboxPodsByDevboxName(devboxName) - const queryName = pods.length > 0 ? pods[0].podName : devboxName + // TODO: change Running to podStatusMap.running + // TODO: check status enum and backend status enum + const devboxStatus = + pods.length === 0 + ? devboxStatusMap.Stopped + : pods.filter((pod) => pod.status.value === PodStatusEnum.running).length > 0 + ? devboxStatusMap.Running + : devboxStatusMap.Pending set((state) => { - state.devboxDetailPods = pods.map((pod) => { - const oldPod = state.devboxDetailPods.find((item) => item.podName === pod.podName) - return { - ...pod, - usedCpu: oldPod ? oldPod.usedCpu : pod.usedCpu, - usedMemory: oldPod ? oldPod.usedMemory : pod.usedMemory - } - }) + if (state?.devboxDetail?.name === devboxName && updateDetail) { + state.devboxDetail.status = devboxStatus + } + state.devboxList = state.devboxList.map((item) => ({ + ...item, + status: item.name === devboxName ? devboxStatus : item.status + })) }) + return 'success' + }, + loadDetailMonitorData: async (devboxName) => { + const pods = await getDevboxPodsByDevboxName(devboxName) - const [cpuData, memoryData, averageCpuData, averageMemoryData] = await Promise.all([ - getDevboxMonitorData({ - queryKey: 'cpu', - queryName: queryName, - step: '2m' - }), - getDevboxMonitorData({ - queryKey: 'memory', - queryName: queryName, - step: '2m' - }), + const queryName = pods.length > 0 ? pods[0].podName : devboxName + + const [averageCpuData, averageMemoryData] = await Promise.all([ getDevboxMonitorData({ queryKey: 'average_cpu', queryName: queryName, @@ -221,15 +178,6 @@ export const useDevboxStore = create()( name: '' } } - state.devboxDetailPods = pods.map((pod) => { - const currentCpu = cpuData.find((item) => item.name === pod.podName) - const currentMemory = memoryData.find((item) => item.name === pod.podName) - return { - ...pod, - usedCpu: currentCpu ? currentCpu : pod.usedCpu, - usedMemory: currentMemory ? currentMemory : pod.usedMemory - } - }) }) return 'success' } diff --git a/frontend/providers/devbox/stores/env.ts b/frontend/providers/devbox/stores/env.ts new file mode 100644 index 00000000000..e2ea9e56a36 --- /dev/null +++ b/frontend/providers/devbox/stores/env.ts @@ -0,0 +1,42 @@ +import { create } from 'zustand' +import { immer } from 'zustand/middleware/immer' +import { devtools, persist } from 'zustand/middleware' + +import { Env } from '@/types/static' +import { getAppEnv } from '@/api/platform' + +export const defaultEnv: Env = { + sealosDomain: 'dev.sealos.plus', + ingressSecret: 'wildcard-cert', + registryAddr: 'hub.dev.sealos.plus', + devboxAffinityEnable: 'true', + squashEnable: 'false', + namespace: 'default', + rootRuntimeNamespace: 'devbox-system', + ingressDomain: 'sealosusw.site' +} + +type State = { + env: Env + setEnv: () => Promise +} + +export const useEnvStore = create()( + devtools( + persist( + immer((set) => ({ + env: defaultEnv, + async setEnv() { + const res = await getAppEnv() + set((state) => { + state.env = res + }) + return res + } + })), + { + name: 'env-storage' + } + ) + ) +) diff --git a/frontend/providers/devbox/stores/price.ts b/frontend/providers/devbox/stores/price.ts new file mode 100644 index 00000000000..9830575b1a0 --- /dev/null +++ b/frontend/providers/devbox/stores/price.ts @@ -0,0 +1,27 @@ +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 } + +type State = { + sourcePrice: SourcePrice + setSourcePrice: () => Promise +} + +export const usePriceStore = create()( + devtools( + immer((set, get) => ({ + sourcePrice: defaultSourcePrice, + async setSourcePrice() { + const res = await getResourcePrice() + set((state) => { + state.sourcePrice = res + }) + return res + } + })) + ) +) diff --git a/frontend/providers/devbox/stores/runtime.ts b/frontend/providers/devbox/stores/runtime.ts new file mode 100644 index 00000000000..5654fb4f97c --- /dev/null +++ b/frontend/providers/devbox/stores/runtime.ts @@ -0,0 +1,83 @@ +import { create } from 'zustand' +import { immer } from 'zustand/middleware/immer' +import { devtools, persist } from 'zustand/middleware' + +import { getRuntime } from '@/api/platform' +import { RuntimeTypeMap, RuntimeVersionMap } from '@/types/static' + +type State = { + languageTypeList: RuntimeTypeMap[] + frameworkTypeList: RuntimeTypeMap[] + osTypeList: RuntimeTypeMap[] + runtimeNamespaceMap: { + [key: string]: string + } + languageVersionMap: RuntimeVersionMap + frameworkVersionMap: RuntimeVersionMap + osVersionMap: RuntimeVersionMap + + setRuntime: () => Promise + getRuntimeVersionList: (runtimeType: string) => { + value: string + label: string + defaultPorts: number[] + }[] + getRuntimeDetailLabel: (runtimeType: string, runtimeVersion: string) => string + getRuntimeVersionDefault: (runtimeType: string) => string +} + +export const useRuntimeStore = create()( + devtools( + persist( + immer((set, get) => ({ + languageTypeList: [], + frameworkTypeList: [], + osTypeList: [], + runtimeNamespaceMap: {}, + languageVersionMap: {}, + frameworkVersionMap: {}, + osVersionMap: {}, + async setRuntime() { + const res = await getRuntime() + set((state) => { + Object.assign(state, res) + }) + }, + getRuntimeVersionList(runtimeType: string) { + const { languageVersionMap, frameworkVersionMap, osVersionMap } = get() + const versions = + languageVersionMap[runtimeType] || + frameworkVersionMap[runtimeType] || + osVersionMap[runtimeType] || + [] + return versions.map((i) => ({ + value: i.id, + label: i.label, + defaultPorts: i.defaultPorts + })) + }, + getRuntimeDetailLabel(runtimeType: string, runtimeVersion: string) { + const { languageVersionMap, frameworkVersionMap, osVersionMap } = get() + const versions = + languageVersionMap[runtimeType] || + frameworkVersionMap[runtimeType] || + osVersionMap[runtimeType] + + const version = versions.find((i) => i.id === runtimeVersion) + return `${runtimeType}-${version?.label}` + }, + getRuntimeVersionDefault(runtimeType: string) { + const { languageVersionMap, frameworkVersionMap, osVersionMap } = get() + return ( + languageVersionMap[runtimeType]?.[0]?.id || + frameworkVersionMap[runtimeType]?.[0]?.id || + osVersionMap[runtimeType]?.[0]?.id + ) + } + })), + { + name: 'runtime-storage' + } + ) + ) +) diff --git a/frontend/providers/devbox/stores/static.ts b/frontend/providers/devbox/stores/static.ts deleted file mode 100644 index 48ffe317eb3..00000000000 --- a/frontend/providers/devbox/stores/static.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { - getRuntime as getRuntimeApi, - getResourcePrice, - getNamespace, - getAppEnv -} from '@/api/platform' -import { - VersionMapType, - runtimeNamespaceMapType, - ValueType, - ValueTypeWithPorts -} from '@/types/devbox' -import type { Response as resourcePriceResponse } from '@/app/api/platform/resourcePrice/route' - -export let SOURCE_PRICE: resourcePriceResponse = { - cpu: 0.067, - memory: 0.033792, - nodeports: 0.0001 -} -export let INSTALL_ACCOUNT = false -export let NAMESPACE = 'default' - -let retryGetRuntimeVersion = 3 -let retryGetEnv = 3 -let retryGetPrice = 3 -let retryGetNamespace = 3 - -export let SEALOS_DOMAIN = 'dev.sealos.plus' -export let INGRESS_SECRET = 'wildcard-cert' -export let REGISTRY_ADDR = 'hub.dev.sealos.plus' -export let DEVBOX_AFFINITY_ENABLE = 'true' -export let SQUASH_ENABLE = 'false' - -export const runtimeNamespace = 'devbox-system' - -export let languageTypeList: ValueType[] = [] -export let frameworkTypeList: ValueType[] = [] -export let osTypeList: ValueType[] = [] - -export let runtimeNamespaceMap: runtimeNamespaceMapType = {} - -export let languageVersionMap: VersionMapType = { - // [LanguageTypeEnum.java]: [{ id: '11', label: 'java-11' }], - // [LanguageTypeEnum.go]: [{ id: '1.17', label: 'go-1.17' }], - // [LanguageTypeEnum.python]: [{ id: '3.9', label: 'python-3.9' }], - // [LanguageTypeEnum.node]: [{ id: '16', label: 'node-16' }], - // [LanguageTypeEnum.rust]: [{ id: '1.55', label: 'rust-1.55' }], - // [LanguageTypeEnum.c]: [{ id: '11', label: 'c-11' }] -} -export let frameworkVersionMap: VersionMapType = { - // [FrameworkTypeEnum.gin]: [{ id: '1.7', label: 'gin-1.7' }], - // [FrameworkTypeEnum.Hertz]: [{ id: '1.0', label: 'Hertz-1.0' }], - // [FrameworkTypeEnum.springBoot]: [{ id: '2.5', label: 'spring-boot-2.5' }], - // [FrameworkTypeEnum.flask]: [{ id: '2.0', label: 'flask-2.0' }], - // [FrameworkTypeEnum.nextjs]: [{ id: '11', label: 'nextjs-11' }], - // [FrameworkTypeEnum.vue]: [{ id: '3.0', label: 'vue-3.0' }] -} - -export let osVersionMap: VersionMapType = { - // [OSTypeEnum.ubuntu]: [{ id: '20.04', label: 'ubuntu-20.04' }], - // [OSTypeEnum.centos]: [{ id: '8', label: 'centos-8' }] -} -export const getRuntimeVersionList = (runtimeType: string) => { - let versions: ValueTypeWithPorts[] = [] - - if (languageVersionMap[runtimeType]) { - versions = languageVersionMap[runtimeType] - } else if (frameworkVersionMap[runtimeType]) { - versions = frameworkVersionMap[runtimeType] - } else if (osVersionMap[runtimeType]) { - versions = osVersionMap[runtimeType] - } - return versions.map((i) => ({ - value: i.id, - label: i.label, - defaultPorts: i.defaultPorts - })) -} - -export const getRuntimeVersionItem = (runtimeType: string, runtimeVersion: string) => { - let versions: ValueType[] = [] - - if (languageVersionMap[runtimeType]) { - versions = languageVersionMap[runtimeType] - } else if (frameworkVersionMap[runtimeType]) { - versions = frameworkVersionMap[runtimeType] - } else if (osVersionMap[runtimeType]) { - versions = osVersionMap[runtimeType] - } - const version = versions.find((i) => i.id === runtimeVersion) - const resp = `${runtimeType}-${version?.label}` - - return resp -} - -export const getUserPrice = async () => { - try { - const res = await getResourcePrice() - SOURCE_PRICE = res - INSTALL_ACCOUNT = true - } catch (err) { - retryGetPrice-- - if (retryGetPrice >= 0) { - setTimeout(() => { - getUserPrice() - }, 1000) - } - } -} - -export const getGlobalNamespace = async () => { - try { - const res = await getNamespace() - NAMESPACE = res - } catch (err) { - retryGetNamespace-- - if (retryGetNamespace >= 0) { - setTimeout(() => { - getNamespace() - }, 1000) - } - } -} - -export const getRuntime = async () => { - try { - const res = await getRuntimeApi() - languageVersionMap = res.languageVersionMap - frameworkVersionMap = res.frameworkVersionMap - osVersionMap = res.osVersionMap - languageTypeList = res.languageTypeList - frameworkTypeList = res.frameworkTypeList - osTypeList = res.osTypeList - runtimeNamespaceMap = res.runtimeNamespaceMap - } catch (err) { - retryGetRuntimeVersion-- - if (retryGetRuntimeVersion >= 0) { - setTimeout(() => { - getRuntime() - }, 1000) - } - } -} - -export const getEnv = async () => { - try { - const res = await getAppEnv() - const { domain, ingressSecret, registryAddr, devboxAffinityEnable, squashEnable } = res - SEALOS_DOMAIN = domain - INGRESS_SECRET = ingressSecret - REGISTRY_ADDR = registryAddr - DEVBOX_AFFINITY_ENABLE = devboxAffinityEnable - SQUASH_ENABLE = squashEnable - } catch (err) { - retryGetEnv-- - if (retryGetEnv >= 0) { - setTimeout(() => { - getAppEnv() - }, 1000) - } - } -} diff --git a/frontend/providers/devbox/types/devbox.d.ts b/frontend/providers/devbox/types/devbox.d.ts index f58462cc680..96df965daf2 100644 --- a/frontend/providers/devbox/types/devbox.d.ts +++ b/frontend/providers/devbox/types/devbox.d.ts @@ -84,13 +84,7 @@ export interface DevboxDetailType extends DevboxEditType { sshPrivateKey: string } sshPort?: number - lastTerminatedState?: { - containerID: string - exitCode: number - finishedAt: string - reason: string - startedAt: string - } + lastTerminatedReason?: string } export interface NetworkType { @@ -114,15 +108,8 @@ export interface DevboxListItemType { memory: number usedCpu: MonitorDataResult usedMemory: MonitorDataResult - networks: NetworkType[] sshPort: number - lastTerminatedState?: { - containerID: string - exitCode: number - finishedAt: string - reason: string - startedAt: string - } + lastTerminatedReason?: string } export interface DevboxVersionListItemType { diff --git a/frontend/providers/devbox/types/k8s.d.ts b/frontend/providers/devbox/types/k8s.d.ts index 298b1644a28..2c9eb3fd226 100644 --- a/frontend/providers/devbox/types/k8s.d.ts +++ b/frontend/providers/devbox/types/k8s.d.ts @@ -9,25 +9,33 @@ export type KBDevboxType = { creationTimestamp: string } spec: KBDevboxSpec - portInfos: { - // Added by logic in api/getDevboxList/route.ts - networkName: string - portName: string - port: number - protocol: ProtocolType - openPublicDomain: boolean - publicDomain: string - customDomain: string - }[] - lastTerminatedState: { - containerID: string - exitCode: number - finishedAt: string - reason: string - startedAt: string - } status: { - phase: 'Pending' | 'Running' | 'Stopped' | 'Stopping' | 'Error' | 'Delete' + lastState: { + terminated?: { + containerID: string + exitCode: number + finishedAt: string + reason: string // normally is Error if it not null + startedAt: string + } + } + state: { + running?: { + startedAt: string + } + waiting?: { + message: string + reason: string + } + terminated?: { + containerID: string + exitCode: number + finishedAt: string + reason: string + startedAt: string + } + } + phase: 'Pending' | 'Running' | 'Stopped' | 'Stopping' | 'Error' | 'Unknown' commitHistory: { image: string pod: string @@ -43,11 +51,10 @@ export type KBDevboxType = { } } -// note: there first three runtime type is I added by logic in api/getDevboxList/route.ts +// note: runtimeType is I added by logic in api/getDevboxList/route.ts export interface KBDevboxSpec { runtimeType?: string - runtimeVersion?: string - runtimeNamespace?: string + squash?: boolean network: { type: 'NodePort' | 'Tailnet' @@ -142,6 +149,8 @@ export type KBRuntimeType = { releaseCommand: string[] releaseArgs: string[] } + state: 'active' | 'deprecated' | 'archived' | 'beta' + runtimeVersion: string category: string[] description: string version: string diff --git a/frontend/providers/devbox/types/static.d.ts b/frontend/providers/devbox/types/static.d.ts new file mode 100644 index 00000000000..b08359a89f5 --- /dev/null +++ b/frontend/providers/devbox/types/static.d.ts @@ -0,0 +1,46 @@ +export interface SourcePrice { + cpu: number + memory: number + nodeports: number +} + +export interface Env { + sealosDomain: string + ingressSecret: string + registryAddr: string + devboxAffinityEnable: string + squashEnable: string + namespace: string + rootRuntimeNamespace: string + ingressDomain: string +} + +export interface RuntimeTypeMap { + id: string + label: string +} + +// RuntimeTypeMap +// { +// id: 'go' +// label: 'go' +// } + +export interface RuntimeVersionMap { + [key: string]: { + id: string + label: string + defaultPorts: number[] + }[] +} + +// RuntimeVersionMap +// { +// go: [ +// { +// id: '1.17' +// label: 'go-1.17' +// defaultPorts: [80] +// } +// ] +// } diff --git a/frontend/providers/devbox/utils/adapt.ts b/frontend/providers/devbox/utils/adapt.ts index 4c1c44f8c08..7f9dacd4bea 100644 --- a/frontend/providers/devbox/utils/adapt.ts +++ b/frontend/providers/devbox/utils/adapt.ts @@ -8,7 +8,12 @@ import { } from '@/constants/devbox' import { calculateUptime, cpuFormatToM, formatPodTime, memoryFormatToMi } from '@/utils/tools' import { KBDevboxType, KBDevboxReleaseType } from '@/types/k8s' -import { DevboxListItemType, DevboxVersionListItemType, PodDetailType } from '@/types/devbox' +import { + DevboxDetailType, + DevboxListItemType, + DevboxVersionListItemType, + PodDetailType +} from '@/types/devbox' import { V1Ingress, V1Pod } from '@kubernetes/client-node' import { DBListItemType, KbPgClusterType } from '@/types/cluster' import { IngressListItemType } from '@/types/ingress' @@ -18,7 +23,7 @@ export const adaptDevboxListItem = (devbox: KBDevboxType): DevboxListItemType => id: devbox.metadata?.uid || ``, name: devbox.metadata.name || 'devbox', runtimeType: devbox.spec.runtimeType || '', - runtimeVersion: devbox.spec.runtimeVersion || '', + runtimeVersion: devbox.spec.runtimeRef.name || '', status: devbox.status.phase && devboxStatusMap[devbox.status.phase] ? devboxStatusMap[devbox.status.phase] @@ -37,11 +42,55 @@ export const adaptDevboxListItem = (devbox: KBDevboxType): DevboxListItemType => xData: new Array(30).fill(0), yData: new Array(30).fill('0') }, - networks: devbox.portInfos || [], - lastTerminatedState: devbox.lastTerminatedState || {} + lastTerminatedReason: + devbox.status.lastState?.terminated && devbox.status.lastState.terminated.reason === 'Error' + ? devbox.status.state.waiting + ? devbox.status.state.waiting.reason + : devbox.status.state.terminated + ? devbox.status.state.terminated.reason + : '' + : '' } } +export const adaptDevboxDetail = ( + devbox: KBDevboxType & { portInfos: any[] } +): DevboxDetailType => { + return { + id: devbox.metadata?.uid || ``, + name: devbox.metadata.name || 'devbox', + runtimeType: devbox.spec.runtimeType || '', + runtimeVersion: devbox.spec.runtimeRef.name || '', + status: + devbox.status.phase && devboxStatusMap[devbox.status.phase] + ? devboxStatusMap[devbox.status.phase] + : devboxStatusMap.Error, + sshPort: devbox.status.network.nodePort, + isPause: devbox.status.phase === 'Stopped', + createTime: dayjs(devbox.metadata.creationTimestamp).format('YYYY-MM-DD HH:mm'), + cpu: cpuFormatToM(devbox.spec.resource.cpu), + memory: memoryFormatToMi(devbox.spec.resource.memory), + usedCpu: { + name: '', + xData: new Array(30).fill(0), + yData: new Array(30).fill('0') + }, + usedMemory: { + name: '', + xData: new Array(30).fill(0), + yData: new Array(30).fill('0') + }, + networks: devbox.portInfos || [], + lastTerminatedReason: + devbox.status.lastState?.terminated && devbox.status.lastState.terminated.reason === 'Error' + ? devbox.status.state.waiting + ? devbox.status.state.waiting.reason + : devbox.status.state.terminated + ? devbox.status.state.terminated.reason + : '' + : '' + } +} export const adaptDevboxVersionListItem = ( devboxRelease: KBDevboxReleaseType ): DevboxVersionListItemType => { @@ -49,7 +98,7 @@ export const adaptDevboxVersionListItem = ( id: devboxRelease.metadata?.uid || '', name: devboxRelease.metadata.name || 'devbox-release-default', devboxName: devboxRelease.spec.devboxName || 'devbox', - createTime: dayjs(devboxRelease.metadata.creationTimestamp).format('YYYY/MM/DD HH:mm'), + createTime: dayjs(devboxRelease.metadata.creationTimestamp).format('YYYY-MM-DD HH:mm'), tag: devboxRelease.spec.newTag || 'v1.0.0', status: devboxRelease.status.phase && devboxReleaseStatusMap[devboxRelease.status.phase] diff --git a/frontend/providers/devbox/utils/json2Yaml.ts b/frontend/providers/devbox/utils/json2Yaml.ts index 066a67f6ac6..40dd83e677b 100644 --- a/frontend/providers/devbox/utils/json2Yaml.ts +++ b/frontend/providers/devbox/utils/json2Yaml.ts @@ -1,10 +1,5 @@ import yaml from 'js-yaml' -import { - INGRESS_SECRET, - SEALOS_DOMAIN, - runtimeNamespaceMap as defaultRuntimeNamespaceMap -} from '@/stores/static' import { str2Num } from './tools' import { getUserNamespace } from './user' import { DevboxEditType, runtimeNamespaceMapType } from '@/types/devbox' @@ -12,7 +7,7 @@ import { devboxKey, publicDomainKey } from '@/constants/devbox' export const json2Devbox = ( data: DevboxEditType, - runtimeNamespaceMap: runtimeNamespaceMapType = defaultRuntimeNamespaceMap, + runtimeNamespaceMap: runtimeNamespaceMapType, devboxAffinityEnable: string = 'true', squashEnable: string = 'false' ) => { @@ -123,11 +118,7 @@ export const json2DevboxRelease = (data: { return yaml.dump(json) } -export const json2Ingress = ( - data: DevboxEditType, - sealosDomain: string = SEALOS_DOMAIN, - ingressSecret: string = INGRESS_SECRET -) => { +export const json2Ingress = (data: DevboxEditType, ingressSecret: string) => { // different protocol annotations const map = { HTTP: { @@ -154,9 +145,7 @@ export const json2Ingress = ( const result = data.networks .filter((item) => item.openPublicDomain) .map((network, i) => { - const host = network.customDomain - ? network.customDomain - : `${network.publicDomain}.${sealosDomain}` + const host = network.customDomain ? network.customDomain : network.publicDomain const secretName = network.customDomain ? network.networkName : ingressSecret diff --git a/frontend/providers/devbox/utils/tools.ts b/frontend/providers/devbox/utils/tools.ts index fd73642b6c2..5b9bd1af0a2 100644 --- a/frontend/providers/devbox/utils/tools.ts +++ b/frontend/providers/devbox/utils/tools.ts @@ -7,7 +7,6 @@ import * as jsonpatch from 'fast-json-patch' import { YamlKindEnum } from '@/constants/devbox' import type { DevboxKindsType, DevboxPatchPropsType } from '@/types/devbox' -import { frameworkVersionMap, languageVersionMap, osVersionMap } from '@/stores/static' dayjs.extend(duration) @@ -125,15 +124,6 @@ export const getErrText = (err: any, def = '') => { return msg } -export const getValueDefault = (valueIndex: string) => { - return ( - languageVersionMap[valueIndex]?.[0]?.id || - frameworkVersionMap[valueIndex]?.[0]?.id || - osVersionMap[valueIndex]?.[0]?.id || - undefined - ) -} - /** * patch yamlList and get action */ @@ -328,5 +318,14 @@ export function calculateUptime(createdTime: Date): string { uptime += `${minutes}m` } - return uptime || '刚刚启动' + return uptime || 'Recently Started' +} + +export const isElementInViewport = (element: Element) => { + const rect = element.getBoundingClientRect() + const windowHeight = window.innerHeight || document.documentElement.clientHeight + const windowWidth = window.innerWidth || document.documentElement.clientWidth + const vertInView = rect.top <= windowHeight && rect.top + rect.height >= 0 + const horInView = rect.left <= windowWidth && rect.left + rect.width >= 0 + return vertInView && horInView } diff --git a/frontend/providers/template/src/components/layout/appmenu.tsx b/frontend/providers/template/src/components/layout/appmenu.tsx index e72c4428c52..15f08ff8690 100644 --- a/frontend/providers/template/src/components/layout/appmenu.tsx +++ b/frontend/providers/template/src/components/layout/appmenu.tsx @@ -93,26 +93,6 @@ export default function AppMenu() { {t('SideBar.My App')} - -
{ - e.stopPropagation(); - changeI18n(); - }} - > - {i18n?.language === 'en' ? 'En' : '中'} -
) : (