Browse Source

数据源管理页面

master
siyaqi 1 week ago
parent
commit
6d8a7d203d
  1. 70
      vue-fastapi-frontend/src/api/meta/dsDataSource.js
  2. 517
      vue-fastapi-frontend/src/views/dsDataSource/index.vue

70
vue-fastapi-frontend/src/api/meta/dsDataSource.js

@ -0,0 +1,70 @@
import request from '@/utils/request'
import cache from '@/plugins/cache'
function getDashHeaders() {
return {
dashUserName: cache.local.get('username'),
dashPassword: cache.local.get('password')
}
}
export function queryDataSourceListPaging(params) {
return request({
url: '/ds-api/dolphinscheduler/datasources',
method: 'get',
params,
headers: getDashHeaders()
})
}
export function queryDataSource(id) {
return request({
url: `/ds-api/dolphinscheduler/datasources/${id}`,
method: 'get',
headers: getDashHeaders()
})
}
export function verifyDataSourceName(params) {
return request({
url: '/ds-api/dolphinscheduler/datasources/verify-name',
method: 'get',
params,
headers: getDashHeaders()
})
}
export function createDataSource(data) {
return request({
url: '/ds-api/dolphinscheduler/datasources',
method: 'post',
data,
headers: getDashHeaders()
})
}
export function connectDataSource(data) {
return request({
url: '/ds-api/dolphinscheduler/datasources/connect',
method: 'post',
data,
headers: getDashHeaders()
})
}
export function updateDataSource(data, id) {
return request({
url: `/ds-api/dolphinscheduler/datasources/${id}`,
method: 'put',
data,
headers: getDashHeaders()
})
}
export function deleteDataSource(id) {
return request({
url: `/ds-api/dolphinscheduler/datasources/${id}`,
method: 'delete',
headers: getDashHeaders()
})
}

517
vue-fastapi-frontend/src/views/dsDataSource/index.vue

