mirror of
https://github.com/labring/sealos.git
synced 2026-09-24 15:46:19 +08:00
refactor: adjusted license billing based on cluster size (#4853)
* refactor: adjusted license billing based on cluster size * One-click purchase * add active cluster * license app add cronjob Signed-off-by: jingyang <3161362058@qq.com> * fix default time * add Asia/Shanghai * Delete Old Notifications * fix ci --------- Signed-off-by: jingyang <3161362058@qq.com>
This commit is contained in:
Generated
+3
@@ -1695,6 +1695,9 @@ importers:
|
||||
next-i18next:
|
||||
specifier: ^13.3.0
|
||||
version: 13.3.0(i18next@22.5.1)(next@13.1.6)(react-i18next@12.3.1)(react@18.2.0)
|
||||
node-cron:
|
||||
specifier: ^3.0.3
|
||||
version: 3.0.3
|
||||
nprogress:
|
||||
specifier: ^0.2.0
|
||||
version: 0.2.0
|
||||
|
||||
@@ -25,6 +25,37 @@ subjects:
|
||||
namespace: license-frontend
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: notification-manager
|
||||
rules:
|
||||
- apiGroups:
|
||||
- notification.sealos.io
|
||||
resources:
|
||||
- notifications
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: license-frontend-notification-manager-role-binding
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: notification-manager
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: license-frontend
|
||||
namespace: license-frontend
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: license-frontend-node-reader-rolebinding
|
||||
@@ -96,6 +127,15 @@ spec:
|
||||
# do not modify this image, it is used for CI/CD
|
||||
image: ghcr.io/labring/sealos-license-frontend:latest
|
||||
imagePullPolicy: Always
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /api/cronjob/init
|
||||
port: 3000
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 3
|
||||
timeoutSeconds: 3
|
||||
successThreshold: 1
|
||||
failureThreshold: 3
|
||||
volumeMounts:
|
||||
- name: license-frontend-volume
|
||||
mountPath: /config.yaml
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"nanoid": "^4.0.2",
|
||||
"next": "13.1.6",
|
||||
"next-i18next": "^13.3.0",
|
||||
"node-cron": "^3.0.3",
|
||||
"nprogress": "^0.2.0",
|
||||
"prettier": "^2.8.8",
|
||||
"react": "18.2.0",
|
||||
|
||||
@@ -6,3 +6,8 @@ export const applyLicense = (yamlList: string[], type: 'create' | 'replace' | 'u
|
||||
|
||||
export const getLicenseRecord = ({ page = 1, pageSize = 10 }: { page: number; pageSize: number }) =>
|
||||
GET<LicenseCR[]>('/api/license/getLicense', { page, pageSize });
|
||||
|
||||
export const getLicenseByName = ({ name }: { name: string }) =>
|
||||
GET<LicenseCR>(`/api/license/get?name=${name}`);
|
||||
|
||||
export const checkLicenses = () => GET(`/api/license/checkLicenses`);
|
||||
|
||||
@@ -28,9 +28,10 @@ interface Props extends BoxProps {
|
||||
fileExtension: string;
|
||||
files: FileItemType[];
|
||||
setFiles: React.Dispatch<React.SetStateAction<FileItemType[]>>;
|
||||
multiple: boolean;
|
||||
}
|
||||
|
||||
const FileSelect = ({ fileExtension, setFiles, files, ...props }: Props) => {
|
||||
const FileSelect = ({ fileExtension, setFiles, files, multiple, ...props }: Props) => {
|
||||
const { Loading: FileSelectLoading } = useLoading();
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
@@ -39,7 +40,7 @@ const FileSelect = ({ fileExtension, setFiles, files, ...props }: Props) => {
|
||||
|
||||
const { File, onOpen } = useSelectFile({
|
||||
fileType: fileExtension,
|
||||
multiple: true
|
||||
multiple: multiple
|
||||
});
|
||||
|
||||
const onSelectFile = useCallback(
|
||||
@@ -49,8 +50,6 @@ const FileSelect = ({ fileExtension, setFiles, files, ...props }: Props) => {
|
||||
// Parse file by file
|
||||
let promise = Promise.resolve<FileItemType[]>([]);
|
||||
files.forEach((file) => {
|
||||
console.log(file, 'fiel');
|
||||
|
||||
promise = promise.then(async (result) => {
|
||||
const extension = file?.name?.split('.')?.pop()?.toLowerCase();
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const LicenseFrontendKey = 'cloud.sealos.io/license-frontend';
|
||||
@@ -0,0 +1,33 @@
|
||||
import { jsonRes } from '@/services/backend/response';
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
const cron = require('node-cron');
|
||||
let hasAddedCron = false;
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const baseurl = `http://${process.env.HOSTNAME || 'localhost'}:${process.env.PORT || 3000}`;
|
||||
|
||||
if (process.env.NODE_ENV !== 'production' && !hasAddedCron) {
|
||||
hasAddedCron = true;
|
||||
cron.schedule(
|
||||
'0 10 * * *',
|
||||
async () => {
|
||||
try {
|
||||
const result = await (await fetch(`${baseurl}/api/license/checkLicenses`)).json();
|
||||
console.log(result);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
},
|
||||
{
|
||||
scheduled: true,
|
||||
timezone: 'Asia/Shanghai'
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
jsonRes(res, {
|
||||
data: hasAddedCron
|
||||
? 'Cron job is set up and running in production.'
|
||||
: 'Cron job setup skipped in development.'
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
import { LicenseFrontendKey } from '@/constants/key';
|
||||
import { K8sApiDefault, createYaml } from '@/services/backend/kubernetes';
|
||||
import { jsonRes } from '@/services/backend/response';
|
||||
import { LicenseCR, NotificationCR } from '@/types';
|
||||
import { decodeJWT } from '@/utils/crypto';
|
||||
import { json2Notification } from '@/utils/json2Yaml';
|
||||
import { formatTimeToDay } from '@/utils/tools';
|
||||
import * as k8s from '@kubernetes/client-node';
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
const createLicenseNotification = (licenses: LicenseCR[], timeUntilExpiration: number) => {
|
||||
if (licenses.length === 0) {
|
||||
return json2Notification({
|
||||
namespace: 'sealos',
|
||||
name: `license-notification-${Date.now()}`,
|
||||
desktopPopup: true,
|
||||
i18ns: {
|
||||
zh: {
|
||||
from: 'License',
|
||||
title: '无有效许可证',
|
||||
message: '未找到有效许可证。请购买许可证。'
|
||||
},
|
||||
en: {
|
||||
from: 'License',
|
||||
title: 'No Active License',
|
||||
message: 'No active licenses found. Please purchase a license.'
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const daysUntilExpiration = Math.ceil(timeUntilExpiration / (24 * 60 * 60));
|
||||
|
||||
if (timeUntilExpiration <= 0) {
|
||||
return json2Notification({
|
||||
namespace: 'sealos',
|
||||
name: `license-notification-${Date.now()}`,
|
||||
desktopPopup: true,
|
||||
i18ns: {
|
||||
zh: {
|
||||
from: 'License',
|
||||
title: '许可证已过期',
|
||||
message: '您的许可证已过期。请立即续期。'
|
||||
},
|
||||
en: {
|
||||
from: 'License',
|
||||
title: 'License Expired',
|
||||
message: 'Your license has expired. Please renew it immediately.'
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (daysUntilExpiration <= 30) {
|
||||
return json2Notification({
|
||||
namespace: 'sealos',
|
||||
name: `license-notification-${Date.now()}`,
|
||||
desktopPopup: true,
|
||||
i18ns: {
|
||||
zh: {
|
||||
from: 'License',
|
||||
title: '许可证即将过期',
|
||||
message: `您的许可证将在 ${daysUntilExpiration} 天后过期。请考虑续期。`
|
||||
},
|
||||
en: {
|
||||
from: 'License',
|
||||
title: 'License Expiring Soon',
|
||||
message: `Your license will expire in ${daysUntilExpiration} days. Please consider renewing.`
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const listNotifications = async (namespace: string): Promise<NotificationCR[]> => {
|
||||
const kc = new k8s.KubeConfig();
|
||||
kc.loadFromDefault();
|
||||
|
||||
const k8sApi = kc.makeApiClient(k8s.CustomObjectsApi);
|
||||
|
||||
try {
|
||||
const response = (await k8sApi.listNamespacedCustomObject(
|
||||
'notification.sealos.io',
|
||||
'v1',
|
||||
namespace,
|
||||
'notifications',
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
`${LicenseFrontendKey}`
|
||||
)) as {
|
||||
body: {
|
||||
items: NotificationCR[];
|
||||
};
|
||||
};
|
||||
|
||||
const notifications = response.body.items;
|
||||
|
||||
return notifications;
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const deleteNotification = async (namespace: string, name: string) => {
|
||||
const kc = new k8s.KubeConfig();
|
||||
kc.loadFromDefault();
|
||||
|
||||
const k8sApi = kc.makeApiClient(k8s.CustomObjectsApi);
|
||||
|
||||
try {
|
||||
await k8sApi.deleteNamespacedCustomObject(
|
||||
'notification.sealos.io',
|
||||
'v1',
|
||||
namespace,
|
||||
'notifications',
|
||||
name
|
||||
);
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
const defaultKc = K8sApiDefault();
|
||||
|
||||
const response = (await defaultKc
|
||||
.makeApiClient(k8s.CustomObjectsApi)
|
||||
.listClusterCustomObject('license.sealos.io', 'v1', 'licenses')) as {
|
||||
body: {
|
||||
items: LicenseCR[];
|
||||
};
|
||||
};
|
||||
const licenses = response.body.items.filter((item) => item.status.phase === 'Active');
|
||||
|
||||
// Delete Old Notifications
|
||||
const notifications = await listNotifications('sealos');
|
||||
for (const notification of notifications) {
|
||||
await deleteNotification(notification.metadata.namespace, notification.metadata.name);
|
||||
}
|
||||
|
||||
if (licenses.length === 0) {
|
||||
const noLicenseNotification = createLicenseNotification(licenses, 0);
|
||||
noLicenseNotification && (await createYaml(defaultKc, [noLicenseNotification]));
|
||||
return jsonRes(res, {
|
||||
data: {
|
||||
message: 'No licenses found'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const latestLicense = licenses.reduce((latest, current) => {
|
||||
const latestExp = decodeJWT(latest.spec.token)?.exp || 0;
|
||||
const currentExp = decodeJWT(current.spec.token)?.exp || 0;
|
||||
return currentExp > latestExp ? current : latest;
|
||||
});
|
||||
|
||||
const maxExpTime = decodeJWT(latestLicense.spec.token)?.exp;
|
||||
if (!maxExpTime) {
|
||||
throw new Error('Failed to decode expiration time from latest license');
|
||||
}
|
||||
|
||||
const now = Math.floor(Date.now() / 1000); // current time in seconds
|
||||
const timeUntilExpiration = maxExpTime - now; // time until expiration in seconds
|
||||
const isExpired = timeUntilExpiration <= 0;
|
||||
|
||||
const notification = createLicenseNotification(licenses, timeUntilExpiration);
|
||||
notification && (await createYaml(defaultKc, [notification]));
|
||||
|
||||
jsonRes(res, {
|
||||
data: {
|
||||
latestLicense: {
|
||||
name: latestLicense.metadata.name,
|
||||
namespace: latestLicense.metadata.namespace,
|
||||
expirationTime: new Date(maxExpTime * 1000).toString(),
|
||||
timeUntilExpiration: formatTimeToDay(timeUntilExpiration),
|
||||
isExpired: isExpired
|
||||
},
|
||||
totalActiveLicenses: licenses.length
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { authSession } from '@/services/backend/auth';
|
||||
import { getK8s } from '@/services/backend/kubernetes';
|
||||
import { jsonRes } from '@/services/backend/response';
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
const { name } = req.query as { name: string };
|
||||
const { k8sCustomObjects, namespace } = await getK8s({
|
||||
kubeconfig: await authSession(req)
|
||||
});
|
||||
|
||||
const response = (await k8sCustomObjects.getNamespacedCustomObject(
|
||||
'license.sealos.io',
|
||||
'v1',
|
||||
namespace,
|
||||
'licenses',
|
||||
name
|
||||
)) as {
|
||||
body: any;
|
||||
};
|
||||
|
||||
jsonRes(res, {
|
||||
data: response.body
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { authSession } from '@/services/backend/auth';
|
||||
import { getK8s } from '@/services/backend/kubernetes';
|
||||
import { jsonRes } from '@/services/backend/response';
|
||||
import { LicenseCR } from '@/types';
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
@@ -20,15 +21,16 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
undefined
|
||||
)) as {
|
||||
body: {
|
||||
items: [];
|
||||
items: LicenseCR[];
|
||||
};
|
||||
};
|
||||
|
||||
const result = response.body.items.filter((item) => item.status.phase === 'Active');
|
||||
|
||||
jsonRes(res, {
|
||||
data: response.body.items
|
||||
data: result
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err, 'getlicense----');
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { applyLicense, getLicenseRecord } from '@/api/license';
|
||||
import { applyLicense, checkLicenses, getLicenseByName, getLicenseRecord } from '@/api/license';
|
||||
import { getClusterId, getPlatformEnv } from '@/api/platform';
|
||||
import FileSelect, { FileItemType } from '@/components/FileSelect';
|
||||
import MyIcon from '@/components/Icon';
|
||||
@@ -12,7 +12,7 @@ import { Box, Center, Divider, Flex, Image, Text } from '@chakra-ui/react';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { debounce } from 'lodash';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
export default function LicenseApp() {
|
||||
const { t } = useTranslation();
|
||||
@@ -20,22 +20,41 @@ export default function LicenseApp() {
|
||||
const { toast } = useToast();
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(5);
|
||||
const { copyData } = useCopyData();
|
||||
const [purchaseLink, setPurchaseLink] = useState('');
|
||||
const [isClient, setIsClient] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsClient(true);
|
||||
}, []);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const licenseMutation = useMutation({
|
||||
mutationFn: (yamlList: string[]) => applyLicense(yamlList, 'create'),
|
||||
onSuccess(data) {
|
||||
console.log(data, 'data');
|
||||
toast({
|
||||
title: t('Activation Successful'),
|
||||
status: 'success'
|
||||
});
|
||||
const licenseMutation = useMutation(['licenseMutation'], {
|
||||
mutationFn: () => {
|
||||
const licenseFile = files[0].text;
|
||||
const licenseStr = json2License(licenseFile).yamlStr;
|
||||
return applyLicense([licenseStr], 'create');
|
||||
},
|
||||
async onSuccess() {
|
||||
const licenseFile = files[0].text;
|
||||
const licenseObj = json2License(licenseFile).yamlObj;
|
||||
const result = await getLicenseByName({ name: licenseObj.metadata.name });
|
||||
|
||||
if (result.status.phase !== 'Active') {
|
||||
toast({
|
||||
title: result.status.reason,
|
||||
status: 'error'
|
||||
});
|
||||
} else {
|
||||
await checkLicenses();
|
||||
toast({
|
||||
title: t('Activation Successful'),
|
||||
status: 'success'
|
||||
});
|
||||
}
|
||||
queryClient.invalidateQueries(['getLicenseActive']);
|
||||
},
|
||||
onError(error: { message?: string }) {
|
||||
console.log(error);
|
||||
if (error?.message && typeof error?.message === 'string') {
|
||||
toast({
|
||||
title: error.message,
|
||||
@@ -81,8 +100,7 @@ export default function LicenseApp() {
|
||||
status: 'error'
|
||||
});
|
||||
}
|
||||
const yamlList = files.map((item) => json2License(item.text));
|
||||
licenseMutation.mutate(yamlList);
|
||||
licenseMutation.mutate();
|
||||
}, 500);
|
||||
|
||||
const downloadToken = (token: string) => {
|
||||
@@ -91,15 +109,16 @@ export default function LicenseApp() {
|
||||
};
|
||||
|
||||
const maxExpTime = useMemo(() => {
|
||||
if (data) {
|
||||
const currentTimeInSeconds = Math.floor(Date.now() / 1000);
|
||||
if (data && data.length > 0) {
|
||||
const maxItem = data.reduce((item, license) => {
|
||||
const maxTime = decodeJWT(item.spec.token)?.exp || 0;
|
||||
const currentTime = decodeJWT(license.spec.token)?.exp || 0;
|
||||
const maxTime = decodeJWT(item.spec.token)?.exp || currentTimeInSeconds;
|
||||
const currentTime = decodeJWT(license.spec.token)?.exp || currentTimeInSeconds;
|
||||
return currentTime > maxTime ? license : item;
|
||||
});
|
||||
return decodeJWT(maxItem.spec.token)?.exp || 0;
|
||||
return decodeJWT(maxItem.spec.token)?.exp || currentTimeInSeconds;
|
||||
} else {
|
||||
return 0;
|
||||
return currentTimeInSeconds;
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
@@ -140,7 +159,7 @@ export default function LicenseApp() {
|
||||
</Text>
|
||||
|
||||
<Text fontSize={'12px'}>
|
||||
{t('expire_date')}: {formatTime(maxExpTime * 1000)}
|
||||
{t('expire_date')}: {isClient && formatTime(maxExpTime * 1000)}
|
||||
</Text>
|
||||
|
||||
<Divider my={'28px'} borderColor={'rgba(255, 255, 255, 0.05)'} />
|
||||
@@ -178,7 +197,7 @@ export default function LicenseApp() {
|
||||
<Text color={'#262A32'} fontSize={'24px'} fontWeight={600}>
|
||||
{t('Activate License')}
|
||||
</Text>
|
||||
<FileSelect fileExtension={'.yaml'} files={files} setFiles={setFiles} />
|
||||
<FileSelect multiple={false} fileExtension={'.yaml'} files={files} setFiles={setFiles} />
|
||||
<Flex
|
||||
userSelect={'none'}
|
||||
ml={'auto'}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { MongoClient } from 'mongodb';
|
||||
|
||||
export * from './license';
|
||||
export * from './user';
|
||||
export * from './notification';
|
||||
|
||||
declare global {
|
||||
var mongodb: MongoClient | null;
|
||||
|
||||
+2
-1
@@ -62,7 +62,8 @@ export type LicenseCR = {
|
||||
status: {
|
||||
activationTime: string;
|
||||
expirationTime: string;
|
||||
phase: string;
|
||||
phase: 'Active' | 'Failed';
|
||||
reason: string;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import * as k8s from '@kubernetes/client-node';
|
||||
|
||||
export type NotificationCR = k8s.KubernetesObject & {
|
||||
apiVersion: 'notification.sealos.io/v1';
|
||||
kind: 'Notification';
|
||||
metadata: {
|
||||
creationTimestamp?: string;
|
||||
labels: {
|
||||
isRead: string;
|
||||
};
|
||||
name: string;
|
||||
namespace: string;
|
||||
uid?: string;
|
||||
};
|
||||
spec: {
|
||||
from: string;
|
||||
message: string;
|
||||
timestamp: number;
|
||||
title: string;
|
||||
desktopPopup?: boolean;
|
||||
i18ns?: {
|
||||
zh?: {
|
||||
from: string;
|
||||
message: string;
|
||||
title: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -1,10 +1,52 @@
|
||||
import { LicenseYaml } from '@/types';
|
||||
import { LicenseFrontendKey } from '@/constants/key';
|
||||
import { LicenseYaml, NotificationCR } from '@/types';
|
||||
import yaml from 'js-yaml';
|
||||
import { getUserNamespace } from './user';
|
||||
|
||||
export const json2License = (licenseYaml: string) => {
|
||||
const namespace = getUserNamespace();
|
||||
const obj = yaml.load(licenseYaml) as LicenseYaml;
|
||||
const template: LicenseYaml = obj;
|
||||
return yaml.dump(template);
|
||||
return {
|
||||
yamlStr: yaml.dump(template),
|
||||
yamlObj: obj
|
||||
};
|
||||
};
|
||||
|
||||
export const json2Notification = ({
|
||||
namespace,
|
||||
name,
|
||||
desktopPopup,
|
||||
i18ns
|
||||
}: {
|
||||
name: string;
|
||||
namespace: string;
|
||||
desktopPopup: boolean;
|
||||
i18ns: {
|
||||
zh: { message: string; title: string; from: string };
|
||||
en: { message: string; title: string; from: string };
|
||||
};
|
||||
}) => {
|
||||
const notification: NotificationCR = {
|
||||
apiVersion: 'notification.sealos.io/v1',
|
||||
kind: 'Notification',
|
||||
metadata: {
|
||||
labels: {
|
||||
isRead: 'false',
|
||||
[LicenseFrontendKey]: name
|
||||
},
|
||||
name: name,
|
||||
namespace: namespace
|
||||
},
|
||||
spec: {
|
||||
from: i18ns.en.from,
|
||||
message: i18ns.en.message,
|
||||
timestamp: Math.floor(Date.now() / 1000),
|
||||
title: i18ns.en.title,
|
||||
desktopPopup: desktopPopup,
|
||||
i18ns: {
|
||||
zh: i18ns.zh
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return notification;
|
||||
};
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"@alicloud/openapi-client": "^0.4.6",
|
||||
"@alicloud/tea-util": "^1.4.7",
|
||||
"@chakra-ui/anatomy": "^2.2.2",
|
||||
"@chakra-ui/icons": "^2.1.1",
|
||||
"@chakra-ui/react": "^2.8.1",
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
|
||||
Generated
+14
@@ -17,6 +17,9 @@ dependencies:
|
||||
'@chakra-ui/anatomy':
|
||||
specifier: ^2.2.2
|
||||
version: 2.2.2
|
||||
'@chakra-ui/icons':
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1(@chakra-ui/system@2.6.1)(react@18.0.0)
|
||||
'@chakra-ui/react':
|
||||
specifier: ^2.8.1
|
||||
version: 2.8.1(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.0.0)(framer-motion@10.16.4)(react-dom@18.0.0)(react@18.0.0)
|
||||
@@ -600,6 +603,17 @@ packages:
|
||||
react: 18.0.0
|
||||
dev: false
|
||||
|
||||
/@chakra-ui/icons@2.1.1(@chakra-ui/system@2.6.1)(react@18.0.0):
|
||||
resolution: {integrity: sha512-3p30hdo4LlRZTT5CwoAJq3G9fHI0wDc0pBaMHj4SUn0yomO+RcDRlzhdXqdr5cVnzax44sqXJVnf3oQG0eI+4g==}
|
||||
peerDependencies:
|
||||
'@chakra-ui/system': '>=2.0.0'
|
||||
react: '>=18'
|
||||
dependencies:
|
||||
'@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.1)(react@18.0.0)
|
||||
'@chakra-ui/system': 2.6.1(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.0.0)
|
||||
react: 18.0.0
|
||||
dev: false
|
||||
|
||||
/@chakra-ui/image@2.1.0(@chakra-ui/system@2.6.1)(react@18.0.0):
|
||||
resolution: {integrity: sha512-bskumBYKLiLMySIWDGcz0+D9Th0jPvmX6xnRMs4o92tT3Od/bW26lahmV2a2Op2ItXeCmRMY+XxJH5Gy1i46VA==}
|
||||
peerDependencies:
|
||||
|
||||
@@ -1,29 +1,30 @@
|
||||
import { ActiveClusterParams } from '@/pages/api/cluster/activeCluster';
|
||||
import { DELETE, GET, POST } from '@/services/request';
|
||||
import { ClusterResult, CreateClusterParams } from '@/types';
|
||||
import { ClusterDB, CreateClusterParams } from '@/types';
|
||||
|
||||
export const createCluster = (payload: CreateClusterParams) => POST('/api/cluster/create', payload);
|
||||
export const createCluster = (payload: CreateClusterParams) =>
|
||||
POST<ClusterDB>('/api/cluster/create', payload);
|
||||
|
||||
export const getClusterList = ({ page, pageSize }: { page: number; pageSize: number }) =>
|
||||
POST<{ total: number; records: ClusterResult[] }>('/api/cluster/getClusterList', {
|
||||
POST<{ total: number; records: ClusterDB[] }>('/api/cluster/getClusterList', {
|
||||
page,
|
||||
pageSize
|
||||
});
|
||||
|
||||
export const findClusterById = (payload: { clusterId: string }) =>
|
||||
GET<ClusterResult>('/api/cluster/findById', payload);
|
||||
GET<ClusterDB>('/api/cluster/findById', payload);
|
||||
|
||||
export const updateClusterName = (payload: { clusterId: string; displayName: string }) =>
|
||||
POST<ClusterResult>('/api/cluster/updateName', payload);
|
||||
POST<ClusterDB>('/api/cluster/updateName', payload);
|
||||
|
||||
export const deleteClusterById = (payload: { clusterId: string }) =>
|
||||
DELETE<ClusterResult>('/api/cluster/delete', payload);
|
||||
DELETE<ClusterDB>('/api/cluster/delete', payload);
|
||||
|
||||
export const isKubeSystemIDBound = (id: string) =>
|
||||
GET<{ isBound: boolean }>('/api/cluster/isBound', { kubeSystemID: id });
|
||||
|
||||
export const activeClusterBySystemId = (payload: ActiveClusterParams) =>
|
||||
POST<ClusterResult>('/api/cluster/activeCluster', payload);
|
||||
POST<ClusterDB>('/api/cluster/activeCluster', payload);
|
||||
|
||||
export const findClusterBySystemId = (payload: { systemId: string }) =>
|
||||
GET<ClusterResult>('/api/cluster/findBySystemId', payload);
|
||||
GET<ClusterDB>('/api/cluster/findBySystemId', payload);
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Icon, IconProps } from '@chakra-ui/react';
|
||||
|
||||
export const ArrowDown = (props: IconProps) => {
|
||||
return (
|
||||
<Icon {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12" fill="none">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M2.64645 4.14718C2.84171 3.95192 3.15829 3.95192 3.35355 4.14718L6 6.79363L8.64645 4.14718C8.84171 3.95192 9.15829 3.95192 9.35355 4.14718C9.54882 4.34244 9.54882 4.65902 9.35355 4.85429L6.35355 7.85429C6.15829 8.04955 5.84171 8.04955 5.64645 7.85429L2.64645 4.85429C2.45118 4.65902 2.45118 4.34244 2.64645 4.14718Z"
|
||||
fill="#667085"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Icon, IconProps } from '@chakra-ui/react';
|
||||
|
||||
export const ArrowUp = (props: IconProps) => {
|
||||
return (
|
||||
<Icon {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 13" fill="none">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M2.64645 8.62626C2.84171 8.82152 3.15829 8.82152 3.35355 8.62626L6 5.97981L8.64645 8.62626C8.84171 8.82152 9.15829 8.82152 9.35355 8.62626C9.54882 8.431 9.54882 8.11441 9.35355 7.91915L6.35355 4.91915C6.15829 4.72389 5.84171 4.72389 5.64645 4.91915L2.64645 7.91915C2.45118 8.11441 2.45118 8.431 2.64645 8.62626Z"
|
||||
fill="#667085"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Icon, IconProps } from '@chakra-ui/react';
|
||||
|
||||
export const CpuIcon = (props: IconProps) => {
|
||||
return (
|
||||
<Icon
|
||||
{...props}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="17px"
|
||||
height="16px"
|
||||
viewBox="0 0 17 16"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M6.50786 1.66642C6.50786 1.29823 6.80634 0.999756 7.17453 0.999756C7.54272 0.999756 7.8412 1.29823 7.8412 1.66642V2.71305H9.57453V1.66642C9.57453 1.29823 9.87301 0.999756 10.2412 0.999756C10.6094 0.999756 10.9079 1.29823 10.9079 1.66642V2.71305H11.4615C12.8606 2.71305 13.9949 3.84727 13.9949 5.24639V5.8001H15.0415C15.4097 5.8001 15.7082 6.09858 15.7082 6.46677C15.7082 6.83496 15.4097 7.13343 15.0415 7.13343H13.9949V8.86675H15.0415C15.4097 8.86675 15.7082 9.16523 15.7082 9.53342C15.7082 9.90161 15.4097 10.2001 15.0415 10.2001H13.9949V10.7538C13.9949 12.1529 12.8606 13.2871 11.4615 13.2871H10.9079V14.3337C10.9079 14.7019 10.6094 15.0004 10.2412 15.0004C9.87301 15.0004 9.57453 14.7019 9.57453 14.3337V13.2871H7.8412V14.3337C7.8412 14.7019 7.54272 15.0004 7.17453 15.0004C6.80634 15.0004 6.50786 14.7019 6.50786 14.3337V13.2871H5.95413C4.55501 13.2871 3.4208 12.1529 3.4208 10.7538V10.2001H2.37419C2.006 10.2001 1.70752 9.90161 1.70752 9.53342C1.70752 9.16523 2.006 8.86675 2.37419 8.86675H3.4208V7.13343H2.37419C2.006 7.13343 1.70752 6.83496 1.70752 6.46677C1.70752 6.09858 2.006 5.8001 2.37419 5.8001H3.4208V5.24639C3.4208 3.84727 4.55501 2.71305 5.95413 2.71305H6.50786V1.66642ZM5.95413 4.04639C5.29139 4.04639 4.75413 4.58364 4.75413 5.24639V10.7538C4.75413 11.4165 5.29139 11.9538 5.95413 11.9538H11.4615C12.1243 11.9538 12.6615 11.4165 12.6615 10.7538V5.24639C12.6615 4.58364 12.1243 4.04639 11.4615 4.04639H5.95413Z"
|
||||
fill="#667085"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Icon, IconProps } from '@chakra-ui/react';
|
||||
|
||||
export const MemoryIcon = (props: IconProps) => {
|
||||
return (
|
||||
<Icon
|
||||
{...props}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="17px"
|
||||
height="16px"
|
||||
viewBox="0 0 17 16"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M5.4988 5.19759C5.4988 4.8294 5.79728 4.53092 6.16547 4.53092C6.53366 4.53092 6.83213 4.8294 6.83213 5.19759V6.53092C6.83213 6.89911 6.53366 7.19759 6.16547 7.19759C5.79728 7.19759 5.4988 6.89911 5.4988 6.53092V5.19759Z"
|
||||
fill="#667085"
|
||||
/>
|
||||
<path
|
||||
d="M8.99276 4.53092C8.62457 4.53092 8.32609 4.8294 8.32609 5.19759V6.53092C8.32609 6.89911 8.62457 7.19759 8.99276 7.19759C9.36095 7.19759 9.65942 6.89911 9.65942 6.53092V5.19759C9.65942 4.8294 9.36095 4.53092 8.99276 4.53092Z"
|
||||
fill="#667085"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M2.64966 3.67799C2.64966 2.38155 3.70063 1.33057 4.99708 1.33057H9.04735C9.66993 1.33057 10.267 1.57788 10.7072 2.01811L13.3706 4.6815C13.8109 5.12173 14.0582 5.71881 14.0582 6.34138V12.3219C14.0582 13.6183 13.0072 14.6693 11.7107 14.6693H4.99708C3.70063 14.6693 2.64966 13.6183 2.64966 12.3219V3.67799ZM4.99708 2.6639C4.43701 2.6639 3.98299 3.11792 3.98299 3.67799V12.3219C3.98299 12.8819 4.43701 13.336 4.99708 13.336H11.7107C12.2708 13.336 12.7248 12.8819 12.7248 12.3219V6.34138C12.7248 6.07243 12.618 5.81449 12.4278 5.62431L9.76442 2.96092C9.57424 2.77074 9.31631 2.6639 9.04735 2.6639H4.99708Z"
|
||||
fill="#667085"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
};
|
||||
@@ -86,7 +86,6 @@ export default function Layout({ children }: { children: ReactNode }) {
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
{/* <LangSelectSimple /> */}
|
||||
<Box ml="auto"></Box>
|
||||
<Account />
|
||||
</Flex>
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
import React, { useRef, forwardRef, useMemo } from 'react';
|
||||
import {
|
||||
Menu,
|
||||
Box,
|
||||
MenuList,
|
||||
MenuItem,
|
||||
Button,
|
||||
useDisclosure,
|
||||
useOutsideClick,
|
||||
MenuButton
|
||||
} from '@chakra-ui/react';
|
||||
import type { ButtonProps } from '@chakra-ui/react';
|
||||
import { ChevronDownIcon } from '@chakra-ui/icons';
|
||||
|
||||
interface Props extends ButtonProps {
|
||||
width?: string;
|
||||
height?: string;
|
||||
value?: string;
|
||||
placeholder?: string;
|
||||
list: {
|
||||
label: string | React.ReactNode;
|
||||
value: string;
|
||||
}[];
|
||||
onchange?: (val: string) => void;
|
||||
isInvalid?: boolean;
|
||||
}
|
||||
|
||||
const MySelect = (
|
||||
{
|
||||
placeholder,
|
||||
value,
|
||||
width = 'auto',
|
||||
height = '30px',
|
||||
list,
|
||||
onchange,
|
||||
isInvalid,
|
||||
...props
|
||||
}: Props,
|
||||
selectRef: any
|
||||
) => {
|
||||
const ref = useRef<HTMLButtonElement>(null);
|
||||
const SelectRef = useRef<HTMLDivElement>(null);
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
|
||||
useOutsideClick({
|
||||
ref: SelectRef,
|
||||
handler: () => {
|
||||
onClose();
|
||||
}
|
||||
});
|
||||
|
||||
const activeMenu = useMemo(() => list.find((item) => item.value === value), [list, value]);
|
||||
|
||||
return (
|
||||
<Menu autoSelect={false} isOpen={isOpen} onOpen={onOpen} onClose={onClose}>
|
||||
<Box
|
||||
ref={SelectRef}
|
||||
position={'relative'}
|
||||
onClick={() => {
|
||||
isOpen ? onClose() : onOpen();
|
||||
}}
|
||||
>
|
||||
<MenuButton
|
||||
as={Button}
|
||||
rightIcon={<ChevronDownIcon />}
|
||||
width={width}
|
||||
height={height}
|
||||
ref={ref}
|
||||
display={'flex'}
|
||||
alignItems={'center'}
|
||||
justifyContent={'space-between'}
|
||||
border={'1px solid #E8EBF0'}
|
||||
borderRadius={'md'}
|
||||
fontSize={'12px'}
|
||||
fontWeight={'400'}
|
||||
variant={'outline'}
|
||||
_hover={{
|
||||
borderColor: '#94B5FF',
|
||||
bg: 'rgba(17, 24, 36, 0.05)'
|
||||
}}
|
||||
_active={{
|
||||
transform: ''
|
||||
}}
|
||||
{...(isOpen
|
||||
? {
|
||||
boxShadow: '0px 0px 0px 2.4px rgba(33, 155, 244, 0.15)',
|
||||
borderColor: '#487FFF',
|
||||
bg: '#FFF'
|
||||
}
|
||||
: {
|
||||
bg: '#F7F8FA',
|
||||
borderColor: isInvalid ? 'red' : ''
|
||||
})}
|
||||
{...props}
|
||||
>
|
||||
{activeMenu ? (
|
||||
<>
|
||||
<Box>{activeMenu.label}</Box>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Box>{placeholder}</Box>
|
||||
</>
|
||||
)}
|
||||
</MenuButton>
|
||||
|
||||
<MenuList
|
||||
minW={(() => {
|
||||
const w = ref.current?.clientWidth;
|
||||
if (w) {
|
||||
return `${w}px !important`;
|
||||
}
|
||||
return Array.isArray(width)
|
||||
? width.map((item) => `${item} !important`)
|
||||
: `${width} !important`;
|
||||
})()}
|
||||
p={'6px'}
|
||||
borderRadius={'base'}
|
||||
border={'1px solid #E8EBF0'}
|
||||
boxShadow={
|
||||
'0px 4px 10px 0px rgba(19, 51, 107, 0.10), 0px 0px 1px 0px rgba(19, 51, 107, 0.10)'
|
||||
}
|
||||
zIndex={99}
|
||||
overflow={'overlay'}
|
||||
maxH={'300px'}
|
||||
>
|
||||
{list.map((item) => (
|
||||
<MenuItem
|
||||
key={item.value}
|
||||
{...(value === item.value
|
||||
? {
|
||||
color: 'brightBlue.600'
|
||||
}
|
||||
: {})}
|
||||
borderRadius={'4px'}
|
||||
_hover={{
|
||||
bg: 'rgba(17, 24, 36, 0.05)',
|
||||
color: '#3370FF'
|
||||
}}
|
||||
onClick={() => {
|
||||
if (onchange && value !== item.value) {
|
||||
onchange(item.value);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Box>{item.label}</Box>
|
||||
</MenuItem>
|
||||
))}
|
||||
</MenuList>
|
||||
</Box>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
export default forwardRef(MySelect);
|
||||
@@ -0,0 +1,302 @@
|
||||
import { ArrowDown } from '@/components/Icon/ArrowDown';
|
||||
import { ArrowUp } from '@/components/Icon/ArrowUp';
|
||||
import { CpuIcon } from '@/components/Icon/CpuIcon';
|
||||
import { MemoryIcon } from '@/components/Icon/MemoryIcon';
|
||||
import MySelect from '@/components/Select';
|
||||
import {
|
||||
CpuSlideMarkList,
|
||||
MemorySlideMarkList,
|
||||
MonthMapList,
|
||||
cpuPriceMonth,
|
||||
freeClusterForm,
|
||||
memoryPriceMonth
|
||||
} from '@/constant/product';
|
||||
import { ClusterFormType } from '@/types';
|
||||
import {
|
||||
Box,
|
||||
Divider,
|
||||
Flex,
|
||||
Input,
|
||||
NumberDecrementStepper,
|
||||
NumberIncrementStepper,
|
||||
NumberInput,
|
||||
NumberInputField,
|
||||
NumberInputStepper,
|
||||
Slider,
|
||||
SliderFilledTrack,
|
||||
SliderMark,
|
||||
SliderThumb,
|
||||
SliderTrack,
|
||||
Text
|
||||
} from '@chakra-ui/react';
|
||||
import { forwardRef } from 'react';
|
||||
import { Controller, UseFormReturn } from 'react-hook-form';
|
||||
|
||||
const BillingMeter = forwardRef(
|
||||
(
|
||||
{
|
||||
formHook,
|
||||
priceMode = true,
|
||||
price,
|
||||
isPayLicense = false
|
||||
}: {
|
||||
formHook: UseFormReturn<ClusterFormType, any, undefined>;
|
||||
price: number;
|
||||
priceMode?: boolean;
|
||||
isPayLicense?: boolean;
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const { setValue, getValues, register, control } = formHook;
|
||||
|
||||
const formArr = [
|
||||
{
|
||||
unit: '核',
|
||||
value: 'cpu' as 'cpu' | 'memory',
|
||||
label: 'CPU',
|
||||
min: 1,
|
||||
max: 9999,
|
||||
sliderList: CpuSlideMarkList
|
||||
},
|
||||
{
|
||||
unit: 'G',
|
||||
value: 'memory' as 'cpu' | 'memory',
|
||||
label: '内存',
|
||||
min: 1,
|
||||
max: 9999,
|
||||
sliderList: MemorySlideMarkList
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<Box minW={'500px'}>
|
||||
{priceMode && (
|
||||
<Text mt={'40px'} textAlign={'center'} fontSize={'24px'} fontWeight={500}>
|
||||
计价方案
|
||||
</Text>
|
||||
)}
|
||||
<Box mt={priceMode ? '24px' : ''} borderRadius={'8px'} bg={'white'} px={'32px'}>
|
||||
{priceMode && (
|
||||
<>
|
||||
<Flex
|
||||
color={'#667085'}
|
||||
fontSize={'12px'}
|
||||
alignItems={'center'}
|
||||
fontWeight={500}
|
||||
py={'16px'}
|
||||
>
|
||||
<Text color={'#485264'}>单价</Text>
|
||||
<Flex gap={'4px'} ml={'auto'}>
|
||||
<CpuIcon />
|
||||
<Text>CPU: {cpuPriceMonth * 12} 元/核心/年</Text>
|
||||
</Flex>
|
||||
<Flex gap={'4px'} ml={'24px'}>
|
||||
<MemoryIcon />
|
||||
<Text>内存: {memoryPriceMonth * 12} 元/G/年</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Divider borderColor={'#E8EBF0'} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{!priceMode && !isPayLicense && (
|
||||
<Flex alignItems={'center'} pt={'32px'}>
|
||||
<Box fontSize={'14px'} fontWeight={500} minW={'88px'}>
|
||||
集群名
|
||||
</Box>
|
||||
<Input
|
||||
flex={1}
|
||||
borderRadius={'6px'}
|
||||
placeholder={'集群名'}
|
||||
{...register('name', {
|
||||
required: '集群名称不能为空'
|
||||
})}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{formArr.map((item) => {
|
||||
const list = item.sliderList;
|
||||
return (
|
||||
<Flex
|
||||
key={item.value}
|
||||
flexDirection={priceMode ? 'column' : 'row'}
|
||||
mt={priceMode ? '20px' : '32px'}
|
||||
>
|
||||
<Box minW={'88px'} fontWeight={500} fontSize={priceMode ? '12px' : '14px'}>
|
||||
{item.label}
|
||||
</Box>
|
||||
<Flex alignItems={'center'} gap={'20px'} flex={1} mt={priceMode ? '12px' : ''}>
|
||||
<Controller
|
||||
control={control}
|
||||
name={item.value}
|
||||
render={({ field }) => (
|
||||
<Slider
|
||||
alignSelf={'self-start'}
|
||||
{...field}
|
||||
mt={'4px'}
|
||||
focusThumbOnChange={false}
|
||||
aria-label="slider-ex-1"
|
||||
min={item.min}
|
||||
max={item.value === 'cpu' ? 2048 : 4096}
|
||||
>
|
||||
{list.map((n, i) => (
|
||||
<SliderMark
|
||||
key={n.value}
|
||||
value={n.value}
|
||||
mt={'6px'}
|
||||
fontSize={'11px'}
|
||||
transform={'translateX(-50%)'}
|
||||
whiteSpace={'nowrap'}
|
||||
{...(getValues(item.value) === n.value
|
||||
? { color: '#111824' }
|
||||
: { color: '#8A95A7' })}
|
||||
>
|
||||
{n.value} {i + 1 === CpuSlideMarkList.length && item.unit}
|
||||
</SliderMark>
|
||||
))}
|
||||
<SliderTrack
|
||||
bg={'#EAEDF3'}
|
||||
borderRadius={'4px'}
|
||||
overflow={'hidden'}
|
||||
h={'4px'}
|
||||
>
|
||||
<SliderFilledTrack bg={'#111824'} />
|
||||
</SliderTrack>
|
||||
<SliderThumb bg={'#111824'} _focusVisible={{ boxShadow: '' }} />
|
||||
</Slider>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name={item.value}
|
||||
render={({ field }) => (
|
||||
<NumberInput
|
||||
ml={'10px'}
|
||||
position={'relative'}
|
||||
max={item.max}
|
||||
min={item.min}
|
||||
{...field}
|
||||
onChange={(valueString) => {
|
||||
const value = Number(valueString);
|
||||
field.onChange(valueString === '' || isNaN(value) ? item.min : value);
|
||||
}}
|
||||
>
|
||||
<NumberInputField
|
||||
width={'89px'}
|
||||
height={'32px'}
|
||||
pl={'12px'}
|
||||
color={'#111824'}
|
||||
fontSize={'12px'}
|
||||
min={item.min}
|
||||
max={item.max}
|
||||
borderRadius={'md'}
|
||||
borderColor={'#E8EBF0'}
|
||||
bg={'#F7F8FA'}
|
||||
_focusVisible={{
|
||||
borderColor: 'brightBlue.500',
|
||||
boxShadow: '0px 0px 0px 2.4px rgba(33, 155, 244, 0.15)',
|
||||
bg: '#FFF',
|
||||
color: '#111824'
|
||||
}}
|
||||
_hover={{
|
||||
borderColor: 'brightBlue.300'
|
||||
}}
|
||||
/>
|
||||
|
||||
<NumberInputStepper>
|
||||
<NumberIncrementStepper>
|
||||
<ArrowUp />
|
||||
</NumberIncrementStepper>
|
||||
<NumberDecrementStepper>
|
||||
<ArrowDown />
|
||||
</NumberDecrementStepper>
|
||||
</NumberInputStepper>
|
||||
<Box
|
||||
zIndex={1}
|
||||
position={'absolute'}
|
||||
right={'28px'}
|
||||
top={'50%'}
|
||||
fontSize={'12px'}
|
||||
fontWeight={500}
|
||||
transform={'translateY(-50%)'}
|
||||
color={'#667085'}
|
||||
>
|
||||
{item.unit}
|
||||
</Box>
|
||||
</NumberInput>
|
||||
)}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
|
||||
<Flex flexDirection={priceMode ? 'column' : 'row'} mt={'32px'}>
|
||||
<Box fontWeight={500} fontSize={priceMode ? '12px' : '14px'} minW={'88px'}>
|
||||
时间
|
||||
</Box>
|
||||
<MySelect
|
||||
textAlign={'left'}
|
||||
mt={priceMode ? '12px' : ''}
|
||||
width={'200px'}
|
||||
value={getValues('months')}
|
||||
list={MonthMapList}
|
||||
onchange={(val: any) => setValue('months', val)}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<Flex
|
||||
justifyContent={'end'}
|
||||
textAlign={'right'}
|
||||
color={'#0884DD'}
|
||||
fontSize={'12px'}
|
||||
fontWeight={500}
|
||||
mt={priceMode ? '24px' : '4px'}
|
||||
>
|
||||
<Text
|
||||
px={'8px'}
|
||||
cursor={'pointer'}
|
||||
onClick={() => {
|
||||
formHook.reset({
|
||||
cpu: freeClusterForm.cpu,
|
||||
memory: freeClusterForm.memory,
|
||||
months: freeClusterForm.months,
|
||||
name: getValues('name'),
|
||||
systemId: getValues('systemId')
|
||||
});
|
||||
}}
|
||||
>
|
||||
免费规模
|
||||
</Text>
|
||||
</Flex>
|
||||
<Divider mt={priceMode ? '8px' : '4px'} borderColor={'#E8EBF0'} />
|
||||
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
pb={'16px'}
|
||||
pt={priceMode ? '16px' : '32px'}
|
||||
>
|
||||
<Text color={'#667085'} fontSize={'11px'} fontWeight={500}>
|
||||
{priceMode ? '价格:' : '需支付:'}
|
||||
</Text>
|
||||
<Text
|
||||
ml={'12px'}
|
||||
color={'#0884DD'}
|
||||
fontSize={priceMode ? '20px' : '28px'}
|
||||
fontWeight={500}
|
||||
>
|
||||
¥ {price}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
BillingMeter.displayName = 'BillingMeter';
|
||||
|
||||
export default BillingMeter;
|
||||
+6
-6
@@ -1,10 +1,10 @@
|
||||
import { getSystemEnv, uploadConvertData } from '@/api/system';
|
||||
import useAuthList from '@/components/Signin/auth/useAuthList';
|
||||
import useCustomError from '@/components/Signin/auth/useCustomError';
|
||||
import Language from '@/components/Signin/auth/useLanguage';
|
||||
import usePassword from '@/components/Signin/auth/usePassword';
|
||||
import useProtocol from '@/components/Signin/auth/useProtocol';
|
||||
import useSms from '@/components/Signin/auth/useSms';
|
||||
import useAuthList from '@/components/login-module/auth/useAuthList';
|
||||
import useCustomError from '@/components/login-module/auth/useCustomError';
|
||||
import Language from '@/components/login-module/auth/useLanguage';
|
||||
import usePassword from '@/components/login-module/auth/usePassword';
|
||||
import useProtocol from '@/components/login-module/auth/useProtocol';
|
||||
import useSms from '@/components/login-module/auth/useSms';
|
||||
import useSessionStore from '@/stores/session';
|
||||
import { LoginType } from '@/types';
|
||||
import {
|
||||
@@ -0,0 +1,621 @@
|
||||
import { activeClusterBySystemId, createCluster, findClusterById } from '@/api/cluster';
|
||||
import { createLicense } from '@/api/license';
|
||||
import { checkWechatPay, createPayment, handlePaymentResult } from '@/api/payment';
|
||||
import { getSystemEnv, uploadConvertData } from '@/api/system';
|
||||
import { AddIcon, LeftIcon, StripeIcon, WechatIcon } from '@/components/Icon';
|
||||
import WechatPayment from '@/components/WechatPayment';
|
||||
import { defaulClustertForm, freeClusterForm } from '@/constant/product';
|
||||
import BillingMeter from '@/components/billing-meter';
|
||||
import useClusterDetail from '@/stores/cluster';
|
||||
import usePaymentDataStore from '@/stores/payment';
|
||||
import {
|
||||
ClusterFormType,
|
||||
ClusterType,
|
||||
CreateClusterParams,
|
||||
PaymentStatus,
|
||||
TPayMethod,
|
||||
WechatPaymentData
|
||||
} from '@/types';
|
||||
import { calculatePrice, deFormatMoney } from '@/utils/tools';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Checkbox,
|
||||
Flex,
|
||||
Link,
|
||||
Modal,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
Spinner,
|
||||
Text,
|
||||
useDisclosure,
|
||||
useToast
|
||||
} from '@chakra-ui/react';
|
||||
import { loadStripe } from '@stripe/stripe-js';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useRouter } from 'next/router';
|
||||
import { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
const RechargeComponent = forwardRef(function RechargeComponent(
|
||||
{ isLicensePay }: { isLicensePay: boolean },
|
||||
ref
|
||||
) {
|
||||
const { clusterDetail, setClusterDetail } = useClusterDetail();
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
const toast = useToast({ position: 'top', duration: 2000 });
|
||||
const { t } = useTranslation();
|
||||
const [isAgree, setIsAgree] = useState(false);
|
||||
const [isInvalid, setIsInvalid] = useState(false);
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
// 整个流程跑通需要状态管理, 0 初始态, 1 创建支付单, 2 支付中, 3 支付成功
|
||||
const [complete, setComplete] = useState<0 | 1 | 2 | 3>(0);
|
||||
const [payType, setPayType] = useState<TPayMethod>('wechat');
|
||||
const [detail, setDetail] = useState(false);
|
||||
const [orderID, setOrderID] = useState('');
|
||||
const [wechatPaymentData, setWechatPaymentData] = useState<WechatPaymentData>();
|
||||
const { data: platformEnv } = useQuery(['getPlatformEnv'], getSystemEnv);
|
||||
// 用于避免微信支付,窗口关闭后感知不到的问题
|
||||
const { paymentData, setPaymentData, deletePaymentData, isExpired } = usePaymentDataStore();
|
||||
const [price, setPrice] = useState(0);
|
||||
|
||||
const formHook = useForm<ClusterFormType>();
|
||||
|
||||
// price
|
||||
formHook.watch((data) => {
|
||||
if (!data) return;
|
||||
const price = calculatePrice(data as ClusterFormType, freeClusterForm);
|
||||
setPrice(price);
|
||||
});
|
||||
|
||||
const onClosePayment = useCallback(() => {
|
||||
formHook.reset();
|
||||
setOrderID('');
|
||||
setComplete(0);
|
||||
setDetail(false);
|
||||
setIsAgree(false);
|
||||
onClose();
|
||||
}, [formHook, onClose]);
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => {
|
||||
return {
|
||||
onOpen(form?: ClusterFormType) {
|
||||
onOpen();
|
||||
if (isLicensePay) {
|
||||
formHook.reset({
|
||||
cpu: clusterDetail?.cpu ?? defaulClustertForm.cpu,
|
||||
memory: clusterDetail?.memory ?? defaulClustertForm.memory,
|
||||
name: clusterDetail?.displayName ?? defaulClustertForm.name,
|
||||
months: clusterDetail?.months ?? defaulClustertForm.months
|
||||
});
|
||||
} else {
|
||||
formHook.reset(form ? form : defaulClustertForm);
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
[
|
||||
onOpen,
|
||||
isLicensePay,
|
||||
formHook,
|
||||
clusterDetail?.cpu,
|
||||
clusterDetail?.memory,
|
||||
clusterDetail?.displayName,
|
||||
clusterDetail?.months
|
||||
]
|
||||
);
|
||||
|
||||
const handleSubmit = (payType: TPayMethod) => {
|
||||
formHook.handleSubmit(
|
||||
(data) => {
|
||||
setPayType(payType);
|
||||
if (!isAgree) {
|
||||
return toast({
|
||||
status: 'error',
|
||||
title: t('Please read and agree to the agreement'),
|
||||
isClosable: true,
|
||||
position: 'top'
|
||||
});
|
||||
}
|
||||
if (price === 0) {
|
||||
if (isLicensePay) {
|
||||
return licenseMutation.mutate({
|
||||
orderID: '',
|
||||
clusterId: clusterDetail?.clusterId || '',
|
||||
...data
|
||||
});
|
||||
} else {
|
||||
return clusterMutation.mutate({
|
||||
orderID: '',
|
||||
type: ClusterType.Standard,
|
||||
...data
|
||||
});
|
||||
}
|
||||
}
|
||||
if (payType === 'stripe' && price < 10) {
|
||||
return toast({
|
||||
status: 'error',
|
||||
title: t('Pay Minimum Tips')
|
||||
});
|
||||
}
|
||||
setDetail(true);
|
||||
setComplete(1);
|
||||
paymentMutation.mutate({ amount: deFormatMoney(price).toString() });
|
||||
},
|
||||
(err) => {
|
||||
const deepSearch = (obj: any): string => {
|
||||
if (!obj || typeof obj !== 'object') return 'Submit Error';
|
||||
if (!!obj.message) {
|
||||
return obj.message;
|
||||
}
|
||||
return deepSearch(Object.values(obj)[0]);
|
||||
};
|
||||
toast({
|
||||
title: deepSearch(err),
|
||||
status: 'error',
|
||||
position: 'top',
|
||||
duration: 3000,
|
||||
isClosable: true
|
||||
});
|
||||
}
|
||||
)();
|
||||
};
|
||||
|
||||
const createPaymentMutation = async ({ amount }: { amount: string }) => {
|
||||
const payMethod = payType;
|
||||
const currency = 'CNY';
|
||||
const stripeSuccessCallBackUrl = isLicensePay
|
||||
? `/cluster?tab=license&stripeState=success`
|
||||
: '/pricing?stripeState=success';
|
||||
const stripeErrorCallBackUrl = isLicensePay
|
||||
? `/cluster?tab=license&stripeState=error`
|
||||
: '/pricing?stripeState=error';
|
||||
|
||||
return createPayment({
|
||||
amount,
|
||||
payMethod,
|
||||
currency,
|
||||
stripeSuccessCallBackUrl,
|
||||
stripeErrorCallBackUrl
|
||||
});
|
||||
};
|
||||
|
||||
const paymentMutation = useMutation(createPaymentMutation, {
|
||||
async onSuccess(data) {
|
||||
setPaymentData({
|
||||
orderId: data.orderID,
|
||||
clusterId: clusterDetail?.clusterId || '',
|
||||
...formHook.getValues()
|
||||
});
|
||||
if (payType === 'stripe' && platformEnv && data?.sessionID) {
|
||||
const stripe = await loadStripe(platformEnv?.stripePub);
|
||||
stripe?.redirectToCheckout({
|
||||
sessionId: data.sessionID
|
||||
});
|
||||
}
|
||||
if (payType === 'wechat' && data?.tradeNO && data?.codeURL) {
|
||||
setOrderID(data.orderID);
|
||||
setWechatPaymentData({ tradeNO: data?.tradeNO, codeURL: data?.codeURL });
|
||||
setComplete(2);
|
||||
}
|
||||
},
|
||||
onError(err: any) {
|
||||
toast({
|
||||
status: 'error',
|
||||
title: err?.message || '',
|
||||
isClosable: true,
|
||||
position: 'top'
|
||||
});
|
||||
setComplete(0);
|
||||
}
|
||||
});
|
||||
|
||||
const createLicenseMutation = async ({
|
||||
orderID,
|
||||
clusterId,
|
||||
cpu,
|
||||
memory,
|
||||
months
|
||||
}: {
|
||||
orderID: string;
|
||||
clusterId: string;
|
||||
cpu: number;
|
||||
memory: number;
|
||||
months: string;
|
||||
}) => {
|
||||
return createLicense({ orderID, clusterId, cpu, memory, months });
|
||||
};
|
||||
|
||||
const licenseMutation = useMutation(createLicenseMutation, {
|
||||
onSuccess() {
|
||||
onClosePayment();
|
||||
toast({
|
||||
status: 'success',
|
||||
title: t('License issued successfully'),
|
||||
isClosable: true,
|
||||
position: 'top'
|
||||
});
|
||||
queryClient.invalidateQueries(['getLicenseByClusterId']);
|
||||
deletePaymentData();
|
||||
},
|
||||
onError(err: any) {
|
||||
toast({
|
||||
status: 'error',
|
||||
title: err?.message || '',
|
||||
isClosable: true,
|
||||
position: 'top'
|
||||
});
|
||||
setOrderID('');
|
||||
setComplete(0);
|
||||
setDetail(false);
|
||||
}
|
||||
});
|
||||
|
||||
// create cluster
|
||||
const clusterMutation = useMutation((payload: CreateClusterParams) => createCluster(payload), {
|
||||
async onSuccess(data) {
|
||||
let cluster;
|
||||
cluster = await findClusterById({
|
||||
clusterId: data.clusterId
|
||||
});
|
||||
|
||||
const systemId = formHook.getValues('systemId') || paymentData?.systemId;
|
||||
if (systemId) {
|
||||
cluster = await activeClusterBySystemId({
|
||||
clusterId: cluster.clusterId,
|
||||
kubeSystemID: systemId
|
||||
});
|
||||
}
|
||||
setClusterDetail(cluster);
|
||||
toast({
|
||||
status: 'success',
|
||||
title: '集群创建成功, 请前往我的集群查看',
|
||||
isClosable: true,
|
||||
position: 'top'
|
||||
});
|
||||
queryClient.invalidateQueries(['getClusterList']);
|
||||
queryClient.invalidateQueries({ queryKey: ['getLicenseByClusterId'] });
|
||||
deletePaymentData();
|
||||
onClosePayment();
|
||||
},
|
||||
onError(err: any) {
|
||||
toast({
|
||||
status: 'error',
|
||||
title: err?.message || '',
|
||||
isClosable: true,
|
||||
position: 'top'
|
||||
});
|
||||
setOrderID('');
|
||||
setComplete(0);
|
||||
setDetail(false);
|
||||
}
|
||||
});
|
||||
|
||||
useQuery(['getPaymentResult', orderID], () => handlePaymentResult({ orderID }), {
|
||||
refetchInterval: complete === 2 ? 3 * 1000 : false,
|
||||
enabled: complete === 2 && !!orderID,
|
||||
cacheTime: 0,
|
||||
staleTime: 0,
|
||||
onSuccess(data) {
|
||||
const cpu = paymentData?.cpu;
|
||||
const months = paymentData?.months;
|
||||
const memory = paymentData?.memory;
|
||||
const clusterName = paymentData?.name;
|
||||
const systemId = paymentData?.systemId;
|
||||
|
||||
if (!cpu || !memory || !months) {
|
||||
return toast({
|
||||
position: 'top',
|
||||
status: 'error',
|
||||
title: '缺少 cpu、memory、months 参数'
|
||||
});
|
||||
}
|
||||
|
||||
if (data.status === PaymentStatus.PaymentSuccess) {
|
||||
if (isLicensePay) {
|
||||
if (!clusterDetail?.clusterId) {
|
||||
return toast({
|
||||
position: 'top',
|
||||
status: 'error',
|
||||
title: '缺少 clusterId 参数'
|
||||
});
|
||||
}
|
||||
licenseMutation.mutate({
|
||||
orderID: data.orderID,
|
||||
clusterId: clusterDetail.clusterId,
|
||||
cpu,
|
||||
memory,
|
||||
months
|
||||
});
|
||||
} else {
|
||||
clusterMutation.mutate({
|
||||
orderID: data.orderID,
|
||||
type: ClusterType.ScaledStandard,
|
||||
cpu,
|
||||
memory,
|
||||
months,
|
||||
name: clusterName,
|
||||
systemId
|
||||
});
|
||||
}
|
||||
uploadConvertData([90])
|
||||
.then((res) => {
|
||||
console.log(res);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
}
|
||||
},
|
||||
onError(err: any) {
|
||||
toast({
|
||||
status: 'error',
|
||||
title: err?.message || '',
|
||||
isClosable: true,
|
||||
position: 'top'
|
||||
});
|
||||
setOrderID('');
|
||||
setComplete(0);
|
||||
setDetail(false);
|
||||
}
|
||||
});
|
||||
|
||||
// checkWechatPay;
|
||||
useQuery(['checkWechatPay'], () => checkWechatPay('license'), {
|
||||
enabled: !isExpired() && !!paymentData?.orderId,
|
||||
onSuccess(data) {
|
||||
if (data.status === PaymentStatus.PaymentSuccess) {
|
||||
const cpu = paymentData?.cpu;
|
||||
const months = paymentData?.months;
|
||||
const memory = paymentData?.memory;
|
||||
const systemId = paymentData?.systemId;
|
||||
|
||||
if (!clusterDetail?.clusterId || !cpu || !memory || !months) {
|
||||
return toast({
|
||||
position: 'top',
|
||||
status: 'error',
|
||||
title: '请联系管理员'
|
||||
});
|
||||
}
|
||||
if (isLicensePay) {
|
||||
licenseMutation.mutate({
|
||||
orderID: data.orderID,
|
||||
clusterId: clusterDetail.clusterId,
|
||||
cpu,
|
||||
memory,
|
||||
months
|
||||
});
|
||||
} else {
|
||||
clusterMutation.mutate({
|
||||
orderID: data.orderID,
|
||||
type: ClusterType.ScaledStandard,
|
||||
cpu,
|
||||
memory,
|
||||
months,
|
||||
systemId
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const getCluster = async (id: string) => {
|
||||
const cluster = await findClusterById({
|
||||
clusterId: id
|
||||
});
|
||||
setClusterDetail(cluster);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const { stripeState, tab } = router.query;
|
||||
const orderID = paymentData?.orderId;
|
||||
const clusterId = paymentData?.clusterId;
|
||||
console.log(stripeState, orderID, clusterId, tab, isLicensePay, paymentData);
|
||||
|
||||
const clearQuery = () => {
|
||||
isLicensePay
|
||||
? router.replace({
|
||||
pathname: '/cluster',
|
||||
query: { tab }
|
||||
})
|
||||
: router.replace({
|
||||
pathname: '/pricing',
|
||||
query: { tab }
|
||||
});
|
||||
};
|
||||
|
||||
if (stripeState === 'success') {
|
||||
toast({
|
||||
status: 'success',
|
||||
title: '正在检查支付结果,请耐心等待。',
|
||||
isClosable: true,
|
||||
position: 'top'
|
||||
});
|
||||
clusterId ? getCluster(clusterId as string) : '';
|
||||
setComplete(2);
|
||||
setOrderID(orderID as string);
|
||||
clearQuery();
|
||||
} else if (stripeState === 'error') {
|
||||
clusterId ? getCluster(clusterId as string) : '';
|
||||
toast({
|
||||
status: 'error',
|
||||
duration: 3000,
|
||||
title: t('Stripe Cancel'),
|
||||
isClosable: true,
|
||||
position: 'top'
|
||||
});
|
||||
onClosePayment();
|
||||
clearQuery();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClosePayment}>
|
||||
<ModalOverlay />
|
||||
<ModalContent
|
||||
borderRadius={'12px'}
|
||||
maxW={'735px'}
|
||||
minH={'495px'}
|
||||
display={'flex'}
|
||||
flexDirection={'column'}
|
||||
>
|
||||
<ModalCloseButton />
|
||||
{detail ? (
|
||||
<>
|
||||
<ModalHeader
|
||||
cursor={'pointer'}
|
||||
pt={'24px'}
|
||||
py={'20px'}
|
||||
onClick={() => {
|
||||
setOrderID('');
|
||||
setComplete(0);
|
||||
setDetail(false);
|
||||
}}
|
||||
display={'flex'}
|
||||
alignItems={'center'}
|
||||
>
|
||||
<LeftIcon />
|
||||
<Text ml="8px" color={'#24282C'}>
|
||||
{isLicensePay ? '购买 License' : '新建集群'}
|
||||
</Text>
|
||||
</ModalHeader>
|
||||
{complete === 1 ? (
|
||||
<Flex flex={1} alignItems={'center'} justifyContent={'center'}>
|
||||
<Spinner size={'xl'} />
|
||||
</Flex>
|
||||
) : (
|
||||
<WechatPayment
|
||||
complete={complete}
|
||||
codeURL={wechatPaymentData?.codeURL}
|
||||
tradeNO={wechatPaymentData?.tradeNO}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ModalHeader
|
||||
height={'56px'}
|
||||
borderTopRadius={'12px'}
|
||||
fontSize={'18px'}
|
||||
fontWeight={500}
|
||||
bg={'#FBFBFC'}
|
||||
py={'14px'}
|
||||
borderBottom={'1px solid #F4F4F7'}
|
||||
>
|
||||
{isLicensePay ? '购买 License' : '新建集群'}
|
||||
</ModalHeader>
|
||||
<Box px={'20px'} pb="45px">
|
||||
<BillingMeter
|
||||
price={price}
|
||||
formHook={formHook}
|
||||
priceMode={false}
|
||||
isPayLicense={isLicensePay}
|
||||
/>
|
||||
<Flex justifyContent={'center'}>
|
||||
{price === 0 ? (
|
||||
<Button
|
||||
size="primary"
|
||||
variant="primary"
|
||||
w="205px"
|
||||
h="auto"
|
||||
py="14px"
|
||||
px="34px"
|
||||
onClick={() => handleSubmit('wechat')}
|
||||
>
|
||||
<AddIcon fill={'white'} />
|
||||
<Text ml="12px"> {isLicensePay ? '购买 License' : '新建集群'}</Text>
|
||||
</Button>
|
||||
) : (
|
||||
<Flex gap={'16px'} width={'426px'} h={'44px'}>
|
||||
{platformEnv?.stripeEnabled && (
|
||||
<Button
|
||||
size="primary"
|
||||
variant="primary"
|
||||
w="full"
|
||||
h="auto"
|
||||
py="14px"
|
||||
px="34px"
|
||||
onClick={() => handleSubmit('stripe')}
|
||||
>
|
||||
<StripeIcon />
|
||||
<Text ml="12px">{t('pay with stripe')}</Text>
|
||||
</Button>
|
||||
)}
|
||||
{platformEnv?.wechatEnabledRecharge && (
|
||||
<Button
|
||||
size="primary"
|
||||
variant="primary"
|
||||
w="full"
|
||||
h="auto"
|
||||
py="14px"
|
||||
px="34px"
|
||||
onClick={() => handleSubmit('wechat')}
|
||||
>
|
||||
<WechatIcon fill={'#33BABB'} />
|
||||
<Text ml="12px">{t('pay with wechat')}</Text>
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<Flex justifyContent={'center'} align={'center'} mt={'24px'}>
|
||||
<Checkbox
|
||||
isInvalid={isInvalid}
|
||||
mr={'8px'}
|
||||
isChecked={isAgree}
|
||||
variant={'unstyled'}
|
||||
onChange={(e) => {
|
||||
setIsInvalid(false);
|
||||
setIsAgree(e.target.checked);
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
fontStyle="normal"
|
||||
fontWeight="400"
|
||||
fontSize="12px"
|
||||
lineHeight="140%"
|
||||
color={'#1D8CDC'}
|
||||
>
|
||||
{t('agree policy')}
|
||||
<Link
|
||||
isExternal
|
||||
href={platformEnv?.service_protocol}
|
||||
_hover={{
|
||||
color: 'rgba(94, 189, 242, 1)',
|
||||
borderBottom: '1px solid rgba(94, 189, 242, 1)'
|
||||
}}
|
||||
px="4px"
|
||||
>
|
||||
{t('Service Agreement')}
|
||||
</Link>
|
||||
{t('and')}
|
||||
<Link
|
||||
isExternal
|
||||
href={platformEnv?.private_protocol}
|
||||
_hover={{
|
||||
color: 'rgba(94, 189, 242, 1)',
|
||||
borderBottom: '1px solid rgba(94, 189, 242, 1)'
|
||||
}}
|
||||
px="4px"
|
||||
>
|
||||
{t('Privacy Policy')}
|
||||
</Link>
|
||||
</Text>
|
||||
</Flex>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
});
|
||||
|
||||
export default RechargeComponent;
|
||||
@@ -1,98 +0,0 @@
|
||||
import { getSystemEnv } from '@/api/system';
|
||||
import { GithubIcon, GoogleIcon, WechatIcon } from '@/components/Icon';
|
||||
import useSessionStore from '@/stores/session';
|
||||
import { OauthProvider } from '@/types/user';
|
||||
import { Button, Flex, Icon } from '@chakra-ui/react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { MouseEventHandler } from 'react';
|
||||
|
||||
const useAuthList = () => {
|
||||
const { data: platformEnv } = useQuery(['getPlatformEnv'], getSystemEnv);
|
||||
|
||||
const {
|
||||
needGithub = false,
|
||||
needWechat = false,
|
||||
needGoogle = false,
|
||||
wechat_client_id = '',
|
||||
github_client_id = '',
|
||||
google_client_id = '',
|
||||
callback_url = ''
|
||||
} = platformEnv || {};
|
||||
const { generateState, setProvider } = useSessionStore();
|
||||
|
||||
const oauthLogin = async ({ url, provider }: { url: string; provider?: OauthProvider }) => {
|
||||
setProvider(provider);
|
||||
window.location.href = url;
|
||||
};
|
||||
|
||||
const authList: { icon: typeof Icon; cb: MouseEventHandler; need: boolean }[] = [
|
||||
{
|
||||
// src: '/images/github.svg',
|
||||
icon: GithubIcon,
|
||||
cb: (e) => {
|
||||
e.preventDefault();
|
||||
const state = generateState();
|
||||
oauthLogin({
|
||||
provider: 'github',
|
||||
url: `https://github.com/login/oauth/authorize?client_id=${github_client_id}&redirect_uri=${callback_url}&scope=user:email%20read:user&state=${state}`
|
||||
});
|
||||
},
|
||||
need: needGithub
|
||||
},
|
||||
{
|
||||
icon: WechatIcon,
|
||||
cb: (e) => {
|
||||
e.preventDefault();
|
||||
const state = generateState();
|
||||
oauthLogin({
|
||||
provider: 'wechat',
|
||||
url: `https://open.weixin.qq.com/connect/qrconnect?appid=${wechat_client_id}&redirect_uri=${callback_url}&response_type=code&state=${state}&scope=snsapi_login&#wechat_redirect`
|
||||
});
|
||||
},
|
||||
need: needWechat
|
||||
},
|
||||
{
|
||||
icon: GoogleIcon,
|
||||
cb: (e) => {
|
||||
e.preventDefault();
|
||||
const state = generateState();
|
||||
const scope = encodeURIComponent(`https://www.googleapis.com/auth/userinfo.profile openid`);
|
||||
oauthLogin({
|
||||
provider: 'google',
|
||||
//https://www.googleapis.com/auth/userinfo.profile
|
||||
url: `https://accounts.google.com/o/oauth2/v2/auth?client_id=${google_client_id}&redirect_uri=${callback_url}&response_type=code&state=${state}&scope=${scope}&include_granted_scopes=true`
|
||||
});
|
||||
},
|
||||
need: needGoogle
|
||||
}
|
||||
];
|
||||
|
||||
const AuthList = () => {
|
||||
return (
|
||||
<Flex gap={'14px'}>
|
||||
{authList
|
||||
.filter((item) => item.need)
|
||||
.map((item, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
onClick={item.cb}
|
||||
borderRadius={'full'}
|
||||
variant={'unstyled'}
|
||||
size={'xs'}
|
||||
w="32px"
|
||||
h="32px"
|
||||
bgColor={'rgba(255, 255, 255, 0.65)'}
|
||||
>
|
||||
<item.icon w="20px" h="20px" />
|
||||
</Button>
|
||||
))}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
AuthList
|
||||
};
|
||||
};
|
||||
|
||||
export default useAuthList;
|
||||
@@ -1,61 +0,0 @@
|
||||
import { Flex, Img, Button, Text } from '@chakra-ui/react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { WarningIcon, CloseIcon } from '@/components/Icon';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
const useCustomError = () => {
|
||||
const { t } = useTranslation();
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const showError = (errorMessage: string, duration = 5000) => {
|
||||
setError(errorMessage);
|
||||
setTimeout(() => {
|
||||
setError('');
|
||||
}, duration);
|
||||
};
|
||||
|
||||
const closeError = () => {
|
||||
setError('');
|
||||
};
|
||||
|
||||
const ErrorComponent = () => {
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
const timeout = setTimeout(() => {
|
||||
closeError();
|
||||
}, 5000); // 默认 5000 毫秒(5秒)后自动关闭错误消息
|
||||
return () => clearTimeout(timeout);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return error ? (
|
||||
<Flex
|
||||
position={'absolute'}
|
||||
top="0"
|
||||
bg={'rgba(249, 78, 97, 1)'}
|
||||
transform={'translateY(-50%)'}
|
||||
width="266px"
|
||||
minH="42px"
|
||||
mb="14px"
|
||||
borderRadius="4px"
|
||||
p="10px"
|
||||
>
|
||||
<WarningIcon />
|
||||
<Text color={'#fff'}>{t(error)}</Text>
|
||||
<Button
|
||||
variant={'unstyled'}
|
||||
ml={'auto'}
|
||||
display={'flex'}
|
||||
justifyContent={'flex-end'}
|
||||
onClick={closeError}
|
||||
>
|
||||
<CloseIcon />
|
||||
</Button>
|
||||
</Flex>
|
||||
) : null;
|
||||
};
|
||||
|
||||
return { showError, ErrorComponent };
|
||||
};
|
||||
|
||||
export default useCustomError;
|
||||
@@ -1,27 +0,0 @@
|
||||
import LangSelectSimple from '@/components/LangSelect';
|
||||
import { Flex, UseDisclosureReturn } from '@chakra-ui/react';
|
||||
import { I18n } from 'next-i18next';
|
||||
|
||||
type LanguageType = { disclosure: UseDisclosureReturn; i18n: I18n | null };
|
||||
|
||||
const Language = ({ disclosure, i18n }: LanguageType) => {
|
||||
return (
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
position={'absolute'}
|
||||
top={'42px'}
|
||||
right={'42px'}
|
||||
cursor={'pointer'}
|
||||
gap={'16px'}
|
||||
>
|
||||
<LangSelectSimple
|
||||
w="36px"
|
||||
h="36px"
|
||||
background={'rgba(244, 246, 248, 0.7)'}
|
||||
boxShadow={'0px 1.2px 2.3px rgba(0, 0, 0, 0.2)'}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default Language;
|
||||
@@ -1,208 +0,0 @@
|
||||
import { signInByPassword } from '@/api/user';
|
||||
import { LocklIcon, PersonIcon, VectorIcon } from '@/components/Icon';
|
||||
import useSessionStore from '@/stores/session';
|
||||
import { Flex, Input, InputGroup, InputLeftAddon, Text } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
export default function usePassword({
|
||||
showError
|
||||
}: {
|
||||
showError: (errorMessage: string, duration?: number) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const [userExist, setUserExist] = useState(true);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
// 对于注册的用户,需要先验证密码 0 默认页面;1为验证密码页面
|
||||
const [pageState, setPageState] = useState(0);
|
||||
|
||||
const setSession = useSessionStore((s) => s.setSession);
|
||||
|
||||
const { register, handleSubmit, watch, trigger, getValues } = useForm<{
|
||||
username: string;
|
||||
password: string;
|
||||
confimPassword: string;
|
||||
}>();
|
||||
|
||||
const login = async () => {
|
||||
const deepSearch = (obj: any): string => {
|
||||
if (!obj || typeof obj !== 'object') return t('Submit Error');
|
||||
if (!!obj.message) {
|
||||
return obj.message;
|
||||
}
|
||||
return deepSearch(Object.values(obj)[0]);
|
||||
};
|
||||
|
||||
handleSubmit(
|
||||
async (data) => {
|
||||
if (data?.username && data?.password) {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const result = await signInByPassword(data.username, data.password);
|
||||
setSession(result);
|
||||
router.replace('/pricing');
|
||||
} catch (error: any) {
|
||||
console.log(error);
|
||||
showError(t('Invalid username or password'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
console.log(err);
|
||||
showError(deepSearch(err));
|
||||
}
|
||||
)();
|
||||
};
|
||||
|
||||
const PasswordComponent = () => {
|
||||
if (pageState === 0) {
|
||||
return <PasswordModal />;
|
||||
} else {
|
||||
return <ConfirmPasswordModal />;
|
||||
}
|
||||
};
|
||||
|
||||
const PasswordModal = () => {
|
||||
return (
|
||||
<>
|
||||
<InputGroup
|
||||
variant={'unstyled'}
|
||||
bg="rgba(255, 255, 255, 0.65)"
|
||||
mt={'20px'}
|
||||
width="266px"
|
||||
minH="42px"
|
||||
mb="14px"
|
||||
borderRadius="4px"
|
||||
p="10px"
|
||||
border="1px solid #E5E5E5"
|
||||
>
|
||||
<InputLeftAddon>
|
||||
<PersonIcon />
|
||||
</InputLeftAddon>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder={t('Username') || ''}
|
||||
pl={'12px'}
|
||||
variant={'unstyled'}
|
||||
fontSize="14px"
|
||||
id="username"
|
||||
fontWeight="400"
|
||||
_autofill={{
|
||||
backgroundColor: 'transparent !important',
|
||||
backgroundImage: 'none !important'
|
||||
}}
|
||||
{...register('username', {
|
||||
pattern: {
|
||||
value: /^[a-zA-Z0-9_-]{3,16}$/,
|
||||
message: 'username tips'
|
||||
},
|
||||
required: true
|
||||
})}
|
||||
/>
|
||||
</InputGroup>
|
||||
<InputGroup
|
||||
variant={'unstyled'}
|
||||
bg="rgba(255, 255, 255, 0.65)"
|
||||
width="266px"
|
||||
minH="42px"
|
||||
mb="14px"
|
||||
borderRadius="4px"
|
||||
p="10px"
|
||||
border="1px solid #E5E5E5"
|
||||
>
|
||||
<InputLeftAddon>
|
||||
<LocklIcon />
|
||||
</InputLeftAddon>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder={t('Password') || ''}
|
||||
pl={'12px'}
|
||||
variant={'unstyled'}
|
||||
fontSize="14px"
|
||||
id="password"
|
||||
fontWeight="400"
|
||||
_autofill={{
|
||||
backgroundColor: 'transparent !important',
|
||||
backgroundImage: 'none !important'
|
||||
}}
|
||||
{...register('password', {
|
||||
pattern: {
|
||||
value: /^(?=.*\S).{8,}$/,
|
||||
message: 'password tips'
|
||||
},
|
||||
required: true
|
||||
})}
|
||||
/>
|
||||
</InputGroup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ConfirmPasswordModal = () => {
|
||||
return (
|
||||
<>
|
||||
<Flex p={'0'} alignItems={'center'} width="266px" minH="42px" mb="14px" borderRadius="4px">
|
||||
<VectorIcon
|
||||
fill={'#FFFFFF'}
|
||||
w={'20px'}
|
||||
transform={'rotate(-90deg)'}
|
||||
h={'20px'}
|
||||
mr={'16px'}
|
||||
display={'inline-block'}
|
||||
verticalAlign={'middle'}
|
||||
cursor={'pointer'}
|
||||
onClick={() => setPageState(0)}
|
||||
/>
|
||||
<Text color={'#FFFFFF'}>{t('Verify password')}</Text>
|
||||
</Flex>
|
||||
<InputGroup
|
||||
variant={'unstyled'}
|
||||
bg="rgba(255, 255, 255, 0.65)"
|
||||
width="266px"
|
||||
minH="42px"
|
||||
mb="14px"
|
||||
borderRadius="4px"
|
||||
p="10px"
|
||||
border="1px solid #E5E5E5"
|
||||
>
|
||||
<InputLeftAddon>
|
||||
<LocklIcon />
|
||||
</InputLeftAddon>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder={t('Verify password') || 'Verify password'}
|
||||
pl={'12px'}
|
||||
fontSize="14px"
|
||||
id="repassword"
|
||||
fontWeight="400"
|
||||
variant={'unstyled'}
|
||||
_autofill={{
|
||||
backgroundColor: 'transparent !important',
|
||||
backgroundImage: 'none !important'
|
||||
}}
|
||||
{...register('confimPassword', {
|
||||
pattern: {
|
||||
value: /^(?=.*\S).{8,}$/,
|
||||
message: 'password tips'
|
||||
},
|
||||
required: true
|
||||
})}
|
||||
/>
|
||||
</InputGroup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
PasswordComponent,
|
||||
login,
|
||||
userExist,
|
||||
pageState,
|
||||
isLoading
|
||||
};
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
import { Checkbox, Flex, Link, Text, TextProps } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useState } from 'react';
|
||||
|
||||
const useProtocol = ({
|
||||
service_protocol,
|
||||
private_protocol
|
||||
}: {
|
||||
service_protocol: string;
|
||||
private_protocol: string;
|
||||
}) => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const [isAgree, setIsAgree] = useState(false);
|
||||
const [isInvalid, setIsInvalid] = useState(false);
|
||||
|
||||
const Protocol = () => (
|
||||
<Flex
|
||||
className="protocol"
|
||||
w="266px"
|
||||
alignItems={'center'}
|
||||
mb={'16px'}
|
||||
width="266px"
|
||||
minH="42px"
|
||||
borderRadius="4px"
|
||||
p="10px"
|
||||
>
|
||||
<Checkbox
|
||||
isInvalid={isInvalid}
|
||||
mr={'8px'}
|
||||
isChecked={isAgree}
|
||||
variant={'unstyled'}
|
||||
onChange={(e) => {
|
||||
setIsInvalid(false);
|
||||
setIsAgree(e.target.checked);
|
||||
}}
|
||||
/>
|
||||
<Text fontStyle="normal" fontWeight="400" fontSize="12px" lineHeight="140%" color="#FFFFFF">
|
||||
{t('agree policy')}
|
||||
<Link
|
||||
href={service_protocol}
|
||||
_hover={{
|
||||
color: 'rgba(94, 189, 242, 1)',
|
||||
borderBottom: '1px solid rgba(94, 189, 242, 1)'
|
||||
}}
|
||||
px="4px"
|
||||
>
|
||||
{t('Service Agreement')}
|
||||
</Link>
|
||||
{t('and')}
|
||||
<Link
|
||||
href={private_protocol}
|
||||
_hover={{
|
||||
color: 'rgba(94, 189, 242, 1)',
|
||||
borderBottom: '1px solid rgba(94, 189, 242, 1)'
|
||||
}}
|
||||
px="4px"
|
||||
>
|
||||
{t('Privacy Policy')}
|
||||
</Link>
|
||||
</Text>
|
||||
</Flex>
|
||||
);
|
||||
return {
|
||||
Protocol,
|
||||
isAgree,
|
||||
setIsInvalid
|
||||
};
|
||||
};
|
||||
|
||||
export default useProtocol;
|
||||
@@ -1,188 +0,0 @@
|
||||
import { sendCodeByPhone, signInByPhone } from '@/api/user';
|
||||
import { CodeDoneIcon, SafetyIcon } from '@/components/Icon';
|
||||
import useSessionStore from '@/stores/session';
|
||||
import { Input, InputGroup, InputLeftAddon, InputRightAddon, Link, Text } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import NextLink from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { MouseEventHandler, useEffect, useRef, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
export default function useSms({
|
||||
showError
|
||||
}: {
|
||||
showError: (errorMessage: string, duration?: number) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const _remainTime = useRef(0);
|
||||
const router = useRouter();
|
||||
const { setSession } = useSessionStore();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const { register, handleSubmit, trigger, getValues } = useForm<{
|
||||
phoneNumber: string;
|
||||
verifyCode: string;
|
||||
}>();
|
||||
|
||||
const login = async () => {
|
||||
const deepSearch = (obj: any): string => {
|
||||
if (!obj || typeof obj !== 'object') return t('Submit Error');
|
||||
if (!!obj.message) {
|
||||
return obj.message;
|
||||
}
|
||||
return deepSearch(Object.values(obj)[0]);
|
||||
};
|
||||
|
||||
handleSubmit(
|
||||
async (data) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const result = await signInByPhone(data.phoneNumber, data.verifyCode);
|
||||
setSession(result);
|
||||
router.replace('/pricing');
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
showError(t('Invalid verification code') || 'Invalid verification code');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
showError(deepSearch(err));
|
||||
}
|
||||
)();
|
||||
};
|
||||
|
||||
const SmsModal = () => {
|
||||
const [remainTime, setRemainTime] = useState(_remainTime.current);
|
||||
|
||||
useEffect(() => {
|
||||
if (remainTime <= 0) return;
|
||||
const interval = setInterval(() => {
|
||||
setRemainTime(remainTime - 1);
|
||||
}, 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, [remainTime]);
|
||||
|
||||
const getCode: MouseEventHandler = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!(await trigger('phoneNumber'))) {
|
||||
showError(t('Invalid phone number') || 'Invalid phone number');
|
||||
return;
|
||||
}
|
||||
setRemainTime(60);
|
||||
_remainTime.current = 60;
|
||||
|
||||
try {
|
||||
await sendCodeByPhone(getValues('phoneNumber'));
|
||||
} catch (err) {
|
||||
showError(t('Get code failed') || 'Get code failed');
|
||||
setRemainTime(0);
|
||||
_remainTime.current = 0;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<InputGroup
|
||||
variant={'unstyled'}
|
||||
bg="rgba(255, 255, 255, 0.65)"
|
||||
mt={'20px'}
|
||||
width="266px"
|
||||
minH="42px"
|
||||
mb="14px"
|
||||
borderRadius="4px"
|
||||
p="10px"
|
||||
border="1px solid #E5E5E5"
|
||||
>
|
||||
<InputLeftAddon>
|
||||
<Text pr={'7px'} borderRight="1px solid rgba(0, 0, 0, 0.4)">
|
||||
+86
|
||||
</Text>
|
||||
</InputLeftAddon>
|
||||
|
||||
<Input
|
||||
type="tel"
|
||||
placeholder={t('phone number tips') || ''}
|
||||
mx={'12px'}
|
||||
variant={'unstyled'}
|
||||
bg={'transparent'}
|
||||
id="phoneNumber"
|
||||
fontSize="14px"
|
||||
fontWeight="400"
|
||||
_autofill={{
|
||||
bg: 'transparent'
|
||||
}}
|
||||
{...register('phoneNumber', {
|
||||
pattern: {
|
||||
value: /^1[3-9]\d{9}$/,
|
||||
message: 'Invalid mobile number'
|
||||
},
|
||||
required: 'Phone number can not be blank'
|
||||
})}
|
||||
/>
|
||||
<InputRightAddon
|
||||
fontStyle="normal"
|
||||
fontWeight="500"
|
||||
fontSize="10px"
|
||||
lineHeight="20px"
|
||||
color="#219BF4"
|
||||
>
|
||||
{remainTime <= 0 ? (
|
||||
<Link as={NextLink} href="" onClick={getCode}>
|
||||
{t('Get Code')}
|
||||
</Link>
|
||||
) : (
|
||||
<Text>{remainTime} s</Text>
|
||||
)}
|
||||
</InputRightAddon>
|
||||
</InputGroup>
|
||||
|
||||
<InputGroup
|
||||
variant={'unstyled'}
|
||||
bg="rgba(255, 255, 255, 0.65)"
|
||||
width="266px"
|
||||
minH="42px"
|
||||
mb="14px"
|
||||
borderRadius="4px"
|
||||
p="10px"
|
||||
border="1px solid #E5E5E5"
|
||||
>
|
||||
<InputLeftAddon>
|
||||
<SafetyIcon />
|
||||
</InputLeftAddon>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder={t('verify code tips') || ''}
|
||||
pl={'12px'}
|
||||
variant={'unstyled'}
|
||||
id="verifyCode"
|
||||
fontSize="14px"
|
||||
fontWeight="400"
|
||||
_autofill={{
|
||||
backgroundColor: 'transparent !important',
|
||||
backgroundImage: 'none !important'
|
||||
}}
|
||||
{...register('verifyCode', {
|
||||
required: 'verify code can not be blank',
|
||||
pattern: {
|
||||
value: /^[0-9]{6}$/,
|
||||
message: 'Invalid verification code'
|
||||
}
|
||||
})}
|
||||
/>
|
||||
<InputRightAddon>
|
||||
{getValues('verifyCode')?.length === 6 && <CodeDoneIcon />}
|
||||
</InputRightAddon>
|
||||
</InputGroup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
SmsModal,
|
||||
login,
|
||||
isLoading
|
||||
};
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import { Box, Flex, Image, Text } from '@chakra-ui/react';
|
||||
|
||||
export default function Background() {
|
||||
return (
|
||||
<>
|
||||
<Image alt="bg" src="/images/bg.svg" zIndex={-100} position={'fixed'} w="100vw" h="100vh" />
|
||||
<Image
|
||||
alt="bg"
|
||||
src="/images/bg-bottom.svg"
|
||||
zIndex={-98}
|
||||
position={'fixed'}
|
||||
w="100vw"
|
||||
bottom={0}
|
||||
left={0}
|
||||
/>
|
||||
<Image
|
||||
alt="bg"
|
||||
src="/images/moon.svg"
|
||||
zIndex={-96}
|
||||
position={'fixed'}
|
||||
w="100vw"
|
||||
top={0}
|
||||
left={0}
|
||||
/>
|
||||
<Flex position={'fixed'} top={'40px'} left={'42px'} alignItems={'center'}>
|
||||
<Image
|
||||
cursor={'pointer'}
|
||||
p="2px"
|
||||
width="36px"
|
||||
height="36px"
|
||||
src={'/images/sealos.svg'}
|
||||
fallbackSrc="/images/sealos.svg"
|
||||
alt="logo"
|
||||
/>
|
||||
<Text ml="6px" mr="12px" fontSize={20} fontWeight={700} color={'#fff'} cursor={'pointer'}>
|
||||
Sealos
|
||||
</Text>
|
||||
<Text fontSize={16} fontWeight={600} color={'#fff'}>
|
||||
| License
|
||||
</Text>
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,207 +0,0 @@
|
||||
import { getSystemEnv, uploadConvertData } from '@/api/system';
|
||||
import useAuthList from '@/components/Signin/auth/useAuthList';
|
||||
import useCustomError from '@/components/Signin/auth/useCustomError';
|
||||
import Language from '@/components/Signin/auth/useLanguage';
|
||||
import usePassword from '@/components/Signin/auth/usePassword';
|
||||
import useProtocol from '@/components/Signin/auth/useProtocol';
|
||||
import useSms from '@/components/Signin/auth/useSms';
|
||||
import useSessionStore from '@/stores/session';
|
||||
import { LoginType } from '@/types';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Flex,
|
||||
Img,
|
||||
Tab,
|
||||
TabIndicator,
|
||||
TabList,
|
||||
Tabs,
|
||||
Image,
|
||||
useDisclosure
|
||||
} from '@chakra-ui/react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { debounce } from 'lodash';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import Head from 'next/head';
|
||||
import { useRouter } from 'next/router';
|
||||
import sealosTitle from 'public/images/sealos-title.png';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import Background from './background';
|
||||
|
||||
export default function SigninComponent() {
|
||||
const { data: platformEnv } = useQuery(['getPlatformEnv'], getSystemEnv);
|
||||
|
||||
const {
|
||||
service_protocol = '',
|
||||
private_protocol = '',
|
||||
needPassword = false,
|
||||
needSms = false
|
||||
} = platformEnv || {};
|
||||
|
||||
const needTabs = needPassword && needSms;
|
||||
|
||||
const disclosure = useDisclosure();
|
||||
const { t, i18n } = useTranslation();
|
||||
const [tabIndex, setTabIndex] = useState<LoginType>(LoginType.NONE);
|
||||
|
||||
const { ErrorComponent, showError } = useCustomError();
|
||||
|
||||
const { Protocol, isAgree, setIsInvalid } = useProtocol({ service_protocol, private_protocol });
|
||||
const { SmsModal, login: smsSubmit, isLoading: smsLoading } = useSms({ showError });
|
||||
const {
|
||||
PasswordComponent,
|
||||
pageState,
|
||||
login: passwordSubmit,
|
||||
isLoading: passwordLoading
|
||||
} = usePassword({ showError });
|
||||
const isLoading = useMemo(() => passwordLoading || smsLoading, [passwordLoading, smsLoading]);
|
||||
// const isSignIn = useSessionStore((s) => s.isUserLogin);
|
||||
// const router = useRouter();
|
||||
// useEffect(() => {
|
||||
// if (isSignIn()) {
|
||||
// router.push('/pricing');
|
||||
// }
|
||||
// }, [isSignIn, router]);
|
||||
|
||||
const { AuthList } = useAuthList();
|
||||
|
||||
const loginConfig = useMemo(() => {
|
||||
return {
|
||||
[LoginType.SMS]: {
|
||||
login: smsSubmit,
|
||||
component: <SmsModal />
|
||||
},
|
||||
[LoginType.PASSWORD]: {
|
||||
login: passwordSubmit,
|
||||
component: <PasswordComponent />
|
||||
},
|
||||
[LoginType.NONE]: null
|
||||
};
|
||||
}, [PasswordComponent, SmsModal, passwordSubmit, smsSubmit]);
|
||||
|
||||
useEffect(() => {
|
||||
setTabIndex(needSms ? LoginType.SMS : needPassword ? LoginType.PASSWORD : LoginType.NONE);
|
||||
}, [needPassword, needSms]);
|
||||
|
||||
const LoginComponent = useMemo(
|
||||
() => (tabIndex !== LoginType.NONE ? loginConfig[tabIndex].component : null),
|
||||
[loginConfig, tabIndex]
|
||||
);
|
||||
|
||||
const handleLogin = debounce(() => {
|
||||
const selectedConfig = loginConfig[tabIndex];
|
||||
if (isAgree && selectedConfig) {
|
||||
const { login } = selectedConfig;
|
||||
login();
|
||||
uploadConvertData([3])
|
||||
.then((res) => {
|
||||
console.log(res);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
} else {
|
||||
setIsInvalid(true);
|
||||
showError(t('Read and agree'));
|
||||
}
|
||||
}, 500);
|
||||
|
||||
return (
|
||||
<Box
|
||||
position={'relative'}
|
||||
overflow={'hidden'}
|
||||
w="100vw"
|
||||
h="100vh"
|
||||
// backgroundImage={'url(/images/bg.svg)'}
|
||||
// backgroundRepeat={'no-repeat'}
|
||||
// backgroundSize={'cover'}
|
||||
// backgroundAttachment={'fixed'}
|
||||
// min-width="100%"
|
||||
// /min-height="100%"
|
||||
// top={0}
|
||||
// left={0}
|
||||
// right={0}
|
||||
// bottom={0}
|
||||
>
|
||||
<Background />
|
||||
<Head>
|
||||
<title>Sealos Cloud</title>
|
||||
<meta name="description" content="sealos cloud dashboard" />
|
||||
</Head>
|
||||
<Flex h="full" w="full" flexDir={'column'} justifyContent={'center'} alignItems={'center'}>
|
||||
<Box mb="36px">
|
||||
<Img src={sealosTitle.src} w="135px"></Img>
|
||||
</Box>
|
||||
<Flex
|
||||
p="30px 48px"
|
||||
background={'rgba(255, 255, 255, 0.20)'}
|
||||
boxShadow="0px 15px 20px 0px rgba(0, 0, 0, 0.20)"
|
||||
backdropFilter="blur(100px)"
|
||||
borderRadius="12px"
|
||||
flexDir="column"
|
||||
justifyContent="center"
|
||||
align="center"
|
||||
position={'relative'}
|
||||
>
|
||||
<ErrorComponent />
|
||||
|
||||
{pageState === 0 && needTabs && (
|
||||
<Tabs
|
||||
index={tabIndex}
|
||||
onChange={(idx) => setTabIndex(idx)}
|
||||
variant="unstyled"
|
||||
p={'0'}
|
||||
width={'full'}
|
||||
>
|
||||
<TabList
|
||||
borderBottom={'2px solid rgba(255, 255, 255, 0.3)'}
|
||||
p={'0'}
|
||||
fontSize={'16px'}
|
||||
fontWeight={'500'}
|
||||
fontFamily={'PingFang SC'}
|
||||
color={'rgba(255, 255, 255, 0.3)'}
|
||||
gap={'20px'}
|
||||
>
|
||||
<Tab px="0" _selected={{ color: 'white' }}>
|
||||
{t('Verification Code Login')}
|
||||
</Tab>
|
||||
<Tab px="0" _selected={{ color: 'white' }}>
|
||||
{t('Password Login')}
|
||||
</Tab>
|
||||
</TabList>
|
||||
<TabIndicator mt="-2px" height="2px" bg="#FFFFFF" borderRadius="1px" />
|
||||
</Tabs>
|
||||
)}
|
||||
|
||||
{LoginComponent}
|
||||
|
||||
<Protocol />
|
||||
|
||||
<Button
|
||||
variant={'unstyled'}
|
||||
background="linear-gradient(90deg, #000000 0%, rgba(36, 40, 44, 0.9) 98.29%)"
|
||||
boxShadow="0px 4px 4px rgba(0, 0, 0, 0.25)"
|
||||
color="#fff"
|
||||
display={'flex'}
|
||||
justifyContent={'center'}
|
||||
alignItems={'center'}
|
||||
type="submit"
|
||||
_hover={{
|
||||
opacity: '0.85'
|
||||
}}
|
||||
width="266px"
|
||||
minH="42px"
|
||||
mb="14px"
|
||||
borderRadius="4px"
|
||||
p="10px"
|
||||
onClick={handleLogin}
|
||||
>
|
||||
{isLoading ? (t('Loading') || 'Loading') + '...' : t('Log In') || 'Log In'}
|
||||
</Button>
|
||||
<AuthList />
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Language disclosure={disclosure} i18n={i18n} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import { ClusterFormType } from '@/types';
|
||||
|
||||
export type ServiceItem = { label: string; icon: 'check' | 'star' };
|
||||
|
||||
const baseServiceItem: ServiceItem[] = [
|
||||
@@ -55,3 +57,46 @@ export const contect: ServiceItem[] = [
|
||||
{ label: '周一到周日,24h 内响应', icon: 'check' },
|
||||
...baseServiceItem
|
||||
];
|
||||
|
||||
export const CpuSlideMarkList = [
|
||||
{ label: 8, value: 8 },
|
||||
{ label: 256, value: 256 },
|
||||
{ label: 512, value: 512 },
|
||||
{ label: 1024, value: 1024 },
|
||||
{ label: 2048, value: 2048 }
|
||||
];
|
||||
|
||||
export const MemorySlideMarkList = [
|
||||
{ label: 16, value: 16 },
|
||||
{ label: 512, value: 512 },
|
||||
{ label: 1024, value: 1024 },
|
||||
{ label: 2048, value: 2048 },
|
||||
{ label: 4096, value: 4096 }
|
||||
];
|
||||
|
||||
export const MonthMapList = [
|
||||
// unit month
|
||||
{ label: '3个月', value: '3' },
|
||||
{ label: '6个月', value: '6' },
|
||||
{ label: '1年', value: '12' },
|
||||
{ label: '2年', value: '24' },
|
||||
{ label: '3年', value: '36' }
|
||||
];
|
||||
|
||||
export const defaulClustertForm: ClusterFormType = {
|
||||
cpu: 8,
|
||||
memory: 16,
|
||||
months: '3',
|
||||
name: ''
|
||||
};
|
||||
|
||||
export const freeClusterForm: ClusterFormType = {
|
||||
cpu: 8,
|
||||
memory: 16,
|
||||
months: '3',
|
||||
name: ''
|
||||
};
|
||||
|
||||
export const cpuPriceMonth = 6; // ¥
|
||||
|
||||
export const memoryPriceMonth = 3; // ¥
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { freeClusterForm } from '@/constant/product';
|
||||
import { authSession } from '@/services/backend/auth';
|
||||
import {
|
||||
findClusterByUIDAndClusterID,
|
||||
isKubeSystemIDBound,
|
||||
updateCluster,
|
||||
updateClusterIdAndIssueLicense
|
||||
} from '@/services/backend/db/cluster';
|
||||
import { generateLicenseToken, hasIssuedLicense } from '@/services/backend/db/license';
|
||||
import { ExpiredTime, generateLicenseToken, hasIssuedLicense } from '@/services/backend/db/license';
|
||||
import { getPaymentByID } from '@/services/backend/db/payment';
|
||||
import { jsonRes } from '@/services/backend/response';
|
||||
import { LicenseRecordPayload } from '@/types';
|
||||
import { result } from 'lodash';
|
||||
import { calculatePrice } from '@/utils/tools';
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
export type ActiveClusterParams = {
|
||||
@@ -32,7 +32,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
if (isBound) {
|
||||
return jsonRes(res, {
|
||||
code: 400,
|
||||
message: 'clusterID is already bound to another cluster'
|
||||
message: 'ID is already bound to another cluster'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -41,8 +41,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
clusterId: clusterId
|
||||
});
|
||||
|
||||
console.log(existingCluster, 'existingCluster');
|
||||
|
||||
if (existingCluster?.kubeSystemID) {
|
||||
return jsonRes(res, {
|
||||
code: 400,
|
||||
@@ -50,29 +48,36 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
});
|
||||
}
|
||||
|
||||
const handleEnterpriseAndLicense = async () => {
|
||||
const handleScaledStandard = async () => {
|
||||
if (!existingCluster?.orderID) return;
|
||||
const payment = await getPaymentByID({
|
||||
uid: userInfo.uid,
|
||||
orderID: existingCluster.orderID
|
||||
});
|
||||
if (!payment) {
|
||||
return jsonRes(res, { code: 400, message: 'No order found' });
|
||||
return { message: 'No order found' };
|
||||
}
|
||||
|
||||
const issuedLicense = await hasIssuedLicense({
|
||||
uid: userInfo.uid,
|
||||
orderID: existingCluster.orderID
|
||||
});
|
||||
if (issuedLicense) {
|
||||
return jsonRes(res, { code: 400, message: 'orderID cannot be reused' });
|
||||
return { message: 'orderID cannot be reused' };
|
||||
}
|
||||
|
||||
const _token = generateLicenseToken({
|
||||
type: 'Account',
|
||||
clusterID: kubeSystemID,
|
||||
data: { amount: payment.amount }
|
||||
});
|
||||
const expiredTime = parseInt(existingCluster.months || freeClusterForm.months) * ExpiredTime;
|
||||
const _token = generateLicenseToken(
|
||||
{
|
||||
type: 'Cluster',
|
||||
clusterID: kubeSystemID,
|
||||
data: {
|
||||
nodeCount: 99,
|
||||
totalCPU: existingCluster.cpu || freeClusterForm.cpu,
|
||||
totalMemory: existingCluster.memory || freeClusterForm.memory
|
||||
}
|
||||
},
|
||||
expiredTime
|
||||
);
|
||||
|
||||
const record: LicenseRecordPayload = {
|
||||
uid: userInfo.uid,
|
||||
@@ -81,8 +86,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
orderID: existingCluster.orderID,
|
||||
quota: payment.amount,
|
||||
payMethod: payment.payMethod,
|
||||
type: 'Account',
|
||||
clusterId: clusterId
|
||||
type: 'Cluster',
|
||||
clusterId: clusterId,
|
||||
cpu: existingCluster.cpu,
|
||||
memory: existingCluster.memory,
|
||||
months: existingCluster.months,
|
||||
expiredTime
|
||||
};
|
||||
|
||||
const result = await updateClusterIdAndIssueLicense({
|
||||
@@ -95,52 +104,52 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
return result;
|
||||
};
|
||||
|
||||
const handleStandardAndLicense = async () => {
|
||||
const _token = generateLicenseToken({
|
||||
type: 'Account',
|
||||
clusterID: kubeSystemID,
|
||||
data: { amount: 299 }
|
||||
});
|
||||
const handleStandard = async () => {
|
||||
const expiredTime = parseInt(freeClusterForm.months) * ExpiredTime;
|
||||
|
||||
const _token = generateLicenseToken(
|
||||
{
|
||||
type: 'Cluster',
|
||||
clusterID: kubeSystemID,
|
||||
data: {
|
||||
nodeCount: 99,
|
||||
totalCPU: freeClusterForm.cpu,
|
||||
totalMemory: freeClusterForm.memory
|
||||
}
|
||||
},
|
||||
expiredTime
|
||||
);
|
||||
|
||||
const record: LicenseRecordPayload = {
|
||||
uid: userInfo.uid,
|
||||
amount: 299,
|
||||
amount: 0,
|
||||
token: _token,
|
||||
orderID: '',
|
||||
quota: 299,
|
||||
quota: 0,
|
||||
payMethod: 'stripe',
|
||||
type: 'Account',
|
||||
clusterId: clusterId
|
||||
type: 'Cluster',
|
||||
clusterId: clusterId,
|
||||
cpu: freeClusterForm.cpu,
|
||||
memory: freeClusterForm.memory,
|
||||
months: freeClusterForm.months,
|
||||
expiredTime
|
||||
};
|
||||
|
||||
const result = await updateClusterIdAndIssueLicense({
|
||||
uid: userInfo.uid,
|
||||
clusterId: clusterId,
|
||||
kubeSystemID: kubeSystemID,
|
||||
licensePayload: record
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
const handleActive = async () => {
|
||||
return await updateCluster({
|
||||
uid: userInfo.uid,
|
||||
clusterId: clusterId,
|
||||
updates: {
|
||||
kubeSystemID: kubeSystemID,
|
||||
kubeSystemUpdateAt: new Date()
|
||||
}
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
let activeResult;
|
||||
if (existingCluster?.orderID === null && existingCluster?.licenseID === null) {
|
||||
activeResult = await handleActive();
|
||||
} else if (existingCluster?.orderID === null && !existingCluster?.licenseID) {
|
||||
activeResult = await handleStandardAndLicense();
|
||||
} else if (existingCluster?.orderID !== null && !existingCluster?.licenseID) {
|
||||
activeResult = await handleEnterpriseAndLicense();
|
||||
if (existingCluster?.orderID) {
|
||||
activeResult = await handleScaledStandard();
|
||||
} else {
|
||||
activeResult = await handleActive();
|
||||
console.log('其他情况');
|
||||
activeResult = await handleStandard();
|
||||
}
|
||||
|
||||
return jsonRes(res, {
|
||||
|
||||
@@ -1,20 +1,29 @@
|
||||
import { freeClusterForm } from '@/constant/product';
|
||||
import { authSession } from '@/services/backend/auth';
|
||||
import { createClusterRecord } from '@/services/backend/db/cluster';
|
||||
import { getPaymentByID } from '@/services/backend/db/payment';
|
||||
import { jsonRes } from '@/services/backend/response';
|
||||
import { ClusterType, CreateClusterParams, PaymentStatus } from '@/types';
|
||||
import { calculatePrice } from '@/utils/tools';
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { type = ClusterType.Standard, orderID } = req.body as CreateClusterParams;
|
||||
const {
|
||||
type = ClusterType.Standard,
|
||||
orderID,
|
||||
cpu,
|
||||
memory,
|
||||
name,
|
||||
months
|
||||
} = req.body as CreateClusterParams;
|
||||
|
||||
const userInfo = await authSession(req.headers);
|
||||
if (!userInfo) {
|
||||
return jsonRes(res, { code: 401, message: 'token verify error' });
|
||||
}
|
||||
|
||||
if (type === ClusterType.Enterprise && orderID) {
|
||||
if (orderID) {
|
||||
const payment = await getPaymentByID({ uid: userInfo.uid, orderID: orderID });
|
||||
if (!payment) {
|
||||
return jsonRes(res, { code: 400, message: 'No order found' });
|
||||
@@ -25,30 +34,24 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
message: 'Unpaid'
|
||||
});
|
||||
}
|
||||
const result = await createClusterRecord({
|
||||
uid: userInfo.uid,
|
||||
type: type,
|
||||
orderID: orderID
|
||||
});
|
||||
|
||||
return jsonRes(res, {
|
||||
data: result
|
||||
});
|
||||
const price = calculatePrice({ cpu, months, memory }, freeClusterForm);
|
||||
if (payment.amount !== price) {
|
||||
return jsonRes(res, { code: 400, message: 'mismatch between order and cluster size' });
|
||||
}
|
||||
}
|
||||
|
||||
if (type === ClusterType.Standard) {
|
||||
const result = await createClusterRecord({
|
||||
uid: userInfo.uid,
|
||||
type: type
|
||||
});
|
||||
return jsonRes(res, {
|
||||
data: result
|
||||
});
|
||||
}
|
||||
const result = await createClusterRecord({
|
||||
uid: userInfo.uid,
|
||||
type: type,
|
||||
orderID: orderID,
|
||||
cpu,
|
||||
memory,
|
||||
months,
|
||||
name
|
||||
});
|
||||
|
||||
return jsonRes(res, {
|
||||
code: 400,
|
||||
message: 'Request parameter error'
|
||||
data: result
|
||||
});
|
||||
} catch (error) {
|
||||
jsonRes(res, { code: 500, data: error });
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { freeClusterForm } from '@/constant/product';
|
||||
import { authSession } from '@/services/backend/auth';
|
||||
import { findClusterByUIDAndClusterID } from '@/services/backend/db/cluster';
|
||||
import {
|
||||
ExpiredTime,
|
||||
createLicenseRecord,
|
||||
generateLicenseToken,
|
||||
hasIssuedLicense
|
||||
@@ -8,27 +10,21 @@ import {
|
||||
import { getPaymentByID } from '@/services/backend/db/payment';
|
||||
import { jsonRes } from '@/services/backend/response';
|
||||
import { CreateLicenseParams, LicenseRecordPayload, PaymentStatus } from '@/types';
|
||||
import { calculatePrice } from '@/utils/tools';
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { orderID, clusterId } = req.body as CreateLicenseParams;
|
||||
const { orderID, clusterId, cpu, memory, months } = req.body as CreateLicenseParams;
|
||||
const userInfo = await authSession(req.headers);
|
||||
if (!userInfo) {
|
||||
return jsonRes(res, { code: 401, message: 'token verify error' });
|
||||
}
|
||||
if (!orderID || !clusterId) {
|
||||
|
||||
if (!clusterId || !cpu || !memory || !months) {
|
||||
return jsonRes(res, { code: 400, message: 'Request parameter error' });
|
||||
}
|
||||
|
||||
const payment = await getPaymentByID({ uid: userInfo.uid, orderID: orderID });
|
||||
if (!payment) {
|
||||
return jsonRes(res, { code: 400, message: 'No order found' });
|
||||
}
|
||||
const issuedLicense = await hasIssuedLicense({ uid: userInfo.uid, orderID: orderID });
|
||||
if (issuedLicense) {
|
||||
return jsonRes(res, { code: 400, message: 'orderID cannot be reused' });
|
||||
}
|
||||
const cluster = await findClusterByUIDAndClusterID({
|
||||
uid: userInfo.uid,
|
||||
clusterId: clusterId
|
||||
@@ -40,11 +36,62 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
});
|
||||
}
|
||||
|
||||
const _token = generateLicenseToken({
|
||||
type: 'Account',
|
||||
clusterID: cluster.kubeSystemID,
|
||||
data: { amount: payment.amount }
|
||||
});
|
||||
const expiredTime = parseInt(months) * ExpiredTime;
|
||||
|
||||
if (!orderID) {
|
||||
const freeToken = generateLicenseToken(
|
||||
{
|
||||
type: 'Cluster',
|
||||
clusterID: cluster.kubeSystemID,
|
||||
data: {
|
||||
totalCPU: freeClusterForm.cpu,
|
||||
totalMemory: freeClusterForm.memory,
|
||||
nodeCount: 99
|
||||
}
|
||||
},
|
||||
expiredTime
|
||||
);
|
||||
const freeRecord: LicenseRecordPayload = {
|
||||
uid: userInfo.uid,
|
||||
amount: 0,
|
||||
token: freeToken,
|
||||
orderID: '',
|
||||
quota: 0,
|
||||
payMethod: 'stripe',
|
||||
type: 'Cluster',
|
||||
clusterId,
|
||||
cpu: freeClusterForm.cpu,
|
||||
memory: freeClusterForm.memory,
|
||||
months: freeClusterForm.months,
|
||||
expiredTime
|
||||
};
|
||||
const freeResult = await createLicenseRecord(freeRecord);
|
||||
return jsonRes(res, { data: freeResult });
|
||||
}
|
||||
|
||||
const payment = await getPaymentByID({ uid: userInfo.uid, orderID: orderID });
|
||||
if (!payment) {
|
||||
return jsonRes(res, { code: 400, message: 'No order found' });
|
||||
}
|
||||
const price = calculatePrice({ cpu, months, memory }, freeClusterForm);
|
||||
|
||||
if (payment.amount !== price) {
|
||||
return jsonRes(res, { code: 400, message: 'mismatch between order and cluster size' });
|
||||
}
|
||||
|
||||
const issuedLicense = await hasIssuedLicense({ uid: userInfo.uid, orderID: orderID });
|
||||
if (issuedLicense) {
|
||||
return jsonRes(res, { code: 400, message: 'orderID cannot be reused' });
|
||||
}
|
||||
|
||||
const _token = generateLicenseToken(
|
||||
{
|
||||
type: 'Cluster',
|
||||
clusterID: cluster.kubeSystemID,
|
||||
data: { totalCPU: cpu, totalMemory: memory, nodeCount: 99 }
|
||||
},
|
||||
expiredTime
|
||||
);
|
||||
|
||||
const record: LicenseRecordPayload = {
|
||||
uid: userInfo.uid,
|
||||
@@ -53,8 +100,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
orderID: orderID,
|
||||
quota: payment.amount,
|
||||
payMethod: payment.payMethod,
|
||||
type: 'Account',
|
||||
clusterId: clusterId
|
||||
type: 'Cluster',
|
||||
clusterId: clusterId,
|
||||
cpu: cpu,
|
||||
memory: memory,
|
||||
months: months,
|
||||
expiredTime
|
||||
};
|
||||
|
||||
if (payment.status !== PaymentStatus.PaymentSuccess) {
|
||||
|
||||
@@ -39,7 +39,6 @@ export default function TutorialPanel() {
|
||||
kubeSystemID: id
|
||||
});
|
||||
}
|
||||
console.log('active cluster', result);
|
||||
setClusterDetail(result);
|
||||
queryClient.invalidateQueries({ queryKey: ['getClusterList'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['getLicenseByClusterId'] });
|
||||
|
||||
@@ -100,7 +100,6 @@ export default function CommandForm({
|
||||
() =>
|
||||
debounce((data: CommandFormType) => {
|
||||
try {
|
||||
console.log(2, data);
|
||||
generateBashScript({ data, basePath, cloudVersion, enterprise });
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { DownloadIcon, EmptyIcon, LicenseIcon, TokenIcon } from '@/components/Icon';
|
||||
import { Box, Button, Flex, Text } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useRef, useState } from 'react';
|
||||
import RechargeComponent from './Recharge';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getLicenseByClusterId } from '@/api/license';
|
||||
import CurrencySymbol from '@/components/CurrencySymbol';
|
||||
import { DownloadIcon, EmptyIcon, LicenseIcon, TokenIcon } from '@/components/Icon';
|
||||
import RechargeComponent from '@/components/recharge';
|
||||
import useClusterDetail from '@/stores/cluster';
|
||||
import { download } from '@/utils/downloadFIle';
|
||||
import { json2License } from '@/utils/json2Yaml';
|
||||
import CurrencySymbol from '@/components/CurrencySymbol';
|
||||
import { getRemainingTime } from '@/utils/tools';
|
||||
import useClusterDetail from '@/stores/cluster';
|
||||
import { Box, Button, Flex, Text, useToast } from '@chakra-ui/react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useRef, useState } from 'react';
|
||||
|
||||
export default function License() {
|
||||
const { t } = useTranslation();
|
||||
@@ -17,6 +17,7 @@ export default function License() {
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const rechargeRef = useRef<{ onOpen: () => void }>();
|
||||
const { clusterDetail } = useClusterDetail();
|
||||
const toast = useToast();
|
||||
|
||||
const { data } = useQuery(
|
||||
['getLicenseByClusterId', page, pageSize, clusterDetail?.clusterId],
|
||||
@@ -35,7 +36,7 @@ export default function License() {
|
||||
);
|
||||
|
||||
const downloadToken = (token: string) => {
|
||||
download('license.yaml', json2License({ token: token, type: 'Account' }));
|
||||
download('license.yaml', json2License({ token: token, type: 'Cluster' }));
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -56,6 +57,16 @@ export default function License() {
|
||||
w="140px"
|
||||
variant={'black'}
|
||||
onClick={() => {
|
||||
if (!clusterDetail?.cpu && !clusterDetail?.memory) {
|
||||
return toast({
|
||||
title: '注意:旧版本不再支持续费,请获取最新版本的集群',
|
||||
description:
|
||||
'如果您需要续费,请先升级到最新版本的集群。遇到其他问题,如需帮助,可以通过工单及时联系我们。',
|
||||
position: 'top',
|
||||
isClosable: true,
|
||||
status: 'warning'
|
||||
});
|
||||
}
|
||||
rechargeRef.current?.onOpen();
|
||||
}}
|
||||
>
|
||||
@@ -64,7 +75,7 @@ export default function License() {
|
||||
</Flex>
|
||||
|
||||
{data?.records && data?.records?.length > 0 ? (
|
||||
<Box w="100%" flex={1} overflowY={'auto'}>
|
||||
<Box w="100%" flex={1} overflowY={'auto'} mb={'16px'}>
|
||||
{data?.records.map((item) => (
|
||||
<Flex
|
||||
key={item?._id?.toString() ?? item.iat}
|
||||
@@ -77,19 +88,30 @@ export default function License() {
|
||||
mt="16px"
|
||||
>
|
||||
<Box w="100%">
|
||||
<Flex alignItems={'center'} mb="8px">
|
||||
<Flex alignItems={'center'}>
|
||||
<LicenseIcon />
|
||||
<Text color={'#485058'} fontSize={'16px'} fontWeight={500} mx="10px">
|
||||
License
|
||||
</Text>
|
||||
<CurrencySymbol w="14px" h="14px" type={'shellCoin'} />
|
||||
<Text color={'#5A646E'} fontSize={'12px'} fontWeight={500}>
|
||||
{item.amount}
|
||||
{item?.amount}
|
||||
</Text>
|
||||
</Flex>
|
||||
<Text color={'#5A646E'} fontSize={'12px'} fontWeight={500}>
|
||||
{t('Remaining Time')} {getRemainingTime(item.exp)}
|
||||
</Text>
|
||||
{item?.cpu && (
|
||||
<Flex
|
||||
mt={'8px'}
|
||||
color={'#5A646E'}
|
||||
fontSize={'12px'}
|
||||
fontWeight={500}
|
||||
alignItems={'center'}
|
||||
gap={'16px'}
|
||||
>
|
||||
<Text>CPU: {item?.cpu} Core</Text>
|
||||
<Text>内存: {item?.memory} G</Text>
|
||||
<Text>有效时间: {getRemainingTime(item.exp)}</Text>
|
||||
</Flex>
|
||||
)}
|
||||
</Box>
|
||||
<Box h="100%" w="1px" border={'1px solid #EFF0F1'}></Box>
|
||||
<Flex justifyContent={'center'} alignItems={'center'} ml="auto" w="160px">
|
||||
@@ -122,7 +144,7 @@ export default function License() {
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
<RechargeComponent ref={rechargeRef} />
|
||||
<RechargeComponent ref={rechargeRef} isLicensePay={true} key={'license'} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ export default function ClusterRecord() {
|
||||
cursor={'pointer'}
|
||||
>
|
||||
<Flex w="100%" flexDirection={'column'}>
|
||||
<Flex>
|
||||
<Flex alignItems={'center'}>
|
||||
<Text
|
||||
color={'#485058'}
|
||||
fontSize={'16px'}
|
||||
@@ -77,25 +77,24 @@ export default function ClusterRecord() {
|
||||
>
|
||||
{item?.displayName ? item.displayName : `${t('Cluster')}`}
|
||||
</Text>
|
||||
<Center
|
||||
bg={
|
||||
item.type === ClusterType.Standard
|
||||
? 'rgba(0, 169, 166, 0.10)'
|
||||
: 'rgba(33, 155, 244, 0.10)'
|
||||
}
|
||||
color={item.type === ClusterType.Standard ? '#00A9A6' : 'brightBlue.500'}
|
||||
borderRadius="4px"
|
||||
p="3px 8px"
|
||||
fontSize={'12px'}
|
||||
fontWeight={500}
|
||||
>
|
||||
{t(item.type)}
|
||||
</Center>
|
||||
<Text color={'#7B838B'} fontSize={'11px'} fontWeight={400}>
|
||||
集群ID: {item?.kubeSystemID ? item?.kubeSystemID : '未绑定'}
|
||||
</Text>
|
||||
</Flex>
|
||||
<Text mt="8px" color={'#7B838B'} fontSize={'11px'} fontWeight={400}>
|
||||
集群ID: {item?.kubeSystemID ? item?.kubeSystemID : '未绑定'}
|
||||
{/* {formatTime(item.createdAt, 'YYYY-MM-DD HH:mm')} */}
|
||||
</Text>
|
||||
|
||||
{item?.cpu && (
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
fontSize={'10px'}
|
||||
fontWeight={500}
|
||||
color={'#667085'}
|
||||
gap={'16px'}
|
||||
mt={'9px'}
|
||||
>
|
||||
<Text>CPU: {item?.cpu} Core</Text>
|
||||
<Text>内存: {item?.memory} G</Text>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
<Flex justifyContent={'center'} alignItems={'center'} ml="auto" flexShrink={0}>
|
||||
{item?.kubeSystemID ? (
|
||||
|
||||
@@ -1,382 +0,0 @@
|
||||
import { checkWechatPay, createPayment, handlePaymentResult } from '@/api/payment';
|
||||
import { getSystemEnv, uploadConvertData } from '@/api/system';
|
||||
import { LeftIcon, StripeIcon, WechatIcon } from '@/components/Icon';
|
||||
import useBonusBox from '@/hooks/useBonusBox';
|
||||
import { PaymentStatus, TPayMethod, WechatPaymentData } from '@/types';
|
||||
import { deFormatMoney } from '@/utils/tools';
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Flex,
|
||||
Link,
|
||||
Modal,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
Spinner,
|
||||
Text,
|
||||
useDisclosure,
|
||||
useToast
|
||||
} from '@chakra-ui/react';
|
||||
import { loadStripe } from '@stripe/stripe-js';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useRouter } from 'next/router';
|
||||
import { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from 'react';
|
||||
import WechatPayment from '@/components/WechatPayment';
|
||||
import { createLicense } from '@/api/license';
|
||||
import usePaymentDataStore from '@/stores/payment';
|
||||
import useClusterDetail from '@/stores/cluster';
|
||||
import { findClusterById } from '@/api/cluster';
|
||||
|
||||
export default forwardRef(function RechargeComponent(props, ref) {
|
||||
const { clusterDetail, setClusterDetail } = useClusterDetail();
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
const toast = useToast({ position: 'top', duration: 2000 });
|
||||
const { t } = useTranslation();
|
||||
const [isAgree, setIsAgree] = useState(false);
|
||||
const [isInvalid, setIsInvalid] = useState(false);
|
||||
const { BonusBox, selectAmount } = useBonusBox();
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
// 整个流程跑通需要状态管理, 0 初始态, 1 创建支付单, 2 支付中, 3 支付成功
|
||||
const [complete, setComplete] = useState<0 | 1 | 2 | 3>(0);
|
||||
const [payType, setPayType] = useState<TPayMethod>('wechat');
|
||||
const [detail, setDetail] = useState(false);
|
||||
const [orderID, setOrderID] = useState('');
|
||||
const [wechatPaymentData, setWechatPaymentData] = useState<WechatPaymentData>();
|
||||
const { data: platformEnv } = useQuery(['getPlatformEnv'], getSystemEnv);
|
||||
// 用于避免微信支付,窗口关闭后感知不到的问题
|
||||
const { paymentData, setPaymentData, deletePaymentData, isExpired } = usePaymentDataStore();
|
||||
|
||||
const onClosePayment = useCallback(() => {
|
||||
setOrderID('');
|
||||
setComplete(0);
|
||||
setDetail(false);
|
||||
onClose();
|
||||
}, [onClose]);
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => {
|
||||
return {
|
||||
onOpen() {
|
||||
onOpen();
|
||||
}
|
||||
};
|
||||
},
|
||||
[onOpen]
|
||||
);
|
||||
|
||||
const handlePayConfirm = (type: TPayMethod) => {
|
||||
setPayType(type);
|
||||
if (!isAgree) {
|
||||
return toast({
|
||||
status: 'error',
|
||||
title: t('Please read and agree to the agreement'),
|
||||
isClosable: true,
|
||||
position: 'top'
|
||||
});
|
||||
}
|
||||
if (type === 'stripe' && selectAmount < 10) {
|
||||
return toast({
|
||||
status: 'error',
|
||||
title: t('Pay Minimum Tips')
|
||||
});
|
||||
}
|
||||
setDetail(true);
|
||||
setComplete(1);
|
||||
paymentMutation.mutate();
|
||||
};
|
||||
|
||||
const paymentMutation = useMutation(
|
||||
() =>
|
||||
createPayment({
|
||||
amount: deFormatMoney(selectAmount).toString(),
|
||||
payMethod: payType,
|
||||
currency: 'CNY',
|
||||
stripeSuccessCallBackUrl: `/cluster?clusterId=${clusterDetail?.clusterId}&tab=license&stripeState=success`,
|
||||
stripeErrorCallBackUrl: `/cluster?clusterId=${clusterDetail?.clusterId}&tab=license&stripeState=error`
|
||||
}),
|
||||
{
|
||||
async onSuccess(data) {
|
||||
if (payType === 'stripe' && platformEnv && data?.sessionID) {
|
||||
const stripe = await loadStripe(platformEnv?.stripePub);
|
||||
stripe?.redirectToCheckout({
|
||||
sessionId: data.sessionID
|
||||
});
|
||||
}
|
||||
if (payType === 'wechat' && data?.tradeNO && data?.codeURL) {
|
||||
setOrderID(data.orderID);
|
||||
setWechatPaymentData({ tradeNO: data?.tradeNO, codeURL: data?.codeURL });
|
||||
setComplete(2);
|
||||
setPaymentData(data.orderID);
|
||||
}
|
||||
},
|
||||
onError(err: any) {
|
||||
toast({
|
||||
status: 'error',
|
||||
title: err?.message || '',
|
||||
isClosable: true,
|
||||
position: 'top'
|
||||
});
|
||||
setComplete(0);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const licenseMutation = useMutation(
|
||||
({ orderID, clusterId }: { orderID: string; clusterId: string }) =>
|
||||
createLicense({ orderID, clusterId }),
|
||||
{
|
||||
onSuccess(data) {
|
||||
onClosePayment();
|
||||
toast({
|
||||
status: 'success',
|
||||
title: t('License issued successfully'),
|
||||
isClosable: true,
|
||||
position: 'top'
|
||||
});
|
||||
queryClient.invalidateQueries(['getLicenseByClusterId']);
|
||||
deletePaymentData();
|
||||
},
|
||||
onError(err: any) {
|
||||
toast({
|
||||
status: 'error',
|
||||
title: err?.message || '',
|
||||
isClosable: true,
|
||||
position: 'top'
|
||||
});
|
||||
setComplete(0);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
useQuery(['getPaymentResult', orderID], () => handlePaymentResult({ orderID }), {
|
||||
refetchInterval: complete === 2 ? 3 * 1000 : false,
|
||||
enabled: complete === 2 && !!orderID,
|
||||
cacheTime: 0,
|
||||
staleTime: 0,
|
||||
onSuccess(data) {
|
||||
console.log(data, clusterDetail, 1111111111111);
|
||||
if (!clusterDetail?.clusterId) {
|
||||
return toast({
|
||||
position: 'top',
|
||||
status: 'error',
|
||||
title: '请联系管理员'
|
||||
});
|
||||
}
|
||||
if (data.status === PaymentStatus.PaymentSuccess) {
|
||||
licenseMutation.mutate({ orderID: data.orderID, clusterId: clusterDetail.clusterId });
|
||||
uploadConvertData([90])
|
||||
.then((res) => {
|
||||
console.log(res);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
}
|
||||
},
|
||||
onError(err: any) {
|
||||
toast({
|
||||
status: 'error',
|
||||
title: err?.message || '',
|
||||
isClosable: true,
|
||||
position: 'top'
|
||||
});
|
||||
setComplete(0);
|
||||
}
|
||||
});
|
||||
|
||||
// checkWechatPay;
|
||||
useQuery(['checkWechatPay'], () => checkWechatPay('license'), {
|
||||
enabled: !isExpired() && !!paymentData?.orderId,
|
||||
onSuccess(data) {
|
||||
console.log(data, 'Handle wechat shutdown situation');
|
||||
if (data.status === PaymentStatus.PaymentSuccess) {
|
||||
if (!clusterDetail?.clusterId) {
|
||||
return toast({
|
||||
position: 'top',
|
||||
status: 'error',
|
||||
title: '请联系管理员'
|
||||
});
|
||||
}
|
||||
licenseMutation.mutate({ orderID: data.orderID, clusterId: clusterDetail.clusterId });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const getCluster = async (id: string) => {
|
||||
const cluster = await findClusterById({
|
||||
clusterId: id
|
||||
});
|
||||
setClusterDetail(cluster);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const { stripeState, orderID, clusterId, tab } = router.query;
|
||||
console.log(stripeState, orderID, clusterId);
|
||||
|
||||
const clearQuery = () => {
|
||||
router.replace({
|
||||
pathname: '/cluster',
|
||||
query: { tab }
|
||||
});
|
||||
};
|
||||
|
||||
if (stripeState === 'success') {
|
||||
clusterId ? getCluster(clusterId as string) : '';
|
||||
setComplete(2);
|
||||
setOrderID(orderID as string);
|
||||
setTimeout(clearQuery, 0);
|
||||
} else if (stripeState === 'error') {
|
||||
clusterId ? getCluster(clusterId as string) : '';
|
||||
toast({
|
||||
status: 'error',
|
||||
duration: 3000,
|
||||
title: t('Stripe Cancel'),
|
||||
isClosable: true,
|
||||
position: 'top'
|
||||
});
|
||||
onClosePayment();
|
||||
setTimeout(clearQuery, 0);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClosePayment}>
|
||||
<ModalOverlay />
|
||||
<ModalContent
|
||||
borderRadius={'12px'}
|
||||
maxW="502px"
|
||||
minH={'495px'}
|
||||
display={'flex'}
|
||||
flexDirection={'column'}
|
||||
>
|
||||
<ModalCloseButton />
|
||||
{detail ? (
|
||||
<>
|
||||
<ModalHeader
|
||||
cursor={'pointer'}
|
||||
pt={'24px'}
|
||||
py={'20px'}
|
||||
onClick={() => {
|
||||
setOrderID('');
|
||||
setComplete(0);
|
||||
setDetail(false);
|
||||
}}
|
||||
display={'flex'}
|
||||
alignItems={'center'}
|
||||
>
|
||||
<LeftIcon />
|
||||
<Text ml="8px" color={'#24282C'}>
|
||||
{t('Purchase License')}
|
||||
</Text>
|
||||
</ModalHeader>
|
||||
{complete === 1 ? (
|
||||
<Flex flex={1} alignItems={'center'} justifyContent={'center'}>
|
||||
<Spinner size={'xl'} />
|
||||
</Flex>
|
||||
) : (
|
||||
<WechatPayment
|
||||
complete={complete}
|
||||
codeURL={wechatPaymentData?.codeURL}
|
||||
tradeNO={wechatPaymentData?.tradeNO}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ModalHeader py={'20px'}>{t('Purchase License')}</ModalHeader>
|
||||
<Flex flex={1} flexDirection="column" alignItems="center" px={'24px'} pb="45px">
|
||||
<Text alignSelf={'start'} color="#7B838B" fontWeight={'normal'} mb={'16px'}>
|
||||
{t('Select Amount')}
|
||||
</Text>
|
||||
<BonusBox />
|
||||
<Flex alignSelf={'flex-start'} align={'center'} mt={'32px'}>
|
||||
<Checkbox
|
||||
isInvalid={isInvalid}
|
||||
mr={'8px'}
|
||||
isChecked={isAgree}
|
||||
variant={'unstyled'}
|
||||
onChange={(e) => {
|
||||
setIsInvalid(false);
|
||||
setIsAgree(e.target.checked);
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
fontStyle="normal"
|
||||
fontWeight="400"
|
||||
fontSize="12px"
|
||||
lineHeight="140%"
|
||||
color={'#1D8CDC'}
|
||||
>
|
||||
{t('agree policy')}
|
||||
<Link
|
||||
isExternal
|
||||
href={platformEnv?.service_protocol}
|
||||
_hover={{
|
||||
color: 'rgba(94, 189, 242, 1)',
|
||||
borderBottom: '1px solid rgba(94, 189, 242, 1)'
|
||||
}}
|
||||
px="4px"
|
||||
>
|
||||
{t('Service Agreement')}
|
||||
</Link>
|
||||
{t('and')}
|
||||
<Link
|
||||
isExternal
|
||||
href={platformEnv?.private_protocol}
|
||||
_hover={{
|
||||
color: 'rgba(94, 189, 242, 1)',
|
||||
borderBottom: '1px solid rgba(94, 189, 242, 1)'
|
||||
}}
|
||||
px="4px"
|
||||
>
|
||||
{t('Privacy Policy')}
|
||||
</Link>
|
||||
</Text>
|
||||
{/* <OuterLink text={t('View Discount Rules')}></OuterLink> */}
|
||||
</Flex>
|
||||
<Flex gap={'16px'} width={'full'} mt="20px">
|
||||
{platformEnv?.stripeEnabled && (
|
||||
<Button
|
||||
size="primary"
|
||||
variant="primary"
|
||||
mt="20px"
|
||||
w="full"
|
||||
h="auto"
|
||||
py="14px"
|
||||
px="34px"
|
||||
onClick={() => handlePayConfirm('stripe')}
|
||||
>
|
||||
<StripeIcon />
|
||||
<Text ml="12px">{t('pay with stripe')}</Text>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{platformEnv?.wechatEnabledRecharge && (
|
||||
<Button
|
||||
size="primary"
|
||||
variant="primary"
|
||||
mt="20px"
|
||||
w="full"
|
||||
h="auto"
|
||||
py="14px"
|
||||
px="34px"
|
||||
onClick={() => handlePayConfirm('wechat')}
|
||||
>
|
||||
<WechatIcon fill={'#33BABB'} />
|
||||
<Text ml="12px">{t('pay with wechat')}</Text>
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
});
|
||||
@@ -109,7 +109,7 @@ export default function Tutorial({ ossFileUrl, customBasePatch }: TutorialProps)
|
||||
}),
|
||||
{
|
||||
async onSuccess(data) {
|
||||
if (data?.orderID && data.type === ClusterType.Enterprise) {
|
||||
if (data?.orderID && data.type !== ClusterType.Standard) {
|
||||
const _link = await getFileByName(ossFileUrl);
|
||||
setOssLink(_link);
|
||||
}
|
||||
@@ -180,7 +180,7 @@ export default function Tutorial({ ossFileUrl, customBasePatch }: TutorialProps)
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
|
||||
{clusterDetail?.type === ClusterType.Enterprise && (
|
||||
{clusterDetail?.type !== ClusterType.Standard && (
|
||||
<AccordionItem
|
||||
bg="#fff"
|
||||
border={'1px solid #E8EBF0'}
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
import { EmptyIcon } from '@/components/Icon';
|
||||
import Layout from '@/components/Layout';
|
||||
import useClusterDetail from '@/stores/cluster';
|
||||
import { ClusterResult } from '@/types';
|
||||
import { ClusterDB } from '@/types';
|
||||
import { compareFirstLanguages } from '@/utils/tools';
|
||||
import {
|
||||
Button,
|
||||
@@ -35,7 +35,7 @@ import Tutorial, { TutorialProps } from './components/Tutorial';
|
||||
// Cluster recharge callback clusterId
|
||||
export default function MyCluster({ ossFileUrl, customBasePatch }: TutorialProps) {
|
||||
const { t } = useTranslation();
|
||||
const { clusterDetail, setClusterDetail, clearClusterDetail } = useClusterDetail();
|
||||
const { clusterDetail, setClusterDetail } = useClusterDetail();
|
||||
const [isLargerThanLG] = useMediaQuery(['(min-width: 992px)', '(display-mode: browser)']);
|
||||
const router = useRouter();
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
@@ -56,7 +56,7 @@ export default function MyCluster({ ossFileUrl, customBasePatch }: TutorialProps
|
||||
enabled: !clusterDetail?.clusterId && !router.query?.clusterId,
|
||||
onSuccess(data) {
|
||||
console.log(data);
|
||||
if (!clusterDetail?.clusterId && !router.query?.clusterIds) {
|
||||
if (!clusterDetail?.clusterId && !router.query?.clusterId) {
|
||||
console.log(data?.records?.[0], 'init cluster list');
|
||||
setClusterDetail(data?.records?.[0]);
|
||||
}
|
||||
@@ -100,7 +100,7 @@ export default function MyCluster({ ossFileUrl, customBasePatch }: TutorialProps
|
||||
}
|
||||
};
|
||||
|
||||
function findStandardRecordWithoutClusterId(records: ClusterResult[], type: string) {
|
||||
function findStandardRecordWithoutClusterId(records: ClusterDB[], type: string) {
|
||||
records.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
||||
for (const record of records) {
|
||||
if (record.type === type && !record?.kubeSystemID) {
|
||||
@@ -115,18 +115,18 @@ export default function MyCluster({ ossFileUrl, customBasePatch }: TutorialProps
|
||||
if (router.query?.systemId) {
|
||||
const systemId = router.query.systemId as string;
|
||||
isKubeSystemIDBound(systemId).then((res) => {
|
||||
console.log(res);
|
||||
if (!res.isBound) {
|
||||
onOpen();
|
||||
router.push({
|
||||
pathname: '/pricing',
|
||||
query: router.query
|
||||
});
|
||||
} else {
|
||||
findClusterBySystemId({ systemId }).then((res) => {
|
||||
console.log(res, 11);
|
||||
setClusterDetail(res);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
if (isSuccess && data?.total === 0) {
|
||||
|
||||
@@ -23,7 +23,7 @@ export default function History() {
|
||||
);
|
||||
|
||||
const downloadToken = (token: string) => {
|
||||
download('license.yaml', json2License({ token: token, type: 'Account' }));
|
||||
download('license.yaml', json2License({ token: token, type: 'Cluster' }));
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,98 +1,42 @@
|
||||
import { createCluster } from '@/api/cluster';
|
||||
import { checkWechatPay, createPayment, handlePaymentResult } from '@/api/payment';
|
||||
import { getSystemEnv, uploadConvertData } from '@/api/system';
|
||||
import { StripeIcon, SuccessIcon } from '@/components/Icon';
|
||||
import { company, contect, standard } from '@/constant/product';
|
||||
import { useConfirm } from '@/hooks/useConfirm';
|
||||
import usePaymentDataStore from '@/stores/payment';
|
||||
import useRouteParamsStore from '@/stores/routeParams';
|
||||
import useSessionStore from '@/stores/session';
|
||||
import {
|
||||
ClusterType,
|
||||
CreateClusterParams,
|
||||
PaymentStatus,
|
||||
TPayMethod,
|
||||
WechatPaymentData
|
||||
} from '@/types';
|
||||
import {
|
||||
AbsoluteCenter,
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
Divider,
|
||||
Flex,
|
||||
Modal,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
Spinner,
|
||||
Text,
|
||||
useDisclosure,
|
||||
useToast
|
||||
} from '@chakra-ui/react';
|
||||
import { loadStripe } from '@stripe/stripe-js';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useRouter } from 'next/router';
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import BillingMeter from '@/components/billing-meter';
|
||||
import RechargeComponent from '@/components/recharge';
|
||||
import { company, contect, defaulClustertForm, freeClusterForm } from '@/constant/product';
|
||||
import { ClusterFormType, ClusterType } from '@/types';
|
||||
import { calculatePrice } from '@/utils/tools';
|
||||
import { Button, Flex, Text } from '@chakra-ui/react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import ServicePackage from './ServicePackage';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
export default function Product() {
|
||||
const { t } = useTranslation();
|
||||
const queryClient = useQueryClient();
|
||||
const [clusterType, setClusterType] = useState<ClusterType>(ClusterType.ScaledStandard);
|
||||
const [price, setPrice] = useState(0);
|
||||
const rechargeRef = useRef<{ onOpen: (form?: ClusterFormType) => void; isOpen: boolean }>();
|
||||
const router = useRouter();
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const { openConfirm, ConfirmChild } = useConfirm();
|
||||
const toast = useToast({ position: 'top', duration: 2000 });
|
||||
// 整个流程跑通需要状态管理, 0 初始态, 1 创建支付单, 2 支付中, 3 支付成功
|
||||
const [complete, setComplete] = useState<0 | 1 | 2 | 3>(0);
|
||||
const [payType, setPayType] = useState<TPayMethod>('wechat');
|
||||
const [clusterType, setClusterType] = useState<ClusterType>(ClusterType.Standard);
|
||||
const [orderID, setOrderID] = useState('');
|
||||
const [wechatData, setWechatData] = useState<WechatPaymentData>();
|
||||
const { data: platformEnv } = useQuery(['getPlatformEnv'], getSystemEnv);
|
||||
const [remainingSeconds, setRemainingSeconds] = useState(1); // 初始值为2秒
|
||||
const { data: routeParams, setRouteParams, clearRouteParams } = useRouteParamsStore();
|
||||
const { isUserLogin } = useSessionStore();
|
||||
// Used to detect missing WeChat payment results
|
||||
const { paymentData, setPaymentData, deletePaymentData, isExpired } = usePaymentDataStore();
|
||||
|
||||
const onClosePayment = useCallback(() => {
|
||||
setOrderID('');
|
||||
setComplete(0);
|
||||
onClose();
|
||||
}, [onClose]);
|
||||
const formHook = useForm<ClusterFormType>({
|
||||
defaultValues: defaulClustertForm
|
||||
});
|
||||
|
||||
const openPayModal = () => {
|
||||
setComplete(1);
|
||||
setPayType('wechat');
|
||||
paymentMutation.mutate((599 * 100).toString());
|
||||
onOpen();
|
||||
};
|
||||
formHook.watch((data) => {
|
||||
if (!data) return;
|
||||
const price = calculatePrice(data as ClusterFormType, freeClusterForm);
|
||||
setPrice(price);
|
||||
});
|
||||
|
||||
const handleStripePay = () => {
|
||||
setComplete(1);
|
||||
setPayType('stripe');
|
||||
paymentMutation.mutate((599 * 100).toString());
|
||||
};
|
||||
useEffect(() => {
|
||||
const price = calculatePrice(defaulClustertForm, freeClusterForm);
|
||||
setPrice(price);
|
||||
}, []);
|
||||
|
||||
const handleProductByType = (type: ClusterType) => {
|
||||
setClusterType(type);
|
||||
if (type === ClusterType.Standard) {
|
||||
openConfirm(
|
||||
() => {
|
||||
clusterMutation.mutate({ type: ClusterType.Standard });
|
||||
},
|
||||
() => {
|
||||
router.push('/cluster');
|
||||
}
|
||||
)();
|
||||
}
|
||||
if (type === ClusterType.Enterprise) {
|
||||
openPayModal();
|
||||
if (type === ClusterType.ScaledStandard) {
|
||||
rechargeRef.current?.onOpen();
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === ClusterType.Contact) {
|
||||
window.open(
|
||||
'https://fael3z0zfze.feishu.cn/share/base/form/shrcnesSfEK65JZaAf2W6Fwz6Ad',
|
||||
@@ -101,167 +45,21 @@ export default function Product() {
|
||||
}
|
||||
};
|
||||
|
||||
const paymentMutation = useMutation(
|
||||
(amount: string) =>
|
||||
createPayment({
|
||||
amount: amount,
|
||||
payMethod: payType,
|
||||
currency: 'CNY',
|
||||
stripeSuccessCallBackUrl: '/pricing?stripeState=success',
|
||||
stripeErrorCallBackUrl: '/pricing?stripeState=error'
|
||||
}),
|
||||
{
|
||||
async onSuccess(data) {
|
||||
if (payType === 'stripe' && platformEnv && data?.sessionID) {
|
||||
const stripe = await loadStripe(platformEnv?.stripePub);
|
||||
stripe?.redirectToCheckout({
|
||||
sessionId: data.sessionID
|
||||
});
|
||||
}
|
||||
if (payType === 'wechat' && data?.tradeNO && data?.codeURL) {
|
||||
setOrderID(data.orderID);
|
||||
setWechatData({ tradeNO: data?.tradeNO, codeURL: data?.codeURL });
|
||||
setComplete(2);
|
||||
setPaymentData(data.orderID);
|
||||
}
|
||||
},
|
||||
onError(err: any) {
|
||||
toast({
|
||||
status: 'error',
|
||||
title: err?.message || '',
|
||||
isClosable: true,
|
||||
position: 'top'
|
||||
});
|
||||
setComplete(0);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Create a standard cluster
|
||||
const clusterMutation = useMutation((payload: CreateClusterParams) => createCluster(payload), {
|
||||
onSuccess(data) {
|
||||
console.log(data, 'clusterMutation');
|
||||
setComplete(3);
|
||||
queryClient.invalidateQueries(['getClusterList']);
|
||||
deletePaymentData();
|
||||
},
|
||||
onError(err: any) {
|
||||
toast({
|
||||
status: 'error',
|
||||
title: err?.message || '',
|
||||
isClosable: true,
|
||||
position: 'top'
|
||||
});
|
||||
setComplete(0);
|
||||
}
|
||||
});
|
||||
|
||||
// Purchase Enterprise Edition Cluster
|
||||
useQuery(['getPaymentResult', orderID], () => handlePaymentResult({ orderID }), {
|
||||
refetchInterval: complete === 2 ? 3 * 1000 : false,
|
||||
enabled: complete === 2 && !!orderID,
|
||||
cacheTime: 0,
|
||||
staleTime: 0,
|
||||
onSuccess(data) {
|
||||
if (data.status === PaymentStatus.PaymentSuccess) {
|
||||
clusterMutation.mutate({ orderID: data.orderID, type: ClusterType.Enterprise });
|
||||
uploadConvertData([90])
|
||||
.then((res) => {
|
||||
console.log(res);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
}
|
||||
},
|
||||
onError(err: any) {
|
||||
toast({
|
||||
status: 'error',
|
||||
title: err?.message || '',
|
||||
isClosable: true,
|
||||
position: 'top'
|
||||
});
|
||||
setComplete(0);
|
||||
}
|
||||
});
|
||||
|
||||
// checkWechatPay
|
||||
useQuery(['checkWechatPay'], () => checkWechatPay('cluster'), {
|
||||
enabled: !isExpired() && !!paymentData?.orderId,
|
||||
onSuccess(data) {
|
||||
console.log(data, 'Handle wechat shutdown situation');
|
||||
if (data.status === PaymentStatus.PaymentSuccess) {
|
||||
clusterMutation.mutate({ orderID: data.orderID, type: ClusterType.Enterprise });
|
||||
}
|
||||
},
|
||||
onError(err) {
|
||||
console.log(err);
|
||||
}
|
||||
});
|
||||
|
||||
// handle stripe
|
||||
useEffect(() => {
|
||||
const { stripeState, orderID } = router.query;
|
||||
const clearQuery = () => {
|
||||
router.replace({
|
||||
pathname: '/pricing',
|
||||
query: null
|
||||
});
|
||||
};
|
||||
if (stripeState === 'success') {
|
||||
toast({
|
||||
status: 'success',
|
||||
title: t('Checking Payment Results'), // 这里改为license 签发成功
|
||||
isClosable: true,
|
||||
duration: 3500,
|
||||
position: 'top'
|
||||
});
|
||||
setClusterType(ClusterType.Enterprise);
|
||||
setComplete(2);
|
||||
setOrderID(orderID as string);
|
||||
setTimeout(clearQuery, 0);
|
||||
} else if (stripeState === 'error') {
|
||||
toast({
|
||||
status: 'error',
|
||||
duration: 3000,
|
||||
title: t('Stripe Cancel'),
|
||||
isClosable: true,
|
||||
position: 'top'
|
||||
});
|
||||
onClosePayment();
|
||||
setTimeout(clearQuery, 0);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// handle success
|
||||
useEffect(() => {
|
||||
if (complete === 3) {
|
||||
onOpen();
|
||||
const timer = setInterval(() => {
|
||||
if (remainingSeconds > 0) {
|
||||
setRemainingSeconds(remainingSeconds - 1);
|
||||
} else {
|
||||
clearInterval(timer);
|
||||
router.push('/cluster');
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
return () => {
|
||||
clearInterval(timer);
|
||||
if (router.query?.systemId) {
|
||||
const { systemId, nodeCount, totalCpu, totalMemory } = router.query as unknown as {
|
||||
systemId: string;
|
||||
nodeCount: number;
|
||||
totalCpu: number;
|
||||
totalMemory: number;
|
||||
};
|
||||
rechargeRef.current?.onOpen({
|
||||
cpu: totalCpu,
|
||||
memory: totalMemory,
|
||||
months: '3',
|
||||
systemId: systemId
|
||||
});
|
||||
}
|
||||
}, [complete, onOpen, remainingSeconds, router]);
|
||||
|
||||
// handle Jump link
|
||||
useEffect(() => {
|
||||
const { clusterType, external } = routeParams;
|
||||
console.log(clusterType, external, 'pricing');
|
||||
if (clusterType && external) {
|
||||
handleProductByType(routeParams.clusterType as ClusterType);
|
||||
clearRouteParams();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
@@ -278,88 +76,35 @@ export default function Product() {
|
||||
gap={'36px'}
|
||||
px="24px"
|
||||
>
|
||||
<ServicePackage items={standard}>
|
||||
<Text color="#00A9A6" fontSize="18px" fontWeight="600">
|
||||
<ServicePackage items={company}>
|
||||
<Text color="#0884DD" fontSize="18px" fontWeight="600">
|
||||
标准版
|
||||
</Text>
|
||||
<Flex alignItems={'center'} mt="16px">
|
||||
<Text color="#000" fontSize="40px" fontWeight="600">
|
||||
¥0
|
||||
</Text>
|
||||
<Center
|
||||
h="34px"
|
||||
px="16px"
|
||||
ml="24px"
|
||||
borderRadius={'15px 15px 15px 0px'}
|
||||
bgColor={'#EBF7FD'}
|
||||
color={'#00A9A6'}
|
||||
fontWeight={500}
|
||||
fontSize={'20px'}
|
||||
>
|
||||
赠 ¥299
|
||||
</Center>
|
||||
</Flex>
|
||||
<Text mt="16px" color={'#24282C'} fontSize={'18px'} fontWeight={600}>
|
||||
适合开发者测试,或 POC demo
|
||||
</Text>
|
||||
<Button
|
||||
w="100%"
|
||||
mt="28px"
|
||||
bgColor={'#F4F6F8'}
|
||||
fontSize={'16px'}
|
||||
color={'#24282C'}
|
||||
fontWeight={600}
|
||||
onClick={() => handleProductByType(ClusterType.Standard)}
|
||||
>
|
||||
一键安装
|
||||
</Button>
|
||||
</ServicePackage>
|
||||
<ServicePackage items={company}>
|
||||
<Text color="#36ADEF" fontSize="18px" fontWeight="600">
|
||||
企业版
|
||||
</Text>
|
||||
<Flex alignItems={'center'} mt="16px">
|
||||
<Text color="#000" fontSize="40px" fontWeight="600">
|
||||
¥599
|
||||
</Text>
|
||||
<Center
|
||||
h="34px"
|
||||
px="16px"
|
||||
ml="24px"
|
||||
borderRadius={'15px 15px 15px 0px'}
|
||||
bgColor={'#EBF7FD'}
|
||||
color={'#36ADEF'}
|
||||
fontWeight={500}
|
||||
fontSize={'20px'}
|
||||
>
|
||||
赠 ¥599
|
||||
</Center>
|
||||
</Flex>
|
||||
<Text mt="16px" color={'#24282C'} fontSize={'18px'} fontWeight={600}>
|
||||
适合企业生产环境
|
||||
<Text mt="24px" color={'#24282C'} fontSize={'24px'} fontWeight={600}>
|
||||
适合开发者测试, POC demo,企业生产环境
|
||||
</Text>
|
||||
<Button
|
||||
w="100%"
|
||||
mt="28px"
|
||||
bgColor={'#AFDEF9'}
|
||||
fontSize={'16px'}
|
||||
fontSize={'14px'}
|
||||
color={'#24282C'}
|
||||
fontWeight={600}
|
||||
onClick={() => handleProductByType(ClusterType.Enterprise)}
|
||||
fontWeight={500}
|
||||
onClick={() => handleProductByType(ClusterType.ScaledStandard)}
|
||||
>
|
||||
购买
|
||||
获取
|
||||
</Button>
|
||||
</ServicePackage>
|
||||
<ServicePackage items={contect}>
|
||||
<Text color="#00A9A6" fontSize="18px" fontWeight="600">
|
||||
定制版
|
||||
</Text>
|
||||
<Text mt="32px" color={'#24282C'} fontSize={'24px'} fontWeight={600} w="200px">
|
||||
<Text mt="24px" color={'#24282C'} fontSize={'24px'} fontWeight={600} w="200px">
|
||||
适合大规模集群与大型企业客户
|
||||
</Text>
|
||||
<Button
|
||||
w="100%"
|
||||
mt="42px"
|
||||
mt="32px"
|
||||
bgColor={'#F4F6F8'}
|
||||
fontSize={'16px'}
|
||||
color={'#24282C'}
|
||||
@@ -369,96 +114,9 @@ export default function Product() {
|
||||
联系我们
|
||||
</Button>
|
||||
</ServicePackage>
|
||||
<BillingMeter formHook={formHook} price={price} />
|
||||
</Flex>
|
||||
<Modal isOpen={isOpen} onClose={onClosePayment} closeOnOverlayClick={false}>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader></ModalHeader>
|
||||
{complete !== 3 && <ModalCloseButton />}
|
||||
{complete === 3 ? (
|
||||
<Flex
|
||||
flexDirection={'column'}
|
||||
px="37px"
|
||||
pt="100px"
|
||||
pb="200px"
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
>
|
||||
<SuccessIcon />
|
||||
<Text mt="24px" color={'#24282C'} fontSize={'18px'} fontWeight={600}>
|
||||
{t('Cluster Pay Success', {
|
||||
clusterType: t(clusterType)
|
||||
})}
|
||||
</Text>
|
||||
<Text color={'#5A646E'} fontSize={'12px'} fontWeight={500} mt="16px">
|
||||
{remainingSeconds} 后跳转至集群部署页
|
||||
</Text>
|
||||
</Flex>
|
||||
) : (
|
||||
<Flex
|
||||
flexDirection="column"
|
||||
px="37px"
|
||||
py="30px"
|
||||
justify={'center'}
|
||||
align={'center'}
|
||||
justifyContent={'center'}
|
||||
alignItems={'center'}
|
||||
position={'relative'}
|
||||
>
|
||||
{payType === 'wechat' ? (
|
||||
<>
|
||||
<Text fontSize={'20px'} fontWeight={500} color="#24282C" textAlign="center">
|
||||
{t('Scan with WeChat')}
|
||||
</Text>
|
||||
<Box my="32px">
|
||||
{complete === 2 && !!wechatData?.codeURL ? (
|
||||
<QRCodeSVG
|
||||
size={185}
|
||||
value={wechatData?.codeURL}
|
||||
style={{ margin: '0 auto' }}
|
||||
imageSettings={{
|
||||
// 二维码中间的logo图片
|
||||
src: 'images/pay_wechat.svg',
|
||||
height: 40,
|
||||
width: 40,
|
||||
excavate: true // 中间图片所在的位置是否镂空
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Spinner />
|
||||
)}
|
||||
</Box>
|
||||
<Text color="#717D8A" fontSize="12px" fontWeight="normal">
|
||||
{t('Order Number')}: {wechatData?.tradeNO || ''}
|
||||
</Text>
|
||||
</>
|
||||
) : (
|
||||
<Spinner />
|
||||
)}
|
||||
|
||||
<Box mt="36px" position="relative" w="100%">
|
||||
<Divider bg={'#E9EEF5'} />
|
||||
<AbsoluteCenter bg="white" px="4" color={'#7B838B'} fontSize={'12px'}>
|
||||
其他方式支付
|
||||
</AbsoluteCenter>
|
||||
</Box>
|
||||
<Button
|
||||
mt="26px"
|
||||
size="primary"
|
||||
variant="primary"
|
||||
w="218px"
|
||||
h="44px"
|
||||
onClick={handleStripePay}
|
||||
backgroundColor={'#24282C'}
|
||||
>
|
||||
<StripeIcon />
|
||||
<Text ml="12px">{t('pay with stripe')}</Text>
|
||||
</Button>
|
||||
</Flex>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<ConfirmChild />
|
||||
<RechargeComponent ref={rechargeRef} isLicensePay={false} key={'cluster'} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ export default function ServicePackage({ items, children }: ServicePackageProps)
|
||||
px="48px"
|
||||
pt="48px"
|
||||
flexDirection={'column'}
|
||||
maxW="400px"
|
||||
w="400px"
|
||||
minH={'676px'}
|
||||
>
|
||||
{children}
|
||||
@@ -28,7 +28,7 @@ export default function ServicePackage({ items, children }: ServicePackageProps)
|
||||
<Text
|
||||
fontSize={'14px'}
|
||||
fontWeight={500}
|
||||
color={item.icon === 'check' ? '#485058' : 'brightBlue.500'}
|
||||
color={item.icon === 'check' ? '#485058' : '#0884DD'}
|
||||
ml="15px"
|
||||
>
|
||||
{item.label}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import SigninComponent from '@/components/Signin';
|
||||
import SigninComponent from '@/components/login-module';
|
||||
import useRouteParamsStore from '@/stores/routeParams';
|
||||
import useSessionStore from '@/stores/session';
|
||||
import { ClusterType } from '@/types';
|
||||
@@ -13,15 +13,9 @@ export default function SigninPage() {
|
||||
const { isUserLogin } = useSessionStore();
|
||||
|
||||
useEffect(() => {
|
||||
const { clusterType, external } = router.query;
|
||||
console.log(clusterType, external, '--------');
|
||||
if (external && clusterType) {
|
||||
setRouteParams(external as string, clusterType as ClusterType);
|
||||
}
|
||||
if (isUserLogin()) {
|
||||
router.push('/pricing');
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// handle baidu id
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { ClusterDB, ClusterRecordPayload, ClusterType, LicenseRecordPayload } from '@/types';
|
||||
import { ClusterDB, ClusterRecordPayload, LicenseRecordPayload } from '@/types';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { connectToDatabase } from './mongodb';
|
||||
import { getOssUrl } from './oss';
|
||||
import { createLicenseRecord } from './license';
|
||||
import { ObjectId } from 'mongodb';
|
||||
|
||||
@@ -15,16 +14,12 @@ async function connectClusterCollection() {
|
||||
|
||||
export async function createClusterRecord(payload: ClusterRecordPayload) {
|
||||
const collection = await connectClusterCollection();
|
||||
const now = new Date();
|
||||
const expirationTime = new Date(now.getTime() + 30 * 60000); // 30 minutes in milliseconds
|
||||
const tarUrl = await getOssUrl({ fileName: 'sealos-cloud-dev.tar.gz' });
|
||||
const md5Url = await getOssUrl({ fileName: 'sealos-cloud-dev.tar.gz.md5' });
|
||||
// const now = new Date();
|
||||
// const expirationTime = new Date(now.getTime() + 30 * 60000); // 30 minutes in milliseconds
|
||||
// const tarUrl = await getOssUrl({ fileName: 'sealos-cloud-dev.tar.gz' });
|
||||
// const md5Url = await getOssUrl({ fileName: 'sealos-cloud-dev.tar.gz.md5' });
|
||||
|
||||
let orderID;
|
||||
|
||||
if (payload.type === ClusterType.Enterprise) {
|
||||
orderID = payload.orderID;
|
||||
}
|
||||
let orderID = payload.orderID;
|
||||
|
||||
const record: ClusterDB = {
|
||||
clusterId: uuid(),
|
||||
@@ -32,11 +27,15 @@ export async function createClusterRecord(payload: ClusterRecordPayload) {
|
||||
orderID: orderID, // order ID
|
||||
type: payload.type,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
updatedAt: new Date(),
|
||||
cpu: payload.cpu,
|
||||
memory: payload.memory,
|
||||
months: payload.months,
|
||||
displayName: payload.name
|
||||
};
|
||||
|
||||
const result = await collection.insertOne(record);
|
||||
return result;
|
||||
await collection.insertOne(record);
|
||||
return record;
|
||||
}
|
||||
|
||||
export async function getClusterRecordsByUid({
|
||||
|
||||
@@ -3,7 +3,7 @@ import { base64Decode } from '@/utils/tools';
|
||||
import { sign } from 'jsonwebtoken';
|
||||
import { connectToDatabase } from './mongodb';
|
||||
|
||||
const ExpiredTime = 30 * 24 * 60 * 60;
|
||||
export const ExpiredTime = 30 * 24 * 60 * 60;
|
||||
|
||||
async function connectLicenseCollection() {
|
||||
const client = await connectToDatabase();
|
||||
@@ -19,7 +19,11 @@ export async function createLicenseRecord({
|
||||
quota,
|
||||
payMethod,
|
||||
type,
|
||||
clusterId
|
||||
clusterId,
|
||||
cpu,
|
||||
memory,
|
||||
months,
|
||||
expiredTime = ExpiredTime
|
||||
}: LicenseRecordPayload) {
|
||||
const collection = await connectLicenseCollection();
|
||||
|
||||
@@ -34,12 +38,15 @@ export async function createLicenseRecord({
|
||||
quota: quota
|
||||
},
|
||||
iat: now, // Store the current timestamp as iat
|
||||
exp: now + ExpiredTime, // Set expiration to one day from now (in seconds)
|
||||
exp: now + expiredTime, // Set expiration to one day from now (in seconds)
|
||||
amount: amount,
|
||||
type: type,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
clusterId: clusterId
|
||||
clusterId: clusterId,
|
||||
cpu,
|
||||
memory,
|
||||
months
|
||||
};
|
||||
|
||||
const result = await collection.insertOne(record);
|
||||
@@ -99,7 +106,7 @@ export async function getLicenseRecordsByUid({
|
||||
return result;
|
||||
}
|
||||
|
||||
export function generateLicenseToken(payload: LicenseToken, time = ExpiredTime) {
|
||||
export function generateLicenseToken(payload: LicenseToken, time: number) {
|
||||
const privateKey = process.env.LICENSE_PRIVATE_KEY;
|
||||
if (!privateKey) {
|
||||
throw new Error('LICENSE PRIVATE KEY IS MISSING');
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { ClusterResult } from '@/types';
|
||||
import { ClusterDB } from '@/types';
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import { immer } from 'zustand/middleware/immer';
|
||||
|
||||
type clusterDetailState = {
|
||||
clusterDetail: ClusterResult | undefined;
|
||||
setClusterDetail: (data: ClusterResult) => void;
|
||||
clusterDetail: ClusterDB | undefined;
|
||||
setClusterDetail: (data: ClusterDB) => void;
|
||||
clearClusterDetail: () => void;
|
||||
};
|
||||
|
||||
|
||||
@@ -6,8 +6,22 @@ type PaymentDataState = {
|
||||
paymentData?: {
|
||||
orderId: string;
|
||||
expirationTime: number;
|
||||
cpu: number;
|
||||
memory: number;
|
||||
months: string;
|
||||
clusterId?: string;
|
||||
name?: string;
|
||||
systemId?: string;
|
||||
};
|
||||
setPaymentData: (orderId: string) => void;
|
||||
setPaymentData: (data: {
|
||||
orderId: string;
|
||||
cpu: number;
|
||||
memory: number;
|
||||
months: string;
|
||||
clusterId?: string;
|
||||
name?: string;
|
||||
systemId?: string;
|
||||
}) => void;
|
||||
deletePaymentData: () => void;
|
||||
isExpired: () => boolean;
|
||||
};
|
||||
@@ -16,12 +30,12 @@ export const usePaymentDataStore = create(
|
||||
persist(
|
||||
immer<PaymentDataState>((set, get) => ({
|
||||
paymentData: undefined,
|
||||
setPaymentData: (id) => {
|
||||
const currentTime = new Date().getTime(); // 获取当前时间戳
|
||||
const expirationTime = currentTime + 5 * 60000; // 10 minutes in milliseconds
|
||||
setPaymentData: (data) => {
|
||||
const currentTime = new Date().getTime();
|
||||
const expirationTime = currentTime + 5 * 60000;
|
||||
set({
|
||||
paymentData: {
|
||||
orderId: id,
|
||||
...data,
|
||||
expirationTime: expirationTime
|
||||
}
|
||||
});
|
||||
|
||||
@@ -22,11 +22,11 @@ const Button = defineStyleConfig({
|
||||
},
|
||||
variants: {
|
||||
primary: {
|
||||
bg: '#3E3B3B',
|
||||
bg: '#111824',
|
||||
_hover: {
|
||||
bg: '#3E3B3B',
|
||||
bg: '#111824',
|
||||
_disabled: {
|
||||
bg: '#3E3B3B'
|
||||
bg: '#111824'
|
||||
}
|
||||
},
|
||||
color: '#FEFEFE'
|
||||
@@ -148,6 +148,13 @@ const Switch = SwitchHelper.defineMultiStyleConfig({
|
||||
export const theme = extendTheme({
|
||||
initialColorMode: 'light',
|
||||
colors: ThemeColors,
|
||||
styles: {
|
||||
global: {
|
||||
'html, body': {
|
||||
fontFamily: 'PingFang SC'
|
||||
}
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Button,
|
||||
Input,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ObjectId } from 'mongodb';
|
||||
export enum ClusterType {
|
||||
Standard = 'Standard',
|
||||
Enterprise = 'Enterprise',
|
||||
ScaledStandard = 'ScaledStandard',
|
||||
Contact = 'Contact'
|
||||
}
|
||||
|
||||
@@ -20,33 +21,22 @@ export type ClusterDB = {
|
||||
kubeSystemID?: string; // bind kube-system id
|
||||
kubeSystemUpdateAt?: Date;
|
||||
isDeleted?: boolean;
|
||||
// v1.1
|
||||
cpu: number;
|
||||
memory: number;
|
||||
months: string;
|
||||
};
|
||||
|
||||
export type ClusterRecordPayload = {
|
||||
uid: string; // user ID
|
||||
orderID?: string; // order ID
|
||||
type: ClusterType; // cluster type
|
||||
};
|
||||
} & ClusterFormType;
|
||||
|
||||
export type CreateClusterParams = {
|
||||
orderID?: string; // order ID
|
||||
type: ClusterType; // license type
|
||||
};
|
||||
|
||||
export type ClusterResult = {
|
||||
clusterId: string; // cluster ID 唯一
|
||||
uid: string; // user ID 唯一
|
||||
orderID?: string; // order ID 唯一
|
||||
licenseID?: ObjectId; // license ID
|
||||
type: ClusterType; // license type
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
// v1 new
|
||||
displayName?: string;
|
||||
kubeSystemID?: string;
|
||||
kubeSystemUpdateAt?: Date;
|
||||
isDeleted?: boolean;
|
||||
};
|
||||
} & ClusterFormType;
|
||||
|
||||
export type CommandFormType = {
|
||||
cloudVersion: string;
|
||||
@@ -72,3 +62,11 @@ export type CommandFormType = {
|
||||
certPath: string;
|
||||
certKeyPath: string;
|
||||
};
|
||||
|
||||
export type ClusterFormType = {
|
||||
cpu: number;
|
||||
memory: number;
|
||||
months: string;
|
||||
name?: string;
|
||||
systemId?: string;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { TPayMethod } from './payment';
|
||||
import { ClusterFormType } from './cluster';
|
||||
|
||||
export type LicenseDB = {
|
||||
_id?: ObjectId; // 唯一
|
||||
@@ -18,6 +19,10 @@ export type LicenseDB = {
|
||||
updatedAt: Date; // Modification timestamp
|
||||
// v1
|
||||
clusterId: string; // Bind to cluster id
|
||||
// v1.1
|
||||
cpu?: number;
|
||||
memory?: number;
|
||||
months?: string;
|
||||
};
|
||||
|
||||
export type LicenseRecordPayload = {
|
||||
@@ -26,10 +31,11 @@ export type LicenseRecordPayload = {
|
||||
orderID: string; // order number
|
||||
payMethod: TPayMethod;
|
||||
quota: number; // 额度
|
||||
amount: number; // 重置金额
|
||||
amount: number; // 金额
|
||||
type: LicenseType;
|
||||
clusterId: string;
|
||||
};
|
||||
expiredTime: number;
|
||||
} & ClusterFormType;
|
||||
|
||||
// new
|
||||
export type LicenseToken = {
|
||||
@@ -38,13 +44,15 @@ export type LicenseToken = {
|
||||
// ClusterDB kubeSystemID
|
||||
clusterID: string;
|
||||
data: {
|
||||
amount: number;
|
||||
nodeCount: number;
|
||||
totalCPU: number;
|
||||
totalMemory: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type LicenseType = 'Account' | 'Cluster';
|
||||
export type LicenseType = 'Cluster'; // 'Account';
|
||||
|
||||
export type CreateLicenseParams = {
|
||||
orderID: string;
|
||||
orderID?: string;
|
||||
clusterId: string;
|
||||
};
|
||||
} & ClusterFormType;
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
import { cpuPriceMonth, memoryPriceMonth } from '@/constant/product';
|
||||
import { ClusterFormType } from '@/types';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
export const calculatePrice = (form: ClusterFormType, freeValues: ClusterFormType): number => {
|
||||
const { cpu, memory, months } = form;
|
||||
|
||||
const cpuTotalPrice = cpuPriceMonth * cpu;
|
||||
const memoryTotalPrice = memoryPriceMonth * memory;
|
||||
|
||||
const totalPrice = (cpuTotalPrice + memoryTotalPrice) * parseInt(months);
|
||||
|
||||
if (cpu <= freeValues.cpu && memory <= freeValues.memory && months === freeValues.months) {
|
||||
return 0;
|
||||
} else {
|
||||
return totalPrice;
|
||||
}
|
||||
};
|
||||
|
||||
export const formatTime = (time: string | number | Date, format = 'YYYY-MM-DD HH:mm:ss') => {
|
||||
return dayjs(time).format(format);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user