From 9ca435cba8059047bf7d63c4b4c2d0ac6869c1a2 Mon Sep 17 00:00:00 2001 From: jingyang <72259332+zjy365@users.noreply.github.com> Date: Thu, 5 Sep 2024 14:42:14 +0800 Subject: [PATCH] feat: add SEO TDK support and pricebox to template (#5038) * feat: add SEO TDK support to template * add pricebox * update styles * fix quota * fix docs ci * fix ci * update toast * update Translation * template done * update price --- docs/website/scripts/sync-docs.js | 14 +- frontend/.lintstagedrc | 2 +- .../template/public/locales/en/common.json | 45 +++--- .../template/public/locales/zh/common.json | 39 ++--- .../src/components/Icon/icons/formInfo.svg | 4 +- .../src/components/Icon/icons/markdown.svg | 7 +- .../src/components/Icon/icons/sealosCoin.svg | 56 +++++++ .../template/src/components/Icon/index.tsx | 1 + .../template/src/components/Table/index.tsx | 10 +- .../src/components/icons/Application.tsx | 43 ++++++ .../src/components/icons/HtmlIcon.tsx | 32 +++- .../src/components/icons/PriceIcon.tsx | 41 +++++ .../template/src/components/icons/Share.tsx | 17 +- .../src/components/layout/appmenu.tsx | 3 +- .../template/src/components/layout/index.tsx | 2 +- .../providers/template/src/constants/theme.ts | 9 -- .../template/src/hooks/useConfirm.tsx | 2 + .../src/pages/api/getTemplateSource.ts | 4 +- .../src/pages/api/platform/resourcePrice.ts | 6 - .../src/pages/deploy/components/Form.tsx | 30 ++-- .../src/pages/deploy/components/Header.tsx | 64 ++++++-- .../src/pages/deploy/components/PriceBox.tsx | 128 ++++++---------- .../src/pages/deploy/components/ReadMe.tsx | 10 +- .../template/src/pages/deploy/index.tsx | 145 ++++++++++++------ .../template/src/pages/develop/index.tsx | 115 +++++++------- .../providers/template/src/pages/index.tsx | 40 +++-- .../src/pages/instance/components/appList.tsx | 44 ++---- .../pages/instance/components/cronjobList.tsx | 46 ++---- .../src/pages/instance/components/dbList.tsx | 46 ++---- .../pages/instance/components/delDodal.tsx | 2 +- .../src/pages/instance/components/header.tsx | 27 ++-- .../instance/components/objStorageList.tsx | 53 ++----- .../pages/instance/components/otherList.tsx | 37 ++--- .../providers/template/src/store/cached.ts | 1 + frontend/providers/template/src/store/user.ts | 2 +- .../providers/template/src/utils/template.ts | 6 +- .../providers/template/src/utils/usage.ts | 8 +- 37 files changed, 667 insertions(+), 474 deletions(-) create mode 100644 frontend/providers/template/src/components/Icon/icons/sealosCoin.svg create mode 100644 frontend/providers/template/src/components/icons/Application.tsx create mode 100644 frontend/providers/template/src/components/icons/PriceIcon.tsx diff --git a/docs/website/scripts/sync-docs.js b/docs/website/scripts/sync-docs.js index 4e2c06da58a..cf3f7c95d6f 100644 --- a/docs/website/scripts/sync-docs.js +++ b/docs/website/scripts/sync-docs.js @@ -11,6 +11,18 @@ async function generateVersionsJson () { await fs.writeJson(versionsJsonPath, versions, { spaces: 2 }) } +function safeJoin (base, ...parts) { + const joined = [base, ...parts].join(path.sep) + const normalized = path.normalize(joined) + + const normalizedBase = path.normalize(base) + if (!normalized.startsWith(normalizedBase) || normalized === normalizedBase) { + throw new Error('Path traversal attempt blocked') + } + + return normalized +} + async function syncDocs () { try { // Remove specified directories @@ -19,7 +31,7 @@ async function syncDocs () { 'i18n/zh-Hans/docusaurus-plugin-content-docs', 'versioned_docs', 'versioned_sidebars' - ].map(dir => path.join(websiteDir, dir)) + ].map(dir => safeJoin(websiteDir, dir)) await Promise.all(dirsToRemove.map(dir => fs.remove(dir))) diff --git a/frontend/.lintstagedrc b/frontend/.lintstagedrc index b09fd3ffd66..26d2af95a25 100644 --- a/frontend/.lintstagedrc +++ b/frontend/.lintstagedrc @@ -1,4 +1,4 @@ { - "**/src/**/*.{js,jsx,ts,tsx}": ["prettier --write", "eslint --fix", "eslint"], + "**/src/**/*.{js,jsx,ts,tsx}": ["prettier --write"], "**/src/**/*.{json,css,scss,md}": ["prettier --write"] } diff --git a/frontend/providers/template/public/locales/en/common.json b/frontend/providers/template/public/locales/en/common.json index 70231a256e6..0ab491a054a 100644 --- a/frontend/providers/template/public/locales/en/common.json +++ b/frontend/providers/template/public/locales/en/common.json @@ -7,13 +7,6 @@ "Advanced Configuration": "Advanced", "Age": "Uptime", "AnticipatedPrice": "Estimated Cost", - "app": { - "Resource Quota": "Resource Quota", - "The applied CPU exceeds the quota": "Requested CPU exceeds your quota. Please contact admin.", - "The applied GPU exceeds the quota": "Requested GPU exceeds your quota. Please contact admin.", - "The applied memory exceeds the quota": "Requested storage exceeds your quota. Please contact admin.", - "The applied storage exceeds the quota": "The applied storage exceeds the quota" - }, "App Name": "App Name", "Application Deployment": "App Deployment", "Application List": " App List", @@ -24,7 +17,6 @@ "Auto scaling": "Scaling", "Basic Information": "Basic", "Button Effect": "Button Appearance", - "cpu": "CPU", "CPU": "CPU", "CPU target is the CPU utilization rate of any container": "CPU target represents the CPU utilization rate of any container", "CPU target value": "CPU Target", @@ -33,10 +25,6 @@ "Command": "Command", "Command parameters": "Command parameters", "Component": "Component", - "common": { - "Surplus": "Surplus", - "Used": "Used" - }, "Config Form": "Form", "ConfigMap Path Conflict": "ConfigMap Path Conflict", "Configuration File": "Configmap", @@ -79,7 +67,6 @@ "File Value can not empty": "File content is required", "Filename can not empty": "File name is required", "Fixed instance": "Fixed", - "gpu": "GPU", "Heading to sealos soon": "Redirecting to Sealos shortly", "Home Page": "Project Home", "Html Part": "HTML Snippet", @@ -102,7 +89,6 @@ "Log": "Log", "Markdown Part": "Markdown Snippet", "Max Storage Value": "Maximum storage: ", - "memory": "Memory", "Memory": "Memory", "Memory target value": "Memory Target", "Min Storage Value": "Minimum storage: ", @@ -110,7 +96,6 @@ "Name": "Name", "Network Configuration": "Network", "Next Execution Time": "Next Scheduled", - "Port": "Port", "No Applications": "No Apps Available", "None": "None", "Not Configured": "Not Configured", @@ -121,7 +106,6 @@ "One click deploy button": "One-Click deploy button", "Operation": "Operation", "Option": "Optional", - "others": "Others", "Parameters": "Parameters", "Password": "Password", "Password for the image registry": "Image Registry Password", @@ -135,6 +119,7 @@ "Pod": "Pod", "Pod Name": "Pod Name", "Pods List": "Pods List", + "Port": "Port", "Private": "Private", "Problem Analysis": "Issue Analysis", "Prompt": "Notice", @@ -169,7 +154,6 @@ "Stateful": "Stateful", "Stateless": "Stateless", "Status": "Status", - "storage": "Storage", "Storage": "Storage", "Storage Range": "Storage Range", "Storage Value can not empty": "Storage capacity is required", @@ -180,7 +164,6 @@ "TemplateNameError": "Template name is required", "Templates": "Template Marketplace", "Terminal": "Terminal", - "There is no resource of this type": "No resources of this type available", "Total": "Total", "TotalPrice": "Total Price", "Type": "Type", @@ -194,13 +177,25 @@ "Username for the image registry": "Image Registry Username", "YAML File": "YAML", "You haven't created any application yet.": "You don't have any apps yet.", + "app": { + "Resource Quota": "Resource Quota", + "The applied CPU exceeds the quota": "Requested CPU exceeds your quota. Please contact admin.", + "The applied GPU exceeds the quota": "Requested GPU exceeds your quota. Please contact admin.", + "The applied memory exceeds the quota": "Requested storage exceeds your quota. Please contact admin.", + "The applied storage exceeds the quota": "The applied storage exceeds the quota" + }, "capacity": "capacity", + "common": { + "Surplus": "Surplus", + "Used": "Used" + }, + "cpu": "CPU", "cronjob": "CronJob", "database": "DataBase", "delete message": "Are you sure you want to delete this app? If you proceed, all project data will be permanently deleted.", "develop": { "Configure Form": "Configure Form", - "Debugging Template": "Debugging Template", + "Debugging Template": "Debugging", "Development": "Development", "Dryrun Deploy": "Test Deployment", "Formal Deploy": "Deploy", @@ -213,23 +208,29 @@ "file": "file", "file value": "File Content", "filename": "Filename", + "gpu": "GPU", "grpcs": "gRPCS", "https": "HTTPS", "jump_message": "This app can't be used independently. Click 'OK' to use it in Sealos Desktop.", "jump_prompt": "Redirect Notice", "launchpad": "App Launchpad", "local storage": "Local Storage", + "memory": "Memory", "mount path": "mount path", "multiple instances do not share data": "Storage volumes are not shared between instances", + "no_resource_type": "There are currently no resources of this type", "object_storage": "Object Storage", + "others": "Others", "pause_message": "Pausing will stop CPU and memory charges, but storage and external network port fees will continue. Pause now?", "permission": "Permission", + "price": "Price", "private": "private", "protocol": "protocol", "public": "public", + "storage": "Storage", "success": "success", "target_value": "Target Value", + "total_price_tip": "The estimated cost does not include traffic fees, and is subject to actual usage.", "users installed the app": "{{count}} users have installed this app", - "websocket": "WebSocket", - "total_price_tip": "The estimated cost does not include port fees and traffic fees, and is subject to actual usage." -} \ No newline at end of file + "websocket": "WebSocket" +} diff --git a/frontend/providers/template/public/locales/zh/common.json b/frontend/providers/template/public/locales/zh/common.json index 9cb060e45eb..603b387c6b9 100644 --- a/frontend/providers/template/public/locales/zh/common.json +++ b/frontend/providers/template/public/locales/zh/common.json @@ -7,13 +7,6 @@ "Advanced Configuration": "高级配置", "Age": "启动时长", "AnticipatedPrice": "预估价格", - "app": { - "Resource Quota": "资源配额", - "The applied CPU exceeds the quota": "申请的 CPU 超出配额限制,请联系管理员", - "The applied GPU exceeds the quota": "申请的 GPU 超出配额限制,请联系管理员", - "The applied memory exceeds the quota": "申请的 '内存' 超出配额限制,请联系管理员", - "The applied storage exceeds the quota": "申请的 '存储' 超出配额限制,请联系管理员" - }, "App Name": "应用名称", "Application Deployment": "应用部署", "Application List": " 应用列表", @@ -24,7 +17,6 @@ "Auto scaling": "弹性伸缩", "Basic Information": "基本信息", "Button Effect": "按钮效果", - "cpu": "CPU", "CPU": "CPU", "CPU target is the CPU utilization rate of any container": "CPU 目标值为任一容器的 CPU 利用率", "CPU target value": "CPU 目标值", @@ -33,10 +25,6 @@ "Command": "启动命令", "Command parameters": "命令参数", "Component": "组件", - "common": { - "Surplus": "剩余", - "Used": "已用" - }, "Config Form": "配置表单", "ConfigMap Path Conflict": "配置文件路径冲突", "Configuration File": "配置文件", @@ -101,7 +89,6 @@ "Log": "日志", "Markdown Part": "Markdown 片段", "Max Storage Value": "容量最大为", - "memory": "内存", "Memory": "内存", "Memory target value": "内存目标值", "Min Storage Value": "容量最小为", @@ -109,7 +96,6 @@ "Name": "名字", "Network Configuration": "网络配置", "Next Execution Time": "下次执行时间", - "Port": "端口", "No Applications": "暂无应用", "None": "暂无", "Not Configured": "未配置", @@ -120,7 +106,6 @@ "One click deploy button": "一键部署按钮", "Operation": "操作", "Option": "选填", - "others": "其他资源", "Parameters": "运行参数", "Password": "密码", "Password for the image registry": "镜像仓库的密码", @@ -134,6 +119,7 @@ "Pod": "实例", "Pod Name": "实例名", "Pods List": "实例列表", + "Port": "端口", "Private": "私有", "Problem Analysis": "问题分析", "Prompt": "提示", @@ -169,7 +155,6 @@ "Stateless": "无状态", "Status": "状态", "Storage": "存储卷", - "storage": "存储卷", "Storage Range": "容量范围", "Storage Value can not empty": "容量不能为空", "Storage path can not empty": "挂载路径不能为空", @@ -179,7 +164,7 @@ "TemplateNameError": "模板名称不能为空", "Templates": "模板市场", "Terminal": "终端", - "There is no resource of this type": "没有此类型的资源", + "no_resource_type": "暂无此类型的资源", "Total": "总计", "TotalPrice": "总价", "Type": "类型", @@ -193,7 +178,19 @@ "Username for the image registry": "镜像仓库的用户名", "YAML File": "YAML 文件", "You haven't created any application yet.": "您还没有新建应用。", + "app": { + "Resource Quota": "资源配额", + "The applied CPU exceeds the quota": "申请的 CPU 超出配额限制,请联系管理员", + "The applied GPU exceeds the quota": "申请的 GPU 超出配额限制,请联系管理员", + "The applied memory exceeds the quota": "申请的 '内存' 超出配额限制,请联系管理员", + "The applied storage exceeds the quota": "申请的 '存储' 超出配额限制,请联系管理员" + }, "capacity": "容量", + "common": { + "Surplus": "剩余", + "Used": "已用" + }, + "cpu": "CPU", "cronjob": "定时任务", "database": "数据库", "delete message": "您确定要删除此应用程序吗?\n如果继续,该项目的所有数据都将被删除。", @@ -219,17 +216,21 @@ "jump_prompt": "跳转提示", "launchpad": "应用管理", "local storage": "本地存储", + "memory": "内存", "mount path": "挂载路径", "multiple instances do not share data": "存储卷不支持多个实例共享", "object_storage": "对象存储", + "others": "其他资源", "pause_message": "暂停服务将停止计算 CPU 和内存等费用,但存储和外网端口仍将产生费用。是否现在暂停?", "permission": "权限", + "price": "价格", "private": "私有", "protocol": "协议", "public": "公共", + "storage": "存储卷", "success": "成功", "target_value": "目标值", + "total_price_tip": "预估费用不包括流量费用,以实际使用为准", "users installed the app": "已有 {{count}} 名用户安装该应用", - "websocket": "WebSocket", - "total_price_tip": "预估费用不包括端口费用和流量费用,以实际使用为准" + "websocket": "WebSocket" } \ No newline at end of file diff --git a/frontend/providers/template/src/components/Icon/icons/formInfo.svg b/frontend/providers/template/src/components/Icon/icons/formInfo.svg index 164e7a19d4c..f131c3b1806 100644 --- a/frontend/providers/template/src/components/Icon/icons/formInfo.svg +++ b/frontend/providers/template/src/components/Icon/icons/formInfo.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + \ No newline at end of file diff --git a/frontend/providers/template/src/components/Icon/icons/markdown.svg b/frontend/providers/template/src/components/Icon/icons/markdown.svg index 8b5c0113d85..5e8a62499ef 100644 --- a/frontend/providers/template/src/components/Icon/icons/markdown.svg +++ b/frontend/providers/template/src/components/Icon/icons/markdown.svg @@ -1,3 +1,4 @@ - - - + + + + \ No newline at end of file diff --git a/frontend/providers/template/src/components/Icon/icons/sealosCoin.svg b/frontend/providers/template/src/components/Icon/icons/sealosCoin.svg new file mode 100644 index 00000000000..7cf408d5b17 --- /dev/null +++ b/frontend/providers/template/src/components/Icon/icons/sealosCoin.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/providers/template/src/components/Icon/index.tsx b/frontend/providers/template/src/components/Icon/index.tsx index 096ef74030e..8be09d09888 100644 --- a/frontend/providers/template/src/components/Icon/index.tsx +++ b/frontend/providers/template/src/components/Icon/index.tsx @@ -38,6 +38,7 @@ const map = { dev: require('./icons/dev.svg').default, eyeShow: require('./icons/eyeShow.svg').default, tool: require('./icons/tool.svg').default, + sealosCoin: require('./icons/sealosCoin.svg').default, help: require('./icons/help.svg').default }; diff --git a/frontend/providers/template/src/components/Table/index.tsx b/frontend/providers/template/src/components/Table/index.tsx index 2f0f98ea0fe..e564dd1fbfb 100644 --- a/frontend/providers/template/src/components/Table/index.tsx +++ b/frontend/providers/template/src/components/Table/index.tsx @@ -26,13 +26,9 @@ const Table = ({ columns, data, itemClass = '' }: Props) => { key={item.key} color={'myGray.700'} whiteSpace={'nowrap'} - _first={{ - borderLeftRadius: 'md', - pl: 7 - }} - _last={{ - borderRightRadius: 'md' - }} + borderLeftRadius={i === 0 ? 'md' : undefined} + pl={i === 0 ? '7' : ''} + borderRightRadius={i === columns.length - 1 ? 'md' : undefined} > {t(item.title)} diff --git a/frontend/providers/template/src/components/icons/Application.tsx b/frontend/providers/template/src/components/icons/Application.tsx new file mode 100644 index 00000000000..1291baeb03a --- /dev/null +++ b/frontend/providers/template/src/components/icons/Application.tsx @@ -0,0 +1,43 @@ +import { Icon, IconProps } from '@chakra-ui/react'; + +export const AppLaunchpadIcon = (props: IconProps) => ( + + + +); + +export const CronjobIcon = () => ( + + + +); + +export const DBIcon = () => ( + + + + + +); + +export const StorageIcon = () => ( + + + +); + +export const OthersIcon = () => ( + + + + +); diff --git a/frontend/providers/template/src/components/icons/HtmlIcon.tsx b/frontend/providers/template/src/components/icons/HtmlIcon.tsx index 3a0cdf6463f..6714f58593d 100644 --- a/frontend/providers/template/src/components/icons/HtmlIcon.tsx +++ b/frontend/providers/template/src/components/icons/HtmlIcon.tsx @@ -1,7 +1,35 @@ import { Icon, IconProps } from '@chakra-ui/react'; export const HtmlIcon = (props: IconProps) => ( - - {' '} + + + + + + ); diff --git a/frontend/providers/template/src/components/icons/PriceIcon.tsx b/frontend/providers/template/src/components/icons/PriceIcon.tsx new file mode 100644 index 00000000000..d17bd50baf3 --- /dev/null +++ b/frontend/providers/template/src/components/icons/PriceIcon.tsx @@ -0,0 +1,41 @@ +import { Icon, IconProps } from '@chakra-ui/react'; + +export const PriceIcon = (props: IconProps) => ( + + + + + + + + +); diff --git a/frontend/providers/template/src/components/icons/Share.tsx b/frontend/providers/template/src/components/icons/Share.tsx index ea015044fb3..51cf4edca2e 100644 --- a/frontend/providers/template/src/components/icons/Share.tsx +++ b/frontend/providers/template/src/components/icons/Share.tsx @@ -1,8 +1,19 @@ import { Icon, IconProps } from '@chakra-ui/react'; export const ShareIcon = (props: IconProps) => ( - - - + + ); diff --git a/frontend/providers/template/src/components/layout/appmenu.tsx b/frontend/providers/template/src/components/layout/appmenu.tsx index d6caf95d111..379069a71c5 100644 --- a/frontend/providers/template/src/components/layout/appmenu.tsx +++ b/frontend/providers/template/src/components/layout/appmenu.tsx @@ -24,7 +24,7 @@ export default function AppMenu() { return ( - + <>{children} diff --git a/frontend/providers/template/src/constants/theme.ts b/frontend/providers/template/src/constants/theme.ts index 6444cf60639..83dafd742c7 100644 --- a/frontend/providers/template/src/constants/theme.ts +++ b/frontend/providers/template/src/constants/theme.ts @@ -251,19 +251,10 @@ export const theme = extendTheme(sealosTheme, { base: '1px solid #DEE0E2', md: '1px solid #BDC1C5' }, - radii: { - xs: '1px', - sm: '2px', - base: '2px', - md: '4px', - lg: '6px' - }, fontWeights: { bold: 500 }, components: { - Button, - Input, Tooltip, Select, Switch, diff --git a/frontend/providers/template/src/hooks/useConfirm.tsx b/frontend/providers/template/src/hooks/useConfirm.tsx index 07b99ccc6ce..a0dc0271bd0 100644 --- a/frontend/providers/template/src/hooks/useConfirm.tsx +++ b/frontend/providers/template/src/hooks/useConfirm.tsx @@ -53,6 +53,7 @@ export const useConfirm = ({ - diff --git a/frontend/providers/template/src/pages/deploy/components/PriceBox.tsx b/frontend/providers/template/src/pages/deploy/components/PriceBox.tsx index 535e288886c..088073c04e0 100644 --- a/frontend/providers/template/src/pages/deploy/components/PriceBox.tsx +++ b/frontend/providers/template/src/pages/deploy/components/PriceBox.tsx @@ -1,50 +1,21 @@ -import React, { useMemo } from 'react'; -import { Box, Center, Flex, useTheme } from '@chakra-ui/react'; +import React from 'react'; +import { Box, Center, Flex, useTheme, Divider } from '@chakra-ui/react'; import { useTranslation } from 'next-i18next'; import { Text, Icon } from '@chakra-ui/react'; import { useUserStore } from '@/store/user'; import MyIcon from '@/components/Icon'; import { MyTooltip } from '@sealos/ui'; import { type ResourceUsage } from '@/utils/usage'; +import { PriceIcon } from '@/components/icons/PriceIcon'; -function Currencysymbol({ +export function Currencysymbol({ type = 'shellCoin', ...props }: { type?: 'shellCoin' | 'cny' | 'usd'; } & Pick[0], 'w' | 'h' | 'color'>) { return type === 'shellCoin' ? ( - - - - - - - - - - + ) : type === 'cny' ? ( ) : ( @@ -54,12 +25,10 @@ function Currencysymbol({ const scale = 1000000; -const PriceBox = ({ cpu, memory, storage, nodeport }: ResourceUsage) => { - const { t } = useTranslation(); +export const usePriceCalculation = ({ cpu, memory, storage, nodeport }: ResourceUsage) => { const { userSourcePrice } = useUserStore(); - const theme = useTheme(); - const priceList = useMemo(() => { + return React.useMemo(() => { if (!userSourcePrice) return []; const cpuPMin = +((userSourcePrice.cpu * cpu.min * 24) / scale).toFixed(2); @@ -73,57 +42,58 @@ const PriceBox = ({ cpu, memory, storage, nodeport }: ResourceUsage) => { const nodePortP = +((userSourcePrice.nodeports * nodeport * 24) / 1000).toFixed(2); - // const gpuPMin = (() => { - // if (!gpu || !gpu[0]) return 0; - // const item = userSourcePrice?.gpu?.find((item) => item.type === gpu[0].type); - // if (!item) return 0; - // return +(item.price * gpu[0].amount * 24).toFixed(2); - // })(); - - // const gpuPMax = (() => { - // if (!gpu || !gpu[1]) return 0; - // const item = userSourcePrice?.gpu?.find((item) => item.type === gpu[1].type); - // if (!item) return 0; - // return +(item.price * gpu[1].amount * 24).toFixed(2); - // })(); - const totalPMin = +(cpuPMin + memoryPMin + storagePMin + nodePortP).toFixed(2); const totalPMax = +(cpuPMax + memoryPMax + storagePMax + nodePortP).toFixed(2); - const podScale = (min: number, max: number) => { - return ( - - - {min === max ? `${min}` : `${min} ~ ${max}`} - - ); - }; + const formatRange = (min: number, max: number) => (min === max ? `${min}` : `${min} ~ ${max}`); return [ { label: 'CPU', color: '#13C4B9', - value: podScale(cpuPMin, cpuPMax) + value: formatRange(cpuPMin, cpuPMax) + }, + { + label: 'Memory', + color: '#219BF4', + value: formatRange(memoryPMin, memoryPMax) + }, + { + label: 'Storage', + color: '#8774EE', + value: formatRange(storagePMin, storagePMax) }, - { label: 'Memory', color: '#219BF4', value: podScale(memoryPMin, memoryPMax) }, - { label: 'Storage', color: '#8774EE', value: podScale(storagePMin, storagePMax) }, - { label: 'Port', color: '#C172E7', value: podScale(nodePortP, nodePortP) }, - // ...(userSourcePrice?.gpu - // ? [{ label: 'GPU', color: '#89CD11', value: podScale(gpuPMin, gpuPMax) }] - // : []), - { label: 'TotalPrice', color: '#111824', value: podScale(totalPMin, totalPMax) } + { + label: 'Port', + color: '#C172E7', + value: `${nodePortP}` + }, + { + label: 'TotalPrice', + color: '#111824', + value: formatRange(totalPMin, totalPMax) + } ]; - }, [cpu, memory, storage, userSourcePrice]); + }, [cpu, memory, storage, nodeport, userSourcePrice]); +}; + +const PriceBox = (props: ResourceUsage) => { + const { t } = useTranslation(); + const priceList = usePriceCalculation(props); return ( - - - - {t('AnticipatedPrice')} - - ({t('Perday')}) + + + + + + {t('AnticipatedPrice')} + + ({t('Perday')}) + + - + {priceList.map((item, index) => ( { )} - : - {item.value} + + + {item.value} + ))} diff --git a/frontend/providers/template/src/pages/deploy/components/ReadMe.tsx b/frontend/providers/template/src/pages/deploy/components/ReadMe.tsx index fd95efbabbe..f1ddde44c36 100644 --- a/frontend/providers/template/src/pages/deploy/components/ReadMe.tsx +++ b/frontend/providers/template/src/pages/deploy/components/ReadMe.tsx @@ -57,18 +57,18 @@ const ReadMe = ({ templateDetail }: { templateDetail: TemplateType }) => { }; return ( - + - + README.md - + import('./components/ErrorModal')); const Header = dynamic(() => import('./components/Header'), { ssr: false }); -export default function EditApp({ appName }: { appName?: string }) { +export default function EditApp({ + appName, + metaData +}: { + appName?: string; + metaData: { + title: string; + keywords: string; + description: string; + }; +}) { const { t, i18n } = useTranslation(); - const { toast } = useToast(); + const { message: toast } = useMessage(); const router = useRouter(); const { copyData } = useCopyData(); const { templateName } = router.query as QueryType; @@ -56,7 +68,7 @@ export default function EditApp({ appName }: { appName?: string }) { ); const usage = useMemo(() => { - const usage = getResourceUsage(yamlList.map((item) => item.value)); + const usage = getResourceUsage(yamlList?.map((item) => item.value) || []); return usage; }, [yamlList]); @@ -144,6 +156,7 @@ export default function EditApp({ appName }: { appName?: string }) { memory: usage.memory.max, storage: usage.storage.max }); + if (quoteCheckRes) { return toast({ status: 'warning', @@ -152,40 +165,62 @@ export default function EditApp({ appName }: { appName?: string }) { isClosable: true }); } - console.log('quoteCheckRes', quoteCheckRes); setIsLoading(true); + try { if (!insideCloud) { - setIsLoading(false); - setCached(JSON.stringify({ ...formHook.getValues(), cachedKey: templateName })); - const _name = encodeURIComponent(`?templateName=${templateName}&sealos_inside=true`); - const _domain = platformEnvs?.SEALOS_CLOUD_DOMAIN; - const href = `https://${_domain}/?openapp=system-template${_name}`; - return window.open(href, '_self'); + handleOutside(); + } else { + await handleInside(); } - const yamls = yamlList.map((item) => item.value); - - await postDeployApp(yamls, 'create'); - - toast({ - title: t(applySuccess), - status: 'success' - }); - - deleteCached(); - setAppType(ApplicationType.MyApp); - router.push({ - pathname: '/instance', - query: { - instanceName: detailName - } - }); } catch (error) { setErrorMessage(JSON.stringify(error)); } setIsLoading(false); }; + const handleOutside = () => { + setCached(JSON.stringify({ ...formHook.getValues(), cachedKey: templateName })); + + const params = new URLSearchParams(); + ['k', 's', 'bd_vid'].forEach((param) => { + const value = router.query[param]; + if (typeof value === 'string') { + params.append(param, value); + } + }); + + const queryString = params.toString(); + + const baseUrl = `https://${platformEnvs?.SEALOS_CLOUD_DOMAIN}/`; + const encodedTemplateQuery = encodeURIComponent( + `?templateName=${templateName}&sealos_inside=true` + ); + const templateQuery = `openapp=system-template${encodedTemplateQuery}`; + const href = `${baseUrl}${ + queryString ? `?${queryString}&${templateQuery}` : `?${templateQuery}` + }`; + + window.open(href, '_self'); + }; + + const handleInside = async () => { + const yamls = yamlList.map((item) => item.value); + await postDeployApp(yamls, 'create'); + + toast({ + title: t(applySuccess), + status: 'success' + }); + + deleteCached(); + setAppType(ApplicationType.MyApp); + router.push({ + pathname: '/instance', + query: { instanceName: detailName } + }); + }; + const submitError = async () => { await formHook.trigger(); toast({ @@ -262,6 +297,15 @@ export default function EditApp({ appName }: { appName?: string }) { borderRadius={'12px'} background={'linear-gradient(180deg, #FFF 0%, rgba(255, 255, 255, 0.70) 100%)'} > + + {`${metaData.title}${ + i18n.language === 'en' + ? 'Deployment and installation tutorial - Sealos' + : '部署和安装教程 - Sealos' + }`} + + + formHook.handleSubmit(openConfirm(submitSuccess), submitError)()} /> - {/* - - */} - - -
- - - {userSourcePrice && ( - - - - )} - - + {/* */} + {/* */} @@ -383,9 +414,29 @@ export async function getServerSideProps(content: any) { const appName = content?.query?.templateName || ''; + const baseurl = `http://${process.env.HOSTNAME || 'localhost'}:${process.env.PORT || 3000}`; + + let metaData = { + title: `${appName}部署和安装教程 - Sealos`, + keywords: '', + description: '' + }; + + try { + const templateSource: { data: TemplateSourceType } = await ( + await fetch(`${baseurl}/api/getTemplateSource?templateName=${appName}`) + ).json(); + metaData = { + title: templateSource?.data.templateYaml.spec.title, + keywords: templateSource?.data.templateYaml.spec.description, + description: templateSource?.data.templateYaml.spec.description + }; + } catch (error) {} + return { props: { appName, + metaData, ...(await serviceSideProps(content)) } }; diff --git a/frontend/providers/template/src/pages/develop/index.tsx b/frontend/providers/template/src/pages/develop/index.tsx index 6edfa49807f..3ef0add5b3f 100644 --- a/frontend/providers/template/src/pages/develop/index.tsx +++ b/frontend/providers/template/src/pages/develop/index.tsx @@ -12,7 +12,7 @@ import { developGenerateYamlList, handleTemplateToInstanceYaml, parseTemplateString, - getYamlSource, + getYamlSource } from '@/utils/json-yaml'; import { getTemplateInputDefaultValues, getTemplateValues } from '@/utils/template'; import { downLoadBold } from '@/utils/tools'; @@ -42,67 +42,76 @@ export default function Develop() { data: EnvResponse; }; - const generateYamlData = useCallback(( - yamlSource: TemplateSourceType, - inputs: Record = {} - ): YamlItemType[] => { - const { defaults, defaultInputs } = getTemplateValues(yamlSource); - const data = { - ...platformEnvs, - ...yamlSource?.source, - inputs: { - ...defaultInputs, - ...inputs - }, - defaults: defaults - }; - const generateStr = parseTemplateString(yamlSource.appYaml, data); - const _instanceName = yamlSource?.source?.defaults?.app_name?.value || ''; - return developGenerateYamlList(generateStr, _instanceName) - }, [platformEnvs]); + const generateYamlData = useCallback( + (yamlSource: TemplateSourceType, inputs: Record = {}): YamlItemType[] => { + const { defaults, defaultInputs } = getTemplateValues(yamlSource); + const data = { + ...platformEnvs, + ...yamlSource?.source, + inputs: { + ...defaultInputs, + ...inputs + }, + defaults: defaults + }; + const generateStr = parseTemplateString(yamlSource.appYaml, data); + const _instanceName = yamlSource?.source?.defaults?.app_name?.value || ''; + return developGenerateYamlList(generateStr, _instanceName); + }, + [platformEnvs] + ); - const parseTemplate = useCallback((str: string) => { - if (!str || !str.trim()) { - setTemplateSource(void 0); - setYamlList([]); - return; - } - try { - const result = getYamlSource(str, platformEnvs); - const formInputs = formHook.getValues(); - setTemplateSource(result); - const correctYamlList = generateYamlData(result, formInputs); - setYamlList(correctYamlList); - } catch (error: any) { - toast({ - title: 'Parsing Yaml Error', - status: 'error', - description:
{error?.message}
, - duration: 4000, - isClosable: true - }); - } - }, [platformEnvs, generateYamlData]); + const parseTemplate = useCallback( + (str: string) => { + if (!str || !str.trim()) { + setTemplateSource(void 0); + setYamlList([]); + return; + } + try { + const result = getYamlSource(str, platformEnvs); + const formInputs = formHook.getValues(); + setTemplateSource(result); + const correctYamlList = generateYamlData(result, formInputs); + setYamlList(correctYamlList); + } catch (error: any) { + toast({ + title: 'Parsing Yaml Error', + status: 'error', + description:
{error?.message}
, + duration: 4000, + isClosable: true + }); + } + }, + [platformEnvs, generateYamlData] + ); - const onYamlChange = useCallback(debounce((doc: string) => { - parseTemplate(doc); - }, 1000), [parseTemplate]); + const onYamlChange = useCallback( + debounce((doc: string) => { + parseTemplate(doc); + }, 1000), + [parseTemplate] + ); // form const formHook = useForm({ defaultValues: getTemplateInputDefaultValues(templateSource) }); - const formOnchangeDebounce = useCallback(debounce((formInputData: Record) => { - try { - if (templateSource) { - const correctYamlList = generateYamlData(templateSource, formInputData); - setYamlList(correctYamlList); + const formOnchangeDebounce = useCallback( + debounce((formInputData: Record) => { + try { + if (templateSource) { + const correctYamlList = generateYamlData(templateSource, formInputData); + setYamlList(correctYamlList); + } + } catch (error) { + console.log(error); } - } catch (error) { - console.log(error); - } - }, 500), [templateSource, generateYamlData]); + }, 500), + [templateSource, generateYamlData] + ); // watch form change, compute new yaml useEffect(() => { diff --git a/frontend/providers/template/src/pages/index.tsx b/frontend/providers/template/src/pages/index.tsx index 53125e02471..ee8dab5af52 100644 --- a/frontend/providers/template/src/pages/index.tsx +++ b/frontend/providers/template/src/pages/index.tsx @@ -26,11 +26,13 @@ import { customAlphabet } from 'nanoid'; import { useTranslation } from 'next-i18next'; import { useRouter } from 'next/router'; import { MouseEvent, useEffect, useMemo } from 'react'; +import Head from 'next/head'; +import { ShareIcon } from '@/components/icons'; const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'); export default function AppList({ showCarousel }: { showCarousel: boolean }) { - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); const router = useRouter(); const { searchValue, appType } = useSearchStore(); const { setInsideCloud } = useCachedStore(); @@ -95,6 +97,29 @@ export default function AppList({ showCarousel }: { showCarousel: boolean }) { py="24px" px="42px" > + + + {i18n.language === 'en' + ? 'Enterprise-level distributed application hosting platform - Sealos' + : '企业级分布式应用托管平台 - Sealos'} + + + + {!!data?.templates?.length ? ( <> {showCarousel && } @@ -208,18 +233,7 @@ export default function AppList({ showCarousel }: { showCarousel: boolean }) { ))}
goGithub(e, item?.spec?.gitRepo)}> - - - - +
diff --git a/frontend/providers/template/src/pages/instance/components/appList.tsx b/frontend/providers/template/src/pages/instance/components/appList.tsx index f4550dac4f9..099217c8582 100644 --- a/frontend/providers/template/src/pages/instance/components/appList.tsx +++ b/frontend/providers/template/src/pages/instance/components/appList.tsx @@ -1,17 +1,19 @@ import { getAppLaunchpadByName } from '@/api/instance'; import MyIcon from '@/components/Icon'; +import { AppLaunchpadIcon } from '@/components/icons/Application'; import StatusTag from '@/components/StatusTag'; import MyTable from '@/components/Table'; import { useResourceStore } from '@/store/resource'; import useSessionStore from '@/store/session'; import { AppListItemType } from '@/types/launchpad'; import { printMemory } from '@/utils/tools'; -import { Box, Button, Flex, Icon, Text } from '@chakra-ui/react'; +import { Box, Button, Center, Flex, Icon, Text } from '@chakra-ui/react'; import { useQuery } from '@tanstack/react-query'; import { useTranslation } from 'next-i18next'; import { useCallback, useMemo } from 'react'; import { sealosApp } from 'sealos-desktop-sdk/app'; +export const EmptyBoxHeight = '60px'; export const refetchIntervalTime = 10 * 1000; export default function AppList({ instanceName }: { instanceName: string }) { @@ -113,9 +115,13 @@ export default function AppList({ instanceName }: { instanceName: string }) { + diff --git a/frontend/providers/template/src/pages/instance/components/objStorageList.tsx b/frontend/providers/template/src/pages/instance/components/objStorageList.tsx index cadbf959a2d..99ed6ebb241 100644 --- a/frontend/providers/template/src/pages/instance/components/objStorageList.tsx +++ b/frontend/providers/template/src/pages/instance/components/objStorageList.tsx @@ -5,12 +5,14 @@ import MyTable from '@/components/Table'; import { useResourceStore } from '@/store/resource'; import useSessionStore from '@/store/session'; import { ObjectStorageItemType } from '@/types/objectStorage'; -import { Box, Button, Flex, Icon, Text } from '@chakra-ui/react'; +import { Box, Button, Center, Flex, Icon, Text } from '@chakra-ui/react'; import { useQuery } from '@tanstack/react-query'; import { useTranslation } from 'next-i18next'; import { useCallback, useMemo } from 'react'; import { sealosApp } from 'sealos-desktop-sdk/app'; import { refetchIntervalTime } from './appList'; +import { StorageIcon } from '@/components/icons/Application'; +import { EmptyBoxHeight } from './appList'; export default function ObjStorageList({ instanceName }: { instanceName: string }) { const { t } = useTranslation(); @@ -84,9 +86,13 @@ export default function ObjStorageList({ instanceName }: { instanceName: string