Skip to content

Commit

Permalink
feat: launchpad multiple port (#3871)
Browse files Browse the repository at this point in the history
* feat: launchpad multiple port

* perf: ui
  • Loading branch information
c121914yu committed Sep 8, 2023
1 parent 4f06cba commit 289994d
Show file tree
Hide file tree
Showing 29 changed files with 9,178 additions and 6,475 deletions.
14,426 changes: 8,328 additions & 6,098 deletions frontend/pnpm-lock.yaml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions frontend/providers/applaunchpad/package.json
Expand Up @@ -22,6 +22,7 @@
"ansi_up": "^5.2.1",
"axios": "^1.4.0",
"dayjs": "^1.11.9",
"dns": "^0.2.2",
"echarts": "^5.4.3",
"fast-json-patch": "^3.1.1",
"framer-motion": "^9.1.7",
Expand Down
Expand Up @@ -106,7 +106,7 @@
"Protocol": "Protocol",
"Export Domain": " Domain",
"Custom Domain": "Custom Domain",
"Please CNAME your custom domain to": "Please CNAME your custom domain to",
"CNAME Tips": "Please go to your domain name service provider, add the domain name 'CNAME' resolution to {{domain}}, after the resolution takes effect, you can bind your own domain name.",
"Option": "Option",
"Run command": "Command",
"Command parameters": "Parameters",
Expand Down
17 changes: 14 additions & 3 deletions frontend/providers/applaunchpad/public/locales/zh/common.json
Expand Up @@ -3,6 +3,7 @@
"memory": "内存",
"storage": "存储卷",
"gpu": "GPU",
"Port": "端口",
"Applications": "应用列表",
"Create Application": "新建应用",
"Cancel": "取消",
Expand Down Expand Up @@ -33,7 +34,7 @@
"Command": "启动命令",
"Parameters": "运行参数",
"Not Configured": "未配置",
"Open Public Access": "可外网访问",
"Open Public Access": "外网访问",
"Configuration File": "配置文件",
"Storage": "存储卷",
"Environment Variables": "环境变量",
Expand Down Expand Up @@ -70,7 +71,7 @@
"jump_message": "该应用不允许单独使用,点击确认前往 Sealos Desktop 使用。",
"pause_message": "请注意,暂停状态下无法变更应用,并且如果您使用了存储卷,存储券仍会收费,请确认!",
"Confirm to restart this application?": "确认重启该应用?",
"You haven't created any application yet.": "您还没有新建应用",
"You haven't created any application yet.": "您还没有新建应用",
"Confirm deletion": "确认删除",
"Deletion warning": "删除警告",
"Are you sure you want to delete this application? If you proceed, all data for this project will be deleted.": "如果确认要删除这个应用吗?如果执行此操作,将删除该项目的所有数据。",
Expand Down Expand Up @@ -107,7 +108,11 @@
"Protocol": "协议",
"Export Domain": "出口域名",
"Custom Domain": "自定义域名",
"Please CNAME your custom domain to": "请将您的自定义域名 cname 到",
"CNAME Tips": "请到您的域名服务商处,添加该域名的 `CNAME` 解析到 {{domain}},解析生效后即可绑定自定义域名。",
"Custom Domain Error": "添加自定义域名失败,请检查是否已经 CName。",
"Copy Domain Success": "复制域名成功",
"Public Access": "外网访问",
"Network port conflict": "容器端口重复",
"Option": "选填",
"Run command": "运行命令",
"Command parameters": "命令参数",
Expand Down Expand Up @@ -175,6 +180,12 @@
"The minimum number of instances is 1": "实例数最小为1",
"The maximum number of instances is 20": "实例数最大为20",
"Amount": "数量",
"Input your custom domain": "输入你的域名",
"Cname auth error: customDomain's cname is not equal to publicDomain": "CName 校验错误,你需要先 CName 到指定域名才能绑定",
"If no, the default command is used": "不填则为默认命令",
"Add Port": "添加端口",
"Copy": "复制",
"Open Link": "打开链接",
"app": {
"Resource Quota": "资源配额",
"The applied CPU exceeds the quota": "申请的 CPU 超出限制,请联系管理员",
Expand Down
4 changes: 4 additions & 0 deletions frontend/providers/applaunchpad/src/api/params.d.ts
@@ -0,0 +1,4 @@
export type AuthCnamePrams = {
publicDomain: string;
customDomain: string;
};
5 changes: 4 additions & 1 deletion frontend/providers/applaunchpad/src/api/platform.ts
@@ -1,6 +1,7 @@
import { GET } from '@/services/request';
import { GET, POST } from '@/services/request';
import type { Response as InitDataType } from '@/pages/api/platform/getInitData';
import type { UserQuotaItemType, userPriceType } from '@/types/user';
import { AuthCnamePrams } from './params';

export const getResourcePrice = () => GET<userPriceType>('/api/platform/resourcePrice');
export const getInitData = () => GET<InitDataType>('/api/platform/getInitData');
Expand All @@ -9,3 +10,5 @@ export const getUserQuota = () =>
balance: number;
quota: UserQuotaItemType[];
}>('/api/platform/getQuota');

export const postAuthCname = (data: AuthCnamePrams) => POST('/api/platform/authCname', data);
54 changes: 54 additions & 0 deletions frontend/providers/applaunchpad/src/components/MyModal/index.tsx
@@ -0,0 +1,54 @@
import React from 'react';
import {
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalCloseButton,
ModalContentProps,
Box
} from '@chakra-ui/react';

interface Props extends ModalContentProps {
showCloseBtn?: boolean;
title?: any;
isCentered?: boolean;
isOpen: boolean;
onClose: () => void;
}

const MyModal = ({
isOpen,
onClose,
title,
children,
showCloseBtn = true,
isCentered,
w = 'auto',
maxW = ['90vw', '600px'],
...props
}: Props) => {
return (
<Modal isOpen={isOpen} onClose={onClose} autoFocus={false} isCentered={isCentered}>
<ModalOverlay />
<ModalContent
display={'flex'}
flexDirection={'column'}
w={w}
minW={['90vw', '400px']}
maxW={maxW}
position={'relative'}
maxH={'90vh'}
{...props}
>
{!!title && <ModalHeader>{title}</ModalHeader>}
<Box overflow={'overlay'} h={'100%'}>
{showCloseBtn && <ModalCloseButton />}
{children}
</Box>
</ModalContent>
</Modal>
);
};

export default MyModal;
Expand Up @@ -111,5 +111,11 @@ export const codeTheme = {
},
'hljs-strong': {
fontWeight: 'bold'
}
},
'-webkit-touch-callout': 'none' /* iOS Safari */,
'-webkit-user-select': 'none' /* Safari */,
'-khtml-user-select': 'none' /* Konqueror HTML */,
'-moz-user-select': 'none' /* Old versions of Firefox */,
'-ms-user-select': 'none' /* Internet Explorer/Edge */,
'user-select': 'none' /* Non-prefixed version, currently supported by Chrome, Opera and Firefox */
};
8 changes: 7 additions & 1 deletion frontend/providers/applaunchpad/src/constants/app.ts
Expand Up @@ -70,12 +70,18 @@ export const podStatusMap = {
}
};

