feat: support mongodb management (#12486)

#### What this PR does / why we need it?

Refs https://github.com/1Panel-dev/1Panel/issues/7759

#### Summary of your change

#### Please indicate you've done the following:

- [ ] Made sure tests are passing and test coverage is added if needed.
- [ ] Made sure commit message follow the rule of [Conventional Commits specification](https://www.conventionalcommits.org/).
- [ ] Considered the docs impact and opened a new docs issue or PR with docs changes if needed.
This commit is contained in:
王贺
2026-04-14 10:40:44 +00:00
committed by GitHub
parent 8362578355
commit 394339d7dc
52 changed files with 4338 additions and 31 deletions
+29 -7
View File
@@ -3,7 +3,7 @@
v-model="open"
:header="$t('menu.terminal')"
@close="handleClose"
:resource="database"
:resource="resourceName"
:autoClose="!open"
size="large"
:fullScreen="true"
@@ -22,14 +22,32 @@ const open = ref(false);
const terminalRef = ref<InstanceType<typeof Terminal> | null>(null);
const database = ref();
const databaseType = ref();
const resourceName = ref();
const command = ref('/bin/sh');
const user = ref('');
const containerID = ref('');
const initCmd = ref('');
const waitForPrompt = ref('');
interface DialogProps {
databaseType: string;
database: string;
databaseType?: string;
database?: string;
resourceName?: string;
command?: string;
user?: string;
containerID?: string;
initCmd?: string;
waitForPrompt?: string;
}
const acceptParams = async (params: DialogProps): Promise<void> => {
database.value = params.database;
databaseType.value = params.databaseType;
database.value = params.database || '';
databaseType.value = params.databaseType || '';
resourceName.value = params.resourceName || params.database || params.containerID || '';
command.value = params.command || '/bin/sh';
user.value = params.user || '';
containerID.value = params.containerID || '';
initCmd.value = params.initCmd || '';
waitForPrompt.value = params.waitForPrompt || '';
open.value = false;
await initTerm();
};
@@ -37,11 +55,15 @@ const acceptParams = async (params: DialogProps): Promise<void> => {
const initTerm = async () => {
open.value = true;
await nextTick();
const args = containerID.value
? `source=container&containerid=${containerID.value}&user=${user.value}&command=${command.value}`
: `source=database&databaseType=${databaseType.value}&database=${database.value}`;
terminalRef.value!.acceptParams({
endpoint: '/api/v2/containers/exec',
args: `source=database&databaseType=${databaseType.value}&database=${database.value}`,
args,
error: '',
initCmd: '',
initCmd: initCmd.value,
waitForPrompt: waitForPrompt.value,
});
};
+52 -6
View File
@@ -34,6 +34,10 @@ const terminalSocket = ref<WebSocket>();
const heartbeatTimer = ref<NodeJS.Timer>();
const latency = ref(0);
const initCmd = ref('');
const hideInitCmdEcho = ref(false);
const initCmdEchoBuffer = ref('');
const waitForPrompt = ref('');
const waitForPromptBuffer = ref('');
const aiNotice = ref({
visible: false,
loading: false,
@@ -105,6 +109,7 @@ interface WsProps {
args: string;
error: string;
initCmd: string;
waitForPrompt?: string;
}
interface TerminalBufferLine {
@@ -117,6 +122,8 @@ const acceptParams = (props: WsProps) => {
initError(props.error);
} else {
initCmd.value = props.initCmd || '';
waitForPrompt.value = props.waitForPrompt || '';
waitForPromptBuffer.value = '';
init(props.endpoint, props.args);
}
});
@@ -258,10 +265,46 @@ const initWebSocket = (endpoint_: string, args: string = '') => {
const runRealTerminal = () => {
webSocketReady.value = true;
if (initCmd.value !== '') {
hideInitCmdEcho.value = true;
initCmdEchoBuffer.value = '';
sendMsg(initCmd.value);
}
};
const stripInitCmdEchoLine = (message: string) => {
if (!hideInitCmdEcho.value) {
return message;
}
initCmdEchoBuffer.value += message;
const lineBreakIndex = initCmdEchoBuffer.value.search(/\r?\n/);
if (lineBreakIndex === -1) {
return '';
}
const lineBreakLength = initCmdEchoBuffer.value[lineBreakIndex] === '\r' ? 2 : 1;
const remaining = initCmdEchoBuffer.value.slice(lineBreakIndex + lineBreakLength);
hideInitCmdEcho.value = false;
initCmdEchoBuffer.value = '';
initCmd.value = '';
return remaining;
};
const flushPromptBuffer = (message: string) => {
if (!waitForPrompt.value) {
return message;
}
waitForPromptBuffer.value += message;
const promptIndex = waitForPromptBuffer.value.indexOf(waitForPrompt.value);
if (promptIndex === -1) {
return '';
}
const visible = waitForPromptBuffer.value.slice(promptIndex);
waitForPrompt.value = '';
waitForPromptBuffer.value = '';
return visible;
};
const onWSReceive = (message: MessageEvent) => {
const wsMsg = JSON.parse(message.data);
switch (wsMsg.type) {
@@ -269,9 +312,14 @@ const onWSReceive = (message: MessageEvent) => {
term.value.element && term.value.focus();
if (wsMsg.data) {
let receiveMsg = decodeBase64(wsMsg.data);
if (initCmd.value != '') {
receiveMsg = receiveMsg?.replace(initCmd.value.trim(), '').trim();
initCmd.value = '';
if (hideInitCmdEcho.value) {
receiveMsg = stripInitCmdEchoLine(receiveMsg);
}
if (receiveMsg && waitForPrompt.value) {
receiveMsg = flushPromptBuffer(receiveMsg);
}
if (!receiveMsg) {
break;
}
term.value.write(receiveMsg);
}
@@ -507,9 +555,7 @@ onBeforeUnmount(() => {
.ai-notice-fade-enter-active,
.ai-notice-fade-leave-active {
transition:
opacity 180ms ease,
transform 180ms ease;
transition: opacity 180ms ease, transform 180ms ease;
}
.ai-mask-fade-enter-active,
+15 -6
View File
@@ -16,7 +16,7 @@
<li v-if="type === 'mysql' || type === 'mariadb' || type === 'mysql-cluster'">
{{ $t('database.formatHelper', [remark]) }}
</li>
<li v-if="isDb()">{{ $t('database.supportUpType') }}</li>
<li v-if="isDb() && type !== 'mongodb'">{{ $t('database.supportUpType') }}</li>
<li v-if="type === 'website' || type === 'app'">
{{ $t('website.websiteBackupWarn') }}
</li>
@@ -40,7 +40,7 @@
:limit="1"
class="float-left"
ref="uploadRef"
accept=".tar.gz,.sql,.gz,.zip"
:accept="uploadAccept"
:show-file-list="false"
:on-exceed="handleExceed"
:on-change="fileOnChange"
@@ -90,7 +90,7 @@
<DialogPro
v-model="recoverDialog"
:title="name ? $t('commons.button.recover') + ' - ' + name : $t('commons.button.recover')"
:title="title ? $t('commons.button.recover') + ' - ' + title : $t('commons.button.recover')"
@close="handleRecoverClose"
size="small"
>
@@ -134,7 +134,7 @@
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { computed, reactive, ref } from 'vue';
import { computeSize } from '@/utils/size';
import { newUUID } from '@/utils/id';
import { transferTimeToSecond } from '@/utils/validate';
@@ -198,6 +198,7 @@ const acceptParams = async (params: DialogProps): Promise<void> => {
case 'postgresql':
case 'mysql-cluster':
case 'postgresql-cluster':
case 'mongodb':
title.value = name.value + ' [ ' + detailName.value + ' ]';
if (detailName.value) {
baseDir.value = `${pathRes.data}/uploads/database/${type.value}/${name.value}/${detailName.value}/`;
@@ -253,10 +254,14 @@ const beforeUpload = (fileName: string) => {
return false;
}
if (isDb()) {
const allowedExtensions = ['.sql', '.sql.gz', '.tar.gz', '.zip'];
const allowedExtensions = type.value === 'mongodb' ? ['.gz'] : ['.sql', '.sql.gz', '.tar.gz', '.zip'];
const isValidFile = allowedExtensions.some((ext) => itemName.endsWith(ext));
if (!isValidFile) {
MsgError(i18n.global.t('database.supportUpType'));
MsgError(
type.value === 'mongodb'
? i18n.global.t('commons.msg.unSupportType')
: i18n.global.t('database.supportUpType'),
);
return false;
}
return true;
@@ -340,6 +345,7 @@ const onRecover = async (row: File.File) => {
const isDb = () => {
return (
type.value === 'mongodb' ||
type.value === 'mysql' ||
type.value === 'mariadb' ||
type.value === 'postgresql' ||
@@ -347,6 +353,9 @@ const isDb = () => {
type.value === 'postgresql-cluster'
);
};
const uploadAccept = computed(() => {
return type.value === 'mongodb' ? '.gz' : '.tar.gz,.sql,.gz,.zip';
});
const uploaderFiles = ref<UploadFiles>([]);
const uploadRef = ref<UploadInstance>();