feat(dameng): add user and role administration

This commit is contained in:
GIWTO authored and GitHub committed 2026-08-12 13:35:03 +08:00
1 parent 9bacb752e4
commit ec4f8582e6
25 files changed
+3435 -4

No files matched your search

@@ -0,0 +1,536 @@
<script setup lang="ts">
import { computed, onMounted, ref } from "vue";
import { useI18n } from "vue-i18n";
import { AlertTriangle, Loader2, Plus, RefreshCcw, ShieldCheck, Trash2, UserRoundPlus, UsersRound } from "@lucide/vue";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { useConnectionStore } from "@/stores/connectionStore";
import { useToast } from "@/composables/useToast";
import type { ConnectionConfig } from "@/types/database";
import * as api from "@/lib/backend/api";
import { executeWithProductionSqlGuard } from "@/lib/database/productionExecutionGuard";
import { translateBackendError } from "@/i18n/backend-errors";
import {
DAMENG_SYSTEM_PRIVILEGES,
damengCreateRoleSql,
damengDropRoleSql,
damengGrantRoleSql,
damengGrantSystemPrivilegeSql,
damengListRoleMembersSql,
damengListRoleSysPrivsSql,
damengListRolesSql,
damengListUsersSql,
damengRevokeRoleSql,
damengRevokeSystemPrivilegeSql,
damengSystemPrivilegeMapSql,
damengEnableDdlAnyPrivSql,
isDamengAnyPrivilege,
parseDamengEnableDdlAnyPriv,
isDamengSystemUser,
isDamengPredefinedRole,
parseDamengRolePrivs,
parseDamengRoles,
parseDamengSysPrivs,
parseDamengUsers,
parseDamengSystemPrivilegeMap,
damengAvailableSystemPrivileges,
type DamengGrant,
type DamengRole,
type DamengSysPrivilege,
type DamengUser,
} from "@/lib/database/damengPrincipalAdmin";
const props = defineProps<{
connection: ConnectionConfig;
}>();
type DetailTab = "members" | "privileges";
const { t } = useI18n();
const { toast } = useToast();
const connectionStore = useConnectionStore();
const roles = ref<DamengRole[]>([]);
const allUsers = ref<DamengUser[]>([]);
const selectedRoleName = ref("");
const search = ref("");
const loadingRoles = ref(false);
const loadingDetails = ref(false);
const applying = ref(false);
const loadError = ref("");
const detailError = ref("");
const detailTab = ref<DetailTab>("members");
const members = ref<DamengGrant[]>([]);
const grantedPrivileges = ref<DamengSysPrivilege[]>([]);
const systemPrivilegeMap = ref<Set<string> | null>(null);
const enableDdlAnyPriv = ref(true);
const createDialogOpen = ref(false);
const createRoleName = ref("");
const grantUserDialogOpen = ref(false);
const grantUserValue = ref("");
const grantPrivilegeDialogOpen = ref(false);
const grantPrivilegeValue = ref("");
const previewDialogOpen = ref(false);
const pendingSql = ref("");
const pendingAfterApply = ref<(() => void | Promise<void>) | undefined>();
const pendingDanger = ref(false);
const supported = computed(() => props.connection.db_type === "dameng");
const executionDatabase = computed(() => (props.connection.db_type === "dameng" ? "" : props.connection.database || ""));
const selectedRole = computed(() => roles.value.find((role) => role.role === selectedRoleName.value));
const selectedIsBuiltinRole = computed(() => !!selectedRole.value && isDamengPredefinedRole(selectedRole.value.role));
const filteredRoles = computed(() => {
const query = search.value.trim().toLowerCase();
if (!query) return roles.value;
return roles.value.filter((role) => role.role.toLowerCase().includes(query));
});
const availableUsersForGrant = computed(() => {
const granted = new Set(members.value.map((member) => member.grantee.toUpperCase()));
// System users are protected: not offered for grant.
return allUsers.value.filter((user) => !granted.has(user.username.toUpperCase()) && !isDamengSystemUser(user.username));
});
const availablePrivilegesForGrant = computed(() => {
const granted = new Set(grantedPrivileges.value.map((grant) => grant.privilege.toUpperCase()));
const catalog = systemPrivilegeMap.value ? damengAvailableSystemPrivileges(DAMENG_SYSTEM_PRIVILEGES, systemPrivilegeMap.value) : DAMENG_SYSTEM_PRIVILEGES;
return catalog.filter((privilege) => !granted.has(privilege.toUpperCase()));
});
const canCreateRole = computed(() => createRoleName.value.trim() !== "");
async function ensureConnection() {
await connectionStore.ensureConnected(props.connection.id);
}
function isDdlAnyPrivDenied(message: string): boolean {
// DM8 error -5567: "Grantor has no granted privilege" / "授权者没有此授权权限",
// typically because ENABLE_DDL_ANY_PRIV=0 blocks granting ANY-type privileges.
return /授权者没有此授权权限|Grantor no granted privilege|[-]?5567/i.test(message);
}
function formatApplyFailedMessage(message: string): string {
return isDdlAnyPrivDenied(message) ? `${message}\n${t("damengRoleAdmin.enableDdlAnyPrivHint")}` : message;
}
function isPrivilegeGrantDisabled(privilege: string): boolean {
return !enableDdlAnyPriv.value && isDamengAnyPrivilege(privilege);
}
async function loadRoles() {
if (!supported.value) return;
loadingRoles.value = true;
loadError.value = "";
try {
await ensureConnection();
const [rolesResult, usersResult, privilegeMapResult, ddlAnyPrivResult] = await Promise.all([
api.executeQuery(props.connection.id, executionDatabase.value, damengListRolesSql(), undefined, undefined, { maxRows: 5000 }),
api.executeQuery(props.connection.id, executionDatabase.value, damengListUsersSql(), undefined, undefined, { maxRows: 5000 }),
// SYSTEM_PRIVILEGE_MAP may be unreadable for non-privileged accounts; fall back to the full catalog.
api.executeQuery(props.connection.id, executionDatabase.value, damengSystemPrivilegeMapSql(), undefined, undefined, { maxRows: 5000 }).catch(() => undefined),
// DM8 default disables ANY-type privilege grants (ENABLE_DDL_ANY_PRIV=0); keep the default enabled on failure.
api.executeQuery(props.connection.id, executionDatabase.value, damengEnableDdlAnyPrivSql(), undefined, undefined, { maxRows: 10 }).catch(() => undefined),
]);
roles.value = parseDamengRoles(rolesResult);
allUsers.value = parseDamengUsers(usersResult);
systemPrivilegeMap.value = privilegeMapResult ? parseDamengSystemPrivilegeMap(privilegeMapResult) : null;
enableDdlAnyPriv.value = ddlAnyPrivResult ? parseDamengEnableDdlAnyPriv(ddlAnyPrivResult) : true;
if (!selectedRole.value) selectedRoleName.value = roles.value[0]?.role || "";
await loadDetails();
} catch (error: any) {
loadError.value = error?.message || String(error);
roles.value = [];
members.value = [];
grantedPrivileges.value = [];
} finally {
loadingRoles.value = false;
}
}
let detailRequestSeq = 0;
async function loadDetails() {
const role = selectedRole.value;
if (!role) {
members.value = [];
grantedPrivileges.value = [];
return;
}
const seq = ++detailRequestSeq;
loadingDetails.value = true;
detailError.value = "";
try {
await ensureConnection();
const [membersResult, privsResult] = await Promise.all([
api.executeQuery(props.connection.id, executionDatabase.value, damengListRoleMembersSql(role.role), undefined, undefined, { maxRows: 1000 }),
api.executeQuery(props.connection.id, executionDatabase.value, damengListRoleSysPrivsSql(role.role), undefined, undefined, { maxRows: 1000 }),
]);
if (seq !== detailRequestSeq) return; // stale response for a previous selection
members.value = parseDamengRolePrivs(membersResult);
grantedPrivileges.value = parseDamengSysPrivs(privsResult);
} catch (error: any) {
if (seq !== detailRequestSeq) return; // stale response for a previous selection
detailError.value = error?.message || String(error);
members.value = [];
grantedPrivileges.value = [];
} finally {
if (seq === detailRequestSeq) loadingDetails.value = false;
}
}
function selectRole(role: DamengRole) {
selectedRoleName.value = role.role;
void loadDetails();
}
function previewSql(sql: string, options: { danger?: boolean; afterApply?: () => void | Promise<void> } = {}) {
pendingSql.value = sql;
pendingDanger.value = !!options.danger;
pendingAfterApply.value = options.afterApply;
previewDialogOpen.value = true;
}
async function applyPendingSql() {
if (!pendingSql.value.trim()) return;
applying.value = true;
try {
await ensureConnection();
const results = await executeWithProductionSqlGuard({
connection: props.connection,
database: executionDatabase.value,
sql: pendingSql.value,
source: t("production.sourceAdmin"),
execute: () =>
api.executeMulti(props.connection.id, executionDatabase.value, pendingSql.value, undefined, undefined, {
maxRows: 1000,
useTransaction: true,
}),
});
if (!results) return;
// Per-statement errors are returned inside the result array (HTTP 200), so
// they must be surfaced explicitly instead of being treated as success.
const failed = results.find((result) => !!result.error);
if (failed) {
toast(t("damengRoleAdmin.applyFailed", { message: formatApplyFailedMessage(translateBackendError(t, failed.error)) }), 5000);
// Statements before the error may already be committed, so refresh regardless.
await loadRoles();
return;
}
toast(t("damengRoleAdmin.applySuccess"), 2500);
previewDialogOpen.value = false;
await (pendingAfterApply.value?.() ?? Promise.resolve());
await loadRoles();
} catch (error: any) {
toast(t("damengRoleAdmin.applyFailed", { message: formatApplyFailedMessage(error?.message || String(error)) }), 5000);
} finally {
applying.value = false;
}
}
function openCreateDialog() {
createRoleName.value = "";
createDialogOpen.value = true;
}
function previewCreateRole() {
if (!canCreateRole.value) return;
previewSql(damengCreateRoleSql(createRoleName.value), {
afterApply: () => {
createDialogOpen.value = false;
},
});
}
function previewDropRole() {
const role = selectedRole.value;
if (!role || selectedIsBuiltinRole.value) return;
previewSql(damengDropRoleSql(role.role), { danger: true });
}
function openGrantUserDialog() {
grantUserValue.value = "";
grantUserDialogOpen.value = true;
}
function previewGrantUser() {
const role = selectedRole.value;
if (!role || !grantUserValue.value) return;
previewSql(damengGrantRoleSql(grantUserValue.value, role.role), {
afterApply: () => {
grantUserDialogOpen.value = false;
},
});
}
function previewRevokeMember(member: DamengGrant) {
const role = selectedRole.value;
if (!role) return;
previewSql(damengRevokeRoleSql(member.grantee, role.role), { danger: true });
}
function openGrantPrivilegeDialog() {
grantPrivilegeValue.value = "";
grantPrivilegeDialogOpen.value = true;
}
function previewGrantPrivilege() {
const role = selectedRole.value;
if (!role || !grantPrivilegeValue.value) return;
previewSql(damengGrantSystemPrivilegeSql(role.role, grantPrivilegeValue.value), {
afterApply: () => {
grantPrivilegeDialogOpen.value = false;
},
});
}
function previewRevokePrivilege(grant: DamengSysPrivilege) {
const role = selectedRole.value;
if (!role) return;
previewSql(damengRevokeSystemPrivilegeSql(role.role, grant.privilege), { danger: true });
}
function isSystemMember(grantee: string): boolean {
return isDamengSystemUser(grantee);
}
onMounted(() => {
void loadRoles();
});
</script>
<template>
<div class="flex h-full min-h-0 flex-col bg-background">
<div class="flex h-12 shrink-0 items-center gap-2 border-b px-3">
<ShieldCheck class="h-4 w-4 text-primary" />
<div class="min-w-0 flex-1">
<div class="truncate text-sm font-semibold">{{ t("damengRoleAdmin.title") }}</div>
<div class="truncate text-[11px] text-muted-foreground">{{ connection.name }}</div>
</div>
<Button size="sm" variant="outline" class="h-8 gap-1.5" :disabled="loadingRoles" @click="loadRoles">
<Loader2 v-if="loadingRoles" class="h-3.5 w-3.5 animate-spin" />
<RefreshCcw v-else class="h-3.5 w-3.5" />
{{ t("contextMenu.refreshChildren") }}
</Button>
<Button size="sm" class="h-8 gap-1.5" @click="openCreateDialog">
<Plus class="h-3.5 w-3.5" />
{{ t("damengRoleAdmin.newRole") }}
</Button>
</div>
<div v-if="!supported" class="m-4 rounded border border-dashed p-4 text-sm text-muted-foreground">
{{ t("damengRoleAdmin.unsupported") }}
</div>
<div v-else class="grid min-h-0 flex-1 grid-cols-[300px_minmax(0,1fr)]">
<aside class="flex min-h-0 flex-col border-r">
<div class="border-b p-2">
<Input v-model="search" class="h-8 text-xs" :placeholder="t('damengRoleAdmin.searchRole')" />
</div>
<div v-if="loadError" class="m-2 rounded border border-destructive/30 bg-destructive/5 p-2 text-xs text-destructive">
<div class="mb-1 flex items-center gap-1 font-medium"><AlertTriangle class="h-3.5 w-3.5" />{{ t("damengRoleAdmin.loadFailed") }}</div>
<div class="break-all">{{ loadError }}</div>
</div>
<div class="min-h-0 flex-1 overflow-auto p-2">
<button v-for="role in filteredRoles" :key="role.role" type="button" class="mb-1 w-full rounded border px-2 py-2 text-left text-xs transition hover:bg-accent" :class="selectedRoleName === role.role ? 'border-primary bg-primary/10' : 'border-transparent'" @click="selectRole(role)">
<div class="flex items-center gap-2">
<span class="min-w-0 flex-1 truncate font-medium">{{ role.role }}</span>
<Badge v-if="role.passwordRequired === 'YES'" variant="outline" class="h-5 px-1.5 text-[10px]">PWD</Badge>
<Badge v-if="isDamengPredefinedRole(role.role)" variant="outline" class="h-5 border-amber-500/50 px-1.5 text-[10px] text-amber-700 dark:text-amber-300">{{ t("damengRoleAdmin.builtinRole") }}</Badge>
</div>
</button>
<div v-if="!loadingRoles && filteredRoles.length === 0" class="p-6 text-center text-xs text-muted-foreground">
{{ t("damengRoleAdmin.emptyRoles") }}
</div>
</div>
</aside>
<main class="flex min-h-0 flex-col">
<div v-if="selectedRole" class="flex shrink-0 flex-wrap items-center gap-2 border-b p-3">
<ShieldCheck class="h-4 w-4 text-primary" />
<div class="min-w-0 flex-1">
<div class="flex items-center gap-2">
<span class="truncate text-sm font-semibold">{{ selectedRole.role }}</span>
</div>
</div>
<Button size="sm" variant="destructive" class="h-8 gap-1.5" :disabled="selectedIsBuiltinRole" :title="selectedIsBuiltinRole ? t('damengRoleAdmin.builtinRoleProtected') : ''" @click="previewDropRole">
<Trash2 class="h-3.5 w-3.5" />
{{ t("damengRoleAdmin.dropRole") }}
</Button>
</div>
<div v-if="selectedRole" class="flex h-10 shrink-0 items-center gap-1 border-b px-3">
<Button size="sm" :variant="detailTab === 'members' ? 'secondary' : 'ghost'" class="h-7 px-2 text-xs" @click="detailTab = 'members'">{{ t("damengRoleAdmin.members") }}</Button>
<Button size="sm" :variant="detailTab === 'privileges' ? 'secondary' : 'ghost'" class="h-7 px-2 text-xs" @click="detailTab = 'privileges'">{{ t("damengRoleAdmin.systemPrivileges") }}</Button>
<Loader2 v-if="loadingDetails" class="ml-auto h-3.5 w-3.5 animate-spin text-muted-foreground" />
</div>
<div class="min-h-0 flex-1 overflow-auto p-3">
<div v-if="!selectedRole" class="flex h-full items-center justify-center text-sm text-muted-foreground">
{{ t("damengRoleAdmin.selectRole") }}
</div>
<div v-else-if="detailError" class="rounded border border-destructive/30 bg-destructive/5 p-3 text-xs text-destructive">
<div class="mb-1 flex items-center gap-1 font-medium"><AlertTriangle class="h-3.5 w-3.5" />{{ t("damengRoleAdmin.loadFailed") }}</div>
<div class="break-all">{{ detailError }}</div>
</div>
<div v-else-if="detailTab === 'members'">
<div class="mb-2 flex items-center gap-2">
<span class="text-xs font-medium">{{ t("damengRoleAdmin.grantedToUsers") }} ({{ members.length }})</span>
<Button size="sm" variant="outline" class="ml-auto h-7 gap-1 px-2 text-xs" :disabled="availableUsersForGrant.length === 0" :title="availableUsersForGrant.length === 0 ? t('damengRoleAdmin.noUsersToGrant') : ''" @click="openGrantUserDialog">
<UserRoundPlus class="h-3.5 w-3.5" />
{{ t("damengRoleAdmin.grantToUser") }}
</Button>
</div>
<div class="grid gap-1.5">
<div v-for="member in members" :key="member.grantee" class="flex items-center gap-2 rounded border bg-muted/20 px-3 py-2">
<UsersRound class="h-3.5 w-3.5 shrink-0 text-primary" />
<span class="min-w-0 flex-1 truncate font-mono text-xs">{{ member.grantee }}</span>
<Badge v-if="isSystemMember(member.grantee)" variant="outline" class="h-5 border-amber-500/50 px-1.5 text-[10px] text-amber-700 dark:text-amber-300">{{ t("damengRoleAdmin.systemUser") }}</Badge>
<Badge v-if="isDamengPredefinedRole(member.grantee)" variant="outline" class="h-5 border-amber-500/50 px-1.5 text-[10px] text-amber-700 dark:text-amber-300">{{ t("damengRoleAdmin.builtinRole") }}</Badge>
<Badge v-if="member.adminOption" variant="outline" class="h-5 px-1.5 text-[10px]">ADMIN OPTION</Badge>
<Button
size="sm"
variant="ghost"
class="h-6 gap-1 px-2 text-xs text-destructive hover:text-destructive"
:disabled="isSystemMember(member.grantee) || (selectedIsBuiltinRole && isDamengPredefinedRole(member.grantee))"
:title="isSystemMember(member.grantee) ? t('damengRoleAdmin.systemUserProtected') : selectedIsBuiltinRole && isDamengPredefinedRole(member.grantee) ? t('damengRoleAdmin.builtinRoleProtected') : ''"
@click="previewRevokeMember(member)"
>
<Trash2 class="h-3 w-3" />
{{ t("damengRoleAdmin.revokeFromUser") }}
</Button>
</div>
<div v-if="members.length === 0" class="rounded border border-dashed p-6 text-center text-xs text-muted-foreground">
{{ t("damengRoleAdmin.noMembers") }}
</div>
</div>
</div>
<div v-else>
<div class="mb-2 flex items-center gap-2">
<span class="text-xs font-medium">{{ t("damengRoleAdmin.grantedPrivileges") }} ({{ grantedPrivileges.length }})</span>
<Button
size="sm"
variant="outline"
class="ml-auto h-7 gap-1 px-2 text-xs"
:disabled="selectedIsBuiltinRole || availablePrivilegesForGrant.length === 0"
:title="selectedIsBuiltinRole ? t('damengRoleAdmin.builtinRoleProtected') : availablePrivilegesForGrant.length === 0 ? t('damengRoleAdmin.noPrivilegesToGrant') : ''"
@click="openGrantPrivilegeDialog"
>
<UserRoundPlus class="h-3.5 w-3.5" />
{{ t("damengRoleAdmin.grantPrivilege") }}
</Button>
</div>
<div class="grid gap-1.5">
<div v-for="grant in grantedPrivileges" :key="grant.privilege" class="flex items-center gap-2 rounded border bg-muted/20 px-3 py-2">
<ShieldCheck class="h-3.5 w-3.5 shrink-0 text-primary" />
<span class="min-w-0 flex-1 truncate font-mono text-xs">{{ grant.privilege }}</span>
<Badge v-if="grant.adminOption" variant="outline" class="h-5 px-1.5 text-[10px]">ADMIN OPTION</Badge>
<Button
size="sm"
variant="ghost"
class="h-6 gap-1 px-2 text-xs text-destructive hover:text-destructive"
:disabled="selectedIsBuiltinRole || isPrivilegeGrantDisabled(grant.privilege)"
:title="selectedIsBuiltinRole ? t('damengRoleAdmin.builtinRoleProtected') : isPrivilegeGrantDisabled(grant.privilege) ? t('damengRoleAdmin.anyPrivilegeDisabled') : ''"
@click="previewRevokePrivilege(grant)"
>
<Trash2 class="h-3 w-3" />
{{ t("damengRoleAdmin.revokePrivilege") }}
</Button>
</div>
<div v-if="grantedPrivileges.length === 0" class="rounded border border-dashed p-6 text-center text-xs text-muted-foreground">
{{ t("damengRoleAdmin.noGrantedPrivileges") }}
</div>
</div>
</div>
</div>
</main>
</div>
<Dialog v-model:open="createDialogOpen">
<DialogContent class="max-w-md">
<DialogHeader>
<DialogTitle>{{ t("damengRoleAdmin.newRole") }}</DialogTitle>
</DialogHeader>
<div class="grid gap-3 text-xs">
<label class="grid gap-1">
<span>{{ t("damengRoleAdmin.roleName") }}</span>
<Input v-model="createRoleName" class="h-8 text-xs" :placeholder="t('damengRoleAdmin.roleNamePlaceholder')" />
</label>
</div>
<DialogFooter>
<Button variant="outline" @click="createDialogOpen = false">{{ t("common.cancel") }}</Button>
<Button :disabled="!canCreateRole" @click="previewCreateRole">{{ t("damengRoleAdmin.previewSql") }}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog v-model:open="grantUserDialogOpen">
<DialogContent class="max-w-md">
<DialogHeader>
<DialogTitle>{{ t("damengRoleAdmin.grantToUser") }}</DialogTitle>
</DialogHeader>
<div class="grid gap-3 text-xs">
<label class="grid gap-1">
<span>{{ t("damengRoleAdmin.userToGrant") }}</span>
<Select v-model="grantUserValue">
<SelectTrigger class="h-8 text-xs"><SelectValue :placeholder="t('damengRoleAdmin.selectUser')" /></SelectTrigger>
<SelectContent>
<SelectItem v-for="user in availableUsersForGrant" :key="user.username" :value="user.username">{{ user.username }}</SelectItem>
</SelectContent>
</Select>
</label>
<p class="text-[11px] text-muted-foreground">{{ t("damengRoleAdmin.grantUserHint", { role: selectedRole?.role ?? "" }) }}</p>
</div>
<DialogFooter>
<Button variant="outline" @click="grantUserDialogOpen = false">{{ t("common.cancel") }}</Button>
<Button :disabled="!grantUserValue" @click="previewGrantUser">{{ t("damengRoleAdmin.previewSql") }}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog v-model:open="grantPrivilegeDialogOpen">
<DialogContent class="max-w-md">
<DialogHeader>
<DialogTitle>{{ t("damengRoleAdmin.grantPrivilege") }}</DialogTitle>
</DialogHeader>
<div class="grid gap-3 text-xs">
<label class="grid gap-1">
<span>{{ t("damengRoleAdmin.privilegeToGrant") }}</span>
<Select v-model="grantPrivilegeValue">
<SelectTrigger class="h-8 text-xs"><SelectValue :placeholder="t('damengRoleAdmin.selectPrivilege')" /></SelectTrigger>
<SelectContent>
<SelectItem v-for="privilege in availablePrivilegesForGrant" :key="privilege" :value="privilege" :disabled="isPrivilegeGrantDisabled(privilege)">{{ privilege }}</SelectItem>
</SelectContent>
</Select>
</label>
<p v-if="!enableDdlAnyPriv" class="text-[11px] text-amber-600 dark:text-amber-400">{{ t("damengRoleAdmin.anyPrivilegeDisabled") }}</p>
<p class="text-[11px] text-muted-foreground">{{ t("damengRoleAdmin.grantPrivilegeHint", { role: selectedRole?.role ?? "" }) }}</p>
</div>
<DialogFooter>
<Button variant="outline" @click="grantPrivilegeDialogOpen = false">{{ t("common.cancel") }}</Button>
<Button :disabled="!grantPrivilegeValue" @click="previewGrantPrivilege">{{ t("damengRoleAdmin.previewSql") }}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog v-model:open="previewDialogOpen">
<DialogContent class="max-w-3xl">
<DialogHeader>
<DialogTitle>{{ t("damengRoleAdmin.sqlPreview") }}</DialogTitle>
</DialogHeader>
<pre class="max-h-[50vh] overflow-auto rounded-md border bg-muted/30 p-3 text-xs"><code>{{ pendingSql }}</code></pre>
<DialogFooter>
<Button variant="outline" @click="previewDialogOpen = false">{{ t("common.cancel") }}</Button>
<Button :variant="pendingDanger ? 'destructive' : 'default'" :disabled="applying" @click="applyPendingSql">
<Loader2 v-if="applying" class="mr-1 h-3.5 w-3.5 animate-spin" />
{{ t("damengRoleAdmin.applySql") }}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</template>
@@ -0,0 +1,787 @@
<script setup lang="ts">
import { computed, onMounted, ref } from "vue";
import { useI18n } from "vue-i18n";
import { AlertTriangle, ChevronRight, Folder, KeyRound, Loader2, Lock, LockOpen, Plus, RefreshCcw, ShieldCheck, Trash2, UserRound, UserRoundPlus, UsersRound } from "@lucide/vue";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import PasswordInput from "@/components/ui/PasswordInput.vue";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { useConnectionStore } from "@/stores/connectionStore";
import { useToast } from "@/composables/useToast";
import type { ConnectionConfig } from "@/types/database";
import * as api from "@/lib/backend/api";
import { executeWithProductionSqlGuard } from "@/lib/database/productionExecutionGuard";
import { translateBackendError } from "@/i18n/backend-errors";
import {
DAMENG_USER_GROUPS,
DAMENG_SYSTEM_PRIVILEGES,
damengAlterUserPasswordSql,
damengCreateUserSql,
damengDropUserSql,
damengGrantRoleSql,
damengGrantSystemPrivilegeSql,
damengListRolesSql,
damengListTablespacesSql,
damengListUserRolesSql,
damengListUserSysPrivsSql,
damengListUsersSql,
damengLockUserSql,
damengRevokeRoleSql,
damengRevokeSystemPrivilegeSql,
damengSystemPrivilegeMapSql,
damengRoleGraphSql,
damengRolesClosure,
damengUnlockUserSql,
damengUserTypeGrantSqls,
canDamengAlterUserPassword,
canDamengRevokeRoleGrant,
isDamengSystemUser,
isDamengPredefinedRole,
DAMENG_HIDDEN_ROLES,
damengUserGroup,
damengSystemUserCategory,
parseDamengRoles,
parseDamengRoleGraph,
parseDamengRolePrivs,
parseDamengSysPrivs,
parseDamengTablespaces,
parseDamengSystemPrivilegeMap,
damengAvailableSystemPrivileges,
parseDamengRoleGrants,
parseDamengUsers,
type DamengGrant,
type DamengRole,
type DamengSysPrivilege,
type DamengUser,
type DamengUserGroup,
damengEnableDdlAnyPrivSql,
isDamengAnyPrivilege,
parseDamengEnableDdlAnyPriv,
} from "@/lib/database/damengPrincipalAdmin";
const props = defineProps<{
connection: ConnectionConfig;
}>();
type DetailTab = "roles" | "privileges";
const { t } = useI18n();
const { toast } = useToast();
const connectionStore = useConnectionStore();
const users = ref<DamengUser[]>([]);
const allRoles = ref<DamengRole[]>([]);
const tablespaces = ref<string[]>([]);
const selectedUsername = ref("");
const search = ref("");
const loadingUsers = ref(false);
const loadingRoles = ref(false);
const loadingDetails = ref(false);
const applying = ref(false);
const loadError = ref("");
const detailError = ref("");
const detailTab = ref<DetailTab>("roles");
const grantedRoles = ref<DamengGrant[]>([]);
const grantedPrivileges = ref<DamengSysPrivilege[]>([]);
const systemPrivilegeMap = ref<Set<string> | null>(null);
const enableDdlAnyPriv = ref(true);
const createDialogOpen = ref(false);
const createUsername = ref("");
const createPassword = ref("");
const createTablespace = ref("");
const createLocked = ref(false);
const createUserType = ref<Exclude<DamengUserGroup, "other">>("admin");
const createdUsername = ref("");
const userRoleGrants = ref<Map<string, Set<string>>>(new Map());
const collapsedGroups = ref<Record<string, boolean>>({});
function groupLabel(group: DamengUserGroup): string {
if (group === "other") return t("damengUserAdmin.groupOther");
const key = "damengUserAdmin.systemUser" + group.charAt(0).toUpperCase() + group.slice(1);
return t(key);
}
function usersInGroup(group: DamengUserGroup): DamengUser[] {
return filteredUsers.value.filter((user) => damengUserGroup(user.username, userRoleGrants.value.get(user.username.trim().toUpperCase())) === group);
}
function toggleGroup(group: DamengUserGroup) {
collapsedGroups.value[group] = !collapsedGroups.value[group];
}
const passwordDialogOpen = ref(false);
const passwordValue = ref("");
const passwordConfirm = ref("");
const grantRoleDialogOpen = ref(false);
const grantRoleValue = ref("");
const grantPrivilegeDialogOpen = ref(false);
const grantPrivilegeValue = ref("");
const previewDialogOpen = ref(false);
const pendingSql = ref("");
const pendingAfterApply = ref<(() => void | Promise<void>) | undefined>();
const pendingDanger = ref(false);
const supported = computed(() => props.connection.db_type === "dameng");
const executionDatabase = computed(() => (props.connection.db_type === "dameng" ? "" : props.connection.database || ""));
const selectedUser = computed(() => users.value.find((user) => user.username === selectedUsername.value));
const filteredUsers = computed(() => {
const query = search.value.trim().toLowerCase();
if (!query) return users.value;
return users.value.filter((user) => `${user.username} ${user.defaultTablespace}`.toLowerCase().includes(query));
});
const selectedIsSystemUser = computed(() => !!selectedUser.value && isDamengSystemUser(selectedUser.value.username));
/**
* Whether the current connection user is allowed to change the selected
* user's password, following the DM security model (see canDamengAlterUserPassword).
*/
const canAlterPasswordForSelected = computed(() => canDamengAlterUserPassword(props.connection.username, selectedUser.value?.username));
const selectedUserCategory = computed(() => (selectedUser.value ? damengSystemUserCategory(selectedUser.value.username) : undefined));
const selectedUserCategoryLabel = computed(() => {
if (!selectedUserCategory.value) return "";
const key = "damengUserAdmin.systemUser" + selectedUserCategory.value.charAt(0).toUpperCase() + selectedUserCategory.value.slice(1);
return t(key);
});
function userCategoryLabel(username: string): string {
const category = damengSystemUserCategory(username);
if (!category) return "";
const key = "damengUserAdmin.systemUser" + category.charAt(0).toUpperCase() + category.slice(1);
return t(key);
}
const availableRolesForGrant = computed(() => {
// Roles already held (directly or via nesting) plus hidden internal roles
// that cannot be granted (e.g. SYS_ADMIN) are not offered.
const owned = new Set(grantedRoles.value.map((grant) => grant.grantedRole.toUpperCase()));
const hidden = DAMENG_HIDDEN_ROLES.map((role) => role.toUpperCase());
return allRoles.value.filter((role) => !owned.has(role.role.toUpperCase()) && !hidden.includes(role.role.toUpperCase()));
});
const availablePrivilegesForGrant = computed(() => {
const granted = new Set(grantedPrivileges.value.map((grant) => grant.privilege.toUpperCase()));
const catalog = systemPrivilegeMap.value ? damengAvailableSystemPrivileges(DAMENG_SYSTEM_PRIVILEGES, systemPrivilegeMap.value) : DAMENG_SYSTEM_PRIVILEGES;
return catalog.filter((privilege) => !granted.has(privilege.toUpperCase()));
});
const canCreateUser = computed(() => createUsername.value.trim() !== "" && createPassword.value !== "");
const canChangePassword = computed(() => passwordValue.value !== "" && passwordValue.value === passwordConfirm.value);
async function ensureConnection() {
await connectionStore.ensureConnected(props.connection.id);
}
async function refreshConnectionTree() {
try {
// DM users map 1:1 to schemas, so reload the sidebar database/schema tree
// after user DDL. Best-effort: the in-page user list is already refreshed.
await connectionStore.loadDatabases(props.connection.id, { force: true });
} catch {
// ignore tree refresh failures
}
}
function isDdlAnyPrivDenied(message: string): boolean {
// DM8 error -5567: "Grantor has no granted privilege" / "授权者没有此授权权限",
// typically because ENABLE_DDL_ANY_PRIV=0 blocks granting ANY-type privileges.
return /授权者没有此授权权限|Grantor no granted privilege|[-]?5567/i.test(message);
}
function formatApplyFailedMessage(message: string): string {
return isDdlAnyPrivDenied(message) ? `${message}\n${t("damengUserAdmin.enableDdlAnyPrivHint")}` : message;
}
function isPrivilegeGrantDisabled(privilege: string): boolean {
return !enableDdlAnyPriv.value && isDamengAnyPrivilege(privilege);
}
async function loadUsers() {
if (!supported.value) return;
loadingUsers.value = true;
loadError.value = "";
try {
await ensureConnection();
// Load the full role-grant map once so list grouping can place regular users
// holding management roles (DBA/DB_AUDIT_ADMIN/DB_POLICY_ADMIN) into the
// matching category. Falls back to an empty map when the view is unreadable.
const [result, roleGrantsResult] = await Promise.all([
api.executeQuery(props.connection.id, executionDatabase.value, damengListUsersSql(), undefined, undefined, { maxRows: 5000 }),
api.executeQuery(props.connection.id, executionDatabase.value, damengRoleGraphSql(), undefined, undefined, { maxRows: 5000 }).catch(() => undefined),
]);
users.value = parseDamengUsers(result);
userRoleGrants.value = roleGrantsResult ? parseDamengRoleGrants(roleGrantsResult) : new Map();
// Auto-select a user just created through the dialog.
if (createdUsername.value) {
const created = users.value.find((user) => user.username.toLowerCase() === createdUsername.value.toLowerCase());
if (created) {
selectedUsername.value = created.username;
createdUsername.value = "";
}
}
if (!selectedUser.value) selectedUsername.value = users.value[0]?.username || "";
await loadDetails();
} catch (error: any) {
loadError.value = error?.message || String(error);
users.value = [];
grantedRoles.value = [];
grantedPrivileges.value = [];
} finally {
loadingUsers.value = false;
}
}
async function loadRolesAndTablespaces() {
if (!supported.value) return;
loadingRoles.value = true;
try {
await ensureConnection();
const [rolesResult, tablespaceResult, privilegeMapResult, ddlAnyPrivResult] = await Promise.all([
api.executeQuery(props.connection.id, executionDatabase.value, damengListRolesSql(), undefined, undefined, { maxRows: 5000 }),
api.executeQuery(props.connection.id, executionDatabase.value, damengListTablespacesSql(), undefined, undefined, { maxRows: 1000 }),
// SYSTEM_PRIVILEGE_MAP may be unreadable for non-privileged accounts; fall back to the full catalog.
api.executeQuery(props.connection.id, executionDatabase.value, damengSystemPrivilegeMapSql(), undefined, undefined, { maxRows: 5000 }).catch(() => undefined),
// DM8 default disables ANY-type privilege grants (ENABLE_DDL_ANY_PRIV=0); keep the default enabled on failure.
api.executeQuery(props.connection.id, executionDatabase.value, damengEnableDdlAnyPrivSql(), undefined, undefined, { maxRows: 10 }).catch(() => undefined),
]);
allRoles.value = parseDamengRoles(rolesResult);
tablespaces.value = parseDamengTablespaces(tablespaceResult);
systemPrivilegeMap.value = privilegeMapResult ? parseDamengSystemPrivilegeMap(privilegeMapResult) : null;
enableDdlAnyPriv.value = ddlAnyPrivResult ? parseDamengEnableDdlAnyPriv(ddlAnyPrivResult) : true;
} catch {
allRoles.value = [];
tablespaces.value = [];
systemPrivilegeMap.value = null;
} finally {
loadingRoles.value = false;
}
}
let detailRequestSeq = 0;
async function loadDetails() {
const user = selectedUser.value;
if (!user) {
grantedRoles.value = [];
grantedPrivileges.value = [];
return;
}
const seq = ++detailRequestSeq;
loadingDetails.value = true;
detailError.value = "";
try {
await ensureConnection();
const [rolesResult, privsResult, graphResult] = await Promise.all([
api.executeQuery(props.connection.id, executionDatabase.value, damengListUserRolesSql(user.username), undefined, undefined, { maxRows: 1000 }),
api.executeQuery(props.connection.id, executionDatabase.value, damengListUserSysPrivsSql(user.username), undefined, undefined, { maxRows: 1000 }),
api.executeQuery(props.connection.id, executionDatabase.value, damengRoleGraphSql(), undefined, undefined, { maxRows: 5000 }),
]);
if (seq !== detailRequestSeq) return; // stale response for a previous selection
const directRoles = parseDamengRolePrivs(rolesResult);
const graph = parseDamengRoleGraph(graphResult);
// Official DM tool shows the transitive closure of roles (e.g. SYSDBA also
// has SVI via PUBLIC and VTI via DBA), excluding hidden internal roles.
grantedRoles.value = damengRolesClosure(user.username, graph).map((entry) => {
const direct = directRoles.find((grant) => grant.grantedRole.toUpperCase() === entry.role);
return direct ?? { grantee: user.username, grantedRole: entry.role, adminOption: false, defaultRole: false, inherited: !entry.direct, via: entry.via };
});
grantedPrivileges.value = parseDamengSysPrivs(privsResult);
} catch (error: any) {
if (seq !== detailRequestSeq) return; // stale response for a previous selection
detailError.value = error?.message || String(error);
grantedRoles.value = [];
grantedPrivileges.value = [];
} finally {
if (seq === detailRequestSeq) loadingDetails.value = false;
}
}
function selectUser(user: DamengUser) {
selectedUsername.value = user.username;
void loadDetails();
}
function previewSql(sql: string, options: { danger?: boolean; afterApply?: () => void | Promise<void> } = {}) {
pendingSql.value = sql;
pendingDanger.value = !!options.danger;
pendingAfterApply.value = options.afterApply;
previewDialogOpen.value = true;
}
async function applyPendingSql() {
if (!pendingSql.value.trim()) return;
applying.value = true;
try {
await ensureConnection();
const results = await executeWithProductionSqlGuard({
connection: props.connection,
database: executionDatabase.value,
sql: pendingSql.value,
source: t("production.sourceAdmin"),
execute: () =>
api.executeMulti(props.connection.id, executionDatabase.value, pendingSql.value, undefined, undefined, {
maxRows: 1000,
useTransaction: true,
}),
});
if (!results) return;
// Per-statement errors are returned inside the result array (HTTP 200), so
// they must be surfaced explicitly instead of being treated as success.
const failed = results.find((result) => !!result.error);
if (failed) {
toast(t("damengUserAdmin.applyFailed", { message: formatApplyFailedMessage(translateBackendError(t, failed.error)) }), 5000);
// Statements before the error may already be committed, so refresh regardless.
await loadUsers();
// Some statements may already be committed (per-statement autocommit), so keep the sidebar tree in sync too.
await refreshConnectionTree();
return;
}
toast(t("damengUserAdmin.applySuccess"), 2500);
previewDialogOpen.value = false;
await (pendingAfterApply.value?.() ?? Promise.resolve());
await loadUsers();
// Users map 1:1 to schemas on DM; keep the sidebar connection tree in sync after create/drop/etc.
await refreshConnectionTree();
} catch (error: any) {
toast(t("damengUserAdmin.applyFailed", { message: formatApplyFailedMessage(error?.message || String(error)) }), 5000);
} finally {
applying.value = false;
}
}
function openCreateDialog() {
createUsername.value = "";
createPassword.value = "";
createTablespace.value = tablespaces.value[0] ?? "";
createLocked.value = false;
createDialogOpen.value = true;
}
function previewCreateUser() {
if (!canCreateUser.value) return;
const statements = [
damengCreateUserSql({
username: createUsername.value,
password: createPassword.value,
tablespace: createTablespace.value,
locked: createLocked.value,
}),
...damengUserTypeGrantSqls(createUserType.value, createUsername.value),
];
previewSql(statements.join("\n"), {
afterApply: () => {
createDialogOpen.value = false;
createdUsername.value = createUsername.value.trim();
},
});
}
function openChangePasswordDialog() {
passwordValue.value = "";
passwordConfirm.value = "";
passwordDialogOpen.value = true;
}
function previewChangePassword() {
const user = selectedUser.value;
if (!user || !canAlterPasswordForSelected.value || !canChangePassword.value) return;
previewSql(damengAlterUserPasswordSql(user.username, passwordValue.value), {
afterApply: () => {
passwordDialogOpen.value = false;
},
});
}
function previewLockUser(locked: boolean) {
const user = selectedUser.value;
if (!user) return;
previewSql(locked ? damengLockUserSql(user.username) : damengUnlockUserSql(user.username));
}
function previewDropUser() {
const user = selectedUser.value;
if (!user) return;
previewSql(damengDropUserSql(user.username), { danger: true });
}
function openGrantRoleDialog() {
grantRoleValue.value = "";
grantRoleDialogOpen.value = true;
}
function previewGrantRole() {
const user = selectedUser.value;
if (!user || !grantRoleValue.value) return;
previewSql(damengGrantRoleSql(user.username, grantRoleValue.value), {
afterApply: () => {
grantRoleDialogOpen.value = false;
},
});
}
function previewRevokeRole(grant: DamengGrant) {
const user = selectedUser.value;
if (!user || !canDamengRevokeRoleGrant(user.username, grant)) return;
previewSql(damengRevokeRoleSql(user.username, grant.grantedRole), { danger: true });
}
function openGrantPrivilegeDialog() {
grantPrivilegeValue.value = "";
grantPrivilegeDialogOpen.value = true;
}
function previewGrantPrivilege() {
const user = selectedUser.value;
if (!user || !grantPrivilegeValue.value) return;
previewSql(damengGrantSystemPrivilegeSql(user.username, grantPrivilegeValue.value), {
afterApply: () => {
grantPrivilegeDialogOpen.value = false;
},
});
}
function previewRevokePrivilege(grant: DamengSysPrivilege) {
const user = selectedUser.value;
if (!user) return;
previewSql(damengRevokeSystemPrivilegeSql(user.username, grant.privilege), { danger: true });
}
function accountStatusBadgeVariant(user: DamengUser): "default" | "secondary" | "destructive" | "outline" {
if (!user.locked) return "default";
return /EXPIRED/i.test(user.accountStatus) ? "destructive" : "secondary";
}
onMounted(() => {
void loadUsers();
void loadRolesAndTablespaces();
});
</script>
<template>
<div class="flex h-full min-h-0 flex-col bg-background">
<div class="flex h-12 shrink-0 items-center gap-2 border-b px-3">
<UsersRound class="h-4 w-4 text-primary" />
<div class="min-w-0 flex-1">
<div class="truncate text-sm font-semibold">{{ t("damengUserAdmin.title") }}</div>
<div class="truncate text-[11px] text-muted-foreground">{{ connection.name }}</div>
</div>
<Button size="sm" variant="outline" class="h-8 gap-1.5" :disabled="loadingUsers" @click="loadUsers">
<Loader2 v-if="loadingUsers" class="h-3.5 w-3.5 animate-spin" />
<RefreshCcw v-else class="h-3.5 w-3.5" />
{{ t("contextMenu.refreshChildren") }}
</Button>
<Button size="sm" class="h-8 gap-1.5" @click="openCreateDialog">
<Plus class="h-3.5 w-3.5" />
{{ t("damengUserAdmin.newUser") }}
</Button>
</div>
<div v-if="!supported" class="m-4 rounded border border-dashed p-4 text-sm text-muted-foreground">
{{ t("damengUserAdmin.unsupported") }}
</div>
<div v-else class="grid min-h-0 flex-1 grid-cols-[300px_minmax(0,1fr)]">
<aside class="flex min-h-0 flex-col border-r">
<div class="border-b p-2">
<Input v-model="search" class="h-8 text-xs" :placeholder="t('damengUserAdmin.searchUser')" />
</div>
<div v-if="loadError" class="m-2 rounded border border-destructive/30 bg-destructive/5 p-2 text-xs text-destructive">
<div class="mb-1 flex items-center gap-1 font-medium"><AlertTriangle class="h-3.5 w-3.5" />{{ t("damengUserAdmin.loadFailed") }}</div>
<div class="break-all">{{ loadError }}</div>
</div>
<div class="min-h-0 flex-1 overflow-auto p-2">
<template v-for="group in DAMENG_USER_GROUPS" :key="group">
<div v-if="usersInGroup(group).length > 0" class="mb-0.5 mt-1 first:mt-0">
<button type="button" class="flex w-full items-center gap-1 rounded px-1 py-1 text-[11px] font-medium text-muted-foreground hover:bg-accent" @click="toggleGroup(group)">
<ChevronRight class="h-3 w-3 transition-transform" :class="collapsedGroups[group] ? '' : 'rotate-90'" />
<Folder class="h-3 w-3" />
<span class="truncate">{{ groupLabel(group) }}</span>
<span class="ml-auto text-[10px] opacity-70">{{ usersInGroup(group).length }}</span>
</button>
<div v-if="!collapsedGroups[group]">
<button
v-for="user in usersInGroup(group)"
:key="user.username"
type="button"
class="mb-1 w-full rounded border px-2 py-2 text-left text-xs transition hover:bg-accent"
:class="selectedUsername === user.username ? 'border-primary bg-primary/10' : 'border-transparent'"
@click="selectUser(user)"
>
<div class="flex items-center gap-2">
<span class="min-w-0 flex-1 truncate font-medium">{{ user.username }}</span>
<Badge v-if="userCategoryLabel(user.username)" variant="outline" class="h-5 border-amber-500/50 px-1.5 text-[10px] text-amber-700 dark:text-amber-300">{{ userCategoryLabel(user.username) }}</Badge>
<Badge v-if="user.locked" :variant="accountStatusBadgeVariant(user)" class="h-5 px-1.5 text-[10px]">{{ user.accountStatus }}</Badge>
</div>
<div class="mt-1 truncate text-[11px] text-muted-foreground">{{ user.defaultTablespace || "-" }}</div>
</button>
</div>
</div>
</template>
<div v-if="!loadingUsers && filteredUsers.length === 0" class="p-6 text-center text-xs text-muted-foreground">
{{ t("damengUserAdmin.emptyUsers") }}
</div>
</div>
</aside>
<main class="flex min-h-0 flex-col">
<div v-if="selectedUser" class="flex shrink-0 flex-wrap items-center gap-2 border-b p-3">
<UserRound class="h-4 w-4 text-primary" />
<div class="min-w-0 flex-1">
<div class="flex items-center gap-2">
<span class="truncate text-sm font-semibold">{{ selectedUser.username }}</span>
<Badge v-if="selectedUser.locked" :variant="accountStatusBadgeVariant(selectedUser)" class="h-5 px-1.5 text-[10px]">{{ selectedUser.accountStatus }}</Badge>
<Badge v-else variant="outline" class="h-5 px-1.5 text-[10px]">{{ t("damengUserAdmin.open") }}</Badge>
<Badge v-if="selectedUserCategoryLabel" variant="outline" class="h-5 border-amber-500/50 px-1.5 text-[10px] text-amber-700 dark:text-amber-300">{{ selectedUserCategoryLabel }}</Badge>
</div>
<div class="truncate text-xs text-muted-foreground">{{ t("damengUserAdmin.defaultTablespace") }}: {{ selectedUser.defaultTablespace || "-" }} · {{ t("damengUserAdmin.created") }}: {{ selectedUser.created || "-" }}</div>
</div>
<Button size="sm" variant="outline" class="h-8 gap-1.5" :disabled="!canAlterPasswordForSelected" :title="canAlterPasswordForSelected ? '' : t('damengUserAdmin.cannotChangePassword')" @click="openChangePasswordDialog">
<KeyRound class="h-3.5 w-3.5" />
{{ t("damengUserAdmin.changePassword") }}
</Button>
<Button size="sm" variant="outline" class="h-8 gap-1.5" :disabled="selectedIsSystemUser" :title="selectedIsSystemUser ? t('damengUserAdmin.systemUserProtected') : ''" @click="previewLockUser(!selectedUser.locked)">
<LockOpen v-if="selectedUser.locked" class="h-3.5 w-3.5" />
<Lock v-else class="h-3.5 w-3.5" />
{{ selectedUser.locked ? t("damengUserAdmin.unlock") : t("damengUserAdmin.lock") }}
</Button>
<Button size="sm" variant="destructive" class="h-8 gap-1.5" :disabled="selectedIsSystemUser" :title="selectedIsSystemUser ? t('damengUserAdmin.systemUserProtected') : ''" @click="previewDropUser">
<Trash2 class="h-3.5 w-3.5" />
{{ t("damengUserAdmin.dropUser") }}
</Button>
</div>
<div v-if="selectedUser" class="flex h-10 shrink-0 items-center gap-1 border-b px-3">
<Button size="sm" :variant="detailTab === 'roles' ? 'secondary' : 'ghost'" class="h-7 px-2 text-xs" @click="detailTab = 'roles'">{{ t("damengUserAdmin.roles") }}</Button>
<Button size="sm" :variant="detailTab === 'privileges' ? 'secondary' : 'ghost'" class="h-7 px-2 text-xs" @click="detailTab = 'privileges'">{{ t("damengUserAdmin.systemPrivileges") }}</Button>
<Loader2 v-if="loadingDetails" class="ml-auto h-3.5 w-3.5 animate-spin text-muted-foreground" />
</div>
<div class="min-h-0 flex-1 overflow-auto p-3">
<div v-if="!selectedUser" class="flex h-full items-center justify-center text-sm text-muted-foreground">
{{ t("damengUserAdmin.selectUser") }}
</div>
<div v-else-if="detailError" class="rounded border border-destructive/30 bg-destructive/5 p-3 text-xs text-destructive">
<div class="mb-1 flex items-center gap-1 font-medium"><AlertTriangle class="h-3.5 w-3.5" />{{ t("damengUserAdmin.loadFailed") }}</div>
<div class="break-all">{{ detailError }}</div>
</div>
<div v-else-if="detailTab === 'roles'">
<div class="mb-2 flex items-center gap-2">
<span class="text-xs font-medium">{{ t("damengUserAdmin.grantedRoles") }} ({{ grantedRoles.length }})</span>
<Button
size="sm"
variant="outline"
class="ml-auto h-7 gap-1 px-2 text-xs"
:disabled="selectedIsSystemUser || availableRolesForGrant.length === 0"
:title="selectedIsSystemUser ? t('damengUserAdmin.systemUserProtected') : availableRolesForGrant.length === 0 ? t('damengUserAdmin.noRolesToGrant') : ''"
@click="openGrantRoleDialog"
>
<UserRoundPlus class="h-3.5 w-3.5" />
{{ t("damengUserAdmin.grantRole") }}
</Button>
</div>
<div class="grid gap-1.5">
<div v-for="grant in grantedRoles" :key="grant.grantedRole" class="flex items-center gap-2 rounded border bg-muted/20 px-3 py-2">
<ShieldCheck class="h-3.5 w-3.5 shrink-0 text-primary" />
<span class="min-w-0 flex-1 truncate font-mono text-xs">{{ grant.grantedRole }}</span>
<Badge v-if="grant.adminOption" variant="outline" class="h-5 px-1.5 text-[10px]">ADMIN OPTION</Badge>
<Badge v-if="isDamengPredefinedRole(grant.grantedRole)" variant="outline" class="h-5 border-amber-500/50 px-1.5 text-[10px] text-amber-700 dark:text-amber-300">{{ t("damengUserAdmin.builtinRole") }}</Badge>
<Badge v-if="grant.inherited" variant="secondary" class="h-5 px-1.5 text-[10px]" :title="grant.via">{{ t("damengUserAdmin.inheritedFrom", { via: grant.via ?? "" }) }}</Badge>
<Button
v-if="!grant.inherited"
size="sm"
variant="ghost"
class="h-6 gap-1 px-2 text-xs text-destructive hover:text-destructive"
:disabled="!canDamengRevokeRoleGrant(selectedUser.username, grant)"
:title="selectedIsSystemUser ? t('damengUserAdmin.systemUserProtected') : ''"
@click="previewRevokeRole(grant)"
>
<Trash2 class="h-3 w-3" />
{{ t("damengUserAdmin.revokeRole") }}
</Button>
</div>
<div v-if="grantedRoles.length === 0" class="rounded border border-dashed p-6 text-center text-xs text-muted-foreground">
{{ t("damengUserAdmin.noGrantedRoles") }}
</div>
</div>
</div>
<div v-else>
<div class="mb-2 flex items-center gap-2">
<span class="text-xs font-medium">{{ t("damengUserAdmin.grantedPrivileges") }} ({{ grantedPrivileges.length }})</span>
<Button
size="sm"
variant="outline"
class="ml-auto h-7 gap-1 px-2 text-xs"
:disabled="selectedIsSystemUser || availablePrivilegesForGrant.length === 0"
:title="selectedIsSystemUser ? t('damengUserAdmin.systemUserProtected') : availablePrivilegesForGrant.length === 0 ? t('damengUserAdmin.noPrivilegesToGrant') : ''"
@click="openGrantPrivilegeDialog"
>
<UserRoundPlus class="h-3.5 w-3.5" />
{{ t("damengUserAdmin.grantPrivilege") }}
</Button>
</div>
<div class="grid gap-1.5">
<div v-for="grant in grantedPrivileges" :key="grant.privilege" class="flex items-center gap-2 rounded border bg-muted/20 px-3 py-2">
<ShieldCheck class="h-3.5 w-3.5 shrink-0 text-primary" />
<span class="min-w-0 flex-1 truncate font-mono text-xs">{{ grant.privilege }}</span>
<Badge v-if="grant.adminOption" variant="outline" class="h-5 px-1.5 text-[10px]">ADMIN OPTION</Badge>
<Button
size="sm"
variant="ghost"
class="h-6 gap-1 px-2 text-xs text-destructive hover:text-destructive"
:disabled="selectedIsSystemUser || isPrivilegeGrantDisabled(grant.privilege)"
:title="selectedIsSystemUser ? t('damengUserAdmin.systemUserProtected') : isPrivilegeGrantDisabled(grant.privilege) ? t('damengUserAdmin.anyPrivilegeDisabled') : ''"
@click="previewRevokePrivilege(grant)"
>
<Trash2 class="h-3 w-3" />
{{ t("damengUserAdmin.revokePrivilege") }}
</Button>
</div>
<div v-if="grantedPrivileges.length === 0" class="rounded border border-dashed p-6 text-center text-xs text-muted-foreground">
{{ t("damengUserAdmin.noGrantedPrivileges") }}
</div>
</div>
</div>
</div>
</main>
</div>
<Dialog v-model:open="createDialogOpen">
<DialogContent class="max-w-md">
<DialogHeader>
<DialogTitle>{{ t("damengUserAdmin.newUser") }}</DialogTitle>
</DialogHeader>
<div class="grid gap-3 text-xs">
<label class="grid gap-1">
<span>{{ t("damengUserAdmin.username") }}</span>
<Input v-model="createUsername" class="h-8 text-xs" :placeholder="t('damengUserAdmin.usernamePlaceholder')" />
</label>
<label class="grid gap-1">
<span>{{ t("damengUserAdmin.userType") }}</span>
<Select v-model="createUserType">
<SelectTrigger class="h-8 text-xs"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem v-for="group in DAMENG_USER_GROUPS.slice(0, 3)" :key="group" :value="group">{{ groupLabel(group) }}</SelectItem>
</SelectContent>
</Select>
<span class="text-[10px] text-muted-foreground">{{ t("damengUserAdmin.userTypeHint") }}</span>
</label>
<label class="grid gap-1">
<span>{{ t("damengUserAdmin.password") }}</span>
<PasswordInput v-model="createPassword" class="h-8 text-xs" />
<span :class="createPassword.length > 0 && createPassword.length < 9 ? 'text-[10px] text-amber-600 dark:text-amber-400' : 'text-[10px] text-muted-foreground'">{{ t("damengUserAdmin.passwordPolicyHint") }}</span>
</label>
<label class="grid gap-1">
<span>{{ t("damengUserAdmin.tablespace") }}</span>
<Select v-if="tablespaces.length > 0" v-model="createTablespace">
<SelectTrigger class="h-8 text-xs"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem v-for="space in tablespaces" :key="space" :value="space">{{ space }}</SelectItem>
</SelectContent>
</Select>
<Input v-else v-model="createTablespace" class="h-8 text-xs" :placeholder="t('damengUserAdmin.tablespacePlaceholder')" />
</label>
<label class="flex items-center gap-2">
<input v-model="createLocked" type="checkbox" class="h-3.5 w-3.5 accent-primary" />
<span>{{ t("damengUserAdmin.locked") }}</span>
</label>
</div>
<DialogFooter>
<Button variant="outline" @click="createDialogOpen = false">{{ t("common.cancel") }}</Button>
<Button :disabled="!canCreateUser" @click="previewCreateUser">{{ t("damengUserAdmin.previewSql") }}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog v-model:open="passwordDialogOpen">
<DialogContent class="max-w-md">
<DialogHeader>
<DialogTitle>{{ t("damengUserAdmin.changePassword") }}</DialogTitle>
</DialogHeader>
<div class="grid gap-3 text-xs">
<label class="grid gap-1">
<span>{{ t("damengUserAdmin.newPassword") }}</span>
<PasswordInput v-model="passwordValue" class="h-8 text-xs" />
<span :class="passwordValue.length > 0 && passwordValue.length < 9 ? 'text-[10px] text-amber-600 dark:text-amber-400' : 'text-[10px] text-muted-foreground'">{{ t("damengUserAdmin.passwordPolicyHint") }}</span>
</label>
<label class="grid gap-1">
<span>{{ t("damengUserAdmin.confirmPassword") }}</span>
<PasswordInput v-model="passwordConfirm" class="h-8 text-xs" />
</label>
</div>
<DialogFooter>
<Button variant="outline" @click="passwordDialogOpen = false">{{ t("common.cancel") }}</Button>
<Button :disabled="!canChangePassword" @click="previewChangePassword">{{ t("damengUserAdmin.previewSql") }}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog v-model:open="grantRoleDialogOpen">
<DialogContent class="max-w-md">
<DialogHeader>
<DialogTitle>{{ t("damengUserAdmin.grantRole") }}</DialogTitle>
</DialogHeader>
<div class="grid gap-3 text-xs">
<label class="grid gap-1">
<span>{{ t("damengUserAdmin.roleToGrant") }}</span>
<Select v-model="grantRoleValue">
<SelectTrigger class="h-8 text-xs"><SelectValue :placeholder="t('damengUserAdmin.selectRole')" /></SelectTrigger>
<SelectContent>
<SelectItem v-for="role in availableRolesForGrant" :key="role.role" :value="role.role">{{ role.role }}</SelectItem>
</SelectContent>
</Select>
</label>
<p class="text-[11px] text-muted-foreground">{{ t("damengUserAdmin.grantRoleHint", { user: selectedUser?.username ?? "" }) }}</p>
</div>
<DialogFooter>
<Button variant="outline" @click="grantRoleDialogOpen = false">{{ t("common.cancel") }}</Button>
<Button :disabled="!grantRoleValue" @click="previewGrantRole">{{ t("damengUserAdmin.previewSql") }}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog v-model:open="grantPrivilegeDialogOpen">
<DialogContent class="max-w-md">
<DialogHeader>
<DialogTitle>{{ t("damengUserAdmin.grantPrivilege") }}</DialogTitle>
</DialogHeader>
<div class="grid gap-3 text-xs">
<label class="grid gap-1">
<span>{{ t("damengUserAdmin.privilegeToGrant") }}</span>
<Select v-model="grantPrivilegeValue">
<SelectTrigger class="h-8 text-xs"><SelectValue :placeholder="t('damengUserAdmin.selectPrivilege')" /></SelectTrigger>
<SelectContent>
<SelectItem v-for="privilege in availablePrivilegesForGrant" :key="privilege" :value="privilege" :disabled="isPrivilegeGrantDisabled(privilege)">{{ privilege }}</SelectItem>
</SelectContent>
</Select>
</label>
<p v-if="!enableDdlAnyPriv" class="text-[11px] text-amber-600 dark:text-amber-400">{{ t("damengUserAdmin.anyPrivilegeDisabled") }}</p>
<p class="text-[11px] text-muted-foreground">{{ t("damengUserAdmin.grantPrivilegeHint", { user: selectedUser?.username ?? "" }) }}</p>
</div>
<DialogFooter>
<Button variant="outline" @click="grantPrivilegeDialogOpen = false">{{ t("common.cancel") }}</Button>
<Button :disabled="!grantPrivilegeValue" @click="previewGrantPrivilege">{{ t("damengUserAdmin.previewSql") }}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog v-model:open="previewDialogOpen">
<DialogContent class="max-w-3xl">
<DialogHeader>
<DialogTitle>{{ t("damengUserAdmin.sqlPreview") }}</DialogTitle>
</DialogHeader>
<pre class="max-h-[50vh] overflow-auto rounded-md border bg-muted/30 p-3 text-xs"><code>{{ pendingSql }}</code></pre>
<DialogFooter>
<Button variant="outline" @click="previewDialogOpen = false">{{ t("common.cancel") }}</Button>
<Button :variant="pendingDanger ? 'destructive' : 'default'" :disabled="applying" @click="applyPendingSql">
<Loader2 v-if="applying" class="mr-1 h-3.5 w-3.5 animate-spin" />
{{ t("damengUserAdmin.applySql") }}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</template>
@@ -74,6 +74,8 @@ const ProcessListPanel = defineAsyncComponent(() => import("@/components/admin/P
const MySqlDashboard = defineAsyncComponent(() => import("@/components/admin/MySqlDashboard.vue"));
const PostgresDashboard = defineAsyncComponent(() => import("@/components/admin/PostgresDashboard.vue"));
const DamengJobAdmin = defineAsyncComponent(() => import("@/components/admin/DamengJobAdmin.vue"));
const DamengUserAdmin = defineAsyncComponent(() => import("@/components/admin/DamengUserAdmin.vue"));
const DamengRoleAdmin = defineAsyncComponent(() => import("@/components/admin/DamengRoleAdmin.vue"));
const ExplainPlanViewer = defineAsyncComponent(() => import("@/components/explain/ExplainPlanViewer.vue"));
const QueryChart = defineAsyncComponent(() => import("@/components/chart/QueryChart.vue"));
import { useQueryStore } from "@/stores/queryStore";
@@ -2119,6 +2121,14 @@ defineExpose({ focusSearch, refreshData, refreshQueryEditorCompletionCache, hand
<template v-else-if="activeTab.mode === 'dameng-jobs' && activeConnection">
<DamengJobAdmin :key="activeTab.id" :connection="activeConnection" />
</template>
<template v-else-if="activeTab.mode === 'dameng-users' && activeConnection">
<DamengUserAdmin :key="activeTab.id" :connection="activeConnection" />
</template>
<template v-else-if="activeTab.mode === 'dameng-roles' && activeConnection">
<DamengRoleAdmin :key="activeTab.id" :connection="activeConnection" />
</template>
</div>
</template>
@@ -45,6 +45,7 @@ import {
ListFilter,
Clipboard,
UsersRound,
ShieldCheck,
Activity,
Gauge,
CalendarClock,
@@ -725,6 +726,12 @@ async function toggle() {
} else if (node.type === "user-admin" && node.connectionId) {
await connectionStore.ensureConnected(node.connectionId);
queryStore.openUserAdmin(node.connectionId);
} else if (node.type === "dameng-users" && node.connectionId) {
await connectionStore.ensureConnected(node.connectionId);
queryStore.openDamengUsers(node.connectionId);
} else if (node.type === "dameng-roles" && node.connectionId) {
await connectionStore.ensureConnected(node.connectionId);
queryStore.openDamengRoles(node.connectionId);
} else if (node.type === "dameng-job-admin" && node.connectionId) {
await connectionStore.ensureConnected(node.connectionId);
queryStore.openDamengJobAdmin(node.connectionId);
@@ -1301,6 +1308,30 @@ async function openServerDashboard() {
}
}
async function openDamengUsers() {
const node = activeNode.value;
if (!node.connectionId) return;
try {
await connectionStore.ensureConnected(node.connectionId);
connectionStore.activeConnectionId = node.connectionId;
queryStore.openDamengUsers(node.connectionId);
} catch (e: any) {
toast(t("connection.connectFailed", { message: translateBackendError(t, e) }), 5000);
}
}
async function openDamengRoles() {
const node = activeNode.value;
if (!node.connectionId) return;
try {
await connectionStore.ensureConnected(node.connectionId);
connectionStore.activeConnectionId = node.connectionId;
queryStore.openDamengRoles(node.connectionId);
} catch (e: any) {
toast(t("connection.connectFailed", { message: translateBackendError(t, e) }), 5000);
}
}
async function openDamengJobAdmin() {
const node = activeNode.value;
if (!node.connectionId) return;
@@ -4148,6 +4179,8 @@ function buildConnectionSidebarMenu(context: SidebarMenuFactoryContext): boolean
items.push({ label: t("contextMenu.serverDashboard"), action: openServerDashboard, icon: Gauge });
}
if (currentDatabaseType() === "dameng") {
items.push({ label: t("contextMenu.damengUsers"), action: openDamengUsers, icon: UsersRound });
items.push({ label: t("contextMenu.damengRoles"), action: openDamengRoles, icon: ShieldCheck });
items.push({ label: t("contextMenu.damengJobAdmin"), action: openDamengJobAdmin, icon: CalendarClock });
}
if (canCopyFinalProxyPort.value) {
@@ -255,6 +255,10 @@ function getIconInfo(node: TreeNode): { icon: any; colorClass: string } | null {
return { icon: TableProperties, colorClass: "text-primary" };
case "user-admin":
return { icon: UsersRound, colorClass: "text-primary" };
case "dameng-users":
return { icon: UsersRound, colorClass: "text-primary" };
case "dameng-roles":
return { icon: ShieldCheck, colorClass: "text-primary" };
case "dameng-job-admin":
return { icon: CalendarClock, colorClass: "text-primary" };
case "index":
@@ -349,7 +353,7 @@ function isGroupLabel(node: TreeNode): boolean {
function displayLabel(node: TreeNode): string {
if (node.type === "load-more") return t(node.label);
if (node.type === "object-browser") return t(node.label, { count: node.objectCount ?? 0 });
if (node.type === "user-admin" || node.type === "dameng-job-admin") return t(node.label);
if (node.type === "user-admin" || node.type === "dameng-users" || node.type === "dameng-roles" || node.type === "dameng-job-admin") return t(node.label);
if (node.type === "linked-server-root") return t(node.label);
if (node.type === "mqtt-topic" && node.id.endsWith(":mqtt-topic:__console__")) return t(node.label);
if (node.label === "tree.defaultDatabase") return t(node.label);
+110
View File
@@ -2540,6 +2540,10 @@ export default {
openUserAdmin: "Open Users & Privileges",
processList: "Process List",
serverDashboard: "Server Dashboard",
damengUsers: "Dameng Users",
openDamengUsers: "Open Dameng User Admin",
damengRoles: "Dameng Roles",
openDamengRoles: "Open Dameng Role Admin",
damengJobAdmin: "Dameng Agent Jobs",
openDamengJobAdmin: "Open Dameng Agent Jobs",
duplicateConnection: "Duplicate Connection",
@@ -2866,6 +2870,8 @@ export default {
tree: {
savedSql: "SQL Library",
userAdmin: "Users & Privileges",
damengUsers: "Users",
damengRoles: "Roles",
damengJobAdmin: "Agent Jobs",
linkedServers: "Linked Servers",
defaultDatabase: "Default DB",
@@ -3114,6 +3120,110 @@ export default {
applySuccess: "Job operation applied",
applyFailed: "Execution failed: {message}",
},
damengUserAdmin: {
title: "Dameng Users",
unsupported: "This page only supports Dameng database connections.",
newUser: "New User",
searchUser: "Search users",
loadFailed: "Load failed",
emptyUsers: "No users, or the current account cannot read the user list.",
selectUser: "Select a user",
username: "Username",
usernamePlaceholder: "e.g. APP_USER",
password: "Password",
passwordPolicyHint: "Password must satisfy the DM8 password policy (default: at least 9 characters; ideally mix uppercase, lowercase and digits)",
tablespace: "Default tablespace",
tablespacePlaceholder: "Enter tablespace name",
locked: "Lock account after creation",
changePassword: "Change Password",
newPassword: "New password",
confirmPassword: "Confirm password",
open: "Open",
systemUser: "System user",
systemUserAdmin: "Admin user",
systemUserAuditor: "Audit user",
systemUserSecurity: "Security user",
systemUserSystem: "System user",
cannotChangePassword: "This user's password cannot be changed by the current connection (only SYSDBA or the user themselves can change it)",
inheritedFrom: "Inherited from {via}",
userType: "User type",
userTypeHint: "Admin automatically grants DBA, audit grants DB_AUDIT_ADMIN, security grants DB_POLICY_ADMIN",
groupOther: "Other users",
systemUserProtected: "System users cannot be altered or dropped",
builtinRole: "Built-in role",
builtinRoleProtected: "Built-in roles cannot be revoked",
defaultTablespace: "Default tablespace",
created: "Created",
lock: "Lock",
unlock: "Unlock",
dropUser: "Drop User",
roles: "Roles",
systemPrivileges: "System Privileges",
grantedRoles: "Granted roles",
grantedPrivileges: "Granted privileges",
grantRole: "Grant Role",
revokeRole: "Revoke Role",
roleToGrant: "Role to grant",
selectRole: "Select role",
grantRoleHint: "Grant the role to user {user}.",
noRolesToGrant: "No roles available to grant",
noGrantedRoles: "This user has no granted roles",
grantPrivilege: "Grant Privilege",
revokePrivilege: "Revoke Privilege",
privilegeToGrant: "Privilege to grant",
selectPrivilege: "Select privilege",
grantPrivilegeHint: "Grant the system privilege to user {user}.",
noPrivilegesToGrant: "No privileges available to grant",
noGrantedPrivileges: "This user has no granted system privileges",
previewSql: "Preview SQL",
sqlPreview: "SQL Preview",
applySql: "Apply SQL",
applySuccess: "User operation applied",
applyFailed: "Execution failed: {message}",
enableDdlAnyPrivHint: "Hint: DM8 disables granting DDL ANY privileges by default (ENABLE_DDL_ANY_PRIV=0). To grant ANY-type privileges, run as SYSDBA: sp_set_para_value(1,'ENABLE_DDL_ANY_PRIV',1). For non-ANY privileges, make sure the current account holds the ADMIN OPTION for that privilege.",
anyPrivilegeDisabled: "ANY-type privileges are disabled by DM8 (ENABLE_DDL_ANY_PRIV=0) and cannot be granted or revoked",
},
damengRoleAdmin: {
title: "Dameng Roles",
unsupported: "This page only supports Dameng database connections.",
newRole: "New Role",
searchRole: "Search roles",
loadFailed: "Load failed",
emptyRoles: "No roles, or the current account cannot read the role list.",
selectRole: "Select a role",
roleName: "Role name",
roleNamePlaceholder: "e.g. APP_ROLE",
members: "Members",
systemPrivileges: "System Privileges",
grantedToUsers: "Users granted this role",
grantedPrivileges: "Granted privileges",
noMembers: "No users have been granted this role",
grantToUser: "Grant to User",
userToGrant: "User to grant",
selectUser: "Select user",
grantUserHint: "Grant role {role} to the user.",
noUsersToGrant: "No users available to grant",
revokeFromUser: "Revoke",
systemUser: "System user",
systemUserProtected: "Cannot revoke from system users",
builtinRole: "Built-in role",
builtinRoleProtected: "Predefined roles cannot be dropped or modified",
grantPrivilege: "Grant Privilege",
revokePrivilege: "Revoke Privilege",
privilegeToGrant: "Privilege to grant",
selectPrivilege: "Select privilege",
grantPrivilegeHint: "Grant the system privilege to role {role}.",
noPrivilegesToGrant: "No privileges available to grant",
noGrantedPrivileges: "This role has no granted system privileges",
dropRole: "Drop Role",
previewSql: "Preview SQL",
sqlPreview: "SQL Preview",
applySql: "Apply SQL",
applySuccess: "Role operation applied",
applyFailed: "Execution failed: {message}",
enableDdlAnyPrivHint: "Hint: DM8 disables granting DDL ANY privileges by default (ENABLE_DDL_ANY_PRIV=0). To grant ANY-type privileges, run as SYSDBA: sp_set_para_value(1,'ENABLE_DDL_ANY_PRIV',1). For non-ANY privileges, make sure the current account holds the ADMIN OPTION for that privilege.",
anyPrivilegeDisabled: "ANY-type privileges are disabled by DM8 (ENABLE_DDL_ANY_PRIV=0) and cannot be granted or revoked",
},
objects: {
all: "All",
tables: "Tables",
+112
View File
@@ -2474,6 +2474,10 @@ export default withEnglishFallback({
openUserAdmin: "Abrir Usuarios y Privilegios",
damengJobAdmin: "Trabajos del agente Dameng",
openDamengJobAdmin: "Abrir trabajos del agente Dameng",
damengUsers: "Usuarios Dameng",
openDamengUsers: "Abrir administración de usuarios Dameng",
damengRoles: "Roles Dameng",
openDamengRoles: "Abrir administración de roles Dameng",
duplicateConnection: "Duplicar conexión",
duplicateSelectedConnections: "Duplicar {count} conexiones seleccionadas",
instanceInfo: "Información de la instancia",
@@ -2800,6 +2804,8 @@ export default withEnglishFallback({
tree: {
savedSql: "Biblioteca SQL",
userAdmin: "Usuarios y Privilegios",
damengUsers: "Usuarios",
damengRoles: "Roles",
damengJobAdmin: "Trabajos del agente",
linkedServers: "Servidores vinculados",
defaultDatabase: "Base predeterminada",
@@ -2959,6 +2965,112 @@ export default withEnglishFallback({
applySuccess: "Operación de trabajo aplicada",
applyFailed: "Ejecución fallida: {message}",
},
damengUserAdmin: {
title: "Usuarios Dameng",
unsupported: "Esta página solo admite conexiones a bases de datos Dameng.",
newUser: "Nuevo usuario",
searchUser: "Buscar usuarios",
loadFailed: "Error al cargar",
emptyUsers: "Sin usuarios, o la cuenta actual no puede leer la lista de usuarios.",
selectUser: "Seleccione un usuario",
username: "Nombre de usuario",
usernamePlaceholder: "p. ej. APP_USER",
password: "Contraseña",
passwordPolicyHint: "La contraseña debe cumplir la política de contraseñas de DM8 (por defecto: al menos 9 caracteres; idealmente combine mayúsculas, minúsculas y dígitos)",
tablespace: "Tablespace predeterminado",
tablespacePlaceholder: "Introduzca el nombre del tablespace",
locked: "Bloquear cuenta tras la creación",
changePassword: "Cambiar contraseña",
newPassword: "Nueva contraseña",
confirmPassword: "Confirmar contraseña",
open: "Abierto",
systemUser: "Usuario del sistema",
systemUserAdmin: "Usuario administrador",
systemUserAuditor: "Usuario de auditoría",
systemUserSecurity: "Usuario de seguridad",
systemUserSystem: "Usuario del sistema",
cannotChangePassword: "La contraseña de este usuario no se puede cambiar desde la conexión actual (solo SYSDBA o el propio usuario puede cambiarla)",
inheritedFrom: "Heredado de {via}",
userType: "Tipo de usuario",
userTypeHint: "Administrador otorga automáticamente DBA, auditoría otorga DB_AUDIT_ADMIN, seguridad otorga DB_POLICY_ADMIN",
groupOther: "Otros usuarios",
systemUserProtected: "Los usuarios del sistema no se pueden modificar ni eliminar",
builtinRole: "Rol integrado",
builtinRoleProtected: "Los roles integrados no se pueden revocar",
defaultTablespace: "Tablespace predeterminado",
created: "Creado",
lock: "Bloquear",
unlock: "Desbloquear",
dropUser: "Eliminar usuario",
roles: "Roles",
systemPrivileges: "Privilegios del sistema",
grantedRoles: "Roles concedidos",
grantedPrivileges: "Privilegios concedidos",
grantRole: "Conceder rol",
revokeRole: "Revocar rol",
roleToGrant: "Rol a conceder",
selectRole: "Seleccionar rol",
grantRoleHint: "Conceder el rol al usuario {user}.",
noRolesToGrant: "No hay roles disponibles para conceder",
noGrantedRoles: "Este usuario no tiene roles concedidos",
grantPrivilege: "Conceder privilegio",
revokePrivilege: "Revocar privilegio",
privilegeToGrant: "Privilegio a conceder",
selectPrivilege: "Seleccionar privilegio",
grantPrivilegeHint: "Conceder el privilegio del sistema al usuario {user}.",
noPrivilegesToGrant: "No hay privilegios disponibles para conceder",
noGrantedPrivileges: "Este usuario no tiene privilegios del sistema concedidos",
previewSql: "Vista previa SQL",
sqlPreview: "Vista previa SQL",
applySql: "Ejecutar SQL",
applySuccess: "Operación de usuario aplicada",
applyFailed: "Error de ejecución: {message}",
enableDdlAnyPrivHint:
"Sugerencia: DM8 deshabilita la concesión de privilegios ANY de DDL de forma predeterminada (ENABLE_DDL_ANY_PRIV=0). Para otorgar privilegios ANY, ejecute como SYSDBA: sp_set_para_value(1,'ENABLE_DDL_ANY_PRIV',1). Para privilegios no ANY, asegúrese de que la cuenta actual tenga el ADMIN OPTION de ese privilegio.",
anyPrivilegeDisabled: "Los privilegios de tipo ANY están deshabilitados por DM8 (ENABLE_DDL_ANY_PRIV=0) y no se pueden otorgar ni revocar",
},
damengRoleAdmin: {
title: "Roles Dameng",
unsupported: "Esta página solo admite conexiones a bases de datos Dameng.",
newRole: "Nuevo rol",
searchRole: "Buscar roles",
loadFailed: "Error al cargar",
emptyRoles: "Sin roles, o la cuenta actual no puede leer la lista de roles.",
selectRole: "Seleccione un rol",
roleName: "Nombre del rol",
roleNamePlaceholder: "p. ej. APP_ROLE",
members: "Miembros",
systemPrivileges: "Privilegios del sistema",
grantedToUsers: "Usuarios con este rol",
grantedPrivileges: "Privilegios concedidos",
noMembers: "Ningún usuario tiene este rol concedido",
grantToUser: "Conceder a usuario",
userToGrant: "Usuario a conceder",
selectUser: "Seleccionar usuario",
grantUserHint: "Conceder el rol {role} al usuario.",
noUsersToGrant: "No hay usuarios disponibles para conceder",
revokeFromUser: "Revocar",
systemUser: "Usuario del sistema",
systemUserProtected: "No se puede revocar de usuarios del sistema",
builtinRole: "Rol integrado",
builtinRoleProtected: "Los roles predefinidos no se pueden eliminar ni modificar",
grantPrivilege: "Conceder privilegio",
revokePrivilege: "Revocar privilegio",
privilegeToGrant: "Privilegio a conceder",
selectPrivilege: "Seleccionar privilegio",
grantPrivilegeHint: "Conceder el privilegio del sistema al rol {role}.",
noPrivilegesToGrant: "No hay privilegios disponibles para conceder",
noGrantedPrivileges: "Este rol no tiene privilegios del sistema concedidos",
dropRole: "Eliminar rol",
previewSql: "Vista previa SQL",
sqlPreview: "Vista previa SQL",
applySql: "Ejecutar SQL",
applySuccess: "Operación de rol aplicada",
applyFailed: "Error de ejecución: {message}",
enableDdlAnyPrivHint:
"Sugerencia: DM8 deshabilita la concesión de privilegios ANY de DDL de forma predeterminada (ENABLE_DDL_ANY_PRIV=0). Para otorgar privilegios ANY, ejecute como SYSDBA: sp_set_para_value(1,'ENABLE_DDL_ANY_PRIV',1). Para privilegios no ANY, asegúrese de que la cuenta actual tenga el ADMIN OPTION de ese privilegio.",
anyPrivilegeDisabled: "Los privilegios de tipo ANY están deshabilitados por DM8 (ENABLE_DDL_ANY_PRIV=0) y no se pueden otorgar ni revocar",
},
objects: {
all: "Todos",
tables: "Tablas",
+112
View File
@@ -2472,6 +2472,10 @@ export default withEnglishFallback({
openUserAdmin: "Apri Utenti e Privilegi",
damengJobAdmin: "Job agente Dameng",
openDamengJobAdmin: "Apri job agente Dameng",
damengUsers: "Utenti Dameng",
openDamengUsers: "Apri gestione utenti Dameng",
damengRoles: "Ruoli Dameng",
openDamengRoles: "Apri gestione ruoli Dameng",
duplicateConnection: "Duplica Connessione",
duplicateSelectedConnections: "Duplica {count} connessioni selezionate",
newQuery: "Nuova Query",
@@ -2798,6 +2802,8 @@ export default withEnglishFallback({
tree: {
savedSql: "Libreria SQL",
userAdmin: "Utenti e Privilegi",
damengUsers: "Utenti",
damengRoles: "Ruoli",
damengJobAdmin: "Job agente",
linkedServers: "Server Collegati",
defaultDatabase: "DB Predefinito",
@@ -2957,6 +2963,112 @@ export default withEnglishFallback({
applySuccess: "Operazione job applicata",
applyFailed: "Esecuzione non riuscita: {message}",
},
damengUserAdmin: {
title: "Utenti Dameng",
unsupported: "Questa pagina supporta solo connessioni a database Dameng.",
newUser: "Nuovo utente",
searchUser: "Cerca utenti",
loadFailed: "Caricamento non riuscito",
emptyUsers: "Nessun utente o l'account corrente non può leggere l'elenco utenti.",
selectUser: "Seleziona un utente",
username: "Nome utente",
usernamePlaceholder: "es. APP_USER",
password: "Password",
passwordPolicyHint: "La password deve soddisfare i criteri DM8 (default: almeno 9 caratteri; idealmente maiuscole, minuscole e cifre)",
tablespace: "Tablespace predefinito",
tablespacePlaceholder: "Inserisci il nome del tablespace",
locked: "Blocca account dopo la creazione",
changePassword: "Cambia password",
newPassword: "Nuova password",
confirmPassword: "Conferma password",
open: "Aperto",
systemUser: "Utente di sistema",
systemUserAdmin: "Utente amministratore",
systemUserAuditor: "Utente audit",
systemUserSecurity: "Utente di sicurezza",
systemUserSystem: "Utente di sistema",
cannotChangePassword: "La password di questo utente non può essere modificata dalla connessione corrente (solo SYSDBA o l'utente stesso possono modificarla)",
inheritedFrom: "Ereditato da {via}",
userType: "Tipo di utente",
userTypeHint: "Amministratore concede automaticamente DBA, audit concede DB_AUDIT_ADMIN, sicurezza concede DB_POLICY_ADMIN",
groupOther: "Altri utenti",
systemUserProtected: "Gli utenti di sistema non possono essere modificati o eliminati",
builtinRole: "Ruolo integrato",
builtinRoleProtected: "I ruoli integrati non possono essere revocati",
defaultTablespace: "Tablespace predefinito",
created: "Creato",
lock: "Blocca",
unlock: "Sblocca",
dropUser: "Elimina utente",
roles: "Ruoli",
systemPrivileges: "Privilegi di sistema",
grantedRoles: "Ruoli concessi",
grantedPrivileges: "Privilegi concessi",
grantRole: "Concedi ruolo",
revokeRole: "Revoca ruolo",
roleToGrant: "Ruolo da concedere",
selectRole: "Seleziona ruolo",
grantRoleHint: "Concedi il ruolo all'utente {user}.",
noRolesToGrant: "Nessun ruolo disponibile da concedere",
noGrantedRoles: "Questo utente non ha ruoli concessi",
grantPrivilege: "Concedi privilegio",
revokePrivilege: "Revoca privilegio",
privilegeToGrant: "Privilegio da concedere",
selectPrivilege: "Seleziona privilegio",
grantPrivilegeHint: "Concedi il privilegio di sistema all'utente {user}.",
noPrivilegesToGrant: "Nessun privilegio disponibile da concedere",
noGrantedPrivileges: "Questo utente non ha privilegi di sistema concessi",
previewSql: "Anteprima SQL",
sqlPreview: "Anteprima SQL",
applySql: "Esegui SQL",
applySuccess: "Operazione utente applicata",
applyFailed: "Esecuzione non riuscita: {message}",
enableDdlAnyPrivHint:
"Suggerimento: DM8 disabilita la concessione dei privilegi ANY DDL per impostazione predefinita (ENABLE_DDL_ANY_PRIV=0). Per concedere privilegi ANY, esegui come SYSDBA: sp_set_para_value(1,'ENABLE_DDL_ANY_PRIV',1). Per privilegi non ANY, assicurati che l'account corrente abbia l'ADMIN OPTION per quel privilegio.",
anyPrivilegeDisabled: "I privilegi di tipo ANY sono disabilitati da DM8 (ENABLE_DDL_ANY_PRIV=0) e non possono essere concessi o revocati",
},
damengRoleAdmin: {
title: "Ruoli Dameng",
unsupported: "Questa pagina supporta solo connessioni a database Dameng.",
newRole: "Nuovo ruolo",
searchRole: "Cerca ruoli",
loadFailed: "Caricamento non riuscito",
emptyRoles: "Nessun ruolo o l'account corrente non può leggere l'elenco ruoli.",
selectRole: "Seleziona un ruolo",
roleName: "Nome ruolo",
roleNamePlaceholder: "es. APP_ROLE",
members: "Membri",
systemPrivileges: "Privilegi di sistema",
grantedToUsers: "Utenti con questo ruolo",
grantedPrivileges: "Privilegi concessi",
noMembers: "Nessun utente ha questo ruolo concesso",
grantToUser: "Concedi all'utente",
userToGrant: "Utente a cui concedere",
selectUser: "Seleziona utente",
grantUserHint: "Concedi il ruolo {role} all'utente.",
noUsersToGrant: "Nessun utente disponibile da concedere",
revokeFromUser: "Revoca",
systemUser: "Utente di sistema",
systemUserProtected: "Impossibile revocare dagli utenti di sistema",
builtinRole: "Ruolo integrato",
builtinRoleProtected: "I ruoli predefiniti non possono essere eliminati o modificati",
grantPrivilege: "Concedi privilegio",
revokePrivilege: "Revoca privilegio",
privilegeToGrant: "Privilegio da concedere",
selectPrivilege: "Seleziona privilegio",
grantPrivilegeHint: "Concedi il privilegio di sistema al ruolo {role}.",
noPrivilegesToGrant: "Nessun privilegio disponibile da concedere",
noGrantedPrivileges: "Questo ruolo non ha privilegi di sistema concessi",
dropRole: "Elimina ruolo",
previewSql: "Anteprima SQL",
sqlPreview: "Anteprima SQL",
applySql: "Esegui SQL",
applySuccess: "Operazione ruolo applicata",
applyFailed: "Esecuzione non riuscita: {message}",
enableDdlAnyPrivHint:
"Suggerimento: DM8 disabilita la concessione dei privilegi ANY DDL per impostazione predefinita (ENABLE_DDL_ANY_PRIV=0). Per concedere privilegi ANY, esegui come SYSDBA: sp_set_para_value(1,'ENABLE_DDL_ANY_PRIV',1). Per privilegi non ANY, assicurati che l'account corrente abbia l'ADMIN OPTION per quel privilegio.",
anyPrivilegeDisabled: "I privilegi di tipo ANY sono disabilitati da DM8 (ENABLE_DDL_ANY_PRIV=0) e non possono essere concessi o revocati",
},
objects: {
all: "Tutti",
tables: "Tabelle",
+110
View File
@@ -2498,6 +2498,10 @@ export default withEnglishFallback({
openUserAdmin: "ユーザーと権限を開く",
damengJobAdmin: "Dameng エージェントジョブ",
openDamengJobAdmin: "Dameng エージェントジョブを開く",
damengUsers: "Dameng ユーザー",
openDamengUsers: "Dameng ユーザー管理を開く",
damengRoles: "Dameng ロール",
openDamengRoles: "Dameng ロール管理を開く",
duplicateConnection: "接続を複製",
duplicateSelectedConnections: "選択した{count}件の接続を複製",
newQuery: "新しいクエリ",
@@ -2825,6 +2829,8 @@ export default withEnglishFallback({
tree: {
savedSql: "SQLライブラリ",
userAdmin: "ユーザーと権限",
damengUsers: "ユーザー",
damengRoles: "ロール",
damengJobAdmin: "エージェントジョブ",
defaultDatabase: "デフォルトDB",
columns: "列",
@@ -3019,6 +3025,110 @@ export default withEnglishFallback({
applySuccess: "ジョブ操作を適用しました",
applyFailed: "実行に失敗しました: {message}",
},
damengUserAdmin: {
title: "Dameng ユーザー",
unsupported: "このページは Dameng データベース接続のみサポートしています。",
newUser: "ユーザー作成",
searchUser: "ユーザーを検索",
loadFailed: "読み込み失敗",
emptyUsers: "ユーザーがありません。または現在のアカウントにユーザー一覧の読み取り権限がありません。",
selectUser: "ユーザーを選択してください",
username: "ユーザー名",
usernamePlaceholder: "例: APP_USER",
password: "パスワード",
passwordPolicyHint: "パスワードはDM8のパスワードポリシーを満たす必要があります(デフォルト: 9文字以上。大文字・小文字・数字の組み合わせ推奨)",
tablespace: "デフォルト表領域",
tablespacePlaceholder: "表領域名を入力",
locked: "作成後にアカウントをロック",
changePassword: "パスワード変更",
newPassword: "新しいパスワード",
confirmPassword: "パスワード確認",
open: "通常",
systemUser: "システムユーザー",
systemUserAdmin: "管理ユーザー",
systemUserAuditor: "監査ユーザー",
systemUserSecurity: "セキュリティユーザー",
systemUserSystem: "システムユーザー",
cannotChangePassword: "このユーザーのパスワードは現在の接続では変更できません(変更できるのは SYSDBA またはユーザー本人のみ)",
inheritedFrom: "{via} から継承",
userType: "ユーザータイプ",
userTypeHint: "管理ユーザーはDBA、監査ユーザーはDB_AUDIT_ADMIN、セキュリティユーザーはDB_POLICY_ADMINを自動付与します",
groupOther: "その他のユーザー",
systemUserProtected: "システムユーザーは変更・削除できません",
builtinRole: "組み込みロール",
builtinRoleProtected: "組み込みロールは回収できません",
defaultTablespace: "デフォルト表領域",
created: "作成日時",
lock: "ロック",
unlock: "ロック解除",
dropUser: "ユーザー削除",
roles: "ロール",
systemPrivileges: "システム権限",
grantedRoles: "付与済みロール",
grantedPrivileges: "付与済み権限",
grantRole: "ロール付与",
revokeRole: "ロール回収",
roleToGrant: "付与するロール",
selectRole: "ロールを選択",
grantRoleHint: "ユーザー {user} にロールを付与します。",
noRolesToGrant: "付与できるロールがありません",
noGrantedRoles: "このユーザーには付与されたロールがありません",
grantPrivilege: "権限付与",
revokePrivilege: "権限回収",
privilegeToGrant: "付与する権限",
selectPrivilege: "権限を選択",
grantPrivilegeHint: "ユーザー {user} にシステム権限を付与します。",
noPrivilegesToGrant: "付与できる権限がありません",
noGrantedPrivileges: "このユーザーには付与されたシステム権限がありません",
previewSql: "SQL プレビュー",
sqlPreview: "SQL プレビュー",
applySql: "SQL を実行",
applySuccess: "ユーザー操作を実行しました",
applyFailed: "実行に失敗しました: {message}",
enableDdlAnyPrivHint: "ヒント: DM8はデフォルトでDDL ANY権限の付与を無効にしています(ENABLE_DDL_ANY_PRIV=0)。ANY系権限を付与するにはSYSDBAで実行: sp_set_para_value(1,'ENABLE_DDL_ANY_PRIV',1)。ANY以外の権限は、現在のアカウントがその権限のADMIN OPTIONを持っている必要があります。",
anyPrivilegeDisabled: "ANY系権限はDM8で無効化されています(ENABLE_DDL_ANY_PRIV=0)ため、付与・取り消しはできません",
},
damengRoleAdmin: {
title: "Dameng ロール",
unsupported: "このページは Dameng データベース接続のみサポートしています。",
newRole: "ロール作成",
searchRole: "ロールを検索",
loadFailed: "読み込み失敗",
emptyRoles: "ロールがありません。または現在のアカウントにロール一覧の読み取り権限がありません。",
selectRole: "ロールを選択してください",
roleName: "ロール名",
roleNamePlaceholder: "例: APP_ROLE",
members: "メンバー",
systemPrivileges: "システム権限",
grantedToUsers: "このロールが付与されたユーザー",
grantedPrivileges: "付与済み権限",
noMembers: "このロールが付与されたユーザーはいません",
grantToUser: "ユーザーに付与",
userToGrant: "付与するユーザー",
selectUser: "ユーザーを選択",
grantUserHint: "ロール {role} をユーザーに付与します。",
noUsersToGrant: "付与できるユーザーがいません",
revokeFromUser: "回収",
systemUser: "システムユーザー",
systemUserProtected: "システムユーザーからは回収できません",
builtinRole: "組み込みロール",
builtinRoleProtected: "定義済みロールは削除・変更できません",
grantPrivilege: "権限付与",
revokePrivilege: "権限回収",
privilegeToGrant: "付与する権限",
selectPrivilege: "権限を選択",
grantPrivilegeHint: "ロール {role} にシステム権限を付与します。",
noPrivilegesToGrant: "付与できる権限がありません",
noGrantedPrivileges: "このロールには付与されたシステム権限がありません",
dropRole: "ロール削除",
previewSql: "SQL プレビュー",
sqlPreview: "SQL プレビュー",
applySql: "SQL を実行",
applySuccess: "ロール操作を実行しました",
applyFailed: "実行に失敗しました: {message}",
enableDdlAnyPrivHint: "ヒント: DM8はデフォルトでDDL ANY権限の付与を無効にしています(ENABLE_DDL_ANY_PRIV=0)。ANY系権限を付与するにはSYSDBAで実行: sp_set_para_value(1,'ENABLE_DDL_ANY_PRIV',1)。ANY以外の権限は、現在のアカウントがその権限のADMIN OPTIONを持っている必要があります。",
anyPrivilegeDisabled: "ANY系権限はDM8で無効化されています(ENABLE_DDL_ANY_PRIV=0)ため、付与・取り消しはできません",
},
objects: {
all: "すべて",
tables: "テーブル",
+110
View File
@@ -2399,6 +2399,10 @@ export default withEnglishFallback({
serverDashboard: "서버 대시보드",
damengJobAdmin: "Dameng 에이전트 작업",
openDamengJobAdmin: "Dameng 에이전트 작업 열기",
damengUsers: "Dameng 사용자",
openDamengUsers: "Dameng 사용자 관리 열기",
damengRoles: "Dameng 역할",
openDamengRoles: "Dameng 역할 관리 열기",
duplicateConnection: "연결 복제",
duplicateSelectedConnections: "선택한 연결 {count}개 복제",
newQuery: "새 쿼리",
@@ -2717,6 +2721,8 @@ export default withEnglishFallback({
tree: {
savedSql: "SQL 라이브러리",
userAdmin: "사용자 및 권한",
damengUsers: "사용자",
damengRoles: "역할",
damengJobAdmin: "에이전트 작업",
linkedServers: "연결된 서버",
defaultDatabase: "기본 DB",
@@ -2964,6 +2970,110 @@ export default withEnglishFallback({
applySuccess: "작업 작업이 적용되었습니다",
applyFailed: "실행 실패: {message}",
},
damengUserAdmin: {
title: "Dameng 사용자",
unsupported: "이 페이지는 Dameng 데이터베이스 연결만 지원합니다.",
newUser: "새 사용자",
searchUser: "사용자 검색",
loadFailed: "불러오기 실패",
emptyUsers: "사용자가 없거나 현재 계정에 사용자 목록을 읽을 권한이 없습니다.",
selectUser: "사용자를 선택하세요",
username: "사용자 이름",
usernamePlaceholder: "예: APP_USER",
password: "비밀번호",
passwordPolicyHint: "비밀번호는 DM8 암호 정책을 충족해야 합니다(기본: 9자 이상, 대소문자와 숫자 조합 권장)",
tablespace: "기본 테이블스페이스",
tablespacePlaceholder: "테이블스페이스 이름 입력",
locked: "생성 후 계정 잠금",
changePassword: "비밀번호 변경",
newPassword: "새 비밀번호",
confirmPassword: "비밀번호 확인",
open: "정상",
systemUser: "시스템 사용자",
systemUserAdmin: "관리 사용자",
systemUserAuditor: "감사 사용자",
systemUserSecurity: "보안 사용자",
systemUserSystem: "시스템 사용자",
cannotChangePassword: "이 사용자의 비밀번호는 현재 연결에서 변경할 수 없습니다(SYSDBA 또는 사용자 본인만 변경 가능)",
inheritedFrom: "{via}에서 상속됨",
userType: "사용자 유형",
userTypeHint: "관리 사용자는 DBA, 감사 사용자는 DB_AUDIT_ADMIN, 보안 사용자는 DB_POLICY_ADMIN을 자동 부여합니다",
groupOther: "기타 사용자",
systemUserProtected: "시스템 사용자는 수정하거나 삭제할 수 없습니다",
builtinRole: "내장 역할",
builtinRoleProtected: "내장 역할은 회수할 수 없습니다",
defaultTablespace: "기본 테이블스페이스",
created: "생성 시간",
lock: "잠금",
unlock: "잠금 해제",
dropUser: "사용자 삭제",
roles: "역할",
systemPrivileges: "시스템 권한",
grantedRoles: "부여된 역할",
grantedPrivileges: "부여된 권한",
grantRole: "역할 부여",
revokeRole: "역할 회수",
roleToGrant: "부여할 역할",
selectRole: "역할 선택",
grantRoleHint: "사용자 {user}에게 역할을 부여합니다.",
noRolesToGrant: "부여할 수 있는 역할이 없습니다",
noGrantedRoles: "이 사용자에게 부여된 역할이 없습니다",
grantPrivilege: "권한 부여",
revokePrivilege: "권한 회수",
privilegeToGrant: "부여할 권한",
selectPrivilege: "권한 선택",
grantPrivilegeHint: "사용자 {user}에게 시스템 권한을 부여합니다.",
noPrivilegesToGrant: "부여할 수 있는 권한이 없습니다",
noGrantedPrivileges: "이 사용자에게 부여된 시스템 권한이 없습니다",
previewSql: "SQL 미리보기",
sqlPreview: "SQL 미리보기",
applySql: "SQL 실행",
applySuccess: "사용자 작업이 실행되었습니다",
applyFailed: "실행 실패: {message}",
enableDdlAnyPrivHint: "힌트: DM8은 기본적으로 DDL ANY 권한 부여를 비활성화합니다(ENABLE_DDL_ANY_PRIV=0). ANY 권한을 부여하려면 SYSDBA로 실행: sp_set_para_value(1,'ENABLE_DDL_ANY_PRIV',1). ANY가 아닌 권한은 현재 계정에 해당 권한의 ADMIN OPTION이 있어야 합니다.",
anyPrivilegeDisabled: "ANY 유형 권한은 DM8에서 비활성화되어(ENABLE_DDL_ANY_PRIV=0) 부여하거나 회수할 수 없습니다",
},
damengRoleAdmin: {
title: "Dameng 역할",
unsupported: "이 페이지는 Dameng 데이터베이스 연결만 지원합니다.",
newRole: "새 역할",
searchRole: "역할 검색",
loadFailed: "불러오기 실패",
emptyRoles: "역할이 없거나 현재 계정에 역할 목록을 읽을 권한이 없습니다.",
selectRole: "역할을 선택하세요",
roleName: "역할 이름",
roleNamePlaceholder: "예: APP_ROLE",
members: "멤버",
systemPrivileges: "시스템 권한",
grantedToUsers: "이 역할이 부여된 사용자",
grantedPrivileges: "부여된 권한",
noMembers: "이 역할이 부여된 사용자가 없습니다",
grantToUser: "사용자에게 부여",
userToGrant: "부여할 사용자",
selectUser: "사용자 선택",
grantUserHint: "역할 {role}을 사용자에게 부여합니다.",
noUsersToGrant: "부여할 수 있는 사용자가 없습니다",
revokeFromUser: "회수",
systemUser: "시스템 사용자",
systemUserProtected: "시스템 사용자에게서는 회수할 수 없습니다",
builtinRole: "내장 역할",
builtinRoleProtected: "미리 정의된 역할은 삭제하거나 수정할 수 없습니다",
grantPrivilege: "권한 부여",
revokePrivilege: "권한 회수",
privilegeToGrant: "부여할 권한",
selectPrivilege: "권한 선택",
grantPrivilegeHint: "역할 {role}에 시스템 권한을 부여합니다.",
noPrivilegesToGrant: "부여할 수 있는 권한이 없습니다",
noGrantedPrivileges: "이 역할에 부여된 시스템 권한이 없습니다",
dropRole: "역할 삭제",
previewSql: "SQL 미리보기",
sqlPreview: "SQL 미리보기",
applySql: "SQL 실행",
applySuccess: "역할 작업이 실행되었습니다",
applyFailed: "실행 실패: {message}",
enableDdlAnyPrivHint: "힌트: DM8은 기본적으로 DDL ANY 권한 부여를 비활성화합니다(ENABLE_DDL_ANY_PRIV=0). ANY 권한을 부여하려면 SYSDBA로 실행: sp_set_para_value(1,'ENABLE_DDL_ANY_PRIV',1). ANY가 아닌 권한은 현재 계정에 해당 권한의 ADMIN OPTION이 있어야 합니다.",
anyPrivilegeDisabled: "ANY 유형 권한은 DM8에서 비활성화되어(ENABLE_DDL_ANY_PRIV=0) 부여하거나 회수할 수 없습니다",
},
objects: {
all: "전체",
tables: "테이블",
+112
View File
@@ -2474,6 +2474,10 @@ export default withEnglishFallback({
openUserAdmin: "Abrir Usuários e Privilégios",
damengJobAdmin: "Jobs do agente Dameng",
openDamengJobAdmin: "Abrir jobs do agente Dameng",
damengUsers: "Usuários Dameng",
openDamengUsers: "Abrir administração de usuários Dameng",
damengRoles: "Funções Dameng",
openDamengRoles: "Abrir administração de funções Dameng",
duplicateConnection: "Duplicar Conexão",
duplicateSelectedConnections: "Duplicar {count} conexões selecionadas",
instanceInfo: "Informações da Instância",
@@ -2800,6 +2804,8 @@ export default withEnglishFallback({
tree: {
savedSql: "Biblioteca SQL",
userAdmin: "Usuários e Privilégios",
damengUsers: "Usuários",
damengRoles: "Funções",
damengJobAdmin: "Jobs do agente",
linkedServers: "Servidores Vinculados",
materializedViews: "Visualizações Materializadas",
@@ -2959,6 +2965,112 @@ export default withEnglishFallback({
applySuccess: "Operação de job aplicada",
applyFailed: "Execução falhou: {message}",
},
damengUserAdmin: {
title: "Usuários Dameng",
unsupported: "Esta página só suporta conexões de banco de dados Dameng.",
newUser: "Novo usuário",
searchUser: "Pesquisar usuários",
loadFailed: "Falha ao carregar",
emptyUsers: "Nenhum usuário, ou a conta atual não pode ler a lista de usuários.",
selectUser: "Selecione um usuário",
username: "Nome de usuário",
usernamePlaceholder: "ex.: APP_USER",
password: "Senha",
passwordPolicyHint: "A senha deve atender à política de senhas do DM8 (padrão: pelo menos 9 caracteres; idealmente combine maiúsculas, minúsculas e dígitos)",
tablespace: "Tablespace padrão",
tablespacePlaceholder: "Digite o nome do tablespace",
locked: "Bloquear conta após a criação",
changePassword: "Alterar senha",
newPassword: "Nova senha",
confirmPassword: "Confirmar senha",
open: "Aberto",
systemUser: "Usuário do sistema",
systemUserAdmin: "Usuário administrador",
systemUserAuditor: "Usuário de auditoria",
systemUserSecurity: "Usuário de segurança",
systemUserSystem: "Usuário do sistema",
cannotChangePassword: "A senha deste usuário não pode ser alterada pela conexão atual (apenas SYSDBA ou o próprio usuário pode alterá-la)",
inheritedFrom: "Herdado de {via}",
userType: "Tipo de usuário",
userTypeHint: "Administrador concede automaticamente DBA, auditoria concede DB_AUDIT_ADMIN, segurança concede DB_POLICY_ADMIN",
groupOther: "Outros usuários",
systemUserProtected: "Usuários do sistema não podem ser alterados ou removidos",
builtinRole: "Papel integrado",
builtinRoleProtected: "Papéis integrados não podem ser revogados",
defaultTablespace: "Tablespace padrão",
created: "Criado em",
lock: "Bloquear",
unlock: "Desbloquear",
dropUser: "Excluir usuário",
roles: "Funções",
systemPrivileges: "Privilégios do sistema",
grantedRoles: "Funções concedidas",
grantedPrivileges: "Privilégios concedidos",
grantRole: "Conceder função",
revokeRole: "Revogar função",
roleToGrant: "Função a conceder",
selectRole: "Selecionar função",
grantRoleHint: "Conceder a função ao usuário {user}.",
noRolesToGrant: "Nenhuma função disponível para conceder",
noGrantedRoles: "Este usuário não possui funções concedidas",
grantPrivilege: "Conceder privilégio",
revokePrivilege: "Revogar privilégio",
privilegeToGrant: "Privilégio a conceder",
selectPrivilege: "Selecionar privilégio",
grantPrivilegeHint: "Conceder o privilégio de sistema ao usuário {user}.",
noPrivilegesToGrant: "Nenhum privilégio disponível para conceder",
noGrantedPrivileges: "Este usuário não possui privilégios de sistema concedidos",
previewSql: "Pré-visualizar SQL",
sqlPreview: "Pré-visualização SQL",
applySql: "Executar SQL",
applySuccess: "Operação de usuário aplicada",
applyFailed: "Falha na execução: {message}",
enableDdlAnyPrivHint:
"Dica: o DM8 desabilita a concessão de privilégios ANY de DDL por padrão (ENABLE_DDL_ANY_PRIV=0). Para conceder privilégios ANY, execute como SYSDBA: sp_set_para_value(1,'ENABLE_DDL_ANY_PRIV',1). Para privilégios não ANY, certifique-se de que a conta atual tenha a ADMIN OPTION desse privilégio.",
anyPrivilegeDisabled: "Privilégios do tipo ANY estão desabilitados pelo DM8 (ENABLE_DDL_ANY_PRIV=0) e não podem ser concedidos ou revogados",
},
damengRoleAdmin: {
title: "Funções Dameng",
unsupported: "Esta página só suporta conexões de banco de dados Dameng.",
newRole: "Nova função",
searchRole: "Pesquisar funções",
loadFailed: "Falha ao carregar",
emptyRoles: "Nenhuma função, ou a conta atual não pode ler a lista de funções.",
selectRole: "Selecione uma função",
roleName: "Nome da função",
roleNamePlaceholder: "ex.: APP_ROLE",
members: "Membros",
systemPrivileges: "Privilégios do sistema",
grantedToUsers: "Usuários com esta função",
grantedPrivileges: "Privilégios concedidos",
noMembers: "Nenhum usuário possui esta função",
grantToUser: "Conceder a usuário",
userToGrant: "Usuário a conceder",
selectUser: "Selecionar usuário",
grantUserHint: "Conceder a função {role} ao usuário.",
noUsersToGrant: "Nenhum usuário disponível para conceder",
revokeFromUser: "Revogar",
systemUser: "Usuário do sistema",
systemUserProtected: "Não é possível revogar de usuários do sistema",
builtinRole: "Papel integrado",
builtinRoleProtected: "Papéis predefinidos não podem ser removidos ou modificados",
grantPrivilege: "Conceder privilégio",
revokePrivilege: "Revogar privilégio",
privilegeToGrant: "Privilégio a conceder",
selectPrivilege: "Selecionar privilégio",
grantPrivilegeHint: "Conceder o privilégio de sistema à função {role}.",
noPrivilegesToGrant: "Nenhum privilégio disponível para conceder",
noGrantedPrivileges: "Esta função não possui privilégios de sistema concedidos",
dropRole: "Excluir função",
previewSql: "Pré-visualizar SQL",
sqlPreview: "Pré-visualização SQL",
applySql: "Executar SQL",
applySuccess: "Operação de função aplicada",
applyFailed: "Falha na execução: {message}",
enableDdlAnyPrivHint:
"Dica: o DM8 desabilita a concessão de privilégios ANY de DDL por padrão (ENABLE_DDL_ANY_PRIV=0). Para conceder privilégios ANY, execute como SYSDBA: sp_set_para_value(1,'ENABLE_DDL_ANY_PRIV',1). Para privilégios não ANY, certifique-se de que a conta atual tenha a ADMIN OPTION desse privilégio.",
anyPrivilegeDisabled: "Privilégios do tipo ANY estão desabilitados pelo DM8 (ENABLE_DDL_ANY_PRIV=0) e não podem ser concedidos ou revogados",
},
objects: {
all: "Todos",
tables: "Tabelas",
+110
View File
@@ -2467,6 +2467,10 @@ export default withEnglishFallback({
openUserAdmin: "打开用户与权限",
processList: "进程列表",
serverDashboard: "服务器仪表盘",
damengUsers: "达梦用户",
openDamengUsers: "打开达梦用户管理",
damengRoles: "达梦角色",
openDamengRoles: "打开达梦角色管理",
damengJobAdmin: "达梦代理作业",
openDamengJobAdmin: "打开达梦代理作业",
newQuery: "新建查询",
@@ -2791,6 +2795,8 @@ export default withEnglishFallback({
tree: {
savedSql: "SQL 库",
userAdmin: "用户与权限",
damengUsers: "用户",
damengRoles: "角色",
damengJobAdmin: "代理作业",
linkedServers: "链接服务器",
defaultDatabase: "默认库",
@@ -3038,6 +3044,110 @@ export default withEnglishFallback({
applySuccess: "作业操作已执行",
applyFailed: "执行失败:{message}",
},
damengUserAdmin: {
title: "达梦用户",
unsupported: "当前页面仅支持达梦数据库连接。",
newUser: "新建用户",
searchUser: "搜索用户",
loadFailed: "加载失败",
emptyUsers: "暂无用户,或当前账号没有读取用户列表的权限。",
selectUser: "请选择一个用户",
username: "用户名",
usernamePlaceholder: "例如 APP_USER",
password: "密码",
passwordPolicyHint: "口令需满足 DM8 密码策略(默认长度 ≥ 9,建议大小写字母和数字组合)",
tablespace: "默认表空间",
tablespacePlaceholder: "输入表空间名",
locked: "创建后锁定账号",
changePassword: "修改密码",
newPassword: "新密码",
confirmPassword: "确认密码",
open: "正常",
systemUser: "系统用户",
systemUserAdmin: "管理用户",
systemUserAuditor: "审计用户",
systemUserSecurity: "安全用户",
systemUserSystem: "系统用户",
cannotChangePassword: "该用户密码无法由当前连接修改(仅 SYSDBA 或用户本人可修改)",
inheritedFrom: "继承自 {via}",
userType: "用户类型",
userTypeHint: "管理用户自动授予 DBA,审计用户自动授予 DB_AUDIT_ADMIN,安全用户自动授予 DB_POLICY_ADMIN",
groupOther: "其他用户",
systemUserProtected: "系统用户禁止修改或删除",
builtinRole: "内置角色",
builtinRoleProtected: "内置角色不可回收",
defaultTablespace: "默认表空间",
created: "创建时间",
lock: "锁定",
unlock: "解锁",
dropUser: "删除用户",
roles: "角色",
systemPrivileges: "系统权限",
grantedRoles: "已授予的角色",
grantedPrivileges: "已授予的权限",
grantRole: "授予角色",
revokeRole: "回收角色",
roleToGrant: "要授予的角色",
selectRole: "选择角色",
grantRoleHint: "将角色授予用户 {user}。",
noRolesToGrant: "没有可授予的角色",
noGrantedRoles: "该用户暂未被授予任何角色",
grantPrivilege: "授予权限",
revokePrivilege: "回收权限",
privilegeToGrant: "要授予的权限",
selectPrivilege: "选择权限",
grantPrivilegeHint: "将系统权限授予用户 {user}。",
noPrivilegesToGrant: "没有可授予的权限",
noGrantedPrivileges: "该用户暂未被授予任何系统权限",
previewSql: "预览 SQL",
sqlPreview: "SQL 预览",
applySql: "执行 SQL",
applySuccess: "用户操作已执行",
applyFailed: "执行失败:{message}",
enableDdlAnyPrivHint: "提示:DM8 默认禁用 DDL 的 ANY 权限授予(ENABLE_DDL_ANY_PRIV=0)。授 ANY 类权限请用 SYSDBA 执行 sp_set_para_value(1,'ENABLE_DDL_ANY_PRIV',1) 开启;授普通权限请确认当前账号持有该权限的 ADMIN OPTION。",
anyPrivilegeDisabled: "ANY 类权限已被 DM8 禁用(ENABLE_DDL_ANY_PRIV=0),无法授予或回收",
},
damengRoleAdmin: {
title: "达梦角色",
unsupported: "当前页面仅支持达梦数据库连接。",
newRole: "新建角色",
searchRole: "搜索角色",
loadFailed: "加载失败",
emptyRoles: "暂无角色,或当前账号没有读取角色列表的权限。",
selectRole: "请选择一个角色",
roleName: "角色名",
roleNamePlaceholder: "例如 APP_ROLE",
members: "成员",
systemPrivileges: "系统权限",
grantedToUsers: "已授予该角色的用户",
grantedPrivileges: "已授予的权限",
noMembers: "该角色暂未被授予任何用户",
grantToUser: "授予用户",
userToGrant: "要授予的用户",
selectUser: "选择用户",
grantUserHint: "将角色 {role} 授予用户。",
noUsersToGrant: "没有可授予的用户",
revokeFromUser: "回收",
systemUser: "系统用户",
systemUserProtected: "系统用户禁止回收",
builtinRole: "内置角色",
builtinRoleProtected: "预定义角色不允许删除或修改",
grantPrivilege: "授予权限",
revokePrivilege: "回收权限",
privilegeToGrant: "要授予的权限",
selectPrivilege: "选择权限",
grantPrivilegeHint: "将系统权限授予角色 {role}。",
noPrivilegesToGrant: "没有可授予的权限",
noGrantedPrivileges: "该角色暂未被授予任何系统权限",
dropRole: "删除角色",
previewSql: "预览 SQL",
sqlPreview: "SQL 预览",
applySql: "执行 SQL",
applySuccess: "角色操作已执行",
applyFailed: "执行失败:{message}",
enableDdlAnyPrivHint: "提示:DM8 默认禁用 DDL 的 ANY 权限授予(ENABLE_DDL_ANY_PRIV=0)。授 ANY 类权限请用 SYSDBA 执行 sp_set_para_value(1,'ENABLE_DDL_ANY_PRIV',1) 开启;授普通权限请确认当前账号持有该权限的 ADMIN OPTION。",
anyPrivilegeDisabled: "ANY 类权限已被 DM8 禁用(ENABLE_DDL_ANY_PRIV=0),无法授予或回收",
},
objects: {
all: "全部",
tables: "表",
+110
View File
@@ -2473,6 +2473,10 @@ export default withEnglishFallback({
openUserAdmin: "開啟使用者與權限",
damengJobAdmin: "達夢代理作業",
openDamengJobAdmin: "開啟達夢代理作業",
damengUsers: "達夢使用者",
openDamengUsers: "開啟達夢使用者管理",
damengRoles: "達夢角色",
openDamengRoles: "開啟達夢角色管理",
duplicateConnection: "複製連線",
duplicateSelectedConnections: "複製選取的 {count} 個連線",
newQuery: "建立查詢",
@@ -2799,6 +2803,8 @@ export default withEnglishFallback({
tree: {
savedSql: "SQL 庫",
userAdmin: "使用者與權限",
damengUsers: "使用者",
damengRoles: "角色",
damengJobAdmin: "代理作業",
linkedServers: "連結伺服器",
materializedViews: "具體化檢視",
@@ -5867,6 +5873,110 @@ export default withEnglishFallback({
applySuccess: "作業操作已執行",
applyFailed: "執行失敗:{message}",
},
damengUserAdmin: {
title: "達夢使用者",
unsupported: "目前頁面僅支援達夢資料庫連線。",
newUser: "新增使用者",
searchUser: "搜尋使用者",
loadFailed: "載入失敗",
emptyUsers: "暫無使用者,或目前帳號沒有讀取使用者清單的權限。",
selectUser: "請選擇一個使用者",
username: "使用者名稱",
usernamePlaceholder: "例如 APP_USER",
password: "密碼",
passwordPolicyHint: "口令需符合 DM8 密碼策略(預設長度 ≥ 9,建議大小寫字母與數字組合)",
tablespace: "預設資料表空間",
tablespacePlaceholder: "輸入資料表空間名稱",
locked: "建立後鎖定帳號",
changePassword: "修改密碼",
newPassword: "新密碼",
confirmPassword: "確認密碼",
open: "正常",
systemUser: "系統使用者",
systemUserAdmin: "管理使用者",
systemUserAuditor: "稽核使用者",
systemUserSecurity: "安全使用者",
systemUserSystem: "系統使用者",
cannotChangePassword: "該用戶密碼無法由當前連線修改(僅 SYSDBA 或用戶本人可修改)",
inheritedFrom: "繼承自 {via}",
userType: "使用者類型",
userTypeHint: "管理使用者自動授予 DBA,稽核使用者自動授予 DB_AUDIT_ADMIN,安全使用者自動授予 DB_POLICY_ADMIN",
groupOther: "其他使用者",
systemUserProtected: "系統使用者禁止修改或刪除",
builtinRole: "內建角色",
builtinRoleProtected: "內建角色不可回收",
defaultTablespace: "預設資料表空間",
created: "建立時間",
lock: "鎖定",
unlock: "解鎖",
dropUser: "刪除使用者",
roles: "角色",
systemPrivileges: "系統權限",
grantedRoles: "已授予的角色",
grantedPrivileges: "已授予的權限",
grantRole: "授予角色",
revokeRole: "回收角色",
roleToGrant: "要授予的角色",
selectRole: "選擇角色",
grantRoleHint: "將角色授予使用者 {user}。",
noRolesToGrant: "沒有可授予的角色",
noGrantedRoles: "該使用者暫未被授予任何角色",
grantPrivilege: "授予權限",
revokePrivilege: "回收權限",
privilegeToGrant: "要授予的權限",
selectPrivilege: "選擇權限",
grantPrivilegeHint: "將系統權限授予使用者 {user}。",
noPrivilegesToGrant: "沒有可授予的權限",
noGrantedPrivileges: "該使用者暫未被授予任何系統權限",
previewSql: "預覽 SQL",
sqlPreview: "SQL 預覽",
applySql: "執行 SQL",
applySuccess: "使用者操作已執行",
applyFailed: "執行失敗:{message}",
enableDdlAnyPrivHint: "提示:DM8 預設停用 DDL 的 ANY 權限授予(ENABLE_DDL_ANY_PRIV=0)。授 ANY 類權限請用 SYSDBA 執行 sp_set_para_value(1,'ENABLE_DDL_ANY_PRIV',1) 開啟;授一般權限請確認目前帳號持有該權限的 ADMIN OPTION。",
anyPrivilegeDisabled: "ANY 類權限已被 DM8 停用(ENABLE_DDL_ANY_PRIV=0),無法授予或回收",
},
damengRoleAdmin: {
title: "達夢角色",
unsupported: "目前頁面僅支援達夢資料庫連線。",
newRole: "新增角色",
searchRole: "搜尋角色",
loadFailed: "載入失敗",
emptyRoles: "暫無角色,或目前帳號沒有讀取角色清單的權限。",
selectRole: "請選擇一個角色",
roleName: "角色名稱",
roleNamePlaceholder: "例如 APP_ROLE",
members: "成員",
systemPrivileges: "系統權限",
grantedToUsers: "已授予該角色的使用者",
grantedPrivileges: "已授予的權限",
noMembers: "該角色暫未被授予任何使用者",
grantToUser: "授予使用者",
userToGrant: "要授予的使用者",
selectUser: "選擇使用者",
grantUserHint: "將角色 {role} 授予使用者。",
noUsersToGrant: "沒有可授予的使用者",
revokeFromUser: "回收",
systemUser: "系統使用者",
systemUserProtected: "系統使用者禁止回收",
builtinRole: "內建角色",
builtinRoleProtected: "預先定義角色不允許刪除或修改",
grantPrivilege: "授予權限",
revokePrivilege: "回收權限",
privilegeToGrant: "要授予的權限",
selectPrivilege: "選擇權限",
grantPrivilegeHint: "將系統權限授予角色 {role}。",
noPrivilegesToGrant: "沒有可授予的權限",
noGrantedPrivileges: "該角色暫未被授予任何系統權限",
dropRole: "刪除角色",
previewSql: "預覽 SQL",
sqlPreview: "SQL 預覽",
applySql: "執行 SQL",
applySuccess: "角色操作已執行",
applyFailed: "執行失敗:{message}",
enableDdlAnyPrivHint: "提示:DM8 預設停用 DDL 的 ANY 權限授予(ENABLE_DDL_ANY_PRIV=0)。授 ANY 類權限請用 SYSDBA 執行 sp_set_para_value(1,'ENABLE_DDL_ANY_PRIV',1) 開啟;授一般權限請確認目前帳號持有該權限的 ADMIN OPTION。",
anyPrivilegeDisabled: "ANY 類權限已被 DM8 停用(ENABLE_DDL_ANY_PRIV=0),無法授予或回收",
},
etcd: {
prefixPlaceholder: "搜尋 Key 路徑,例如 /app/ 或 service",
newKey: "新增 Key",
@@ -0,0 +1,585 @@
import type { QueryResult } from "@/types/database";
/**
* Dameng (达梦 DM8) user & role administration SQL layer.
*
* Follows the same architecture as `damengJobAdmin.ts` / `databaseUserAdmin.ts`:
* SQL is generated on the frontend and executed through the generic query
* channel (`api.executeQuery` / `executeWithProductionSqlGuard`).
*
* Metadata views are Oracle-style: DBA_USERS, DBA_ROLES, DBA_ROLE_PRIVS,
* DBA_SYS_PRIVS and DBA_TABLESPACES. Identifiers (users, roles, tablespaces)
* are double-quoted; string literals (passwords) are single-quoted and escaped.
*/
export interface DamengUser {
username: string;
accountStatus: string;
locked: boolean;
defaultTablespace: string;
temporaryTablespace: string;
profile: string;
created: string;
raw: Record<string, unknown>;
}
export interface DamengRole {
role: string;
passwordRequired: string;
raw: Record<string, unknown>;
}
export interface DamengGrant {
grantee: string;
grantedRole: string;
adminOption: boolean;
defaultRole: boolean;
/** True when the role is only reachable through role nesting (not granted directly). */
inherited?: boolean;
/** The parent role through which an inherited role is reachable. */
via?: string;
}
export interface DamengRoleEntry {
role: string;
direct: boolean;
via?: string;
}
export interface DamengSysPrivilege {
grantee: string;
privilege: string;
adminOption: boolean;
}
export interface DamengCreateUserInput {
username: string;
password: string;
tablespace?: string;
locked?: boolean;
}
/**
* Built-in system accounts that must never be dropped, locked or altered from
* the UI. Compared case-insensitively (see `isDamengSystemUser`).
*/
export const DAMENG_SYSTEM_USERS = ["SYSDBA", "SYSSSO", "SYSAUDITOR", "SYSDBO", "SYS"] as const;
/**
* Identity category of each built-in system account, matching the DM
* management tool terminology: SYSDBA = admin, SYSAUDITOR = audit,
* SYSSSO = security. SYSDBO and SYS remain in the protected system group.
*/
export const DAMENG_SYSTEM_USER_CATEGORIES: Record<string, string> = {
SYSDBA: "admin",
SYSAUDITOR: "auditor",
SYSSSO: "security",
SYSDBO: "system",
SYS: "system",
};
/** Returns the category key ("admin" | "auditor" | "security" | "system") of a system user, or undefined for regular users. */
export function damengSystemUserCategory(username: string): string | undefined {
const normalized = username.trim().toUpperCase();
return DAMENG_SYSTEM_USER_CATEGORIES[normalized];
}
/** Sidebar list grouping: the four system-user categories plus regular users. */
export type DamengUserGroup = "admin" | "auditor" | "security" | "system" | "other";
export const DAMENG_USER_GROUPS: readonly DamengUserGroup[] = ["admin", "auditor", "security", "system", "other"];
/**
* Groups a username: the four system accounts map to their category; a regular
* user is grouped by the management roles they hold (DBA -> admin,
* DB_AUDIT_ADMIN -> auditor, DB_POLICY_ADMIN -> security), everything else to
* "other". Pass the user's directly granted roles (e.g. from DBA_ROLE_PRIVS).
*/
export function damengUserGroup(username: string, grantedRoles?: Iterable<string>): DamengUserGroup {
const normalized = username.trim().toUpperCase();
const system = DAMENG_SYSTEM_USER_CATEGORIES[normalized];
if (system) return system as DamengUserGroup;
if (grantedRoles) {
const roles = new Set([...grantedRoles].map((role) => role.trim().toUpperCase()));
if (roles.has("DBA")) return "admin";
if (roles.has("DB_AUDIT_ADMIN")) return "auditor";
if (roles.has("DB_POLICY_ADMIN")) return "security";
}
return "other";
}
/** Common Oracle-style system privileges supported by Dameng DM8. */
export const DAMENG_SYSTEM_PRIVILEGES = [
"CREATE SESSION",
"CREATE TABLE",
"CREATE ANY TABLE",
"ALTER ANY TABLE",
"DROP ANY TABLE",
"SELECT ANY TABLE",
"INSERT ANY TABLE",
"UPDATE ANY TABLE",
"DELETE ANY TABLE",
"CREATE VIEW",
"CREATE ANY VIEW",
"DROP ANY VIEW",
"CREATE PROCEDURE",
"CREATE ANY PROCEDURE",
"ALTER ANY PROCEDURE",
"DROP ANY PROCEDURE",
"EXECUTE ANY PROCEDURE",
"CREATE FUNCTION",
"CREATE PACKAGE",
"CREATE SEQUENCE",
"CREATE ANY SEQUENCE",
"ALTER ANY SEQUENCE",
"DROP ANY SEQUENCE",
"SELECT ANY SEQUENCE",
"CREATE TRIGGER",
"CREATE ANY TRIGGER",
"ALTER ANY TRIGGER",
"DROP ANY TRIGGER",
"CREATE SYNONYM",
"CREATE PUBLIC SYNONYM",
"DROP PUBLIC SYNONYM",
"CREATE ROLE",
"DROP ROLE",
"CREATE USER",
"ALTER USER",
"DROP USER",
"ALTER ANY USER",
"MANAGE ANY USER",
"CREATE TABLESPACE",
"ALTER TABLESPACE",
"DROP TABLESPACE",
"MANAGE TABLESPACE",
"CREATE DATABASE LINK",
"CREATE MATERIALIZED VIEW",
"ALTER DATABASE",
"ALTER SYSTEM",
"GRANT ANY PRIVILEGE",
"GRANT ANY ROLE",
"UNLIMITED TABLESPACE",
"SELECT ANY DICTIONARY",
"EXECUTE ANY TYPE",
] as const;
export function isDamengSystemUser(username: string): boolean {
const normalized = username.trim().toUpperCase();
return DAMENG_SYSTEM_USERS.some((systemUser) => systemUser === normalized);
}
/** Direct role grants on regular users may be revoked, including predefined roles. */
export function canDamengRevokeRoleGrant(username: string | undefined, grant: Pick<DamengGrant, "grantedRole" | "inherited"> | undefined): boolean {
return !!username && !!grant && !isDamengSystemUser(username) && !grant.inherited;
}
/**
* Whether the given connection user may ALTER USER (change password) for the
* target user, following the DM (达梦) security model:
* - SYS is a built-in non-login user with no password: never changeable.
* - SYSDBA / SYSSSO / SYSAUDITOR passwords can only be changed by themselves
* (a SYSDBA session cannot reset SYSSSO or SYSAUDITOR passwords).
* - SYSDBA can change any regular (non-system) user's password.
* - A regular user can only change their own password.
*/
export function canDamengAlterUserPassword(connectionUser: string | undefined, target: string | undefined): boolean {
if (!target) return false;
const conn = (connectionUser || "").trim().toUpperCase();
const tgt = target.trim().toUpperCase();
if (tgt === "SYS") return false;
if (isDamengSystemUser(tgt)) return conn === tgt;
return conn === "SYSDBA" || conn === tgt;
}
// ---------------------------------------------------------------------------
// Queries
// ---------------------------------------------------------------------------
export function damengListUsersSql(): string {
return `SELECT USERNAME, ACCOUNT_STATUS, DEFAULT_TABLESPACE, TEMPORARY_TABLESPACE, PROFILE, CREATED
FROM DBA_USERS
ORDER BY USERNAME`;
}
export function damengListRolesSql(): string {
return `SELECT ROLE, PASSWORD_REQUIRED
FROM DBA_ROLES
ORDER BY ROLE`;
}
export function damengListTablespacesSql(): string {
return `SELECT TABLESPACE_NAME
FROM DBA_TABLESPACES
ORDER BY TABLESPACE_NAME`;
}
export function damengSystemPrivilegeMapSql(): string {
return `SELECT NAME FROM SYSTEM_PRIVILEGE_MAP ORDER BY NAME`;
}
export function damengEnableDdlAnyPrivSql(): string {
return `SELECT PARA_VALUE FROM V$DM_INI WHERE PARA_NAME = 'ENABLE_DDL_ANY_PRIV'`;
}
export function damengListRoleMembersSql(role: string): string {
return `SELECT GRANTEE, GRANTED_ROLE, ADMIN_OPTION, DEFAULT_ROLE
FROM DBA_ROLE_PRIVS
WHERE GRANTED_ROLE = ${quoteDamengString(role)}
ORDER BY GRANTEE`;
}
export function damengListUserRolesSql(user: string): string {
return `SELECT GRANTEE, GRANTED_ROLE, ADMIN_OPTION, DEFAULT_ROLE
FROM DBA_ROLE_PRIVS
WHERE GRANTEE = ${quoteDamengString(user)}
ORDER BY GRANTED_ROLE`;
}
export function damengRoleGraphSql(): string {
return `SELECT GRANTEE, GRANTED_ROLE FROM DBA_ROLE_PRIVS`;
}
export function damengListUserSysPrivsSql(user: string): string {
return `SELECT GRANTEE, PRIVILEGE, ADMIN_OPTION
FROM DBA_SYS_PRIVS
WHERE GRANTEE = ${quoteDamengString(user)}
ORDER BY PRIVILEGE`;
}
export function damengListRoleSysPrivsSql(role: string): string {
return `SELECT GRANTEE, PRIVILEGE, ADMIN_OPTION
FROM DBA_SYS_PRIVS
WHERE GRANTEE = ${quoteDamengString(role)}
ORDER BY PRIVILEGE`;
}
// ---------------------------------------------------------------------------
// DDL
// ---------------------------------------------------------------------------
export function damengCreateUserSql(input: DamengCreateUserInput): string {
const username = input.username.trim();
const password = input.password;
if (!username) throw new Error("Dameng username must not be empty");
if (!password || !password.trim()) throw new Error("Dameng user password must not be empty");
const parts: string[] = [`CREATE USER ${quoteDamengIdentifier(username)} IDENTIFIED BY ${quoteDamengPassword(password)}`];
if (input.tablespace && input.tablespace.trim()) {
parts.push(`DEFAULT TABLESPACE ${quoteDamengIdentifier(input.tablespace)}`);
}
if (input.locked) parts.push("ACCOUNT LOCK");
return `${parts.join(" ")};`;
}
/**
* Predefined DM role automatically granted when creating a user of each type,
* matching the roles the built-in accounts hold: SYSDBA has DBA, SYSAUDITOR has
* DB_AUDIT_ADMIN, SYSSSO has DB_POLICY_ADMIN. "system" (SYS) has no grantable
* role — SYS is a built-in non-login user that cannot be replicated via GRANT.
*/
export const DAMENG_USER_TYPE_GRANTS: Record<string, readonly string[]> = {
admin: ["DBA"],
auditor: ["DB_AUDIT_ADMIN"],
security: ["DB_POLICY_ADMIN"],
system: [],
};
/** GRANT statements to append when creating a user with the given type. */
export function damengUserTypeGrantSqls(userType: string, username: string): string[] {
const roles = DAMENG_USER_TYPE_GRANTS[userType] ?? [];
return roles.map((role) => `GRANT ${quoteDamengIdentifier(role)} TO ${quoteDamengIdentifier(username)};`);
}
export function damengAlterUserPasswordSql(username: string, password: string): string {
return `ALTER USER ${quoteDamengIdentifier(username)} IDENTIFIED BY ${quoteDamengPassword(password)};`;
}
export function damengLockUserSql(username: string): string {
return `ALTER USER ${quoteDamengIdentifier(username)} ACCOUNT LOCK;`;
}
export function damengUnlockUserSql(username: string): string {
return `ALTER USER ${quoteDamengIdentifier(username)} ACCOUNT UNLOCK;`;
}
export function damengDropUserSql(username: string): string {
return `DROP USER ${quoteDamengIdentifier(username)} CASCADE;`;
}
export function damengCreateRoleSql(role: string): string {
return `CREATE ROLE ${quoteDamengIdentifier(role)};`;
}
export function damengDropRoleSql(role: string): string {
return `DROP ROLE ${quoteDamengIdentifier(role)};`;
}
// ---------------------------------------------------------------------------
// DCL
// ---------------------------------------------------------------------------
export function damengGrantRoleSql(grantee: string, role: string): string {
return `GRANT ${quoteDamengIdentifier(role)} TO ${quoteDamengIdentifier(grantee)};`;
}
export function damengRevokeRoleSql(grantee: string, role: string): string {
return `REVOKE ${quoteDamengIdentifier(role)} FROM ${quoteDamengIdentifier(grantee)};`;
}
export function damengGrantSystemPrivilegeSql(grantee: string, privilege: string, adminOption = false): string {
const suffix = adminOption ? " WITH ADMIN OPTION" : "";
return `GRANT ${privilege} TO ${quoteDamengIdentifier(grantee)}${suffix};`;
}
export function damengRevokeSystemPrivilegeSql(grantee: string, privilege: string): string {
return `REVOKE ${privilege} FROM ${quoteDamengIdentifier(grantee)};`;
}
// ---------------------------------------------------------------------------
// Parsers
// ---------------------------------------------------------------------------
export function parseDamengUsers(result: QueryResult): DamengUser[] {
return result.rows.map((row) => {
const raw = rowToObject(result.columns, row);
const accountStatus = valueAt(raw, "ACCOUNT_STATUS").toUpperCase();
return {
username: valueAt(raw, "USERNAME"),
accountStatus,
locked: /LOCKED/i.test(accountStatus),
defaultTablespace: valueAt(raw, "DEFAULT_TABLESPACE"),
temporaryTablespace: valueAt(raw, "TEMPORARY_TABLESPACE"),
profile: valueAt(raw, "PROFILE"),
created: valueAt(raw, "CREATED"),
raw,
};
});
}
export function parseDamengRoles(result: QueryResult): DamengRole[] {
return result.rows.map((row) => {
const raw = rowToObject(result.columns, row);
return {
role: valueAt(raw, "ROLE"),
passwordRequired: valueAt(raw, "PASSWORD_REQUIRED"),
raw,
};
});
}
export function parseDamengRoleGrants(result: QueryResult): Map<string, Set<string>> {
const grants = new Map<string, Set<string>>();
for (const row of result.rows) {
const raw = rowToObject(result.columns, row);
const grantee = valueAt(raw, "GRANTEE").trim().toUpperCase();
const role = valueAt(raw, "GRANTED_ROLE").trim().toUpperCase();
if (!grantee || !role) continue;
let roles = grants.get(grantee);
if (!roles) {
roles = new Set();
grants.set(grantee, roles);
}
roles.add(role);
}
return grants;
}
export function parseDamengRolePrivs(result: QueryResult): DamengGrant[] {
return result.rows.map((row) => {
const raw = rowToObject(result.columns, row);
return {
grantee: valueAt(raw, "GRANTEE"),
grantedRole: valueAt(raw, "GRANTED_ROLE"),
adminOption: isYes(valueAt(raw, "ADMIN_OPTION")),
defaultRole: isYes(valueAt(raw, "DEFAULT_ROLE")),
};
});
}
export function parseDamengSysPrivs(result: QueryResult): DamengSysPrivilege[] {
return result.rows.map((row) => {
const raw = rowToObject(result.columns, row);
return {
grantee: valueAt(raw, "GRANTEE"),
privilege: valueAt(raw, "PRIVILEGE"),
adminOption: isYes(valueAt(raw, "ADMIN_OPTION")),
};
});
}
export function parseDamengTablespaces(result: QueryResult): string[] {
return result.rows
.map((row) => {
const raw = rowToObject(result.columns, row);
return valueAt(raw, "TABLESPACE_NAME");
})
.filter((name) => name !== "");
}
/** The set of system privilege names this DM instance supports (SYSTEM_PRIVILEGE_MAP). */
export function parseDamengSystemPrivilegeMap(result: QueryResult): Set<string> {
const names = new Set<string>();
for (const row of result.rows) {
const raw = rowToObject(result.columns, row);
const name = valueAt(raw, "NAME").trim().toUpperCase();
if (name) names.add(name);
}
return names;
}
/**
* Whether the instance allows granting/revoking ANY-type DDL system privileges
* (DM8 default: ENABLE_DDL_ANY_PRIV=0, which rejects them with error -5567).
*/
export function parseDamengEnableDdlAnyPriv(result: QueryResult): boolean {
if (result.rows.length === 0) return true;
const raw = rowToObject(result.columns, result.rows[0]);
return String(valueAt(raw, "PARA_VALUE") ?? "0").trim() !== "0";
}
/** True for ANY-type system privileges (CREATE ANY TABLE, SELECT ANY VIEW, ...). */
export function isDamengAnyPrivilege(privilege: string): boolean {
return /\bANY\b/.test(privilege.toUpperCase());
}
/**
* Filters the static privilege catalog down to privileges that actually exist
* on this DM instance. The static catalog is an Oracle-style best-effort list
* and DM versions differ; the runtime map makes the grant dialog accurate.
*/
export function damengAvailableSystemPrivileges(catalog: readonly string[], existing: Set<string>): string[] {
const normalized = new Set([...existing].map((name) => name.toUpperCase()));
return catalog.filter((privilege) => normalized.has(privilege.toUpperCase()));
}
/**
* The DM role graph as GRANTEE -> directly granted roles. SVI / VTI and other
* roles are often granted through nesting (DBA -> VTI, PUBLIC -> SVI), and the
* official DM management tool shows the transitive closure, so we compute it
* client-side from this full graph.
*/
export function parseDamengRoleGraph(result: QueryResult): Map<string, string[]> {
const graph = new Map<string, string[]>();
for (const row of result.rows) {
const raw = rowToObject(result.columns, row);
const grantee = valueAt(raw, "GRANTEE").toUpperCase();
const role = valueAt(raw, "GRANTED_ROLE").toUpperCase();
if (!grantee || !role) continue;
const list = graph.get(grantee) ?? [];
if (!list.includes(role)) list.push(role);
graph.set(grantee, list);
}
return graph;
}
/** Internal DM roles that cannot be granted or revoked (GRANT fails with
* "Grantor no granted privilege") and are hidden from the official client. */
/** Internal DM roles that cannot be granted or revoked (GRANT fails with
* "Grantor no granted privilege") and are hidden from the official client. */
export const DAMENG_HIDDEN_ROLES = ["SYS_ADMIN"] as const;
/**
* DM pre-defined roles that exist from database initialization and cannot be
* dropped or modified (their privilege sets are fixed by the system). Includes
* the DBA / AUDITOR / SSO / DBO type roles documented in the DM8 safety manual.
*/
export const DAMENG_PREDEFINED_ROLES: readonly string[] = [
"DBA",
"RESOURCE",
"PUBLIC",
"VTI",
"SOI",
"SVI",
"SYS_ADMIN",
"SELECT_CATALOG_ROLE",
"DB_AUDIT_ADMIN",
"DB_AUDIT_OPER",
"DB_AUDIT_PUBLIC",
"DB_AUDIT_VTI",
"DB_AUDIT_SOI",
"DB_AUDIT_SVI",
"DB_AUDIT_SELECT_CATALOG_ROLE",
"DB_POLICY_ADMIN",
"DB_POLICY_OPER",
"DB_POLICY_PUBLIC",
"DB_POLICY_VTI",
"DB_POLICY_SOI",
"DB_POLICY_SVI",
"DB_POLICY_SELECT_CATALOG_ROLE",
"DB_OBJECT_ADMIN",
"DB_OBJECT_OPER",
"DB_OBJECT_PUBLIC",
"DB_OBJECT_VTI",
"DB_OBJECT_SOI",
"DB_OBJECT_SVI",
"DB_OBJECT_SELECT_CATALOG_ROLE",
];
/** Case-insensitive check for DM pre-defined roles. */
export function isDamengPredefinedRole(role: string | undefined): boolean {
if (!role) return false;
const normalized = role.trim().toUpperCase();
return DAMENG_PREDEFINED_ROLES.some((predefined) => predefined === normalized);
}
/**
* Transitive closure of the roles a user effectively holds, mirroring the
* official DM management tool: direct grants plus roles reachable through
* role nesting (e.g. SYSDBA -> DBA -> VTI). Hidden internal roles such as
* SYS_ADMIN are excluded.
*/
export function damengRolesClosure(username: string, graph: Map<string, string[]>): DamengRoleEntry[] {
const start = username.trim().toUpperCase();
const seen = new Set<string>();
const entries: DamengRoleEntry[] = [];
const queue: { role: string; direct: boolean; via?: string }[] = (graph.get(start) ?? []).map((role) => ({
role,
direct: true,
}));
while (queue.length > 0) {
const { role, direct, via } = queue.shift()!;
if (seen.has(role) || DAMENG_HIDDEN_ROLES.includes(role as (typeof DAMENG_HIDDEN_ROLES)[number])) continue;
seen.add(role);
entries.push({ role, direct, via });
for (const child of graph.get(role) ?? []) {
queue.push({ role: child, direct: false, via: role });
}
}
return entries;
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
export function quoteDamengIdentifier(value: string): string {
return `"${value.replace(/"/g, '""')}"`;
}
export function quoteDamengString(value: string): string {
return `'${value.replace(/'/g, "''")}'`;
}
export function quoteDamengPassword(value: string): string {
// DM8 CREATE/ALTER USER IDENTIFIED BY takes a double-quoted password token
// (single quotes are string literals and fail with a parse error there).
// Double quotes preserve case; a literal " inside the password is escaped as "".
return `"${value.replace(/"/g, '""')}"`;
}
function isYes(value: string): boolean {
return value.trim().toUpperCase() === "YES";
}
function rowToObject(columns: string[], row: QueryResult["rows"][number]): Record<string, unknown> {
const object: Record<string, unknown> = {};
columns.forEach((column, index) => {
object[column] = row[index] ?? null;
});
return object;
}
function valueAt(row: Record<string, unknown>, column: string): string {
const key = Object.keys(row).find((candidate) => candidate.toLowerCase() === column.toLowerCase());
const value = key ? row[key] : undefined;
return value == null ? "" : String(value);
}
@@ -36,8 +36,8 @@ export function sortSidebarTreeChildrenForParent(parent: Pick<TreeNode, "type">,
if (parent.type === "connection") {
const savedSqlNodes = normalized.filter((child) => child.type === "saved-sql-root");
const userAdminNodes = normalized.filter((child) => child.type === "user-admin");
const regularChildren = normalized.filter((child) => child.type !== "user-admin" && child.type !== "saved-sql-root");
const userAdminNodes = normalized.filter((child) => child.type === "user-admin" || child.type === "dameng-users" || child.type === "dameng-roles");
const regularChildren = normalized.filter((child) => child.type !== "user-admin" && child.type !== "dameng-users" && child.type !== "dameng-roles" && child.type !== "saved-sql-root");
const withConnectionUtilityOrder = (children: TreeNode[]) => [...savedSqlNodes, ...children, ...userAdminNodes];
if (databaseType === "mongodb" || databaseType === "elasticsearch" || databaseType === "easysearch" || databaseType === "qdrant" || databaseType === "milvus" || databaseType === "weaviate" || databaseType === "chromadb") {
@@ -28,6 +28,8 @@ const leafTypes: Set<TreeNodeType> = new Set([
"vector-collection",
"elasticsearch-index",
"user-admin",
"dameng-users",
"dameng-roles",
"saved-sql-file",
"table-search-control",
"load-more",
@@ -25,6 +25,8 @@ const toggleLeafNodeTypes = new Set<TreeNodeType>([
"vector-collection",
"elasticsearch-index",
"user-admin",
"dameng-users",
"dameng-roles",
]);
// These are application entry points rather than database objects. They should
// always navigate on a single click, even when the user prefers double-click
@@ -80,6 +80,10 @@ export function getTreeNodeIconInfo(node: TreeNode): TreeNodeIconInfo | null {
return { icon: TableProperties, colorClass: "text-primary" };
case "user-admin":
return { icon: UsersRound, colorClass: "text-primary" };
case "dameng-users":
return { icon: UsersRound, colorClass: "text-primary" };
case "dameng-roles":
return { icon: ShieldCheck, colorClass: "text-primary" };
case "redis-db":
return { icon: Database, colorClass: "text-red-400" };
case "mongo-gridfs":
+31 -1
View File
@@ -1380,6 +1380,34 @@ export const useConnectionStore = defineStore("connection", () => {
};
}
function buildDamengUserNode(connectionId: string, existingConnectionNode?: TreeNode): TreeNode | undefined {
const config = getConfig(connectionId);
if (effectiveDatabaseTypeForConnection(config) !== "dameng") return undefined;
const existing = existingConnectionNode?.children?.find((child) => child.type === "dameng-users");
return {
id: `${connectionId}:__dameng_users`,
label: "tree.damengUsers",
type: "dameng-users",
connectionId,
database: "",
isExpanded: existing?.isExpanded ?? false,
};
}
function buildDamengRoleNode(connectionId: string, existingConnectionNode?: TreeNode): TreeNode | undefined {
const config = getConfig(connectionId);
if (effectiveDatabaseTypeForConnection(config) !== "dameng") return undefined;
const existing = existingConnectionNode?.children?.find((child) => child.type === "dameng-roles");
return {
id: `${connectionId}:__dameng_roles`,
label: "tree.damengRoles",
type: "dameng-roles",
connectionId,
database: "",
isExpanded: existing?.isExpanded ?? false,
};
}
function buildDamengJobAdminNode(connectionId: string, existingConnectionNode?: TreeNode): TreeNode | undefined {
const config = getConfig(connectionId);
if (effectiveDatabaseTypeForConnection(config) !== "dameng") return undefined;
@@ -1397,8 +1425,10 @@ export const useConnectionStore = defineStore("connection", () => {
function withConnectionUtilityNodes(connectionId: string, children: TreeNode[], existingConnectionNode?: TreeNode): TreeNode[] {
const nonUtilityChildren = connectionMetadataChildren(children);
const userAdminNode = buildUserAdminNode(connectionId, existingConnectionNode);
const damengUserNode = buildDamengUserNode(connectionId, existingConnectionNode);
const damengRoleNode = buildDamengRoleNode(connectionId, existingConnectionNode);
const damengJobAdminNode = buildDamengJobAdminNode(connectionId, existingConnectionNode);
return [...nonUtilityChildren, userAdminNode, damengJobAdminNode].filter(Boolean) as TreeNode[];
return [...nonUtilityChildren, userAdminNode, damengUserNode, damengRoleNode, damengJobAdminNode].filter(Boolean) as TreeNode[];
}
function withSavedSqlRoot(connectionId: string, children: TreeNode[], existingConnectionNode?: TreeNode): TreeNode[] {
+52
View File
@@ -1880,6 +1880,56 @@ export const useQueryStore = defineStore("query", () => {
return id;
}
function openDamengUsers(connectionId: string) {
const existing = tabs.value.find((tab) => tab.mode === "dameng-users" && tab.connectionId === connectionId);
if (existing) {
switchTab(existing.id);
return existing.id;
}
const conn = useConnectionStore().getConfig(connectionId);
const id = uuid();
const tab: QueryTab = {
id,
title: t("damengUserAdmin.title"),
connectionId,
database: conn?.database || "",
sql: "",
isExecuting: false,
isCancelling: false,
isExplaining: false,
mode: "dameng-users",
};
tabs.value.push(tab);
activeTabId.value = id;
return id;
}
function openDamengRoles(connectionId: string) {
const existing = tabs.value.find((tab) => tab.mode === "dameng-roles" && tab.connectionId === connectionId);
if (existing) {
switchTab(existing.id);
return existing.id;
}
const conn = useConnectionStore().getConfig(connectionId);
const id = uuid();
const tab: QueryTab = {
id,
title: t("damengRoleAdmin.title"),
connectionId,
database: conn?.database || "",
sql: "",
isExecuting: false,
isCancelling: false,
isExplaining: false,
mode: "dameng-roles",
};
tabs.value.push(tab);
activeTabId.value = id;
return id;
}
function openMongoBucket(connectionId: string, database: string, bucketName: string) {
const title = `${database}.${bucketName}`;
const existing = tabs.value.find((tab) => tab.mode === "mongo-bucket" && tab.connectionId === connectionId && tab.database === database && tab.mongoBucket?.bucketName === bucketName);
@@ -5768,6 +5818,8 @@ export const useQueryStore = defineStore("query", () => {
openMysqlDashboard,
openPostgresDashboard,
openNacosDashboard,
openDamengUsers,
openDamengRoles,
openDamengJobAdmin,
openMqAdmin,
openMqttAdmin,
+4
View File
@@ -899,6 +899,8 @@ export type TreeNodeType =
| "extension"
| "object-browser"
| "user-admin"
| "dameng-users"
| "dameng-roles"
| "dameng-job-admin"
| "saved-sql-root"
| "saved-sql-folder"
@@ -1157,6 +1159,8 @@ export interface QueryTab {
| "objects"
| "structure"
| "users"
| "dameng-users"
| "dameng-roles"
| "dameng-jobs"
| "processlist"
| "mysql-dashboard"