feat(elasticsearch): add mapping, settings, stats and clear actions
This commit is contained in:
1 parent
a84cb637ba
commit
8f3134dce8
32 files changed
+1262
-3
No files matched your search
@@ -8,6 +8,7 @@ import { useSavedSqlStore } from "@/stores/savedSqlStore";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import type { ObjectSourceKind, QueryTab, TableInfo, TableNameFilter, TreeNode, TreeNodeType } from "@/types/database";
|
||||
import type { ElasticsearchIndexMetadataKind } from "@/lib/backend/tauri";
|
||||
import {
|
||||
createSidebarSearchSubtreePreserver,
|
||||
filterSidebarSearchRootsByConnectionState,
|
||||
@@ -79,7 +80,7 @@ import { createSidebarActionTarget, findSidebarActionTarget, matchesSidebarActio
|
||||
import { syncSidebarTreeNodeExpansion } from "@/lib/sidebar/sidebarTreeExpansion";
|
||||
import type { SidebarDangerDialogOption, SidebarDangerDialogRequest } from "@/lib/sidebar/sidebarDangerDialog";
|
||||
import { resetSidebarTreeDialogState, sidebarDangerRunningExecutionId } from "./sidebarTreeDialogState";
|
||||
import { SidebarDangerConfirmDialog, SidebarDdlViewDialog, SidebarObjectSourceDialog, SidebarProcedureExecutionDialog, SidebarVisibleDatabasesDialog, SidebarVisibleNacosNamespacesDialog, SidebarVisibleSchemasDialog } from "./sidebarAsyncDialogs";
|
||||
import { SidebarDangerConfirmDialog, SidebarDdlViewDialog, SidebarElasticsearchIndexMetadataDialog, SidebarObjectSourceDialog, SidebarProcedureExecutionDialog, SidebarVisibleDatabasesDialog, SidebarVisibleNacosNamespacesDialog, SidebarVisibleSchemasDialog } from "./sidebarAsyncDialogs";
|
||||
import { sortConnectionListForDisplay } from "@/lib/sidebar/connectionListSort";
|
||||
import { sidebarDisplayTableName } from "@/lib/sidebar/sidebarTableNameDisplay";
|
||||
import { alignedSidebarCommentLabelWidths, isSidebarCommentAlignableNode, sidebarTreeNaturalContentWidth, sidebarTreeNodeComment, usesFullWidthTreeLabel } from "@/lib/sidebar/sidebarTreeItemLayout";
|
||||
@@ -133,6 +134,8 @@ const sidebarTreeRuntime = createSidebarTreeRuntime();
|
||||
const sidebarTreeRuntimeInitialNode: TreeNode = { id: "__sidebar-runtime__", label: "", type: "connection-group" };
|
||||
const sidebarDdlTarget = ref<TreeNode | null>(null);
|
||||
const sidebarDdlOpen = ref(false);
|
||||
const sidebarElasticsearchIndexMetadataTarget = ref<{ node: TreeNode; kind: ElasticsearchIndexMetadataKind } | null>(null);
|
||||
const sidebarElasticsearchIndexMetadataOpen = ref(false);
|
||||
const sidebarObjectSourceTarget = ref<{ node: TreeNode; initialEditing: boolean } | null>(null);
|
||||
const sidebarObjectSourceOpen = ref(false);
|
||||
const sidebarProcedureTarget = ref<TreeNode | null>(null);
|
||||
@@ -1921,6 +1924,7 @@ async function openSidebarExtensionDetails(node: TreeNode) {
|
||||
function beginSidebarAction(): number {
|
||||
sidebarActionGeneration += 1;
|
||||
sidebarDdlOpen.value = false;
|
||||
sidebarElasticsearchIndexMetadataOpen.value = false;
|
||||
sidebarObjectSourceOpen.value = false;
|
||||
sidebarProcedureOpen.value = false;
|
||||
sidebarVisibleDatabasesOpen.value = false;
|
||||
@@ -1928,6 +1932,7 @@ function beginSidebarAction(): number {
|
||||
sidebarVisibleNacosNamespacesOpen.value = false;
|
||||
sidebarTableNameFilterOpen.value = false;
|
||||
sidebarDdlTarget.value = null;
|
||||
sidebarElasticsearchIndexMetadataTarget.value = null;
|
||||
sidebarObjectSourceTarget.value = null;
|
||||
sidebarProcedureTarget.value = null;
|
||||
sidebarVisibleDatabasesTarget.value = null;
|
||||
@@ -1958,6 +1963,13 @@ function openSidebarDdlForSelection(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
function openSidebarElasticsearchIndexMetadata(node: TreeNode, kind: ElasticsearchIndexMetadataKind) {
|
||||
if (!node.connectionId) return;
|
||||
beginSidebarAction();
|
||||
sidebarElasticsearchIndexMetadataTarget.value = { node: createSidebarActionTarget(node), kind };
|
||||
sidebarElasticsearchIndexMetadataOpen.value = true;
|
||||
}
|
||||
|
||||
function openSidebarObjectSource(node: TreeNode, initialEditing: boolean) {
|
||||
if (!node.connectionId || !node.database || !objectSourceTargetForTreeNode(node)) return;
|
||||
// TYPE/TYPE_BODY only have a source implementation on Xugu; PostgreSQL-family
|
||||
@@ -2114,6 +2126,10 @@ watch(sidebarDdlOpen, (open) => {
|
||||
if (!open) sidebarDdlTarget.value = null;
|
||||
});
|
||||
|
||||
watch(sidebarElasticsearchIndexMetadataOpen, (open) => {
|
||||
if (!open) sidebarElasticsearchIndexMetadataTarget.value = null;
|
||||
});
|
||||
|
||||
watch(sidebarObjectSourceOpen, (open) => {
|
||||
if (!open) sidebarObjectSourceTarget.value = null;
|
||||
});
|
||||
@@ -2356,6 +2372,7 @@ onUnmounted(() => {
|
||||
sidebarContextMenuTarget.value = null;
|
||||
sidebarContextMenuItems.value = [];
|
||||
sidebarDdlTarget.value = null;
|
||||
sidebarElasticsearchIndexMetadataTarget.value = null;
|
||||
sidebarObjectSourceTarget.value = null;
|
||||
sidebarProcedureTarget.value = null;
|
||||
sidebarVisibleDatabasesTarget.value = null;
|
||||
@@ -2394,6 +2411,7 @@ defineExpose({ focusSearch, createNewGroup, collapseAllTreeNodes, locateTabInSid
|
||||
@search-toggle="onSearchToggle"
|
||||
@node-toggled="onNodeToggled"
|
||||
@open-ddl="openSidebarDdl"
|
||||
@open-elasticsearch-index-metadata="openSidebarElasticsearchIndexMetadata"
|
||||
@open-object-source="openSidebarObjectSource"
|
||||
@open-procedure="openSidebarProcedure"
|
||||
@open-settings="openSidebarSettings"
|
||||
@@ -2598,6 +2616,14 @@ defineExpose({ focusSearch, createNewGroup, collapseAllTreeNodes, locateTabInSid
|
||||
:format-dialect="sqlFormatDialectForDbType(sidebarDdlDatabaseType)"
|
||||
/>
|
||||
|
||||
<SidebarElasticsearchIndexMetadataDialog
|
||||
v-if="sidebarElasticsearchIndexMetadataTarget"
|
||||
v-model:open="sidebarElasticsearchIndexMetadataOpen"
|
||||
:connection-id="sidebarElasticsearchIndexMetadataTarget.node.connectionId!"
|
||||
:index="sidebarElasticsearchIndexMetadataTarget.node.label"
|
||||
:kind="sidebarElasticsearchIndexMetadataTarget.kind"
|
||||
/>
|
||||
|
||||
<SidebarObjectSourceDialog
|
||||
v-if="sidebarObjectSourceTarget && sidebarObjectSourceType"
|
||||
v-model:open="sidebarObjectSourceOpen"
|
||||
@@ -2684,6 +2710,7 @@ defineExpose({ focusSearch, createNewGroup, collapseAllTreeNodes, locateTabInSid
|
||||
:details-text="sidebarDangerDialogRequest.detailsText"
|
||||
:confirm-label="sidebarDangerDialogRequest.confirmLabel"
|
||||
:loading="sidebarDangerDialogConfirming || sidebarDangerDialogRequest.loading"
|
||||
:confirm-disabled="sidebarDangerDialogRequest.confirmDisabled"
|
||||
:close-on-confirm="false"
|
||||
:cancelable="!!sidebarDangerDialogRequest.cancelRunning"
|
||||
:cancel-running-loading="sidebarDangerDialogCancelling"
|
||||
|
||||
@@ -11,7 +11,9 @@ import { useSidebarTreeToolRuntime } from "@/composables/useSidebarTreeToolRunti
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { translateBackendError } from "@/i18n/backend-errors";
|
||||
import {
|
||||
BarChart3,
|
||||
BookOpen,
|
||||
Braces,
|
||||
Database,
|
||||
ChevronsDown,
|
||||
FolderOpen,
|
||||
@@ -70,6 +72,7 @@ import { useToast } from "@/composables/useToast";
|
||||
import { useDatabaseOptions } from "@/composables/useDatabaseOptions";
|
||||
import type { ColumnInfo, DatabaseType, TreeNode, TreeNodeType } from "@/types/database";
|
||||
import * as api from "@/lib/backend/api";
|
||||
import type { ElasticsearchIndexMetadataKind } from "@/lib/backend/tauri";
|
||||
import { queryTimeoutSecsForConnection } from "@/lib/sql/queryTimeout";
|
||||
import { resolveDefaultDatabase } from "@/lib/database/defaultDatabase";
|
||||
import { connectionUsesVisibleSchemaFilter } from "@/lib/database/visibleDatabases";
|
||||
@@ -82,6 +85,7 @@ import { loadSidebarObjectGroup } from "@/lib/sidebar/sidebarObjectGroupRouting"
|
||||
import { isXuguTypeMemberContainer } from "@/lib/sidebar/xuguTypeMembers";
|
||||
import { isXuguSyntheticTreeNode } from "@/lib/sidebar/xuguPublicSynonyms";
|
||||
import { buildXuguSchedulerJobSql, type XuguSchedulerJobAction } from "@/lib/database/xuguSchedulerJobSql";
|
||||
import { elasticsearchClearIndexPreview, isElasticsearchClearConfirmed, isElasticsearchIndexPattern } from "@/lib/sidebar/elasticsearchIndexActions";
|
||||
import { mysqlObjectTemplateForGroup } from "@/lib/sidebar/mysqlObjectTemplates";
|
||||
import { buildTableDeleteTemplate, buildTableInsertTemplate, buildTableSelectTemplate, buildTableUpdateTemplate } from "@/lib/table/tableSqlTemplates";
|
||||
import { qualifiedTableName } from "@/lib/table/tableSelectSql";
|
||||
@@ -296,6 +300,9 @@ import {
|
||||
mongoIndexManagerSelectedName,
|
||||
mongoIndexManagerMode,
|
||||
mongoEditIndexOriginalName,
|
||||
showClearElasticsearchIndexConfirm,
|
||||
clearElasticsearchIndexLoading,
|
||||
clearElasticsearchIndexTypedName,
|
||||
showFlushRedisDbConfirm,
|
||||
showCreateSchemaDialog,
|
||||
createSchemaName,
|
||||
@@ -393,6 +400,7 @@ const emit = defineEmits<{
|
||||
"search-toggle": [node: TreeNode];
|
||||
"context-menu": [event: MouseEvent, node: TreeNode, items: ContextMenuItem[]];
|
||||
"open-ddl": [node: TreeNode];
|
||||
"open-elasticsearch-index-metadata": [node: TreeNode, kind: ElasticsearchIndexMetadataKind];
|
||||
"open-object-source": [node: TreeNode, initialEditing: boolean];
|
||||
"open-procedure": [node: TreeNode];
|
||||
"open-settings": [initialTab: string];
|
||||
@@ -527,6 +535,9 @@ const {
|
||||
dropMilvusCollection,
|
||||
dropMongoIndex,
|
||||
dropAllMongoIndexes,
|
||||
canManageElasticsearchIndex,
|
||||
clearElasticsearchIndex,
|
||||
confirmClearElasticsearchIndex,
|
||||
flushRedisDb,
|
||||
prepareRedisDatabaseAliasDialog,
|
||||
confirmRedisDatabaseAlias,
|
||||
@@ -1918,6 +1929,10 @@ async function openDdl() {
|
||||
emit("open-ddl", targets[0]!);
|
||||
}
|
||||
|
||||
function openElasticsearchIndexMetadata(kind: ElasticsearchIndexMetadataKind) {
|
||||
emit("open-elasticsearch-index-metadata", createSidebarActionTarget(activeNode.value), kind);
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
const node = activeNode.value;
|
||||
try {
|
||||
@@ -4555,6 +4570,51 @@ routeDangerDialog(showDropAllMongoIndexesConfirm, () =>
|
||||
}),
|
||||
);
|
||||
|
||||
routeDangerDialog(showClearElasticsearchIndexConfirm, () => {
|
||||
// Pin the label for the life of this dialog. `activeNode` follows the tree
|
||||
// selection, which the confirmation must not, and the typed-name gate below
|
||||
// has to compare against the index the operator actually opened.
|
||||
const index = activeNode.value.label;
|
||||
const isPattern = isElasticsearchIndexPattern(index);
|
||||
clearElasticsearchIndexTypedName.value = "";
|
||||
return dangerRequest({
|
||||
title: t("contextMenu.elasticsearchClearIndex"),
|
||||
// A grouped node is a wildcard covering many indexes, so it gets its own
|
||||
// wording rather than one that reads as a single index.
|
||||
message: t(isPattern ? "contextMenu.elasticsearchClearIndexPatternMessage" : "contextMenu.elasticsearchClearIndexMessage", { index }),
|
||||
detailsText: t("contextMenu.elasticsearchClearIndexDetails"),
|
||||
sql: elasticsearchClearIndexPreview(index),
|
||||
confirmLabel: t("contextMenu.elasticsearchClearIndexConfirm"),
|
||||
get loading() {
|
||||
return clearElasticsearchIndexLoading.value;
|
||||
},
|
||||
// A wildcard node clears every index it matches, so the pattern has to be
|
||||
// typed back before the button unlocks. Concrete index names skip this.
|
||||
...(isPattern
|
||||
? {
|
||||
textInput: {
|
||||
value: "",
|
||||
label: t("contextMenu.elasticsearchClearIndexTypeToConfirm", { index }),
|
||||
placeholder: index,
|
||||
onInput(value: string) {
|
||||
clearElasticsearchIndexTypedName.value = value;
|
||||
},
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
// Declared directly on this literal, never inside the spread above: object
|
||||
// spread evaluates an accessor once and copies the resulting value, which
|
||||
// would freeze the gate shut at its construction-time answer. Concrete
|
||||
// index names need no gate, and `isElasticsearchClearConfirmed` already
|
||||
// returns true for them, so this stays unlocked for them.
|
||||
get confirmDisabled() {
|
||||
return !isElasticsearchClearConfirmed(index, clearElasticsearchIndexTypedName.value);
|
||||
},
|
||||
closeOnConfirm: false,
|
||||
confirm: confirmClearElasticsearchIndex,
|
||||
});
|
||||
});
|
||||
|
||||
routeDangerDialog(showFlushRedisDbConfirm, () =>
|
||||
dangerRequest({
|
||||
title: t("redis.flushDb"),
|
||||
@@ -5428,6 +5488,14 @@ function buildSpecialSidebarMenu(context: SidebarMenuFactoryContext): boolean {
|
||||
if (canRenameMongoCollection.value) {
|
||||
items.push({ label: t("contextMenu.renameObject"), action: openRenameMongoCollectionDialog, icon: Pencil, shortcut: shortcutRename });
|
||||
}
|
||||
if (canManageElasticsearchIndex.value) {
|
||||
items.push({ label: "", separator: true });
|
||||
items.push({ label: t("contextMenu.elasticsearchViewMapping"), action: () => openElasticsearchIndexMetadata("mapping"), icon: Braces });
|
||||
items.push({ label: t("contextMenu.elasticsearchViewSettings"), action: () => openElasticsearchIndexMetadata("settings"), icon: Settings2 });
|
||||
items.push({ label: t("contextMenu.elasticsearchViewStats"), action: () => openElasticsearchIndexMetadata("stats"), icon: BarChart3 });
|
||||
items.push({ label: "", separator: true });
|
||||
items.push({ label: t("contextMenu.elasticsearchClearIndex"), action: clearElasticsearchIndex, icon: Eraser, variant: "destructive" as const });
|
||||
}
|
||||
if (canDropMilvusCollection.value) {
|
||||
items.push({ label: "", separator: true });
|
||||
items.push({ label: t("contextMenu.dropCollection"), action: dropMilvusCollection, icon: Trash2, shortcut: shortcutDelete, variant: "destructive" as const });
|
||||
|
||||
apps/desktop/src/components/sidebar/__tests__/SidebarTreeRuntimeHost.elasticsearchClearGuard.spec.ts
Vendored
+64
@@ -0,0 +1,64 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const runtimeHostSource = readFileSync(new URL("../SidebarTreeRuntimeHost.vue", import.meta.url), "utf8");
|
||||
const connectionTreeSource = readFileSync(new URL("../ConnectionTree.vue", import.meta.url), "utf8");
|
||||
const dangerDialogSource = readFileSync(new URL("../../editor/DangerConfirmDialog.vue", import.meta.url), "utf8");
|
||||
|
||||
/**
|
||||
* `isElasticsearchClearConfirmed` is unit-tested on its own, but a correct pure
|
||||
* function is worthless if the dialog never consults it. These assertions pin
|
||||
* the wiring that connects the two, end to end through the shared dialog.
|
||||
*/
|
||||
describe("Elasticsearch clear-index wildcard guard wiring", () => {
|
||||
it("gates the confirm button on the typed pattern for wildcard nodes only", () => {
|
||||
const clearRoute = runtimeHostSource.slice(runtimeHostSource.indexOf("routeDangerDialog(showClearElasticsearchIndexConfirm"));
|
||||
expect(clearRoute).toContain("const isPattern = isElasticsearchIndexPattern(index)");
|
||||
expect(clearRoute).toMatch(/\.\.\.\(isPattern\s*\?\s*\{/);
|
||||
});
|
||||
|
||||
/**
|
||||
* Regression: the gate once lived inside the `...(isPattern ? {…} : {})`
|
||||
* spread. Object spread *invokes* an accessor and copies the resulting value
|
||||
* as a plain data property, so the getter answered once at construction time
|
||||
* — while the input was still empty — and the confirm button stayed disabled
|
||||
* no matter what was typed. The wording read correctly and the getter was
|
||||
* present in the source the whole time, so only its position catches this.
|
||||
*/
|
||||
it("declares confirmDisabled outside the spread, so it stays a live getter", () => {
|
||||
const clearRoute = runtimeHostSource.slice(runtimeHostSource.indexOf("routeDangerDialog(showClearElasticsearchIndexConfirm"), runtimeHostSource.indexOf("routeDangerDialog(showFlushRedisDbConfirm"));
|
||||
const spreadStart = clearRoute.indexOf("...(isPattern");
|
||||
const spreadEnd = clearRoute.indexOf(": {}),", spreadStart);
|
||||
expect(spreadStart).toBeGreaterThan(-1);
|
||||
expect(spreadEnd).toBeGreaterThan(spreadStart);
|
||||
// Nothing spread into the request may be an accessor.
|
||||
expect(clearRoute.slice(spreadStart, spreadEnd)).not.toMatch(/\bget\s+\w+\s*\(/);
|
||||
// It has to sit directly on the request literal instead.
|
||||
expect(clearRoute.slice(spreadEnd)).toMatch(/get confirmDisabled\(\) \{\s*return !isElasticsearchClearConfirmed\(index, clearElasticsearchIndexTypedName\.value\);/);
|
||||
});
|
||||
|
||||
it("pins the index label for the life of the dialog instead of following the tree selection", () => {
|
||||
const clearRoute = runtimeHostSource.slice(runtimeHostSource.indexOf("routeDangerDialog(showClearElasticsearchIndexConfirm"), runtimeHostSource.indexOf("routeDangerDialog(showFlushRedisDbConfirm"));
|
||||
expect(clearRoute).toContain("const index = activeNode.value.label;");
|
||||
// Reading activeNode again inside the request would reintroduce the drift.
|
||||
expect(clearRoute.match(/activeNode\.value/g)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("clears any previously typed name when the dialog is opened again", () => {
|
||||
const clearRoute = runtimeHostSource.slice(runtimeHostSource.indexOf("routeDangerDialog(showClearElasticsearchIndexConfirm"), runtimeHostSource.indexOf("routeDangerDialog(showFlushRedisDbConfirm"));
|
||||
expect(clearRoute).toContain('clearElasticsearchIndexTypedName.value = "";');
|
||||
});
|
||||
|
||||
it("forwards confirmDisabled from the sidebar request to the shared danger dialog", () => {
|
||||
expect(connectionTreeSource).toContain(':confirm-disabled="sidebarDangerDialogRequest.confirmDisabled"');
|
||||
});
|
||||
|
||||
it("honours confirmDisabled on both the button and the confirm handler", () => {
|
||||
expect(dangerDialogSource).toContain(':disabled="loading || confirmDisabled"');
|
||||
expect(dangerDialogSource).toMatch(/function onConfirm\(\) \{[\s\S]*?if \(props\.loading \|\| props\.confirmDisabled\) return;/);
|
||||
});
|
||||
|
||||
it("leaves confirmDisabled off by default so existing danger dialogs are unaffected", () => {
|
||||
expect(dangerDialogSource).toContain("confirmDisabled: false,");
|
||||
});
|
||||
});
|
||||
@@ -22,5 +22,6 @@ export const SidebarVisibleNacosNamespacesDialog = lazySidebarDialog(() => impor
|
||||
export const SidebarVisibleSchemasDialog = lazySidebarDialog(() => import("@/components/sidebar/VisibleSchemasDialog.vue"));
|
||||
export const SidebarDdlViewDialog = lazySidebarDialog(() => import("@/components/objects/DdlViewDialog.vue"));
|
||||
export const SidebarObjectSourceDialog = lazySidebarDialog(() => import("@/components/objects/ObjectSourceDialog.vue"));
|
||||
export const SidebarElasticsearchIndexMetadataDialog = lazySidebarDialog(() => import("@/components/objects/ElasticsearchIndexMetadataDialog.vue"));
|
||||
export const SidebarProcedureExecutionDialog = lazySidebarDialog(() => import("@/components/objects/ProcedureExecutionDialog.vue"));
|
||||
export const SidebarMongoIndexManagerDialog = lazySidebarDialog(() => import("@/components/sidebar/MongoIndexManagerDialog.vue"));
|
||||
@@ -163,6 +163,10 @@ export function resetMongoIndexManager() {
|
||||
mongoIndexManagerMode.value = "view";
|
||||
mongoEditIndexOriginalName.value = "";
|
||||
}
|
||||
export const showClearElasticsearchIndexConfirm = ref(false);
|
||||
export const clearElasticsearchIndexLoading = ref(false);
|
||||
/** Name typed back by the operator before a wildcard index node may be cleared. */
|
||||
export const clearElasticsearchIndexTypedName = ref("");
|
||||
export const showFlushRedisDbConfirm = ref(false);
|
||||
export const showRedisDatabaseAliasDialog = ref(false);
|
||||
export const redisDatabaseAliasInput = ref("");
|
||||
@@ -213,6 +217,7 @@ const openFlags = [
|
||||
showDropAllMongoIndexesConfirm,
|
||||
showCreateMongoIndexDialog,
|
||||
showMongoIndexManagerDialog,
|
||||
showClearElasticsearchIndexConfirm,
|
||||
showFlushRedisDbConfirm,
|
||||
showRedisDatabaseAliasDialog,
|
||||
showCreateSchemaDialog,
|
||||
@@ -232,6 +237,8 @@ export function resetSidebarTreeDialogState() {
|
||||
createDatabasePreviewSql.value = "";
|
||||
createDatabaseAuthorizationResults.value = [];
|
||||
createDatabaseAuthorizationApplying.value = false;
|
||||
clearElasticsearchIndexLoading.value = false;
|
||||
clearElasticsearchIndexTypedName.value = "";
|
||||
redisDatabaseAliasInput.value = "";
|
||||
redisDatabaseAliasSaving.value = false;
|
||||
cloneMongoCollectionName.value = "";
|
||||
|
||||
Reference in new issue
Block a user