@ -0,0 +1,517 @@
<template>
<div class="app-container">
<el-row :gutter="10" class="mb8">
<el-form :inline="true" :model="queryForm">
<el-form-item label="关键字">
<el-input
v-model="queryForm.searchVal"
placeholder="请输入关键字"
clearable
style="width: 240px"
@keyup.enter="handleSearch"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleSearch">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-row>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="openAddDialog">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="success"
plain
icon="Edit"
:disabled="selectedRows.length !== 1"
@click="openEditDialog(selectedRows[0])"
>
修改
</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="Delete"
:disabled="selectedRows.length === 0"
@click="deleteSelected"
>
删除
</el-button>
</el-col>
</el-row>
<el-table
v-loading="loading"
:data="list"
border
stripe
style="width: 100%"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column type="index" label="#" width="60" />
<el-table-column prop="name" label="数据源名称" min-width="180" />
<el-table-column prop="userName" label="所属用户" min-width="140" />
<el-table-column prop="type" label="数据源类型" min-width="140" />
<el-table-column label="数据源参数" min-width="120">
<template #default="{ row }">
<el-button link type="primary" @click="showParams(row)">点击查看</el-button>
</template>
</el-table-column>
<el-table-column prop="note" label="描述" min-width="160">
<template #default="{ row }">{{ row.note || '-' }}</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="180" />
<el-table-column prop="updateTime" label="更新时间" width="180" />
<el-table-column label="操作" width="140" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="openEditDialog(row)">编辑</el-button>
<el-button link type="danger" @click="deleteRow(row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total > 0"
:total="total"
v-model:page="queryForm.pageNo"
v-model:limit="queryForm.pageSize"
@pagination="getList"
/>
<el-dialog v-model="paramDialogOpen" title="数据源参数" width="700px" append-to-body>
<pre class="json-content">{{ currentParams }}</pre>
<template #footer>
<el-button @click="paramDialogOpen = false">关闭</el-button>
</template>
</el-dialog>
<el-dialog
:title="dialogTitle"
v-model="open"
width="760px"
append-to-body
:before-close="handleClose"
>
<el-form ref="formRef" :model="form" :rules="rules" label-width="140px">
<el-form-item label="数据源类型" prop="type">
<el-select
v-model="form.type"
placeholder="请选择数据源类型"
style="width: 100%"
:disabled="isEdit"
@change="handleTypeChange"
>
<el-option v-for="item in datasourceTypeList" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="数据源名称" prop="name">
<el-input v-model="form.name" maxlength="60" placeholder="请输入数据源名称" />
</el-form-item>
<el-form-item label="描述" prop="note">
<el-input v-model="form.note" type="textarea" placeholder="请输入描述" />
</el-form-item>
<el-form-item v-if="showHost" label="IP主机名" prop="host">
<el-input v-model="form.host" maxlength="255" placeholder="请输入IP主机名" />
</el-form-item>
<el-form-item v-if="showPort" label="端口" prop="port">
<el-input-number v-model="form.port" :min="0" :controls="false" style="width: 100%" @blur="rememberPort" />
</el-form-item>
<el-form-item v-if="showPrincipal" label="Principal" prop="principal">
<el-input v-model="form.principal" placeholder="请输入Principal" />
</el-form-item>
<el-form-item v-if="showPrincipal" label="krb5.conf" prop="javaSecurityKrb5Conf">
<el-input v-model="form.javaSecurityKrb5Conf" placeholder="请输入Kerberos认证参数 java.security.krb5.conf" />
</el-form-item>
<el-form-item v-if="showPrincipal" label="keytab.username" prop="loginUserKeytabUsername">
<el-input v-model="form.loginUserKeytabUsername" placeholder="请输入Kerberos认证参数 login.user.keytab.username" />
</el-form-item>
<el-form-item v-if="showPrincipal" label="keytab.path" prop="loginUserKeytabPath">
<el-input v-model="form.loginUserKeytabPath" placeholder="请输入Kerberos认证参数 login.user.keytab.path" />
</el-form-item>
<el-form-item label="用户名" prop="userName">
<el-input v-model="form.userName" maxlength="60" placeholder="请输入用户名" />
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="form.password" type="password" show-password placeholder="请输入密码" />
</el-form-item>
<el-form-item v-if="showAwsRegion" label="AwsRegion" prop="awsRegion">
<el-input v-model="form.awsRegion" maxlength="60" placeholder="请输入AwsRegion" />
</el-form-item>
<el-form-item label="数据库名" prop="database">
<el-input v-model="form.database" maxlength="60" placeholder="请输入数据库名" />
</el-form-item>
<el-form-item v-if="showConnectType" label="服务名或SID" prop="connectType">
<el-radio-group v-model="form.connectType">
<el-radio label="ORACLE_SERVICE_NAME">服务名</el-radio>
<el-radio label="ORACLE_SID">SID</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="jdbc连接参数" prop="other">
<el-input
v-model="form.other"
type="textarea"
:autosize="{ minRows: 2 }"
placeholder='请输入格式为 {"key1":"value1","key2":"value2"...} 连接参数'
/>
</el-form-item>
</el-form>
<template #footer>
<el-button :loading="testing" @click="testConnect">测试连接</el-button>
<el-button @click="open = false">取消</el-button>
<el-button type="primary" :loading="saving" @click="submitForm">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { computed, onMounted, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
queryDataSourceListPaging,
queryDataSource,
verifyDataSourceName,
createDataSource,
updateDataSource,
connectDataSource,
deleteDataSource
} from '@/api/meta/dsDataSource'
const datasourceTypeMap = {
MYSQL: { value: 'MYSQL', label: 'MYSQL', defaultPort: 3306 },
POSTGRESQL: { value: 'POSTGRESQL', label: 'POSTGRESQL', defaultPort: 5432 },
HIVE: { value: 'HIVE', label: 'HIVE/IMPALA', defaultPort: 10000 },
SPARK: { value: 'SPARK', label: 'SPARK', defaultPort: 10015 },
CLICKHOUSE: { value: 'CLICKHOUSE', label: 'CLICKHOUSE', defaultPort: 8123 },
ORACLE: { value: 'ORACLE', label: 'ORACLE', defaultPort: 1521 },
SQLSERVER: { value: 'SQLSERVER', label: 'SQLSERVER', defaultPort: 1433 },
DB2: { value: 'DB2', label: 'DB2', defaultPort: 50000 },
PRESTO: { value: 'PRESTO', label: 'PRESTO', defaultPort: 8080 },
REDSHIFT: { value: 'REDSHIFT', label: 'REDSHIFT', defaultPort: 5439 },
ATHENA: { value: 'ATHENA', label: 'ATHENA', defaultPort: 0 }
}
const datasourceTypeList = Object.values(datasourceTypeMap)
const queryForm = reactive({
pageNo: 1,
pageSize: 10,
searchVal: ''
})
const loading = ref(false)
const list = ref([])
const total = ref(0)
const selectedRows = ref([])
const open = ref(false)
const isEdit = ref(false)
const editingId = ref(undefined)
const previousName = ref('')
const saving = ref(false)
const testing = ref(false)
const formRef = ref()
const paramDialogOpen = ref(false)
const currentParams = ref('')
const form = reactive({
type: 'MYSQL',
name: '',
note: '',
host: '',
port: 3306,
principal: '',
javaSecurityKrb5Conf: '',
loginUserKeytabUsername: '',
loginUserKeytabPath: '',
userName: '',
password: '',
database: '',
awsRegion: '',
connectType: '',
other: ''
})
const formFlags = reactive({
requiredDataBase: true,
showHost: true,
showPort: true,
showAwsRegion: false,
showConnectType: false,
showPrincipal: false
})
const showHost = computed(() => formFlags.showHost)
const showPort = computed(() => formFlags.showPort)
const showAwsRegion = computed(() => formFlags.showAwsRegion)
const showConnectType = computed(() => formFlags.showConnectType)
const showPrincipal = computed(() => formFlags.showPrincipal)
const dialogTitle = computed(() => `${isEdit.value ? '编辑' : '创建'}数据源`)
const validateJson = (_, value, callback) => {
if (!value) {
callback()
return
}
try {
JSON.parse(value)
callback()
} catch (e) {
callback(new Error('jdbc连接参数不是一个正确的JSON格式'))
}
}
const rules = {
name: [{ required: true, message: '请输入数据源名称', trigger: 'blur' }],
host: [{ validator: (_, value, callback) => (!showHost.value || value ? callback() : callback(new Error('请输入IP主机名'))), trigger: 'blur' }],
port: [{ validator: (_, value, callback) => (!showPort.value || value !== null && value !== undefined ? callback() : callback(new Error('请输入端口'))), trigger: 'blur' }],
principal: [{ validator: (_, value, callback) => (!showPrincipal.value || value ? callback() : callback(new Error('请输入Principal'))), trigger: 'blur' }],
userName: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
awsRegion: [{ validator: (_, value, callback) => (!showAwsRegion.value || value ? callback() : callback(new Error('请输入AwsRegion'))), trigger: 'blur' }],
database: [{ validator: (_, value, callback) => (!formFlags.requiredDataBase || value ? callback() : callback(new Error('请输入数据库名'))), trigger: 'blur' }],
connectType: [{ validator: (_, value, callback) => (!showConnectType.value || value ? callback() : callback(new Error('请选择服务名或SID'))), trigger: 'change' }],
other: [{ validator: validateJson, trigger: ['blur', 'change'] }]
}
function resetForm() {
Object.assign(form, {
type: 'MYSQL',
name: '',
note: '',
host: '',
port: datasourceTypeMap.MYSQL.defaultPort,
principal: '',
javaSecurityKrb5Conf: '',
loginUserKeytabUsername: '',
loginUserKeytabPath: '',
userName: '',
password: '',
database: '',
awsRegion: '',
connectType: '',
other: ''
})
handleTypeChange(form.type)
}
function handleSelectionChange(rows) {
selectedRows.value = rows
}
async function getList() {
loading.value = true
try {
const res = await queryDataSourceListPaging({ ...queryForm })
const payload = res?.data || res
list.value = payload?.totalList || payload?.rows || []
total.value = payload?.total || 0
} finally {
loading.value = false
}
}
function handleSearch() {
queryForm.pageNo = 1
getList()
}
function resetQuery() {
queryForm.searchVal = ''
queryForm.pageNo = 1
queryForm.pageSize = 10
getList()
}
function showParams(row) {
const payload = row.other || row.parameter || row
currentParams.value = typeof payload === 'string' ? payload : JSON.stringify(payload, null, 2)
paramDialogOpen.value = true
}
async function openAddDialog() {
isEdit.value = false
editingId.value = undefined
previousName.value = ''
resetForm()
open.value = true
}
async function openEditDialog(row) {
if (!row?.id) {
ElMessage.warning('请选择一条数据源记录')
return
}
isEdit.value = true
editingId.value = row.id
resetForm()
open.value = true
const detail = await queryDataSource(row.id)
const payload = detail?.data || detail
previousName.value = payload?.name || ''
Object.assign(form, {
...payload,
other: payload?.other ? JSON.stringify(payload.other) : ''
})
handleTypeChange(form.type)
}
function rememberPort() {
if (form.type && datasourceTypeMap[form.type]) {
datasourceTypeMap[form.type].previousPort = form.port
}
}
async function handleTypeChange(type) {
const current = datasourceTypeMap[type]
if (!current) {
return
}
form.type = type
form.port = current.previousPort ?? current.defaultPort
formFlags.requiredDataBase = type !== 'POSTGRESQL' && type !== 'ATHENA'
formFlags.showHost = type !== 'ATHENA'
formFlags.showPort = type !== 'ATHENA'
formFlags.showAwsRegion = type === 'ATHENA'
formFlags.showConnectType = type === 'ORACLE'
formFlags.showPrincipal = false
if (type === 'ORACLE' && !isEdit.value && !form.connectType) {
form.connectType = 'ORACLE_SERVICE_NAME'
}
}
function formatSubmitParams() {
return {
...form,
other: form.other ? JSON.parse(form.other) : null
}
}
async function testConnect() {
try {
await formRef.value.validate()
} catch {
return
}
testing.value = true
try {
const res = await connectDataSource(formatSubmitParams())
ElMessage.success(res?.msg || '测试连接成功')
} finally {
testing.value = false
}
}
async function submitForm() {
try {
await formRef.value.validate()
} catch {
return
}
saving.value = true
try {
if (previousName.value !== form.name) {
const verifyRes = await verifyDataSourceName({ name: form.name })
if (verifyRes && (verifyRes.failed === true || verifyRes.success === false)) {
ElMessage.error(verifyRes.msg || '数据源名称校验失败')
return
}
}
if (isEdit.value && editingId.value) {
await updateDataSource(formatSubmitParams(), editingId.value)
ElMessage.success('编辑成功')
} else {
await createDataSource(formatSubmitParams())
ElMessage.success('新增成功')
}
open.value = false
getList()
} finally {
saving.value = false
}
}
function deleteRow(id) {
ElMessageBox.confirm('确认删除该数据源吗?', '提示', { type: 'warning' })
.then(async () => {
await deleteDataSource(id)
ElMessage.success('删除成功')
getList()
})
.catch(() => {})
}
function deleteSelected() {
if (!selectedRows.value.length) {
ElMessage.warning('请至少选择一条记录')
return
}
ElMessageBox.confirm(`确认删除选中的 ${selectedRows.value.length} 条记录吗?`, '提示', { type: 'warning' })
.then(async () => {
await Promise.all(selectedRows.value.map((row) => deleteDataSource(row.id)))
ElMessage.success('删除成功')
getList()
})
.catch(() => {})
}
function handleClose(done) {
formRef.value?.resetFields()
done()
}
onMounted(() => {
getList()
})
</script>
<style scoped>
.app-container {
padding: 20px;
}
.mb8 {
margin-bottom: 8px;
}
.json-content {
margin: 0;
max-height: 420px;
overflow: auto;
padding: 12px;
background: #f6f8fa;
border-radius: 4px;
white-space: pre-wrap;
word-break: break-all;
}
</style>
Loading…
Cancel
Save