feat(database): add creation and authorization flows
This commit is contained in:
14 files changed
+1500
-36
No files matched your search
@@ -15,6 +15,18 @@ import type { ConnectionConfig } from "@/types/database";
|
||||
import * as api from "@/lib/backend/api";
|
||||
import { executeWithProductionSqlGuard } from "@/lib/database/productionExecutionGuard";
|
||||
import { grantsFromQueryResult, resolveDatabaseUserAdminProviderForConnection, type DatabaseUserIdentity, type PrivilegeScope } from "@/lib/database/databaseUserAdmin";
|
||||
import {
|
||||
authorizationPlanSql,
|
||||
authorizationPlanStatus,
|
||||
authorizationPrivileges,
|
||||
buildCreateUserAuthorizationPlan,
|
||||
executeAuthorizationPlan,
|
||||
type AuthorizationAccountType,
|
||||
type AuthorizationPlan,
|
||||
type AuthorizationPreset,
|
||||
type AuthorizationStepResult,
|
||||
type DatabaseAuthorizationSelection,
|
||||
} from "@/lib/database/databaseAuthorizationPlan";
|
||||
|
||||
const props = defineProps<{
|
||||
connection: ConnectionConfig;
|
||||
@@ -32,6 +44,7 @@ const search = ref("");
|
||||
const loadingUsers = ref(false);
|
||||
const loadingGrants = ref(false);
|
||||
const applying = ref(false);
|
||||
const preparingCreatePlan = ref(false);
|
||||
const loadError = ref("");
|
||||
const grantError = ref("");
|
||||
|
||||
@@ -39,6 +52,8 @@ const createDialogOpen = ref(false);
|
||||
const passwordDialogOpen = ref(false);
|
||||
const sqlDialogOpen = ref(false);
|
||||
const pendingSql = ref("");
|
||||
const pendingPlan = ref<AuthorizationPlan>();
|
||||
const pendingResults = ref<AuthorizationStepResult[]>([]);
|
||||
const pendingDanger = ref(false);
|
||||
const pendingAfterApply = ref<(() => Promise<void>) | undefined>();
|
||||
|
||||
@@ -53,6 +68,12 @@ const privilegeRole = ref("");
|
||||
const grantOption = ref(false);
|
||||
const selectedPrivileges = ref<string[]>(["SELECT"]);
|
||||
const createCanLogin = ref(true);
|
||||
const createAccountType = ref<AuthorizationAccountType>("standard");
|
||||
const createDatabases = ref<string[]>([]);
|
||||
const createDatabasesLoading = ref(false);
|
||||
const createDatabaseSearch = ref("");
|
||||
const createDatabaseAuthorizations = ref<DatabaseAuthorizationSelection[]>([]);
|
||||
let createPlanRequestId = 0;
|
||||
|
||||
const provider = computed(() => resolveDatabaseUserAdminProviderForConnection(props.connection));
|
||||
const supported = computed(() => provider.value !== null);
|
||||
@@ -83,6 +104,14 @@ const selectedDetail = computed(() => {
|
||||
const grantsSqlText = computed(() => grants.value.join("\n") || t("userAdmin.noGrants"));
|
||||
const highlightedGrantsSql = computed(() => highlight(grantsSqlText.value));
|
||||
const highlightedPendingSql = computed(() => highlight(pendingSql.value));
|
||||
const createAuthorizationPrivileges = computed(() => (provider.value ? authorizationPrivileges(provider.value) : []));
|
||||
const filteredCreateDatabases = computed(() => {
|
||||
const query = createDatabaseSearch.value.trim().toLowerCase();
|
||||
return query ? createDatabases.value.filter((database) => database.toLowerCase().includes(query)) : createDatabases.value;
|
||||
});
|
||||
const selectedCreateDatabaseSet = computed(() => new Set(createDatabaseAuthorizations.value.map((selection) => selection.database)));
|
||||
const createDatabaseAuthorizationsValid = computed(() => createDatabaseAuthorizations.value.every((selection) => selection.preset !== "custom" || (selection.privileges?.length ?? 0) > 0));
|
||||
const pendingStatus = computed(() => (pendingResults.value.length > 0 ? authorizationPlanStatus(pendingResults.value) : undefined));
|
||||
|
||||
function userKey(user: DatabaseUserIdentity): string {
|
||||
return `${user.user}\u0000${user.host}`;
|
||||
@@ -162,8 +191,58 @@ function togglePrivilege(privilege: string) {
|
||||
selectedPrivileges.value = Array.from(set);
|
||||
}
|
||||
|
||||
async function openCreateUserDialog() {
|
||||
createDialogOpen.value = true;
|
||||
createAccountType.value = "standard";
|
||||
createCanLogin.value = true;
|
||||
createPassword.value = "";
|
||||
createDatabaseSearch.value = "";
|
||||
createDatabaseAuthorizations.value = [];
|
||||
if (createDatabasesLoading.value) return;
|
||||
createDatabases.value = [];
|
||||
createDatabasesLoading.value = true;
|
||||
try {
|
||||
await ensureConnection();
|
||||
createDatabases.value = (await api.listDatabases(props.connection.id)).map((database) => database.name);
|
||||
} catch (error: any) {
|
||||
toast(t("userAdmin.loadDatabasesFailed", { message: error?.message || String(error) }), 5000);
|
||||
} finally {
|
||||
createDatabasesLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function createAuthorization(database: string): DatabaseAuthorizationSelection | undefined {
|
||||
return createDatabaseAuthorizations.value.find((selection) => selection.database === database);
|
||||
}
|
||||
|
||||
function toggleCreateDatabase(database: string) {
|
||||
const existing = createAuthorization(database);
|
||||
if (existing) {
|
||||
createDatabaseAuthorizations.value = createDatabaseAuthorizations.value.filter((selection) => selection.database !== database);
|
||||
} else {
|
||||
createDatabaseAuthorizations.value = [...createDatabaseAuthorizations.value, { database, preset: "readOnly", privileges: ["SELECT"] }];
|
||||
}
|
||||
}
|
||||
|
||||
function updateCreateDatabasePreset(database: string, preset: unknown) {
|
||||
const selection = createAuthorization(database);
|
||||
if (!selection || typeof preset !== "string") return;
|
||||
selection.preset = preset as AuthorizationPreset;
|
||||
}
|
||||
|
||||
function toggleCreateDatabasePrivilege(database: string, privilege: string) {
|
||||
const selection = createAuthorization(database);
|
||||
if (!selection) return;
|
||||
const privileges = new Set(selection.privileges ?? []);
|
||||
if (privileges.has(privilege)) privileges.delete(privilege);
|
||||
else privileges.add(privilege);
|
||||
selection.privileges = Array.from(privileges);
|
||||
}
|
||||
|
||||
function previewSql(sql: string, options: { danger?: boolean; afterApply?: () => Promise<void> } = {}) {
|
||||
pendingSql.value = sql;
|
||||
pendingPlan.value = undefined;
|
||||
pendingResults.value = [];
|
||||
pendingDanger.value = !!options.danger;
|
||||
pendingAfterApply.value = options.afterApply;
|
||||
sqlDialogOpen.value = true;
|
||||
@@ -178,12 +257,23 @@ async function applyPendingSql() {
|
||||
database: "",
|
||||
sql: pendingSql.value,
|
||||
source: t("production.sourceAdmin"),
|
||||
execute: () => api.executeMulti(props.connection.id, "", pendingSql.value, undefined, undefined, { maxRows: 1000 }),
|
||||
execute: async () => {
|
||||
if (pendingPlan.value) {
|
||||
return executeAuthorizationPlan(pendingPlan.value, (step) => api.executeMulti(props.connection.id, step.database, step.sql, undefined, undefined, { maxRows: 1000, continueOnError: true }));
|
||||
}
|
||||
const queryResults = await api.executeMulti(props.connection.id, "", pendingSql.value, undefined, undefined, { maxRows: 1000, continueOnError: true });
|
||||
const failed = queryResults.find((item) => item.execution_error === true);
|
||||
if (failed) throw new Error(String(failed.rows[0]?.[0] ?? t("userAdmin.applyFailedUnknown")));
|
||||
return [] as AuthorizationStepResult[];
|
||||
},
|
||||
});
|
||||
if (!result) return;
|
||||
toast(t("userAdmin.applySuccess"), 2500);
|
||||
sqlDialogOpen.value = false;
|
||||
await (pendingAfterApply.value?.() ?? Promise.resolve());
|
||||
pendingResults.value = result;
|
||||
const status = result.length > 0 ? authorizationPlanStatus(result) : "success";
|
||||
toast(t(status === "success" ? "userAdmin.applySuccess" : status === "partial" ? "userAdmin.applyPartial" : "userAdmin.applyFailedSummary"), status === "success" ? 2500 : 5000);
|
||||
const createSucceeded = !pendingPlan.value || result.some((item) => item.step.id === "create-user" && item.status === "success");
|
||||
if (createSucceeded) await (pendingAfterApply.value?.() ?? Promise.resolve());
|
||||
if (!pendingPlan.value) sqlDialogOpen.value = false;
|
||||
await loadUsers();
|
||||
await loadGrants();
|
||||
} catch (error: any) {
|
||||
@@ -193,25 +283,81 @@ async function applyPendingSql() {
|
||||
}
|
||||
}
|
||||
|
||||
function previewCreateUser() {
|
||||
async function previewCreateUser() {
|
||||
const userProvider = provider.value;
|
||||
const createUserSql = userProvider?.createUserSql;
|
||||
if (!createUserSql) return;
|
||||
if (!createUser.value.trim() || !createPassword.value) return;
|
||||
previewSql(
|
||||
createUserSql({
|
||||
if (!userProvider?.createUserSql || preparingCreatePlan.value) return;
|
||||
if (!createUser.value.trim() || !createPassword.value || !createDatabaseAuthorizationsValid.value) return;
|
||||
const requestId = ++createPlanRequestId;
|
||||
preparingCreatePlan.value = true;
|
||||
try {
|
||||
const principal = {
|
||||
user: createUser.value.trim(),
|
||||
host: createHost.value.trim() || "%",
|
||||
password: createPassword.value,
|
||||
canLogin: createCanLogin.value,
|
||||
}),
|
||||
{
|
||||
afterApply: async () => {
|
||||
createDialogOpen.value = false;
|
||||
createPassword.value = "";
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
const databases =
|
||||
createAccountType.value === "standard" && userProvider.dialect === "postgres"
|
||||
? await Promise.all(
|
||||
createDatabaseAuthorizations.value.map(async (selection) => ({
|
||||
...selection,
|
||||
schemas: await api.listSchemas(props.connection.id, selection.database),
|
||||
})),
|
||||
)
|
||||
: createAccountType.value === "standard"
|
||||
? createDatabaseAuthorizations.value
|
||||
: [];
|
||||
if (requestId !== createPlanRequestId || !createDialogOpen.value) return;
|
||||
const plan = buildCreateUserAuthorizationPlan({
|
||||
provider: userProvider,
|
||||
principal,
|
||||
accountType: createAccountType.value,
|
||||
databases,
|
||||
});
|
||||
pendingPlan.value = plan;
|
||||
pendingSql.value = authorizationPlanSql(plan);
|
||||
pendingResults.value = [];
|
||||
pendingDanger.value = createAccountType.value === "admin";
|
||||
pendingAfterApply.value = async () => {
|
||||
createDialogOpen.value = false;
|
||||
createPassword.value = "";
|
||||
selectedUserKey.value = userKey({ user: principal.user, host: isPostgres.value ? (principal.canLogin ? "LOGIN" : "ROLE") : principal.host });
|
||||
};
|
||||
sqlDialogOpen.value = true;
|
||||
} catch (error: any) {
|
||||
if (requestId === createPlanRequestId) toast(t("userAdmin.prepareAuthorizationFailed", { message: error?.message || String(error) }), 5000);
|
||||
} finally {
|
||||
if (requestId === createPlanRequestId) preparingCreatePlan.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function authorizationStepLabel(result: AuthorizationStepResult): string {
|
||||
const step = result.step;
|
||||
if (step.operation === "createUser") return t("userAdmin.stepCreateUser");
|
||||
if (step.operation === "grantAdmin") return t("userAdmin.stepGrantAdmin", { user: step.subject });
|
||||
if (step.operation === "grantDatabase") return t("userAdmin.stepGrantDatabase", { user: step.subject, database: step.targetDatabase });
|
||||
if (step.operation === "grantCurrentObjects") {
|
||||
return t("userAdmin.stepGrantCurrentObjects", {
|
||||
user: step.subject,
|
||||
database: step.targetDatabase,
|
||||
schema: step.schema,
|
||||
scope: authorizationObjectScopeLabel(step.objectScope),
|
||||
});
|
||||
}
|
||||
if (step.operation === "grantFutureObjects") {
|
||||
return step.owner
|
||||
? t("userAdmin.stepGrantFutureObjectsForOwner", { user: step.subject, database: step.targetDatabase, owner: step.owner, scope: authorizationObjectScopeLabel(step.objectScope) })
|
||||
: t("userAdmin.stepGrantFutureObjects", { user: step.subject, database: step.targetDatabase, scope: authorizationObjectScopeLabel(step.objectScope) });
|
||||
}
|
||||
return step.label;
|
||||
}
|
||||
|
||||
function authorizationObjectScopeLabel(scope: AuthorizationStepResult["step"]["objectScope"]): string {
|
||||
if (scope === "schemas") return t("userAdmin.scopeSchemas");
|
||||
if (scope === "tables") return t("userAdmin.scopeTables");
|
||||
if (scope === "sequences") return t("userAdmin.scopeSequences");
|
||||
if (scope === "functions") return t("userAdmin.scopeFunctions");
|
||||
return "";
|
||||
}
|
||||
|
||||
function previewPasswordChange() {
|
||||
@@ -291,6 +437,15 @@ function resetPrivilegeDefaults(scope: PrivilegeScope) {
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => createDialogOpen.value,
|
||||
(open) => {
|
||||
if (open) return;
|
||||
createPlanRequestId += 1;
|
||||
preparingCreatePlan.value = false;
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => selectedUserKey.value,
|
||||
() => void loadGrants(),
|
||||
@@ -300,6 +455,8 @@ watch(
|
||||
() => props.connection.id,
|
||||
() => {
|
||||
users.value = [];
|
||||
createDatabases.value = [];
|
||||
createDatabaseAuthorizations.value = [];
|
||||
selectedUserKey.value = "";
|
||||
grants.value = [];
|
||||
privilegeScope.value = provider.value?.defaultScope ?? "mysql";
|
||||
@@ -339,7 +496,7 @@ onMounted(loadUsers);
|
||||
<RefreshCcw v-else class="h-3.5 w-3.5" />
|
||||
{{ t("grid.refresh") }}
|
||||
</Button>
|
||||
<Button v-if="canCreateUser" size="sm" class="h-7 gap-1.5 px-2 text-xs" @click="createDialogOpen = true">
|
||||
<Button v-if="canCreateUser" size="sm" class="h-7 gap-1.5 px-2 text-xs" @click="openCreateUserDialog">
|
||||
<Plus class="h-3.5 w-3.5" />
|
||||
{{ t("userAdmin.newUser") }}
|
||||
</Button>
|
||||
@@ -508,11 +665,21 @@ onMounted(loadUsers);
|
||||
</div>
|
||||
|
||||
<Dialog v-model:open="createDialogOpen">
|
||||
<DialogContent class="max-w-sm">
|
||||
<DialogContent class="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ t("userAdmin.newUser") }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div class="space-y-3">
|
||||
<div class="grid max-h-[70vh] gap-4 overflow-auto pr-1">
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<button type="button" class="rounded-md border p-3 text-left" :class="createAccountType === 'standard' ? 'border-primary bg-primary/5' : 'hover:bg-muted/40'" @click="createAccountType = 'standard'">
|
||||
<span class="block text-sm font-medium">{{ t("userAdmin.standardUser") }}</span>
|
||||
<span class="mt-1 block text-xs text-muted-foreground">{{ t("userAdmin.standardUserHint") }}</span>
|
||||
</button>
|
||||
<button type="button" class="rounded-md border p-3 text-left" :class="createAccountType === 'admin' ? 'border-destructive bg-destructive/5' : 'hover:bg-muted/40'" @click="createAccountType = 'admin'">
|
||||
<span class="block text-sm font-medium">{{ t("userAdmin.adminUser") }}</span>
|
||||
<span class="mt-1 block text-xs text-muted-foreground">{{ t("userAdmin.adminUserHint") }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<label class="block text-xs font-medium">{{ createNameLabel }}</label>
|
||||
<Input v-model="createUser" />
|
||||
<template v-if="!isPostgres">
|
||||
@@ -525,10 +692,71 @@ onMounted(loadUsers);
|
||||
</label>
|
||||
<label class="block text-xs font-medium">{{ t("connection.password") }}</label>
|
||||
<PasswordInput v-model="createPassword" />
|
||||
|
||||
<div v-if="createAccountType === 'admin'" class="rounded-md border border-destructive/40 bg-destructive/5 p-3 text-xs text-destructive">
|
||||
{{ t("userAdmin.adminUserWarning") }}
|
||||
</div>
|
||||
|
||||
<div v-else class="grid gap-2">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<div class="text-xs font-medium">{{ t("userAdmin.databaseAccess") }}</div>
|
||||
<div class="mt-1 text-[11px] text-muted-foreground">{{ t("userAdmin.databaseAccessHint") }}</div>
|
||||
</div>
|
||||
<Badge variant="outline">{{ t("userAdmin.selectedDatabaseCount", { count: createDatabaseAuthorizations.length }) }}</Badge>
|
||||
</div>
|
||||
<div class="flex h-8 items-center gap-2 rounded-md border px-2">
|
||||
<Search class="h-3.5 w-3.5 text-muted-foreground" />
|
||||
<input v-model="createDatabaseSearch" class="min-w-0 flex-1 bg-transparent text-xs outline-none" :placeholder="t('userAdmin.searchDatabase')" />
|
||||
</div>
|
||||
<div class="max-h-64 overflow-auto rounded-md border">
|
||||
<div v-if="createDatabasesLoading" class="flex items-center gap-2 p-3 text-xs text-muted-foreground">
|
||||
<Loader2 class="h-3.5 w-3.5 animate-spin" />
|
||||
{{ t("userAdmin.loadingDatabases") }}
|
||||
</div>
|
||||
<div v-else-if="filteredCreateDatabases.length === 0" class="p-3 text-center text-xs text-muted-foreground">{{ t("userAdmin.emptyDatabases") }}</div>
|
||||
<div v-for="database in filteredCreateDatabases" :key="database" class="border-b p-2 last:border-b-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<input :checked="selectedCreateDatabaseSet.has(database)" type="checkbox" class="h-3.5 w-3.5 accent-primary" @change="toggleCreateDatabase(database)" />
|
||||
<button type="button" class="min-w-0 flex-1 truncate text-left text-xs font-medium" @click="toggleCreateDatabase(database)">{{ database }}</button>
|
||||
<Select v-if="selectedCreateDatabaseSet.has(database)" :model-value="createAuthorization(database)?.preset" @update:model-value="updateCreateDatabasePreset(database, $event)">
|
||||
<SelectTrigger class="h-7 w-32 text-xs"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="readWrite">{{ t("userAdmin.presetReadWrite") }}</SelectItem>
|
||||
<SelectItem value="readOnly">{{ t("userAdmin.presetReadOnly") }}</SelectItem>
|
||||
<SelectItem value="ddl">{{ t("userAdmin.presetDdl") }}</SelectItem>
|
||||
<SelectItem value="dml">{{ t("userAdmin.presetDml") }}</SelectItem>
|
||||
<SelectItem value="custom">{{ t("userAdmin.presetCustom") }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div v-if="createAuthorization(database)?.preset === 'custom'" class="mt-2 grid grid-cols-3 gap-1.5 pl-5">
|
||||
<button
|
||||
v-for="privilege in createAuthorizationPrivileges"
|
||||
:key="privilege"
|
||||
type="button"
|
||||
class="flex h-7 items-center gap-1.5 rounded border px-2 text-[10px]"
|
||||
:class="createAuthorization(database)?.privileges?.includes(privilege) ? 'border-primary bg-primary/10 text-primary' : 'bg-background'"
|
||||
@click="toggleCreateDatabasePrivilege(database, privilege)"
|
||||
>
|
||||
<Check v-if="createAuthorization(database)?.privileges?.includes(privilege)" class="h-3 w-3" />
|
||||
<span class="truncate">{{ privilege }}</span>
|
||||
</button>
|
||||
<p v-if="!createAuthorization(database)?.privileges?.length" class="col-span-3 text-[10px] text-destructive">
|
||||
{{ t("userAdmin.customPrivilegeRequired") }}
|
||||
</p>
|
||||
</div>
|
||||
<p v-if="isPostgres && selectedCreateDatabaseSet.has(database) && createAuthorization(database)?.preset === 'ddl'" class="mt-2 pl-5 text-[10px] text-muted-foreground">
|
||||
{{ t("userAdmin.postgresDdlHint") }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="createDialogOpen = false">{{ t("dangerDialog.cancel") }}</Button>
|
||||
<Button :disabled="!createUser.trim() || !createPassword" @click="previewCreateUser">
|
||||
<Button :disabled="!createUser.trim() || !createPassword || !createDatabaseAuthorizationsValid || preparingCreatePlan" @click="previewCreateUser">
|
||||
<Loader2 v-if="preparingCreatePlan" class="mr-1.5 h-3.5 w-3.5 animate-spin" />
|
||||
{{ t("userAdmin.previewSql") }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
@@ -557,12 +785,30 @@ onMounted(loadUsers);
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<pre class="max-h-[50vh] min-h-44 overflow-auto whitespace-pre-wrap rounded-md border bg-muted/30 p-3 font-mono text-xs leading-5" v-html="highlightedPendingSql" />
|
||||
<div v-if="pendingResults.length > 0" class="grid gap-2 rounded-md border p-3">
|
||||
<div class="text-xs font-semibold" :class="pendingStatus === 'success' ? 'text-green-600' : pendingStatus === 'partial' ? 'text-amber-600' : 'text-destructive'">
|
||||
{{ t(pendingStatus === "success" ? "userAdmin.resultSuccess" : pendingStatus === "partial" ? "userAdmin.resultPartial" : "userAdmin.resultFailed") }}
|
||||
</div>
|
||||
<div v-for="result in pendingResults" :key="result.step.id" class="flex items-start gap-2 text-xs">
|
||||
<Check v-if="result.status === 'success'" class="mt-0.5 h-3.5 w-3.5 shrink-0 text-green-600" />
|
||||
<AlertTriangle v-else-if="result.status === 'failed'" class="mt-0.5 h-3.5 w-3.5 shrink-0 text-destructive" />
|
||||
<span v-else class="mt-1 h-2 w-2 shrink-0 rounded-full bg-muted-foreground" />
|
||||
<span class="min-w-0">
|
||||
<span class="block">{{ authorizationStepLabel(result) }}</span>
|
||||
<span v-if="result.message" class="mt-0.5 block break-all text-destructive">{{ result.message }}</span>
|
||||
<span v-else-if="result.status === 'skipped'" class="mt-0.5 block text-muted-foreground">{{ t("userAdmin.stepSkipped") }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="sqlDialogOpen = false">{{ t("dangerDialog.cancel") }}</Button>
|
||||
<Button :variant="pendingDanger ? 'destructive' : 'default'" :disabled="applying" @click="applyPendingSql">
|
||||
<Loader2 v-if="applying" class="mr-1.5 h-3.5 w-3.5 animate-spin" />
|
||||
{{ t("userAdmin.applySql") }}
|
||||
</Button>
|
||||
<template v-if="pendingResults.length === 0">
|
||||
<Button variant="outline" @click="sqlDialogOpen = false">{{ t("dangerDialog.cancel") }}</Button>
|
||||
<Button :variant="pendingDanger ? 'destructive' : 'default'" :disabled="applying" @click="applyPendingSql">
|
||||
<Loader2 v-if="applying" class="mr-1.5 h-3.5 w-3.5 animate-spin" />
|
||||
{{ t("userAdmin.applySql") }}
|
||||
</Button>
|
||||
</template>
|
||||
<Button v-else @click="sqlDialogOpen = false">{{ t("userAdmin.closeResult") }}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { toRefs, watch } from "vue";
|
||||
import { Loader2, Clipboard, Upload } from "@lucide/vue";
|
||||
import { AlertTriangle, Check, Loader2, Clipboard, Upload } from "@lucide/vue";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
@@ -55,6 +55,19 @@ const {
|
||||
createDatabaseCollation,
|
||||
createDatabaseCollationOptionsForCharset,
|
||||
createDatabaseCollationsByCharset,
|
||||
createDatabaseUsers,
|
||||
createDatabaseSelectedUsers,
|
||||
createDatabaseUsersLoading,
|
||||
createDatabaseUserKey,
|
||||
createDatabaseUserLabel,
|
||||
createDatabaseUserSelected,
|
||||
toggleCreateDatabaseUser,
|
||||
showCreateDatabasePreviewDialog,
|
||||
createDatabasePreviewSql,
|
||||
createDatabaseAuthorizationResults,
|
||||
createDatabaseAuthorizationApplying,
|
||||
applyCreateDatabaseAuthorizationPlan,
|
||||
createDatabaseAuthorizationStepLabel,
|
||||
confirmCreateDatabase,
|
||||
showEditDatabasePropertiesDialog,
|
||||
editDatabasePropertiesLoading,
|
||||
@@ -93,6 +106,28 @@ function pasteTargetsMissing(entries: Array<{ targetName: string }>): boolean {
|
||||
return entries.every((entry) => !entry.targetName.trim());
|
||||
}
|
||||
|
||||
function returnToCreateDatabaseDialog() {
|
||||
showCreateDatabaseDialog.value = true;
|
||||
showCreateDatabasePreviewDialog.value = false;
|
||||
}
|
||||
|
||||
function updateCreateDatabasePreviewDialog(open: boolean) {
|
||||
if (open) {
|
||||
showCreateDatabasePreviewDialog.value = true;
|
||||
return;
|
||||
}
|
||||
if (createDatabaseAuthorizationApplying.value) return;
|
||||
if (createDatabaseAuthorizationResults.value.length === 0) {
|
||||
returnToCreateDatabaseDialog();
|
||||
return;
|
||||
}
|
||||
showCreateDatabasePreviewDialog.value = false;
|
||||
}
|
||||
|
||||
function closeCreateDatabaseResult() {
|
||||
showCreateDatabasePreviewDialog.value = false;
|
||||
}
|
||||
|
||||
watch(
|
||||
[
|
||||
showDeleteConfirm,
|
||||
@@ -104,6 +139,7 @@ watch(
|
||||
showDuplicateDialog,
|
||||
showPasteDialog,
|
||||
showCreateDatabaseDialog,
|
||||
showCreateDatabasePreviewDialog,
|
||||
showEditDatabasePropertiesDialog,
|
||||
showCreateNacosNamespaceDialog,
|
||||
showEditNacosNamespaceDialog,
|
||||
@@ -331,9 +367,63 @@ watch(
|
||||
</SearchableSelect>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="createDatabaseUsersLoading || createDatabaseUsers.length > 0" class="grid gap-2">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div>
|
||||
<div class="text-xs font-medium text-muted-foreground">{{ t("contextMenu.createDatabaseUsers") }}</div>
|
||||
<div class="mt-1 text-[11px] text-muted-foreground">{{ t("contextMenu.createDatabaseUsersHint") }}</div>
|
||||
</div>
|
||||
<span class="text-[11px] text-muted-foreground">{{ t("contextMenu.createDatabaseUsersSelected", { count: createDatabaseSelectedUsers.length }) }}</span>
|
||||
</div>
|
||||
<div class="max-h-40 overflow-auto rounded-md border">
|
||||
<div v-if="createDatabaseUsersLoading" class="flex items-center gap-2 p-3 text-xs text-muted-foreground">
|
||||
<Loader2 class="h-3.5 w-3.5 animate-spin" />
|
||||
{{ t("contextMenu.createDatabaseUsersLoading") }}
|
||||
</div>
|
||||
<template v-else>
|
||||
<button v-for="user in createDatabaseUsers" :key="createDatabaseUserKey(user)" type="button" class="flex w-full items-center gap-2 border-b px-3 py-2 text-left text-xs last:border-b-0 hover:bg-muted/50" @click="toggleCreateDatabaseUser(user)">
|
||||
<span class="flex h-3.5 w-3.5 items-center justify-center rounded border" :class="createDatabaseUserSelected(user) ? 'border-primary bg-primary text-primary-foreground' : 'border-border'">
|
||||
<Check v-if="createDatabaseUserSelected(user)" class="h-2.5 w-2.5" />
|
||||
</span>
|
||||
<span class="truncate">{{ createDatabaseUserLabel(user) }}</span>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="showCreateDatabaseDialog = false">{{ t("dangerDialog.cancel") }}</Button>
|
||||
<Button :disabled="!createDatabaseName.trim()" @click="confirmCreateDatabase">{{ t("dangerDialog.confirm") }}</Button>
|
||||
<Button :disabled="!createDatabaseName.trim()" @click="confirmCreateDatabase">{{ t("contextMenu.previewCreateDatabaseSql") }}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog :open="showCreateDatabasePreviewDialog" @update:open="updateCreateDatabasePreviewDialog">
|
||||
<DialogContent class="sm:max-w-[720px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ t("contextMenu.createDatabaseSqlPreview") }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<pre class="max-h-[48vh] min-h-44 overflow-auto whitespace-pre-wrap rounded-md border bg-muted/30 p-3 font-mono text-xs leading-5" v-html="highlight(createDatabasePreviewSql)" />
|
||||
<div v-if="createDatabaseAuthorizationResults.length > 0" class="grid gap-2 rounded-md border p-3">
|
||||
<div v-for="result in createDatabaseAuthorizationResults" :key="result.step.id" class="flex items-start gap-2 text-xs">
|
||||
<Check v-if="result.status === 'success'" class="mt-0.5 h-3.5 w-3.5 shrink-0 text-green-600" />
|
||||
<AlertTriangle v-else-if="result.status === 'failed'" class="mt-0.5 h-3.5 w-3.5 shrink-0 text-destructive" />
|
||||
<span v-else class="mt-1 h-2 w-2 shrink-0 rounded-full bg-muted-foreground" />
|
||||
<span class="min-w-0">
|
||||
<span class="block">{{ createDatabaseAuthorizationStepLabel(result) }}</span>
|
||||
<span v-if="result.message" class="mt-0.5 block break-all text-destructive">{{ result.message }}</span>
|
||||
<span v-else-if="result.status === 'skipped'" class="mt-0.5 block text-muted-foreground">{{ t("contextMenu.createDatabaseStepSkipped") }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<template v-if="createDatabaseAuthorizationResults.length === 0">
|
||||
<Button variant="outline" :disabled="createDatabaseAuthorizationApplying" @click="updateCreateDatabasePreviewDialog(false)">{{ t("dangerDialog.cancel") }}</Button>
|
||||
<Button :disabled="createDatabaseAuthorizationApplying" @click="applyCreateDatabaseAuthorizationPlan">
|
||||
<Loader2 v-if="createDatabaseAuthorizationApplying" class="mr-1.5 h-3.5 w-3.5 animate-spin" />
|
||||
{{ t("contextMenu.applyCreateDatabaseSql") }}
|
||||
</Button>
|
||||
</template>
|
||||
<Button v-else @click="closeCreateDatabaseResult">{{ t("contextMenu.closeCreateDatabaseResult") }}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -147,7 +147,8 @@ import { sidebarDisplayTableName } from "@/lib/sidebar/sidebarTableNameDisplay";
|
||||
import { shouldMeasureSidebarLabelOverflow } from "@/lib/sidebar/sidebarLabelTooltip";
|
||||
import { selectedTreeNodesInVisibleOrder as orderSelectedTreeNodes, treeSelectionRangeIdsByIndex, treeSelectionRangeIds } from "@/lib/sidebar/sidebarTreeSelection";
|
||||
import { connectionPasteTargetGroupId, selectedConnectionClipboardTargets, selectedConnectionDeleteTargets, selectedConnectionDuplicateTargets, selectedConnectionEditTarget } from "@/lib/sidebar/sidebarConnectionSelection";
|
||||
import { connectionSupportsDatabaseUserAdmin } from "@/lib/database/databaseUserAdmin";
|
||||
import { connectionSupportsDatabaseUserAdmin, resolveDatabaseUserAdminProviderForConnection, type DatabaseUserIdentity } from "@/lib/database/databaseUserAdmin";
|
||||
import { authorizationPlanSql, authorizationPlanStatus, buildCreateDatabaseAuthorizationPlan, executeAuthorizationPlan, type AuthorizationPlan, type AuthorizationStepResult } from "@/lib/database/databaseAuthorizationPlan";
|
||||
import { connectionSupportsProcessList } from "@/lib/database/processListDrivers";
|
||||
import { connectionSupportsServerDashboard } from "@/lib/database/mysqlServerStatus";
|
||||
import { connectionSupportsServerDashboard as connectionSupportsPgServerDashboard } from "@/lib/database/postgresServerStatus";
|
||||
@@ -230,6 +231,14 @@ import {
|
||||
createDatabaseName,
|
||||
createDatabaseCharset,
|
||||
createDatabaseCollation,
|
||||
createDatabaseUsers,
|
||||
createDatabaseSelectedUsers,
|
||||
createDatabaseUsersLoading,
|
||||
showCreateDatabasePreviewDialog,
|
||||
createDatabaseAuthorizationPlan,
|
||||
createDatabasePreviewSql,
|
||||
createDatabaseAuthorizationResults,
|
||||
createDatabaseAuthorizationApplying,
|
||||
showCreateNacosNamespaceDialog,
|
||||
createNacosNamespaceId,
|
||||
createNacosNamespaceName,
|
||||
@@ -3126,12 +3135,76 @@ function openCreateDatabaseDialog() {
|
||||
createDatabaseName.value = "";
|
||||
createDatabaseCharset.value = "utf8mb4";
|
||||
createDatabaseCollation.value = "utf8mb4_unicode_ci";
|
||||
createDatabaseUsers.value = [];
|
||||
createDatabaseSelectedUsers.value = [];
|
||||
createDatabaseUsersLoading.value = false;
|
||||
createDatabaseAuthorizationPlan.value = undefined;
|
||||
createDatabasePreviewSql.value = "";
|
||||
createDatabaseAuthorizationResults.value = [];
|
||||
createDatabaseCharsetOptions.value = fallbackCreateDatabaseCharset.charsets;
|
||||
createDatabaseCollationsByCharset.value = fallbackCreateDatabaseCharset.collationsByCharset;
|
||||
showCreateDatabaseDialog.value = true;
|
||||
if (canSetCreateDatabaseCharset.value) {
|
||||
void loadCreateDatabaseCharsetMetadata();
|
||||
}
|
||||
void loadCreateDatabaseUsers();
|
||||
}
|
||||
|
||||
let createDatabaseUsersRequestId = 0;
|
||||
|
||||
async function loadCreateDatabaseUsers() {
|
||||
const node = sidebarFormTarget.value ?? props.node;
|
||||
if (!node.connectionId) return;
|
||||
const connectionId = node.connectionId;
|
||||
const requestId = ++createDatabaseUsersRequestId;
|
||||
const requestOwner = sidebarTreeDialogOwner.value;
|
||||
const requestIsActive = () => requestId === createDatabaseUsersRequestId && sidebarTreeDialogOwner.value === requestOwner && sidebarFormTarget.value?.connectionId === connectionId && showCreateDatabaseDialog.value;
|
||||
const config = connectionStore.getConfig(node.connectionId);
|
||||
const userProvider = resolveDatabaseUserAdminProviderForConnection(config);
|
||||
if (!userProvider) return;
|
||||
createDatabaseUsersLoading.value = true;
|
||||
try {
|
||||
await connectionStore.ensureConnected(connectionId);
|
||||
try {
|
||||
const result = await api.executeQuery(connectionId, "", userProvider.listUsersSql(), undefined, undefined, { maxRows: 5000 });
|
||||
if (requestIsActive()) createDatabaseUsers.value = userProvider.parseUsers(result);
|
||||
} catch (error) {
|
||||
if (!userProvider.fallbackListUsersSql || !userProvider.parseFallbackUsers) throw error;
|
||||
const result = await api.executeQuery(connectionId, "", userProvider.fallbackListUsersSql(), undefined, undefined, { maxRows: 5000 });
|
||||
if (requestIsActive()) createDatabaseUsers.value = userProvider.parseFallbackUsers(result);
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (requestIsActive()) {
|
||||
createDatabaseUsers.value = [];
|
||||
toast(t("contextMenu.createDatabaseUsersLoadFailed", { message: error?.message || String(error) }), 5000);
|
||||
}
|
||||
} finally {
|
||||
if (requestIsActive()) createDatabaseUsersLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function createDatabaseUserKey(user: DatabaseUserIdentity): string {
|
||||
return `${user.user}\u0000${user.host}`;
|
||||
}
|
||||
|
||||
function createDatabaseUserLabel(user: DatabaseUserIdentity): string {
|
||||
const node = sidebarFormTarget.value ?? props.node;
|
||||
const provider = resolveDatabaseUserAdminProviderForConnection(connectionStore.getConfig(node.connectionId || ""));
|
||||
return provider?.label(user) ?? user.user;
|
||||
}
|
||||
|
||||
function createDatabaseUserSelected(user: DatabaseUserIdentity): boolean {
|
||||
const key = createDatabaseUserKey(user);
|
||||
return createDatabaseSelectedUsers.value.some((selected) => createDatabaseUserKey(selected) === key);
|
||||
}
|
||||
|
||||
function toggleCreateDatabaseUser(user: DatabaseUserIdentity) {
|
||||
const key = createDatabaseUserKey(user);
|
||||
if (createDatabaseSelectedUsers.value.some((selected) => createDatabaseUserKey(selected) === key)) {
|
||||
createDatabaseSelectedUsers.value = createDatabaseSelectedUsers.value.filter((selected) => createDatabaseUserKey(selected) !== key);
|
||||
} else {
|
||||
createDatabaseSelectedUsers.value = [...createDatabaseSelectedUsers.value, user];
|
||||
}
|
||||
}
|
||||
|
||||
function openConnectionNamespaceCreation() {
|
||||
@@ -3302,11 +3375,11 @@ async function confirmCreateDatabase() {
|
||||
const node = sidebarFormTarget.value ?? props.node;
|
||||
const name = createDatabaseName.value.trim();
|
||||
if (!name || !node.connectionId) return;
|
||||
showCreateDatabaseDialog.value = false;
|
||||
try {
|
||||
await connectionStore.ensureConnected(node.connectionId);
|
||||
const config = connectionStore.getConfig(node.connectionId);
|
||||
if (config?.db_type === "mongodb") {
|
||||
showCreateDatabaseDialog.value = false;
|
||||
await api.mongoCreateDatabase(node.connectionId, name);
|
||||
toast(t("contextMenu.createDatabaseSuccess", { name }), 3000);
|
||||
await connectionStore.ensureVisibleDatabase(node.connectionId, name);
|
||||
@@ -3321,15 +3394,78 @@ async function confirmCreateDatabase() {
|
||||
charset: createDatabaseCharset.value,
|
||||
collation: createDatabaseCollation.value,
|
||||
});
|
||||
await executeTreeNodeSqlWithProductionGuard(node, sql, { database: "" });
|
||||
toast(t("contextMenu.createDatabaseSuccess", { name }), 3000);
|
||||
await connectionStore.ensureVisibleDatabase(node.connectionId, name);
|
||||
await connectionStore.loadDatabases(node.connectionId, { force: true });
|
||||
const provider = resolveDatabaseUserAdminProviderForConnection(config);
|
||||
const plan: AuthorizationPlan = provider
|
||||
? buildCreateDatabaseAuthorizationPlan({ provider, database: name, createSql: sql, users: createDatabaseSelectedUsers.value })
|
||||
: { steps: [{ id: "create-database", label: `create database ${name}`, database: "", sql, operation: "createDatabase", targetDatabase: name }] };
|
||||
createDatabaseAuthorizationPlan.value = plan;
|
||||
createDatabasePreviewSql.value = authorizationPlanSql(plan);
|
||||
createDatabaseAuthorizationResults.value = [];
|
||||
showCreateDatabaseDialog.value = false;
|
||||
showCreateDatabasePreviewDialog.value = true;
|
||||
} catch (e: any) {
|
||||
toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
async function applyCreateDatabaseAuthorizationPlan() {
|
||||
const node = sidebarFormTarget.value ?? props.node;
|
||||
const plan = createDatabaseAuthorizationPlan.value;
|
||||
const name = createDatabaseName.value.trim();
|
||||
if (!node.connectionId || !plan || createDatabaseAuthorizationApplying.value) return;
|
||||
createDatabaseAuthorizationApplying.value = true;
|
||||
try {
|
||||
const results = await executeWithProductionSqlGuard({
|
||||
connection: connectionStore.getConfig(node.connectionId),
|
||||
database: "",
|
||||
sql: createDatabasePreviewSql.value,
|
||||
source: t("production.sourceSidebar"),
|
||||
execute: () => executeAuthorizationPlan(plan, (step) => api.executeMulti(node.connectionId!, step.database, step.sql, undefined, undefined, { maxRows: 1000, continueOnError: true })),
|
||||
});
|
||||
if (!results) return;
|
||||
createDatabaseAuthorizationResults.value = results;
|
||||
const created = results.some((result) => result.step.id === "create-database" && result.status === "success");
|
||||
const status = authorizationPlanStatus(results);
|
||||
if (created) {
|
||||
await connectionStore.ensureVisibleDatabase(node.connectionId, name);
|
||||
await connectionStore.loadDatabases(node.connectionId, { force: true });
|
||||
}
|
||||
toast(t(status === "success" ? "contextMenu.createDatabaseSuccess" : status === "partial" ? "contextMenu.createDatabasePartial" : "contextMenu.createDatabaseFailed", { name }), status === "success" ? 3000 : 5000);
|
||||
} catch (error: any) {
|
||||
toast(t("contextMenu.tableOperationFailed", { message: error?.message || String(error) }), 5000);
|
||||
} finally {
|
||||
createDatabaseAuthorizationApplying.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function createDatabaseAuthorizationStepLabel(result: AuthorizationStepResult): string {
|
||||
const step = result.step;
|
||||
if (step.operation === "createDatabase") return t("contextMenu.createDatabaseStep", { database: step.targetDatabase });
|
||||
if (step.operation === "grantDatabase") return t("contextMenu.createDatabaseGrantStep", { user: step.subject, database: step.targetDatabase });
|
||||
if (step.operation === "grantCurrentObjects") {
|
||||
return t("userAdmin.stepGrantCurrentObjects", {
|
||||
user: step.subject,
|
||||
database: step.targetDatabase,
|
||||
schema: step.schema,
|
||||
scope: createDatabaseAuthorizationScopeLabel(step.objectScope),
|
||||
});
|
||||
}
|
||||
if (step.operation === "grantFutureObjects") {
|
||||
return step.owner
|
||||
? t("userAdmin.stepGrantFutureObjectsForOwner", { user: step.subject, database: step.targetDatabase, owner: step.owner, scope: createDatabaseAuthorizationScopeLabel(step.objectScope) })
|
||||
: t("userAdmin.stepGrantFutureObjects", { user: step.subject, database: step.targetDatabase, scope: createDatabaseAuthorizationScopeLabel(step.objectScope) });
|
||||
}
|
||||
return step.label;
|
||||
}
|
||||
|
||||
function createDatabaseAuthorizationScopeLabel(scope: AuthorizationStepResult["step"]["objectScope"]): string {
|
||||
if (scope === "schemas") return t("userAdmin.scopeSchemas");
|
||||
if (scope === "tables") return t("userAdmin.scopeTables");
|
||||
if (scope === "sequences") return t("userAdmin.scopeSequences");
|
||||
if (scope === "functions") return t("userAdmin.scopeFunctions");
|
||||
return "";
|
||||
}
|
||||
|
||||
function dropDatabase() {
|
||||
void refreshDropDatabasePreviewSql();
|
||||
dropDatabaseLoading.value = false;
|
||||
@@ -4772,6 +4908,19 @@ function getTreeItemDialogController(): Record<string, any> {
|
||||
createDatabaseCharsetLoading,
|
||||
normalizeCreateDatabaseCharset,
|
||||
createDatabaseCollation,
|
||||
createDatabaseUsers,
|
||||
createDatabaseSelectedUsers,
|
||||
createDatabaseUsersLoading,
|
||||
createDatabaseUserKey,
|
||||
createDatabaseUserLabel,
|
||||
createDatabaseUserSelected,
|
||||
toggleCreateDatabaseUser,
|
||||
showCreateDatabasePreviewDialog,
|
||||
createDatabasePreviewSql,
|
||||
createDatabaseAuthorizationResults,
|
||||
createDatabaseAuthorizationApplying,
|
||||
applyCreateDatabaseAuthorizationPlan,
|
||||
createDatabaseAuthorizationStepLabel,
|
||||
createDatabaseCollationOptionsForCharset,
|
||||
createDatabaseCollationsByCharset,
|
||||
confirmCreateDatabase,
|
||||
|
||||
@@ -2,6 +2,8 @@ import { ref, shallowRef } from "vue";
|
||||
import type { TreeNode } from "@/types/database";
|
||||
import type { PasteTableMode } from "@/lib/table/tableClipboard";
|
||||
import { fallbackCreateDatabaseCharsetMetadata } from "@/lib/database/createDatabaseCharsetOptions";
|
||||
import type { DatabaseUserIdentity } from "@/lib/database/databaseUserAdmin";
|
||||
import type { AuthorizationPlan, AuthorizationStepResult } from "@/lib/database/databaseAuthorizationPlan";
|
||||
|
||||
export type DuplicateStructureSource = TreeNode & { connectionId: string; database: string };
|
||||
type ConnectionDeleteTarget = TreeNode & { connectionId: string };
|
||||
@@ -61,6 +63,14 @@ export const showCreateDatabaseDialog = ref(false);
|
||||
export const createDatabaseName = ref("");
|
||||
export const createDatabaseCharset = ref("utf8mb4");
|
||||
export const createDatabaseCollation = ref("utf8mb4_unicode_ci");
|
||||
export const createDatabaseUsers = ref<DatabaseUserIdentity[]>([]);
|
||||
export const createDatabaseSelectedUsers = ref<DatabaseUserIdentity[]>([]);
|
||||
export const createDatabaseUsersLoading = ref(false);
|
||||
export const showCreateDatabasePreviewDialog = ref(false);
|
||||
export const createDatabaseAuthorizationPlan = ref<AuthorizationPlan>();
|
||||
export const createDatabasePreviewSql = ref("");
|
||||
export const createDatabaseAuthorizationResults = ref<AuthorizationStepResult[]>([]);
|
||||
export const createDatabaseAuthorizationApplying = ref(false);
|
||||
export const showCreateNacosNamespaceDialog = ref(false);
|
||||
export const createNacosNamespaceId = ref("");
|
||||
export const createNacosNamespaceName = ref("");
|
||||
@@ -115,6 +125,7 @@ const openFlags = [
|
||||
showDuplicateDialog,
|
||||
showPasteDialog,
|
||||
showCreateDatabaseDialog,
|
||||
showCreateDatabasePreviewDialog,
|
||||
showCreateNacosNamespaceDialog,
|
||||
showEditNacosNamespaceDialog,
|
||||
showDropDatabaseConfirm,
|
||||
@@ -132,6 +143,13 @@ const openFlags = [
|
||||
|
||||
export function resetSidebarTreeDialogState() {
|
||||
for (const flag of openFlags) flag.value = false;
|
||||
createDatabaseUsers.value = [];
|
||||
createDatabaseSelectedUsers.value = [];
|
||||
createDatabaseUsersLoading.value = false;
|
||||
createDatabaseAuthorizationPlan.value = undefined;
|
||||
createDatabasePreviewSql.value = "";
|
||||
createDatabaseAuthorizationResults.value = [];
|
||||
createDatabaseAuthorizationApplying.value = false;
|
||||
sidebarTreeDialogOwner.value = null;
|
||||
sidebarDangerTarget.value = null;
|
||||
sidebarFormTarget.value = null;
|
||||
|
||||
Reference in new issue
Block a user