diff --git a/frontend/desktop/package.json b/frontend/desktop/package.json index 75652a85e27..2ce017578b0 100644 --- a/frontend/desktop/package.json +++ b/frontend/desktop/package.json @@ -37,6 +37,7 @@ "clsx": "^1.2.1", "cors": "^2.8.5", "dayjs": "^1.11.10", + "decimal.js": "^10.4.3", "eslint": "8.38.0", "eslint-config-next": "13.3.0", "framer-motion": "^10.16.4", @@ -58,6 +59,7 @@ "qrcode.react": "^3.1.0", "randexp": "^0.5.3", "react": "18.2.0", + "react-contexify": "^6.0.0", "react-dom": "18.2.0", "react-draggable": "^4.4.6", "react-hook-form": "^7.46.2", diff --git a/frontend/desktop/public/icons/apps.svg b/frontend/desktop/public/icons/apps.svg new file mode 100644 index 00000000000..354bb75cf2b --- /dev/null +++ b/frontend/desktop/public/icons/apps.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/desktop/public/images/bg-blue.svg b/frontend/desktop/public/images/bg-blue.svg index 4189a7ea91f..a2bcf99ee2e 100644 --- a/frontend/desktop/public/images/bg-blue.svg +++ b/frontend/desktop/public/images/bg-blue.svg @@ -1,151 +1,151 @@ - - - - - - - - - - - + + + + + + + + + + + - - - - - - + + + + + + - - - + + + - + - - + + - - - + + + - + - + - + - + - + - + - + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/desktop/public/images/default-user.svg b/frontend/desktop/public/images/default-user.svg new file mode 100644 index 00000000000..6b567782068 --- /dev/null +++ b/frontend/desktop/public/images/default-user.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/frontend/desktop/public/locales/en/common.json b/frontend/desktop/public/locales/en/common.json index bb08442cd26..2c7480b0321 100644 --- a/frontend/desktop/public/locales/en/common.json +++ b/frontend/desktop/public/locales/en/common.json @@ -84,7 +84,7 @@ "Added": "Added", "Invaild Context": "You need switch to other workspace for handling", "Remove Member Tips": "Determine that you want to remove the member?", - "Quit Workspace Tips":"Confirm leaving workspace?", + "Quit Workspace Tips": "Confirm leaving workspace?", "Invalid User ID": "Invalid User ID", "The invited user must be others": "The invited user must be others", "Dissovle Tips": "Deleting the workspace will clear all resources. Are you sure you want to disband", @@ -131,5 +131,26 @@ "Year": "Year", "Core": "Core", "Yuan": "Yuan", - "Description": "Description" -} + "Description": "Description", + "All Apps": "All Apps", + "Account Settings": "Account Settings", + "Alerts": "Alerts", + "Monitor": "Monitor", + "Used Resources": "Used Resources", + "Memory": "Memory", + "Storage": "Storage", + "Flow": "Flow", + "Healthy Pod": "Healthy Pod: {{count}}", + "Alarm Pod": "Alarm Pod: {{count}}", + "Expected used": "Expected used", + "Used last month": "Used last month", + "Expected to use next month": "Expected to use next month", + "Day": "Day", + "Search Apps": "Search Apps", + "No Apps Found": "No Apps Found", + "Switching Disc": "Switching Disc", + "Toggle App Bar": "Toggle App Bar", + "Work Order": "Work Order", + "Under active development": "Under active development 🚧", + "Sealos Copilot": "Sealos Copilot" +} \ No newline at end of file diff --git a/frontend/desktop/public/locales/zh/common.json b/frontend/desktop/public/locales/zh/common.json index 22a7a0b8a6d..c463ef4d60f 100644 --- a/frontend/desktop/public/locales/zh/common.json +++ b/frontend/desktop/public/locales/zh/common.json @@ -10,7 +10,7 @@ "Password": "密码", "Log In": "登录", "Loading": "加载中", - "Log Out": "退出账号", + "Log Out": "登出", "From": "来自", "Balance": "余额", "verify code tips": "6位验证码", @@ -78,7 +78,7 @@ "Added": "已加入", "Invaild Context": "你需要切换到其他工作空间操作", "Remove Member Tips": "确认要移除该成员?", - "Quit Workspace Tips":"确认要退出工作空间吗?", + "Quit Workspace Tips": "确认要退出工作空间吗?", "Invalid User ID": "用户的 ID 不合法", "The invited user must be others": "只能邀请其他人", "Dissovle Tips": "删除工作空间会清空所有资源,确定要删除吗?", @@ -124,5 +124,26 @@ "Year": "年", "Core": "核", "Yuan": "元", - "Description": "描述" -} + "Description": "描述", + "All Apps": "所有应用", + "Account Settings": "账户设置", + "Alerts": "告警", + "Monitor": "监控", + "Used Resources": "已用资源", + "Memory": "内存", + "Storage": "存储", + "Flow": "流量", + "Healthy Pod": "健康 Pod: {{count}}", + "Alarm Pod": "告警 Pod: {{count}}", + "Expected used": "预计还能使用", + "Used last month": "上月已使用", + "Expected to use next month": "下月预计使用", + "Day": "天", + "Search Apps": "搜索应用", + "No Apps Found": "未找到任何应​​用", + "Switching Disc": "切换圆盘", + "Toggle App Bar": "切换应用栏", + "Work Order": "工单", + "Under active development": "正在积极开发中 🚧", + "Sealos Copilot": "Sealos 小助理" +} \ No newline at end of file diff --git a/frontend/desktop/src/api/platform.ts b/frontend/desktop/src/api/platform.ts index 35dac9e942b..c88b4273fdd 100644 --- a/frontend/desktop/src/api/platform.ts +++ b/frontend/desktop/src/api/platform.ts @@ -78,3 +78,26 @@ export const getWechatResult = (payload: { code: string }) => export const getGlobalNotification = () => { return request.get>('/api/notification/global'); }; + +export const getResource = () => { + return request.get< + any, + ApiResp<{ + totalCpu: string; + totalMemory: string; + totalStorage: string; + runningPodCount: string; + totalPodCount: string; + }> + >('/api/desktop/getResource'); +}; + +export const getUserBilling = () => { + return request.post< + any, + ApiResp<{ + prevMonthTime: number; + prevDayTime: number; + }> + >('/api/desktop/getBilling'); +}; diff --git a/frontend/desktop/src/components/AppDock/index.module.css b/frontend/desktop/src/components/AppDock/index.module.css new file mode 100644 index 00000000000..f79511bc9cd --- /dev/null +++ b/frontend/desktop/src/components/AppDock/index.module.css @@ -0,0 +1,23 @@ +.contexify { + background: rgba(239, 239, 242, 0.7); + backdrop-filter: blur(80px) saturate(150%); + color: #111824; + padding: 6px; + font-size: 12px; + top: 0; + left: 0; + min-width: 140px; +} + +.arrow { + position: absolute; + top: 100%; + left: 46%; + width: 0; + height: 0; + width: 16px; + height: 9px; + background: rgba(239, 239, 242, 0.7); + backdrop-filter: blur(80px) saturate(150%); + clip-path: polygon(50% 100%, 0 0, 100% 0); +} diff --git a/frontend/desktop/src/components/AppDock/index.tsx b/frontend/desktop/src/components/AppDock/index.tsx new file mode 100644 index 00000000000..b8098855a9e --- /dev/null +++ b/frontend/desktop/src/components/AppDock/index.tsx @@ -0,0 +1,223 @@ +import { MoreAppsContext } from '@/pages/index'; +import useAppStore, { AppInfo } from '@/stores/app'; +import { useConfigStore } from '@/stores/config'; +import { useDesktopConfigStore } from '@/stores/desktopConfig'; +import { APPTYPE, TApp } from '@/types'; +import { Box, Center, Flex, Image } from '@chakra-ui/react'; +import { MouseEvent, useContext, useMemo, useState } from 'react'; +import { Menu, useContextMenu } from 'react-contexify'; +import { ChevronDownIcon } from '../icons'; +import styles from './index.module.css'; +import { useTranslation } from 'next-i18next'; + +const APP_DOCK_MENU_ID = 'APP_DOCK_MENU_ID'; + +export default function AppDock() { + const { t } = useTranslation(); + const { + installedApps: apps, + runningInfo, + setToHighestLayerById, + currentAppPid, + openApp, + switchAppById, + findAppInfoById, + updateOpenedAppInfo + } = useAppStore(); + const logo = useConfigStore().layoutConfig?.logo; + const moreAppsContent = useContext(MoreAppsContext); + const [isNavbarVisible, setNavbarVisible] = useState(true); + const { show } = useContextMenu({ + id: APP_DOCK_MENU_ID + }); + const { toggleShape } = useDesktopConfigStore(); + const normalApps = apps.filter((item: TApp) => item?.displayType === 'normal'); + + const AppMenuLists = useMemo(() => { + const initialApps: TApp[] = [ + { + name: 'home', + icon: '/icons/home.svg', + zIndex: 99999, + isShow: true, + pid: -9, + size: 'maxmin', + cacheSize: 'maxmin', + style: {}, + mouseDowning: false, + key: `system-sealos-home`, + type: APPTYPE.IFRAME, + data: { + url: '', + desc: '' + }, + displayType: 'hidden' + }, + ...normalApps.slice(0, 5).map((app, index) => ({ ...app, pid: -2 })) + ]; + + const mergedApps = initialApps.map((app) => { + const runningApp = runningInfo.find((running) => running.key === app.key); + return runningApp ? { ...app, ...runningApp } : app; + }); + + return [ + ...mergedApps, + ...runningInfo.filter((running) => !initialApps.some((app) => app.key === running.key)) + ]; + }, [normalApps, runningInfo]); + + // Handle icon click event + const handleNavItem = (e: MouseEvent, item: AppInfo) => { + if (item.key === 'system-sealos-home') { + const isNotMinimized = runningInfo.some((item) => item.size !== 'minimize'); + runningInfo.forEach((item) => { + updateOpenedAppInfo({ + ...item, + size: isNotMinimized ? 'minimize' : item.cacheSize + }); + }); + return; + } + + if (item.key === 'system-sealos-apps') { + moreAppsContent?.setShowMoreApps(true); + return; + } + + if (item.pid === currentAppPid && item.size !== 'minimize') { + updateOpenedAppInfo({ + ...item, + size: 'minimize', + cacheSize: item.size + }); + } else { + const app = findAppInfoById(item.pid); + if (!app) { + openApp(item); + } else { + switchAppById(item.pid); + } + } + }; + + const displayMenu = (e: MouseEvent) => { + show({ + event: e, + position: { + // @ts-ignore + x: '60px', + // @ts-ignore + y: '-114px' + } + }); + }; + + const transitionValue = 'transform 200ms ease-in-out, opacity 200ms ease-in-out'; + + return ( + +
setNavbarVisible((prev) => !prev)} + > + +
+ displayMenu(e)} + borderRadius="12px" + border={'1px solid rgba(255, 255, 255, 0.07)'} + bg="rgba(220, 220, 224, 0.3)" + backdropFilter="blur(80px) saturate(150%)" + boxShadow={ + '0px 0px 20px -4px rgba(12, 26, 67, 0.25), 0px 0px 1px 0px rgba(24, 43, 100, 0.25)' + } + minW={'326px'} + w={'auto'} + gap={'12px'} + userSelect={'none'} + px={'12px'} + transition={transitionValue} + opacity={isNavbarVisible ? 1 : 0} + position="absolute" + top={'-64px'} + transform={isNavbarVisible ? 'translate(-50%, 0)' : 'translate(-50%, 68px)'} + will-change="transform, opacity" + overflow="hidden" + > + {AppMenuLists.map((item: AppInfo, index: number) => { + return ( + handleNavItem(e, item)} + > +
+ {item?.name} +
+ +
+ ); + })} +
+ + + <> + + {t('Switching Disc')} + +
+ +
+ + ); +} diff --git a/frontend/desktop/src/components/LangSelect/simple.tsx b/frontend/desktop/src/components/LangSelect/simple.tsx index 75cd047114a..2ff8f543f41 100644 --- a/frontend/desktop/src/components/LangSelect/simple.tsx +++ b/frontend/desktop/src/components/LangSelect/simple.tsx @@ -10,7 +10,6 @@ export default function LangSelectSimple(props: FlexProps) { return ( s.openApp); + const installApp = useAppStore((s) => s.installedApps); + const { session } = useSessionStore(); + const user = session?.user; + const isLargerThanXl = useBreakpointValue({ base: true, xl: false }); + + const { data } = useQuery({ + queryKey: ['getAmount', { userId: user?.userCrUid }], + queryFn: () => + request>( + '/api/account/getAmount' + ), + enabled: !!user + }); + + const { data: billing, isSuccess } = useQuery(['getUserBilling'], () => getUserBilling(), { + cacheTime: 5 * 60 * 1000 + }); + + const balance = useMemo(() => { + let realBalance = new Decimal(data?.data?.balance || 0); + if (data?.data?.deductionBalance) { + realBalance = realBalance.minus(new Decimal(data.data.deductionBalance)); + } + return realBalance.toNumber(); + }, [data]); + + const calculations = useMemo(() => { + const prevDayAmount = new Decimal(billing?.data?.prevDayTime || 0); + const estimatedNextMonthAmount = prevDayAmount.times(30).toNumber(); + const _balance = new Decimal(balance || 0); + + const estimatedDaysUsable = prevDayAmount.greaterThan(0) + ? _balance.div(prevDayAmount).ceil().toNumber() + : Number.POSITIVE_INFINITY; + + return { + prevMonthAmount: new Decimal(billing?.data?.prevMonthTime || 0).toNumber(), + estimatedNextMonthAmount, + estimatedDaysUsable + }; + }, [billing?.data?.prevDayTime, billing?.data?.prevMonthTime, , balance]); + + return ( + + + + + + {t('Balance')} + + + + {formatMoney(balance).toFixed(2)} + + + + + {rechargeEnabled && ( +
{ + const costcenter = installApp.find((t) => t.key === 'system-costcenter'); + if (!costcenter) return; + openApp(costcenter, { + query: { + openRecharge: 'true' + } + }); + }} + color={'rgba(255, 255, 255, 0.90)'} + cursor={'pointer'} + > + {t('Charge')} +
+ )} +
+ {calculations && ( + + + + + {t('Expected used')} + + + {calculations.estimatedDaysUsable === Number.POSITIVE_INFINITY ? ( + <> + {t('Day')} + + ) : ( + <> + {calculations.estimatedDaysUsable} {t('Day')} + + )} + + + +
+ + {t('Used last month')} + + + {formatMoney(calculations.prevMonthAmount).toFixed(2)} + + +
+ +
+ + {t('Expected to use next month')} + + + {formatMoney(calculations.estimatedNextMonthAmount).toFixed(2)} + + +
+
+ )} +
+ + {isLargerThanXl && ( + + + + + + {t('Monitor')} + + + + + + + + + + )} + + {/* */} +
+ ); +} diff --git a/frontend/desktop/src/components/account/github.tsx b/frontend/desktop/src/components/account/github.tsx new file mode 100644 index 00000000000..efb82ab222a --- /dev/null +++ b/frontend/desktop/src/components/account/github.tsx @@ -0,0 +1,39 @@ +import { useConfigStore } from '@/stores/config'; +import { Flex, FlexProps, Icon, Tooltip } from '@chakra-ui/react'; +import { useQuery } from '@tanstack/react-query'; + +export default function GithubComponent(props: FlexProps) { + // const { data } = useQuery( + // ['getGithubStar'], + // () => fetch('https://api.github.com/repos/labring/sealos').then((res) => res.json()), + // { + // staleTime: 24 * 60 * 60 * 1000 + // } + // ); + + return ( + window.open('https://github.com/labring/sealos')} + > + + + + + ); +} diff --git a/frontend/desktop/src/components/account/index.tsx b/frontend/desktop/src/components/account/index.tsx index 1a4b99c3c88..5088fc8949f 100644 --- a/frontend/desktop/src/components/account/index.tsx +++ b/frontend/desktop/src/components/account/index.tsx @@ -1,58 +1,59 @@ +import Notification from '@/components/notification'; import { useCopyData } from '@/hooks/useCopyData'; -import request from '@/services/request'; +import { useConfigStore } from '@/stores/config'; import useSessionStore from '@/stores/session'; import download from '@/utils/downloadFIle'; +import { Box, Center, Flex, IconButton, Image, Text, useDisclosure } from '@chakra-ui/react'; import { - Box, - Flex, - Image, - Stack, - Text, - type UseDisclosureReturn, - IconButton, - HStack, - VStack -} from '@chakra-ui/react'; -import { useQuery, useQueryClient } from '@tanstack/react-query'; + CopyIcon, + DocsIcon, + DownloadIcon, + LogoutIcon, + NotificationIcon, + SettingIcon +} from '@sealos/ui'; +import { useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'next-i18next'; import { useRouter } from 'next/router'; -import { useMemo, useState } from 'react'; -import useAppStore from '@/stores/app'; -import { ApiResp } from '@/types'; -import { formatMoney } from '@/utils/format'; +import { useCallback, useState } from 'react'; +import LangSelectSimple from '../LangSelect/simple'; +import { blurBackgroundStyles } from '../desktop_content'; +import RegionToggle from '../region/RegionToggle'; +import WorkspaceToggle from '../team/WorkspaceToggle'; import PasswordModify from './PasswordModify'; -import { CopyIcon, DownloadIcon, LogoutIcon } from '@sealos/ui'; -import { useConfigStore } from '@/stores/config'; +import GithubComponent from './github'; +import { ArrowIcon } from '../icons'; +import useAppStore from '@/stores/app'; + +const baseItemStyle = { + w: '52px', + h: '40px', + background: 'rgba(255, 255, 255, 0.07)', + color: 'white', + borderRadius: '100px', + _hover: { + background: 'rgba(255, 255, 255, 0.15)' + } +}; -export default function Account({ disclosure }: { disclosure: UseDisclosureReturn }) { +export default function Account() { + const { layoutConfig } = useConfigStore(); const [showId, setShowId] = useState(true); const passwordEnabled = useConfigStore().authConfig?.idp?.password?.enabled; - const rechargeEnabled = useConfigStore().commonConfig?.rechargeEnabled; - const logo = useConfigStore().layoutConfig?.logo; + const router = useRouter(); const { copyData } = useCopyData(); - const openApp = useAppStore((s) => s.openApp); - const installApp = useAppStore((s) => s.installedApps); const { t } = useTranslation(); const { delSession, session, setToken } = useSessionStore(); const user = session?.user; - const { data } = useQuery({ - queryKey: ['getAmount', { userId: user?.userCrUid }], - queryFn: () => - request>( - '/api/account/getAmount' - ), - enabled: !!user - }); - const balance = useMemo(() => { - let real_balance = data?.data?.balance || 0; - if (data?.data?.deductionBalance) { - real_balance -= data?.data.deductionBalance; - } - return real_balance; - }, [data]); const queryclient = useQueryClient(); const kubeconfig = session?.kubeconfig || ''; + const showDisclosure = useDisclosure(); + const [notificationAmount, setNotificationAmount] = useState(0); + const { installedApps, openApp } = useAppStore(); + + const onAmount = useCallback((amount: number) => setNotificationAmount(amount), []); + const logout = (e: React.MouseEvent) => { e.preventDefault(); delSession(); @@ -61,145 +62,193 @@ export default function Account({ disclosure }: { disclosure: UseDisclosureRetur setToken(''); }; - return disclosure.isOpen ? ( - <> - - - - - - {t('Log Out')} - - - - user avator - - {user?.name} - - - { + const workorder = installedApps.find((t) => t.key === 'system-workorder'); + if (!workorder) return; + openApp(workorder); + }; + + return ( + + + +
+ user avator +
+ + + {user?.name} + + setShowId((s) => !s)} + gap="2px" + fontSize={'11px'} + lineHeight={'16px'} + fontWeight={'500'} + color={'rgba(255, 255, 255, 0.70)'} + alignItems={'center'} > - {showId ? `ID: ${user?.userId}` : `NS: ${user?.nsid}`} + setShowId((s) => !s)}> + {showId ? `ID:${user?.userId}` : `NS:${user?.nsid}`} + + { + if (user?.userId && user.nsid) copyData(showId ? user?.userId : user?.nsid); + }} + boxSize={'12px'} + fill={'rgba(255, 255, 255, 0.70)'} + /> + + +
+ + + {t('Log Out')} +
+
+ +
window.open(layoutConfig?.common?.docsUrl)} + > + +
+ + {layoutConfig?.common.githubStarEnabled && } +
showDisclosure.onOpen()}> + +
+ +
+ + + + + + {layoutConfig?.common.accountSettingEnabled && ( + + {t('Account Settings')} { - if (user?.userId && user.nsid) copyData(showId ? user?.userId : user?.nsid); - }} - icon={} - aria-label={'copy nsid'} + // onClick={() => kubeconfig && copyData(kubeconfig)} + icon={} + aria-label={'setting'} /> -
- - - {passwordEnabled && ( - - {t('changePassword')} - - - )} - - - {t('Balance')}: {formatMoney(balance).toFixed(2)} - - {rechargeEnabled && ( - { - const costcenter = installApp.find((t) => t.key === 'system-costcenter'); - if (!costcenter) return; - openApp(costcenter, { - query: { - openRecharge: 'true' - } - }); - disclosure.onClose(); - }} - _hover={{ - bgColor: 'rgba(0, 0, 0, 0.03)' - }} - transition={'0.3s'} - p="4px" - color={'#219BF4'} - fontWeight="500" - fontSize="12px" - cursor={'pointer'} - > - {t('Charge')} - - )} - - { - - kubeconfig + + )} + {layoutConfig?.common.workorderEnabled && ( + + {t('Work Order')} + } + aria-label={'setting'} + /> + + )} - kubeconfig && download('kubeconfig.yaml', kubeconfig)} - icon={} - aria-label={'Download kc'} - /> - kubeconfig && copyData(kubeconfig)} - icon={} - aria-label={'copy kc'} - /> -
+ + kubeconfig + + kubeconfig && download('kubeconfig.yaml', kubeconfig)} + icon={ + } - - + aria-label={'Download kc'} + /> + kubeconfig && copyData(kubeconfig)} + icon={} + aria-label={'copy kc'} + /> + -
- - ) : ( - <> + {/* {passwordEnabled && ( + + {t('changePassword')} + + + )} */} +
+ + ); } diff --git a/frontend/desktop/src/components/account/trigger.tsx b/frontend/desktop/src/components/account/trigger.tsx new file mode 100644 index 00000000000..7dcb15335b9 --- /dev/null +++ b/frontend/desktop/src/components/account/trigger.tsx @@ -0,0 +1,46 @@ +import { useConfigStore } from '@/stores/config'; +import useSessionStore from '@/stores/session'; +import { Center, Flex, Image } from '@chakra-ui/react'; +import { Dispatch, SetStateAction } from 'react'; + +export default function Trigger({ + setShowAccount, + showAccount +}: { + showAccount: boolean; + setShowAccount: Dispatch>; +}) { + const user = useSessionStore((state) => state.session)?.user; + const logo = useConfigStore().layoutConfig?.logo; + + return ( + { + setShowAccount(true); + }} + cursor={'pointer'} + > +
+ user avator +
+
+ ); +} diff --git a/frontend/desktop/src/components/desktop_content/ChakraIndicator.tsx b/frontend/desktop/src/components/desktop_content/ChakraIndicator.tsx new file mode 100644 index 00000000000..e15b604cf35 --- /dev/null +++ b/frontend/desktop/src/components/desktop_content/ChakraIndicator.tsx @@ -0,0 +1,34 @@ +import { useBreakpointValue, Center } from '@chakra-ui/react'; + +export function ChakraIndicator() { + const breakpoint = useBreakpointValue({ + base: 'base 0', + xs: 'xs 375', + sm: 'sm 640', + md: 'md 768', + lg: 'lg 1024', + xl: 'xl 1280', + '2xl': '2xl 1440' + }); + + if (process.env.NODE_ENV === 'production') { + return null; + } + + return ( +
+ {breakpoint} +
+ ); +} diff --git a/frontend/desktop/src/components/desktop_content/apps.tsx b/frontend/desktop/src/components/desktop_content/apps.tsx new file mode 100644 index 00000000000..4e35b8edc7b --- /dev/null +++ b/frontend/desktop/src/components/desktop_content/apps.tsx @@ -0,0 +1,187 @@ +import useAppStore from '@/stores/app'; +import { useConfigStore } from '@/stores/config'; +import { TApp } from '@/types'; +import { Box, Button, Flex, Grid, HStack, Image, Text, useBreakpointValue } from '@chakra-ui/react'; +import { throttle } from 'lodash'; +import { useTranslation } from 'next-i18next'; +import { MouseEvent, useCallback, useEffect, useMemo, useState } from 'react'; +import { ArrowLeftIcon, ArrowRightIcon, DesktopSealosCoinIcon } from '../icons'; +import { blurBackgroundStyles } from './index'; +import { validateNumber } from '@/utils/tools'; + +export default function Apps() { + const { t, i18n } = useTranslation(); + const { installedApps: renderApps, openApp } = useAppStore(); + const logo = useConfigStore().layoutConfig?.logo || '/logo.svg'; + + const [pageSize, setPageSize] = useState(10); + const [page, setPage] = useState(1); + + // grid value + const gridMX = useBreakpointValue({ base: 32, lg: 48 }) || 32; + const gridMT = 32; + const gridSpacing = 36; + const appWidth = 80; + const appHeight = 86; + const pageButton = 12; + + const handleDoubleClick = (e: MouseEvent, item: TApp) => { + e.preventDefault(); + if (item?.name) { + openApp(item); + } + }; + + // eslint-disable-next-line react-hooks/exhaustive-deps + const calculateMaxAppsPerPage = useCallback( + throttle(() => { + const appsContainer = document.getElementById('apps-container'); + if (appsContainer) { + const gridWidth = appsContainer.offsetWidth - gridMX * 2 - pageButton * 2; + const gridHeight = appsContainer.offsetHeight - gridMT + gridSpacing; + + const maxAppsInRow = Math.floor((gridWidth + gridSpacing) / (appWidth + gridSpacing)); + const maxAppsInColumn = Math.floor(gridHeight / (appHeight + gridSpacing)); + + const maxApps = maxAppsInRow * maxAppsInColumn; + + setPage(1); + setPageSize(maxApps); + } + }, 100), + [gridMX] + ); + + useEffect(() => { + calculateMaxAppsPerPage(); + window.addEventListener('resize', calculateMaxAppsPerPage); + return () => { + window.removeEventListener('resize', calculateMaxAppsPerPage); + }; + }, [calculateMaxAppsPerPage, gridMX]); + + const paginatedApps = useMemo( + () => renderApps.slice((page - 1) * pageSize, page * pageSize), + [renderApps, page, pageSize] + ); + + const totalPages = useMemo(() => { + const renderAppsLength = renderApps.length; + const validRenderAppsLength = validateNumber(renderAppsLength) ? renderAppsLength : 1; + const validPageSize = validateNumber(pageSize) ? pageSize : 1; + + return Math.ceil(validRenderAppsLength / validPageSize) || 1; + }, [renderApps.length, pageSize]); + + return ( + + + {t('All Apps')} + + + {totalPages !== 1 && ( + + )} + + {paginatedApps && + paginatedApps.map((item: TApp, index) => ( + handleDoubleClick(e, item)} + > + + app logo + + + {item?.i18n?.[i18n?.language]?.name + ? item?.i18n?.[i18n?.language]?.name + : t(item?.name)} + + + ))} + + {totalPages !== 1 && ( + + )} + + + {Array.from({ length: totalPages }, (_, index) => ( + + ))} + + + ); +} diff --git a/frontend/desktop/src/components/desktop_content/assistant.tsx b/frontend/desktop/src/components/desktop_content/assistant.tsx new file mode 100644 index 00000000000..f3e2798664a --- /dev/null +++ b/frontend/desktop/src/components/desktop_content/assistant.tsx @@ -0,0 +1,53 @@ +import { Flex, Icon, Text } from '@chakra-ui/react'; +import { useMessage } from '@sealos/ui'; +import { useTranslation } from 'next-i18next'; + +export default function Assistant() { + const { t } = useTranslation(); + const { message } = useMessage(); + + return ( + { + message({ + title: t('Under active development') + }); + }} + > + + + + + 🤖 + + + {t('Sealos Copilot')} + + ); +} diff --git a/frontend/desktop/src/components/desktop_content/index.tsx b/frontend/desktop/src/components/desktop_content/index.tsx index 3245d53cecc..1cd481f3cef 100644 --- a/frontend/desktop/src/components/desktop_content/index.tsx +++ b/frontend/desktop/src/components/desktop_content/index.tsx @@ -1,32 +1,47 @@ +import { getGlobalNotification } from '@/api/platform'; import AppWindow from '@/components/app_window'; -import MoreButton from '@/components/more_button'; -import UserMenu from '@/components/user_menu'; -import useDriver from '@/hooks/useDriver'; +// import useDriver from '@/hooks/useDriver'; import useAppStore from '@/stores/app'; +import { useConfigStore } from '@/stores/config'; import { TApp, WindowSize } from '@/types'; -import { Box, Flex, Grid, GridItem, Image, Text } from '@chakra-ui/react'; +import { Box, Center, Flex, Text } from '@chakra-ui/react'; +import { WarnTriangleIcon, useMessage } from '@sealos/ui'; +import { useQuery } from '@tanstack/react-query'; import { useTranslation } from 'next-i18next'; import dynamic from 'next/dynamic'; import { MouseEvent, useCallback, useEffect, useState } from 'react'; import { createMasterAPP, masterApp } from 'sealos-desktop-sdk/master'; +import Cost from '../account/cost'; +import TriggerAccountModule from '../account/trigger'; +import { ChakraIndicator } from './ChakraIndicator'; +import Apps from './apps'; +import Assistant from './assistant'; import IframeWindow from './iframe_window'; import styles from './index.module.scss'; -import { useQuery } from '@tanstack/react-query'; -import { getGlobalNotification } from '@/api/platform'; -import { useMessage } from '@sealos/ui'; -import { useConfigStore } from '@/stores/config'; -const TimeComponent = dynamic(() => import('./time'), { - ssr: false -}); +import Monitor from './monitor'; +import SearchBox from './searchBox'; +import { EmptyIcon } from '../icons'; +import { useDesktopConfigStore } from '@/stores/desktopConfig'; +import Warn from './warn'; + +const AppDock = dynamic(() => import('../AppDock'), { ssr: false }); +const FloatButton = dynamic(() => import('@/components/floating_button'), { ssr: false }); +const Account = dynamic(() => import('../account'), { ssr: false }); + +export const blurBackgroundStyles = { + bg: 'rgba(22, 30, 40, 0.35)', + backdropFilter: 'blur(80px) saturate(150%)', + border: 'none', + borderRadius: '12px' +}; -export default function DesktopContent(props: any) { +export default function Desktop(props: any) { const { t, i18n } = useTranslation(); + const { isAppBar, toggleShape } = useDesktopConfigStore(); const { installedApps: apps, runningInfo, openApp, setToHighestLayerById } = useAppStore(); const backgroundImage = useConfigStore().layoutConfig?.backgroundImage; - const logo = useConfigStore().layoutConfig?.logo; - const renderApps = apps.filter((item: TApp) => item?.displayType === 'normal'); - const [maxItems, setMaxItems] = useState(10); const { message } = useMessage(); + const [showAccount, setShowAccount] = useState(false); const handleDoubleClick = (e: MouseEvent, item: TApp) => { e.preventDefault(); @@ -88,7 +103,7 @@ export default function DesktopContent(props: any) { return masterApp?.addEventListen('openDesktopApp', openDesktopApp); }, [openDesktopApp]); - const { UserGuide, showGuide } = useDriver({ openDesktopApp }); + // const { UserGuide, showGuide } = useDriver({ openDesktopApp }); useQuery(['getGlobalNotification'], getGlobalNotification, { onSuccess(data) { @@ -114,12 +129,78 @@ export default function DesktopContent(props: any) { backgroundImage={`url(${backgroundImage || '/images/bg-blue.jpg'})`} backgroundRepeat={'no-repeat'} backgroundSize={'cover'} + position={'relative'} > - - - + + + {/* monitor */} + + + + + + + {/* apps */} + + + + + + + + + + + + {/* user account */} + + {showAccount && ( + { + e.stopPropagation(); + setShowAccount(false); + }} + > + )} + + + + - {showGuide ? ( + + {/* {showGuide ? ( <> ) : ( <> - )} - {/* desktop apps */} - - {renderApps && - renderApps.slice(0, maxItems).map((item: TApp, index) => ( - handleDoubleClick(e, item)} - > - - user avator - - - {item?.i18n?.[i18n?.language]?.name - ? item?.i18n?.[i18n?.language]?.name - : t(item?.name)} - - - ))} - - - + )} */} + + {isAppBar ? : } + {/* opened apps */} {runningInfo.map((process) => { return ( diff --git a/frontend/desktop/src/components/desktop_content/monitor.tsx b/frontend/desktop/src/components/desktop_content/monitor.tsx new file mode 100644 index 00000000000..4876320f17d --- /dev/null +++ b/frontend/desktop/src/components/desktop_content/monitor.tsx @@ -0,0 +1,122 @@ +import { getResource } from '@/api/platform'; +import { Box, CircularProgress, CircularProgressLabel, Flex, Text } from '@chakra-ui/react'; +import { MonitorIcon } from '@sealos/ui'; +import { useQuery } from '@tanstack/react-query'; +import { useTranslation } from 'next-i18next'; +import { CpuIcon, FlowIcon, MemoryIcon, StorageIcon } from '../icons'; +import { blurBackgroundStyles } from './index'; + +export default function Monitor({ needStyles = true }: { needStyles?: boolean }) { + const { t } = useTranslation(); + const { data } = useQuery(['appListQuery'], getResource, { + cacheTime: 5 * 60 * 1000 + }); + + const info = [ + { + label: 'CPU', + value: data?.data?.totalCpu, + icon: , + unit: 'C' + }, + { + label: t('Memory'), + value: data?.data?.totalMemory, + icon: , + unit: 'GB' + }, + { + label: t('Storage'), + value: data?.data?.totalStorage, + icon: , + unit: 'GB' + }, + { + label: t('Flow'), + value: `~`, + icon: , + unit: 'GB' + } + ]; + + const totalPodCount = Number(data?.data?.totalPodCount) || 0; + const runningPodCount = Number(data?.data?.runningPodCount) || 0; + const runningPodPercentage = + totalPodCount > 0 ? Math.round((runningPodCount / totalPodCount) * 100) : 0; + + return ( + + {needStyles && ( + + + + {t('Monitor')} + + + )} + + + + + {runningPodPercentage}% + + + + + + {t('Healthy Pod', { count: runningPodCount })} + + + + {t('Alarm Pod', { count: totalPodCount - runningPodCount })} + + + + + {t('Used Resources')} + + + {info.map((item) => ( + + + {item.icon} + + {item.label} + + + + {item.value} + + {item.unit} + + + + ))} + + + ); +} diff --git a/frontend/desktop/src/components/desktop_content/searchBox.tsx b/frontend/desktop/src/components/desktop_content/searchBox.tsx new file mode 100644 index 00000000000..caba7042e99 --- /dev/null +++ b/frontend/desktop/src/components/desktop_content/searchBox.tsx @@ -0,0 +1,147 @@ +import useAppStore from '@/stores/app'; +import { useConfigStore } from '@/stores/config'; +import { TApp } from '@/types'; +import { Box, Center, Flex, Image, Input } from '@chakra-ui/react'; +import { SearchIcon } from '@sealos/ui'; +import { useTranslation } from 'next-i18next'; +import { useRef, useState } from 'react'; +import { blurBackgroundStyles } from './index'; + +export default function SearchBox() { + const { t, i18n } = useTranslation(); + const logo = useConfigStore().layoutConfig?.logo; + const { installedApps: apps, runningInfo, openApp, setToHighestLayerById } = useAppStore(); + const inputRef = useRef(null); + const [searchTerm, setSearchTerm] = useState(''); + + const getAppNames = (app: TApp) => { + const names = [app.name]; + if (app.i18n) { + Object.values(app.i18n).forEach((i18nData) => { + if (i18nData.name) { + names.push(i18nData.name); + } + }); + } + return names; + }; + + // Filter apps based on search term + const filteredApps = apps.filter((app) => { + const appNames = getAppNames(app); + return appNames.some((name) => name.toLowerCase().includes(searchTerm.toLowerCase())); + }); + + return ( + { + inputRef.current?.focus(); + }} + cursor={'pointer'} + position={'relative'} + > + + + setSearchTerm(e.target.value)} + w={'full'} + outline={'none'} + type="text" + placeholder={t('Search Apps') || 'Search Apps'} + bg={'transparent'} + outlineOffset={''} + border={'none'} + _placeholder={{ color: 'white' }} + boxShadow={'none'} + _hover={{ + bg: 'transparent' + }} + _focus={{ + bg: 'transparent', + color: 'white', + border: 'none', + boxShadow: 'none' + }} + /> + + {searchTerm !== '' && ( + + {filteredApps.length > 0 ? ( + filteredApps.map((app) => ( + { + openApp(app); + setSearchTerm(''); + }} + display={'flex'} + gap={'10px'} + fontSize={'12px'} + fontWeight={500} + color={'rgba(255, 255, 255, 0.90)'} + > +
+ app logo +
+ + {app?.i18n?.[i18n?.language]?.name + ? app?.i18n?.[i18n?.language]?.name + : t(app?.name)} +
+ )) + ) : ( + + {t('No Apps Found') || 'No Apps Found'} + + )} +
+ )} +
+ ); +} diff --git a/frontend/desktop/src/components/desktop_content/time.tsx b/frontend/desktop/src/components/desktop_content/time.tsx index a475170b310..35b75c01ad9 100644 --- a/frontend/desktop/src/components/desktop_content/time.tsx +++ b/frontend/desktop/src/components/desktop_content/time.tsx @@ -1,7 +1,7 @@ import { formatTime } from '@/utils/tools'; import { Flex, Text } from '@chakra-ui/react'; import { useTranslation } from 'next-i18next'; -import { useEffect, useLayoutEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; const WeekDay = { Sunday: '周日', @@ -25,9 +25,7 @@ export default function TimeComponent(props: any) { clearInterval(timer); }; }, []); - // useLayoutEffect(()=>{ - // setTime(new Date()) - // },[]) + const day = useMemo(() => { try { const temp = formatTime(time, 'dddd') as keyof typeof WeekDay; diff --git a/frontend/desktop/src/components/desktop_content/warn.tsx b/frontend/desktop/src/components/desktop_content/warn.tsx new file mode 100644 index 00000000000..722b27a0154 --- /dev/null +++ b/frontend/desktop/src/components/desktop_content/warn.tsx @@ -0,0 +1,38 @@ +import { Flex, Center, Text } from '@chakra-ui/react'; +import { WarnTriangleIcon } from '@sealos/ui'; +import { EmptyIcon } from '../icons'; +import { blurBackgroundStyles } from './index'; +import { useTranslation } from 'next-i18next'; + +export default function Warn() { + const { t } = useTranslation(); + return ( + + + + + + {t('Alerts')} + + +
+
+ +
+
+
+
+ ); +} diff --git a/frontend/desktop/src/components/floating_button/index.module.scss b/frontend/desktop/src/components/floating_button/index.module.scss index 7097029af07..191cb7ea52e 100644 --- a/frontend/desktop/src/components/floating_button/index.module.scss +++ b/frontend/desktop/src/components/floating_button/index.module.scss @@ -156,3 +156,28 @@ left: 50px; } } + +// .contexify { +// background: rgba(239, 239, 242, 0.7); +// backdrop-filter: blur(80px) saturate(150%); +// color: #111824; +// padding: 6px; +// font-size: 12px; +// top: 0; +// left: 0; +// min-width: 140px; +// } + +// .arrow { +// position: absolute; +// top: 100%; +// left: 46%; +// width: 0; +// height: 0; +// width: 16px; +// height: 9px; +// background: rgba(239, 239, 242, 0.7); +// backdrop-filter: blur(80px) saturate(150%); +// clip-path: polygon(50% 100%, 0 0, 100% 0); +// border-radius: 4px; +// } diff --git a/frontend/desktop/src/components/floating_button/index.tsx b/frontend/desktop/src/components/floating_button/index.tsx index f8f31349038..532f012e9b4 100644 --- a/frontend/desktop/src/components/floating_button/index.tsx +++ b/frontend/desktop/src/components/floating_button/index.tsx @@ -1,13 +1,16 @@ import useAppStore, { AppInfo } from '@/stores/app'; -import { Box, Flex, useDisclosure, Image, Img } from '@chakra-ui/react'; +import { useConfigStore } from '@/stores/config'; +import { useDesktopConfigStore } from '@/stores/desktopConfig'; +import { APPTYPE } from '@/types'; +import { Box, Flex, Image, useDisclosure } from '@chakra-ui/react'; import clsx from 'clsx'; import { MouseEvent, useMemo, useState } from 'react'; +import { Menu, useContextMenu } from 'react-contexify'; import Draggable, { DraggableEventHandler } from 'react-draggable'; import Iconfont from '../iconfont'; import styles from './index.module.scss'; -import homeIcon from 'public/icons/home.svg'; -import { APPTYPE } from '@/types'; -import { useConfigStore } from '@/stores/config'; +import dockStyles from '@/components/AppDock/index.module.css'; +import { useTranslation } from 'next-i18next'; enum Suction { None, @@ -15,7 +18,10 @@ enum Suction { Right = 'right' } -export default function Index(props: any) { +const Floating_Button_Menu_Id = 'Floating_Button_Menu_Id'; + +export default function FloatingButton() { + const { t } = useTranslation(); const { installedApps: apps, runningInfo, @@ -31,6 +37,12 @@ export default function Index(props: any) { const [endPosition, setEndPosition] = useState({ x: 0, y: 0 }); const [suction, setSuction] = useState(Suction.None); const [lockSuction, setLockSuction] = useState(true); + const { isAppBar, toggleShape } = useDesktopConfigStore(); + const { show } = useContextMenu({ + id: Floating_Button_Menu_Id + }); + const [isRightClick, setIsRightClick] = useState(false); + // Hover Ball Menu const AppMenuLists: AppInfo[] = [ { @@ -152,6 +164,21 @@ export default function Index(props: any) { } }; + const displayMenu = (e: MouseEvent) => { + e.stopPropagation(); + setIsRightClick(true); + onClose(); + show({ + event: e, + position: { + // @ts-ignore + x: '-65%', + // @ts-ignore + y: '-80%' + } + }); + }; + return ( <>
displayMenu(e)} id="floatButtonNav" className={clsx(styles.container, 'floatButtonNav', dragging ? styles.notrans : '')} data-open={isOpen} onMouseEnter={(e) => { if (suction !== Suction.None) return; + if (isRightClick) return; onOpen(); }} onMouseLeave={(e) => { @@ -283,6 +312,22 @@ export default function Index(props: any) { height={24} /> + + <> + + {t('Toggle App Bar')} +
+
+ +
{dragging && ( diff --git a/frontend/desktop/src/components/icons/index.tsx b/frontend/desktop/src/components/icons/index.tsx new file mode 100644 index 00000000000..b399bfd1c86 --- /dev/null +++ b/frontend/desktop/src/components/icons/index.tsx @@ -0,0 +1,357 @@ +import { Icon, IconProps } from '@chakra-ui/react'; + +export function ArrowLeftIcon(props: IconProps) { + return ( + + + + ); +} + +export function ArrowRightIcon(props: IconProps) { + return ( + + + + ); +} + +export function CpuIcon(props: IconProps) { + return ( + + + + + + + + + + + ); +} + +export function MemoryIcon(props: IconProps) { + return ( + + + + + + ); +} + +export function FlowIcon(props: IconProps) { + return ( + + + + + + + + + + + ); +} + +export function StorageIcon(props: IconProps) { + return ( + + + + + + + + + + + ); +} + +export function ClockIcon(props: IconProps) { + return ( + + + + ); +} + +export function DesktopSealosCoinIcon(props: IconProps) { + return ( + + + + + + + + + + + + + ); +} + +export function DesktopExchangeIcon(props: IconProps) { + return ( + + + + + ); +} + +export function InfiniteIcon(props: IconProps) { + return ( + + + + ); +} + +export function CubeIcon(props: IconProps) { + return ( + + + + + + ); +} + +export function ZoneIcon(props: IconProps) { + return ( + + + + ); +} + +export function ChevronDownIcon(props: IconProps) { + return ( + + + + ); +} + +export function EmptyIcon(props: IconProps) { + return ( + + + + + ); +} + +export function ArrowIcon(props: IconProps) { + return ( + + + + ); +} diff --git a/frontend/desktop/src/components/notification/index.module.scss b/frontend/desktop/src/components/notification/index.module.scss index 6180ce00d6a..bb1ac0ae8fe 100644 --- a/frontend/desktop/src/components/notification/index.module.scss +++ b/frontend/desktop/src/components/notification/index.module.scss @@ -10,8 +10,9 @@ .container { width: 360px; height: 524px; - background: rgba(255, 255, 255, 0.7); - backdrop-filter: blur(150px); + background: rgba(220, 220, 224, 0.05); + box-shadow: 0px 15px 20px 0px rgba(0, 0, 0, 0.1); + backdrop-filter: blur(50px); border-radius: 12px; padding: 24px; z-index: 999; @@ -24,7 +25,7 @@ .title { font-weight: 500; font-size: 18px; - color: #0d1a2d; + color: white; } } @@ -56,7 +57,7 @@ .tab { font-weight: 400; font-size: 12px; - color: #717d8a; + color: rgba(255, 255, 255, 0.6); border-bottom: 2px solid transparent; cursor: pointer; } @@ -64,7 +65,7 @@ .active { font-weight: 500; font-size: 12px; - color: #0d1a2d; + color: white; border-bottom: 2px solid #1599e9; } diff --git a/frontend/desktop/src/components/notification/index.tsx b/frontend/desktop/src/components/notification/index.tsx index 9f8feeccb0d..fac65496ec7 100644 --- a/frontend/desktop/src/components/notification/index.tsx +++ b/frontend/desktop/src/components/notification/index.tsx @@ -36,15 +36,19 @@ export default function Notification(props: TNotification) { popupMessage: undefined }); - const { refetch } = useQuery(['getNotifications'], () => request('/api/notification/list'), { - onSuccess: (data) => { - const messages = data?.data?.items as NotificationItem[]; - if (messages) { - handleNotificationData(messages); - } - }, - refetchInterval: 5 * 60 * 1000 - }); + const { refetch } = useQuery( + ['getNotifications'], + () => request('/api/notification/listNotification'), + { + onSuccess: (data) => { + const messages = data?.data?.items as NotificationItem[]; + if (messages) { + handleNotificationData(messages); + } + }, + refetchInterval: 5 * 60 * 1000 + } + ); const handleNotificationData = (data: NotificationItem[]) => { const parseIsRead = (item: NotificationItem) => @@ -193,12 +197,11 @@ export default function Notification(props: TNotification) { ml={'auto'} onClick={() => markAllAsRead()} variant={'white-bg-icon'} - leftIcon={} + leftIcon={} iconSpacing="4px" + borderRadius={'4px'} > - - {t('Read All')} - + {t('Read All')}
@@ -317,28 +320,33 @@ export default function Notification(props: TNotification) { h={'170px'} top={'48px'} right={'0px'} - bg="rgba(255, 255, 255, 0.80)" + bg="rgba(220, 220, 224, 0.05)" backdropFilter={'blur(50px)'} boxShadow={'0px 15px 20px 0px rgba(0, 0, 0, 0.10)'} borderRadius={'12px 0px 12px 12px'} p="20px" + zIndex={9} + color={'white'} > - + {i18n.language === 'zh' && MessageConfig.popupMessage?.spec?.i18ns?.zh?.title ? MessageConfig.popupMessage?.spec?.i18ns?.zh?.title : MessageConfig.popupMessage?.spec?.title} { + const temp = MessageConfig.popupMessage; setMessageConfig( produce((draft) => { draft.popupMessage = undefined; }) ); + readMsgMutation.mutate([temp?.metadata?.name || '']); }} /> @@ -347,7 +355,6 @@ export default function Notification(props: TNotification) { mt="14px" fontSize="12px" fontWeight={400} - color="#000000" className="overflow-auto" noOfLines={2} height={'36px'} @@ -361,13 +368,12 @@ export default function Notification(props: TNotification) {