diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 3308ede506d..c59d36c6b7c 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -656,7 +656,7 @@ importers: version: 5.9.1 next: specifier: 13.1.6 - version: 13.1.6(@babel/core@7.23.3)(react-dom@18.2.0)(react@18.2.0)(sass@1.69.5) + version: 13.1.6(react-dom@18.2.0)(react@18.2.0)(sass@1.69.5) next-i18next: specifier: ^13.3.0 version: 13.3.0(i18next@22.5.1)(next@13.1.6)(react-i18next@12.3.1)(react@18.2.0) @@ -1454,6 +1454,9 @@ importers: next-i18next: specifier: ^13.3.0 version: 13.3.0(i18next@22.5.1)(next@13.1.6)(react-i18next@12.3.1)(react@18.2.0) + node-cron: + specifier: ^3.0.3 + version: 3.0.3 nprogress: specifier: ^0.2.0 version: 0.2.0 @@ -4101,7 +4104,7 @@ packages: '@chakra-ui/react': 2.8.2(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.37)(framer-motion@10.16.5)(react-dom@18.2.0)(react@18.2.0) '@emotion/cache': 11.11.0 '@emotion/react': 11.11.1(@types/react@18.2.37)(react@18.2.0) - next: 13.5.6(react-dom@18.2.0)(react@18.2.0) + next: 13.5.6(@babel/core@7.23.5)(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 dev: false @@ -14137,6 +14140,51 @@ packages: - babel-plugin-macros dev: false + /next@13.1.6(react-dom@18.2.0)(react@18.2.0)(sass@1.69.5): + resolution: {integrity: sha512-hHlbhKPj9pW+Cymvfzc15lvhaOZ54l+8sXDXJWm3OBNBzgrVj6hwGPmqqsXg40xO1Leq+kXpllzRPuncpC0Phw==} + engines: {node: '>=14.6.0'} + hasBin: true + peerDependencies: + fibers: '>= 3.1.0' + node-sass: ^6.0.0 || ^7.0.0 + react: ^18.2.0 + react-dom: ^18.2.0 + sass: ^1.3.0 + peerDependenciesMeta: + fibers: + optional: true + node-sass: + optional: true + sass: + optional: true + dependencies: + '@next/env': 13.1.6 + '@swc/helpers': 0.4.14 + caniuse-lite: 1.0.30001565 + postcss: 8.4.14 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + sass: 1.69.5 + styled-jsx: 5.1.1(@babel/core@7.23.5)(react@18.2.0) + optionalDependencies: + '@next/swc-android-arm-eabi': 13.1.6 + '@next/swc-android-arm64': 13.1.6 + '@next/swc-darwin-arm64': 13.1.6 + '@next/swc-darwin-x64': 13.1.6 + '@next/swc-freebsd-x64': 13.1.6 + '@next/swc-linux-arm-gnueabihf': 13.1.6 + '@next/swc-linux-arm64-gnu': 13.1.6 + '@next/swc-linux-arm64-musl': 13.1.6 + '@next/swc-linux-x64-gnu': 13.1.6 + '@next/swc-linux-x64-musl': 13.1.6 + '@next/swc-win32-arm64-msvc': 13.1.6 + '@next/swc-win32-ia32-msvc': 13.1.6 + '@next/swc-win32-x64-msvc': 13.1.6 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + dev: false + /next@13.2.4(react-dom@18.2.0)(react@18.2.0)(sass@1.69.5): resolution: {integrity: sha512-g1I30317cThkEpvzfXujf0O4wtaQHtDCLhlivwlTJ885Ld+eOgcz7r3TGQzeU+cSRoNHtD8tsJgzxVdYojFssw==} engines: {node: '>=14.6.0'} @@ -14165,7 +14213,7 @@ packages: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) sass: 1.69.5 - styled-jsx: 5.1.1(@babel/core@7.23.3)(react@18.2.0) + styled-jsx: 5.1.1(@babel/core@7.23.5)(react@18.2.0) optionalDependencies: '@next/swc-android-arm-eabi': 13.2.4 '@next/swc-android-arm64': 13.2.4 @@ -14256,7 +14304,7 @@ packages: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) sass: 1.69.5 - styled-jsx: 5.1.1(@babel/core@7.23.3)(react@18.2.0) + styled-jsx: 5.1.1(@babel/core@7.23.5)(react@18.2.0) watchpack: 2.4.0 zod: 3.21.4 optionalDependencies: @@ -14313,43 +14361,11 @@ packages: - babel-plugin-macros dev: false - /next@13.5.6(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-Y2wTcTbO4WwEsVb4A8VSnOsG1I9ok+h74q0ZdxkwM3EODqrs4pasq7O0iUxbcS9VtWMicG7f3+HAj0r1+NtKSw==} - engines: {node: '>=16.14.0'} - hasBin: true - peerDependencies: - '@opentelemetry/api': ^1.1.0 - react: ^18.2.0 - react-dom: ^18.2.0 - sass: ^1.3.0 - peerDependenciesMeta: - '@opentelemetry/api': - optional: true - sass: - optional: true + /node-cron@3.0.3: + resolution: {integrity: sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==} + engines: {node: '>=6.0.0'} dependencies: - '@next/env': 13.5.6 - '@swc/helpers': 0.5.2 - busboy: 1.6.0 - caniuse-lite: 1.0.30001565 - postcss: 8.4.31 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - styled-jsx: 5.1.1(@babel/core@7.23.3)(react@18.2.0) - watchpack: 2.4.0 - optionalDependencies: - '@next/swc-darwin-arm64': 13.5.6 - '@next/swc-darwin-x64': 13.5.6 - '@next/swc-linux-arm64-gnu': 13.5.6 - '@next/swc-linux-arm64-musl': 13.5.6 - '@next/swc-linux-x64-gnu': 13.5.6 - '@next/swc-linux-x64-musl': 13.5.6 - '@next/swc-win32-arm64-msvc': 13.5.6 - '@next/swc-win32-ia32-msvc': 13.5.6 - '@next/swc-win32-x64-msvc': 13.5.6 - transitivePeerDependencies: - - '@babel/core' - - babel-plugin-macros + uuid: 8.3.2 dev: false /node-int64@0.4.0: diff --git a/frontend/providers/applaunchpad/.env.template b/frontend/providers/applaunchpad/.env.template index d3fad87f235..a27352112d3 100644 --- a/frontend/providers/applaunchpad/.env.template +++ b/frontend/providers/applaunchpad/.env.template @@ -2,4 +2,7 @@ NEXT_PUBLIC_MOCK_USER= SEALOS_DOMAIN="cloud.sealos.io" DOMAIN_PORT= FASTGPT_KEY= -CURRENCY= \ No newline at end of file +CURRENCY= +MONITOR_URL= +INGRESS_SECRET= +GUIDE_ENABLED= \ No newline at end of file diff --git a/frontend/providers/applaunchpad/next-i18next.config.js b/frontend/providers/applaunchpad/next-i18next.config.js index 8dca5135fe0..1a032fe9ec2 100644 --- a/frontend/providers/applaunchpad/next-i18next.config.js +++ b/frontend/providers/applaunchpad/next-i18next.config.js @@ -9,4 +9,4 @@ module.exports = { locales: ['en', 'zh'], localeDetection: false } -}; +} diff --git a/frontend/providers/applaunchpad/public/locales/en/common.json b/frontend/providers/applaunchpad/public/locales/en/common.json index 2097d7e465e..40db3e86997 100644 --- a/frontend/providers/applaunchpad/public/locales/en/common.json +++ b/frontend/providers/applaunchpad/public/locales/en/common.json @@ -182,5 +182,49 @@ "Add Port": "Add Port", "Age": "Age", "Amount": "Amount", - "ConfigMap Tip": "ConfigMap" + "ConfigMap Tip": "ConfigMap", + "No Data Available": "No Data", + "Restart Success": "Restart Success", + "Total": "Total", + "Copy": "Copy", + "Open Link": "Open Link", + "Application paused": "Application Paused", + "Start Successful": "Start Successful", + "Card": "Card", + "Pod": "Pod", + "No GPU": "No Gpu", + "Pod Name": "Pod Name", + "Port": "Port", + "Waiting": "Waiting", + "Yes": "Yes", + "vm": "vm", + "Deploy": "Deploy", + "Application failed": "Application failed", + "CPU target value": "cpu target value", + "Cname auth error: customDomain's cname is not equal to publicDomain": "Cname auth error: customDomain's cname is not equal to publicDomain", + "ConfigMap Conflict": "ConfigMap Conflict", + "Copy Domain Success": "Copy Domain Success", + "Custom Domain Error": "Custom Domain Error", + "If no, the default command is used": "If no, the default command is used", + "Input your custom domain": "Input your custom domain", + "Inventory": "Inventory", + "Items": "Items", + "Memory target value": "Memory target value", + "Not allowed to change app name": "Not allowed to change app name", + "Number of instances: 1 to 20": "Number of instances: 1 to 20", + "Public Access": "Public Access", + "Restart Failed": "Restart Failed", + "Start Failed": "Start Failed", + "Starts with a letter and can contain only lowercase letters, digits, and hyphens (-)": "Starts with a letter and can contain only lowercase letters, digits, and hyphens (-)", + "The Cpu target is empty": "The Cpu target is empty", + "The application name can contain only lowercase letters, digits, and hyphens (-) and must start with a letter": "The application name can contain only lowercase letters, digits, and hyphens (-) and must start with a letter", + "The cpu target value must be positive": "The cpu target value must be positive", + "The image cannot be empty": "The image cannot be empty", + "The maximum number of instances is 20": "The maximum number of instances is 20", + "The minimum number of instances is 1": "The minimum number of instances is 1", + "The number of instances cannot be empty": "The number of instances cannot be empty", + "The password cannot be empty": "The password cannot be empty", + "The target cpu value must be less than 100": "The target cpu value must be less than 100", + "The user name cannot be empty": "The user name cannot be empty", + "Under Stock": "Under Stock" } diff --git a/frontend/providers/applaunchpad/public/locales/zh/common.json b/frontend/providers/applaunchpad/public/locales/zh/common.json index a1596913eaa..f6f1570f229 100644 --- a/frontend/providers/applaunchpad/public/locales/zh/common.json +++ b/frontend/providers/applaunchpad/public/locales/zh/common.json @@ -141,7 +141,7 @@ "File Name": "文件名", "Filename can not empty": "文件名不能为空", "File Value can not empty": "文件值不能为空", - "ConfigMap Conflict": "ConfigMap Path Conflict", + "ConfigMap Conflict": "配置映射冲突", "Storage path can not empty": "挂载路径不能为空", "Storage Range": "容量范围", "Storage Value can not empty": "容量不能为空", @@ -221,5 +221,10 @@ "Configurable number of instances or automatic horizontal scaling": "可配置实例数或自动横向伸缩", "gift time tip": "{{time}} 小时内", "gift amount tip": "充值 {{amount}} 赠送 {{gift}} ", - "ConfigMap Tip": "配置文件" + "ConfigMap Tip": "配置文件", + "No Data Available": "暂无数据", + "Restart Success": "重启成功", + "ConfigMap Path Conflict": "ConfigMap 路径冲突", + "Deploy Application": "部署应用", + "Reboot Success": "重启成功" } diff --git a/frontend/providers/applaunchpad/src/api/app.ts b/frontend/providers/applaunchpad/src/api/app.ts index 850adc7f989..310e9043a39 100644 --- a/frontend/providers/applaunchpad/src/api/app.ts +++ b/frontend/providers/applaunchpad/src/api/app.ts @@ -8,6 +8,7 @@ import { adaptEvents } from '@/utils/adapt'; import type { AppPatchPropsType } from '@/types/app'; +import { MonitorDataResult, MonitorQueryKey } from '@/types/monitor'; export const postDeployApp = (yamlList: string[]) => POST('/api/applyApp', { yamlList }); @@ -50,3 +51,9 @@ export const pauseAppByName = (appName: string) => GET(`/api/pauseApp?appName=${ export const startAppByName = (appName: string) => GET(`/api/startApp?appName=${appName}`); export const restartPodByName = (podName: string) => GET(`/api/restartPod?podName=${podName}`); + +export const getAppMonitorData = (payload: { + queryName: string; + queryKey: keyof MonitorQueryKey; + step: string; +}) => GET(`/api/monitor/getMonitorData`, payload); diff --git a/frontend/providers/applaunchpad/src/components/PodLineChart/index.tsx b/frontend/providers/applaunchpad/src/components/PodLineChart/index.tsx index 7ff182ada02..3c553f52ee4 100644 --- a/frontend/providers/applaunchpad/src/components/PodLineChart/index.tsx +++ b/frontend/providers/applaunchpad/src/components/PodLineChart/index.tsx @@ -1,6 +1,8 @@ import React, { useEffect, useMemo, useRef } from 'react'; import * as echarts from 'echarts'; import { useGlobalStore } from '@/store/global'; +import { MonitorDataResult } from '@/types/monitor'; +import dayjs from 'dayjs'; const map = { blue: { @@ -92,14 +94,16 @@ const map = { const PodLineChart = ({ type, - limit = 1000000, data }: { type: 'blue' | 'deepBlue' | 'green' | 'purple'; - limit: number; - data: number[]; + data?: MonitorDataResult; }) => { const { screenWidth } = useGlobalStore(); + const xData = + data?.xData?.map((time) => (time ? dayjs(time * 1000).format('HH:mm') : '')) || + new Array(30).fill(0); + const yData = data?.yData || new Array(30).fill(''); const Dom = useRef(null); const myChart = useRef(); @@ -125,7 +129,7 @@ const PodLineChart = ({ type: 'category', show: false, boundaryGap: false, - data: data.map((_, i) => i) + data: xData }, yAxis: { type: 'value', @@ -146,11 +150,14 @@ const PodLineChart = ({ axisPointer: { type: 'line' }, - formatter: (e: any[]) => `${e[0]?.value || 0}%` + formatter: (params: any[]) => { + const axisValue = params[0]?.axisValue; + return `${axisValue} ${params[0]?.value || 0}%`; + } }, series: [ { - data: new Array(data.length).fill(0), + data: yData, type: 'line', showSymbol: false, smooth: true, @@ -158,7 +165,6 @@ const PodLineChart = ({ animationEasingUpdate: 'linear', ...optionStyle, emphasis: { - // highlight disabled: true } } @@ -175,14 +181,10 @@ const PodLineChart = ({ // data changed, update useEffect(() => { if (!myChart.current || !myChart?.current?.getOption()) return; - - const uniData = data.map((item) => ((item / limit) * 100).toFixed(2)); - - const x = option.current.xAxis.data; - option.current.xAxis.data = [...x.slice(1), x[x.length - 1] + 1]; - option.current.series[0].data = uniData; + option.current.xAxis.data = xData; + option.current.series[0].data = yData; myChart.current.setOption(option.current); - }, [data, limit]); + }, [xData, yData]); // type changed, update useEffect(() => { diff --git a/frontend/providers/applaunchpad/src/components/Table/index.tsx b/frontend/providers/applaunchpad/src/components/Table/index.tsx index f0970f15912..2f0f98ea0fe 100644 --- a/frontend/providers/applaunchpad/src/components/Table/index.tsx +++ b/frontend/providers/applaunchpad/src/components/Table/index.tsx @@ -16,65 +16,54 @@ interface Props extends BoxProps { const Table = ({ columns, data, itemClass = '' }: Props) => { const { t } = useTranslation(); return ( - <> - - {columns.map((item, i) => ( - - {t(item.title)} - - ))} - - {data.map((item: any, index1) => ( - + {columns.map((item, i) => ( + - {columns.map((col, index2) => ( - - {col.render ? col.render(item) : col.dataIndex ? `${item[col.dataIndex]}` : ''} - - ))} - + {t(item.title)} + ))} - + {data.map((item: any, index1) => + columns.map((col, index2) => ( + + {col.render ? col.render(item) : col.dataIndex ? `${item[col.dataIndex]}` : ''} + + )) + )} + ); }; diff --git a/frontend/providers/applaunchpad/src/constants/monitor.ts b/frontend/providers/applaunchpad/src/constants/monitor.ts new file mode 100644 index 00000000000..713e057f8b5 --- /dev/null +++ b/frontend/providers/applaunchpad/src/constants/monitor.ts @@ -0,0 +1,36 @@ +export const LineStyleMap = [ + { + backgroundColor: '#EBF5FB', + lineColor: '#5EBDF2' + }, + { + backgroundColor: 'rgba(241, 240, 249, 1)', + lineColor: 'rgba(154, 142, 224, 1)' + }, + { + backgroundColor: 'rgba(237, 247, 247, 1)', + lineColor: 'rgba(108, 211, 204, 1)' + }, + + { + backgroundColor: 'rgba(250, 239, 244, 1)', + lineColor: 'rgba(241, 130, 170, 1)' + }, + { + backgroundColor: 'rgba(108, 211, 204, 0.1)', + lineColor: 'rgba(108, 211, 204, 1)' + }, + { + backgroundColor: 'rgba(250, 239, 244, 1)', + lineColor: 'rgba(252, 150, 99, 1)' + }, + + { + backgroundColor: 'rgba(251, 235, 238, 1)', + lineColor: 'rgba(255, 91, 110, 1)' + }, + { + backgroundColor: 'rgba(249, 248, 234, 1)', + lineColor: 'rgba(236, 218, 70, 1)' + } +]; diff --git a/frontend/providers/applaunchpad/src/mock/apps.ts b/frontend/providers/applaunchpad/src/mock/apps.ts index 8aa13128006..a9854306371 100644 --- a/frontend/providers/applaunchpad/src/mock/apps.ts +++ b/frontend/providers/applaunchpad/src/mock/apps.ts @@ -11,8 +11,16 @@ export const MOCK_APPS: AppListItemType[] = [ createTime: 'string', cpu: 100, memory: 100, - usedCpu: new Array(30).fill(0), - useMemory: new Array(30).fill(0), + 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') + }, activeReplicas: 1, isPause: false, maxReplicas: 1, @@ -26,9 +34,17 @@ export const MOCK_APPS: AppListItemType[] = [ createTime: 'string', cpu: 100, memory: 100, - usedCpu: new Array(30).fill(0), isPause: false, - useMemory: new Array(30).fill(0), + 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') + }, activeReplicas: 1, maxReplicas: 1, minReplicas: 1, @@ -42,8 +58,16 @@ export const MOCK_APPS: AppListItemType[] = [ isPause: false, cpu: 100, memory: 100, - usedCpu: new Array(30).fill(0), - useMemory: new Array(30).fill(0), + 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') + }, activeReplicas: 1, maxReplicas: 1, minReplicas: 1, @@ -158,8 +182,16 @@ export const MOCK_PODS: PodDetailType[] = [ restarts: 10, age: '22', status: podStatusMap.running, - usedCpu: new Array(30).fill(0), - usedMemory: new Array(30).fill(0), + 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') + }, cpu: 0, memory: 0 }, @@ -170,8 +202,16 @@ export const MOCK_PODS: PodDetailType[] = [ restarts: 10, age: '22', status: podStatusMap.running, - usedCpu: new Array(30).fill(0), - usedMemory: new Array(30).fill(0), + 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') + }, cpu: 0, memory: 0 }, @@ -182,8 +222,16 @@ export const MOCK_PODS: PodDetailType[] = [ restarts: 10, age: '22', status: podStatusMap.running, - usedCpu: new Array(30).fill(0), - usedMemory: new Array(30).fill(0), + 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') + }, cpu: 0, memory: 0 }, @@ -195,8 +243,16 @@ export const MOCK_PODS: PodDetailType[] = [ restarts: 10, age: '22', status: podStatusMap.running, - usedCpu: new Array(30).fill(0), - usedMemory: new Array(30).fill(0), + 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') + }, cpu: 0, memory: 0 }, @@ -209,8 +265,16 @@ export const MOCK_PODS: PodDetailType[] = [ restarts: 10, age: '22', status: podStatusMap.running, - usedCpu: new Array(30).fill(0), - usedMemory: new Array(30).fill(0), + 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') + }, cpu: 0, memory: 0 }, @@ -222,8 +286,16 @@ export const MOCK_PODS: PodDetailType[] = [ restarts: 10, age: '22', status: podStatusMap.running, - usedCpu: new Array(30).fill(0), - usedMemory: new Array(30).fill(0), + 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') + }, cpu: 0, memory: 0 }, @@ -235,8 +307,16 @@ export const MOCK_PODS: PodDetailType[] = [ restarts: 10, age: '22', status: podStatusMap.running, - usedCpu: new Array(30).fill(0), - usedMemory: new Array(30).fill(0), + 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') + }, cpu: 0, memory: 0 }, @@ -248,8 +328,16 @@ export const MOCK_PODS: PodDetailType[] = [ restarts: 10, age: '22', status: podStatusMap.running, - usedCpu: new Array(30).fill(0), - usedMemory: new Array(30).fill(0), + 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') + }, cpu: 0, memory: 0 }, @@ -262,8 +350,16 @@ export const MOCK_PODS: PodDetailType[] = [ restarts: 10, age: '22', status: podStatusMap.running, - usedCpu: new Array(30).fill(0), - usedMemory: new Array(30).fill(0), + 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') + }, cpu: 0, memory: 0 }, @@ -276,8 +372,16 @@ export const MOCK_PODS: PodDetailType[] = [ restarts: 10, age: '22', status: podStatusMap.running, - usedCpu: new Array(30).fill(0), - usedMemory: new Array(30).fill(0), + 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') + }, cpu: 0, memory: 0 } @@ -296,8 +400,16 @@ export const MOCK_APP_DETAIL: AppDetailType = { replicas: 5, cpu: 0, memory: 0, - usedCpu: new Array(30).fill(0), - usedMemory: new Array(30).fill(0), + 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: [ { networkName: '', diff --git a/frontend/providers/applaunchpad/src/pages/api/guide/getAccount.ts b/frontend/providers/applaunchpad/src/pages/api/guide/getAccount.ts index 4fc2b993322..245adbb8653 100644 --- a/frontend/providers/applaunchpad/src/pages/api/guide/getAccount.ts +++ b/frontend/providers/applaunchpad/src/pages/api/guide/getAccount.ts @@ -5,6 +5,7 @@ import type { NextApiRequest, NextApiResponse } from 'next'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { + if (process.env.GUIDE_ENABLED !== 'true') return jsonRes(res, { data: null }); const kubeconfig = await authSession(req.headers); const domain = process.env.SEALOS_DOMAIN; console.log(`https://${domain}/api/v1alpha/account/getAccount`); diff --git a/frontend/providers/applaunchpad/src/pages/api/guide/getBonus.ts b/frontend/providers/applaunchpad/src/pages/api/guide/getBonus.ts index fefa154be23..d6e41d8fa84 100644 --- a/frontend/providers/applaunchpad/src/pages/api/guide/getBonus.ts +++ b/frontend/providers/applaunchpad/src/pages/api/guide/getBonus.ts @@ -6,6 +6,7 @@ import type { NextApiRequest, NextApiResponse } from 'next'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { + if (process.env.GUIDE_ENABLED !== 'true') return jsonRes(res, { data: null }); const { k8sCore, namespace } = await getK8s({ kubeconfig: await authSession(req.headers) }); diff --git a/frontend/providers/applaunchpad/src/pages/api/guide/updateGuide.ts b/frontend/providers/applaunchpad/src/pages/api/guide/updateGuide.ts index c95135fb73c..40ffb80f3f6 100644 --- a/frontend/providers/applaunchpad/src/pages/api/guide/updateGuide.ts +++ b/frontend/providers/applaunchpad/src/pages/api/guide/updateGuide.ts @@ -12,6 +12,7 @@ export type UpdateUserGuideParams = { export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { + if (process.env.GUIDE_ENABLED !== 'true') return jsonRes(res, { data: null }); const { activityType, phase, phasePage, shouldSendGift } = req.body as UpdateUserGuideParams; if (!activityType || !phase || !phasePage) diff --git a/frontend/providers/applaunchpad/src/pages/api/monitor/getMonitorData.ts b/frontend/providers/applaunchpad/src/pages/api/monitor/getMonitorData.ts new file mode 100644 index 00000000000..85fd1556224 --- /dev/null +++ b/frontend/providers/applaunchpad/src/pages/api/monitor/getMonitorData.ts @@ -0,0 +1,129 @@ +import { authSession } from '@/services/backend/auth'; +import { getK8s } from '@/services/backend/kubernetes'; +import { jsonRes } from '@/services/backend/response'; +import { ApiResp } from '@/services/kubernet'; +import { monitorFetch } from '@/services/monitorFetch'; +import { MonitorServiceResult, MonitorDataResult, MonitorQueryKey } from '@/types/monitor'; +import type { NextApiRequest, NextApiResponse } from 'next'; + +const AdapterChartData: Record< + keyof MonitorQueryKey, + (data: MonitorServiceResult) => MonitorDataResult[] +> = { + disk: (data: MonitorServiceResult) => { + const newDataArray = data.data.result.map((item) => { + let name = item.metric.pod; + let xData = item.values.map((value) => value[0]); + let yData = item.values.map((value) => (parseFloat(value[1]) * 100).toFixed(2)); + return { + name: name, + xData: xData, + yData: yData + }; + }); + return newDataArray; + }, + cpu: (data: MonitorServiceResult) => { + const newDataArray = data.data.result.map((item) => { + let name = item.metric.pod; + let xData = item.values.map((value) => value[0]); + let yData = item.values.map((value) => (parseFloat(value[1]) * 100).toFixed(2)); + return { + name: name, + xData: xData, + yData: yData + }; + }); + return newDataArray; + }, + memory: (data: MonitorServiceResult) => { + const newDataArray = data.data.result.map((item) => { + let name = item.metric.pod; + let xData = item.values.map((value) => value[0]); + let yData = item.values.map((value) => (parseFloat(value[1]) * 100).toFixed(2)); + return { + name: name, + xData: xData, + yData: yData + }; + }); + return newDataArray; + }, + average_cpu: (data: MonitorServiceResult) => { + const newDataArray = data.data.result.map((item) => { + let name = item.metric.pod; + let xData = item.values.map((value) => value[0]); + let yData = item.values.map((value) => (parseFloat(value[1]) * 100).toFixed(2)); + return { + name: name, + xData: xData, + yData: yData + }; + }); + return newDataArray; + }, + average_memory: (data: MonitorServiceResult) => { + const newDataArray = data.data.result.map((item) => { + let name = item.metric.pod; + let xData = item.values.map((value) => value[0]); + let yData = item.values.map((value) => (parseFloat(value[1]) * 100).toFixed(2)); + return { + name: name, + xData: xData, + yData: yData + }; + }); + return newDataArray; + } +}; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + const kubeconfig = await authSession(req.headers); + const { namespace, kc } = await getK8s({ + kubeconfig: kubeconfig + }); + + const { queryName, queryKey, start, end, step = '1m' } = req.query; + + // One hour of monitoring data + const endTime = Date.now(); + const startTime = endTime - 60 * 60 * 1000; + + const params = { + type: queryKey, + launchPadName: queryName, + namespace: namespace, + start: startTime / 1000, + end: endTime / 1000, + step: step + }; + + console.log(params, 'getMonitorData'); + + const result: MonitorDataResult = await monitorFetch( + { + url: '/query', + params: params + }, + kubeconfig + ).then((res) => { + // console.log(res.data.result, res.data.result[0].values.length, 'AdapterChartData'); + // @ts-ignore + return AdapterChartData[queryKey] + ? // @ts-ignore + AdapterChartData[queryKey](res as MonitorDataResult) + : res; + }); + + jsonRes(res, { + code: 200, + data: result + }); + } catch (error) { + jsonRes(res, { + code: 500, + error: error + }); + } +} diff --git a/frontend/providers/applaunchpad/src/pages/app/detail/components/AppMainInfo.tsx b/frontend/providers/applaunchpad/src/pages/app/detail/components/AppMainInfo.tsx index 599f48337e1..f5eca89ed83 100644 --- a/frontend/providers/applaunchpad/src/pages/app/detail/components/AppMainInfo.tsx +++ b/frontend/providers/applaunchpad/src/pages/app/detail/components/AppMainInfo.tsx @@ -1,20 +1,21 @@ -import React, { useMemo } from 'react'; -import { Box, Flex, Grid, Link, Text } from '@chakra-ui/react'; -import type { AppDetailType } from '@/types/app'; -import PodLineChart from '@/components/PodLineChart'; -import { printMemory, useCopyData } from '@/utils/tools'; -import dayjs from 'dayjs'; -import { getUserNamespace } from '@/utils/user'; -import { SEALOS_DOMAIN, DOMAIN_PORT } from '@/store/static'; import MyIcon from '@/components/Icon'; +import MyTooltip from '@/components/MyTooltip'; +import PodLineChart from '@/components/PodLineChart'; +import { ProtocolList } from '@/constants/app'; import { MOCK_APP_DETAIL } from '@/mock/apps'; +import { DOMAIN_PORT, SEALOS_DOMAIN } from '@/store/static'; +import type { AppDetailType } from '@/types/app'; +import { useCopyData } from '@/utils/tools'; +import { getUserNamespace } from '@/utils/user'; +import { Box, Flex, Grid } from '@chakra-ui/react'; +import dayjs from 'dayjs'; import { useTranslation } from 'next-i18next'; -import { ProtocolList } from '@/constants/app'; -import MyTooltip from '@/components/MyTooltip'; +import { useMemo } from 'react'; const AppMainInfo = ({ app = MOCK_APP_DETAIL }: { app: AppDetailType }) => { const { t } = useTranslation(); const { copyData } = useCopyData(); + const networks = useMemo( () => app.networks.map((network) => ({ @@ -30,12 +31,6 @@ const AppMainInfo = ({ app = MOCK_APP_DETAIL }: { app: AppDetailType }) => { [app] ); - const cpuUsed = useMemo( - () => `${((app.usedCpu[app.usedCpu.length - 1] / app.cpu) * 100).toFixed(2)}%`, - [app] - ); - const memoryUsed = useMemo(() => printMemory(app.usedMemory[app.usedMemory.length - 1]), [app]); - return ( <> @@ -46,7 +41,7 @@ const AppMainInfo = ({ app = MOCK_APP_DETAIL }: { app: AppDetailType }) => { ({t('Update Time')}  - {dayjs().format('HH:mm:ss')}) + {dayjs().format('HH:mm')}) { > - CPU ({cpuUsed}) + CPU ({app.usedCpu.yData[app.usedCpu.yData.length - 1]}%) - + - {t('Memory')} ({memoryUsed}) + {t('Memory')} ({app.usedMemory.yData[app.usedMemory.yData.length - 1]}%) - + diff --git a/frontend/providers/applaunchpad/src/pages/app/detail/components/PodDetailModal.tsx b/frontend/providers/applaunchpad/src/pages/app/detail/components/PodDetailModal.tsx index 488a8ebf8a0..cb4995c0792 100644 --- a/frontend/providers/applaunchpad/src/pages/app/detail/components/PodDetailModal.tsx +++ b/frontend/providers/applaunchpad/src/pages/app/detail/components/PodDetailModal.tsx @@ -188,21 +188,17 @@ const Logs = ({ - - CPU ({((pod.usedCpu[pod.usedCpu.length - 1] / pod.cpu) * 100).toFixed(2)}%) - + CPU ({pod.usedCpu.yData[pod.usedCpu.yData.length - 1]}%) - + - {t('Memory')} ( - {((pod.usedMemory[pod.usedMemory.length - 1] / pod.memory) * 100).toFixed(2)} - %) + {t('Memory')} ({pod.usedMemory.yData[pod.usedMemory.yData.length - 1]}%) - + diff --git a/frontend/providers/applaunchpad/src/pages/app/detail/components/Pods.tsx b/frontend/providers/applaunchpad/src/pages/app/detail/components/Pods.tsx index f5b27560739..8d0b3577255 100644 --- a/frontend/providers/applaunchpad/src/pages/app/detail/components/Pods.tsx +++ b/frontend/providers/applaunchpad/src/pages/app/detail/components/Pods.tsx @@ -1,33 +1,29 @@ -import React, { useState, useCallback } from 'react'; +import { restartPodByName } from '@/api/app'; +import MyIcon from '@/components/Icon'; +import MyTooltip from '@/components/MyTooltip'; +import PodLineChart from '@/components/PodLineChart'; +import { PodStatusEnum } from '@/constants/app'; +import { useConfirm } from '@/hooks/useConfirm'; +import { useLoading } from '@/hooks/useLoading'; +import { useToast } from '@/hooks/useToast'; +import type { PodDetailType } from '@/types/app'; +import { QuestionOutlineIcon } from '@chakra-ui/icons'; import { Box, - Button, + Center, + Flex, Table, - Thead, + TableContainer, Tbody, - Tr, - Th, Td, - TableContainer, - Flex, - MenuButton, - Tooltip, - Center + Th, + Thead, + Tr } from '@chakra-ui/react'; -import { sealosApp } from 'sealos-desktop-sdk/app'; -import { restartPodByName } from '@/api/app'; -import type { PodDetailType } from '@/types/app'; -import { useLoading } from '@/hooks/useLoading'; -import { useToast } from '@/hooks/useToast'; -import PodLineChart from '@/components/PodLineChart'; -import dynamic from 'next/dynamic'; -import MyIcon from '@/components/Icon'; -import { PodStatusEnum } from '@/constants/app'; -import { useConfirm } from '@/hooks/useConfirm'; -import MyMenu from '@/components/Menu'; import { useTranslation } from 'next-i18next'; -import { QuestionOutlineIcon } from '@chakra-ui/icons'; -import MyTooltip from '@/components/MyTooltip'; +import dynamic from 'next/dynamic'; +import React, { useCallback, useState } from 'react'; +import { sealosApp } from 'sealos-desktop-sdk/app'; const LogsModal = dynamic(() => import('./LogsModal')); const DetailModel = dynamic(() => import('./PodDetailModal')); @@ -120,7 +116,7 @@ const Pods = ({ key: 'cpu', render: (item: PodDetailType) => ( - + ) }, @@ -129,7 +125,7 @@ const Pods = ({ key: 'memory', render: (item: PodDetailType) => ( - + ) }, diff --git a/frontend/providers/applaunchpad/src/pages/app/detail/index.tsx b/frontend/providers/applaunchpad/src/pages/app/detail/index.tsx index 0337b9d02ba..c952e7c27b9 100644 --- a/frontend/providers/applaunchpad/src/pages/app/detail/index.tsx +++ b/frontend/providers/applaunchpad/src/pages/app/detail/index.tsx @@ -26,8 +26,10 @@ const AppDetail = ({ appName }: { appName: string }) => { appDetail = MOCK_APP_DETAIL, setAppDetail, appDetailPods, - intervalLoadPods + intervalLoadPods, + loadDetailMonitorData } = useAppStore(); + const [podsLoaded, setPodsLoaded] = useState(false); const [showSlider, setShowSlider] = useState(false); @@ -40,7 +42,6 @@ const AppDetail = ({ appName }: { appName: string }) => { } }); - // interval get pods metrics useQuery( ['app-detail-pod'], () => { @@ -56,6 +57,18 @@ const AppDetail = ({ appName }: { appName: string }) => { } ); + useQuery( + ['loadDetailMonitorData', appName, appDetail?.isPause], + () => { + if (appDetail?.isPause) return null; + return loadDetailMonitorData(appName); + }, + { + refetchOnMount: true, + refetchInterval: 2 * 60 * 1000 + } + ); + return ( diff --git a/frontend/providers/applaunchpad/src/pages/app/edit/index.tsx b/frontend/providers/applaunchpad/src/pages/app/edit/index.tsx index 400b9c3d194..b3196e2d72b 100644 --- a/frontend/providers/applaunchpad/src/pages/app/edit/index.tsx +++ b/frontend/providers/applaunchpad/src/pages/app/edit/index.tsx @@ -137,7 +137,7 @@ const EditApp = ({ appName, tabType }: { appName?: string; tabType: string }) => const { refetch: refetchPrice } = useQuery(['init-price'], loadUserSourcePrice, { enabled: !!userSourcePrice?.gpu, - refetchInterval: 5000 + refetchInterval: 6000 }); // add already deployment gpu amount if they exists diff --git a/frontend/providers/applaunchpad/src/pages/apps/components/appList.tsx b/frontend/providers/applaunchpad/src/pages/apps/components/appList.tsx index 42e84e27f30..f2b7dfddacc 100644 --- a/frontend/providers/applaunchpad/src/pages/apps/components/appList.tsx +++ b/frontend/providers/applaunchpad/src/pages/apps/components/appList.tsx @@ -1,24 +1,22 @@ -import React, { useCallback, useMemo, useState } from 'react'; -import { useRouter } from 'next/router'; -import { Box, Button, Flex, MenuButton } from '@chakra-ui/react'; -import { AppListItemType } from '@/types/app'; -import PodLineChart from '@/components/PodLineChart'; +import { pauseAppByName, restartAppByName, startAppByName } from '@/api/app'; import AppStatusTag from '@/components/AppStatusTag'; +import GPUItem from '@/components/GPUItem'; import MyIcon from '@/components/Icon'; -import { useTheme } from '@chakra-ui/react'; -import { useGlobalStore } from '@/store/global'; -import { useToast } from '@/hooks/useToast'; -import { restartAppByName, pauseAppByName, startAppByName } from '@/api/app'; +import MyMenu from '@/components/Menu'; +import PodLineChart from '@/components/PodLineChart'; +import MyTable from '@/components/Table'; import { useConfirm } from '@/hooks/useConfirm'; -import { useTranslation } from 'next-i18next'; +import { useToast } from '@/hooks/useToast'; +import { useGlobalStore } from '@/store/global'; import { useUserStore } from '@/store/user'; - +import { AppListItemType } from '@/types/app'; +import { getErrText } from '@/utils/tools'; +import { Box, Button, Flex, MenuButton, useTheme } from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; import dynamic from 'next/dynamic'; +import { useRouter } from 'next/router'; +import React, { useCallback, useMemo, useState } from 'react'; -import MyMenu from '@/components/Menu'; -import MyTable from '@/components/Table'; -import GPUItem from '@/components/GPUItem'; -import { getErrText } from '@/utils/tools'; const DelModal = dynamic(() => import('@/pages/app/detail/components/DelModal')); const AppList = ({ @@ -34,6 +32,7 @@ const AppList = ({ const { toast } = useToast(); const theme = useTheme(); const router = useRouter(); + // console.log(apps, 'apps'); const [delAppName, setDelAppName] = useState(''); const { openConfirm: onOpenPause, ConfirmChild: PauseChild } = useConfirm({ @@ -142,7 +141,7 @@ const AppList = ({ key: 'cpu', render: (item: AppListItemType) => ( - + ) }, @@ -151,7 +150,7 @@ const AppList = ({ key: 'storage', render: (item: AppListItemType) => ( - + ) }, @@ -304,7 +303,9 @@ const AppList = ({ {t('Create Application')} + + {!!delAppName && ( setDelAppName('')} onSuccess={refetchApps} /> diff --git a/frontend/providers/applaunchpad/src/pages/apps/index.tsx b/frontend/providers/applaunchpad/src/pages/apps/index.tsx index 4af3149d8bc..1fa79452138 100644 --- a/frontend/providers/applaunchpad/src/pages/apps/index.tsx +++ b/frontend/providers/applaunchpad/src/pages/apps/index.tsx @@ -12,7 +12,7 @@ import Empty from './components/empty'; const Home = () => { const router = useRouter(); - const { appList, setAppList, intervalLoadPods } = useAppStore(); + const { appList, setAppList, intervalLoadPods, loadAvgMonitorData } = useAppStore(); const { Loading } = useLoading(); const [refresh, setFresh] = useState(false); const list = useRef(appList); @@ -26,12 +26,16 @@ const Home = () => { [appList] ); - const { isLoading, refetch } = useQuery(['appListQuery'], () => setAppList(false), { - onSettled(res) { - if (!res) return; - refreshList(res); + const { isLoading, refetch: refetchAppList } = useQuery( + ['appListQuery'], + () => setAppList(false), + { + onSettled(res) { + if (!res) return; + refreshList(res); + } } - }); + ); const requestController = useRef(new RequestController()); @@ -77,6 +81,40 @@ const Home = () => { } ); + const { refetch: refetchAvgMonitorData } = useQuery( + ['loadAvgMonitorData', appList.length], + () => { + const doms = document.querySelectorAll(`.appItem`); + const viewportDomIds = Array.from(doms) + .filter((item) => isElementInViewport(item)) + .map((item) => item.getAttribute('data-id')); + + const viewportApps = + viewportDomIds.length < 3 + ? appList + : appList.filter((app) => viewportDomIds.includes(app.id)); + + return requestController.current.runTasks({ + tasks: viewportApps + .filter((app) => !app.isPause) + .map((app) => { + return () => loadAvgMonitorData(app.name); + }), + limit: 3 + }); + }, + { + refetchOnMount: true, + refetchInterval: 2 * 60 * 1000, + onError(err) { + console.log(err); + }, + onSettled() { + refreshList(); + } + } + ); + useEffect(() => { router.prefetch('/app/detail'); router.prefetch('/app/edit'); @@ -91,7 +129,13 @@ const Home = () => { {appList.length === 0 && !isLoading ? ( ) : ( - + { + refetchAppList(); + refetchAvgMonitorData(); + }} + /> )} diff --git a/frontend/providers/applaunchpad/src/services/monitorFetch.ts b/frontend/providers/applaunchpad/src/services/monitorFetch.ts new file mode 100644 index 00000000000..044392139e2 --- /dev/null +++ b/frontend/providers/applaunchpad/src/services/monitorFetch.ts @@ -0,0 +1,17 @@ +import { AxiosRequestConfig } from 'axios'; + +export const monitorFetch = async (props: AxiosRequestConfig, kubeconfig: string) => { + const { url, params } = props; + const queryString = new URLSearchParams(params).toString(); + const requestOptions = { + method: 'GET', + headers: { + Authorization: encodeURIComponent(kubeconfig) + } + }; + const doMain = process.env.MONITOR_URL || 'http://monitor-system.cloud.sealos.run'; + const response = await fetch(`${doMain}${url}?${queryString}`, requestOptions).then((res) => + res.json() + ); + return response; +}; diff --git a/frontend/providers/applaunchpad/src/store/app.ts b/frontend/providers/applaunchpad/src/store/app.ts index d738e2bfb36..e8e4ea1ee13 100644 --- a/frontend/providers/applaunchpad/src/store/app.ts +++ b/frontend/providers/applaunchpad/src/store/app.ts @@ -1,10 +1,10 @@ +import { getAppByName, getAppMonitorData, getAppPodsByAppName, getMyApps } from '@/api/app'; +import { PodStatusEnum, appStatusMap } from '@/constants/app'; +import { MOCK_APP_DETAIL } from '@/mock/apps'; +import type { AppDetailType, AppListItemType, PodDetailType } from '@/types/app'; import { create } from 'zustand'; import { devtools } from 'zustand/middleware'; import { immer } from 'zustand/middleware/immer'; -import type { AppListItemType, AppDetailType, PodDetailType } from '@/types/app'; -import { getMyApps, getAppPodsByAppName, getAppByName, getPodsMetrics } from '@/api/app'; -import { appStatusMap, PodStatusEnum } from '@/constants/app'; -import { MOCK_APP_DETAIL } from '@/mock/apps'; type State = { appList: AppListItemType[]; @@ -13,34 +13,22 @@ type State = { appDetailPods: PodDetailType[]; setAppDetail: (appName: string) => Promise; intervalLoadPods: (appName: string, updateDetail: boolean) => Promise; + loadAvgMonitorData: (appName: string) => Promise; + loadDetailMonitorData: (appName: string) => Promise; }; export const useAppStore = create()( devtools( immer((set, get) => ({ - appList: [], + appList: [] as AppListItemType[], appDetail: MOCK_APP_DETAIL, - appDetailPods: [], + appDetailPods: [] as PodDetailType[], setAppList: async (init = false) => { const res = await getMyApps(); - - const storeList = res.map((item) => { - const store = get().appList.find((app) => app.name === item.name); - - if (!store || init) return item; - - return { - ...item, - status: store.status, - usedCpu: store.usedCpu, - useMemory: store.useMemory - }; - }); - set((state) => { - state.appList = storeList; + state.appList = res; }); - return storeList; + return res; }, setAppDetail: async (appName: string) => { set((state) => { @@ -56,9 +44,9 @@ export const useAppStore = create()( }); return res; }, + // updata applist appdetail status intervalLoadPods: async (appName, updateDetail) => { if (!appName) return Promise.reject('app name is empty'); - // get pod and update const pods = await getAppPodsByAppName(appName); // one pod running, app is running @@ -68,13 +56,10 @@ export const useAppStore = create()( : appStatusMap.creating; set((state) => { - // update app detail if (state?.appDetail?.appName === appName && updateDetail) { state.appDetail.status = appStatus; - // update pods info except cpu and memory state.appDetailPods = pods.map((pod) => { const oldPod = state.appDetailPods.find((item) => item.podName === pod.podName); - return { ...pod, usedCpu: oldPod ? oldPod.usedCpu : pod.usedCpu, @@ -87,46 +72,68 @@ export const useAppStore = create()( status: item.name === appName ? appStatus : item.status })); }); - - // ============================================ - - // get metrics and update - const metrics = await getPodsMetrics(pods.map((pod) => pod.podName)); + return 'success'; + }, + loadAvgMonitorData: async (appName) => { + const [averageCpu, averageMemory] = await Promise.all([ + getAppMonitorData({ + queryKey: 'average_cpu', + queryName: appName, + step: '2m' + }), + getAppMonitorData({ + queryKey: 'average_memory', + queryName: appName, + step: '2m' + }) + ]); set((state) => { - const aveCpu = Number( - metrics.reduce((sum, item) => sum + item.cpu / metrics.length, 0).toFixed(4) - ); - const aveMemory = Number( - metrics.reduce((sum, item) => sum + item.memory / metrics.length, 0).toFixed(4) - ); - - // update detailApp average cpu and memory - if (state?.appDetail?.appName === appName && updateDetail) { - state.appDetail.usedCpu = [...state.appDetail.usedCpu.slice(1), aveCpu]; - state.appDetail.usedMemory = [...state.appDetail.usedMemory.slice(1), aveMemory]; - - // update pod cpu and memory - state.appDetailPods = state.appDetailPods.map((pod) => { - const currentCpu = metrics.find((item) => item.podName === pod.podName)?.cpu || 0; - const currentMemory = - metrics.find((item) => item.podName === pod.podName)?.memory || 0; - - return { - ...pod, - usedCpu: [...pod.usedCpu.slice(1), currentCpu], - usedMemory: [...pod.usedMemory.slice(1), currentMemory] - }; - }); - } - - // update appList state.appList = state.appList.map((item) => ({ ...item, - usedCpu: item.name === appName ? [...item.usedCpu.slice(1), aveCpu] : item.usedCpu, - useMemory: - item.name === appName ? [...item.useMemory.slice(1), aveMemory] : item.useMemory + usedCpu: item.name === appName && averageCpu[0] ? averageCpu[0] : item.usedCpu, + usedMemory: + item.name === appName && averageMemory[0] ? averageMemory[0] : item.usedMemory })); }); + }, + loadDetailMonitorData: async (appName) => { + const pods = await getAppPodsByAppName(appName); + set((state) => { + state.appDetailPods = pods.map((pod) => { + const oldPod = state.appDetailPods.find((item) => item.podName === pod.podName); + return { + ...pod, + usedCpu: oldPod ? oldPod.usedCpu : pod.usedCpu, + usedMemory: oldPod ? oldPod.usedMemory : pod.usedMemory + }; + }); + }); + + const [cpuData, memoryData, averageCpuData, averageMemoryData] = await Promise.all([ + getAppMonitorData({ queryKey: 'cpu', queryName: appName, step: '2m' }), + getAppMonitorData({ queryKey: 'memory', queryName: appName, step: '2m' }), + getAppMonitorData({ queryKey: 'average_cpu', queryName: appName, step: '2m' }), + getAppMonitorData({ queryKey: 'average_memory', queryName: appName, step: '2m' }) + ]); + set((state) => { + if (state?.appDetail?.appName === appName && state.appDetail?.isPause !== true) { + state.appDetail.usedCpu = averageCpuData[0] + ? averageCpuData[0] + : { xData: new Array(30).fill(0), yData: new Array(30).fill('0'), name: '' }; + state.appDetail.usedMemory = averageMemoryData[0] + ? averageMemoryData[0] + : { xData: new Array(30).fill(0), yData: new Array(30).fill('0'), name: '' }; + } + state.appDetailPods = 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/applaunchpad/src/types/app.d.ts b/frontend/providers/applaunchpad/src/types/app.d.ts index 33e4d2e6571..8a9f824fe7c 100644 --- a/frontend/providers/applaunchpad/src/types/app.d.ts +++ b/frontend/providers/applaunchpad/src/types/app.d.ts @@ -11,6 +11,7 @@ import type { SinglePodMetrics, V1StatefulSet } from '@kubernetes/client-node'; +import { MonitorDataResult } from './monitor'; export type HpaTarget = 'cpu' | 'memory'; @@ -48,8 +49,8 @@ export interface AppListItemType { cpu: number; memory: number; gpu?: GpuType; - usedCpu: number[]; - useMemory: number[]; + usedCpu: MonitorDataResult; + usedMemory: MonitorDataResult; // average value activeReplicas: number; minReplicas: number; maxReplicas: number; @@ -109,8 +110,8 @@ export interface AppDetailType extends AppEditType { status: AppStatusMapType; isPause: boolean; imageName: string; - usedCpu: number[]; - usedMemory: number[]; + usedCpu: MonitorDataResult; + usedMemory: MonitorDataResult; crYamlList: DeployKindsType[]; // pods: PodDetailType[]; @@ -130,8 +131,8 @@ export interface PodDetailType extends V1Pod { ip: string; restarts: number; age: string; - usedCpu: number[]; - usedMemory: number[]; + usedCpu: MonitorDataResult; + usedMemory: MonitorDataResult; cpu: number; memory: number; podReason?: string; diff --git a/frontend/providers/applaunchpad/src/types/monitor.d.ts b/frontend/providers/applaunchpad/src/types/monitor.d.ts new file mode 100644 index 00000000000..658b991ccf5 --- /dev/null +++ b/frontend/providers/applaunchpad/src/types/monitor.d.ts @@ -0,0 +1,48 @@ +import { BoxProps } from '@chakra-ui/react'; + +export interface MonitorServiceResult { + status: string; + data: { + resultType: string; + result: { + metric: { + app_kubernetes_io_instance: string; + app_kubernetes_io_managed_by: string; + app_kubernetes_io_name: string; + apps_kubeblocks_io_component_name: string; + datname: string; + instance: string; + job: string; + namespace: string; + node: string; + pod: string; + server: string; + service: string; + __name__: string; + state?: string; + command?: string; + database?: string; + db: string; + type?: string; + cmd?: string; + persistentvolumeclaim?: string; + }; + value: [number, string]; + values: [[number, string]]; + }[]; + }; +} + +export type MonitorQueryKey = { + cpu: string; + memory: string; + disk: string; + average_memory: string; + average_cpu: string; +}; + +export type MonitorDataResult = { + name: string; + xData: number[]; + yData: string[]; +}; diff --git a/frontend/providers/applaunchpad/src/utils/adapt.ts b/frontend/providers/applaunchpad/src/utils/adapt.ts index 5e61a25b4b2..1475c74998c 100644 --- a/frontend/providers/applaunchpad/src/utils/adapt.ts +++ b/frontend/providers/applaunchpad/src/utils/adapt.ts @@ -73,8 +73,16 @@ export const adaptAppListItem = (app: V1Deployment & V1StatefulSet): AppListItem ), manufacturers: 'nvidia' }, - usedCpu: new Array(30).fill(0), - useMemory: new Array(30).fill(0), + 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') + }, activeReplicas: app.status?.readyReplicas || 0, maxReplicas: +(app.metadata?.annotations?.[maxReplicasKey] || app.status?.readyReplicas || 0), minReplicas: +(app.metadata?.annotations?.[minReplicasKey] || app.status?.readyReplicas || 0), @@ -111,8 +119,16 @@ export const adaptPod = (pod: V1Pod): PodDetailType => { ip: pod.status?.podIP || 'pod ip', restarts: pod.status?.containerStatuses ? pod.status?.containerStatuses[0].restartCount : 0, age: formatPodTime(pod.metadata?.creationTimestamp), - usedCpu: new Array(30).fill(0), - usedMemory: new Array(30).fill(0), + 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') + }, cpu: cpuFormatToM(pod.spec?.containers?.[0]?.resources?.limits?.cpu || '0'), memory: memoryFormatToMi(pod.spec?.containers?.[0]?.resources?.limits?.memory || '0') }; @@ -217,8 +233,16 @@ export const adaptAppDetail = (configs: DeployKindsType[]): AppDetailType => { ), manufacturers: 'nvidia' }, - usedCpu: new Array(30).fill(0), - usedMemory: new Array(30).fill(0), + 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') + }, envs: appDeploy.spec?.template?.spec?.containers?.[0]?.env?.map((env) => { return { diff --git a/frontend/providers/applaunchpad/src/utils/deployYaml2Json.ts b/frontend/providers/applaunchpad/src/utils/deployYaml2Json.ts index 5cd1a61f051..3003c68a0b8 100644 --- a/frontend/providers/applaunchpad/src/utils/deployYaml2Json.ts +++ b/frontend/providers/applaunchpad/src/utils/deployYaml2Json.ts @@ -241,6 +241,142 @@ export const json2Service = (data: AppEditType) => { return yaml.dump(template); }; +export const json2NetWorkByType = (type: 'ingress' | 'gateway', data: AppEditType) => { + // different protocol annotations + const map = { + HTTP: { + 'nginx.ingress.kubernetes.io/ssl-redirect': 'false', + 'nginx.ingress.kubernetes.io/backend-protocol': 'HTTP', + 'nginx.ingress.kubernetes.io/client-body-buffer-size': '64k', + 'nginx.ingress.kubernetes.io/proxy-buffer-size': '64k', + 'nginx.ingress.kubernetes.io/proxy-send-timeout': '300', + 'nginx.ingress.kubernetes.io/proxy-read-timeout': '300', + 'nginx.ingress.kubernetes.io/server-snippet': + 'client_header_buffer_size 64k;\nlarge_client_header_buffers 4 128k;\n' + }, + GRPC: { + 'nginx.ingress.kubernetes.io/ssl-redirect': 'false', + 'nginx.ingress.kubernetes.io/backend-protocol': 'GRPC' + }, + WS: { + 'nginx.ingress.kubernetes.io/proxy-read-timeout': '3600', + 'nginx.ingress.kubernetes.io/proxy-send-timeout': '3600', + 'nginx.ingress.kubernetes.io/backend-protocol': 'WS' + } + }; + + const result = data.networks + .filter((item) => item.openPublicDomain) + .map((network, i) => { + const host = network.customDomain + ? network.customDomain + : `${network.publicDomain}.${SEALOS_DOMAIN}`; + + const secretName = network.customDomain ? network.networkName : INGRESS_SECRET; + + const ingress = { + apiVersion: 'networking.k8s.io/v1', + kind: 'Ingress', + metadata: { + name: network.networkName, + labels: { + [appDeployKey]: data.appName, + [publicDomainKey]: network.publicDomain + }, + annotations: { + 'kubernetes.io/ingress.class': 'nginx', + 'nginx.ingress.kubernetes.io/proxy-body-size': '32m', + ...map[network.protocol] + } + }, + spec: { + rules: [ + { + host, + http: { + paths: [ + { + pathType: 'Prefix', + path: '/', + backend: { + service: { + name: data.appName, + port: { + number: network.port + } + } + } + } + ] + } + } + ], + tls: [ + { + hosts: [host], + secretName + } + ] + } + }; + const issuer = { + apiVersion: 'cert-manager.io/v1', + kind: 'Issuer', + metadata: { + name: network.networkName, + labels: { + [appDeployKey]: data.appName + } + }, + spec: { + acme: { + server: 'https://acme-v02.api.letsencrypt.org/directory', + email: 'admin@sealos.io', + privateKeySecretRef: { + name: 'letsencrypt-prod' + }, + solvers: [ + { + http01: { + ingress: { + class: 'nginx', + serviceType: 'ClusterIP' + } + } + } + ] + } + } + }; + const certificate = { + apiVersion: 'cert-manager.io/v1', + kind: 'Certificate', + metadata: { + name: network.networkName, + labels: { + [appDeployKey]: data.appName + } + }, + spec: { + secretName, + dnsNames: [network.customDomain], + issuerRef: { + name: network.networkName, + kind: 'Issuer' + } + } + }; + + let resYaml = yaml.dump(ingress); + if (network.customDomain) { + resYaml += `\n---\n${yaml.dump(issuer)}\n---\n${yaml.dump(certificate)}`; + } + return resYaml; + }); + + return result.join('\n---\n'); +}; + export const json2Ingress = (data: AppEditType) => { // different protocol annotations const map = { diff --git a/frontend/providers/applaunchpad/src/utils/tools.ts b/frontend/providers/applaunchpad/src/utils/tools.ts index 4c7dd3d567d..671f1f03150 100644 --- a/frontend/providers/applaunchpad/src/utils/tools.ts +++ b/frontend/providers/applaunchpad/src/utils/tools.ts @@ -233,6 +233,8 @@ export const patchYamlList = ({ newYamlList: string[]; crYamlList: DeployKindsType[]; }) => { + console.log(formOldYamlList, newYamlList, crYamlList, '======='); + const oldFormJsonList = formOldYamlList .map((item) => yaml.loadAll(item)) .flat() as DeployKindsType[]; @@ -378,7 +380,7 @@ export const patchYamlList = ({ }); } }); - + console.log(actions, 'actions'); return actions; }; @@ -444,3 +446,19 @@ export const getErrText = (err: any, def = '') => { export const formatMoney = (mone: number) => { return mone / 1000000; }; + +// convertBytes 1024 +export const convertBytes = (bytes: number, unit: 'kb' | 'mb' | 'gb' | 'tb') => { + switch (unit.toLowerCase()) { + case 'kb': + return bytes / 1024; + case 'mb': + return bytes / Math.pow(1024, 2); + case 'gb': + return bytes / Math.pow(1024, 3); + case 'tb': + return bytes / Math.pow(1024, 4); + default: + return bytes; + } +}; diff --git a/frontend/providers/dbprovider/src/pages/db/detail/components/Monitor/ChartTemplate.tsx b/frontend/providers/dbprovider/src/pages/db/detail/components/Monitor/ChartTemplate.tsx index b7e92fbdf8e..003f5cf3a3a 100644 --- a/frontend/providers/dbprovider/src/pages/db/detail/components/Monitor/ChartTemplate.tsx +++ b/frontend/providers/dbprovider/src/pages/db/detail/components/Monitor/ChartTemplate.tsx @@ -1,13 +1,11 @@ import MyIcon from '@/components/Icon'; import MonitorChart from '@/components/MonitorChart'; +import { LineStyleMap } from '@/constants/monitor'; import { GET } from '@/services/request'; -import { DBDetailType } from '@/types/db'; -import { ChartTemplateProps, MonitorQueryKey } from '@/types/monitor'; +import { ChartTemplateProps } from '@/types/monitor'; import { Box, Flex, Text } from '@chakra-ui/react'; import { useQuery } from '@tanstack/react-query'; import { useTranslation } from 'next-i18next'; -import { useMemo } from 'react'; -import { LineStyleMap } from '@/constants/monitor'; const ChartTemplate = ({ db, diff --git a/frontend/providers/template/.env.template b/frontend/providers/template/.env.template index eb627d53e32..747aa277ba0 100644 --- a/frontend/providers/template/.env.template +++ b/frontend/providers/template/.env.template @@ -2,5 +2,6 @@ NEXT_PUBLIC_MOCK_USER= SEALOS_CLOUD_DOMAIN= SEALOS_CERT_SECRET_NAME= TEMPLATE_REPO_URL="https://github.com/labring-actions/templates" +TEMPLATE_REPO_BRANCH="main" # The CDN_URL environment variable is used to specify a CDN address; when set, it replaces raw.githubusercontent.com in the resource loading URL. If not set, the default address is used. CDN_URL= \ No newline at end of file diff --git a/frontend/providers/template/package.json b/frontend/providers/template/package.json index fca653b26bf..5178614a302 100644 --- a/frontend/providers/template/package.json +++ b/frontend/providers/template/package.json @@ -43,6 +43,7 @@ "nanoid": "^4.0.2", "next": "13.1.6", "next-i18next": "^13.3.0", + "node-cron": "^3.0.3", "nprogress": "^0.2.0", "octokit": "^3.1.1", "pluralize": "^8.0.0", diff --git a/frontend/providers/template/public/locales/en/common.json b/frontend/providers/template/public/locales/en/common.json index 2f446293f94..64fb18ebd10 100644 --- a/frontend/providers/template/public/locales/en/common.json +++ b/frontend/providers/template/public/locales/en/common.json @@ -187,5 +187,8 @@ "Edit": "Edit", "Edit App Name": "Edit App Name", "Installation Time": "Installation Time", - "users installed the app": "{{count}} users have installed the app" -} \ No newline at end of file + "users installed the app": "{{count}} users have installed the app", + "Please Enter": "Please Enter", + "Delete successful": "Delete successful", + "Delete Failed": "Delete Failed" +} diff --git a/frontend/providers/template/public/locales/zh/common.json b/frontend/providers/template/public/locales/zh/common.json index 952ca177013..0381fd82562 100644 --- a/frontend/providers/template/public/locales/zh/common.json +++ b/frontend/providers/template/public/locales/zh/common.json @@ -193,5 +193,8 @@ "Edit": "编辑", "Edit App Name": "编辑应用名称", "Installation Time": "安装时间", - "users installed the app": "已有 {{count}} 名用户安装应用" -} \ No newline at end of file + "users installed the app": "已有 {{count}} 名用户安装应用", + "Please Enter": "请输入", + "Delete successful": "删除成功", + "Delete Failed": "删除失败" +} diff --git a/frontend/providers/template/src/pages/api/listTemplate.ts b/frontend/providers/template/src/pages/api/listTemplate.ts index af9cd429508..bc4b1b94c38 100644 --- a/frontend/providers/template/src/pages/api/listTemplate.ts +++ b/frontend/providers/template/src/pages/api/listTemplate.ts @@ -5,6 +5,8 @@ import { parseGithubUrl } from '@/utils/tools'; import fs from 'fs'; import type { NextApiRequest, NextApiResponse } from 'next'; import path from 'path'; +const cron = require('node-cron'); +let hasAddCron = false; export function replaceRawWithCDN(url: string, cdnUrl: string) { let parsedUrl = parseGithubUrl(url); @@ -16,28 +18,44 @@ export function replaceRawWithCDN(url: string, cdnUrl: string) { return url; } +export const readTemplates = (jsonPath: string, cdnUrl?: string) => { + const jsonData = fs.readFileSync(jsonPath, 'utf8'); + const _templates: TemplateType[] = JSON.parse(jsonData); + const templates = _templates + .filter((item) => item?.spec?.draft !== true) + .map((item) => { + if (!!cdnUrl) { + item.spec.readme = replaceRawWithCDN(item.spec.readme, cdnUrl); + item.spec.icon = replaceRawWithCDN(item.spec.icon, cdnUrl); + } + return item; + }); + return templates; +}; + export default async function handler(req: NextApiRequest, res: NextApiResponse) { const originalPath = process.cwd(); const jsonPath = path.resolve(originalPath, 'templates.json'); const cdnUrl = process.env.CDN_URL; - try { - if (fs.existsSync(jsonPath)) { - const jsonData = fs.readFileSync(jsonPath, 'utf8'); - const _templates: TemplateType[] = JSON.parse(jsonData); + const baseurl = `http://${process.env.HOSTNAME || 'localhost'}:${process.env.PORT || 3000}`; - const templates = _templates - .filter((item) => item?.spec?.draft !== true) - .map((item) => { - if (!!cdnUrl) { - item.spec.readme = replaceRawWithCDN(item.spec.readme, cdnUrl); - item.spec.icon = replaceRawWithCDN(item.spec.icon, cdnUrl); - } - return item; - }); + try { + if (!hasAddCron) { + cron.schedule('*/5 * * * *', async () => { + const result = await (await fetch(`${baseurl}/api/updateRepo`)).json(); + console.log('scheduling cron: */5 * * * *', result); + }); + hasAddCron = true; + } + if (fs.existsSync(jsonPath)) { + console.log(1); + const templates = readTemplates(jsonPath, cdnUrl); return jsonRes(res, { data: templates, code: 200 }); } else { - return jsonRes(res, { data: [], code: 200 }); + console.log(2); + await fetch(`${baseurl}/api/updateRepo`); + return jsonRes(res, { data: [], code: 201 }); } } catch (error) { console.log(error); diff --git a/frontend/providers/template/src/pages/api/updateRepo.ts b/frontend/providers/template/src/pages/api/updateRepo.ts index 2e466097088..c21d6c8bfcc 100644 --- a/frontend/providers/template/src/pages/api/updateRepo.ts +++ b/frontend/providers/template/src/pages/api/updateRepo.ts @@ -71,15 +71,17 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< const originalPath = process.cwd(); const targetPath = path.resolve(originalPath, 'templates'); const jsonPath = path.resolve(originalPath, 'templates.json'); + const branch = process.env.TEMPLATE_REPO_BRANCH || 'main'; try { + execAsync('git config --global --add safe.directory /app/providers/template/templates'); const timeoutPromise = new Promise((resolve, reject) => { setTimeout(() => { reject(new Error('operation timed out')); }, 60 * 1000); }); const gitOperationPromise = !fs.existsSync(targetPath) - ? execAsync(`git clone ${repoHttpUrl} ${targetPath} --depth=1`) + ? execAsync(`git clone -b ${branch} ${repoHttpUrl} ${targetPath} --depth=1`) : execAsync(`cd ${targetPath} && git pull --depth=1 --rebase`); await Promise.race([gitOperationPromise, timeoutPromise]); @@ -120,7 +122,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< const jsonContent = JSON.stringify(jsonObjArr, null, 2); fs.writeFileSync(jsonPath, jsonContent, 'utf-8'); - jsonRes(res, { data: `success update template ${repoHttpUrl}`, code: 200 }); + jsonRes(res, { data: `success update template ${repoHttpUrl} branch ${branch}`, code: 200 }); } catch (err: any) { console.log(err, '===update repo log==='); jsonRes(res, { diff --git a/frontend/providers/template/src/pages/index.tsx b/frontend/providers/template/src/pages/index.tsx index c42f2e4be1a..3750448e28d 100644 --- a/frontend/providers/template/src/pages/index.tsx +++ b/frontend/providers/template/src/pages/index.tsx @@ -1,9 +1,8 @@ -import { updateRepo } from '@/api/platform'; -import { GET } from '@/services/request'; import { useCachedStore } from '@/store/cached'; import { useSearchStore } from '@/store/search'; import { TemplateType } from '@/types/app'; import { serviceSideProps } from '@/utils/i18n'; +import { formatStarNumber } from '@/utils/tools'; import { Avatar, AvatarGroup, @@ -16,44 +15,24 @@ import { Text, Tooltip } from '@chakra-ui/react'; -import { useQuery } from '@tanstack/react-query'; +import { customAlphabet } from 'nanoid'; import { useTranslation } from 'next-i18next'; import { useRouter } from 'next/router'; import { MouseEvent, useEffect, useMemo } from 'react'; -import { customAlphabet } from 'nanoid'; -import { formatStarNumber } from '@/utils/tools'; const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'); -export default function AppList() { +export default function AppList({ tempaltes }: { tempaltes: any }) { const { t } = useTranslation(); const router = useRouter(); const { searchValue } = useSearchStore(); const { setInsideCloud, insideCloud } = useCachedStore(); - const { data: FastDeployTemplates, refetch } = useQuery( - ['listTemplte'], - () => GET('/api/listTemplate'), - { - refetchInterval: 5 * 60 * 1000, - staleTime: 5 * 60 * 1000 - } - ); - - const { isLoading } = useQuery(['updateRepo'], () => updateRepo(), { - refetchInterval: 5 * 60 * 1000, - staleTime: 5 * 60 * 1000, - onSettled(data) { - console.log(data); - refetch(); - } - }); - const filterData = useMemo(() => { - const searchResults = FastDeployTemplates?.filter((item: TemplateType) => { + const searchResults = tempaltes?.filter((item: TemplateType) => { return item?.metadata?.name?.toLowerCase().includes(searchValue.toLowerCase()); }); - return searchValue ? searchResults : FastDeployTemplates; - }, [FastDeployTemplates, searchValue]); + return searchValue ? searchResults : tempaltes; + }, [tempaltes, searchValue]); const goDeploy = (name: string) => { if (!name) return; @@ -93,7 +72,7 @@ export default function AppList() { background={'linear-gradient(180deg, #FFF 0%, rgba(255, 255, 255, 0.70) 100%)'} py={'36px'} px="42px"> - {!!FastDeployTemplates?.length ? ( + {!!tempaltes?.length ? ( + onClick={handleDelApp}> {t('Confirm deletion')}