feat(nacos): add Nacos management console (#1456)
This commit is contained in:
62 files changed
+7314
-55
No files matched your search
@@ -81,7 +81,7 @@ const isFiltering = computed(() => !!searchQuery.value.trim() || hasSearchScopeF
|
||||
|
||||
const SEARCH_SCOPE_TO_NODE_TYPES: Record<SearchScope, TreeNodeType[]> = {
|
||||
connection: ["connection"],
|
||||
database: ["database", "redis-db", "mq-tenant", "mongo-db"],
|
||||
database: ["database", "redis-db", "mq-tenant", "nacos-namespace", "mongo-db"],
|
||||
schema: ["schema"],
|
||||
table: ["table", "mongo-collection", "vector-collection", "elasticsearch-index"],
|
||||
view: ["view"],
|
||||
@@ -340,6 +340,8 @@ async function ensureTreeLoadedForTarget(target: ActiveTabSidebarTarget, opts?:
|
||||
await store.loadVectorCollections(connId);
|
||||
} else if (config.db_type === "mq") {
|
||||
await store.loadMqTenants(connId, loadOptions);
|
||||
} else if (config.db_type === "nacos") {
|
||||
await store.loadNacosNamespaces(connId, loadOptions);
|
||||
} else {
|
||||
await store.loadDatabases(connId, loadOptions);
|
||||
}
|
||||
@@ -348,7 +350,7 @@ async function ensureTreeLoadedForTarget(target: ActiveTabSidebarTarget, opts?:
|
||||
}
|
||||
}
|
||||
|
||||
if (config.db_type === "mq") return;
|
||||
if (config.db_type === "mq" || config.db_type === "nacos") return;
|
||||
if (!("database" in target) || !target.database) return;
|
||||
|
||||
// Find the database node
|
||||
|
||||
@@ -246,6 +246,8 @@ function getIconInfo(node: TreeNode): { icon: any; colorClass: string } | null {
|
||||
return { icon: Database, colorClass: "text-red-400" };
|
||||
case "mq-tenant":
|
||||
return { icon: FolderOpen, colorClass: "text-sky-400" };
|
||||
case "nacos-namespace":
|
||||
return { icon: FolderOpen, colorClass: "text-sky-500" };
|
||||
case "etcd-root":
|
||||
return { icon: Database, colorClass: "text-sky-500" };
|
||||
case "mongo-db":
|
||||
@@ -468,6 +470,8 @@ async function toggle() {
|
||||
await connectionStore.loadVectorCollections(node.connectionId);
|
||||
} else if (config?.db_type === "mq") {
|
||||
await connectionStore.loadMqTenants(node.connectionId);
|
||||
} else if (config?.db_type === "nacos") {
|
||||
await connectionStore.loadNacosNamespaces(node.connectionId);
|
||||
} else {
|
||||
await connectionStore.loadDatabases(node.connectionId);
|
||||
}
|
||||
@@ -476,6 +480,8 @@ async function toggle() {
|
||||
queryStore.createTab(node.connectionId, node.database, tabTitle, "redis");
|
||||
} else if (node.type === "mq-tenant" && node.connectionId) {
|
||||
queryStore.openMqAdmin(node.connectionId, { tenant: node.mqTenant || node.label });
|
||||
} else if (node.type === "nacos-namespace" && node.connectionId) {
|
||||
queryStore.openNacosAdmin(node.connectionId, { namespace: node.nacosNamespace || "", namespaceName: node.nacosNamespaceName || node.label });
|
||||
} else if (node.type === "etcd-root" && node.connectionId) {
|
||||
const tabTitle = `${connectionStore.getConfig(node.connectionId)?.name || "etcd"}:keys`;
|
||||
queryStore.createTab(node.connectionId, "", tabTitle, "etcd");
|
||||
@@ -1371,6 +1377,15 @@ const showCreateDatabaseDialog = ref(false);
|
||||
const createDatabaseName = ref("");
|
||||
const createDatabaseCharset = ref("utf8mb4");
|
||||
const createDatabaseCollation = ref("utf8mb4_unicode_ci");
|
||||
const showCreateNacosNamespaceDialog = ref(false);
|
||||
const createNacosNamespaceId = ref("");
|
||||
const createNacosNamespaceName = ref("");
|
||||
const createNacosNamespaceDesc = ref("");
|
||||
const createNacosNamespaceLoading = ref(false);
|
||||
const showEditNacosNamespaceDialog = ref(false);
|
||||
const editNacosNamespaceName = ref("");
|
||||
const editNacosNamespaceDesc = ref("");
|
||||
const editNacosNamespaceLoading = ref(false);
|
||||
const fallbackCreateDatabaseCharset = fallbackCreateDatabaseCharsetMetadata();
|
||||
const createDatabaseCharsetOptions = ref<string[]>(fallbackCreateDatabaseCharset.charsets);
|
||||
const createDatabaseCollationsByCharset = ref<Record<string, string[]>>(fallbackCreateDatabaseCharset.collationsByCharset);
|
||||
@@ -1851,6 +1866,17 @@ const canCreateDatabase = computed(() => {
|
||||
return props.node.type === "connection" && (supportsDatabaseCreation(config?.db_type) || config?.db_type === "duckdb" || (config?.db_type === "mongodb" && config.driver_profile !== "mongodb-legacy"));
|
||||
});
|
||||
|
||||
const canCreateNacosNamespace = computed(() => {
|
||||
const config = props.node.connectionId ? connectionStore.getConfig(props.node.connectionId) : undefined;
|
||||
return props.node.type === "connection" && config?.db_type === "nacos" && !config.read_only;
|
||||
});
|
||||
|
||||
const canEditNacosNamespace = computed(() => {
|
||||
if (props.node.type !== "nacos-namespace" || !props.node.connectionId || !props.node.nacosNamespace) return false;
|
||||
const config = connectionStore.getConfig(props.node.connectionId);
|
||||
return config?.db_type === "nacos" && !config.read_only;
|
||||
});
|
||||
|
||||
const isDuckDbConnection = computed(() => {
|
||||
const config = props.node.connectionId ? connectionStore.getConfig(props.node.connectionId) : undefined;
|
||||
return props.node.type === "connection" && config?.db_type === "duckdb";
|
||||
@@ -2037,6 +2063,63 @@ async function loadCreateDatabaseCharsetMetadata() {
|
||||
}
|
||||
}
|
||||
|
||||
function openCreateNacosNamespaceDialog() {
|
||||
createNacosNamespaceId.value = "";
|
||||
createNacosNamespaceName.value = "";
|
||||
createNacosNamespaceDesc.value = "";
|
||||
showCreateNacosNamespaceDialog.value = true;
|
||||
}
|
||||
|
||||
async function confirmCreateNacosNamespace() {
|
||||
const node = props.node;
|
||||
const namespaceName = createNacosNamespaceName.value.trim();
|
||||
if (!node.connectionId || !namespaceName || createNacosNamespaceLoading.value) return;
|
||||
createNacosNamespaceLoading.value = true;
|
||||
try {
|
||||
await api.nacosCreateNamespace(node.connectionId, {
|
||||
namespaceId: createNacosNamespaceId.value.trim() || undefined,
|
||||
namespaceName,
|
||||
namespaceDesc: createNacosNamespaceDesc.value.trim() || namespaceName,
|
||||
});
|
||||
showCreateNacosNamespaceDialog.value = false;
|
||||
await connectionStore.loadNacosNamespaces(node.connectionId, { force: true });
|
||||
node.isExpanded = true;
|
||||
toast(t("nacos.namespaceCreated", { name: namespaceName }), 3000);
|
||||
} catch (e: any) {
|
||||
toast(t("contextMenu.tableOperationFailed", { message: translateBackendError(t, e?.message || String(e)) }), 5000);
|
||||
} finally {
|
||||
createNacosNamespaceLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openEditNacosNamespaceDialog() {
|
||||
editNacosNamespaceName.value = props.node.nacosNamespaceName || props.node.label;
|
||||
editNacosNamespaceDesc.value = props.node.comment || "";
|
||||
showEditNacosNamespaceDialog.value = true;
|
||||
}
|
||||
|
||||
async function confirmEditNacosNamespace() {
|
||||
const node = props.node;
|
||||
const namespaceId = node.nacosNamespace?.trim() || "";
|
||||
const namespaceName = editNacosNamespaceName.value.trim();
|
||||
if (!node.connectionId || !namespaceId || !namespaceName || editNacosNamespaceLoading.value) return;
|
||||
editNacosNamespaceLoading.value = true;
|
||||
try {
|
||||
await api.nacosUpdateNamespace(node.connectionId, {
|
||||
namespaceId,
|
||||
namespaceName,
|
||||
namespaceDesc: editNacosNamespaceDesc.value.trim() || namespaceName,
|
||||
});
|
||||
showEditNacosNamespaceDialog.value = false;
|
||||
await connectionStore.loadNacosNamespaces(node.connectionId, { force: true });
|
||||
toast(t("nacos.namespaceUpdated", { name: namespaceName }), 3000);
|
||||
} catch (e: any) {
|
||||
toast(t("contextMenu.tableOperationFailed", { message: translateBackendError(t, e?.message || String(e)) }), 5000);
|
||||
} finally {
|
||||
editNacosNamespaceLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function ensureDuckDbFileExtension(path: string): string {
|
||||
return /\.(duckdb|db)$/i.test(path) ? path : `${path}.duckdb`;
|
||||
}
|
||||
@@ -2861,7 +2944,7 @@ const nodeIconClass = computed(() => {
|
||||
const canConfigureVisibleDatabases = computed(() => {
|
||||
if (props.node.type !== "connection" || !props.node.connectionId) return false;
|
||||
const dbType = connectionStore.getConfig(props.node.connectionId)?.db_type;
|
||||
return dbType !== "elasticsearch" && dbType !== "qdrant" && dbType !== "milvus" && dbType !== "etcd" && dbType !== "mq";
|
||||
return dbType !== "elasticsearch" && dbType !== "qdrant" && dbType !== "milvus" && dbType !== "etcd" && dbType !== "mq" && dbType !== "nacos";
|
||||
});
|
||||
const canCopyFinalProxyPort = computed(() => {
|
||||
if (props.node.type !== "connection" || !props.node.connectionId) return false;
|
||||
@@ -3273,6 +3356,13 @@ function treeItemMenuItems(): ContextMenuItem[] {
|
||||
icon: Plus,
|
||||
});
|
||||
}
|
||||
if (canCreateNacosNamespace.value) {
|
||||
items.push({
|
||||
label: t("nacos.createNamespace"),
|
||||
action: openCreateNacosNamespaceDialog,
|
||||
icon: FolderPlus,
|
||||
});
|
||||
}
|
||||
items.push({ label: "", separator: true });
|
||||
if (availableGroups.value.length > 0 || currentGroupId.value) {
|
||||
const groupChildren: ContextMenuItem[] = availableGroups.value.map((group: { id: string; name: string }) => ({
|
||||
@@ -3455,6 +3545,22 @@ function treeItemMenuItems(): ContextMenuItem[] {
|
||||
return items;
|
||||
}
|
||||
|
||||
if (node.type === "nacos-namespace") {
|
||||
items.push({ label: t("contextMenu.openConnection"), action: toggle, icon: FolderOpen });
|
||||
if (canEditNacosNamespace.value) {
|
||||
items.push({ label: t("nacos.editNamespace"), action: openEditNacosNamespaceDialog, icon: Pencil });
|
||||
}
|
||||
items.push({
|
||||
label: t("contextMenu.refreshChildren"),
|
||||
action: refresh,
|
||||
icon: RefreshCw,
|
||||
shortcut: shortcutRefresh,
|
||||
});
|
||||
items.push({ label: "", separator: true });
|
||||
items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
|
||||
return items;
|
||||
}
|
||||
|
||||
if (node.type === "mongo-collection") {
|
||||
items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
|
||||
items.push({ label: "", separator: true });
|
||||
@@ -4004,6 +4110,58 @@ function treeItemMenuItems(): ContextMenuItem[] {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:open="showCreateNacosNamespaceDialog">
|
||||
<DialogContent class="sm:max-w-[420px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ t("nacos.createNamespace") }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div class="grid gap-3">
|
||||
<div class="grid gap-1.5">
|
||||
<label class="text-xs font-medium text-muted-foreground">{{ t("nacos.namespaceId") }}</label>
|
||||
<Input v-model="createNacosNamespaceId" :placeholder="t('nacos.namespaceIdPlaceholder')" @keydown.enter.prevent="confirmCreateNacosNamespace" />
|
||||
</div>
|
||||
<div class="grid gap-1.5">
|
||||
<label class="text-xs font-medium text-muted-foreground">{{ t("nacos.namespaceName") }}</label>
|
||||
<Input v-model="createNacosNamespaceName" :placeholder="t('nacos.namespaceNamePlaceholder')" @keydown.enter.prevent="confirmCreateNacosNamespace" />
|
||||
</div>
|
||||
<div class="grid gap-1.5">
|
||||
<label class="text-xs font-medium text-muted-foreground">{{ t("nacos.namespaceDesc") }}</label>
|
||||
<Input v-model="createNacosNamespaceDesc" :placeholder="t('nacos.namespaceDescPlaceholder')" @keydown.enter.prevent="confirmCreateNacosNamespace" />
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" :disabled="createNacosNamespaceLoading" @click="showCreateNacosNamespaceDialog = false">{{ t("dangerDialog.cancel") }}</Button>
|
||||
<Button :disabled="!createNacosNamespaceName.trim() || createNacosNamespaceLoading" @click="confirmCreateNacosNamespace">
|
||||
{{ createNacosNamespaceLoading ? t("nacos.creatingNamespace") : t("dangerDialog.confirm") }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:open="showEditNacosNamespaceDialog">
|
||||
<DialogContent class="sm:max-w-[420px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ t("nacos.editNamespace") }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div class="grid gap-3">
|
||||
<div class="grid gap-1.5">
|
||||
<label class="text-xs font-medium text-muted-foreground">{{ t("nacos.namespaceName") }}</label>
|
||||
<Input v-model="editNacosNamespaceName" :placeholder="t('nacos.namespaceNamePlaceholder')" @keydown.enter.prevent="confirmEditNacosNamespace" />
|
||||
</div>
|
||||
<div class="grid gap-1.5">
|
||||
<label class="text-xs font-medium text-muted-foreground">{{ t("nacos.namespaceDesc") }}</label>
|
||||
<Input v-model="editNacosNamespaceDesc" :placeholder="t('nacos.namespaceDescPlaceholder')" @keydown.enter.prevent="confirmEditNacosNamespace" />
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" :disabled="editNacosNamespaceLoading" @click="showEditNacosNamespaceDialog = false">{{ t("dangerDialog.cancel") }}</Button>
|
||||
<Button :disabled="!editNacosNamespaceName.trim() || editNacosNamespaceLoading" @click="confirmEditNacosNamespace">
|
||||
{{ editNacosNamespaceLoading ? t("nacos.updatingNamespace") : t("dangerDialog.confirm") }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<DangerConfirmDialog
|
||||
v-model:open="showDropDatabaseConfirm"
|
||||
:title="t('contextMenu.confirmDropDatabaseTitle')"
|
||||
|
||||
Reference in new issue
Block a user