jcloud/dashboard/src2/components/site_database_user/SiteDatabaseAddEditUserDialog.vue
2025-04-12 17:39:38 +08:00

434 lines
11 KiB
Vue

<template>
<Dialog
:options="{
title: isEditMode ? '编辑数据库用户' : '添加数据库用户',
size: '2xl'
}"
v-model="showDialog"
>
<template #body-content>
<div class="flex flex-col gap-2">
<AlertBanner
v-if="
isEditMode &&
this.$resources?.databaseUser?.pg?.status === 'Failed' &&
this.$resources?.databaseUser?.pg?.failure_reason
"
:title="this.$resources?.databaseUser?.pg?.failure_reason"
type="error"
>
</AlertBanner>
<FormControl
class="mt-2"
type="text"
size="sm"
variant="subtle"
label="标签(用于标识用户)"
v-model="label"
/>
<FormControl
type="select"
:options="[
{
label: '对所有表的只读访问权限',
value: 'read_only'
},
{
label: '对所有表的读写访问权限',
value: 'read_write'
},
{
label: '细粒度访问权限',
value: 'granular'
}
]"
size="sm"
variant="subtle"
:disabled="false"
label="访问模式"
v-model="mode"
/>
<FormControl
v-if="!isEditMode"
class="mt-2"
type="number"
size="sm"
variant="subtle"
label="数据库连接数"
v-model="database_connections"
/>
<!-- 细粒度模式的权限配置 -->
<div v-if="mode == 'granular'">
<div
v-if="isLoadingTableSchemas"
class="flex w-full flex-col items-center justify-center gap-2 text-base text-gray-700"
>
<span class="flex flex-row gap-2 py-20">
<Spinner class="w-4" /> 正在获取表结构
</span>
<p class="text-sm">这可能需要长达5分钟</p>
</div>
<div v-else class="mt-2">
<p class="text-sm font-medium text-gray-600">
配置权限
</p>
<ObjectList :options="listOptions" />
<div class="mt-4 flex w-full gap-2">
<Button
variant="outline"
iconLeft="plus"
@click="addNewTablePermissionEntry"
>添加表</Button
>
<Button
variant="outline"
iconLeft="refresh-ccw"
:loading="isLoadingTableSchemas"
@click="() => fetchTableSchemas(true)"
>刷新结构</Button
>
</div>
</div>
</div>
</div>
<ErrorMessage
:message="
this.$resources?.createDatabaseUser?.error ||
this.$resources?.updateDatabaseUser?.error
"
class="mt-2"
/>
</template>
<template #actions>
<Button
v-if="!isEditMode"
class="w-full"
variant="solid"
theme="gray"
:disabled="mode == 'granular' && isLoadingTableSchemas"
:loading="this.$resources.createDatabaseUser.loading"
@click="this.$resources.createDatabaseUser.submit()"
>
创建用户
</Button>
<Button
v-else
class="w-full"
variant="solid"
theme="gray"
:disabled="mode == 'granular' && isLoadingTableSchemas"
:loading="this.$resources.updateDatabaseUser.loading"
@click="this.$resources.updateDatabaseUser.submit()"
>保存更改</Button
>
</template>
</Dialog>
</template>
<script>
import { h } from 'vue';
import ObjectList from '../ObjectList.vue';
import { ErrorMessage, FormControl } from 'jingrow-ui';
import { icon } from '../../utils/components';
import { toast } from 'vue-sonner';
import AlertBanner from '../AlertBanner.vue';
import SiteDatabaseColumnsSelector from './SiteDatabaseColumnsSelector.vue';
import { DashboardError } from '../../utils/error';
export default {
name: 'SiteDatabaseAddEditUserDialog',
props: ['site', 'db_user_name', 'modelValue'], // db_user_name 是可选的,仅在编辑时提供
emits: ['success'],
components: {
FormControl,
ObjectList,
icon,
AlertBanner,
SiteDatabaseColumnsSelector
},
data() {
return {
label: '',
mode: 'read_only',
database_connections: 1,
permissions: [],
lastGeneratedRowId: 0
};
},
mounted() {
this.fetchTableSchemas();
if (!this.isEditMode) {
this.addNewTablePermissionEntry();
} else {
this.$resources.databaseUser.reload();
}
},
resources: {
tableSchemas() {
return {
url: 'jcloud.api.client.run_pg_method',
initialData: {},
auto: false,
onSuccess: data => {
if (data?.message?.loading) {
setTimeout(this.fetchTableSchemas, 5000);
}
}
};
},
databaseUser() {
return {
type: 'document',
pagetype: 'Site Database User',
name: this.db_user_name,
auto: false,
onSuccess: data => {
this.label = data?.label;
this.mode = data?.mode;
let fetched_permissions = (data?.permissions ?? []).map(x => {
return {
...x,
columns: x.selected_columns.split('\n').filter(x => x)
};
});
this.permissions = fetched_permissions;
this.database_connections = data?.max_connections ?? 1;
}
};
},
createDatabaseUser() {
return {
url: 'jcloud.api.client.insert',
makeParams() {
let permissions = [];
this.permissions.forEach(permission => {
if (permission.table) {
permissions.push({
table: permission.table,
mode: permission.mode,
allow_all_columns: permission.columns.length == 0,
selected_columns: permission.columns.join('\n')
});
}
});
return {
pg: {
pagetype: 'Site Database User',
label: this.label,
team: this.$team.pg.name,
site: this.site,
mode: this.mode,
permissions: permissions,
max_connections: parseInt(this.database_connections || 1)
}
};
},
validate() {
if (!this.label)
throw new DashboardError('请为用户提供一个标签');
},
onSuccess() {
toast.success('用户创建成功');
this.$emit('success');
}
};
},
updateDatabaseUser() {
return {
url: 'jcloud.api.client.run_pg_method',
makeParams() {
let permissions = [];
this.permissions.forEach(permission => {
if (permission.table) {
permissions.push({
table: permission.table,
mode: permission.mode,
allow_all_columns: permission.columns.length == 0,
selected_columns: permission.columns.join('\n')
});
}
});
return {
dt: 'Site Database User',
dn: this.db_user_name,
method: 'save_and_apply_changes',
args: {
label: this.label,
mode: this.mode,
permissions: permissions
}
};
},
validate() {
if (!this.label)
throw new DashboardError('请为用户提供一个标签');
},
onSuccess() {
toast.success('用户更新成功');
this.$emit('success');
}
};
}
},
computed: {
listOptions() {
return {
data: () => this.permissions,
columns: [
{
label: '表',
fieldname: 'table',
width: '200px',
type: 'Component',
component: ({ row }) => {
return h(FormControl, {
class: 'w-full -mx-1.5',
type: 'autocomplete',
modelValue: row.table,
'onUpdate:modelValue': newValue => {
row.table = newValue?.value || '';
},
options: this.autocompleteTableOptions
});
}
},
{
label: '模式',
fieldname: 'mode',
width: 1,
align: 'center',
type: 'Component',
component: ({ row }) => {
return h(FormControl, {
type: 'select',
class: 'w-full',
options: [
{
label: '只读',
value: 'read_only'
},
{
label: '读写',
value: 'read_write'
}
],
modelValue: row.mode,
'onUpdate:modelValue': newValue => {
row.mode = newValue;
}
});
}
},
{
label: '列',
width: '200px',
fieldname: 'columns',
align: 'center',
type: 'Component',
component: ({ row }) => {
return h(SiteDatabaseColumnsSelector, {
modelValue: row.columns,
availableColumns: this.getColumns(row.table),
'onUpdate:modelValue': newValues => {
row.columns = [...newValues];
}
});
}
},
{
label: '',
width: 0.6,
align: 'right',
type: 'Button',
Button: ({ row }) => {
return {
label: true ? '检查' : '添加',
slots: {
icon: icon('x')
},
variant: 'subtle',
onClick: event => {
this.removePermissionEntry(row.name);
event.stopPropagation();
}
};
}
}
]
};
},
showDialog: {
get() {
return this.modelValue;
},
set(value) {
this.$emit('update:modelValue', value);
}
},
isEditMode() {
return !!this.db_user_name;
},
isLoadingTableSchemas() {
if (this.$resources?.tableSchemas?.loading) return true;
if (this.$resources?.tableSchemas?.data?.message?.loading) return true;
return false;
},
tableNames() {
if (this.isLoadingTableSchemas) return [];
if (!this.$resources?.tableSchemas?.data?.message?.data) return [];
return Object.keys(this.$resources?.tableSchemas?.data?.message?.data);
},
autocompleteTableOptions() {
if (this.isLoadingTableSchemas) return [];
if (!this.$resources?.tableSchemas?.data?.message?.data) return [];
return Object.keys(
this.$resources?.tableSchemas?.data?.message?.data
).map(x => ({
label: x,
value: x
}));
}
},
methods: {
fetchTableSchemas(reload = false) {
if (!this.site) return;
this.$resources.tableSchemas.submit({
dt: 'Site',
dn: this.site,
method: 'fetch_database_table_schema',
args: {
reload
}
});
},
getColumns(table) {
if (!table) return [];
if (this.isLoadingTableSchemas) return [];
if (!this.$resources?.tableSchemas?.data?.message?.data) return [];
let columnSchemas =
this.$resources?.tableSchemas?.data?.message?.data[table]?.columns ??
[];
let columns = [];
columnSchemas.forEach(x => {
columns.push(x.column);
});
return columns;
},
addNewTablePermissionEntry() {
this.lastGeneratedRowId = this.lastGeneratedRowId + 1;
this.permissions = [
...this.permissions,
{
name: String(this.lastGeneratedRowId),
table: '',
mode: 'read_only',
columns: []
}
];
},
removePermissionEntry(name) {
this.permissions = this.permissions.filter(x => x.name !== name);
}
}
};
</script>