2 changed files with 587 additions and 0 deletions
@ -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() |
|||
}) |
|||
} |
|||
@ -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…
Reference in new issue