diff --git a/frontend/providers/applaunchpad/public/locales/en/common.json b/frontend/providers/applaunchpad/public/locales/en/common.json
index 57396db55da..8cb013d4582 100644
--- a/frontend/providers/applaunchpad/public/locales/en/common.json
+++ b/frontend/providers/applaunchpad/public/locales/en/common.json
@@ -159,5 +159,7 @@
"common": {
"Used": "Used",
"Surplus": "Surplus"
- }
+ },
+ "Store At Least One": "Store At Least One",
+ "Network port conflict": "Network port conflict"
}
diff --git a/frontend/providers/applaunchpad/public/locales/zh/common.json b/frontend/providers/applaunchpad/public/locales/zh/common.json
index f8415242cc4..0f1c1dfd0e7 100644
--- a/frontend/providers/applaunchpad/public/locales/zh/common.json
+++ b/frontend/providers/applaunchpad/public/locales/zh/common.json
@@ -202,5 +202,6 @@
},
"user": {
"Insufficient account balance": "账号余额不足~"
- }
+ },
+ "Store At Least One": "存储最少保留一个"
}
diff --git a/frontend/providers/applaunchpad/src/components/Icon/icons/detail.svg b/frontend/providers/applaunchpad/src/components/Icon/icons/detail.svg
index 951e9eb6cbb..e4c6fa97280 100644
--- a/frontend/providers/applaunchpad/src/components/Icon/icons/detail.svg
+++ b/frontend/providers/applaunchpad/src/components/Icon/icons/detail.svg
@@ -1 +1,3 @@
-
\ No newline at end of file
+
diff --git a/frontend/providers/applaunchpad/src/components/Icon/icons/log.svg b/frontend/providers/applaunchpad/src/components/Icon/icons/log.svg
index 9140c6fb531..364239937be 100644
--- a/frontend/providers/applaunchpad/src/components/Icon/icons/log.svg
+++ b/frontend/providers/applaunchpad/src/components/Icon/icons/log.svg
@@ -1,6 +1,4 @@
-
\ No newline at end of file
+
diff --git a/frontend/providers/applaunchpad/src/components/Icon/icons/restart.svg b/frontend/providers/applaunchpad/src/components/Icon/icons/restart.svg
index de42eb25696..69676a80067 100644
--- a/frontend/providers/applaunchpad/src/components/Icon/icons/restart.svg
+++ b/frontend/providers/applaunchpad/src/components/Icon/icons/restart.svg
@@ -1 +1,3 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/frontend/providers/applaunchpad/src/components/Icon/icons/terminal.svg b/frontend/providers/applaunchpad/src/components/Icon/icons/terminal.svg
index db9147be96f..8df6de88aa3 100644
--- a/frontend/providers/applaunchpad/src/components/Icon/icons/terminal.svg
+++ b/frontend/providers/applaunchpad/src/components/Icon/icons/terminal.svg
@@ -1 +1,3 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/frontend/providers/applaunchpad/src/pages/app/detail/components/Header.tsx b/frontend/providers/applaunchpad/src/pages/app/detail/components/Header.tsx
index bf8945ecabb..a8b0c247b63 100644
--- a/frontend/providers/applaunchpad/src/pages/app/detail/components/Header.tsx
+++ b/frontend/providers/applaunchpad/src/pages/app/detail/components/Header.tsx
@@ -117,7 +117,7 @@ const Header = ({
flex={1}
h={'40px'}
borderColor={'myGray.200'}
- leftIcon={}
+ leftIcon={}
variant={'base'}
bg={'white'}
onClick={() => setShowSlider(true)}
@@ -180,7 +180,7 @@ const Header = ({
borderColor={'myGray.200'}
variant={'base'}
bg={'white'}
- leftIcon={}
+ leftIcon={}
onClick={openRestartConfirm(handleRestartApp)}
isLoading={loading}
>
diff --git a/frontend/providers/applaunchpad/src/pages/app/detail/components/PodDetailModal.tsx b/frontend/providers/applaunchpad/src/pages/app/detail/components/PodDetailModal.tsx
index eb501692a62..488a8ebf8a0 100644
--- a/frontend/providers/applaunchpad/src/pages/app/detail/components/PodDetailModal.tsx
+++ b/frontend/providers/applaunchpad/src/pages/app/detail/components/PodDetailModal.tsx
@@ -99,7 +99,7 @@ const Logs = ({
);
}, []);
- const { isLoading } = useQuery(['init'], () => getPodEvents(pod.podName), {
+ const { isLoading } = useQuery(['initPodEvents'], () => getPodEvents(pod.podName), {
refetchInterval: 3000,
onSuccess(res) {
setEvents(res);
diff --git a/frontend/providers/applaunchpad/src/pages/app/detail/components/Pods.tsx b/frontend/providers/applaunchpad/src/pages/app/detail/components/Pods.tsx
index b2014c9ff1d..1e321823a61 100644
--- a/frontend/providers/applaunchpad/src/pages/app/detail/components/Pods.tsx
+++ b/frontend/providers/applaunchpad/src/pages/app/detail/components/Pods.tsx
@@ -10,7 +10,9 @@ import {
Td,
TableContainer,
Flex,
- MenuButton
+ MenuButton,
+ Tooltip,
+ Center
} from '@chakra-ui/react';
import { sealosApp } from 'sealos-desktop-sdk/app';
import { restartPodByName } from '@/api/app';
@@ -135,70 +137,68 @@ const Pods = ({
title: 'Operation',
key: 'control',
render: (item: PodDetailType, i: number) => (
-
- }
- variant={'base'}
- px={3}
- onClick={() => setLogsPodIndex(i)}
- >
- {t('Log')}
-
-
-
-
- }
- menuList={[
- {
- child: (
- <>
-
- {t('Terminal')}
- >
- ),
- onClick: () => {
- const defaultCommand = `kubectl exec -it ${item.podName} -c ${appName} -- sh -c "clear; (bash || ash || sh)"`;
- sealosApp.runEvents('openDesktopApp', {
- appKey: 'system-terminal',
- query: {
- defaultCommand
- },
- messageData: { type: 'new terminal', command: defaultCommand }
- });
- }
- },
- {
- child: (
- <>
-
- {t('Details')}
- >
- ),
- onClick: () => setDetailPodIndex(i)
- },
- {
- child: (
- <>
-
- {t('Restart')}
- >
- ),
- onClick: openConfirmRestart(() => handleRestartPod(item.podName))
- }
- ]}
- />
+
+
+ setLogsPodIndex(i)}
+ >
+
+
+
+
+ {
+ const defaultCommand = `kubectl exec -it ${item.podName} -c ${appName} -- sh -c "clear; (bash || ash || sh)"`;
+ sealosApp.runEvents('openDesktopApp', {
+ appKey: 'system-terminal',
+ query: {
+ defaultCommand
+ },
+ messageData: { type: 'new terminal', command: defaultCommand }
+ });
+ }}
+ >
+
+
+
+
+ setDetailPodIndex(i)}
+ >
+
+
+
+
+ handleRestartPod(item.podName))}
+ >
+
+
+
)
}
diff --git a/frontend/providers/applaunchpad/src/pages/app/edit/components/Form.tsx b/frontend/providers/applaunchpad/src/pages/app/edit/components/Form.tsx
index 5744b3eae61..d405aafc1d8 100644
--- a/frontend/providers/applaunchpad/src/pages/app/edit/components/Form.tsx
+++ b/frontend/providers/applaunchpad/src/pages/app/edit/components/Form.tsx
@@ -56,6 +56,7 @@ import { obj2Query } from '@/api/tools';
import { throttle } from 'lodash';
import { ProtocolList, noGpuSliderKey } from '@/constants/app';
import { sliderNumber2MarkList } from '@/utils/adapt';
+import { useToast } from '@/hooks/useToast';
const labelWidth = 120;
@@ -79,6 +80,7 @@ const Form = ({
const { formSliderListConfig } = useGlobalStore();
const { userSourcePrice } = useUserStore();
const router = useRouter();
+ const { toast } = useToast();
const { name } = router.query as QueryType;
const theme = useTheme();
const isEdit = useMemo(() => !!name, [name]);
@@ -1101,7 +1103,16 @@ const Form = ({
className={styles.deleteIcon}
ml={3}
cursor={'pointer'}
- onClick={() => removeStoreList(index)}
+ onClick={() => {
+ if (storeList.length === 1) {
+ toast({
+ title: t('Store At Least One'),
+ status: 'error'
+ });
+ } else {
+ removeStoreList(index);
+ }
+ }}
>
diff --git a/frontend/providers/applaunchpad/src/pages/app/edit/index.tsx b/frontend/providers/applaunchpad/src/pages/app/edit/index.tsx
index 48ec6e20f03..5e2c15f634a 100644
--- a/frontend/providers/applaunchpad/src/pages/app/edit/index.tsx
+++ b/frontend/providers/applaunchpad/src/pages/app/edit/index.tsx
@@ -26,14 +26,18 @@ import Form from './components/Form';
import Yaml from './components/Yaml';
import dynamic from 'next/dynamic';
import { serviceSideProps } from '@/utils/i18n';
-import { getErrText, patchYamlList } from '@/utils/tools';
+import { getErrText, patchYamlListV1, patchYamlList } from '@/utils/tools';
import { useTranslation } from 'next-i18next';
import { noGpuSliderKey } from '@/constants/app';
import { useUserStore } from '@/store/user';
const ErrorModal = dynamic(() => import('./components/ErrorModal'));
-export const formData2Yamls = (data: AppEditType) => [
+export const formData2Yamls = (
+ data: AppEditType,
+ handleType: 'edit' | 'create' = 'create',
+ crYamlList?: DeployKindsType[]
+) => [
{
filename: 'service.yaml',
value: json2Service(data)
@@ -41,11 +45,11 @@ export const formData2Yamls = (data: AppEditType) => [
!!data.storeList?.length
? {
filename: 'statefulSet.yaml',
- value: json2DeployCr(data, 'statefulset')
+ value: json2DeployCr(data, 'statefulset', handleType, crYamlList)
}
: {
filename: 'deployment.yaml',
- value: json2DeployCr(data, 'deployment')
+ value: json2DeployCr(data, 'deployment', handleType, crYamlList)
},
...(data.configMapList.length > 0
? [
@@ -83,7 +87,7 @@ export const formData2Yamls = (data: AppEditType) => [
const EditApp = ({ appName, tabType }: { appName?: string; tabType: string }) => {
const { t } = useTranslation();
- const formOldYamls = useRef([]);
+
const crOldYamls = useRef([]);
const oldAppEditData = useRef();
@@ -150,12 +154,10 @@ const EditApp = ({ appName, tabType }: { appName?: string; tabType: string }) =>
const yamls = yamlList.map((item) => item.value);
if (appName) {
- const patch = patchYamlList({
- formOldYamlList: formOldYamls.current.map((item) => item.value),
- crYamlList: crOldYamls.current,
+ const patch = patchYamlListV1({
+ oldYamlList: crOldYamls.current,
newYamlList: yamls
});
- console.log(patch);
await putApp({
patch,
@@ -213,7 +215,7 @@ const EditApp = ({ appName, tabType }: { appName?: string; tabType: string }) =>
}, [formHook.formState.errors, t, toast]);
useQuery(
- ['init'],
+ ['initLaunchpadApp'],
() => {
if (!appName) {
const defaultApp = {
@@ -242,7 +244,7 @@ const EditApp = ({ appName, tabType }: { appName?: string; tabType: string }) =>
onSuccess(res) {
if (!res) return;
oldAppEditData.current = res;
- formOldYamls.current = formData2Yamls(res);
+
crOldYamls.current = res.crYamlList;
setDefaultStorePathList(res.storeList.map((item) => item.path));
@@ -265,10 +267,16 @@ const EditApp = ({ appName, tabType }: { appName?: string; tabType: string }) =>
useEffect(() => {
if (tabType === 'yaml') {
try {
- setYamlList(formData2Yamls(realTimeForm.current));
+ setYamlList(
+ formData2Yamls(
+ realTimeForm.current,
+ appName !== '' ? 'edit' : 'create',
+ crOldYamls.current
+ )
+ );
} catch (error) {}
}
- }, [router.query.name, tabType]);
+ }, [appName, router.query.name, tabType]);
return (
<>
@@ -286,7 +294,11 @@ const EditApp = ({ appName, tabType }: { appName?: string; tabType: string }) =>
applyBtnText={applyBtnText}
applyCb={() =>
formHook.handleSubmit((data) => {
- const parseYamls = formData2Yamls(data);
+ const parseYamls = formData2Yamls(
+ data,
+ appName !== '' ? 'edit' : 'create',
+ crOldYamls.current
+ );
setYamlList(parseYamls);
// balance check
if (balance <= 0) {
diff --git a/frontend/providers/applaunchpad/src/pages/apps/components/appList.tsx b/frontend/providers/applaunchpad/src/pages/apps/components/appList.tsx
index bf9abf08969..42e84e27f30 100644
--- a/frontend/providers/applaunchpad/src/pages/apps/components/appList.tsx
+++ b/frontend/providers/applaunchpad/src/pages/apps/components/appList.tsx
@@ -195,7 +195,7 @@ const AppList = ({
}
+ leftIcon={}
px={3}
onClick={() => router.push(`/app/detail?name=${item.name}`)}
>
@@ -251,7 +251,7 @@ const AppList = ({
{
child: (
<>
-
+
{t('Restart')}
>
),
@@ -262,7 +262,7 @@ const AppList = ({
{
child: (
<>
-
+
{t('Delete')}
>
),
diff --git a/frontend/providers/applaunchpad/src/utils/deployYaml2Json.ts b/frontend/providers/applaunchpad/src/utils/deployYaml2Json.ts
index f208cf9dae2..c871636093f 100644
--- a/frontend/providers/applaunchpad/src/utils/deployYaml2Json.ts
+++ b/frontend/providers/applaunchpad/src/utils/deployYaml2Json.ts
@@ -1,5 +1,5 @@
import yaml from 'js-yaml';
-import type { AppEditType } from '@/types/app';
+import type { AppEditType, DeployKindsType } from '@/types/app';
import { strToBase64, str2Num, pathFormat, pathToNameFormat } from '@/utils/tools';
import { SEALOS_DOMAIN, INGRESS_SECRET } from '@/store/static';
import {
@@ -12,8 +12,16 @@ import {
deployPVCResizeKey
} from '@/constants/app';
import dayjs from 'dayjs';
+import jsonpatch, { Operation } from 'fast-json-patch';
+
+export const json2DeployCr = (
+ data: AppEditType,
+ type: 'deployment' | 'statefulset',
+ handleType: 'edit' | 'create' = 'create',
+ crYamlList?: DeployKindsType[]
+) => {
+ console.log(data, 'form data');
-export const json2DeployCr = (data: AppEditType, type: 'deployment' | 'statefulset') => {
const totalStorage = data.storeList.reduce((acc, item) => acc + item.value, 0);
const metadata = {
@@ -214,6 +222,91 @@ export const json2DeployCr = (data: AppEditType, type: 'deployment' | 'statefuls
}
};
+ const differentJsonPatch = jsonpatch.compare(template['deployment'], template['statefulset']);
+
+ // JSON Patch
+ const patch: Operation[] = [
+ { op: 'add', path: '/metadata', value: metadata },
+ { op: 'replace', path: '/spec/replicas', value: commonSpec.replicas },
+ { op: 'replace', path: '/spec/revisionHistoryLimit', value: commonSpec.revisionHistoryLimit },
+ { op: 'replace', path: '/spec/selector', value: commonSpec.selector },
+ { op: 'replace', path: '/spec/template/metadata/labels', value: templateMetadata.labels },
+ {
+ op: 'replace',
+ path: '/spec/template/spec/containers/0',
+ value: {
+ ...commonContainer,
+ volumeMounts: [...configMapVolumeMounts]
+ }
+ },
+ {
+ op: 'replace',
+ path: '/spec/template/spec/volumes',
+ value: [...configMapVolumes]
+ },
+ // gpu
+ {
+ op: 'replace',
+ path: '/spec/template/spec/restartPolicy',
+ value: gpuMap.restartPolicy
+ },
+ {
+ op: 'replace',
+ path: '/spec/template/spec/runtimeClassName',
+ value: gpuMap.runtimeClassName
+ },
+ {
+ op: 'replace',
+ path: '/spec/template/spec/nodeSelector',
+ value: gpuMap.nodeSelector
+ },
+ // status
+ {
+ op: 'remove',
+ path: '/status'
+ }
+ ];
+
+ const statefulsetPatch: Operation[] = [
+ {
+ op: 'replace',
+ path: '/spec/template/spec/containers/0',
+ value: {
+ ...commonContainer,
+ volumeMounts: [
+ ...configMapVolumeMounts,
+ ...data.storeList.map((item) => ({
+ name: item.name,
+ mountPath: item.path
+ }))
+ ]
+ }
+ }
+ ];
+
+ if (handleType === 'edit' && crYamlList) {
+ if (type === 'deployment') {
+ const originYaml = crYamlList.find((i) => i.kind === 'Deployment');
+ const cloneYaml = jsonpatch.deepClone(originYaml);
+ const updated = jsonpatch.applyPatch(cloneYaml, patch);
+ return yaml.dump(updated.newDocument);
+ } else {
+ let originStatefulSetYaml = crYamlList.find((i) => i.kind === 'StatefulSet');
+ const deploymentYaml = crYamlList.find((i) => i.kind === 'Deployment');
+
+ // handle deployment to StatefulSet
+ if (!originStatefulSetYaml && deploymentYaml?.metadata?.name) {
+ originStatefulSetYaml = jsonpatch.applyPatch(
+ jsonpatch.deepClone(deploymentYaml),
+ differentJsonPatch
+ ).newDocument;
+ }
+
+ const cloneYaml = jsonpatch.deepClone(originStatefulSetYaml);
+ const updated = jsonpatch.applyPatch(cloneYaml, patch.concat(statefulsetPatch));
+ return yaml.dump(updated.newDocument);
+ }
+ }
return yaml.dump(template[type]);
};
diff --git a/frontend/providers/applaunchpad/src/utils/tools.ts b/frontend/providers/applaunchpad/src/utils/tools.ts
index 5f41bcb5d2a..d2f7059f342 100644
--- a/frontend/providers/applaunchpad/src/utils/tools.ts
+++ b/frontend/providers/applaunchpad/src/utils/tools.ts
@@ -55,7 +55,11 @@ export const pathFormat = (str: string) => {
return `./${str}`;
};
export const pathToNameFormat = (str: string) => {
- return str.replace(/(\/|\.)/g, 'vn-').toLocaleLowerCase();
+ const endsWithSlash = str.endsWith('/');
+ const withoutTrailingSlash = endsWithSlash ? str.slice(0, -1) : str;
+ const replacedStr = withoutTrailingSlash.replace(/_/g, '-').replace(/[\/.]/g, 'vn-');
+
+ return endsWithSlash ? replacedStr : replacedStr.toLowerCase();
};
/**
@@ -355,6 +359,69 @@ export const patchYamlList = ({
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;
+};
+
/* request number limit */
export class RequestController {
results: any[] = [];
diff --git a/frontend/providers/dbprovider/src/pages/db/detail/components/AppBaseInfo.tsx b/frontend/providers/dbprovider/src/pages/db/detail/components/AppBaseInfo.tsx
index 16b58e275d2..1a9fedf3aa2 100644
--- a/frontend/providers/dbprovider/src/pages/db/detail/components/AppBaseInfo.tsx
+++ b/frontend/providers/dbprovider/src/pages/db/detail/components/AppBaseInfo.tsx
@@ -42,7 +42,6 @@ const AppBaseInfo = ({ db = defaultDBDetail }: { db: DBDetailType }) => {
const [isChecked, setIsChecked] = useState(false);
const { isOpen, onOpen, onClose } = useDisclosure();
const { toast } = useToast();
- const { dbPods } = useDBStore();
const supportConnectDB = useMemo(() => {
return !!['postgresql', 'mongodb', 'apecloud-mysql', 'redis'].find(
@@ -401,7 +400,7 @@ const AppBaseInfo = ({ db = defaultDBDetail }: { db: DBDetailType }) => {
0.5
/ {t('Hour')}
- {
>
免费方案
-
+ */}