feat:launchpad jsonpatch & template add filename (#4436)

* feat:launchpad jsonpatch & template add filename

Signed-off-by: jingyang <3161362058@qq.com>

* add db services.nodeports price

Signed-off-by: jingyang <3161362058@qq.com>

---------

Signed-off-by: jingyang <3161362058@qq.com>
This commit is contained in:
zhujingyang
2023-12-20 14:57:45 +08:00
committed by GitHub
parent 97c3ad2c16
commit 8069caa36b
19 changed files with 99 additions and 170 deletions
@@ -116,5 +116,6 @@
"Click on any shadow to skip": "Click on any shadow to skip",
"Start your Sealos journey": "Start your Sealos journey",
"gift amount": "Reward {{amount}} balance.",
"Recharge Amount": "Recharge Amount"
"Recharge Amount": "Recharge Amount",
"Doc": "Doc"
}
@@ -109,5 +109,6 @@
"start immediately": "立即开始",
"Click on any shadow to skip": "点击任意阴影跳过",
"Start your Sealos journey": "开始您的 Sealos 之旅",
"gift amount": "赠送 {{amount}} 余额."
}
"gift amount": "赠送 {{amount}} 余额.",
"Doc": "文档"
}
@@ -100,7 +100,7 @@ const NsMenu = () => {
w="250px"
background="linear-gradient(270deg, #F1F1F1 0%, #EEE 43.75%, #ECECEC 100%)"
>
<PopoverBody px="0" pb="0" pt="4px">
<PopoverBody px="0" pb="0" pt="4px" maxH={'320px'} overflowY={'auto'}>
<NsList
displayPoint={true}
selected_ns_uid={ns_uid}
@@ -1,6 +1,6 @@
/* eslint-disable jsx-a11y/alt-text */
import useAppStore from '@/stores/app';
import { Box, Flex, Image } from '@chakra-ui/react';
import { Box, Flex, Image, Text } from '@chakra-ui/react';
import clsx from 'clsx';
import React, { useRef, useState } from 'react';
import Draggable, { DraggableEventHandler } from 'react-draggable';
@@ -107,7 +107,7 @@ export default function AppWindow(props: {
setPosition({ x: 0, y: 0 });
}}
>
<Flex ml="16px" alignItems={'center'}>
<Flex ml="16px" alignItems={'center'} fontSize={'12px'} fontWeight={400}>
<Image
src={wnapp?.icon}
fallbackSrc="/images/sealos.svg"
@@ -120,6 +120,18 @@ export default function AppWindow(props: {
? wnapp.i18n?.[i18n?.language]?.name
: t(wnapp?.name)}
</Box>
{wnapp.menuData?.helpDocs && (
<Text
color={'#24282C'}
ml="16px"
onClick={() => {
typeof wnapp.menuData?.helpDocs === 'string' &&
window.open(wnapp.menuData?.helpDocs);
}}
>
{t('Doc')}
</Text>
)}
</Flex>
<Flex ml={'auto'}>
<Box
@@ -11,6 +11,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
await (async () =>
new Promise((resolve, reject) => {
dns.resolveCname(customDomain, (err, address) => {
console.log(err, address);
if (err) return reject(err);
if (address[0] !== publicDomain)
@@ -974,16 +974,18 @@ const Form = ({
return (
<tr key={env.id}>
<th>{env.key}</th>
<MyTooltip label={valText}>
<th
className={styles.textEllipsis}
style={{
userSelect: 'auto'
}}
>
{valText}
</th>
</MyTooltip>
<th>
<MyTooltip label={valText}>
<Box
className={styles.textEllipsis}
style={{
userSelect: 'auto'
}}
>
{valText}
</Box>
</MyTooltip>
</th>
</tr>
);
})}
@@ -21,7 +21,7 @@ import {
json2Service
} from '@/utils/deployYaml2Json';
import { serviceSideProps } from '@/utils/i18n';
import { getErrText, patchYamlList, patchYamlListV1 } from '@/utils/tools';
import { getErrText, patchYamlList } from '@/utils/tools';
import { Box, Flex } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import { useTranslation } from 'next-i18next';
@@ -158,10 +158,9 @@ const EditApp = ({ appName, tabType }: { appName?: string; tabType: string }) =>
if (appName) {
const patch = patchYamlList({
formOldYamlList: formOldYamls.current.map((item) => item.value),
crYamlList: crOldYamls.current,
newYamlList: yamls
newYamlList: yamls,
crYamlList: crOldYamls.current
});
await putApp({
patch,
appName,
@@ -1,18 +1,17 @@
import yaml from 'js-yaml';
import type { AppEditType, DeployKindsType } from '@/types/app';
import { strToBase64, str2Num, pathFormat, pathToNameFormat } from '@/utils/tools';
import { SEALOS_DOMAIN, INGRESS_SECRET } from '@/store/static';
import {
maxReplicasKey,
minReplicasKey,
appDeployKey,
publicDomainKey,
deployPVCResizeKey,
gpuNodeSelectorKey,
gpuResourceKey,
deployPVCResizeKey
maxReplicasKey,
minReplicasKey,
publicDomainKey
} from '@/constants/app';
import { INGRESS_SECRET, SEALOS_DOMAIN } from '@/store/static';
import type { AppEditType } from '@/types/app';
import { pathFormat, pathToNameFormat, str2Num, strToBase64 } from '@/utils/tools';
import dayjs from 'dayjs';
import jsonpatch, { Operation } from 'fast-json-patch';
import yaml from 'js-yaml';
export const json2DeployCr = (data: AppEditType, type: 'deployment' | 'statefulset') => {
const totalStorage = data.storeList.reduce((acc, item) => acc + item.value, 0);
@@ -233,13 +233,13 @@ export const patchYamlList = ({
newYamlList: string[];
crYamlList: DeployKindsType[];
}) => {
console.log(formOldYamlList, newYamlList, crYamlList, '=======');
const oldFormJsonList = formOldYamlList
.map((item) => yaml.loadAll(item))
.flat() as DeployKindsType[];
const newFormJsonList = newYamlList.map((item) => yaml.loadAll(item)).flat() as DeployKindsType[];
console.log(oldFormJsonList, newFormJsonList, crYamlList, '===patchYamlList===');
const actions: AppPatchPropsType = [];
// find delete
@@ -298,7 +298,21 @@ export const patchYamlList = ({
}
/* generate new json */
const patchResYamlJson = jsonpatch.applyPatch(crOldYamlJson, patchRes, true).newDocument;
const _patchRes: jsonpatch.Operation[] = patchRes.map((item) => {
let jsonPatchError = jsonpatch.validate([item], crOldYamlJson);
if (
jsonPatchError?.operation &&
jsonPatchError?.name === 'OPERATION_PATH_UNRESOLVABLE'
) {
return {
...jsonPatchError.operation,
op: 'add'
};
} else {
return item;
}
});
const patchResYamlJson = jsonpatch.applyPatch(crOldYamlJson, _patchRes, true).newDocument;
// delete invalid field
// @ts-ignore
@@ -314,8 +328,7 @@ export const patchYamlList = ({
return patchResYamlJson;
} catch (error) {
console.log(error);
console.log('ACTIONS JSON ERROR\n', error);
return newYamlJson;
}
})();
@@ -344,6 +357,14 @@ export const patchYamlList = ({
}
}
console.log(
'patch result===',
oldFormJson.metadata?.name,
oldFormJson.kind,
patchRes,
actionsJson
);
actions.push({
type: 'patch',
kind: newYamlJson.kind as `${YamlKindEnum}`,
@@ -357,69 +378,6 @@ export const patchYamlList = ({
});
}
});
console.log(actions, 'actions');
return actions;
};
export const patchYamlListV1 = ({
newYamlList,
oldYamlList
}: {
newYamlList: string[];
oldYamlList: DeployKindsType[];
}) => {
const newFormJsonList = newYamlList.map((item) => yaml.loadAll(item)).flat() as DeployKindsType[];
console.log('new:', newFormJsonList, '\n old', oldYamlList);
const actions: AppPatchPropsType = [];
// find delete
oldYamlList.forEach((oldYamlJson) => {
const item = newFormJsonList.find(
(item) => item.kind === oldYamlJson.kind && item.metadata?.name === oldYamlJson.metadata?.name
);
if (!item && oldYamlJson.metadata?.name) {
actions.push({
type: 'delete',
kind: oldYamlJson.kind as `${YamlKindEnum}`,
name: oldYamlJson.metadata?.name
});
}
});
// find create and patch
newFormJsonList.forEach((newYamlJson) => {
const oldFormJson = oldYamlList.find(
(item) =>
item.kind === newYamlJson.kind && item?.metadata?.name === newYamlJson?.metadata?.name
);
if (oldFormJson) {
// adapt service ports
if (newYamlJson.kind === YamlKindEnum.Service) {
// @ts-ignore
const ports = newYamlJson?.spec.ports || [];
// @ts-ignore
if (ports.length > 1 && !ports[0]?.name) {
// @ts-ignore
newYamlJson.spec.ports[0].name = 'adaptport';
}
}
actions.push({
type: 'patch',
kind: newYamlJson.kind as `${YamlKindEnum}`,
value: newYamlJson as any
});
} else {
actions.push({
type: 'create',
kind: newYamlJson.kind as `${YamlKindEnum}`,
value: yaml.dump(newYamlJson)
});
}
});
return actions;
};
@@ -8,6 +8,7 @@ export type Response = {
cpu: number;
memory: number;
storage: number;
nodeports: number;
};
type ResourceType =
| 'cpu'
@@ -18,7 +19,8 @@ type ResourceType =
| 'mongodb'
| 'minio'
| 'infra-memory'
| 'infra-disk';
| 'infra-disk'
| 'services.nodeports';
type PriceCrdType = {
apiVersion: 'account.sealos.io/v1';
kind: 'PriceQuery';
@@ -72,7 +74,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const data = {
cpu: countSourcePrice(priceResponse, 'cpu'),
memory: countSourcePrice(priceResponse, 'memory'),
storage: countSourcePrice(priceResponse, 'storage')
storage: countSourcePrice(priceResponse, 'storage'),
nodeports: countSourcePrice(priceResponse, 'services.nodeports')
};
jsonRes(res, {
@@ -9,6 +9,7 @@ import MyIcon from '@/components/Icon';
import { DBStatusEnum, DBTypeEnum, DBTypeSecretMap, defaultDBDetail } from '@/constants/db';
import { useToast } from '@/hooks/useToast';
import useEnvStore from '@/store/env';
import { SOURCE_PRICE } from '@/store/static';
import type { DBDetailType } from '@/types/db';
import { json2NetworkService } from '@/utils/json2Yaml';
import { printMemory, useCopyData } from '@/utils/tools';
@@ -395,21 +396,9 @@ const AppBaseInfo = ({ db = defaultDBDetail }: { db: DBDetailType }) => {
{t('Billing Standards')}
</Text>
<Center mt="16px" color={'#24282C'} fontSize={'24px'} fontWeight={600}>
0.5
{SOURCE_PRICE.nodeports}
<MyIcon ml="8px" name="currency" w="32px" h="32px"></MyIcon>/ {t('Hour')}
</Center>
{/* <Center
p="8px"
mt="20px"
bg="#EBF7FD"
color={'#1D8CDC'}
cursor={'pointer'}
w="100%"
onClick={() => window.open('https://forum.laf.run/d/1092')}
>
<MyIcon name="infoCircle" w="16px" fill={'#48A7EC'}></MyIcon>
免费方案
</Center> */}
<Button mt="32px" variant={'primary'} onClick={openNetWorkService}>
{t('Turn On')}
</Button>
@@ -6,7 +6,8 @@ import type { Response as resourcePriceResponse } from '@/pages/api/platform/res
export let SOURCE_PRICE: resourcePriceResponse = {
cpu: 0.067,
memory: 0.033792,
storage: 0.002048
storage: 0.002048,
nodeports: 0.5
};
export let INSTALL_ACCOUNT = false;
@@ -1,44 +0,0 @@
import { authSession } from '@/services/backend/auth';
import { getK8s } from '@/services/backend/kubernetes';
import { jsonRes } from '@/services/backend/response';
import { ApiResp } from '@/services/kubernet';
import { TemplateType } from '@/types/app';
import fs from 'fs';
import yaml from 'js-yaml';
import type { NextApiRequest, NextApiResponse } from 'next';
import path from 'path';
export default async function handler(req: NextApiRequest, res: NextApiResponse<ApiResp>) {
try {
const { templateName } = req.query as { templateName: string };
let user_namespace = '';
try {
const { namespace } = await getK8s({
kubeconfig: await authSession(req.headers)
});
user_namespace = namespace;
} catch (error) {
console.log(error, 'errpr-');
}
const originalPath = process.cwd();
const targetPath = path.resolve(originalPath, 'FastDeployTemplates', 'template');
const yamlString = fs.readFileSync(`${targetPath}/${templateName}.yaml`, 'utf-8');
const yamlData = yaml.loadAll(yamlString);
const templateYaml: TemplateType = yamlData.find(
(item: any) => item.kind === 'Template'
) as TemplateType;
jsonRes(res, {
code: 200,
data: templateYaml
});
} catch (err: any) {
console.log(err);
jsonRes(res, {
code: 500,
error: err
});
}
}
@@ -13,6 +13,7 @@ import { replaceRawWithCDN } from './listTemplate';
export default async function handler(req: NextApiRequest, res: NextApiResponse<ApiResp>) {
try {
const { templateName } = req.query as { templateName: string };
let user_namespace = '';
try {
@@ -72,8 +73,12 @@ export async function GetTemplateByName({
const originalPath = process.cwd();
const targetPath = path.resolve(originalPath, 'FastDeployTemplates', 'template');
const yamlString = fs.readFileSync(`${targetPath}/${templateName}.yaml`, 'utf-8');
// Query by file name in template details
const jsonPath = path.resolve(originalPath, 'fast_deploy_template.json');
const jsonData: TemplateType[] = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
const _tempalte = jsonData.find((item) => item.metadata.name === templateName);
const _tempalteName = _tempalte ? _tempalte.spec.fileName : `${templateName}.yaml`;
const yamlString = fs.readFileSync(`${targetPath}/${_tempalteName}`, 'utf-8');
const yamlData = yaml.loadAll(yamlString);
const templateYaml: TemplateType = yamlData.find(
@@ -1,5 +1,6 @@
import { jsonRes } from '@/services/backend/response';
import { ApiResp } from '@/services/kubernet';
import { TemplateType } from '@/types/app';
import { exec } from 'child_process';
import fs from 'fs';
import JSYAML from 'js-yaml';
@@ -65,9 +66,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
fileList.forEach((item: any) => {
try {
if (!item) return;
const fileName = path.basename(item);
const content = fs.readFileSync(item, 'utf-8');
const yamlTemplate: any = JSYAML.loadAll(content)[0];
const yamlTemplate = JSYAML.loadAll(content)[0] as TemplateType;
if (!!yamlTemplate) {
yamlTemplate.spec['fileName'] = fileName;
jsonObjArr.push(yamlTemplate);
}
} catch (error) {
@@ -62,8 +62,7 @@ const Form = ({
w="200px"
className="template-dynamic-label"
color={'#333'}
userSelect={'none'}
>
userSelect={'none'}>
{item?.label}
{item?.required && (
<Text ml="2px" color={'#E53E3E'}>
@@ -79,7 +78,7 @@ const Form = ({
autoFocus={true}
placeholder={item?.description}
{...register(item?.key, {
required: item?.required
required: item?.required ? `${item.label} is required` : ''
})}
/>
</Flex>
@@ -93,16 +92,14 @@ const Form = ({
alignItems="center"
h={'160px'}
w={'100%'}
flexDirection="column"
>
flexDirection="column">
<Flex
border={'1px dashed #9CA2A8'}
borderRadius="50%"
w={'48px'}
h={'48px'}
justifyContent="center"
alignItems={'center'}
>
alignItems={'center'}>
<MyIcon color={'#7B838B'} name="empty"></MyIcon>
</Flex>
<Text mt={'12px'} fontSize={14} color={'#5A646E'}>
@@ -162,8 +162,8 @@ export default function EditApp({ appName }: { appName?: string }) {
setIsLoading(false);
};
const submitError = () => {
formHook.getValues();
const submitError = async () => {
await formHook.trigger();
toast({
title: deepSearch(formHook.formState.errors),
status: 'error',
@@ -56,7 +56,7 @@ export default function Develop() {
const onYamlChange = debounce((state: EditorState) => {
const value = state.doc.toString();
parseTemplate(value);
}, 1000);
}, 500);
const getYamlSource = (str: string): TemplateSourceType => {
const yamlData = JsYaml.loadAll(str);
@@ -7,6 +7,8 @@ export type TemplateType = {
spec: {
gitRepo: string; // new
templateType: 'inline'; // new
fileName: string; // new
template_type?: string;
author: string;
title: string;