feat:desktop & applaunchpad novice guide (#4392)

* add package @sealos/driver

* account

* done

* fix

* fix

* fix appcr
This commit is contained in:
zhujingyang
2023-12-08 16:32:36 +08:00
committed by GitHub
parent c6c1b58b0f
commit 174f08fb93
63 changed files with 3722 additions and 127 deletions
+1 -1
View File
@@ -15,7 +15,7 @@ const nextConfig = withPWA({
reactStrictMode: false,
swcMinify: isProduction,
output: 'standalone',
transpilePackages: ['@sealos/ui', 'sealos-desktop-sdk'],
transpilePackages: ['@sealos/ui', 'sealos-desktop-sdk', '@sealos/driver'],
experimental: {
outputFileTracingRoot: path.join(__dirname, '../')
}
+1
View File
@@ -25,6 +25,7 @@
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@kubernetes/client-node": "^0.18.1",
"@sealos/driver": "workspace:^",
"@sealos/ui": "workspace:^",
"@tanstack/react-query": "^4.35.3",
"axios": "^1.5.1",
@@ -0,0 +1,10 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.74999 1.08801C4.0696 1.08801 4.32869 1.34711 4.32869 1.66672V3.17135H5.83332C6.15293 3.17135 6.41203 3.43044 6.41203 3.75005C6.41203 4.06966 6.15293 4.32875 5.83332 4.32875H4.32869V5.83338C4.32869 6.15299 4.0696 6.41209 3.74999 6.41209C3.43038 6.41209 3.17128 6.15299 3.17128 5.83338V4.32875H1.66666C1.34705 4.32875 1.08795 4.06966 1.08795 3.75005C1.08795 3.43044 1.34705 3.17135 1.66666 3.17135H3.17128V1.66672C3.17128 1.34711 3.43038 1.08801 3.74999 1.08801ZM10.8333 1.92135C11.0728 1.92135 11.2875 2.06882 11.3735 2.29231L12.8186 6.04969C13.0643 6.68839 13.1548 6.91199 13.2853 7.0956C13.4098 7.27064 13.5627 7.42357 13.7378 7.54803C13.9214 7.67859 14.145 7.76912 14.7837 8.01477L18.5411 9.45992C18.7646 9.54588 18.912 9.7606 18.912 10C18.912 10.2395 18.7646 10.4542 18.5411 10.5402L14.7837 11.9853C14.145 12.231 13.9214 12.3215 13.7378 12.4521C13.5627 12.5765 13.4098 12.7295 13.2853 12.9045C13.1548 13.0881 13.0643 13.3117 12.8186 13.9504L11.3735 17.7078C11.2875 17.9313 11.0728 18.0788 10.8333 18.0788C10.5939 18.0788 10.3792 17.9313 10.2932 17.7078L8.84804 13.9504C8.60239 13.3117 8.51186 13.0881 8.3813 12.9045C8.25684 12.7295 8.10391 12.5765 7.92887 12.4521C7.74526 12.3215 7.52166 12.231 6.88297 11.9853L3.12558 10.5402C2.90209 10.4542 2.75462 10.2395 2.75462 10C2.75462 9.7606 2.90209 9.54588 3.12558 9.45992L6.88296 8.01477C7.52166 7.76912 7.74526 7.67859 7.92887 7.54803C8.10391 7.42357 8.25684 7.27064 8.3813 7.0956C8.51186 6.91199 8.60239 6.68839 8.84804 6.04969L10.2932 2.29231C10.3791 2.06882 10.5939 1.92135 10.8333 1.92135ZM10.8333 4.11213L9.9283 6.46518C9.91949 6.48809 9.9108 6.51071 9.90222 6.53303C9.69245 7.07888 9.55023 7.44893 9.32456 7.76631C9.12512 8.04679 8.88006 8.29185 8.59958 8.49129C8.28221 8.71696 7.91216 8.85917 7.36631 9.06895C7.34398 9.07753 7.32137 9.08622 7.29845 9.09503L4.9454 10L7.29845 10.9051C7.32137 10.9139 7.34398 10.9226 7.36631 10.9312C7.91216 11.1409 8.28221 11.2831 8.59958 11.5088C8.88006 11.7083 9.12512 11.9533 9.32456 12.2338C9.55023 12.5512 9.69245 12.9212 9.90222 13.4671C9.9108 13.4894 9.91949 13.512 9.9283 13.5349L10.8333 15.888L11.7383 13.5349C11.7472 13.512 11.7558 13.4894 11.7644 13.4671C11.9742 12.9212 12.1164 12.5512 12.3421 12.2338C12.5415 11.9533 12.7866 11.7083 13.0671 11.5088C13.3844 11.2831 13.7545 11.1409 14.3003 10.9311C14.3227 10.9226 14.3453 10.9139 14.3682 10.9051L16.7212 10L14.3682 9.09503C14.3453 9.08622 14.3227 9.07753 14.3004 9.06896C13.7545 8.85918 13.3844 8.71696 13.0671 8.49129C12.7866 8.29185 12.5415 8.04679 12.3421 7.76631C12.1164 7.44894 11.9742 7.07889 11.7644 6.53304C11.7558 6.51071 11.7472 6.4881 11.7383 6.46518L10.8333 4.11213ZM3.74999 13.588C4.0696 13.588 4.32869 13.8471 4.32869 14.1667V15.6713H5.83332C6.15293 15.6713 6.41203 15.9304 6.41203 16.25C6.41203 16.5697 6.15293 16.8288 5.83332 16.8288H4.32869V18.3334C4.32869 18.653 4.0696 18.9121 3.74999 18.9121C3.43038 18.9121 3.17128 18.653 3.17128 18.3334V16.8288H1.66666C1.34705 16.8288 1.08795 16.5697 1.08795 16.25C1.08795 15.9304 1.34705 15.6713 1.66666 15.6713H3.17128V14.1667C3.17128 13.8471 3.43038 13.588 3.74999 13.588Z" fill="url(#paint0_linear_124_3715)"/>
<defs>
<linearGradient id="paint0_linear_124_3715" x1="9.99999" y1="1.66672" x2="9.99999" y2="18.3334" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFC3A4"/>
<stop offset="0.432292" stop-color="#FE8BA7"/>
<stop offset="1" stop-color="#8695FF"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 817 KiB

+17 -3
View File
@@ -100,7 +100,21 @@
"Purchase Link Error": "Purchase Link Error",
"You have not purchased the License": "You have not purchased the License",
"App Info": "App Info",
"Click anywhere to continue": "Click on any blank space to continue",
"Jump Over": "Jump Over",
"Detail": "Detail"
}
"Detail": "Detail",
"Hello, welcome": "Hello, welcome to",
"Quick application switching floating ball": "Quick application switching floating ball",
"You can use the kubectl command directly from the terminal": "You can use the kubectl command directly from the terminal",
"Help you enable high availability database": "Help you enable high availability database",
"Launch various third-party applications with one click": "Launch various third-party applications with one click",
"You can view fees through the fee center": "You can view fees through the fee center",
"deploy an application": "Lets deploy an application~",
"spend": "spend",
"Completed the deployment of an nginx for the first time": "Completed the deployment of an nginx for the first time",
"Next time": "Next",
"start immediately": "Start",
"Click on any shadow to skip": "Click on any shadow to skip",
"Start your Sealos journey": "Start your Sealos journey",
"gift amount": "Reward {{amount}} balance.",
"Recharge Amount": "Recharge Amount"
}
+15 -4
View File
@@ -24,8 +24,6 @@
"confirmNewPassword": "确认新密码",
"passwordMismatch": "密码不一致",
"passwordChangeSuccess": "密码修改成功",
"passwordMismatch": "密码不一致",
"newPassword": "新密码",
"Verify password": "确认密码",
"change": "修改",
"Invalid username or password": "用户名或密码错误",
@@ -96,7 +94,20 @@
"Purchase Link Error": "购买链接错误",
"You have not purchased the License": "您还没有购买 License",
"App Info": "应用信息",
"Click anywhere to continue": "点击任意空白继续",
"Jump Over": "跳过",
"Detail": "详情"
"Detail": "详情",
"Hello, welcome": "您好, 欢迎来到",
"Quick application switching floating ball": "快捷应用切换悬浮球",
"You can use the kubectl command directly from the terminal": "您可通过终端直接使用 kubectl 命令",
"Help you enable high availability database": "帮您启用高可用数据库",
"Launch various third-party applications with one click": "一键启动各种第三方应用",
"You can view fees through the fee center": "您可通过费用中心查看费用",
"deploy an application": "来部署一个应用吧~",
"spend": "花",
"Completed the deployment of an nginx for the first time": "部署一个 nginx ,首次完成 将",
"Next time": "下次吧",
"start immediately": "立即开始",
"Click on any shadow to skip": "点击任意阴影跳过",
"Start your Sealos journey": "开始您的 Sealos 之旅",
"gift amount": "赠送 {{amount}} 余额."
}
+25
View File
@@ -1,4 +1,6 @@
import request from '@/services/request';
import { ApiResp } from '@/types';
import { AccountCRD } from '@/types/user';
// handle baidu
export const uploadConvertData = (newType: number[], url?: string) => {
@@ -14,3 +16,26 @@ export const uploadConvertData = (newType: number[], url?: string) => {
main_url
});
};
export const updateDesktopGuide = () => {
return request.post('/api/account/updateGuide');
};
export const getUserAccount = () => {
return request.get<AccountCRD>('/api/account/getAccount');
};
export const getSystemEnv = () => {
return request.get('/api/platform/getEnv');
};
export const getPriceBonus = () => {
return request.get<
any,
ApiResp<{
steps: string;
ratios: string;
activities: string;
}>
>('/api/price/bonus');
};
@@ -1,19 +1,20 @@
import AppWindow from '@/components/app_window';
import MoreButton from '@/components/more_button';
import UserMenu from '@/components/user_menu';
import useAppStore from '@/stores/app';
import { TApp } from '@/types';
import { Box, Flex, Grid, GridItem, Image, Text } from '@chakra-ui/react';
import { Box, Flex, Grid, GridItem, Image, Text, Icon, FlexProps, Button } from '@chakra-ui/react';
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 IframeWindow from './iframe_window';
import styles from './index.module.scss';
import useDriver from '@/hooks/useDriver';
const TimeComponent = dynamic(() => import('./time'), {
ssr: false
});
import UserMenu from '@/components/user_menu';
export default function DesktopContent(props: any) {
const { t, i18n } = useTranslation();
@@ -66,6 +67,8 @@ export default function DesktopContent(props: any) {
return masterApp?.addEventListen('openDesktopApp', openDesktopApp);
}, [openDesktopApp]);
const { UserGuide, showGuide } = useDriver({ openDesktopApp });
return (
<Box
id="desktop"
@@ -78,6 +81,22 @@ export default function DesktopContent(props: any) {
<Box mt="12vh" minW={'508px'}>
<TimeComponent />
</Box>
{showGuide ? (
<>
<UserGuide />
<Box
position="fixed"
top="0"
left="0"
width="100%"
height="100%"
backgroundColor="rgba(0, 0, 0, 0.7)" // 半透明黑色背景
zIndex="11000" // 保证蒙层在最上层
/>
</>
) : (
<></>
)}
{/* desktop apps */}
<Grid
mt="50px"
@@ -98,6 +117,7 @@ export default function DesktopContent(props: any) {
onClick={(e) => handleDoubleClick(e, item)}
>
<Box
className={item.key}
w="72px"
h="72px"
p={'12px'}
@@ -132,7 +152,6 @@ export default function DesktopContent(props: any) {
<MoreButton />
<UserMenu />
</Flex>
{/* opened apps */}
{runningInfo.map((process) => {
return (
@@ -169,7 +169,7 @@ export default function Index(props: any) {
>
<div
id="floatButtonNav"
className={clsx(styles.container, dragging ? styles.notrans : '')}
className={clsx(styles.container, 'floatButtonNav', dragging ? styles.notrans : '')}
data-open={isOpen}
onMouseEnter={(e) => {
if (suction !== Suction.None) return;
@@ -0,0 +1 @@
export const GUIDE_DESKTOP_INDEX_KEY = 'frontend.guide.desktop.index';
+343
View File
@@ -0,0 +1,343 @@
import { getPriceBonus, getSystemEnv, getUserAccount, updateDesktopGuide } from '@/api/platform';
import { GUIDE_DESKTOP_INDEX_KEY } from '@/constants/account';
import { formatMoney } from '@/utils/format';
import { Box, Button, Flex, FlexProps, Icon, Image, Text } from '@chakra-ui/react';
import { driver } from '@sealos/driver';
import { useTranslation } from 'next-i18next';
import { useEffect, useState } from 'react';
export function DriverStarIcon() {
return (
<Icon
width="20px"
height="20px"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3.74999 1.08801C4.0696 1.08801 4.32869 1.34711 4.32869 1.66672V3.17135H5.83332C6.15293 3.17135 6.41203 3.43044 6.41203 3.75005C6.41203 4.06966 6.15293 4.32875 5.83332 4.32875H4.32869V5.83338C4.32869 6.15299 4.0696 6.41209 3.74999 6.41209C3.43038 6.41209 3.17128 6.15299 3.17128 5.83338V4.32875H1.66666C1.34705 4.32875 1.08795 4.06966 1.08795 3.75005C1.08795 3.43044 1.34705 3.17135 1.66666 3.17135H3.17128V1.66672C3.17128 1.34711 3.43038 1.08801 3.74999 1.08801ZM10.8333 1.92135C11.0728 1.92135 11.2875 2.06882 11.3735 2.29231L12.8186 6.04969C13.0643 6.68839 13.1548 6.91199 13.2853 7.0956C13.4098 7.27064 13.5627 7.42357 13.7378 7.54803C13.9214 7.67859 14.145 7.76912 14.7837 8.01477L18.5411 9.45992C18.7646 9.54588 18.912 9.7606 18.912 10C18.912 10.2395 18.7646 10.4542 18.5411 10.5402L14.7837 11.9853C14.145 12.231 13.9214 12.3215 13.7378 12.4521C13.5627 12.5765 13.4098 12.7295 13.2853 12.9045C13.1548 13.0881 13.0643 13.3117 12.8186 13.9504L11.3735 17.7078C11.2875 17.9313 11.0728 18.0788 10.8333 18.0788C10.5939 18.0788 10.3792 17.9313 10.2932 17.7078L8.84804 13.9504C8.60239 13.3117 8.51186 13.0881 8.3813 12.9045C8.25684 12.7295 8.10391 12.5765 7.92887 12.4521C7.74526 12.3215 7.52166 12.231 6.88297 11.9853L3.12558 10.5402C2.90209 10.4542 2.75462 10.2395 2.75462 10C2.75462 9.7606 2.90209 9.54588 3.12558 9.45992L6.88296 8.01477C7.52166 7.76912 7.74526 7.67859 7.92887 7.54803C8.10391 7.42357 8.25684 7.27064 8.3813 7.0956C8.51186 6.91199 8.60239 6.68839 8.84804 6.04969L10.2932 2.29231C10.3791 2.06882 10.5939 1.92135 10.8333 1.92135ZM10.8333 4.11213L9.9283 6.46518C9.91949 6.48809 9.9108 6.51071 9.90222 6.53303C9.69245 7.07888 9.55023 7.44893 9.32456 7.76631C9.12512 8.04679 8.88006 8.29185 8.59958 8.49129C8.28221 8.71696 7.91216 8.85917 7.36631 9.06895C7.34398 9.07753 7.32137 9.08622 7.29845 9.09503L4.9454 10L7.29845 10.9051C7.32137 10.9139 7.34398 10.9226 7.36631 10.9312C7.91216 11.1409 8.28221 11.2831 8.59958 11.5088C8.88006 11.7083 9.12512 11.9533 9.32456 12.2338C9.55023 12.5512 9.69245 12.9212 9.90222 13.4671C9.9108 13.4894 9.91949 13.512 9.9283 13.5349L10.8333 15.888L11.7383 13.5349C11.7472 13.512 11.7558 13.4894 11.7644 13.4671C11.9742 12.9212 12.1164 12.5512 12.3421 12.2338C12.5415 11.9533 12.7866 11.7083 13.0671 11.5088C13.3844 11.2831 13.7545 11.1409 14.3003 10.9311C14.3227 10.9226 14.3453 10.9139 14.3682 10.9051L16.7212 10L14.3682 9.09503C14.3453 9.08622 14.3227 9.07753 14.3004 9.06896C13.7545 8.85918 13.3844 8.71696 13.0671 8.49129C12.7866 8.29185 12.5415 8.04679 12.3421 7.76631C12.1164 7.44894 11.9742 7.07889 11.7644 6.53304C11.7558 6.51071 11.7472 6.4881 11.7383 6.46518L10.8333 4.11213ZM3.74999 13.588C4.0696 13.588 4.32869 13.8471 4.32869 14.1667V15.6713H5.83332C6.15293 15.6713 6.41203 15.9304 6.41203 16.25C6.41203 16.5697 6.15293 16.8288 5.83332 16.8288H4.32869V18.3334C4.32869 18.653 4.0696 18.9121 3.74999 18.9121C3.43038 18.9121 3.17128 18.653 3.17128 18.3334V16.8288H1.66666C1.34705 16.8288 1.08795 16.5697 1.08795 16.25C1.08795 15.9304 1.34705 15.6713 1.66666 15.6713H3.17128V14.1667C3.17128 13.8471 3.43038 13.588 3.74999 13.588Z"
fill="url(#paint0_linear_124_3715)"
/>
<defs>
<linearGradient
id="paint0_linear_124_3715"
x1="9.99999"
y1="1.66672"
x2="9.99999"
y2="18.3334"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#FFC3A4" />
<stop offset="0.432292" stopColor="#FE8BA7" />
<stop offset="1" stopColor="#8695FF" />
</linearGradient>
</defs>
</Icon>
);
}
export default function useDriver({ openDesktopApp }: { openDesktopApp: any }) {
const { t, i18n } = useTranslation();
const [showGuide, setShowGuide] = useState(false);
const [giftAmount, setGiftAmount] = useState(8);
const handleSkipGuide = () => {
console.log('handleSkipGuide');
setShowGuide(false);
updateDesktopGuide().catch((err) => {
console.log(err);
});
};
useEffect(() => {
const handleUserGuide = async () => {
try {
const { data: env } = await getSystemEnv();
const { data } = await getUserAccount();
const bonus = await getPriceBonus();
if (bonus.data?.activities) {
const strategy = JSON.parse(bonus.data?.activities);
const rewardBalance = formatMoney(
strategy?.['beginner-guide']?.phases?.launchpad?.giveAmount || 8000000
);
setGiftAmount(rewardBalance);
}
if (env?.guideEnabled && data?.metadata?.annotations) {
const isGuidedDesktop = !!data.metadata.annotations?.[GUIDE_DESKTOP_INDEX_KEY];
!isGuidedDesktop ? setShowGuide(true) : '';
}
} catch (error) {
console.log(error);
}
};
handleUserGuide();
}, []);
const PopoverBodyInfo = (props: FlexProps) => (
<Flex
gap="4px"
id="popover-info"
position={'absolute'}
bottom={'-30px'}
color={'white'}
alignItems={'center'}
{...props}
>
<Text color={'#FFF'} fontSize={'13px'} fontWeight={500}>
{t('Click on any shadow to skip')}
</Text>
<Icon
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 20 20"
fill="white"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M7.50001 0.97229C7.88354 0.97229 8.19445 1.2832 8.19445 1.66673V2.91673C8.19445 3.30027 7.88354 3.61118 7.50001 3.61118C7.11648 3.61118 6.80556 3.30027 6.80556 2.91673V1.66673C6.80556 1.2832 7.11648 0.97229 7.50001 0.97229ZM2.84229 2.84235C3.11349 2.57116 3.55319 2.57116 3.82439 2.84235L4.70827 3.72624C4.97947 3.99744 4.97947 4.43713 4.70827 4.70833C4.43707 4.97953 3.99737 4.97953 3.72618 4.70833L2.84229 3.82445C2.5711 3.55325 2.5711 3.11355 2.84229 2.84235ZM12.2083 2.84235C12.4795 3.11355 12.4795 3.55325 12.2083 3.82445L11.3244 4.70833C11.0532 4.97953 10.6135 4.97953 10.3423 4.70833C10.0711 4.43713 10.0711 3.99744 10.3423 3.72624L11.2262 2.84235C11.4974 2.57116 11.9371 2.57116 12.2083 2.84235ZM8.05107 6.36654C8.05921 6.36904 8.06735 6.37155 8.07548 6.37406L17.4584 9.26437C17.6747 9.33097 17.8794 9.394 18.0383 9.45881C18.183 9.51788 18.4465 9.63567 18.6129 9.8969C18.8034 10.1959 18.8398 10.5678 18.7109 10.8981C18.5983 11.1866 18.3627 11.3532 18.2321 11.4393C18.0888 11.5336 17.9002 11.6352 17.7009 11.7425L13.8279 13.828L11.7424 17.701C11.6351 17.9003 11.5336 18.0889 11.4392 18.2322C11.3531 18.3628 11.1865 18.5984 10.898 18.711C10.5677 18.8399 10.1958 18.8035 9.8968 18.613C9.63557 18.4466 9.51779 18.1831 9.45872 18.0384C9.39391 17.8795 9.33088 17.6748 9.26429 17.4586L6.37399 8.07554C6.37148 8.0674 6.36897 8.05926 6.36647 8.05112C6.31285 7.87717 6.25951 7.70409 6.2288 7.55823C6.19822 7.413 6.15971 7.17003 6.25365 6.91402C6.36609 6.60762 6.60756 6.36615 6.91396 6.25371C7.16997 6.15977 7.41294 6.19829 7.55817 6.22886C7.70404 6.25957 7.87712 6.31292 8.05107 6.36654ZM7.71679 7.71686L10.564 16.9599L12.609 13.1621C12.6112 13.158 12.6136 13.1535 12.6162 13.1486C12.6422 13.0999 12.6884 13.0133 12.7511 12.9339C12.8047 12.8661 12.866 12.8048 12.9339 12.7512C13.0132 12.6884 13.0999 12.6422 13.1485 12.6163C13.1535 12.6137 13.158 12.6112 13.1621 12.609L16.9598 10.5641L7.71679 7.71686ZM0.972229 7.50007C0.972229 7.11654 1.28314 6.80562 1.66667 6.80562H2.91667C3.3002 6.80562 3.61112 7.11654 3.61112 7.50007C3.61112 7.8836 3.3002 8.19451 2.91667 8.19451H1.66667C1.28314 8.19451 0.972229 7.8836 0.972229 7.50007ZM4.70827 10.3424C4.97947 10.6136 4.97947 11.0532 4.70827 11.3244L3.82439 12.2083C3.55319 12.4795 3.11349 12.4795 2.84229 12.2083C2.5711 11.9371 2.5711 11.4974 2.84229 11.2262L3.72618 10.3424C3.99737 10.0712 4.43707 10.0712 4.70827 10.3424Z"
/>
</Icon>
</Flex>
);
const driverObj = driver({
showProgress: false,
allowClose: false,
allowClickMaskNextStep: true,
allowPreviousStep: true,
isShowButtons: false,
allowKeyboardControl: false,
disableActiveInteraction: true,
steps: [
{
element: '.floatButtonNav',
popover: {
side: 'left',
align: 'center',
borderRadius: '12px 12px 0px 12px',
PopoverBody: (
<Flex gap={'6px'}>
<DriverStarIcon />
<Text color={'#24282C'} fontSize={'13px'}>
{t('Quick application switching floating ball')}
</Text>
<PopoverBodyInfo />
</Flex>
)
}
},
{
element: '.system-terminal',
popover: {
side: 'bottom',
align: 'start',
borderRadius: '0px 12px 12px 12px',
PopoverBody: (
<Flex gap={'6px'}>
<DriverStarIcon />
<Text color={'#24282C'} fontSize={'13px'}>
{t('You can use the kubectl command directly from the terminal')}
</Text>
<PopoverBodyInfo />
</Flex>
)
}
},
{
element: '.system-dbprovider',
popover: {
side: 'bottom',
align: 'start',
borderRadius: '0px 12px 12px 12px',
PopoverBody: (
<Flex gap={'6px'}>
<DriverStarIcon />
<Text color={'#24282C'} fontSize={'13px'}>
{t('Help you enable high availability database')}
</Text>
<PopoverBodyInfo />
</Flex>
)
}
},
{
element: '.system-template',
popover: {
side: 'left',
align: 'center',
borderRadius: '12px 12px 0px 12px',
PopoverBody: (
<Flex gap={'6px'}>
<DriverStarIcon />
<Text color={'#24282C'} fontSize={'13px'}>
{t('Launch various third-party applications with one click')}
</Text>
<PopoverBodyInfo top={'-120px'} />
</Flex>
)
}
},
{
element: '.system-costcenter',
popover: {
side: 'top',
align: 'start',
borderRadius: '12px 12px 12px 0px',
PopoverBody: (
<Flex gap={'6px'}>
<DriverStarIcon />
<Text color={'#24282C'} fontSize={'13px'}>
{t('You can view fees through the fee center')}
</Text>
<PopoverBodyInfo top={'-120px'} />
</Flex>
)
}
},
{
element: '.system-applaunchpad',
popover: {
side: 'left',
align: 'start',
borderRadius: '12px 0px 12px 12px',
onPopoverRender: () => {
const svg = driverObj.getState('__overlaySvg');
if (svg) {
const pathElement = svg.querySelector('path');
if (pathElement) {
pathElement.style.pointerEvents = 'none';
}
}
},
PopoverBody: (
<Box color={'#24282C'} padding={'10px'}>
<Flex gap={'6px'}>
<DriverStarIcon />
<Text fontSize={'16px'} fontWeight={'600'}>
{t('deploy an application')}
</Text>
</Flex>
<Box fontSize={'13px'} fontWeight={500} mt="12px">
{t('spend')}
<Text color={'#219BF4'} display={'inline'} px="2px">
30s
</Text>
{t('Completed the deployment of an nginx for the first time')}
<Text color={'#219BF4'} display={'inline'} px="2px">
{t('gift amount', { amount: giftAmount })}
</Text>
</Box>
<Flex mt="20px">
<Button
color={'#24282C'}
fontSize={'12px'}
p="4px 16px"
border={'1px solid #DEE0E2'}
bg="#FFF"
ml="auto"
borderRadius={'4px'}
cursor={'pointer'}
onClick={() => {
driverObj.destroy();
}}
>
{t('Next time')}
</Button>
<Button
ml="12px"
color={'#FEFEFE'}
fontSize={'12px'}
p="4px 16px"
bg="#000"
borderRadius={'4px'}
cursor={'pointer'}
onClick={() => {
driverObj.destroy();
openDesktopApp({ appKey: 'system-applaunchpad', pathname: '/app/edit' });
}}
>
{t('start immediately')}
</Button>
</Flex>
</Box>
)
}
}
],
onDestroyed: () => {
console.log('onDestroyed');
handleSkipGuide();
}
});
const startGuide = () => {
setShowGuide(false);
driverObj.drive();
};
const UserGuide = () => (
<Box
zIndex={11100}
position="fixed"
top="50%"
left="50%"
transform="translate(-50%, -50%)"
bg={'linear-gradient(180deg, #FFFDFD 9.04%, #F6F7FE 46.15%, #FAFAFF 87.5%)'}
borderRadius={'12px'}
>
<Image src="/images/driver-bg.png" alt="driver" draggable="false" userSelect={'none'} />
<Flex
flexDirection={'column'}
justifyContent={'center'}
alignItems={'center'}
position={'absolute'}
top="50%"
left="50%"
transform="translate(-50%, -50%)"
>
<Box fontSize={'32px'} fontWeight={500}>
{t('Hello, welcome')}
<Text display={'inline'} color={'#0884DD'} px="8px">
Sealos
</Text>
👏
</Box>
<Button
w="206px"
mt="46px"
variant={'primary'}
bg="#121416"
rightIcon={
<Icon
xmlns="http://www.w3.org/2000/svg"
width="16px"
height="16px"
viewBox="0 0 16 16"
fill="none"
>
<path
d="M3.33331 8.00001H12.6666M12.6666 8.00001L7.99998 3.33334M12.6666 8.00001L7.99998 12.6667"
stroke="white"
strokeWidth="1.33333"
strokeLinecap="round"
strokeLinejoin="round"
/>
</Icon>
}
onClick={() => {
startGuide();
}}
>
{t('Start your Sealos journey')}
</Button>
</Flex>
</Box>
);
return { UserGuide, showGuide, startGuide };
}
+1
View File
@@ -8,6 +8,7 @@ import type { AppProps } from 'next/app';
import Router from 'next/router';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
import '@sealos/driver/src/driver.css';
import { useEffect } from 'react';
const queryClient = new QueryClient({
@@ -0,0 +1,26 @@
import { authSession } from '@/services/backend/auth';
import { GetCRD } from '@/services/backend/kubernetes/user';
import { jsonRes } from '@/services/backend/response';
import { CRDMeta } from '@/types';
import type { NextApiRequest, NextApiResponse } from 'next';
export const AccountMeta: CRDMeta = {
group: 'account.sealos.io',
version: 'v1',
namespace: 'sealos-system',
plural: 'accounts'
};
// req header is kubeconfig
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const payload = await authSession(req.headers);
if (!payload) return jsonRes(res, { code: 401, message: 'token is vaild' });
const result = await GetCRD(payload.kc, AccountMeta, payload.user.k8s_username);
jsonRes(res, { data: result?.body });
} catch (error) {
console.log(error);
jsonRes(res, { code: 500, data: error });
}
}
@@ -0,0 +1,36 @@
import { authSession } from '@/services/backend/auth';
import { K8sApiDefault } from '@/services/backend/kubernetes/admin';
import { UpdateCRD } from '@/services/backend/kubernetes/user';
import { jsonRes } from '@/services/backend/response';
import type { NextApiRequest, NextApiResponse } from 'next';
import { AccountMeta } from './getAccount';
import { GUIDE_DESKTOP_INDEX_KEY } from '@/constants/account';
// req header is kubeconfig
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const payload = await authSession(req.headers);
if (!payload) return jsonRes(res, { code: 401, message: 'token is vaild' });
const defaultKc = K8sApiDefault();
if (!defaultKc) return jsonRes(res, { code: 401, message: 'No cluster permissions' });
const endTime = new Date().toISOString();
const jsonPatch = [
{
op: 'add',
path: `/metadata/annotations/${GUIDE_DESKTOP_INDEX_KEY}`,
value: endTime
}
];
const reuslt = await UpdateCRD(defaultKc, AccountMeta, payload.user.k8s_username, jsonPatch);
jsonRes(res, { data: reuslt?.body });
} catch (error) {
console.log(error, 'get user account err');
jsonRes(res, { code: 500, data: error });
}
}
@@ -28,6 +28,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const wechatEnabledRecharge = enableWechatRecharge();
const licenseEnabled = enableLicense();
const rechargeEnabled = enableRecharge();
const guideEnabled = process.env.GUIDE_ENABLED === 'true';
jsonRes(res, {
data: {
SEALOS_CLOUD_DOMAIN: process.env.SEALOS_CLOUD_DOMAIN || 'cloud.sealos.io',
@@ -45,7 +47,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
stripeEnabled,
wechatEnabledRecharge,
rechargeEnabled,
licenseEnabled
licenseEnabled,
guideEnabled
}
});
}
@@ -1,7 +1,6 @@
import { authSession } from '@/services/backend/auth';
import { GetConfigMap } from '@/services/backend/kubernetes/user';
import { jsonRes } from '@/services/backend/response';
import { enableRecharge } from '@/services/enable';
import type { NextApiRequest, NextApiResponse } from 'next';
export default async function handler(req: NextApiRequest, resp: NextApiResponse) {
@@ -18,7 +17,8 @@ export default async function handler(req: NextApiRequest, resp: NextApiResponse
code: 200,
data: {
ratios: result.body.data.ratios,
steps: result.body.data.steps
steps: result.body.data.steps,
activities: result.body.data.activities
}
});
} catch (error) {
@@ -0,0 +1,26 @@
import { AccountMeta } from '@/pages/api/account/getAccount';
import { jsonRes } from '@/services/backend/response';
import type { NextApiRequest, NextApiResponse } from 'next';
import { initK8s } from 'sealos-desktop-sdk/service';
// req header is kubeconfig
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { k8sCustomObjects, kube_user } = await initK8s({
req
});
const { body } = await k8sCustomObjects.getNamespacedCustomObject(
AccountMeta.group,
AccountMeta.version,
AccountMeta.namespace,
AccountMeta.plural,
kube_user.name
);
jsonRes(res, { data: body });
} catch (error) {
console.log(error);
jsonRes(res, { code: 500, data: error });
}
}
@@ -0,0 +1,68 @@
import { K8sApiDefault } from '@/services/backend/kubernetes/admin';
import { UpdateCRD } from '@/services/backend/kubernetes/user';
import { jsonRes } from '@/services/backend/response';
import type { NextApiRequest, NextApiResponse } from 'next';
import { initK8s } from 'sealos-desktop-sdk/service';
import { AccountMeta } from '@/pages/api/account/getAccount';
export type UpdateUserGuideParams = {
activityType: 'beginner-guide';
phase: 'launchpad' | 'database' | 'template' | 'terminal';
phasePage: 'create' | 'detail' | 'index';
shouldSendGift: boolean;
};
// req header is kubeconfig
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const {
activityType,
phase,
phasePage,
shouldSendGift = false
} = req.body as UpdateUserGuideParams;
if (!activityType || !phase || !phasePage)
return jsonRes(res, { code: 400, message: 'Desktop Bad Request: Invalid parameters' });
const { namespace, kube_user, kc } = await initK8s({ req });
const defaultKc = K8sApiDefault();
if (!namespace) return jsonRes(res, { code: 401, message: 'kubeConfig is vaild' });
if (!defaultKc) return jsonRes(res, { code: 401, message: 'No cluster permissions' });
const endTime = new Date().toISOString();
const jsonPatch = shouldSendGift
? [
{
op: 'add',
path: `/metadata/annotations/activity.${activityType}.current-phase`,
value: phase
},
{
op: 'add',
path: `/metadata/annotations/activity.${activityType}.${phase}.endTime`,
value: endTime
},
{
op: 'add',
path: `/metadata/annotations/frontend.guide.${phase}.${phasePage}`,
value: endTime
}
]
: [
{
op: 'add',
path: `/metadata/annotations/frontend.guide.${phase}.${phasePage}`,
value: endTime
}
];
const reuslt = await UpdateCRD(defaultKc, AccountMeta, kube_user.name, jsonPatch);
console.log('updateDesktopGuide', jsonPatch, reuslt?.body);
jsonRes(res, { data: reuslt?.body });
} catch (error) {
console.log(error, 'get user account err');
jsonRes(res, { code: 500, data: error });
}
}
+1 -1
View File
@@ -47,7 +47,7 @@ export default function Home({ sealos_cloud_domain }: { sealos_cloud_domain: str
useEffect(() => {
const { query } = router;
const is_login = isUserLogin();
const whitelistApps = ['system-fastdeploy'];
const whitelistApps = ['system-template'];
if (!is_login) {
const { appkey, appQuery } = parseOpenappQuery((query?.openapp as string) || '');
// sealos_inside=true internal call
+4
View File
@@ -77,3 +77,7 @@ div {
}
}
}
#driver-popover-content {
padding: 14px;
}
+17
View File
@@ -97,3 +97,20 @@ export type TeamUserDto = UserDto & {
role: UserRole;
status: InvitedStatus;
};
export type AccountCRD = {
apiVersion: string;
kind: string;
metadata: {
annotations: Record<string, string>;
creationTimestamp: string;
generation: number;
};
status: {
activityBonus: number;
balance: number;
deductionBalance: number;
encryptBalance: string;
encryptDeductionBalance: string;
};
};
+27
View File
@@ -0,0 +1,27 @@
{
"name": "@sealos/driver",
"version": "1.0.0",
"description": "",
"main": "./src/driver.ts",
"src": "./src",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "next dev"
},
"keywords": [
"driver",
"tour",
"guide",
"overlay"
],
"author": "3161362058@qq.com",
"license": "ISC",
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "18.2.37",
"@types/react-dom": "18.0.11"
}
}
+88
View File
@@ -0,0 +1,88 @@
import { DriveStep } from './driver';
import { AllowedButtons, PopoverDOM } from './popover';
import { State } from './state';
export type DriverHook = (
element: Element | undefined,
step: DriveStep,
opts: { config: Config; state: State }
) => void;
export type Config = {
steps?: DriveStep[];
animate?: boolean;
overlayColor?: string;
overlayOpacity?: number;
smoothScroll?: boolean;
allowClose?: boolean;
stagePadding?: number;
stageRadius?: number;
disableActiveInteraction?: boolean;
allowKeyboardControl?: boolean;
// Popover specific configuration
popoverClass?: string;
popoverOffset?: number;
showButtons?: AllowedButtons[];
disableButtons?: AllowedButtons[];
showProgress?: boolean;
// Button texts
progressText?: string;
nextBtnText?: string;
prevBtnText?: string;
doneBtnText?: string;
// Called after the popover is rendered
onPopoverRender?: (popover: PopoverDOM, opts: { config: Config; state: State }) => void;
// State based callbacks, called upon state changes
onHighlightStarted?: DriverHook;
onHighlighted?: DriverHook;
onDeselected?: DriverHook;
onDestroyStarted?: DriverHook;
onDestroyed?: DriverHook;
// Event based callbacks, called upon events
onNextClick?: DriverHook;
onPrevClick?: DriverHook;
onCloseClick?: DriverHook;
// custom config
allowClickMaskNextStep?: boolean;
allowPreviousStep?: boolean;
isShowButtons?: boolean;
overlaySkipButton?: string;
interceptSkipButtonClick?: () => void;
};
let currentConfig: Config = {};
export function configure(config: Config = {}) {
currentConfig = {
animate: true,
allowClose: true,
overlayOpacity: 0.7,
smoothScroll: false,
disableActiveInteraction: false,
showProgress: false,
stagePadding: 10,
stageRadius: 5,
popoverOffset: 10,
showButtons: ['next', 'previous', 'close'],
disableButtons: [],
overlayColor: '#000',
allowPreviousStep: false,
isShowButtons: true,
...config
};
}
export function getConfig(): Config;
export function getConfig<K extends keyof Config>(key: K): Config[K];
export function getConfig<K extends keyof Config>(key?: K) {
return key ? currentConfig[key] : currentConfig;
}
+269
View File
@@ -0,0 +1,269 @@
.driver-active .driver-overlay {
pointer-events: none;
}
.driver-active * {
pointer-events: none;
}
.driver-active .driver-popover-overlay-skip-btn,
.driver-active .driver-active-element,
.driver-active .driver-active-element *,
.driver-popover,
.driver-popover * {
pointer-events: auto;
}
@keyframes animate-fade-in {
0% {
opacity: 0;
}
to {
opacity: 1;
}
}
.driver-fade .driver-overlay {
animation: animate-fade-in 200ms ease-in-out;
}
.driver-fade .driver-popover {
animation: animate-fade-in 200ms;
}
/* Popover styles */
.driver-popover {
all: unset;
box-sizing: border-box;
color: #2d2d2d;
margin: 0;
padding: 15px;
border-radius: 5px;
border: 1px solid #ffc1a4;
min-width: 250px;
max-width: 300px;
box-shadow: 0 1px 10px #0006;
z-index: 1000000000;
position: fixed;
top: 0;
right: 0;
background-color: #fff;
}
.driver-popover * {
font-family: 'Helvetica Neue', Inter, ui-sans-serif, 'Apple Color Emoji', Helvetica, Arial,
sans-serif;
}
.driver-popover-title {
font: 19px / normal sans-serif;
font-weight: 700;
display: block;
position: relative;
line-height: 1.5;
zoom: 1;
margin: 0;
}
.driver-popover-close-btn {
all: unset;
position: absolute;
top: 0;
right: 0;
width: 32px;
height: 28px;
cursor: pointer;
font-size: 18px;
font-weight: 500;
color: #d2d2d2;
z-index: 1;
text-align: center;
transition: color;
transition-duration: 200ms;
}
.driver-popover-close-btn:hover,
.driver-popover-close-btn:focus {
color: #2d2d2d;
}
.driver-popover-title[style*='block'] + .driver-popover-description {
margin-top: 5px;
}
.driver-popover-description {
margin-bottom: 0;
font: 14px / normal sans-serif;
line-height: 1.4;
font-weight: 400;
color: #24282c;
zoom: 1;
}
.driver-popover-footer {
margin-top: 15px;
text-align: right;
zoom: 1;
display: flex;
align-items: center;
justify-content: space-between;
}
.driver-popover-progress-text {
font-size: 13px;
font-weight: 400;
color: #727272;
zoom: 1;
}
.driver-popover-footer button {
all: unset;
display: inline-block;
box-sizing: border-box;
padding: 3px 7px;
text-decoration: none;
text-shadow: 1px 1px 0 #fff;
background-color: #ffffff;
color: #2d2d2d;
font: 12px / normal sans-serif;
cursor: pointer;
outline: 0;
zoom: 1;
line-height: 1.3;
border: 1px solid #ccc;
border-radius: 3px;
}
.driver-popover-footer .driver-popover-btn-disabled {
opacity: 0.5;
pointer-events: none;
}
/* Disable the scrolling of parent element if it has an active element*/
:not(body):has(> .driver-active-element) {
overflow: hidden !important;
}
.driver-no-interaction,
.driver-no-interaction * {
pointer-events: none !important;
}
.driver-popover-footer button:hover,
.driver-popover-footer button:focus {
background-color: #f7f7f7;
}
.driver-popover-navigation-btns {
display: flex;
flex-grow: 1;
justify-content: flex-end;
}
.driver-popover-navigation-btns button + button {
margin-left: 4px;
}
.driver-popover-arrow {
content: '';
position: absolute;
border: 5px solid #fff;
/* 取消 Arrow */
display: none;
}
.driver-popover-arrow-side-over {
display: none;
}
/** Popover Arrow Sides **/
.driver-popover-arrow-side-left {
left: 100%;
border-right-color: transparent;
border-bottom-color: transparent;
border-top-color: transparent;
}
.driver-popover-arrow-side-right {
right: 100%;
border-left-color: transparent;
border-bottom-color: transparent;
border-top-color: transparent;
}
.driver-popover-arrow-side-top {
top: 100%;
border-right-color: transparent;
border-bottom-color: transparent;
border-left-color: transparent;
}
.driver-popover-arrow-side-bottom {
bottom: 100%;
border-left-color: transparent;
border-top-color: transparent;
border-right-color: transparent;
}
.driver-popover-arrow-side-center {
display: none;
}
/* Left/Start + Right/Start */
.driver-popover-arrow-side-left.driver-popover-arrow-align-start,
.driver-popover-arrow-side-right.driver-popover-arrow-align-start {
top: 15px;
}
/* Top/Start + Bottom/Start */
.driver-popover-arrow-side-top.driver-popover-arrow-align-start,
.driver-popover-arrow-side-bottom.driver-popover-arrow-align-start {
left: 15px;
}
/* End/Left + End/Right */
.driver-popover-arrow-align-end.driver-popover-arrow-side-left,
.driver-popover-arrow-align-end.driver-popover-arrow-side-right {
bottom: 15px;
}
/* Top/End + Bottom/End */
.driver-popover-arrow-side-top.driver-popover-arrow-align-end,
.driver-popover-arrow-side-bottom.driver-popover-arrow-align-end {
right: 15px;
}
/* Left/Center + Right/Center */
.driver-popover-arrow-side-left.driver-popover-arrow-align-center,
.driver-popover-arrow-side-right.driver-popover-arrow-align-center {
top: 50%;
margin-top: -5px;
}
/* Top/Center + Bottom/Center */
.driver-popover-arrow-side-top.driver-popover-arrow-align-center,
.driver-popover-arrow-side-bottom.driver-popover-arrow-align-center {
left: 50%;
margin-left: -5px;
}
/* No arrow */
.driver-popover-arrow-none {
display: none;
}
.driver-popover-overlay-skip-btn {
position: fixed;
top: 20px;
left: 20px;
z-index: 11111;
padding: 4px 16px;
color: #ffffff;
font-size: 12px;
font-weight: 500;
border-radius: 6px;
background: #a9a9a9;
opacity: 0.9;
cursor: pointer;
}
+345
View File
@@ -0,0 +1,345 @@
import { AllowedButtons, destroyPopover, Popover } from './popover';
import { destroyOverlay, destroyOverlayBtn } from './overlay';
import { destroyEvents, initEvents, requireRefresh } from './events';
import { Config, configure, DriverHook, getConfig } from './config';
import { destroyHighlight, highlight } from './highlight';
import { destroyEmitter, listen } from './emitter';
import { getState, resetState, setState } from './state';
export type DriveStep = {
element?: string | Element;
onHighlightStarted?: DriverHook;
onHighlighted?: DriverHook;
onDeselected?: DriverHook;
popover?: Popover;
};
export function driver(options: Config = {}) {
configure(options);
function handleClose() {
if (!getConfig('allowClose')) {
return;
}
destroy();
}
function moveNext() {
const activeIndex = getState('activeIndex');
const steps = getConfig('steps') || [];
if (typeof activeIndex === 'undefined') {
return;
}
const nextStepIndex = activeIndex + 1;
if (steps[nextStepIndex]) {
drive(nextStepIndex);
} else {
destroy();
}
}
function movePrevious() {
const activeIndex = getState('activeIndex');
const steps = getConfig('steps') || [];
if (typeof activeIndex === 'undefined') {
return;
}
const previousStepIndex = activeIndex - 1;
if (steps[previousStepIndex]) {
drive(previousStepIndex);
} else {
destroy();
}
}
function moveTo(index: number) {
const steps = getConfig('steps') || [];
if (steps[index]) {
drive(index);
} else {
destroy();
}
}
function handleArrowLeft() {
const isTransitioning = getState('__transitionCallback');
if (isTransitioning) {
return;
}
const activeIndex = getState('activeIndex');
const activeStep = getState('__activeStep');
const activeElement = getState('__activeElement');
if (typeof activeIndex === 'undefined' || typeof activeStep === 'undefined') {
return;
}
const currentStepIndex = getState('activeIndex');
if (typeof currentStepIndex === 'undefined') {
return;
}
const onPrevClick = activeStep.popover?.onPrevClick || getConfig('onPrevClick');
if (onPrevClick) {
return onPrevClick(activeElement, activeStep, {
config: getConfig(),
state: getState()
});
}
movePrevious();
}
function handleArrowRight() {
const isTransitioning = getState('__transitionCallback');
if (isTransitioning) {
return;
}
const activeIndex = getState('activeIndex');
const activeStep = getState('__activeStep');
const activeElement = getState('__activeElement');
if (typeof activeIndex === 'undefined' || typeof activeStep === 'undefined') {
return;
}
const onNextClick = activeStep.popover?.onNextClick || getConfig('onNextClick');
if (onNextClick) {
return onNextClick(activeElement, activeStep, {
config: getConfig(),
state: getState()
});
}
moveNext();
}
function init() {
if (getState('isInitialized')) {
return;
}
setState('isInitialized', true);
document.body.classList.add(
'driver-active',
getConfig('animate') ? 'driver-fade' : 'driver-simple'
);
initEvents();
listen('overlayClick', getConfig('allowClickMaskNextStep') ? moveNext : handleClose);
listen('escapePress', handleClose);
listen('skipButtonClick', () => {
const interceptSkipButtonClick = getConfig('interceptSkipButtonClick');
if (interceptSkipButtonClick) {
interceptSkipButtonClick();
} else {
destroy();
}
});
listen('arrowLeftPress', handleArrowLeft);
listen('arrowRightPress', handleArrowRight);
}
function drive(stepIndex: number = 0) {
const steps = getConfig('steps');
if (!steps) {
console.error('No steps to drive through');
destroy();
return;
}
if (!steps[stepIndex]) {
destroy();
return;
}
setState('__activeOnDestroyed', document.activeElement as HTMLElement);
setState('activeIndex', stepIndex);
const currentStep = steps[stepIndex];
const hasNextStep = steps[stepIndex + 1];
const hasPreviousStep = steps[stepIndex - 1];
const doneBtnText = currentStep.popover?.doneBtnText || getConfig('doneBtnText') || 'Done';
const allowsClosing = getConfig('allowClose');
const showProgress =
typeof currentStep.popover?.showProgress !== 'undefined'
? currentStep.popover?.showProgress
: getConfig('showProgress');
const progressText =
currentStep.popover?.progressText || getConfig('progressText') || '{{current}} of {{total}}';
const progressTextReplaced = progressText
.replace('{{current}}', `${stepIndex + 1}`)
.replace('{{total}}', `${steps.length}`);
const configuredButtons = currentStep.popover?.showButtons || getConfig('showButtons');
const calculatedButtons: AllowedButtons[] = [
'next',
'previous',
...(allowsClosing ? ['close' as AllowedButtons] : [])
].filter((b) => {
return !configuredButtons?.length || configuredButtons.includes(b as AllowedButtons);
}) as AllowedButtons[];
const onNextClick = currentStep.popover?.onNextClick || getConfig('onNextClick');
const onPrevClick = currentStep.popover?.onPrevClick || getConfig('onPrevClick');
const onCloseClick = currentStep.popover?.onCloseClick || getConfig('onCloseClick');
highlight({
...currentStep,
popover: {
showButtons: calculatedButtons,
nextBtnText: !hasNextStep ? doneBtnText : undefined,
disableButtons: [...(!hasPreviousStep ? ['previous' as AllowedButtons] : [])],
showProgress: showProgress,
progressText: progressTextReplaced,
onNextClick: onNextClick
? onNextClick
: () => {
if (!hasNextStep) {
destroy();
} else {
drive(stepIndex + 1);
}
},
onPrevClick: onPrevClick
? onPrevClick
: () => {
drive(stepIndex - 1);
},
onCloseClick: onCloseClick
? onCloseClick
: () => {
destroy();
},
...(currentStep?.popover || {})
}
});
}
function destroy(withOnDestroyStartedHook = true) {
const activeElement = getState('__activeElement');
const activeStep = getState('__activeStep');
const activeOnDestroyed = getState('__activeOnDestroyed');
const onDestroyStarted = getConfig('onDestroyStarted');
// `onDestroyStarted` is used to confirm the exit of tour. If we trigger
// the hook for when user calls `destroy`, driver will get into infinite loop
// not causing tour to be destroyed.
if (withOnDestroyStartedHook && onDestroyStarted) {
const isActiveDummyElement = !activeElement || activeElement?.id === 'driver-dummy-element';
onDestroyStarted(isActiveDummyElement ? undefined : activeElement, activeStep!, {
config: getConfig(),
state: getState()
});
return;
}
const onDeselected = activeStep?.onDeselected || getConfig('onDeselected');
const onDestroyed = getConfig('onDestroyed');
document.body.classList.remove('driver-active', 'driver-fade', 'driver-simple');
destroyEvents();
destroyPopover();
destroyHighlight();
destroyOverlay();
destroyEmitter();
destroyOverlayBtn();
resetState();
if (activeElement && activeStep) {
const isActiveDummyElement = activeElement.id === 'driver-dummy-element';
if (onDeselected) {
onDeselected(isActiveDummyElement ? undefined : activeElement, activeStep, {
config: getConfig(),
state: getState()
});
}
if (onDestroyed) {
onDestroyed(isActiveDummyElement ? undefined : activeElement, activeStep, {
config: getConfig(),
state: getState()
});
}
}
if (activeOnDestroyed) {
(activeOnDestroyed as HTMLElement).focus();
}
}
return {
isActive: () => getState('isInitialized') || false,
refresh: requireRefresh,
drive: (stepIndex: number = 0) => {
init();
drive(stepIndex);
},
setConfig: configure,
setSteps: (steps: DriveStep[]) => {
resetState();
configure({
...getConfig(),
steps
});
},
getConfig,
getState,
getActiveIndex: () => getState('activeIndex'),
isFirstStep: () => getState('activeIndex') === 0,
isLastStep: () => {
const steps = getConfig('steps') || [];
const activeIndex = getState('activeIndex');
return activeIndex !== undefined && activeIndex === steps.length - 1;
},
getActiveStep: () => getState('activeStep'),
getActiveElement: () => getState('activeElement'),
getPreviousElement: () => getState('previousElement'),
getPreviousStep: () => getState('previousStep'),
moveNext,
movePrevious,
moveTo,
hasNextStep: () => {
const steps = getConfig('steps') || [];
const activeIndex = getState('activeIndex');
return activeIndex !== undefined && steps[activeIndex + 1];
},
hasPreviousStep: () => {
const steps = getConfig('steps') || [];
const activeIndex = getState('activeIndex');
return activeIndex !== undefined && steps[activeIndex - 1];
},
highlight: (step: DriveStep) => {
init();
highlight({
...step,
popover: step.popover
? {
showButtons: [],
showProgress: false,
progressText: '',
...step.popover!
}
: undefined
});
},
destroy: () => {
destroy(false);
}
};
}
export type Driver = ReturnType<typeof driver>;
+23
View File
@@ -0,0 +1,23 @@
type allowedEvents =
| 'overlayClick'
| 'escapePress'
| 'nextClick'
| 'prevClick'
| 'closeClick'
| 'arrowRightPress'
| 'arrowLeftPress'
| 'skipButtonClick';
let registeredListeners: Partial<{ [key in allowedEvents]: () => void }> = {};
export function listen(hook: allowedEvents, callback: () => void) {
registeredListeners[hook] = callback;
}
export function emit(hook: allowedEvents) {
registeredListeners[hook]?.();
}
export function destroyEmitter() {
registeredListeners = {};
}
+133
View File
@@ -0,0 +1,133 @@
import { refreshActiveHighlight } from './highlight';
import { emit } from './emitter';
import { getState, setState } from './state';
import { getConfig } from './config';
import { getFocusableElements } from './utils';
export function requireRefresh() {
const resizeTimeout = getState('__resizeTimeout');
if (resizeTimeout) {
window.cancelAnimationFrame(resizeTimeout);
}
setState('__resizeTimeout', window.requestAnimationFrame(refreshActiveHighlight));
}
function trapFocus(e: KeyboardEvent) {
const isActivated = getState('isInitialized');
if (!isActivated) {
return;
}
const isTabKey = e.key === 'Tab' || e.keyCode === 9;
if (!isTabKey) {
return;
}
const activeElement = getState('__activeElement');
const popoverEl = getState('popover')?.wrapper;
const focusableEls = getFocusableElements([
...(popoverEl ? [popoverEl] : []),
...(activeElement ? [activeElement] : [])
]);
const firstFocusableEl = focusableEls[0];
const lastFocusableEl = focusableEls[focusableEls.length - 1];
e.preventDefault();
if (e.shiftKey) {
const previousFocusableEl =
focusableEls[focusableEls.indexOf(document.activeElement as HTMLElement) - 1] ||
lastFocusableEl;
previousFocusableEl?.focus();
} else {
const nextFocusableEl =
focusableEls[focusableEls.indexOf(document.activeElement as HTMLElement) + 1] ||
firstFocusableEl;
nextFocusableEl?.focus();
}
}
function onKeyup(e: KeyboardEvent) {
const allowKeyboardControl = getConfig('allowKeyboardControl') ?? true;
if (!allowKeyboardControl) {
return;
}
if (e.key === 'Escape') {
emit('escapePress');
} else if (e.key === 'ArrowRight') {
emit('arrowRightPress');
} else if (e.key === 'ArrowLeft') {
emit('arrowLeftPress');
}
}
/**
* Attaches click handler to the elements created by driver.js. It makes
* sure to give the listener the first chance to handle the event, and
* prevents all other pointer-events to make sure no external-library
* ever knows the click happened.
*
* @param {Element} element Element to listen for click events
* @param {(pointer: MouseEvent | PointerEvent) => void} listener Click handler
* @param {(target: HTMLElement) => boolean} shouldPreventDefault Whether to prevent default action i.e. link clicks etc
*/
export function onDriverClick(
element: Element,
listener: (pointer: MouseEvent | PointerEvent) => void,
shouldPreventDefault?: (target: HTMLElement) => boolean
) {
const listenerWrapper = (
e: MouseEvent | PointerEvent,
listener?: (pointer: MouseEvent | PointerEvent) => void
) => {
const target = e.target as HTMLElement;
if (!element.contains(target)) {
return;
}
if (!shouldPreventDefault || shouldPreventDefault(target)) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
}
listener?.(e);
};
// We want to be the absolute first one to hear about the event
const useCapture = true;
// Events to disable
document.addEventListener('pointerdown', listenerWrapper, useCapture);
document.addEventListener('mousedown', listenerWrapper, useCapture);
document.addEventListener('pointerup', listenerWrapper, useCapture);
document.addEventListener('mouseup', listenerWrapper, useCapture);
// Actual click handler
document.addEventListener(
'click',
(e) => {
listenerWrapper(e, listener);
},
useCapture
);
}
export function initEvents() {
window.addEventListener('keyup', onKeyup, false);
window.addEventListener('keydown', trapFocus, false);
window.addEventListener('resize', requireRefresh);
window.addEventListener('scroll', requireRefresh);
}
export function destroyEvents() {
window.removeEventListener('keyup', onKeyup);
window.removeEventListener('resize', requireRefresh);
window.removeEventListener('scroll', requireRefresh);
}
+185
View File
@@ -0,0 +1,185 @@
import { DriveStep } from './driver';
import { refreshOverlay, trackActiveElement, transitionStage } from './overlay';
import { getConfig } from './config';
import { hidePopover, renderPopover, repositionPopover } from './popover';
import { bringInView } from './utils';
import { getState, setState } from './state';
function mountDummyElement(): Element {
const existingDummy = document.getElementById('driver-dummy-element');
if (existingDummy) {
return existingDummy;
}
let element = document.createElement('div');
element.id = 'driver-dummy-element';
element.style.width = '0';
element.style.height = '0';
element.style.pointerEvents = 'none';
element.style.opacity = '0';
element.style.position = 'fixed';
element.style.top = '50%';
element.style.left = '50%';
document.body.appendChild(element);
return element;
}
export function highlight(step: DriveStep) {
const { element } = step;
let elemObj = typeof element === 'string' ? document.querySelector(element) : element;
// If the element is not found, we mount a 1px div
// at the center of the screen to highlight and show
// the popover on top of that. This is to show a
// modal-like highlight.
if (!elemObj) {
elemObj = mountDummyElement();
}
transferHighlight(elemObj, step);
}
export function refreshActiveHighlight() {
const activeHighlight = getState('__activeElement');
const activeStep = getState('__activeStep')!;
if (!activeHighlight) {
return;
}
// 如果存在上一步的 popover,则不隐藏
// if (previousPopover) {
// refreshOverlay();
// repositionPopover(activeHighlight, activeStep);
// return;
// }
trackActiveElement(activeHighlight);
refreshOverlay();
repositionPopover(activeHighlight, activeStep);
}
function transferHighlight(toElement: Element, toStep: DriveStep) {
const duration = 400;
const start = Date.now();
const fromStep = getState('__activeStep');
const fromElement = getState('__activeElement') || toElement;
// If it's the first time we're highlighting an element, we show
// the popover immediately. Otherwise, we wait for the animation
// to finish before showing the popover.
const isFirstHighlight = !fromElement || fromElement === toElement;
const isToDummyElement = toElement.id === 'driver-dummy-element';
const isFromDummyElement = fromElement.id === 'driver-dummy-element';
const isAnimatedTour = getConfig('animate');
const highlightStartedHook = toStep.onHighlightStarted || getConfig('onHighlightStarted');
const highlightedHook = toStep?.onHighlighted || getConfig('onHighlighted');
const deselectedHook = fromStep?.onDeselected || getConfig('onDeselected');
const config = getConfig();
const state = getState();
if (!isFirstHighlight && deselectedHook) {
deselectedHook(isFromDummyElement ? undefined : fromElement, fromStep!, {
config,
state
});
}
if (highlightStartedHook) {
highlightStartedHook(isToDummyElement ? undefined : toElement, toStep, {
config,
state
});
}
const hasDelayedPopover = !isFirstHighlight && isAnimatedTour;
let isPopoverRendered = false;
hidePopover();
setState('previousStep', fromStep);
setState('previousElement', fromElement);
setState('activeStep', toStep);
setState('activeElement', toElement);
const animate = () => {
const transitionCallback = getState('__transitionCallback');
// This makes sure that the repeated calls to transferHighlight
// don't interfere with each other. Only the last call will be
// executed.
if (transitionCallback !== animate) {
return;
}
const elapsed = Date.now() - start;
const timeRemaining = duration - elapsed;
const isHalfwayThrough = timeRemaining <= duration / 2;
if (toStep.popover && isHalfwayThrough && !isPopoverRendered && hasDelayedPopover) {
renderPopover(toElement, toStep);
isPopoverRendered = true;
}
if (getConfig('animate') && elapsed < duration) {
transitionStage(elapsed, duration, fromElement, toElement);
} else {
trackActiveElement(toElement);
if (highlightedHook) {
highlightedHook(isToDummyElement ? undefined : toElement, toStep, {
config: getConfig(),
state: getState()
});
}
setState('__transitionCallback', undefined);
setState('__previousStep', fromStep);
setState('__previousElement', fromElement);
setState('__activeStep', toStep);
setState('__activeElement', toElement);
}
window.requestAnimationFrame(animate);
};
setState('__transitionCallback', animate);
window.requestAnimationFrame(animate);
bringInView(toElement);
if (!hasDelayedPopover && toStep.popover) {
renderPopover(toElement, toStep);
}
fromElement.classList.remove('driver-active-element', 'driver-no-interaction');
fromElement.removeAttribute('aria-haspopup');
fromElement.removeAttribute('aria-expanded');
fromElement.removeAttribute('aria-controls');
const disableActiveInteraction = getConfig('disableActiveInteraction');
if (disableActiveInteraction) {
toElement.classList.add('driver-no-interaction');
}
toElement.classList.add('driver-active-element');
toElement.setAttribute('aria-haspopup', 'dialog');
toElement.setAttribute('aria-expanded', 'true');
toElement.setAttribute('aria-controls', 'driver-popover-content');
}
export function destroyHighlight() {
document.getElementById('driver-dummy-element')?.remove();
document.querySelectorAll('.driver-active-element').forEach((element) => {
element.classList.remove('driver-active-element', 'driver-no-interaction');
element.removeAttribute('aria-haspopup');
element.removeAttribute('aria-expanded');
element.removeAttribute('aria-controls');
});
}
+254
View File
@@ -0,0 +1,254 @@
import { easeInOutQuad } from './utils';
import { onDriverClick } from './events';
import { emit } from './emitter';
import { getConfig } from './config';
import { getState, setState } from './state';
import { PopoverDOM } from './popover';
import { driver } from './driver';
export const allPopovers: PopoverDOM[] = [];
export type StageDefinition = {
x: number;
y: number;
width: number;
height: number;
};
// This method calculates the animated new position of the
// stage (called for each frame by requestAnimationFrame)
export function transitionStage(elapsed: number, duration: number, from: Element, to: Element) {
let activeStagePosition = getState('__activeStagePosition');
const fromDefinition = activeStagePosition ? activeStagePosition : from.getBoundingClientRect();
const toDefinition = to.getBoundingClientRect();
const x = easeInOutQuad(elapsed, fromDefinition.x, toDefinition.x - fromDefinition.x, duration);
const y = easeInOutQuad(elapsed, fromDefinition.y, toDefinition.y - fromDefinition.y, duration);
const width = easeInOutQuad(
elapsed,
fromDefinition.width,
toDefinition.width - fromDefinition.width,
duration
);
const height = easeInOutQuad(
elapsed,
fromDefinition.height,
toDefinition.height - fromDefinition.height,
duration
);
activeStagePosition = {
x,
y,
width,
height
};
renderOverlay(activeStagePosition);
setState('__activeStagePosition', activeStagePosition);
}
export function trackActiveElement(element: Element) {
if (!element) {
return;
}
const definition = element.getBoundingClientRect();
const activeStagePosition: StageDefinition = {
x: definition.x,
y: definition.y,
width: definition.width,
height: definition.height
};
setState('__activeStagePosition', activeStagePosition);
renderOverlay(activeStagePosition);
}
export function refreshOverlay() {
const activeStagePosition = getState('__activeStagePosition');
const overlaySvg = getState('__overlaySvg');
if (!activeStagePosition) {
return;
}
if (!overlaySvg) {
console.warn('No stage svg found.');
return;
}
const windowX = window.innerWidth;
const windowY = window.innerHeight;
overlaySvg.setAttribute('viewBox', `0 0 ${windowX} ${windowY}`);
}
function mountOverlay(stagePosition: StageDefinition) {
const overlaySvg = createOverlaySvg(stagePosition);
document.body.appendChild(overlaySvg);
onDriverClick(overlaySvg, (e) => {
const target = e.target as SVGElement;
if (target.tagName !== 'path') {
return;
}
emit('overlayClick');
});
setState('__overlaySvg', overlaySvg);
const allowSkipButton = getConfig('overlaySkipButton');
if (allowSkipButton && allowSkipButton !== '') {
const skipButton = document.createElement('div');
skipButton.id = 'driver-popover-overlay-skip-btn';
skipButton.classList.add('driver-popover-overlay-skip-btn');
skipButton.innerText = getConfig('overlaySkipButton') || '';
document.body.appendChild(skipButton);
let countdown = 5;
let timeoutId: any;
let isButtonDisabled = true;
const updateButtonText = () => {
skipButton.innerText = `${getConfig('overlaySkipButton') || ''} (${countdown}s)`;
};
const countdownInterval = setInterval(() => {
countdown -= 1;
updateButtonText();
}, 1000);
timeoutId = setTimeout(() => {
console.log('Auto skipping after 5 seconds');
clearInterval(countdownInterval);
enableButton();
skipButton.innerText = getConfig('overlaySkipButton') || '';
}, 5000);
updateButtonText();
function disableButton() {
isButtonDisabled = true;
skipButton.style.pointerEvents = 'none';
skipButton.style.opacity = '0.5';
}
function enableButton() {
isButtonDisabled = false;
skipButton.style.pointerEvents = 'auto';
skipButton.style.opacity = '1';
}
onDriverClick(skipButton, () => {
if (!isButtonDisabled) {
disableButton();
clearInterval(countdownInterval);
clearTimeout(timeoutId);
emit('skipButtonClick');
}
});
setState('__overlaySkipBtn', skipButton);
}
}
function renderOverlay(stagePosition: StageDefinition) {
const overlaySvg = getState('__overlaySvg');
// TODO: cancel rendering if element is not visible
if (!overlaySvg) {
mountOverlay(stagePosition);
return;
}
const pathElement = overlaySvg.firstElementChild as SVGPathElement | null;
if (pathElement?.tagName !== 'path') {
throw new Error('no path element found in stage svg');
}
pathElement.setAttribute('d', generateStageSvgPathString(stagePosition));
}
function createOverlaySvg(stage: StageDefinition): SVGSVGElement {
const windowX = window.innerWidth;
const windowY = window.innerHeight;
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.classList.add('driver-overlay', 'driver-overlay-animated');
svg.setAttribute('viewBox', `0 0 ${windowX} ${windowY}`);
svg.setAttribute('xmlSpace', 'preserve');
svg.setAttribute('xmlnsXlink', 'http://www.w3.org/1999/xlink');
svg.setAttribute('version', '1.1');
svg.setAttribute('preserveAspectRatio', 'xMinYMin slice');
svg.style.fillRule = 'evenodd';
svg.style.clipRule = 'evenodd';
svg.style.strokeLinejoin = 'round';
svg.style.strokeMiterlimit = '2';
svg.style.zIndex = '10000';
svg.style.position = 'fixed';
svg.style.top = '0';
svg.style.left = '0';
svg.style.width = '100%';
svg.style.height = '100%';
const stagePath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
stagePath.setAttribute('d', generateStageSvgPathString(stage));
stagePath.style.fill = getConfig('overlayColor') || 'rgb(0,0,0)';
stagePath.style.opacity = `${getConfig('overlayOpacity')}`;
stagePath.style.pointerEvents = 'auto';
stagePath.style.cursor = 'auto';
svg.appendChild(stagePath);
return svg;
}
function generateStageSvgPathString(stage: StageDefinition) {
const windowX = window.innerWidth;
const windowY = window.innerHeight;
const stagePadding = getConfig('stagePadding') || 0;
const stageRadius = getConfig('stageRadius') || 0;
const stageWidth = stage.width + stagePadding * 2;
const stageHeight = stage.height + stagePadding * 2;
// prevent glitches when stage is too small for radius
const limitedRadius = Math.min(stageRadius, stageWidth / 2, stageHeight / 2);
// no value below 0 allowed + round down
const normalizedRadius = Math.floor(Math.max(limitedRadius, 0));
const highlightBoxX = stage.x - stagePadding + normalizedRadius;
const highlightBoxY = stage.y - stagePadding;
const highlightBoxWidth = stageWidth - normalizedRadius * 2;
const highlightBoxHeight = stageHeight - normalizedRadius * 2;
return `M${windowX},0L0,0L0,${windowY}L${windowX},${windowY}L${windowX},0Z
M${highlightBoxX},${highlightBoxY} h${highlightBoxWidth} a${normalizedRadius},${normalizedRadius} 0 0 1 ${normalizedRadius},${normalizedRadius} v${highlightBoxHeight} a${normalizedRadius},${normalizedRadius} 0 0 1 -${normalizedRadius},${normalizedRadius} h-${highlightBoxWidth} a${normalizedRadius},${normalizedRadius} 0 0 1 -${normalizedRadius},-${normalizedRadius} v-${highlightBoxHeight} a${normalizedRadius},${normalizedRadius} 0 0 1 ${normalizedRadius},-${normalizedRadius} z`;
}
export function destroyOverlay() {
const overlaySvg = getState('__overlaySvg');
if (overlaySvg) {
overlaySvg.remove();
}
}
export function destroyOverlayBtn() {
const overlayBtn = getState('__overlaySkipBtn');
if (overlayBtn) {
overlayBtn.remove();
}
}
+737
View File
@@ -0,0 +1,737 @@
import { ReactNode } from 'react';
import { Config, DriverHook, getConfig } from './config';
import { DriveStep } from './driver';
import { emit } from './emitter';
import { onDriverClick } from './events';
import { State, getState, setState } from './state';
import { bringInView, getFocusableElements } from './utils';
import { createRoot } from 'react-dom/client';
export type Side = 'top' | 'right' | 'bottom' | 'left' | 'over';
export type Alignment = 'start' | 'center' | 'end';
export type AllowedButtons = 'next' | 'previous' | 'close';
export type Popover = {
title?: string;
description?: string;
side?: Side;
align?: Alignment;
showButtons?: AllowedButtons[];
showProgress?: boolean;
disableButtons?: AllowedButtons[];
popoverClass?: string;
// Button texts
progressText?: string;
doneBtnText?: string;
nextBtnText?: string;
prevBtnText?: string;
// Called after the popover is rendered
onPopoverRender?: (popover: PopoverDOM, opts: { config: Config; state: State }) => void;
// Button callbacks
onNextClick?: DriverHook;
onPrevClick?: DriverHook;
onCloseClick?: DriverHook;
// custom config
isShowButtons?: boolean;
borderRadius?: string;
PopoverBody?: ReactNode;
};
export type PopoverDOM = {
wrapper: HTMLElement;
arrow: HTMLElement;
title: HTMLElement;
description: HTMLElement;
footer: HTMLElement;
progress: HTMLElement;
previousButton: HTMLButtonElement;
nextButton: HTMLButtonElement;
closeButton: HTMLButtonElement;
footerButtons: HTMLElement;
popoverBody: HTMLElement;
};
export function hidePopover() {
const popover = getState('popover');
if (!popover) {
return;
}
if (getConfig('allowPreviousStep')) {
popover.wrapper.style.display = 'block';
popover.wrapper.style.opacity = '0.4';
} else {
popover.wrapper.style.display = 'none';
}
const popoverInfoElement = document.getElementById('popover-info');
if (popoverInfoElement) {
const parentElement = popoverInfoElement.parentElement;
if (parentElement) {
parentElement.removeChild(popoverInfoElement);
}
}
}
export function renderPopover(element: Element, step: DriveStep) {
let popover: PopoverDOM;
if (!getConfig('allowPreviousStep')) {
let temp = getState('popover');
if (temp) {
document.body.removeChild(temp.wrapper);
}
}
// render custom body
const { PopoverBody } = step.popover || {};
popover = createPopover(PopoverBody);
document.body.appendChild(popover.wrapper);
// important react render popover body done
requestAnimationFrame(() => {
repositionPopover(element, step);
});
const {
title,
description,
showButtons,
disableButtons,
showProgress,
nextBtnText = getConfig('nextBtnText') || 'Next &rarr;',
prevBtnText = getConfig('prevBtnText') || '&larr; Previous',
progressText = getConfig('progressText') || '{current} of {total}',
isShowButtons = getConfig('isShowButtons'),
borderRadius
} = step.popover || {};
popover.nextButton.textContent = nextBtnText;
popover.previousButton.textContent = prevBtnText;
popover.progress.textContent = progressText;
if (title) {
popover.title.textContent = title;
popover.title.style.display = 'block';
} else {
popover.title.style.display = 'none';
}
if (description) {
popover.description.textContent = description;
popover.description.style.display = 'block';
} else {
popover.description.style.display = 'none';
}
const showButtonsConfig: AllowedButtons[] = showButtons || getConfig('showButtons')!;
const showProgressConfig = showProgress || getConfig('showProgress') || false;
const showFooter = isShowButtons
? showButtonsConfig?.includes('next') ||
showButtonsConfig?.includes('previous') ||
showProgressConfig
: false;
popover.closeButton.style.display = showButtonsConfig.includes('close') ? 'block' : 'none';
if (showFooter) {
popover.footer.style.display = 'flex';
popover.progress.style.display = showProgressConfig ? 'block' : 'none';
popover.nextButton.style.display = showButtonsConfig.includes('next') ? 'block' : 'none';
popover.previousButton.style.display = showButtonsConfig.includes('previous')
? 'block'
: 'none';
} else {
popover.footer.style.display = 'none';
}
const disabledButtonsConfig: AllowedButtons[] =
disableButtons || getConfig('disableButtons')! || [];
if (disabledButtonsConfig?.includes('next')) {
popover.nextButton.disabled = true;
popover.nextButton.classList.add('driver-popover-btn-disabled');
}
if (disabledButtonsConfig?.includes('previous')) {
popover.previousButton.disabled = true;
popover.previousButton.classList.add('driver-popover-btn-disabled');
}
if (disabledButtonsConfig?.includes('close')) {
popover.closeButton.disabled = true;
popover.closeButton.classList.add('driver-popover-btn-disabled');
}
// Reset the popover position
const popoverWrapper = popover.wrapper;
popoverWrapper.style.display = 'block';
popoverWrapper.style.left = '';
popoverWrapper.style.top = '';
popoverWrapper.style.bottom = '';
popoverWrapper.style.right = '';
popoverWrapper.style.borderRadius = borderRadius || '';
popoverWrapper.id = 'driver-popover-content';
popoverWrapper.setAttribute('role', 'dialog');
popoverWrapper.setAttribute('aria-labelledby', 'driver-popover-title');
popoverWrapper.setAttribute('aria-describedby', 'driver-popover-description');
// Reset the classes responsible for the arrow position
const popoverArrow = popover.arrow;
popoverArrow.className = 'driver-popover-arrow';
// Reset any custom classes on the popover
const customPopoverClass = step.popover?.popoverClass || getConfig('popoverClass') || '';
popoverWrapper.className = `driver-popover ${customPopoverClass}`.trim();
// Handles the popover button clicks
onDriverClick(
popover.wrapper,
(e) => {
const target = e.target as HTMLElement;
const onNextClick = step.popover?.onNextClick || getConfig('onNextClick');
const onPrevClick = step.popover?.onPrevClick || getConfig('onPrevClick');
const onCloseClick = step.popover?.onCloseClick || getConfig('onCloseClick');
if (target.classList.contains('driver-popover-next-btn')) {
// If the user has provided a custom callback, call it
// otherwise, emit the event.
if (onNextClick) {
return onNextClick(element, step, {
config: getConfig(),
state: getState()
});
} else {
return emit('nextClick');
}
}
if (target.classList.contains('driver-popover-prev-btn')) {
if (onPrevClick) {
return onPrevClick(element, step, {
config: getConfig(),
state: getState()
});
} else {
return emit('prevClick');
}
}
if (target.classList.contains('driver-popover-close-btn')) {
if (onCloseClick) {
return onCloseClick(element, step, {
config: getConfig(),
state: getState()
});
} else {
return emit('closeClick');
}
}
return undefined;
},
(target) => {
// Only prevent the default action if we're clicking on a driver button
// This allows us to have links inside the popover title and description
return (
!popover?.description.contains(target) &&
!popover?.title.contains(target) &&
typeof target.className === 'string' &&
target.className.includes('driver-popover')
);
}
);
setState('popover', popover);
const onPopoverRender = step.popover?.onPopoverRender || getConfig('onPopoverRender');
if (onPopoverRender) {
onPopoverRender(popover, {
config: getConfig(),
state: getState()
});
}
repositionPopover(element, step);
bringInView(popoverWrapper);
// Focus on the first focusable element in active element or popover
const isToDummyElement = element.classList.contains('driver-dummy-element');
const focusableElement = getFocusableElements([
popoverWrapper,
...(isToDummyElement ? [] : [element])
]);
if (focusableElement.length > 0) {
focusableElement[0].focus();
}
}
type PopoverDimensions = {
width: number;
height: number;
realWidth: number;
realHeight: number;
};
function getPopoverDimensions(): PopoverDimensions | undefined {
const popover = getState('popover');
if (!popover?.wrapper) {
return;
}
const boundingClientRect = popover.wrapper.getBoundingClientRect();
const stagePadding = getConfig('stagePadding') || 0;
const popoverOffset = getConfig('popoverOffset') || 0;
return {
width: boundingClientRect.width + stagePadding + popoverOffset,
height: boundingClientRect.height + stagePadding + popoverOffset,
realWidth: boundingClientRect.width,
realHeight: boundingClientRect.height
};
}
function calculateTopForLeftRight(
alignment: Alignment,
config: {
elementDimensions: DOMRect;
popoverDimensions: PopoverDimensions;
popoverPadding: number;
popoverArrowDimensions: { width: number; height: number };
}
): number {
const { elementDimensions, popoverDimensions, popoverPadding, popoverArrowDimensions } = config;
if (alignment === 'start') {
return Math.max(
Math.min(
elementDimensions.top - popoverPadding,
window.innerHeight - popoverDimensions!.realHeight - popoverArrowDimensions.width
),
popoverArrowDimensions.width
);
}
if (alignment === 'end') {
return Math.max(
Math.min(
elementDimensions.top -
popoverDimensions?.realHeight +
elementDimensions.height +
popoverPadding,
window.innerHeight - popoverDimensions?.realHeight - popoverArrowDimensions.width
),
popoverArrowDimensions.width
);
}
if (alignment === 'center') {
return Math.max(
Math.min(
elementDimensions.top + elementDimensions.height / 2 - popoverDimensions?.realHeight / 2,
window.innerHeight - popoverDimensions?.realHeight - popoverArrowDimensions.width
),
popoverArrowDimensions.width
);
}
return 0;
}
// Calculate the left placement for top and bottom sides
function calculateLeftForTopBottom(
alignment: Alignment,
config: {
elementDimensions: DOMRect;
popoverDimensions: PopoverDimensions;
popoverPadding: number;
popoverArrowDimensions: { width: number; height: number };
}
): number {
const { elementDimensions, popoverDimensions, popoverPadding, popoverArrowDimensions } = config;
if (alignment === 'start') {
return Math.max(
Math.min(
elementDimensions.left - popoverPadding,
window.innerWidth - popoverDimensions!.realWidth - popoverArrowDimensions.width
),
popoverArrowDimensions.width
);
}
if (alignment === 'end') {
return Math.max(
Math.min(
elementDimensions.left -
popoverDimensions?.realWidth +
elementDimensions.width +
popoverPadding,
window.innerWidth - popoverDimensions?.realWidth - popoverArrowDimensions.width
),
popoverArrowDimensions.width
);
}
if (alignment === 'center') {
return Math.max(
Math.min(
elementDimensions.left + elementDimensions.width / 2 - popoverDimensions?.realWidth / 2,
window.innerWidth - popoverDimensions?.realWidth - popoverArrowDimensions.width
),
popoverArrowDimensions.width
);
}
return 0;
}
export function repositionPopover(element: Element, step: DriveStep) {
const popover = getState('popover');
if (!popover) {
return;
}
const { align = 'start', side = 'left' } = step?.popover || {};
// Configure the popover positioning
const requiredAlignment: Alignment = align;
const requiredSide: Side = element.id === 'driver-dummy-element' ? 'over' : side;
const popoverPadding = getConfig('stagePadding') || 0;
const popoverDimensions = getPopoverDimensions()!;
const popoverArrowDimensions = popover.arrow.getBoundingClientRect();
const elementDimensions = element.getBoundingClientRect();
const topValue = elementDimensions.top - popoverDimensions!.height;
let isTopOptimal = topValue >= 0;
const bottomValue = window.innerHeight - (elementDimensions.bottom + popoverDimensions!.height);
let isBottomOptimal = bottomValue >= 0;
const leftValue = elementDimensions.left - popoverDimensions!.width;
let isLeftOptimal = leftValue >= 0;
const rightValue = window.innerWidth - (elementDimensions.right + popoverDimensions!.width);
let isRightOptimal = rightValue >= 0;
const noneOptimal = !isTopOptimal && !isBottomOptimal && !isLeftOptimal && !isRightOptimal;
let popoverRenderedSide: Side = requiredSide;
if (requiredSide === 'top' && isTopOptimal) {
isRightOptimal = isLeftOptimal = isBottomOptimal = false;
} else if (requiredSide === 'bottom' && isBottomOptimal) {
isRightOptimal = isLeftOptimal = isTopOptimal = false;
} else if (requiredSide === 'left' && isLeftOptimal) {
isRightOptimal = isTopOptimal = isBottomOptimal = false;
} else if (requiredSide === 'right' && isRightOptimal) {
isLeftOptimal = isTopOptimal = isBottomOptimal = false;
}
if (requiredSide === 'over') {
const leftToSet = window.innerWidth / 2 - popoverDimensions!.realWidth / 2;
const topToSet = window.innerHeight / 2 - popoverDimensions!.realHeight / 2;
popover.wrapper.style.left = `${leftToSet}px`;
popover.wrapper.style.right = `auto`;
popover.wrapper.style.top = `${topToSet}px`;
popover.wrapper.style.bottom = `auto`;
} else if (noneOptimal) {
const leftValue = window.innerWidth / 2 - popoverDimensions?.realWidth! / 2;
const bottomValue = 10;
popover.wrapper.style.left = `${leftValue}px`;
popover.wrapper.style.right = `auto`;
popover.wrapper.style.bottom = `${bottomValue}px`;
popover.wrapper.style.top = `auto`;
} else if (isLeftOptimal) {
const leftToSet = Math.min(
leftValue,
window.innerWidth - popoverDimensions?.realWidth - popoverArrowDimensions.width
);
const topToSet = calculateTopForLeftRight(requiredAlignment, {
elementDimensions,
popoverDimensions,
popoverPadding,
popoverArrowDimensions
});
popover.wrapper.style.left = `${leftToSet}px`;
popover.wrapper.style.top = `${topToSet}px`;
popover.wrapper.style.bottom = `auto`;
popover.wrapper.style.right = 'auto';
popoverRenderedSide = 'left';
} else if (isRightOptimal) {
const rightToSet = Math.min(
rightValue,
window.innerWidth - popoverDimensions?.realWidth - popoverArrowDimensions.width
);
const topToSet = calculateTopForLeftRight(requiredAlignment, {
elementDimensions,
popoverDimensions,
popoverPadding,
popoverArrowDimensions
});
popover.wrapper.style.right = `${rightToSet}px`;
popover.wrapper.style.top = `${topToSet}px`;
popover.wrapper.style.bottom = `auto`;
popover.wrapper.style.left = 'auto';
popoverRenderedSide = 'right';
} else if (isTopOptimal) {
const topToSet = Math.min(
topValue,
window.innerHeight - popoverDimensions!.realHeight - popoverArrowDimensions.width
);
let leftToSet = calculateLeftForTopBottom(requiredAlignment, {
elementDimensions,
popoverDimensions,
popoverPadding,
popoverArrowDimensions
});
popover.wrapper.style.top = `${topToSet}px`;
popover.wrapper.style.left = `${leftToSet}px`;
popover.wrapper.style.bottom = `auto`;
popover.wrapper.style.right = 'auto';
popoverRenderedSide = 'top';
} else if (isBottomOptimal) {
const bottomToSet = Math.min(
bottomValue,
window.innerHeight - popoverDimensions?.realHeight - popoverArrowDimensions.width
);
let leftToSet = calculateLeftForTopBottom(requiredAlignment, {
elementDimensions,
popoverDimensions,
popoverPadding,
popoverArrowDimensions
});
popover.wrapper.style.left = `${leftToSet}px`;
popover.wrapper.style.bottom = `${bottomToSet}px`;
popover.wrapper.style.top = `auto`;
popover.wrapper.style.right = 'auto';
popoverRenderedSide = 'bottom';
}
// Popover stays on the screen if the element scrolls out of the visible area.
// Render the arrow again to make sure it's in the correct position
// e.g. if element scrolled out of the screen to the top, the arrow should be rendered
// pointing to the top. If the element scrolled out of the screen to the bottom,
// the arrow should be rendered pointing to the bottom.
if (!noneOptimal) {
renderPopoverArrow(requiredAlignment, popoverRenderedSide, element);
} else {
popover.arrow.classList.add('driver-popover-arrow-none');
}
}
function renderPopoverArrow(alignment: Alignment, side: Side, element: Element) {
const popover = getState('popover');
if (!popover) {
return;
}
const elementDimensions = element.getBoundingClientRect();
const popoverDimensions = getPopoverDimensions()!;
const popoverArrow = popover.arrow;
const popoverWidth = popoverDimensions.width;
const windowWidth = window.innerWidth;
const elementWidth = elementDimensions.width;
const elementLeft = elementDimensions.left;
const popoverHeight = popoverDimensions.height;
const windowHeight = window.innerHeight;
const elementTop = elementDimensions.top;
const elementHeight = elementDimensions.height;
// Remove all arrow classes
popoverArrow.className = 'driver-popover-arrow';
let arrowSide = side;
let arrowAlignment = alignment;
if (side === 'top') {
if (elementLeft + elementWidth <= 0) {
arrowSide = 'right';
arrowAlignment = 'end';
} else if (elementLeft + elementWidth - popoverWidth <= 0) {
arrowSide = 'top';
arrowAlignment = 'start';
}
if (elementLeft >= windowWidth) {
arrowSide = 'left';
arrowAlignment = 'end';
} else if (elementLeft + popoverWidth >= windowWidth) {
arrowSide = 'top';
arrowAlignment = 'end';
}
} else if (side === 'bottom') {
if (elementLeft + elementWidth <= 0) {
arrowSide = 'right';
arrowAlignment = 'start';
} else if (elementLeft + elementWidth - popoverWidth <= 0) {
arrowSide = 'bottom';
arrowAlignment = 'start';
}
if (elementLeft >= windowWidth) {
arrowSide = 'left';
arrowAlignment = 'start';
} else if (elementLeft + popoverWidth >= windowWidth) {
arrowSide = 'bottom';
arrowAlignment = 'end';
}
} else if (side === 'left') {
if (elementTop + elementHeight <= 0) {
arrowSide = 'bottom';
arrowAlignment = 'end';
} else if (elementTop + elementHeight - popoverHeight <= 0) {
arrowSide = 'left';
arrowAlignment = 'start';
}
if (elementTop >= windowHeight) {
arrowSide = 'top';
arrowAlignment = 'end';
} else if (elementTop + popoverHeight >= windowHeight) {
arrowSide = 'left';
arrowAlignment = 'end';
}
} else if (side === 'right') {
if (elementTop + elementHeight <= 0) {
arrowSide = 'bottom';
arrowAlignment = 'start';
} else if (elementTop + elementHeight - popoverHeight <= 0) {
arrowSide = 'right';
arrowAlignment = 'start';
}
if (elementTop >= windowHeight) {
arrowSide = 'top';
arrowAlignment = 'start';
} else if (elementTop + popoverHeight >= windowHeight) {
arrowSide = 'right';
arrowAlignment = 'end';
}
} else {
}
if (!arrowSide) {
popoverArrow.classList.add('driver-popover-arrow-none');
} else {
popoverArrow.classList.add(`driver-popover-arrow-side-${arrowSide}`);
popoverArrow.classList.add(`driver-popover-arrow-align-${arrowAlignment}`);
}
}
function createPopover(popoverBodyComponent?: ReactNode): PopoverDOM {
const wrapper = document.createElement('div');
wrapper.classList.add('driver-popover');
const arrow = document.createElement('div');
arrow.classList.add('driver-popover-arrow');
const title = document.createElement('header');
title.id = 'driver-popover-title';
title.classList.add('driver-popover-title');
title.style.display = 'none';
title.innerText = 'Popover Title';
const description = document.createElement('div');
description.id = 'driver-popover-description';
description.classList.add('driver-popover-description');
description.style.display = 'none';
description.innerText = 'Popover description is here';
const popoverBody = document.createElement('div');
popoverBody.id = 'driver-popover-body';
popoverBody.classList.add('driver-popover-body');
popoverBody.style.display = 'block';
description.innerText = 'Popover body is here';
if (popoverBodyComponent) {
createRoot(popoverBody).render(popoverBodyComponent);
}
const closeButton = document.createElement('button');
closeButton.type = 'button';
closeButton.classList.add('driver-popover-close-btn');
closeButton.setAttribute('aria-label', 'Close');
closeButton.innerHTML = '&times;';
const footer = document.createElement('footer');
footer.classList.add('driver-popover-footer');
const progress = document.createElement('span');
progress.classList.add('driver-popover-progress-text');
progress.innerText = '';
const footerButtons = document.createElement('span');
footerButtons.classList.add('driver-popover-navigation-btns');
const previousButton = document.createElement('button');
previousButton.type = 'button';
previousButton.classList.add('driver-popover-prev-btn');
previousButton.innerHTML = '&larr; Previous';
const nextButton = document.createElement('button');
nextButton.type = 'button';
nextButton.classList.add('driver-popover-next-btn');
nextButton.innerHTML = 'Next &rarr;';
footerButtons.appendChild(previousButton);
footerButtons.appendChild(nextButton);
footer.appendChild(progress);
footer.appendChild(footerButtons);
wrapper.appendChild(closeButton);
wrapper.appendChild(arrow);
wrapper.appendChild(title);
wrapper.appendChild(description);
wrapper.appendChild(popoverBody);
wrapper.appendChild(footer);
return {
wrapper,
arrow,
title,
description,
footer,
previousButton,
nextButton,
closeButton,
footerButtons,
progress,
popoverBody
};
}
export function destroyPopover() {
const popover = getState('popover');
if (!popover) {
return;
}
// popover.wrapper.parentElement?.removeChild(popover.wrapper);
const popovers = document.querySelectorAll('.driver-popover');
popovers.forEach((popover) => {
popover.parentElement?.removeChild(popover);
});
}
+46
View File
@@ -0,0 +1,46 @@
import { StageDefinition } from './overlay';
import { PopoverDOM } from './popover';
import { DriveStep } from './driver';
export type State = {
isInitialized?: boolean;
activeIndex?: number;
activeElement?: Element;
activeStep?: DriveStep;
previousElement?: Element;
previousStep?: DriveStep;
popover?: PopoverDOM;
// actual values considering the animation
// and delays. These are used to determine
// the positions etc.
__previousElement?: Element;
__activeElement?: Element;
__previousStep?: DriveStep;
__activeStep?: DriveStep;
__activeOnDestroyed?: Element;
__resizeTimeout?: number;
__transitionCallback?: () => void;
__activeStagePosition?: StageDefinition;
__overlaySvg?: SVGSVGElement;
__overlaySkipBtn?: HTMLDivElement;
};
let currentState: State = {};
export function setState<K extends keyof State>(key: K, value: State[K]) {
currentState[key] = value;
}
export function getState(): State;
export function getState<K extends keyof State>(key: K): State[K];
export function getState<K extends keyof State>(key?: K) {
return key ? currentState[key] : currentState;
}
export function resetState() {
currentState = {};
}
+65
View File
@@ -0,0 +1,65 @@
import { getConfig } from "./config";
export function easeInOutQuad(elapsed: number, initialValue: number, amountOfChange: number, duration: number): number {
if ((elapsed /= duration / 2) < 1) {
return (amountOfChange / 2) * elapsed * elapsed + initialValue;
}
return (-amountOfChange / 2) * (--elapsed * (elapsed - 2) - 1) + initialValue;
}
export function getFocusableElements(parentEls: Element[] | HTMLElement[]) {
const focusableQuery =
'a[href]:not([disabled]), button:not([disabled]), textarea:not([disabled]), input[type="text"]:not([disabled]), input[type="radio"]:not([disabled]), input[type="checkbox"]:not([disabled]), select:not([disabled])';
return parentEls
.flatMap(parentEl => {
const isParentFocusable = parentEl.matches(focusableQuery);
const focusableEls: HTMLElement[] = Array.from(parentEl.querySelectorAll(focusableQuery));
return [...(isParentFocusable ? [parentEl as HTMLElement] : []), ...focusableEls];
})
.filter(el => {
return getComputedStyle(el).pointerEvents !== "none" && isElementVisible(el);
});
}
export function bringInView(element: Element) {
if (!element || isElementInView(element)) {
return;
}
const shouldSmoothScroll = getConfig("smoothScroll");
element.scrollIntoView({
// Removing the smooth scrolling for elements which exist inside the scrollable parent
// This was causing the highlight to not properly render
behavior: !shouldSmoothScroll || hasScrollableParent(element) ? "auto" : "smooth",
inline: "center",
block: "center",
});
}
function hasScrollableParent(e: Element) {
if (!e || !e.parentElement) {
return;
}
const parent = e.parentElement as HTMLElement & { scrollTopMax?: number };
return parent.scrollHeight > parent.clientHeight;
}
function isElementInView(element: Element) {
const rect = element.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
}
export function isElementVisible(el: HTMLElement) {
return !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length);
}
+8
View File
@@ -0,0 +1,8 @@
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"],
"extends": "../../tsconfig.web.json"
}
+22
View File
@@ -78,6 +78,9 @@ importers:
'@kubernetes/client-node':
specifier: ^0.18.1
version: 0.18.1
'@sealos/driver':
specifier: workspace:^
version: link:../packages/driver
'@sealos/ui':
specifier: workspace:^
version: link:../packages/ui
@@ -270,6 +273,22 @@ importers:
specifier: ^2.6.2
version: 2.6.2
packages/driver:
dependencies:
react:
specifier: ^18.2.0
version: 18.2.0
react-dom:
specifier: ^18.2.0
version: 18.2.0(react@18.2.0)
devDependencies:
'@types/react':
specifier: 18.2.37
version: 18.2.37
'@types/react-dom':
specifier: 18.0.11
version: 18.0.11
packages/ui:
dependencies:
react:
@@ -415,6 +434,9 @@ importers:
'@next/font':
specifier: 13.1.6
version: 13.1.6
'@sealos/driver':
specifier: workspace:^
version: link:../../packages/driver
'@tanstack/react-query':
specifier: ^4.35.3
version: 4.36.1(react-dom@18.2.0)(react@18.2.0)
+10 -9
View File
@@ -1,8 +1,8 @@
/** @type {import('next').NextConfig} */
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const { i18n } = require('./next-i18next.config');
const analyzer = process.env === 'production' ? [new BundleAnalyzerPlugin()] : [];
const path = require('path');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
const { i18n } = require('./next-i18next.config')
const analyzer = process.env === 'production' ? [new BundleAnalyzerPlugin()] : []
const path = require('path')
const nextConfig = {
i18n,
output: 'standalone',
@@ -15,13 +15,14 @@ const nextConfig = {
issuer: /\.[jt]sx?$/,
use: ['@svgr/webpack']
}
]);
config.plugins = [...config.plugins, ...analyzer];
return config;
])
config.plugins = [...config.plugins, ...analyzer]
return config
},
transpilePackages: ['@sealos/driver'],
experimental: {
outputFileTracingRoot: path.join(__dirname, '../../')
}
};
}
module.exports = nextConfig;
module.exports = nextConfig
@@ -20,6 +20,7 @@
"@emotion/styled": "^11.11.0",
"@kubernetes/client-node": "^0.18.1",
"@next/font": "13.1.6",
"@sealos/driver": "workspace:^",
"@tanstack/react-query": "^4.35.3",
"ansi_up": "^5.2.1",
"axios": "^1.5.1",
@@ -161,5 +161,22 @@
"Surplus": "Surplus"
},
"Store At Least One": "Store At Least One",
"Network port conflict": "Network port conflict"
}
"Network port conflict": "Network port conflict",
"skip": "skip",
"Can help you deploy any Docker image": "Can help you deploy any Docker image",
"Second-level domain name tips": "After turning it on, you will be automatically assigned an external second-level domain name.",
"Click the Deploy Application button": "Click the Deploy Application button",
"Click on any shadow to skip": "Click on any shadow to skip",
"Click here to visit the website": "Click here to visit the website",
"You can enter the container through the terminal": "You can enter the container through the terminal",
"Adjust application configuration": "Adjust application configuration",
"You have successfully deployed an application": "You have successfully deployed an application",
"receive": "receive",
"Balance": "Balance",
"First time completion guide benefits": "First time completion guide benefits",
"Go to recharge": "Recharge",
"let me think again": "let me think again",
"Configurable number of instances or automatic horizontal scaling": "Configurable number of instances or automatic horizontal scaling",
"gift time tip": "Within {{time}} hours ",
"gift amount tip": "Recharge {{amount}} and get {{gift}} free"
}
@@ -203,5 +203,22 @@
"user": {
"Insufficient account balance": "账号余额不足~"
},
"Store At Least One": "存储最少保留一个"
}
"Store At Least One": "存储最少保留一个",
"skip": "跳过",
"Can help you deploy any Docker image": "可以帮您部署任意 Docker 镜像",
"Second-level domain name tips": "开启后将自动为您分配一个外网二级域名",
"Click the Deploy Application button": "点击部署应用按钮",
"Click on any shadow to skip": "点击任意阴影跳过",
"Click here to visit the website": "点击这里访问网站",
"You can enter the container through the terminal": "可以通过终端进入容器",
"Adjust application configuration": "调整应用配置",
"You have successfully deployed an application": "您已成功部署一个应用",
"receive": "获得",
"Balance": "余额",
"First time completion guide benefits": "首次完成引导福利",
"Go to recharge": "去充值",
"let me think again": "我再想想",
"Configurable number of instances or automatic horizontal scaling": "可配置实例数或自动横向伸缩",
"gift time tip": "{{time}} 小时内",
"gift amount tip": "充值 {{amount}} 赠送 {{gift}} "
}
@@ -1,8 +1,9 @@
import { GET, POST } from '@/services/request';
import type { Response as InitDataType } from '@/pages/api/platform/getInitData';
import type { UserQuotaItemType, userPriceType } from '@/types/user';
import { AuthCnamePrams } from './params';
import { GET, POST } from '@/services/request';
import { EnvResponse } from '@/types';
import type { AccountCRD, UserQuotaItemType, userPriceType } from '@/types/user';
import { AuthCnamePrams } from './params';
import { UpdateUserGuideParams } from '@/pages/api/guide/updateGuide';
export const getResourcePrice = () => GET<userPriceType>('/api/platform/resourcePrice');
@@ -17,3 +18,10 @@ export const getUserQuota = () =>
export const postAuthCname = (data: AuthCnamePrams) => POST('/api/platform/authCname', data);
export const getPlatformEnv = () => GET<EnvResponse>('/api/platform/getEnv');
export const updateDesktopGuide = (payload: UpdateUserGuideParams) =>
POST('/api/guide/updateGuide', payload);
export const getUserAccount = () => GET<AccountCRD>('/api/guide/getAccount');
export const getPriceBonus = () => GET('/api/guide/getBonus');
@@ -0,0 +1,4 @@
export const GUIDE_LAUNCHPAD_CREATE_KEY = 'frontend.guide.launchpad.create';
export const GUIDE_LAUNCHPAD_GIFT_KEY = 'activity.beginner-guide.launchpad.giveAmount';
export const GUIDE_LAUNCHPAD_DETAIL_KEY = 'frontend.guide.launchpad.detail';
@@ -0,0 +1,346 @@
import { getPlatformEnv, getPriceBonus, getUserAccount, updateDesktopGuide } from '@/api/platform';
import { GUIDE_LAUNCHPAD_DETAIL_KEY, GUIDE_LAUNCHPAD_GIFT_KEY } from '@/constants/account';
import { Flex, FlexProps, Icon, Text } from '@chakra-ui/react';
import { DriveStep, driver } from '@sealos/driver';
import { useTranslation } from 'next-i18next';
import { useEffect, useMemo, useState } from 'react';
import { sealosApp } from 'sealos-desktop-sdk/app';
import { DriverStarIcon } from './useDriver';
import { formatMoney } from '@/utils/tools';
export default function useDriver() {
const { t, i18n } = useTranslation();
const [showGiftStep, setShowGiftStep] = useState(false);
const [activity, setActivity] = useState({
balance: 8,
limitDuration: '1',
amount: '8',
giftAmount: '8'
});
const PopoverBodyInfo = (props: FlexProps) => {
return (
<Flex
gap="4px"
id="popover-info"
position={'absolute'}
bottom={'-30px'}
color={'white'}
alignItems={'center'}
{...props}
>
<Text color={'#FFF'} fontSize={'12px'} fontWeight={500}>
{t('Click on any shadow to skip')}
</Text>
<Icon
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 20 20"
fill="white"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M7.50001 0.97229C7.88354 0.97229 8.19445 1.2832 8.19445 1.66673V2.91673C8.19445 3.30027 7.88354 3.61118 7.50001 3.61118C7.11648 3.61118 6.80556 3.30027 6.80556 2.91673V1.66673C6.80556 1.2832 7.11648 0.97229 7.50001 0.97229ZM2.84229 2.84235C3.11349 2.57116 3.55319 2.57116 3.82439 2.84235L4.70827 3.72624C4.97947 3.99744 4.97947 4.43713 4.70827 4.70833C4.43707 4.97953 3.99737 4.97953 3.72618 4.70833L2.84229 3.82445C2.5711 3.55325 2.5711 3.11355 2.84229 2.84235ZM12.2083 2.84235C12.4795 3.11355 12.4795 3.55325 12.2083 3.82445L11.3244 4.70833C11.0532 4.97953 10.6135 4.97953 10.3423 4.70833C10.0711 4.43713 10.0711 3.99744 10.3423 3.72624L11.2262 2.84235C11.4974 2.57116 11.9371 2.57116 12.2083 2.84235ZM8.05107 6.36654C8.05921 6.36904 8.06735 6.37155 8.07548 6.37406L17.4584 9.26437C17.6747 9.33097 17.8794 9.394 18.0383 9.45881C18.183 9.51788 18.4465 9.63567 18.6129 9.8969C18.8034 10.1959 18.8398 10.5678 18.7109 10.8981C18.5983 11.1866 18.3627 11.3532 18.2321 11.4393C18.0888 11.5336 17.9002 11.6352 17.7009 11.7425L13.8279 13.828L11.7424 17.701C11.6351 17.9003 11.5336 18.0889 11.4392 18.2322C11.3531 18.3628 11.1865 18.5984 10.898 18.711C10.5677 18.8399 10.1958 18.8035 9.8968 18.613C9.63557 18.4466 9.51779 18.1831 9.45872 18.0384C9.39391 17.8795 9.33088 17.6748 9.26429 17.4586L6.37399 8.07554C6.37148 8.0674 6.36897 8.05926 6.36647 8.05112C6.31285 7.87717 6.25951 7.70409 6.2288 7.55823C6.19822 7.413 6.15971 7.17003 6.25365 6.91402C6.36609 6.60762 6.60756 6.36615 6.91396 6.25371C7.16997 6.15977 7.41294 6.19829 7.55817 6.22886C7.70404 6.25957 7.87712 6.31292 8.05107 6.36654ZM7.71679 7.71686L10.564 16.9599L12.609 13.1621C12.6112 13.158 12.6136 13.1535 12.6162 13.1486C12.6422 13.0999 12.6884 13.0133 12.7511 12.9339C12.8047 12.8661 12.866 12.8048 12.9339 12.7512C13.0132 12.6884 13.0999 12.6422 13.1485 12.6163C13.1535 12.6137 13.158 12.6112 13.1621 12.609L16.9598 10.5641L7.71679 7.71686ZM0.972229 7.50007C0.972229 7.11654 1.28314 6.80562 1.66667 6.80562H2.91667C3.3002 6.80562 3.61112 7.11654 3.61112 7.50007C3.61112 7.8836 3.3002 8.19451 2.91667 8.19451H1.66667C1.28314 8.19451 0.972229 7.8836 0.972229 7.50007ZM4.70827 10.3424C4.97947 10.6136 4.97947 11.0532 4.70827 11.3244L3.82439 12.2083C3.55319 12.4795 3.11349 12.4795 2.84229 12.2083C2.5711 11.9371 2.5711 11.4974 2.84229 11.2262L3.72618 10.3424C3.99737 10.0712 4.43707 10.0712 4.70827 10.3424Z"
/>
</Icon>
</Flex>
);
};
const baseSteps: DriveStep[] = [
{
element: '.driver-detail-network-public',
popover: {
side: 'left',
align: 'start',
borderRadius: '12px 12px 0px 12px',
PopoverBody: (
<Flex gap={'6px'}>
<DriverStarIcon />
<Text color={'#24282C'} fontSize={'12px'}>
{t('Click here to visit the website')}
</Text>
<PopoverBodyInfo top={'-120px'} />
</Flex>
)
}
},
{
element: '.driver-detail-terminal',
popover: {
side: 'left',
align: 'center',
borderRadius: '12px 12px 0px 12px',
PopoverBody: (
<Flex gap={'6px'}>
<DriverStarIcon />
<Text color={'#24282C'} fontSize={'12px'}>
{t('You can enter the container through the terminal')}
</Text>
<PopoverBodyInfo top={'-120px'} />
</Flex>
)
}
},
{
element: '.driver-detail-update-button',
popover: {
side: 'bottom',
align: 'start',
borderRadius: '12px 12px 0px 12px',
PopoverBody: (
<Flex gap={'6px'}>
<DriverStarIcon />
<Text color={'#24282C'} fontSize={'12px'}>
{t('Adjust application configuration')}
</Text>
<PopoverBodyInfo top={'80px'} />
</Flex>
)
}
}
];
const giftStep: DriveStep[] = [
{
popover: {
borderRadius: '12px 12px 12px 12px',
PopoverBody: (
<Flex flexDirection={'column'} alignItems={'center'} padding={'27px 40px'} w="450px">
<Flex
w="100%"
color={'#24282C'}
fontSize={'14px'}
fontWeight={500}
bg="#F6EEFA"
borderRadius={'8px'}
p={'16px'}
alignItems={'center'}
>
<DriverStarIcon />
<Text ml="8px">{t('You have successfully deployed an application')}</Text>
<Text color={'#5A646E'} fontSize={'12px '} fontWeight={500} ml="auto">
{t('receive')}
</Text>
<Text ml="12px">{activity.balance}</Text>
<Icon w="16px" h={'16px'} viewBox="0 0 43 43" mx="4px">
<path
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
d="M38.8452 16.3383C38.8452 16.3383 38.169 13.0791 34.6177 12.5018C34.6177 12.5018 32.91 5.22138 26.476 8.01336C26.476 8.01336 21.7775 1.34481 16.7833 8.01336C16.7833 8.01336 10.8267 4.86984 8.35511 12.513C8.35511 12.513 4.03522 12.6233 4.00011 18.5272C3.96499 24.4311 12.4728 30.8168 13.6867 31.3741C13.6867 31.3741 11.0158 34.2719 14.2876 36.284C18.8172 38.6577 27.3756 37.7936 29.4606 35.9326C30.7532 34.7789 30.6523 33.0064 29.4606 31.5498C29.4556 31.5548 40.4202 25.1038 38.8452 16.3383ZM31.6228 11.4841C29.9736 8.80397 26.7453 10.5751 26.7453 10.5751C27.9505 20.3212 24.1256 29.8479 24.1256 29.8479C27.7512 28.7132 33.6121 14.7169 31.6228 11.4841ZM21.1951 7.66792C21.1951 7.66792 18.5641 7.83387 18.1203 11.4413C17.6764 15.0488 18.7299 29.0557 21.5427 30.0673C21.5427 30.0673 23.5266 30.5865 24.8902 18.319C26.6924 6.40218 21.1951 7.66792 21.1951 7.66792ZM16.2936 10.5751C16.2936 10.5751 13.488 8.61467 11.4786 11.4841C9.49465 14.7169 15.3556 28.7132 18.9759 29.8479C18.9759 29.8479 15.2143 21.3388 16.2936 10.5751ZM8.87726 14.9454C8.87726 14.9454 6.13705 14.9454 6.43049 18.7633C6.79949 23.5643 12.9309 28.6588 15.2143 29.5044C15.2089 29.5044 10.0763 22.326 8.87726 14.9454ZM15.1097 34.6383C15.8744 35.9813 24.6742 36.3866 28.102 34.8719C28.102 34.8719 29.1783 33.7787 27.532 31.5994C27.4731 31.5216 26.8769 31.5791 26.5874 31.5627C26.2978 31.5463 26.0133 31.4758 25.6763 31.4068C25.6763 31.4068 24.6168 32.1662 22.9437 31.5994C22.9437 31.5994 21.7614 32.2601 20.3823 31.5994C20.3823 31.5994 18.6756 32.1821 17.6497 31.4068C17.6497 31.4068 16.9291 31.6032 15.8719 31.4068C15.8719 31.4068 14.3671 33.3342 15.1097 34.6383ZM34.2051 14.9533C32.8914 23.1395 27.9505 29.5113 27.9505 29.5113C30.9382 28.8784 36.0244 22.5811 36.6816 18.7633C37.3388 14.9454 34.2051 14.9533 34.2051 14.9533Z"
/>
</Icon>
<Text>{t('Balance')}</Text>
</Flex>
<Flex
alignItems={'center'}
justifyContent={'center'}
color={'#24282C'}
fontSize={'14px'}
fontWeight={500}
mt="42px"
>
<Icon
xmlns="http://www.w3.org/2000/svg"
width="16px"
height="16px"
viewBox="0 0 16 16"
fill="none"
>
<g clipPath="url(#clip0_45_25863)">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M4.99984 1.99984C4.73462 1.99984 4.48027 2.10519 4.29273 2.29273C4.10519 2.48027 3.99984 2.73462 3.99984 2.99984C3.99984 3.26505 4.10519 3.51941 4.29273 3.70694C4.48027 3.89448 4.73462 3.99984 4.99984 3.99984H7.10252C7.02114 3.78691 6.91811 3.55278 6.79069 3.32112C6.37138 2.55873 5.79577 1.99984 4.99984 1.99984ZM7.33317 5.33317V8.6665H1.99984V6.79984C1.99984 6.41547 2.00036 6.16729 2.01579 5.97839C2.03059 5.79729 2.05571 5.73014 2.0725 5.69718C2.13642 5.57174 2.2384 5.46975 2.36384 5.40583C2.39681 5.38904 2.46395 5.36392 2.64506 5.34912C2.83395 5.33369 3.08214 5.33317 3.46651 5.33317H7.33317ZM2.89338 4.00348C2.74541 3.69289 2.6665 3.35043 2.6665 2.99984C2.6665 2.381 2.91234 1.78751 3.34992 1.34992C3.78751 0.912337 4.381 0.666504 4.99984 0.666504C6.53724 0.666504 7.46163 1.77427 7.95898 2.67856C7.97285 2.70377 7.98646 2.72896 7.99984 2.75411C8.01321 2.72896 8.02683 2.70377 8.04069 2.67856C8.53805 1.77427 9.46244 0.666504 10.9998 0.666504C11.6187 0.666504 12.2122 0.912337 12.6498 1.34992C13.0873 1.78751 13.3332 2.381 13.3332 2.99984C13.3332 3.35043 13.2543 3.69289 13.1063 4.00348C13.2351 4.0063 13.3543 4.01132 13.4632 4.02022C13.7268 4.04175 13.9889 4.0893 14.2412 4.21783C14.6175 4.40957 14.9234 4.71553 15.1152 5.09186C15.2437 5.34411 15.2913 5.60626 15.3128 5.86982C15.3332 6.11953 15.3332 6.4227 15.3332 6.77413V12.5589C15.3332 12.9103 15.3332 13.2135 15.3128 13.4632C15.2913 13.7268 15.2437 13.9889 15.1152 14.2411C14.9234 14.6175 14.6175 14.9234 14.2412 15.1152C13.9889 15.2437 13.7268 15.2913 13.4632 15.3128C13.2135 15.3332 12.9103 15.3332 12.5588 15.3332H7.99984L3.44079 15.3332C3.08937 15.3332 2.7862 15.3332 2.53649 15.3128C2.27292 15.2913 2.01078 15.2437 1.75852 15.1152C1.3822 14.9234 1.07624 14.6175 0.884492 14.2411C0.755963 13.9889 0.708419 13.7268 0.686885 13.4632C0.666482 13.2135 0.666493 12.9103 0.666504 12.5589L0.666505 9.33357L0.666505 6.77414C0.666493 6.42271 0.666482 6.11953 0.686885 5.86982C0.708419 5.60626 0.755963 5.34411 0.884492 5.09186C1.07624 4.71553 1.3822 4.40957 1.75852 4.21782C2.01078 4.08929 2.27292 4.04175 2.53649 4.02022C2.64542 4.01132 2.76453 4.0063 2.89338 4.00348ZM1.99984 9.99984L1.99984 12.5332C1.99984 12.9175 2.00036 13.1657 2.01579 13.3546C2.03059 13.5357 2.05571 13.6029 2.0725 13.6358C2.13642 13.7613 2.2384 13.8633 2.36384 13.9272C2.39681 13.944 2.46395 13.9691 2.64506 13.9839C2.83395 13.9993 3.08214 13.9998 3.4665 13.9998H7.33317V9.99984H1.99984ZM8.6665 9.99984V13.9998H12.5332C12.9175 13.9998 13.1657 13.9993 13.3546 13.9839C13.5357 13.9691 13.6029 13.944 13.6358 13.9272C13.7613 13.8633 13.8633 13.7613 13.9272 13.6358C13.944 13.6029 13.9691 13.5357 13.9839 13.3546C13.9993 13.1657 13.9998 12.9175 13.9998 12.5332V9.99984H8.6665ZM13.9998 8.6665V6.79984C13.9998 6.41547 13.9993 6.16729 13.9839 5.97839C13.9691 5.79729 13.944 5.73014 13.9272 5.69718C13.8633 5.57174 13.7613 5.46975 13.6358 5.40583C13.6029 5.38904 13.5357 5.36392 13.3546 5.34912C13.1657 5.33369 12.9175 5.33317 12.5332 5.33317L8.6665 5.33317V8.6665H13.9998ZM8.89716 3.99984C8.97854 3.78691 9.08157 3.55278 9.20898 3.32112C9.62829 2.55873 10.2039 1.99984 10.9998 1.99984C11.2651 1.99984 11.5194 2.10519 11.7069 2.29273C11.8945 2.48027 11.9998 2.73462 11.9998 2.99984C11.9998 3.26505 11.8945 3.51941 11.7069 3.70694C11.5194 3.89448 11.2651 3.99984 10.9998 3.99984H8.89716Z"
fill="url(#paint0_linear_45_25863)"
/>
</g>
<defs>
<linearGradient
id="paint0_linear_45_25863"
x1="7.99984"
y1="0.666504"
x2="7.99984"
y2="15.3332"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#FFC3A4" />
<stop offset="0.432292" stopColor="#FE8BA7" />
<stop offset="1" stopColor="#8695FF" />
</linearGradient>
<clipPath id="clip0_45_25863">
<rect width="16" height="16" fill="white" />
</clipPath>
</defs>
</Icon>
<Text ml="8px">{t('First time completion guide benefits')}</Text>
</Flex>
<Flex
justifyContent={'center'}
fontSize={i18n.language === 'en' ? '18px' : '24px'}
fontWeight={500}
mt="16px"
>
<Text color={'#24282C'} pr="4px">
{t('gift time tip', { time: activity.limitDuration })}
</Text>
<Text color={'#219BF4'}>
{t('gift amount tip', {
amount: activity.amount,
gift: activity.giftAmount
})}
</Text>
</Flex>
<Flex
mt={'32px'}
bg="#24282C"
borderRadius={'4px'}
alignItems={'center'}
justifyContent={'center'}
w={'179px'}
h={'36px'}
color={'#FFF'}
fontSize={'12px'}
fontWeight={500}
cursor={'pointer'}
onClick={() => {
console.log('充值');
driverObj.destroy();
openCostCenterApp();
}}
>
{t('Go to recharge')}
</Flex>
<Text
mt="16px"
cursor={'pointer'}
color={'#7B838B'}
fontSize={'12px'}
fontWeight={500}
onClick={() => {
driverObj.destroy();
}}
>
{t('let me think again')}
</Text>
</Flex>
),
onPopoverRender: () => {
const svg = driverObj.getState('__overlaySvg');
if (svg) {
const pathElement = svg.querySelector('path');
if (pathElement) {
pathElement.style.pointerEvents = 'none';
}
}
}
}
}
];
const driverConfig = useMemo(() => {
return {
disableActiveInteraction: true,
showProgress: false,
allowClose: false,
allowClickMaskNextStep: true,
allowPreviousStep: false,
isShowButtons: false,
allowKeyboardControl: false,
overlaySkipButton: t('skip') || 'skip',
steps: showGiftStep ? [...baseSteps, ...giftStep] : baseSteps,
onDestroyed: () => {
console.log('onDestroyed Detail');
updateGuideStatus();
},
interceptSkipButtonClick: () => {
const skipButton = driverObj.getState('__overlaySkipBtn');
skipButton?.remove();
if (driverObj.isLastStep()) {
driverObj.destroy();
} else {
driverObj.drive(3);
}
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [showGiftStep]);
const driverObj = driver(driverConfig);
const openCostCenterApp = () => {
sealosApp.runEvents('openDesktopApp', {
appKey: 'system-costcenter',
query: {
openRecharge: 'true'
}
});
};
const updateGuideStatus = () => {
updateDesktopGuide({
activityType: 'beginner-guide',
phase: 'launchpad',
phasePage: 'detail',
shouldSendGift: false
}).catch((err) => {
console.log(err);
});
};
useEffect(() => {
const handleUserGuide = async () => {
try {
const systemEnv = await getPlatformEnv();
const userAccount = await getUserAccount();
const bonus = await getPriceBonus();
if (bonus?.data?.activities) {
const strategy = JSON.parse(bonus.data?.activities);
const activity = {
balance: formatMoney(
strategy?.['beginner-guide']?.phases?.launchpad?.giveAmount || 8000000
),
limitDuration: strategy?.[
'beginner-guide'
]?.phases?.launchpad?.RechargeDiscount?.limitDuration?.replace('h', ''),
amount: Object.entries(
strategy?.['beginner-guide']?.phases?.launchpad?.RechargeDiscount?.specialDiscount
)[0][0],
giftAmount:
Object.entries(
strategy?.['beginner-guide']?.phases?.launchpad?.RechargeDiscount?.specialDiscount
)[0][1] + ''
};
setActivity(activity);
}
if (systemEnv.guideEnabled && userAccount?.metadata?.annotations) {
const showGiftStep = !!userAccount.metadata.annotations?.[GUIDE_LAUNCHPAD_GIFT_KEY];
const isGuided = !!userAccount.metadata.annotations?.[GUIDE_LAUNCHPAD_DETAIL_KEY];
if (!isGuided) {
setShowGiftStep(showGiftStep);
startGuide();
}
}
} catch (error) {
console.log(error);
}
};
handleUserGuide();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const startGuide = () => {
driverObj.drive();
};
const closeGuide = () => {
driverObj.destroy();
};
return { startGuide, closeGuide };
}
@@ -0,0 +1,214 @@
import { getPlatformEnv, getUserAccount, updateDesktopGuide } from '@/api/platform';
import { GUIDE_LAUNCHPAD_CREATE_KEY } from '@/constants/account';
import { AppEditType } from '@/types/app';
import { Flex, FlexProps, Icon, Text } from '@chakra-ui/react';
import { driver } from '@sealos/driver';
import { useTranslation } from 'next-i18next';
import { useEffect, useState } from 'react';
import { UseFormReturn } from 'react-hook-form';
export function DriverStarIcon() {
return (
<Icon
width="20px"
height="20px"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3.74999 1.08801C4.0696 1.08801 4.32869 1.34711 4.32869 1.66672V3.17135H5.83332C6.15293 3.17135 6.41203 3.43044 6.41203 3.75005C6.41203 4.06966 6.15293 4.32875 5.83332 4.32875H4.32869V5.83338C4.32869 6.15299 4.0696 6.41209 3.74999 6.41209C3.43038 6.41209 3.17128 6.15299 3.17128 5.83338V4.32875H1.66666C1.34705 4.32875 1.08795 4.06966 1.08795 3.75005C1.08795 3.43044 1.34705 3.17135 1.66666 3.17135H3.17128V1.66672C3.17128 1.34711 3.43038 1.08801 3.74999 1.08801ZM10.8333 1.92135C11.0728 1.92135 11.2875 2.06882 11.3735 2.29231L12.8186 6.04969C13.0643 6.68839 13.1548 6.91199 13.2853 7.0956C13.4098 7.27064 13.5627 7.42357 13.7378 7.54803C13.9214 7.67859 14.145 7.76912 14.7837 8.01477L18.5411 9.45992C18.7646 9.54588 18.912 9.7606 18.912 10C18.912 10.2395 18.7646 10.4542 18.5411 10.5402L14.7837 11.9853C14.145 12.231 13.9214 12.3215 13.7378 12.4521C13.5627 12.5765 13.4098 12.7295 13.2853 12.9045C13.1548 13.0881 13.0643 13.3117 12.8186 13.9504L11.3735 17.7078C11.2875 17.9313 11.0728 18.0788 10.8333 18.0788C10.5939 18.0788 10.3792 17.9313 10.2932 17.7078L8.84804 13.9504C8.60239 13.3117 8.51186 13.0881 8.3813 12.9045C8.25684 12.7295 8.10391 12.5765 7.92887 12.4521C7.74526 12.3215 7.52166 12.231 6.88297 11.9853L3.12558 10.5402C2.90209 10.4542 2.75462 10.2395 2.75462 10C2.75462 9.7606 2.90209 9.54588 3.12558 9.45992L6.88296 8.01477C7.52166 7.76912 7.74526 7.67859 7.92887 7.54803C8.10391 7.42357 8.25684 7.27064 8.3813 7.0956C8.51186 6.91199 8.60239 6.68839 8.84804 6.04969L10.2932 2.29231C10.3791 2.06882 10.5939 1.92135 10.8333 1.92135ZM10.8333 4.11213L9.9283 6.46518C9.91949 6.48809 9.9108 6.51071 9.90222 6.53303C9.69245 7.07888 9.55023 7.44893 9.32456 7.76631C9.12512 8.04679 8.88006 8.29185 8.59958 8.49129C8.28221 8.71696 7.91216 8.85917 7.36631 9.06895C7.34398 9.07753 7.32137 9.08622 7.29845 9.09503L4.9454 10L7.29845 10.9051C7.32137 10.9139 7.34398 10.9226 7.36631 10.9312C7.91216 11.1409 8.28221 11.2831 8.59958 11.5088C8.88006 11.7083 9.12512 11.9533 9.32456 12.2338C9.55023 12.5512 9.69245 12.9212 9.90222 13.4671C9.9108 13.4894 9.91949 13.512 9.9283 13.5349L10.8333 15.888L11.7383 13.5349C11.7472 13.512 11.7558 13.4894 11.7644 13.4671C11.9742 12.9212 12.1164 12.5512 12.3421 12.2338C12.5415 11.9533 12.7866 11.7083 13.0671 11.5088C13.3844 11.2831 13.7545 11.1409 14.3003 10.9311C14.3227 10.9226 14.3453 10.9139 14.3682 10.9051L16.7212 10L14.3682 9.09503C14.3453 9.08622 14.3227 9.07753 14.3004 9.06896C13.7545 8.85918 13.3844 8.71696 13.0671 8.49129C12.7866 8.29185 12.5415 8.04679 12.3421 7.76631C12.1164 7.44894 11.9742 7.07889 11.7644 6.53304C11.7558 6.51071 11.7472 6.4881 11.7383 6.46518L10.8333 4.11213ZM3.74999 13.588C4.0696 13.588 4.32869 13.8471 4.32869 14.1667V15.6713H5.83332C6.15293 15.6713 6.41203 15.9304 6.41203 16.25C6.41203 16.5697 6.15293 16.8288 5.83332 16.8288H4.32869V18.3334C4.32869 18.653 4.0696 18.9121 3.74999 18.9121C3.43038 18.9121 3.17128 18.653 3.17128 18.3334V16.8288H1.66666C1.34705 16.8288 1.08795 16.5697 1.08795 16.25C1.08795 15.9304 1.34705 15.6713 1.66666 15.6713H3.17128V14.1667C3.17128 13.8471 3.43038 13.588 3.74999 13.588Z"
fill="url(#paint0_linear_124_3715)"
/>
<defs>
<linearGradient
id="paint0_linear_124_3715"
x1="9.99999"
y1="1.66672"
x2="9.99999"
y2="18.3334"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#FFC3A4" />
<stop offset="0.432292" stopColor="#FE8BA7" />
<stop offset="1" stopColor="#8695FF" />
</linearGradient>
</defs>
</Icon>
);
}
export default function useDriver({
formHook
}: {
formHook: UseFormReturn<AppEditType, any, undefined>;
}) {
const { t } = useTranslation();
const [isGuided, setIsGuided] = useState(true);
useEffect(() => {
const handleUserGuide = async () => {
try {
const systemEnv = await getPlatformEnv();
const userAccount = await getUserAccount();
if (systemEnv.guideEnabled && userAccount?.metadata?.annotations) {
const isGuided = !!userAccount.metadata.annotations?.[GUIDE_LAUNCHPAD_CREATE_KEY];
if (!isGuided) {
startGuide();
formHook.setValue('appName', 'hello-world-guide');
}
setIsGuided(isGuided);
}
} catch (error) {
console.log(error);
}
};
handleUserGuide();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const updateGuideStatus = () => {
updateDesktopGuide({
activityType: 'beginner-guide',
phase: 'launchpad',
phasePage: 'create',
shouldSendGift: false
}).catch((err) => {
console.log(err);
});
};
const PopoverBodyInfo = (props: FlexProps) => {
return (
<Flex
gap="4px"
id="popover-info"
position={'absolute'}
bottom={'-30px'}
color={'white'}
alignItems={'center'}
{...props}
>
<Text color={'#FFF'} fontSize={'12px'} fontWeight={500}>
{t('Click on any shadow to skip')}
</Text>
<Icon
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 20 20"
fill="white"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M7.50001 0.97229C7.88354 0.97229 8.19445 1.2832 8.19445 1.66673V2.91673C8.19445 3.30027 7.88354 3.61118 7.50001 3.61118C7.11648 3.61118 6.80556 3.30027 6.80556 2.91673V1.66673C6.80556 1.2832 7.11648 0.97229 7.50001 0.97229ZM2.84229 2.84235C3.11349 2.57116 3.55319 2.57116 3.82439 2.84235L4.70827 3.72624C4.97947 3.99744 4.97947 4.43713 4.70827 4.70833C4.43707 4.97953 3.99737 4.97953 3.72618 4.70833L2.84229 3.82445C2.5711 3.55325 2.5711 3.11355 2.84229 2.84235ZM12.2083 2.84235C12.4795 3.11355 12.4795 3.55325 12.2083 3.82445L11.3244 4.70833C11.0532 4.97953 10.6135 4.97953 10.3423 4.70833C10.0711 4.43713 10.0711 3.99744 10.3423 3.72624L11.2262 2.84235C11.4974 2.57116 11.9371 2.57116 12.2083 2.84235ZM8.05107 6.36654C8.05921 6.36904 8.06735 6.37155 8.07548 6.37406L17.4584 9.26437C17.6747 9.33097 17.8794 9.394 18.0383 9.45881C18.183 9.51788 18.4465 9.63567 18.6129 9.8969C18.8034 10.1959 18.8398 10.5678 18.7109 10.8981C18.5983 11.1866 18.3627 11.3532 18.2321 11.4393C18.0888 11.5336 17.9002 11.6352 17.7009 11.7425L13.8279 13.828L11.7424 17.701C11.6351 17.9003 11.5336 18.0889 11.4392 18.2322C11.3531 18.3628 11.1865 18.5984 10.898 18.711C10.5677 18.8399 10.1958 18.8035 9.8968 18.613C9.63557 18.4466 9.51779 18.1831 9.45872 18.0384C9.39391 17.8795 9.33088 17.6748 9.26429 17.4586L6.37399 8.07554C6.37148 8.0674 6.36897 8.05926 6.36647 8.05112C6.31285 7.87717 6.25951 7.70409 6.2288 7.55823C6.19822 7.413 6.15971 7.17003 6.25365 6.91402C6.36609 6.60762 6.60756 6.36615 6.91396 6.25371C7.16997 6.15977 7.41294 6.19829 7.55817 6.22886C7.70404 6.25957 7.87712 6.31292 8.05107 6.36654ZM7.71679 7.71686L10.564 16.9599L12.609 13.1621C12.6112 13.158 12.6136 13.1535 12.6162 13.1486C12.6422 13.0999 12.6884 13.0133 12.7511 12.9339C12.8047 12.8661 12.866 12.8048 12.9339 12.7512C13.0132 12.6884 13.0999 12.6422 13.1485 12.6163C13.1535 12.6137 13.158 12.6112 13.1621 12.609L16.9598 10.5641L7.71679 7.71686ZM0.972229 7.50007C0.972229 7.11654 1.28314 6.80562 1.66667 6.80562H2.91667C3.3002 6.80562 3.61112 7.11654 3.61112 7.50007C3.61112 7.8836 3.3002 8.19451 2.91667 8.19451H1.66667C1.28314 8.19451 0.972229 7.8836 0.972229 7.50007ZM4.70827 10.3424C4.97947 10.6136 4.97947 11.0532 4.70827 11.3244L3.82439 12.2083C3.55319 12.4795 3.11349 12.4795 2.84229 12.2083C2.5711 11.9371 2.5711 11.4974 2.84229 11.2262L3.72618 10.3424C3.99737 10.0712 4.43707 10.0712 4.70827 10.3424Z"
/>
</Icon>
</Flex>
);
};
const driverObj = driver({
showProgress: false,
allowClose: false,
allowClickMaskNextStep: true,
allowPreviousStep: false,
isShowButtons: false,
allowKeyboardControl: false,
overlaySkipButton: t('skip') || 'skip',
steps: [
{
element: '.driver-deploy-image',
popover: {
side: 'right',
align: 'center',
borderRadius: '0px 12px 12px 12px',
PopoverBody: (
<Flex gap={'6px'}>
<DriverStarIcon />
<Text color={'#24282C'} fontSize={'13px'}>
{t('Can help you deploy any Docker image')}
</Text>
<PopoverBodyInfo />
</Flex>
)
}
},
{
element: '.driver-deploy-instance',
popover: {
side: 'top',
align: 'start',
borderRadius: '12px 12px 12px 0px',
PopoverBody: (
<Flex gap={'6px'}>
<DriverStarIcon />
<Text color={'#24282C'} fontSize={'13px'}>
{t('Configurable number of instances or automatic horizontal scaling')}
</Text>
<PopoverBodyInfo top={'-120px'} />
</Flex>
)
}
},
{
element: '.driver-deploy-network-switch',
popover: {
side: 'top',
align: 'start',
borderRadius: '12px 12px 12px 0px',
PopoverBody: (
<Flex gap={'6px'}>
<DriverStarIcon />
<Text color={'#24282C'} fontSize={'13px'}>
{t('Second-level domain name tips')}
</Text>
<PopoverBodyInfo top={'-120px'} />
</Flex>
)
}
},
{
element: '.driver-deploy-button',
popover: {
side: 'left',
align: 'center',
borderRadius: '12px 12px 0px 12px',
PopoverBody: (
<Flex gap={'6px'}>
<DriverStarIcon />
<Text color={'#24282C'} fontSize={'13px'}>
{t('Click the Deploy Application button')}
</Text>
</Flex>
),
onPopoverRender: () => {
const svg = driverObj.getState('__overlaySvg');
if (svg) {
const pathElement = svg.querySelector('path');
if (pathElement) {
pathElement.style.pointerEvents = 'none';
}
}
}
}
}
],
onDestroyed: () => {
updateGuideStatus();
}
});
const startGuide = () => {
driverObj.drive();
};
const closeGuide = () => {
driverObj.destroy();
};
return { driverObj, startGuide, closeGuide, isGuided };
}
@@ -16,10 +16,10 @@ 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 { getPlatformEnv } from '@/api/platform';
import '@/styles/reset.scss';
import 'nprogress/nprogress.css';
import '@sealos/driver/src/driver.css';
//Binding events.
Router.events.on('routeChangeStart', () => NProgress.start());
@@ -174,6 +174,19 @@ const App = ({ Component, pageProps }: AppProps) => {
</Head>
<QueryClientProvider client={queryClient}>
<ChakraProvider theme={theme}>
<button
onClick={() => {
const lastLang = getLangStore();
let lang = lastLang === 'en' ? 'zh' : 'en';
if (lastLang !== lang) {
i18n.changeLanguage(lang);
setLangStore(lang);
setRefresh((state) => !state);
}
}}
>
changeLanguage
</button>
<Component {...pageProps} />
<ConfirmChild />
<Loading loading={loading} />
@@ -0,0 +1,36 @@
import { authSession } from '@/services/backend/auth';
import { jsonRes } from '@/services/backend/response';
import { ApiResp } from '@/services/kubernet';
import type { NextApiRequest, NextApiResponse } from 'next';
export default async function handler(req: NextApiRequest, res: NextApiResponse<ApiResp>) {
try {
const kubeconfig = await authSession(req.headers);
const domain = process.env.SEALOS_DOMAIN;
console.log(`https://${domain}/api/v1alpha/account/getAccount`);
const response = await fetch(`https://${domain}/api/v1alpha/account/getAccount`, {
method: 'GET',
headers: {
Authorization: encodeURIComponent(kubeconfig)
}
});
const result: {
code: number;
data: any;
message: string;
} = await response.json();
if (result.code !== 200) {
return jsonRes(res, { code: result.code, message: 'desktop api is err' });
} else {
return jsonRes(res, { data: result.data });
}
} catch (err: any) {
console.log(err);
jsonRes(res, {
code: 500,
error: err
});
}
}
@@ -0,0 +1,24 @@
import { authSession } from '@/services/backend/auth';
import { getK8s } from '@/services/backend/kubernetes';
import { jsonRes } from '@/services/backend/response';
import { ApiResp } from '@/services/kubernet';
import type { NextApiRequest, NextApiResponse } from 'next';
export default async function handler(req: NextApiRequest, res: NextApiResponse<ApiResp>) {
try {
const { k8sCore, namespace } = await getK8s({
kubeconfig: await authSession(req.headers)
});
const result = await k8sCore.readNamespacedConfigMap('recharge-gift', 'sealos');
jsonRes(res, {
data: result.body
});
} catch (err: any) {
jsonRes(res, {
code: 500,
error: err
});
}
}
@@ -0,0 +1,56 @@
import { authSession } from '@/services/backend/auth';
import { jsonRes } from '@/services/backend/response';
import { ApiResp } from '@/services/kubernet';
import type { NextApiRequest, NextApiResponse } from 'next';
export type UpdateUserGuideParams = {
activityType: 'beginner-guide';
phase: 'launchpad' | 'database' | 'template' | 'terminal';
phasePage: 'create' | 'detail' | 'index';
shouldSendGift: boolean;
};
export default async function handler(req: NextApiRequest, res: NextApiResponse<ApiResp>) {
try {
const { activityType, phase, phasePage, shouldSendGift } = req.body as UpdateUserGuideParams;
if (!activityType || !phase || !phasePage)
return jsonRes(res, { code: 400, message: 'Bad Request: Invalid parameters' });
const kubeconfig = await authSession(req.headers);
const domain = process.env.SEALOS_DOMAIN;
const payload = {
activityType,
phase,
phasePage,
shouldSendGift
};
const response = await fetch(`https://${domain}/api/v1alpha/account/updateGuide`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: encodeURIComponent(kubeconfig)
},
body: JSON.stringify(payload)
});
const result: {
code: number;
data: any;
message: string;
} = await response.json();
if (result.code !== 200) {
return jsonRes(res, { code: result.code, message: 'desktop api is err' });
} else {
return jsonRes(res, { data: result.data });
}
} catch (err: any) {
console.log(err);
jsonRes(res, {
code: 500,
error: err
});
}
}
@@ -6,7 +6,8 @@ import type { NextApiRequest, NextApiResponse } from 'next';
export default async function handler(req: NextApiRequest, res: NextApiResponse<ApiResp>) {
jsonRes<EnvResponse>(res, {
data: {
domain: process.env.SEALOS_DOMAIN || 'cloud.sealos.io'
domain: process.env.SEALOS_DOMAIN || 'cloud.sealos.io',
guideEnabled: process.env.GUIDE_ENABLED === 'true'
}
});
}
@@ -110,7 +110,7 @@ const AppMainInfo = ({ app = MOCK_APP_DETAIL }: { app: AppDetailType }) => {
</MyTooltip>
</Flex>
</th>
<th>
<th className="driver-detail-network-public">
<Flex alignItems={'center'} justifyContent={'space-between'}>
<MyTooltip
label={network.public ? t('Open Link') : ''}
@@ -158,6 +158,8 @@ const Header = ({
)}
{!isPause && (
<Button
className="driver-detail-update-button"
_focusVisible={{ boxShadow: '' }}
mr={5}
h={'40px'}
borderColor={'myGray.200'}
@@ -170,7 +170,7 @@ const Pods = ({
});
}}
>
<MyIcon name={'terminal'} w="20px" h="20px" />
<MyIcon className="driver-detail-terminal" name={'terminal'} w="20px" h="20px" />
</Center>
</MyTooltip>
<MyTooltip offset={[0, 10]} label={t('Details')}>
@@ -1,20 +1,22 @@
import React, { useMemo, useState } from 'react';
import useDetailDriver from '@/hooks/useDetailDriver';
import { useLoading } from '@/hooks/useLoading';
import { useToast } from '@/hooks/useToast';
import { MOCK_APP_DETAIL } from '@/mock/apps';
import { useAppStore } from '@/store/app';
import { useGlobalStore } from '@/store/global';
import { serviceSideProps } from '@/utils/i18n';
import { Box, Flex, useTheme } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import { useAppStore } from '@/store/app';
import { useToast } from '@/hooks/useToast';
import { useLoading } from '@/hooks/useLoading';
import { useGlobalStore } from '@/store/global';
import Header from './components/Header';
import AppBaseInfo from './components/AppBaseInfo';
import Pods from './components/Pods';
import dynamic from 'next/dynamic';
import { MOCK_APP_DETAIL } from '@/mock/apps';
import { serviceSideProps } from '@/utils/i18n';
import React, { useMemo, useState } from 'react';
import AppBaseInfo from './components/AppBaseInfo';
import Header from './components/Header';
import Pods from './components/Pods';
const AppMainInfo = dynamic(() => import('./components/AppMainInfo'), { ssr: false });
const AppDetail = ({ appName }: { appName: string }) => {
const { startGuide } = useDetailDriver();
const theme = useTheme();
const { toast } = useToast();
const { Loading } = useLoading();
@@ -29,7 +31,7 @@ const AppDetail = ({ appName }: { appName: string }) => {
const [podsLoaded, setPodsLoaded] = useState(false);
const [showSlider, setShowSlider] = useState(false);
const { refetch } = useQuery(['setAppDetail'], () => setAppDetail(appName), {
const { refetch, isSuccess } = useQuery(['setAppDetail'], () => setAppDetail(appName), {
onError(err) {
toast({
title: String(err),
@@ -413,7 +413,7 @@ const Form = ({
</Flex>
</FormControl>
{/* image */}
<Box mb={7}>
<Box mb={7} className="driver-deploy-image">
<Flex alignItems={'center'}>
<Label>{t('Image')}</Label>
<Tabs
@@ -504,6 +504,7 @@ const Form = ({
<Flex alignItems={'center'}>
<Label>{t('Deployment Mode')}</Label>
<Tabs
className="driver-deploy-instance"
w={'195px'}
size={'sm'}
list={[
@@ -797,6 +798,7 @@ const Form = ({
</Box>
<Flex alignItems={'center'} h={'35px'}>
<Switch
className="driver-deploy-network-switch"
size={'lg'}
colorScheme={'blackAlpha'}
isChecked={!!network.openPublicDomain}
@@ -1,13 +1,12 @@
import React, { useCallback } from 'react';
import { Box, Flex, Button } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import MyIcon from '@/components/Icon';
import JSZip from 'jszip';
import { useGlobalStore } from '@/store/global';
import type { YamlItemType } from '@/types/index';
import { downLoadBold } from '@/utils/tools';
import { Box, Button, Flex } from '@chakra-ui/react';
import dayjs from 'dayjs';
import { useGlobalStore } from '@/store/global';
import { useTranslation } from 'next-i18next';
import { useRouter } from 'next/router';
import { useCallback } from 'react';
const Header = ({
appName,
@@ -57,7 +56,15 @@ const Header = ({
>
{t('Export')} Yaml
</Button>
<Button px={4} minW={'140px'} h={'40px'} variant={'primary'} onClick={applyCb}>
<Button
className="driver-deploy-button"
px={4}
minW={'140px'}
h={'40px'}
variant={'primary'}
onClick={applyCb}
_focusVisible={{ boxShadow: '' }}
>
{t(applyBtnText)}
</Button>
</Flex>
@@ -1,35 +1,37 @@
import React, { useState, useCallback, useMemo, useRef, useEffect } from 'react';
import { useRouter } from 'next/router';
import { Flex, Box } from '@chakra-ui/react';
import type { YamlItemType } from '@/types';
import {
json2DeployCr,
json2Service,
json2Ingress,
json2ConfigMap,
json2Secret,
json2HPA
} from '@/utils/deployYaml2Json';
import { useForm } from 'react-hook-form';
import { defaultEditVal, editModeMap } from '@/constants/editApp';
import { postDeployApp, putApp } from '@/api/app';
import { updateDesktopGuide } from '@/api/platform';
import { noGpuSliderKey } from '@/constants/app';
import { defaultEditVal, editModeMap } from '@/constants/editApp';
import { useConfirm } from '@/hooks/useConfirm';
import useDriver from '@/hooks/useDriver';
import { useLoading } from '@/hooks/useLoading';
import { useToast } from '@/hooks/useToast';
import { useAppStore } from '@/store/app';
import { useGlobalStore } from '@/store/global';
import { useUserStore } from '@/store/user';
import type { YamlItemType } from '@/types';
import type { AppEditType, DeployKindsType } from '@/types/app';
import { adaptEditAppData } from '@/utils/adapt';
import { useToast } from '@/hooks/useToast';
import { useQuery } from '@tanstack/react-query';
import { useAppStore } from '@/store/app';
import { useLoading } from '@/hooks/useLoading';
import { useGlobalStore } from '@/store/global';
import Header from './components/Header';
import Form from './components/Form';
import Yaml from './components/Yaml';
import dynamic from 'next/dynamic';
import {
json2ConfigMap,
json2DeployCr,
json2HPA,
json2Ingress,
json2Secret,
json2Service
} from '@/utils/deployYaml2Json';
import { serviceSideProps } from '@/utils/i18n';
import { getErrText, patchYamlListV1, patchYamlList } from '@/utils/tools';
import { getErrText, patchYamlListV1 } from '@/utils/tools';
import { Box, Flex } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import { useTranslation } from 'next-i18next';
import { noGpuSliderKey } from '@/constants/app';
import { useUserStore } from '@/store/user';
import dynamic from 'next/dynamic';
import { useRouter } from 'next/router';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useForm } from 'react-hook-form';
import Form from './components/Form';
import Header from './components/Header';
import Yaml from './components/Yaml';
const ErrorModal = dynamic(() => import('./components/ErrorModal'));
@@ -87,10 +89,8 @@ export const formData2Yamls = (
const EditApp = ({ appName, tabType }: { appName?: string; tabType: string }) => {
const { t } = useTranslation();
const crOldYamls = useRef<DeployKindsType[]>([]);
const oldAppEditData = useRef<AppEditType>();
const { toast } = useToast();
const { Loading, setIsLoading } = useLoading();
const router = useRouter();
@@ -123,6 +123,8 @@ const EditApp = ({ appName, tabType }: { appName?: string; tabType: string }) =>
const formHook = useForm<AppEditType>({
defaultValues: defaultEditVal
});
const { isGuided, closeGuide } = useDriver({ formHook });
const realTimeForm = useRef(defaultEditVal);
// watch form change, compute new yaml
@@ -169,6 +171,16 @@ const EditApp = ({ appName, tabType }: { appName?: string; tabType: string }) =>
}
router.replace(`/app/detail?name=${formHook.getValues('appName')}`);
if (!isGuided) {
updateDesktopGuide({
activityType: 'beginner-guide',
phase: 'launchpad',
phasePage: 'create',
shouldSendGift: true
}).catch((err) => {
console.log(err);
});
}
toast({
title: t(applySuccess),
status: 'success'
@@ -193,7 +205,8 @@ const EditApp = ({ appName, tabType }: { appName?: string; tabType: string }) =>
t,
applySuccess,
userSourcePrice?.gpu,
refetchPrice
refetchPrice,
isGuided
]
);
const submitError = useCallback(() => {
@@ -292,7 +305,8 @@ const EditApp = ({ appName, tabType }: { appName?: string; tabType: string }) =>
title={title}
yamlList={yamlList}
applyBtnText={applyBtnText}
applyCb={() =>
applyCb={() => {
closeGuide();
formHook.handleSubmit((data) => {
const parseYamls = formData2Yamls(
data,
@@ -339,8 +353,8 @@ const EditApp = ({ appName, tabType }: { appName?: string; tabType: string }) =>
}
openConfirm(() => submitSuccess(parseYamls))();
}, submitError)()
}
}, submitError)();
}}
/>
<Box flex={'1 0 0'} h={0} w={'100%'} pb={4}>
@@ -24,3 +24,8 @@
white-space: nowrap;
overflow: hidden;
}
#driver-popover-content {
padding: 14px;
max-width: 600px;
}
+1
View File
@@ -24,4 +24,5 @@ declare global {
export type EnvResponse = {
domain: string;
guideEnabled: boolean;
};
+17
View File
@@ -35,3 +35,20 @@ export type UserQuotaItemType = {
used: number;
limit: number;
};
export type AccountCRD = {
apiVersion: string;
kind: string;
metadata: {
annotations: Record<string, string>;
creationTimestamp: string;
generation: number;
};
status: {
activityBonus: number;
balance: number;
deductionBalance: number;
encryptBalance: string;
encryptDeductionBalance: string;
};
};
@@ -20,8 +20,6 @@ export const json2DeployCr = (
handleType: 'edit' | 'create' = 'create',
crYamlList?: DeployKindsType[]
) => {
console.log(data, 'form data');
const totalStorage = data.storeList.reduce((acc, item) => acc + item.value, 0);
const metadata = {
@@ -480,3 +480,7 @@ export const getErrText = (err: any, def = '') => {
msg && console.log('error =>', msg);
return msg;
};
export const formatMoney = (mone: number) => {
return mone / 1000000;
};
@@ -1,4 +1,6 @@
// use client;
import yaml from 'js-yaml';
export const getUserKubeConfig = () => {
let kubeConfig: string =
process.env.NODE_ENV === 'development' ? process.env.NEXT_PUBLIC_MOCK_USER || '' : '';
+15 -4
View File
@@ -1,11 +1,22 @@
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
"@/*": [
"./src/*"
]
}
},
"include": ["next-env.d.ts", "src/**/*.ts", "src/**/*.tsx", "src/**/*.d.ts"],
"include": [
"next-env.d.ts",
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.d.ts"
],
"extends": "../../tsconfig.web.json"
}
}
@@ -21,9 +21,9 @@ import dayjs from 'dayjs';
import { useTranslation } from 'next-i18next';
import { MouseEvent, useCallback, useMemo } from 'react';
const MdPart = `![](https://raw.githubusercontent.com/labring-actions/templates/main/Deploy-on-Sealos.svg)](https://cloud.sealos.io/?openapp=system-fastdeploy%3FtemplateName%3Dfastgpt`;
const MdPart = `![](https://raw.githubusercontent.com/labring-actions/templates/main/Deploy-on-Sealos.svg)](https://cloud.sealos.io/?openapp=system-template%3FtemplateName%3Dfastgpt`;
const HtmlPart = `<a href="https://cloud.sealos.io/?openapp=system-fastdeploy%3FtemplateName%3Dfastgpt"><img src="https://raw.githubusercontent.com/labring-actions/templates/main/Deploy-on-Sealos.svg" alt="Deploy on Sealos"/></a>`;
const HtmlPart = `<a href="https://cloud.sealos.io/?openapp=system-template%3FtemplateName%3Dfastgpt"><img src="https://raw.githubusercontent.com/labring-actions/templates/main/Deploy-on-Sealos.svg" alt="Deploy on Sealos"/></a>`;
const Header = ({
appName,
@@ -70,7 +70,7 @@ const Header = ({
};
const copyTemplateLink = () => {
const str = `https://${cloudDomain}/?openapp=system-fastdeploy%3FtemplateName%3D${appName}`;
const str = `https://${cloudDomain}/?openapp=system-template%3FtemplateName%3D${appName}`;
copyData(str);
};
@@ -85,8 +85,7 @@ const Header = ({
h={'80px'}
borderRadius={'8px'}
backgroundColor={'#FBFBFC'}
border={' 1px solid rgba(255, 255, 255, 0.50)'}
>
border={' 1px solid rgba(255, 255, 255, 0.50)'}>
<Image src={templateDetail?.spec?.icon} alt="" width={'60px'} height={'60px'} />
</Flex>
<Flex ml={'24px'} w="520px" flexDirection={'column'}>
@@ -103,8 +102,7 @@ const Header = ({
_hover={{
background: '#F4F6F8'
}}
onClick={(e) => goGithub(e, templateDetail?.spec?.gitRepo)}
>
onClick={(e) => goGithub(e, templateDetail?.spec?.gitRepo)}>
<HomePageIcon />
<Text fontSize={'12px '} fontWeight={400} pl="6px">
{t('Home Page')}
@@ -121,8 +119,7 @@ const Header = ({
alignItems={'center'}
_hover={{
background: '#F4F6F8'
}}
>
}}>
<ShareIcon />
<Text fontSize={'12px '} fontWeight={400} color={'#485058'} pl="6px">
{t('Share')}
@@ -137,8 +134,7 @@ const Header = ({
w="60px"
flexDirection={'column'}
justifyContent={'center'}
alignItems={'center'}
>
alignItems={'center'}>
<Flex {...IconBox}>
<CopyLinkIcon />
</Flex>
@@ -154,8 +150,7 @@ const Header = ({
<Flex
flexDirection={'column'}
alignItems={'center'}
onClick={() => copyData(HtmlPart)}
>
onClick={() => copyData(HtmlPart)}>
<Flex {...IconBox}>
<HtmlIcon />
</Flex>
@@ -167,8 +162,7 @@ const Header = ({
flexDirection={'column'}
alignItems={'center'}
ml="auto"
onClick={() => copyData(MdPart)}
>
onClick={() => copyData(MdPart)}>
<Flex {...IconBox}>
<MdIcon />
</Flex>
@@ -194,8 +188,7 @@ const Header = ({
mt={'8px'}
fontSize={'12px'}
color={'5A646E'}
fontWeight={400}
>
fontWeight={400}>
{templateDetail?.spec?.description}
</Text>
</Tooltip>
@@ -210,8 +203,7 @@ const Header = ({
bg={'myWhite.600'}
borderColor={'myGray.200'}
variant={'base'}
onClick={handleExportYaml}
>
onClick={handleExportYaml}>
{t('Export')} Yaml
</Button>
<Button px={4} minW={'140px'} h={'40px'} variant={'primary'} onClick={applyCb}>
@@ -136,7 +136,7 @@ export default function EditApp({ appName }: { appName?: string }) {
setCached(JSON.stringify({ ...formHook.getValues(), cachedKey: templateName }));
const _name = encodeURIComponent(`?templateName=${templateName}&sealos_inside=true`);
const _domain = platformEnvs?.SEALOS_CLOUD_DOMAIN;
const href = `https://${_domain}/?openapp=system-fastdeploy${_name}`;
const href = `https://${_domain}/?openapp=system-template${_name}`;
return window.open(href, '_self');
}
const yamls = yamlList.map((item) => item.value);
@@ -231,7 +231,7 @@ export default function EditApp({ appName }: { appName?: string }) {
);
const copyTemplateLink = () => {
const str = `https://${platformEnvs?.SEALOS_CLOUD_DOMAIN}/?openapp=system-fastdeploy%3FtemplateName%3D${appName}`;
const str = `https://${platformEnvs?.SEALOS_CLOUD_DOMAIN}/?openapp=system-template%3FtemplateName%3D${appName}`;
copyData(str);
};
@@ -256,8 +256,7 @@ export default function EditApp({ appName }: { appName?: string }) {
overflow={'auto'}
position={'relative'}
borderRadius={'12px'}
background={'linear-gradient(180deg, #FFF 0%, rgba(255, 255, 255, 0.70) 100%)'}
>
background={'linear-gradient(180deg, #FFF 0%, rgba(255, 255, 255, 0.70) 100%)'}>
<Flex
zIndex={99}
position={'sticky'}
@@ -269,15 +268,13 @@ export default function EditApp({ appName }: { appName?: string }) {
justifyContent={'start'}
alignItems={'center'}
backgroundColor={'rgba(255, 255, 255)'}
backdropBlur={'100px'}
>
backdropBlur={'100px'}>
<Flex
alignItems={'center'}
fontWeight={500}
fontSize={16}
color={'#7B838B'}
cursor={'pointer'}
>
cursor={'pointer'}>
<Flex
alignItems={'center'}
css={{
@@ -288,16 +285,14 @@ export default function EditApp({ appName }: { appName?: string }) {
fill: '#219BF4'
}
}
}}
>
}}>
<Icon
ml={'19px'}
viewBox="0 0 15 15"
fill={'#24282C'}
w={'15px'}
h="15px"
onClick={() => router.push('/')}
>
onClick={() => router.push('/')}>
<path d="M9.1875 13.1875L3.92187 7.9375C3.85937 7.875 3.81521 7.80729 3.78937 7.73438C3.76312 7.66146 3.75 7.58333 3.75 7.5C3.75 7.41667 3.76312 7.33854 3.78937 7.26562C3.81521 7.19271 3.85937 7.125 3.92187 7.0625L9.1875 1.79687C9.33333 1.65104 9.51562 1.57812 9.73438 1.57812C9.95312 1.57812 10.1406 1.65625 10.2969 1.8125C10.4531 1.96875 10.5312 2.15104 10.5312 2.35938C10.5312 2.56771 10.4531 2.75 10.2969 2.90625L5.70312 7.5L10.2969 12.0938C10.4427 12.2396 10.5156 12.4192 10.5156 12.6325C10.5156 12.8463 10.4375 13.0312 10.2812 13.1875C10.125 13.3438 9.94271 13.4219 9.73438 13.4219C9.52604 13.4219 9.34375 13.3438 9.1875 13.1875Z" />
</Icon>
<Text ml="4px" onClick={() => router.push('/')}>
@@ -308,8 +303,7 @@ export default function EditApp({ appName }: { appName?: string }) {
<Text
onClick={copyTemplateLink}
_hover={{ fill: '#219BF4', color: '#219BF4' }}
color={router.pathname === '/deploy' ? '#262A32' : '#7B838B'}
>
color={router.pathname === '/deploy' ? '#262A32' : '#7B838B'}>
{data?.templateYaml?.metadata?.name}
</Text>
</Flex>
@@ -320,8 +314,7 @@ export default function EditApp({ appName }: { appName?: string }) {
flexDirection={'column'}
width={'100%'}
flexGrow={1}
backgroundColor={'rgba(255, 255, 255, 0.90)'}
>
backgroundColor={'rgba(255, 255, 255, 0.90)'}>
<Header
cloudDomain={platformEnvs?.SEALOS_CLOUD_DOMAIN || ''}
templateDetail={data?.templateYaml!}