diff --git a/frontend/desktop/src/components/user_menu/github.tsx b/frontend/desktop/src/components/user_menu/github.tsx index c3e6c8a1254..1bf0610f90b 100644 --- a/frontend/desktop/src/components/user_menu/github.tsx +++ b/frontend/desktop/src/components/user_menu/github.tsx @@ -6,7 +6,7 @@ export default function GithubComponent(props: FlexProps) { ['getGithubStar'], () => fetch('https://api.github.com/repos/labring/sealos').then((res) => res.json()), { - staleTime: 60 * 60 * 1000 + staleTime: 24 * 60 * 60 * 1000 } ); diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 4e65bf35ed8..300e1dff521 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -1157,7 +1157,7 @@ importers: version: 4.0.2 next: specifier: 13.1.6 - version: 13.1.6(@babel/core@7.23.5)(react-dom@18.2.0)(react@18.2.0)(sass@1.69.5) + version: 13.1.6(@babel/core@7.23.3)(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) @@ -1847,8 +1847,8 @@ importers: specifier: workspace:^ version: link:../../packages/client-sdk swiper: - specifier: ^11.0.5 - version: 11.0.5 + specifier: ^11.0.7 + version: 11.0.7 typescript: specifier: 5.2.2 version: 5.2.2 @@ -10861,7 +10861,6 @@ packages: electron-to-chromium: 1.4.692 node-releases: 2.0.14 update-browserslist-db: 1.0.13(browserslist@4.23.0) - dev: true /bser@2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} @@ -10982,7 +10981,6 @@ packages: /caniuse-lite@1.0.30001594: resolution: {integrity: sha512-VblSX6nYqyJVs8DKFMldE2IVCJjZ225LW00ydtUWwh5hk9IfkTOffO6r8gJNsH0qqqeAF8KrbMYA2VEwTlGW5g==} - dev: true /caseless@0.12.0: resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} @@ -12151,7 +12149,6 @@ packages: /electron-to-chromium@1.4.692: resolution: {integrity: sha512-d5rZRka9n2Y3MkWRN74IoAsxR0HK3yaAt7T50e3iT9VZmCCQDT3geXUO5ZRMhDToa1pkCeQXuNo+0g+NfDOVPA==} - dev: true /element-resize-detector@1.2.4: resolution: {integrity: sha512-Fl5Ftk6WwXE0wqCgNoseKWndjzZlDCwuPTcoVZfCP9R3EHQF8qUtr3YUPNETegRBOKqQKPW3n4kiIWngGi8tKg==} @@ -17319,51 +17316,6 @@ packages: - babel-plugin-macros dev: false - /next@13.1.6(@babel/core@7.23.5)(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'} @@ -20549,8 +20501,8 @@ packages: stable: 0.1.8 dev: true - /swiper@11.0.5: - resolution: {integrity: sha512-rhCwupqSyRnWrtNzWzemnBLMoyYuoDgGgspAm/8iBD3jCvAWycPLH4Z3TB0O5520DHLzMx94yUMH/B9Efpa48w==} + /swiper@11.0.7: + resolution: {integrity: sha512-cDfglW1B6uSmB6eB6pNmzDTNLmZtu5bWWa1vak0RU7fOI9qHjMzl7gVBvYSl34b0RU2N11HxxETJqQ5LeqI1cA==} engines: {node: '>= 4.7.0'} dev: false @@ -21224,7 +21176,6 @@ packages: browserslist: 4.23.0 escalade: 3.1.1 picocolors: 1.0.0 - dev: true /uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -21513,7 +21464,7 @@ packages: '@webassemblyjs/wasm-parser': 1.11.6 acorn: 8.11.2 acorn-import-assertions: 1.9.0(acorn@8.11.2) - browserslist: 4.22.2 + browserslist: 4.23.0 chrome-trace-event: 1.0.3 enhanced-resolve: 5.15.0 es-module-lexer: 1.4.1 diff --git a/frontend/providers/template/.env.template b/frontend/providers/template/.env.template index fe7d1b594ea..7344a111d51 100644 --- a/frontend/providers/template/.env.template +++ b/frontend/providers/template/.env.template @@ -5,4 +5,5 @@ 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= -BLACKLIST_CATEGORIES="" \ No newline at end of file +BLACKLIST_CATEGORIES="" +SIDEBAR_MENU_COUNT="" \ No newline at end of file diff --git a/frontend/providers/template/.gitignore b/frontend/providers/template/.gitignore index 2fcf9ab4d70..46b33cbd5e8 100644 --- a/frontend/providers/template/.gitignore +++ b/frontend/providers/template/.gitignore @@ -42,3 +42,4 @@ yalc.lock templates templates.json +data/config.local.json diff --git a/frontend/providers/template/deploy/manifests/appcr.yaml.tmpl b/frontend/providers/template/deploy/manifests/appcr.yaml.tmpl index 0071b78062f..2a6a49abbf7 100644 --- a/frontend/providers/template/deploy/manifests/appcr.yaml.tmpl +++ b/frontend/providers/template/deploy/manifests/appcr.yaml.tmpl @@ -9,11 +9,11 @@ spec: displayType: normal i18n: zh: - name: 模板市场 + name: 应用商店 zh-Hans: - name: 模板市场 + name: 应用商店 icon: https://template.{{ .cloudDomain }}{{ if .cloudPort }}:{{ .cloudPort }}{{ end }}/logo.svg menuData: nameColor: text-black - name: Templates + name: App Store type: iframe \ No newline at end of file diff --git a/frontend/providers/template/deploy/manifests/deploy.yaml.tmpl b/frontend/providers/template/deploy/manifests/deploy.yaml.tmpl index 01face4aeb1..e3f025c2dad 100644 --- a/frontend/providers/template/deploy/manifests/deploy.yaml.tmpl +++ b/frontend/providers/template/deploy/manifests/deploy.yaml.tmpl @@ -13,6 +13,11 @@ metadata: data: config.yaml: |- addr: :3000 + config.json: |- + { + "showCarousel": false, + "slideData": [] + } --- apiVersion: apps/v1 kind: Deployment @@ -75,6 +80,9 @@ spec: - name: template-frontend-volume mountPath: /config.yaml subPath: config.yaml + - name: template-frontend-volume + mountPath: /app/data/config.json + subPath: config.json - name: template-data mountPath: /app/providers/template/templates volumes: diff --git a/frontend/providers/template/package.json b/frontend/providers/template/package.json index be1ec5db44a..21fc75ed495 100644 --- a/frontend/providers/template/package.json +++ b/frontend/providers/template/package.json @@ -59,7 +59,7 @@ "remark-unwrap-images": "^3.0.1", "sass": "^1.68.0", "sealos-desktop-sdk": "workspace:^", - "swiper": "^11.0.5", + "swiper": "^11.0.7", "typescript": "5.2.2", "zustand": "^4.4.1" }, diff --git a/frontend/providers/template/public/locales/en/common.json b/frontend/providers/template/public/locales/en/common.json index 5cd3f377ca4..7f93cf6faa8 100644 --- a/frontend/providers/template/public/locales/en/common.json +++ b/frontend/providers/template/public/locales/en/common.json @@ -139,8 +139,8 @@ "Template Config": "Template Config", "Templates": "Templates", "One Click Deployment": "Pre-build solutions for you and experience one-click deployment of applications", - "Template Name": "Template Name", - "Template List": " Templates", + "Application Name": "Application Name", + "Application List": " Application List", "Configure Project": "Configure Project", "Not need to configure any parameters": "The current application does not need to configure any parameters", "Do you want to jump to the app details page": "Do you want to jump to the app details page", @@ -160,7 +160,18 @@ }, "SideBar": { "Applications": "Applications", - "My App": "My Apps" + "My App": "My Apps", + "backend": "Backend", + "database": "Database", + "monitor": "Monitor", + "frontend": "Frontend", + "game": "Game", + "ai": "AI", + "tool": "Tool", + "dev-ops": "DevOps", + "blog": "Blog", + "low-code": "LowCode", + "storage": "Storage" }, "Schedule": "Schedule", "Last Schedule": "Last Schedule", @@ -192,4 +203,4 @@ "Delete successful": "Delete successful", "Delete Failed": "Delete Failed", "Description": "Description" -} \ No newline at end of file +} diff --git a/frontend/providers/template/public/locales/zh/common.json b/frontend/providers/template/public/locales/zh/common.json index c8e27200127..cd046f01783 100644 --- a/frontend/providers/template/public/locales/zh/common.json +++ b/frontend/providers/template/public/locales/zh/common.json @@ -146,8 +146,8 @@ "Template Config": "模板配置", "Templates": "模板市场", "One Click Deployment": "为您预先构建解决方案,体验一键部署应用", - "Template Name": "模板名称", - "Template List": " 模板列表", + "Application Name": "应用名称", + "Application List": " 应用列表", "Not need to configure any parameters": "当前应用不需要配置任何参数", "Do you want to jump to the app details page": "您要跳转到应用详情页吗", "Deploy on sealos": "去sealos部署", @@ -166,7 +166,18 @@ }, "SideBar": { "Applications": "所有应用", - "My App": "我的应用" + "My App": "我的应用", + "backend": "后端", + "database": "数据库", + "monitor": "监控", + "frontend": "前端", + "game": "游戏", + "ai": "AI", + "tool": "工具", + "dev-ops": "运维", + "blog": "博客", + "low-code": "低代码", + "storage": "存储" }, "Schedule": "执行周期", "Last Schedule": "上次执行", @@ -198,4 +209,4 @@ "Delete successful": "删除成功", "Delete Failed": "删除失败", "Description": "描述" -} \ No newline at end of file +} diff --git a/frontend/providers/template/src/api/platform.ts b/frontend/providers/template/src/api/platform.ts index 43d0163ffe0..c4eac571190 100644 --- a/frontend/providers/template/src/api/platform.ts +++ b/frontend/providers/template/src/api/platform.ts @@ -1,6 +1,14 @@ import { EnvResponse } from '@/types/index'; import { GET } from '@/services/request'; +import { SystemConfigType, TemplateType } from '@/types/app'; export const updateRepo = () => GET('/api/updateRepo'); +export const getTemplates = () => + GET<{ templates: TemplateType[]; menuKeys: string }>('/api/listTemplate'); + export const getPlatformEnv = () => GET('/api/platform/getEnv'); + +export const getSystemConfig = () => { + return GET('/api/platform/getSystemConfig'); +}; diff --git a/frontend/providers/template/src/components/Banner/index.tsx b/frontend/providers/template/src/components/Banner/index.tsx index d4fd1c898b5..106cc3cd385 100644 --- a/frontend/providers/template/src/components/Banner/index.tsx +++ b/frontend/providers/template/src/components/Banner/index.tsx @@ -1,51 +1,62 @@ import { Box, Center, Flex, Image, Text } from '@chakra-ui/react'; import { useRef } from 'react'; -import { Autoplay } from 'swiper/modules'; -import { Swiper, SwiperRef, SwiperSlide, useSwiper } from 'swiper/react'; import 'swiper/css'; +import { Autoplay } from 'swiper/modules'; +import { Swiper, SwiperRef, SwiperSlide } from 'swiper/react'; import { ArrowRightIcon } from '../icons/ArrowRight'; +import { useSystemConfigStore } from '@/store/config'; +import { useRouter } from 'next/router'; +import { SlideDataType } from '@/types/app'; +import React from 'react'; -export const SlideData = [ - { - image: - 'https://images.unsplash.com/photo-1546768292-fb12f6c92568?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=1350&q=80', - bg: 'linear-gradient(274deg, #824DFF 13.19%, #A97CFF 93.1%)', - title: 'Laf', - desc: 'Laf 是开源的云开发平台,提供云函数、云数据库、云存储等开箱即用的应用资源。让开发者专注于业务开发,无需折腾服务器,快速释放创意。', - borderRadius: '8px', - icon: 'https://laf.run/homepage/logo_icon.svg' - }, - { - image: - 'https://images.unsplash.com/photo-1501446529957-6226bd447c46?ixlib=rb-1.2.1&ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&auto=format&fit=crop&w=1489&q=80', - bg: 'linear-gradient(274deg, #3770FE 6.31%, #6793FF 93.69%)', - title: 'Umami', - desc: 'Umami is an open source, privacy-focused alternative to Google Analytics.', - borderRadius: '8px', - icon: 'https://jsd.onmicrosoft.cn/gh/umami-software/umami@master/src/assets/logo.svg' - }, - { - image: - 'https://images.unsplash.com/photo-1483729558449-99ef09a8c325?ixlib=rb-1.2.1&ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&auto=format&fit=crop&w=1350&q=80', - bg: '#824DFF', - title: 'Lobe Chat', - desc: 'LobeChat 是开源的高性能聊天机器人框架,支持语音合成、多模态、可扩展的(Function Call)插件系统。支持一键免费部署私人 ChatGPT/LLM 网页应用程序。', - borderRadius: '8px', - icon: 'https://jsd.onmicrosoft.cn/npm/@lobehub/assets-logo@1.0.0/assets/logo-3d.webp' - }, - { - image: - 'https://images.unsplash.com/photo-1475189778702-5ec9941484ae?ixlib=rb-1.2.1&ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&auto=format&fit=crop&w=1351&q=80', - bg: '#824DFF', - title: '4444', - desc: 'Build the tools you can’t buy off the shelf', - borderRadius: '8px', - icon: 'https://jsd.onmicrosoft.cn/gh/appsmithorg/appsmith@release/static/logo.png' - } -]; +const Card = ({ item, onClick }: { item: SlideDataType; onClick: () => void }) => { + return ( + + + +
+ logo +
+ + {item.title} + +
+ + {item.desc} + +
-export default function Banner() { + {`slide-${item.templateName}`} +
+ ); +}; + +export default React.memo(function Banner() { const swiperRef = useRef(null); + const { systemConfig } = useSystemConfigStore(); + const router = useRouter(); const handlePrev = () => { if (swiperRef.current) { @@ -59,11 +70,25 @@ export default function Banner() { } }; + const goDeploy = (name: string) => { + if (!name) return; + router.push({ + pathname: '/deploy', + query: { + templateName: name + } + }); + }; + + if (!systemConfig?.showCarousel) { + return null; + } + return ( - {SlideData.map((item, index) => ( + {systemConfig?.slideData.map((item, index) => ( - - - -
- logo -
- - {item.title} - -
- - {item.desc} - -
- {`slide-${index}`} -
- - - -
- logo -
- - {SlideData[(index + 1) % SlideData.length].title} - -
- - {SlideData[(index + 1) % SlideData.length].desc} - -
- {`slide-${index}`} -
+ goDeploy(item.templateName)} /> + + goDeploy( + (systemConfig?.slideData[(index + 1) % systemConfig?.slideData.length]) + .templateName + ) + } + />
))} @@ -199,4 +157,4 @@ export default function Banner() {
); -} +}); diff --git a/frontend/providers/template/src/components/layout/appmenu.tsx b/frontend/providers/template/src/components/layout/appmenu.tsx index 9b3b7848751..d6caf95d111 100644 --- a/frontend/providers/template/src/components/layout/appmenu.tsx +++ b/frontend/providers/template/src/components/layout/appmenu.tsx @@ -1,127 +1,120 @@ import { useCachedStore } from '@/store/cached'; import { useSearchStore } from '@/store/search'; import { getLangStore, setLangStore } from '@/utils/cookieUtils'; -import { Box, Flex, FlexProps, Input, InputGroup, InputLeftElement, Text } from '@chakra-ui/react'; +import { Center, Flex, Icon, Input, InputGroup, InputLeftElement, Text } from '@chakra-ui/react'; import { useTranslation } from 'next-i18next'; import { useRouter } from 'next/router'; -import { useEffect, useState } from 'react'; -import MyIcon from '../Icon'; import SideBar from './sidebar'; +import { ApplicationType } from '@/types/app'; -export default function AppMenu({ isMobile }: { isMobile: boolean }) { +export default function AppMenu() { const { t, i18n } = useTranslation(); const router = useRouter(); - const { searchValue, setSearchValue } = useSearchStore(); + const { setSearchValue, setAppType } = useSearchStore(); const { insideCloud } = useCachedStore(); - const [isClientRendered, setClientRendered] = useState(false); - useEffect(() => { - setClientRendered(true); - }, []); - - const changeI18n = async (newLang: string) => { + const changeI18n = () => { const lastLang = getLangStore(); - if (lastLang !== newLang && i18n?.changeLanguage) { + const newLang = lastLang === 'en' ? 'zh' : 'en'; + if (i18n?.changeLanguage) { i18n.changeLanguage(newLang); setLangStore(newLang); } }; - const baseStyle: FlexProps = { - position: 'absolute', - justifyContent: 'center', - alignItems: 'center', - bg: 'rgba(150, 153, 180, 0.15)', - userSelect: 'none' - }; - return ( - - {!isMobile && ( - <> - - {t('Templates')} - - - - - - - - { - setSearchValue(e.target.value); - }} - _focus={{ - boxShadow: 'none', - border: '1.5px solid #219BF4', - background: '#FFF' - }} + + + + + - - - )} + + + { + setSearchValue(e.target.value); + }} + _focus={{ + boxShadow: 'none', + border: '1.5px solid #219BF4', + background: '#FFF' + }} + /> + - {isClientRendered && } + - {isMobile ? ( - router.push('/develop')} - > - - - ) : ( - router.push('/develop')} + { + router.replace('/app'); + setAppType(ApplicationType.MyApp); + }} + > + - - - {t('develop.Debugging Template')} - - - )} + + + + + {t('SideBar.My App')} + - {isClientRendered && !insideCloud && ( - { - changeI18n(i18n?.language === 'en' ? 'zh' : 'en'); - }} - > - {i18n?.language === 'en' ? 'En' : '中'} - - )} - + {!insideCloud && ( +
{ + e.stopPropagation(); + changeI18n(); + }} + > + {i18n?.language === 'en' ? 'En' : '中'} +
+ )} + + ); } diff --git a/frontend/providers/template/src/components/layout/index.tsx b/frontend/providers/template/src/components/layout/index.tsx index 63ab6ba6bd5..422cb51f6d9 100644 --- a/frontend/providers/template/src/components/layout/index.tsx +++ b/frontend/providers/template/src/components/layout/index.tsx @@ -1,7 +1,5 @@ -import { useGlobalStore } from '@/store/global'; import { Box, Grid } from '@chakra-ui/react'; import { useRouter } from 'next/router'; -import { useMemo } from 'react'; import AppMenu from './appmenu'; const ShowLayoutRoute: Record = { @@ -11,20 +9,18 @@ const ShowLayoutRoute: Record = { }; export default function Layout({ children }: { children: JSX.Element }) { - const { screenWidth } = useGlobalStore(); const router = useRouter(); - const isMobile = useMemo(() => screenWidth < 1024, [screenWidth]); return ( <> {ShowLayoutRoute[router.pathname] ? ( - + <>{children} ) : ( diff --git a/frontend/providers/template/src/components/layout/sidebar.tsx b/frontend/providers/template/src/components/layout/sidebar.tsx index 7b975f64322..8aebcdfa2c4 100644 --- a/frontend/providers/template/src/components/layout/sidebar.tsx +++ b/frontend/providers/template/src/components/layout/sidebar.tsx @@ -1,100 +1,46 @@ -import { useCachedStore } from '@/store/cached'; -import { Flex, Icon, Text } from '@chakra-ui/react'; +import { SideBarMenu } from '@/store/config'; +import { useSearchStore } from '@/store/search'; +import { Flex, Text } from '@chakra-ui/react'; import { useTranslation } from 'next-i18next'; import { useRouter } from 'next/router'; -import { ReactElement, useMemo } from 'react'; -export type SideBarMenu = { - id: string; - url: string; - acriveUrl: string[]; - value: string; - icon: ReactElement; - display: boolean; - showLayout: boolean; -}; - -export default function SideBar({ isMobile }: { isMobile: boolean }) { - const router = useRouter(); +export default function SideBar() { const { t } = useTranslation(); - const { insideCloud } = useCachedStore(); - - const menus: SideBarMenu[] = useMemo( - () => [ - { - id: 'Applications', - url: '/', - acriveUrl: ['/', '/deploy'], - value: 'SideBar.Applications', - icon: ( - - ), - display: true, - showLayout: true - }, - { - id: 'MyApp', - url: '/app', - value: 'SideBar.My App', - acriveUrl: ['/app'], - icon: ( - - ), - display: insideCloud, - showLayout: true - } - ], - [insideCloud] - ); + const { appType, setAppType } = useSearchStore(); + const router = useRouter(); return ( - - {menus && - menus - .filter((item) => item.display) - .map((item) => { - return ( - { - router.push(item.url); - }} + + {SideBarMenu && + SideBarMenu.map((item) => { + return ( + { + router.replace('/'); + setAppType(item.type); + }} + > + - - {item.icon} - - {!isMobile && ( - - {t(item.value)} - - )} - - ); - })} + {t(item.value)} + + + ); + })} ); } diff --git a/frontend/providers/template/src/pages/_app.tsx b/frontend/providers/template/src/pages/_app.tsx index 06686476104..16d0123b596 100644 --- a/frontend/providers/template/src/pages/_app.tsx +++ b/frontend/providers/template/src/pages/_app.tsx @@ -1,6 +1,5 @@ +import Layout from '@/components/layout'; import { theme } from '@/constants/theme'; -import { useConfirm } from '@/hooks/useConfirm'; -import { useLoading } from '@/hooks/useLoading'; import { useGlobalStore } from '@/store/global'; import { getLangStore, setLangStore } from '@/utils/cookieUtils'; import { ChakraProvider } from '@chakra-ui/react'; @@ -14,11 +13,11 @@ import NProgress from 'nprogress'; //nprogress module import { useEffect, useState } from 'react'; import { EVENT_NAME } from 'sealos-desktop-sdk'; import { createSealosApp, sealosApp } from 'sealos-desktop-sdk/app'; -import Layout from '@/components/layout'; +import { useSystemConfigStore } from '@/store/config'; +import useSessionStore from '@/store/session'; import '@/styles/reset.scss'; import 'nprogress/nprogress.css'; -import useSessionStore from '@/store/session'; //Binding events. Router.events.on('routeChangeStart', () => NProgress.start()); @@ -36,17 +35,17 @@ const queryClient = new QueryClient({ } }); -const App = ({ Component, pageProps, domain }: AppProps & { domain: string }) => { +const App = ({ Component, pageProps }: AppProps) => { const router = useRouter(); const { setSession } = useSessionStore(); const { i18n } = useTranslation(); - const { setScreenWidth, loading, setLastRoute } = useGlobalStore(); - const { Loading } = useLoading(); + const { setScreenWidth, setLastRoute } = useGlobalStore(); + const { initSystemConfig } = useSystemConfigStore(); const [refresh, setRefresh] = useState(false); - const { openConfirm, ConfirmChild } = useConfirm({ - title: 'jump_prompt', - content: 'jump_message' - }); + + useEffect(() => { + initSystemConfig(); + }, []); useEffect(() => { NProgress.start(); @@ -61,7 +60,7 @@ const App = ({ Component, pageProps, domain }: AppProps & { domain: string }) => })(); NProgress.done(); return response; - }, [openConfirm]); + }, []); // add resize event useEffect(() => { @@ -114,7 +113,7 @@ const App = ({ Component, pageProps, domain }: AppProps & { domain: string }) => if (lang) { i18n?.changeLanguage?.(lang); } - }, [refresh, router.pathname]); + }, [i18n, refresh, router.pathname]); return ( <> @@ -135,8 +134,4 @@ const App = ({ Component, pageProps, domain }: AppProps & { domain: string }) => ); }; -App.getInitialProps = async () => { - return { domain: process.env.SEALOS_DOMAIN || 'cloud.sealos.io' }; -}; - export default appWithTranslation(App); diff --git a/frontend/providers/template/src/pages/api/listTemplate.ts b/frontend/providers/template/src/pages/api/listTemplate.ts index 507e1e41229..20a2564a833 100644 --- a/frontend/providers/template/src/pages/api/listTemplate.ts +++ b/frontend/providers/template/src/pages/api/listTemplate.ts @@ -1,6 +1,7 @@ import { jsonRes } from '@/services/backend/response'; import { ApiResp } from '@/services/kubernet'; import { TemplateType } from '@/types/app'; +import { findTopKeyWords } from '@/utils/template'; import { parseGithubUrl } from '@/utils/tools'; import fs from 'fs'; import type { NextApiRequest, NextApiResponse } from 'next'; @@ -22,7 +23,7 @@ export const readTemplates = ( jsonPath: string, cdnUrl?: string, blacklistedCategories?: string[] -) => { +): TemplateType[] => { const jsonData = fs.readFileSync(jsonPath, 'utf8'); const _templates: TemplateType[] = JSON.parse(jsonData); @@ -30,7 +31,9 @@ export const readTemplates = ( .filter((item) => { const isBlacklisted = blacklistedCategories && - blacklistedCategories.some((category) => (item?.spec?.categories ?? []).includes(category)); + blacklistedCategories.some((category) => + (item?.spec?.categories ?? []).map((c) => c.toLowerCase()).includes(category) + ); return !item?.spec?.draft && !isBlacklisted; }) .map((item) => { @@ -52,6 +55,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< const blacklistedCategories = process.env.BLACKLIST_CATEGORIES ? process.env.BLACKLIST_CATEGORIES.split(',') : []; + const menuCount = Number(process.env.SIDEBAR_MENU_COUNT) || 10; try { if (!hasAddCron) { @@ -68,8 +72,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< } const templates = readTemplates(jsonPath, cdnUrl, blacklistedCategories); - jsonRes(res, { data: templates, code: 200 }); + const categories = templates.map((item) => (item.spec?.categories ? item.spec.categories : [])); + const topKeys = findTopKeyWords(categories, menuCount); + + jsonRes(res, { data: { templates: templates, menuKeys: topKeys.join(',') }, code: 200 }); } catch (error) { - jsonRes(res, { code: 500, data: 'error' }); + jsonRes(res, { code: 500, data: 'api listTemplate error' }); } } diff --git a/frontend/providers/template/src/pages/api/platform/getSystemConfig.ts b/frontend/providers/template/src/pages/api/platform/getSystemConfig.ts new file mode 100644 index 00000000000..56b5b291904 --- /dev/null +++ b/frontend/providers/template/src/pages/api/platform/getSystemConfig.ts @@ -0,0 +1,29 @@ +import { jsonRes } from '@/services/backend/response'; +import { ApplicationType, SystemConfigType } from '@/types/app'; +import { readFileSync } from 'fs'; +import type { NextApiRequest, NextApiResponse } from 'next'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const config = await getSystemConfig(); + jsonRes(res, { + data: config, + code: 200 + }); +} + +export const defaultConfig: SystemConfigType = { + showCarousel: false, + slideData: [] +}; + +export async function getSystemConfig(): Promise { + try { + const filename = + process.env.NODE_ENV === 'development' ? 'data/config.local.json' : '/app/data/config.json'; + const res = JSON.parse(readFileSync(filename, 'utf-8')) as SystemConfigType; + return res; + } catch (error) { + console.log('-getSystemConfig-\n', error); + return defaultConfig; + } +} diff --git a/frontend/providers/template/src/pages/api/updateRepo.ts b/frontend/providers/template/src/pages/api/updateRepo.ts index 290f0fa5b3d..1b0834720bc 100644 --- a/frontend/providers/template/src/pages/api/updateRepo.ts +++ b/frontend/providers/template/src/pages/api/updateRepo.ts @@ -57,8 +57,8 @@ export async function GetTemplateStatic() { } }); return temp; - } catch (error) { - console.log(error, 'error: kubectl get configmap/template-static '); + } catch (error: any) { + console.log('error: kubectl get configmap/template-static \n', error?.body); return {}; } } diff --git a/frontend/providers/template/src/pages/app/components/list.tsx b/frontend/providers/template/src/pages/app/components/list.tsx index e6506a16956..ec44005b378 100644 --- a/frontend/providers/template/src/pages/app/components/list.tsx +++ b/frontend/providers/template/src/pages/app/components/list.tsx @@ -54,11 +54,10 @@ export default function InstanceList() { w={'100%'} gridTemplateColumns="repeat(auto-fill,minmax(344px,1fr))" gridGap={'24px'} - minW={'765px'} pt="24px" - pr="42px" pb="42px" overflow={'auto'} + minW={'712px'} > {data && data?.map((item) => { diff --git a/frontend/providers/template/src/pages/app/index.tsx b/frontend/providers/template/src/pages/app/index.tsx index 0b6b9ead50b..030a21e7a32 100644 --- a/frontend/providers/template/src/pages/app/index.tsx +++ b/frontend/providers/template/src/pages/app/index.tsx @@ -1,12 +1,15 @@ import { serviceSideProps } from '@/utils/i18n'; -import { Box, Flex, Tab, TabIndicator, TabList, Tabs } from '@chakra-ui/react'; +import { Box, Center, Flex, Tab, TabIndicator, TabList, Tabs, Text } from '@chakra-ui/react'; import { useTranslation } from 'next-i18next'; import { useState } from 'react'; import InstanceList from './components/list'; +import { useRouter } from 'next/router'; +import MyIcon from '@/components/Icon'; export default function MyApp() { const { t } = useTranslation(); const [tabIndex, setTabIndex] = useState(0); + const router = useRouter(); const handleTabsChange = (index: number) => { setTabIndex(index); @@ -15,28 +18,46 @@ export default function MyApp() { return ( - - - - {t('Installed')} - - - - + + + + + {t('Installed')} + + + + +
router.push('/develop')} + > + + + {t('develop.Debugging Template')} + +
+
+
); diff --git a/frontend/providers/template/src/pages/deploy/index.tsx b/frontend/providers/template/src/pages/deploy/index.tsx index 19db3fa7857..d08de1c680c 100644 --- a/frontend/providers/template/src/pages/deploy/index.tsx +++ b/frontend/providers/template/src/pages/deploy/index.tsx @@ -301,7 +301,7 @@ export default function EditApp({ appName }: { appName?: string }) { router.push('/')}> - {t('Template List')} + {t('Application List')}
/ diff --git a/frontend/providers/template/src/pages/develop/components/BreadCrumbHeader.tsx b/frontend/providers/template/src/pages/develop/components/BreadCrumbHeader.tsx index fbdf651df06..42abc0f577b 100644 --- a/frontend/providers/template/src/pages/develop/components/BreadCrumbHeader.tsx +++ b/frontend/providers/template/src/pages/develop/components/BreadCrumbHeader.tsx @@ -1,3 +1,5 @@ +import { useSearchStore } from '@/store/search'; +import { ApplicationType } from '@/types/app'; import { Box, Button, Flex, Icon, Text } from '@chakra-ui/react'; import { useTranslation } from 'next-i18next'; import { useRouter } from 'next/router'; @@ -13,6 +15,7 @@ const BreadCrumbHeader = ({ }) => { const router = useRouter(); const { t } = useTranslation(); + const { setAppType } = useSearchStore(); return ( { + router.push('/'); + setAppType(ApplicationType.All); + }} > - router.push('/')} - > + - router.push('/')}> - {t('Template List')} - + {t('Application List')} / { + if (!formHook) return null; const { t } = useTranslation(); const isShowContent = useMemo( diff --git a/frontend/providers/template/src/pages/index.tsx b/frontend/providers/template/src/pages/index.tsx index 49f83d36485..a36dbaefd59 100644 --- a/frontend/providers/template/src/pages/index.tsx +++ b/frontend/providers/template/src/pages/index.tsx @@ -1,5 +1,6 @@ +import { getTemplates } from '@/api/platform'; import Banner from '@/components/Banner'; -import { GET } from '@/services/request'; +import MyIcon from '@/components/Icon'; import { useCachedStore } from '@/store/cached'; import { useSearchStore } from '@/store/search'; import { TemplateType } from '@/types/app'; @@ -9,11 +10,13 @@ import { Avatar, AvatarGroup, Box, + Center, Flex, Grid, Icon, Image, Spinner, + Tag, Text, Tooltip } from '@chakra-ui/react'; @@ -22,29 +25,34 @@ import { customAlphabet } from 'nanoid'; import { useTranslation } from 'next-i18next'; import { useRouter } from 'next/router'; import { MouseEvent, useEffect, useMemo } from 'react'; + const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'); export default function AppList() { const { t } = useTranslation(); const router = useRouter(); - const { searchValue } = useSearchStore(); - const { setInsideCloud, insideCloud } = useCachedStore(); + const { searchValue, appType } = useSearchStore(); + const { setInsideCloud } = useCachedStore(); - const { data: FastDeployTemplates, refetch } = useQuery( - ['listTemplte'], - () => GET('/api/listTemplate'), - { - refetchInterval: 5 * 60 * 1000, - staleTime: 5 * 60 * 1000 - } - ); + const { data } = useQuery(['listTemplate'], getTemplates, { + refetchInterval: 5 * 60 * 1000, + staleTime: 5 * 60 * 1000, + retry: 3 + }); const filterData = useMemo(() => { - const searchResults = FastDeployTemplates?.filter((item: TemplateType) => { + const typeFilteredResults = data?.templates?.filter((item: TemplateType) => { + if (appType === 'all') return true; + const isMatchType = item?.spec?.categories?.includes(appType); + return isMatchType; + }); + + const searchResults = typeFilteredResults?.filter((item: TemplateType) => { return item?.metadata?.name?.toLowerCase().includes(searchValue.toLowerCase()); }); - return searchValue ? searchResults : FastDeployTemplates; - }, [FastDeployTemplates, searchValue]); + + return searchResults; + }, [data?.templates, appType, searchValue]); const goDeploy = (name: string) => { if (!name) return; @@ -82,114 +90,141 @@ export default function AppList() { position={'relative'} borderRadius={'12px'} background={'linear-gradient(180deg, #FFF 0%, rgba(255, 255, 255, 0.70) 100%)'} - py={'36px'} + py="24px" px="42px" > - {/* */} - {!!FastDeployTemplates?.length ? ( - - {filterData && - filterData?.map((item: TemplateType) => { - return ( - goDeploy(item?.metadata?.name)} - _hover={{ - borderColor: '#36ADEF', - boxShadow: '0px 4px 5px 0px rgba(185, 196, 205, 0.25)' - }} - key={item?.metadata?.name} - flexDirection={'column'} - minH={'214px'} - h="214px" - p={'24px'} - borderRadius={'8px'} - backgroundColor={'#fff'} - boxShadow={'0px 2px 4px 0px rgba(187, 196, 206, 0.25)'} - border={'1px solid #EAEBF0'} - > - - - - - {item.spec?.deployCount && item.spec?.deployCount > 6 && ( - - - - - - - - +{formatStarNumber(item.spec.deployCount)} - - - )} - - - - {item?.spec?.title} - - + - + {filterData?.length && filterData?.length > 0 ? ( + + {filterData?.map((item: TemplateType) => { + return ( + goDeploy(item?.metadata?.name)} + _hover={{ + borderColor: '#36ADEF', + boxShadow: '0px 4px 5px 0px rgba(185, 196, 205, 0.25)' + }} + key={item?.metadata?.name} + flexDirection={'column'} + h={'184px'} + p={'24px'} + borderRadius={'8px'} + backgroundColor={'#fff'} + boxShadow={'0px 2px 4px 0px rgba(187, 196, 206, 0.25)'} + border={'1px solid #EAEBF0'} > - {item?.spec?.description} - - - - By - {item?.spec?.author} - - goGithub(e, item?.spec?.gitRepo)}> - + - - - - + + + + + {item?.spec?.title} + + + By {item?.spec?.author} + + + {item.spec?.deployCount && item.spec?.deployCount > 6 && ( + + + + + + + + +{formatStarNumber(item.spec.deployCount)} + + + )} + + + {item?.spec?.description} + + + + {item?.spec?.categories?.map((i) => ( + + {t(`SideBar.${i}`)} + + ))} + +
goGithub(e, item?.spec?.gitRepo)}> + + + + +
+
- - ); - })} -
+ ); + })} + + ) : ( +
+
+ +
+
+ )} + ) : ( diff --git a/frontend/providers/template/src/pages/instance/components/header.tsx b/frontend/providers/template/src/pages/instance/components/header.tsx index 5974e42b20a..16e8abca994 100644 --- a/frontend/providers/template/src/pages/instance/components/header.tsx +++ b/frontend/providers/template/src/pages/instance/components/header.tsx @@ -3,7 +3,7 @@ import { getInstanceByName } from '@/api/instance'; import { templateDisplayNameKey } from '@/constants/keys'; import { useToast } from '@/hooks/useToast'; import { useResourceStore } from '@/store/resource'; -import { TemplateInstanceType } from '@/types/app'; +import { ApplicationType, TemplateInstanceType } from '@/types/app'; import { Box, Button, @@ -25,6 +25,7 @@ import { useTranslation } from 'next-i18next'; import { useRouter } from 'next/router'; import { useRef, useState } from 'react'; import DelModal from './delDodal'; +import { useSearchStore } from '@/store/search'; export default function Header({ instanceName }: { instanceName: string }) { const router = useRouter(); @@ -39,6 +40,7 @@ export default function Header({ instanceName }: { instanceName: string }) { } = useDisclosure(); const [displayName, setDisplayName] = useState(''); const yamlCR = useRef(); + const { setAppType } = useSearchStore(); const { data, refetch } = useQuery( ['getInstanceByName', instanceName], @@ -93,7 +95,10 @@ export default function Header({ instanceName }: { instanceName: string }) { height="36px" viewBox="0 0 35 36" fill="#5A646E" - onClick={() => router.push('/app')} + onClick={() => { + setAppType(ApplicationType.MyApp); + router.push('/app'); + }} > diff --git a/frontend/providers/template/src/store/config.ts b/frontend/providers/template/src/store/config.ts new file mode 100644 index 00000000000..8db4c817452 --- /dev/null +++ b/frontend/providers/template/src/store/config.ts @@ -0,0 +1,44 @@ +import { getSystemConfig, getTemplates } from '@/api/platform'; +import { ApplicationType, SideBarMenuType, SystemConfigType } from '@/types/app'; +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; +import { immer } from 'zustand/middleware/immer'; + +type State = { + systemConfig: SystemConfigType | undefined; + initSystemConfig: () => Promise; +}; + +export let SideBarMenu = [ + { + id: 'applications', + type: ApplicationType.All, + value: 'SideBar.Applications' + } +]; + +export const useSystemConfigStore = create()( + devtools( + immer((set, get) => ({ + systemConfig: undefined, + async initSystemConfig() { + const data = await getSystemConfig(); + + const { menuKeys } = await getTemplates(); + const menus = SideBarMenu.concat( + menuKeys.split(',').map((i) => ({ + id: i, + type: i as ApplicationType, + value: `SideBar.${i}` + })) + ); + SideBarMenu = menus; + + set((state) => { + state.systemConfig = data; + }); + return data; + } + })) + ) +); diff --git a/frontend/providers/template/src/store/search.ts b/frontend/providers/template/src/store/search.ts index 15ddf01c8b2..398424be34f 100644 --- a/frontend/providers/template/src/store/search.ts +++ b/frontend/providers/template/src/store/search.ts @@ -1,20 +1,29 @@ +import { ApplicationType } from '@/types/app'; import { create } from 'zustand'; import { devtools } from 'zustand/middleware'; import { immer } from 'zustand/middleware/immer'; type State = { searchValue: string; + appType: ApplicationType; setSearchValue: (e: string) => void; + setAppType: (e: ApplicationType) => void; }; export const useSearchStore = create()( devtools( immer((set, get) => ({ searchValue: '', + appType: ApplicationType.All, setSearchValue(e: string) { set((state) => { state.searchValue = e; }); + }, + setAppType(e: ApplicationType) { + set((state) => { + state.appType = e; + }); } })) ) diff --git a/frontend/providers/template/src/types/app.ts b/frontend/providers/template/src/types/app.ts index fe9c1c190d3..fa4f044db17 100644 --- a/frontend/providers/template/src/types/app.ts +++ b/frontend/providers/template/src/types/app.ts @@ -149,3 +149,29 @@ export type InstanceListItemType = { yamlCR: TemplateInstanceType; displayName?: string; }; + +export enum ApplicationType { + All = 'all', + MyApp = 'myapp' +} + +export type SlideDataType = { + title: string; + desc: string; + bg: string; + image: string; + borderRadius: string; + icon: string; + templateName: string; +}; + +export type SideBarMenuType = { + id: string; + value: string; + type: ApplicationType; +}; + +export type SystemConfigType = { + showCarousel: boolean; + slideData: SlideDataType[]; +}; diff --git a/frontend/providers/template/src/utils/template.ts b/frontend/providers/template/src/utils/template.ts index be5253b5b3a..9d589a58e7d 100644 --- a/frontend/providers/template/src/utils/template.ts +++ b/frontend/providers/template/src/utils/template.ts @@ -13,3 +13,20 @@ export const getTemplateDefaultValues = (templateSource: TemplateSourceType | un {} ); }; + +export function findTopKeyWords(keywordsList: string[][], topCount: number) { + const flatKeywordsList = keywordsList.filter(Boolean).flat(); + + const keywordCountMap = new Map(); + + flatKeywordsList.forEach((keyword) => { + const count = keywordCountMap.get(keyword) || 0; + keywordCountMap.set(keyword, count + 1); + }); + + const sortedKeywords = Array.from(keywordCountMap.entries()).sort((a, b) => b[1] - a[1]); + + const topKeywords = sortedKeywords.slice(0, topCount).map((entry) => entry[0]); + + return topKeywords; +}