export const ProtocolList = [
{ value: 'HTTP', label: 'https://' },
{ value: 'GRPC', label: 'grpcs://' },
{ value: 'WS', label: 'wss://' }
];

export const noGpuSliderKey = 'NoGpu';
export const pauseKey = 'deploy.cloud.sealos.io/pause';
export const maxReplicasKey = 'deploy.cloud.sealos.io/maxReplicas';
export const minReplicasKey = 'deploy.cloud.sealos.io/minReplicas';
export const appDeployKey = 'cloud.sealos.io/app-deploy-manager';
export const domainKey = `cloud.sealos.io/app-deploy-manager-domain`;
export const publicDomainKey = `cloud.sealos.io/app-deploy-manager-domain`;
export const gpuNodeSelectorKey = 'nvidia.com/gpu.product';
export const gpuResourceKey = 'nvidia.com/gpu';
export enum Coin {
Expand Down
20 changes: 13 additions & 7 deletions frontend/providers/applaunchpad/src/constants/editApp.ts
@@ -1,4 +1,6 @@
import type { AppEditType } from '@/types/app';
import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 12);

export const editModeMap = (isEdit: boolean) => {
if (isEdit) {
Expand Down Expand Up @@ -28,13 +30,17 @@ export const defaultEditVal: AppEditType = {
replicas: 1,
cpu: 100,
memory: 64,
containerOutPort: 80,
accessExternal: {
use: false,
backendProtocol: 'HTTP',
outDomain: '',
selfDomain: ''
},
networks: [
{
networkName: '',
portName: nanoid(),
port: 80,
protocol: 'HTTP',
openPublicDomain: false,
publicDomain: '',
customDomain: ''
}
],
envs: [],
hpa: {
use: false,
Expand Down
5 changes: 5 additions & 0 deletions frontend/providers/applaunchpad/src/constants/theme.ts
Expand Up @@ -16,6 +16,11 @@ const Button = defineStyleConfig({
baseStyle: {
_active: {
transform: 'scale(0.98)'
},
_disabled: {
_hover: {
bg: 'myGray.900 !important'
}
}
},
sizes: {
Expand Down
36 changes: 36 additions & 0 deletions frontend/providers/applaunchpad/src/hooks/useRequest.tsx
@@ -0,0 +1,36 @@
import { useToast } from '@/hooks/useToast';
import { useMutation } from '@tanstack/react-query';
import type { UseMutationOptions } from '@tanstack/react-query';
import { getErrText } from '@/utils/tools';
import { useTranslation } from 'react-i18next';

interface Props extends UseMutationOptions<any, any, any, any> {
successToast?: string | null;
errorToast?: string | null;
}

export const useRequest = ({ successToast, errorToast, onSuccess, onError, ...props }: Props) => {
const { toast } = useToast();
const { t } = useTranslation();
const mutation = useMutation<unknown, unknown, any, unknown>({
...props,
onSuccess(res, variables: void, context: unknown) {
onSuccess?.(res, variables, context);
successToast &&
toast({
title: successToast,
status: 'success'
});
},
onError(err: any, variables: void, context: unknown) {
onError?.(err, variables, context);
errorToast &&
toast({
title: t(getErrText(err, errorToast)),
status: 'error'
});
}
});

return mutation;
};
21 changes: 14 additions & 7 deletions frontend/providers/applaunchpad/src/mock/apps.ts
@@ -1,5 +1,8 @@
import { AppListItemType, AppDetailType, PodDetailType } from '@/types/app';
import { appStatusMap, podStatusMap } from '@/constants/app';
import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 12);

export const MOCK_APPS: AppListItemType[] = [
{
id: 'string',
Expand Down Expand Up @@ -296,13 +299,17 @@ export const MOCK_APP_DETAIL: AppDetailType = {
memory: 0,
usedCpu: new Array(30).fill(0),
usedMemory: new Array(30).fill(0),
containerOutPort: 8000,
accessExternal: {
use: true,
backendProtocol: 'HTTP',
outDomain: '',
selfDomain: ''
},
networks: [
{
networkName: '',
portName: nanoid(),
port: 80,
protocol: 'HTTP',
openPublicDomain: false,
publicDomain: '',
customDomain: ''
}
],
envs: [],
hpa: {
use: false,
Expand Down
11 changes: 10 additions & 1 deletion frontend/providers/applaunchpad/src/pages/api/delApp.ts
Expand Up @@ -3,6 +3,7 @@ import { ApiResp } from '@/services/kubernet';
import { authSession } from '@/services/backend/auth';
import { getK8s } from '@/services/backend/kubernetes';
import { jsonRes } from '@/services/backend/response';
import { appDeployKey } from '@/constants/app';

export default async function handler(req: NextApiRequest, res: NextApiResponse<ApiResp>) {
try {
Expand All @@ -21,7 +22,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
k8sCore.deleteNamespacedService(name, namespace), // delete service
k8sCore.deleteNamespacedConfigMap(name, namespace), // delete configMap
k8sCore.deleteNamespacedSecret(name, namespace), // delete secret
k8sNetworkingApp.deleteNamespacedIngress(name, namespace), // delete Ingress
k8sNetworkingApp.deleteCollectionNamespacedIngress(
namespace,
undefined,
undefined,
undefined,
undefined,
undefined,
`${appDeployKey}=${name}`
), // delete Ingress
k8sCustomObjects.deleteNamespacedCustomObject(
// delete Issuer
'cert-manager.io',
Expand Down
21 changes: 19 additions & 2 deletions frontend/providers/applaunchpad/src/pages/api/getAppByAppName.ts
Expand Up @@ -3,6 +3,7 @@ import { ApiResp } from '@/services/kubernet';
import { authSession } from '@/services/backend/auth';
import { getK8s } from '@/services/backend/kubernetes';
import { jsonRes } from '@/services/backend/response';
import { appDeployKey } from '@/constants/app';

export default async function handler(req: NextApiRequest, res: NextApiResponse<ApiResp>) {
try {
Expand All @@ -20,7 +21,22 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
k8sApp.readNamespacedStatefulSet(appName, namespace),
k8sCore.readNamespacedService(appName, namespace),
k8sCore.readNamespacedConfigMap(appName, namespace),
k8sNetworkingApp.readNamespacedIngress(appName, namespace),
k8sNetworkingApp
.listNamespacedIngress(
namespace,
undefined,
undefined,
undefined,
undefined,
`${appDeployKey}=${appName}`
)
.then((res) => ({
body: res.body.items.map((item) => ({
...item,
apiVersion: res.body.apiVersion, // item does not contain apiversion and kind
kind: 'Ingress'
}))
})),
k8sCore.readNamespacedSecret(appName, namespace),
k8sAutoscaling.readNamespacedHorizontalPodAutoscaler(appName, namespace)
]);
Expand All @@ -32,7 +48,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
if (+item.reason?.body?.code === 404) return '';
throw new Error('Get APP Deployment Error');
})
.filter((item) => item);
.filter((item) => item)
.flat();

jsonRes(res, {
data: responseData
Expand Down
@@ -0,0 +1,29 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/services/backend/response';
import dns from 'dns';
import type { AuthCnamePrams } from '@/api/params';
import { getErrText } from '@/utils/tools';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { publicDomain, customDomain } = req.body as AuthCnamePrams;

await (async () =>
new Promise((resolve, reject) => {
dns.resolveCname(customDomain, (err, address) => {
if (err) return reject(err);

if (address[0] !== publicDomain)
return reject("Cname auth error: customDomain's cname is not equal to publicDomain");
resolve('');
});
}))();

jsonRes(res);
} catch (error) {
jsonRes(res, {
code: 500,
error
});
}
}

0 comments on commit 289994d

Please sign in to comment.