<template>
<div class="jdbc-instance-table">
<el-table
:data="data.nodeList"
:border="false"
style="width: 100%"
>
<el-table-column prop="ip" :label="$t('database.JdbcNodeTable.else1')">
<template #default="{ row: record }">
{{ record.ip || '--' }}
</template>
</el-table-column>
<el-table-column prop="port" :label="$t('database.JdbcNodeTable.else2')">
<template #default="{ row: record }">
{{ record.port || '--' }}
</template>
</el-table-column>
<el-table-column prop="username" :label="$t('database.JdbcInstance.5oxhtcboa240')">
<template #default="{ row: record }">
{{ record.username || '--' }}
</template>
</el-table-column>
<el-table-column prop="state" :label="$t('database.JdbcNodeTable.connectionStatus')">
<template #default="{ row: record }">
<span class="status-container">
<span class="status-dot"
:class="{ 'error-dot': record.status === 'error', 'success-dot': record.status === 'success', 'info-dot': record.status === 'loading'}" />
{{record.status}}
</span>
</template>
</el-table-column>
<template v-for="col in dbSpecificColumns" :key="col.prop">
<el-table-column
:prop="col.prop"
:label="getColumnLabel(col)"
:min-width="120"
>
<template #default="{ row }">
{{ formatColumnValue(row[col.prop], col.prop) }}
</template>
</el-table-column>
</template>
</el-table>
</div>
</template>
<script setup lang="ts">
import { PropType, onMounted, onUnmounted, reactive, ref, computed, watch } from 'vue'
import { KeyValue } from '@/types/global'
import Socket from '@/utils/websocket'
import {jdbcNodeMonitor} from '@/api/ops'
import { useI18n } from 'vue-i18n'
import {JDBCType} from "@/types/jdbc";
const { t } = useI18n()
const data = reactive<KeyValue>({
socketArr: [],
nodeList: []
})
const props = defineProps({
jdbcData: {
type: Array as PropType<KeyValue>,
required: true
}
})
onMounted(() => {
data.nodeList = JSON.parse(JSON.stringify(props.jdbcData.nodes))
openMonitor()
})
onUnmounted(() => {
if (data.socketArr.length) {
data.socketArr.forEach((item: Socket<any, any>) => {
if (item) {
item.destroy()
}
})
}
})
const emits = defineEmits(['validRes'])
const stateComp = computed(() => {
const stateList = data.nodeList.filter((item: KeyValue) => {
return item.state === 1
})
if (stateList.length === data.nodeList.length) {
return true
}
return false
})
watch(stateComp, (val: boolean) => {
if (val) {
emits('validRes', 1)
} else {
emits('validRes', 0)
}
})
const getNodeStateColor = (state: number) => {
switch (state) {
case -1:
return 'un-check'
case 1:
return 'check-pass'
case 0:
return 'check-error'
}
}
const getNodeState = (state?: number): string => {
if (state == null) {
return 'error'
}
switch (state) {
case -1:
return 'checking'
case 1:
return 'pass'
case 0:
return 'error'
default:
return 'error'
}
}
const openMonitor = () => {
data.nodeList.filter((item: KeyValue, index: number) => {
item.loading = false
item.state = -1
openNodeMonitor(item, index)
})
}
const openNodeMonitor = (nodeData: KeyValue, index: number) => {
const socketKey = new Date().getTime()
const param = {
businessId: 'monitor_ops_jdbc_' + nodeData.clusterNodeId + '_' + socketKey,
dbType: props.jdbcData.dbType
}
const websocket = new Socket({ url: `COMMAND_EXEC/${param.businessId}` })
websocket.onopen(() => {
data.nodeList[index].loading = true
jdbcNodeMonitor(nodeData.clusterNodeId, param).then((res: KeyValue) => {
if (Number(res.code) !== 200) {
data.nodeList[index].state = 0
websocket.destroy()
} else {
data.nodeList[index].state = 1
data.socketArr.push(websocket)
}
}).catch((error: any) => {
console.log('Websocket error:', error)
data.nodeList[index].state = 0
websocket.destroy()
}).finally(() => {
data.nodeList[index].loading = false
})
})
websocket.onclose(() => {
data.nodeList[index].state = 0
})
websocket.onerror((error) => {
console.error('WebSocket Error:', error)
data.nodeList[index].state = 0
})
websocket.onmessage((messageData: any) => {
const eventData = JSON.parse(messageData)
if (Object.keys(eventData).length) {
data.nodeList[index].status = eventData.status
if (props.jdbcData.dbType === JDBCType.openGauss || props.jdbcData.dbType === JDBCType.PostgreSQL) {
data.nodeList[index].connNum = eventData.connNum
data.nodeList[index].lockNum = eventData.lockNum
data.nodeList[index].sessionNum = eventData.sessionNum
} else if (props.jdbcData.dbType === JDBCType.MySQL) {
data.nodeList[index].tableSpaceUsed = Number(eventData.tableSpaceUsed / 1024 / 1024).toFixed(2)
data.nodeList[index].memoryUsed = Number(eventData.memoryUsed / 1024 / 1024 / 1024).toFixed(2)
data.nodeList[index].connNum = eventData.connNum
data.nodeList[index].qps = eventData.qps
data.nodeList[index].tps = eventData.tps
data.nodeList[index].role = eventData.role
} else if (props.jdbcData.dbType === JDBCType.Milvus) {
data.nodeList[index].collectionNum = eventData.collectionNum
data.nodeList[index].apiResponseDelay = eventData.apiResponseDelay
} else {
data.nodeList[index].unassigned_shards = eventData.unassigned_shards
data.nodeList[index].active_shards_percent_as_number = Number(eventData.active_shards_percent_as_number).toFixed(2)
data.nodeList[index].clusterStatus = eventData.clusterStatus
}
}
})
}
const dbColumnConfigs = {
[JDBCType.MySQL]: [
{ prop: 'tps', label: 'TPS' },
{ prop: 'qps', label: 'QPS' },
{ prop: 'connNum', i18nKey: 'database.JdbcNodeTable.5oxhv6qcnuk0'},
{ prop: 'tableSpaceUsed', i18nKey: 'database.JdbcNodeTable.5oxhv6qco4c0'},
{ prop: 'memoryUsed', i18nKey: 'database.JdbcNodeTable.5oxhv6qcobk0'}
],
[JDBCType.openGauss]: [
{ prop: 'connNum', i18nKey: 'database.JdbcNodeTable.5oxhv6qcnuk0'},
{ prop: 'sessionNum', i18nKey: 'database.JdbcNodeTable.else7'},
{ prop: 'lockNum', i18nKey: 'database.JdbcNodeTable.else8'}
],
[JDBCType.PostgreSQL]: [
{ prop: 'connNum', i18nKey: 'database.JdbcNodeTable.5oxhv6qcnuk0'},
{ prop: 'sessionNum', i18nKey: 'database.JdbcNodeTable.else7'},
{ prop: 'lockNum', i18nKey: 'database.JdbcNodeTable.else8' }
],
[JDBCType.Milvus]: [
{ prop: 'collectionNum', i18nKey: 'database.JdbcNodeTable.collectionNum'},
{ prop: 'apiResponseDelay', i18nKey: 'database.JdbcNodeTable.apiResponseDelay', formatter: (v) => v ? `${v}ms` : '--' }
],
[JDBCType.Elasticsearch]: [
{ prop: 'unassigned_shards', i18nKey: 'database.JdbcNodeTable.unassigned_shards'},
{ prop: 'active_shards_percent_as_number', i18nKey: 'database.JdbcNodeTable.active_shards'},
{ prop: 'clusterStatus', i18nKey: 'database.JdbcNodeTable.clusterStatus'},
]
}
const dbSpecificColumns = computed(() => {
return dbColumnConfigs[props.jdbcData.dbType] || []
})
const getColumnLabel = (col) => {
if (col.label) {
return col.label
}
if (col.labelKey) {
if (col.labelKey.includes('.')) {
try {
const translated = t(col.labelKey)
return translated
} catch (error) {
return col.labelKey
}
}
return col.labelKey
}
if (col.i18nKey) {
try {
const translated = t(col.i18nKey)
return translated
} catch (error) {
return col.i18nKey
}
}
return col.prop
}
const formatColumnValue = (value, prop) => {
if (prop === 'active_shards_percent_as_number') {
if (value == null || value === '') return '--'
const num = Number(value)
return isNaN(num) ? '--' : `${num.toFixed(2)}%`
}else if (prop === 'apiResponseDelay') {
return value ? `${value}ms` : '--'
} else if (prop === 'memoryUsed') {
return value ? `${value}GB` : '--'
} else if (prop === 'tableSpaceUsed') {
return value ? `${value}MB` : '--'
} else {
return value || '--'
}
}
</script>
<style lang="less" scoped>
.jdbc-instance-table {
:deep(.el-table) {
.el-table__header .el-table__cell {
padding-top: 0px;
padding-bottom: 0px;
padding-left: 8px;
padding-right: 8px;
}
.el-table__body .el-table__cell {
padding-top: 16px;
padding-bottom: 16px;
padding-left: 8px;
padding-right: 8px;
}
.el-table__fixed .el-table__cell,
.el-table__fixed-right .el-table__cell {
padding-top: 16px;
padding-bottom: 16px;
padding-left: 8px;
padding-right: 8px;
}
}
}
.table-row {
display: flex;
align-items: center;
overflow-x: auto;
min-width: 100%;
}
.table-cell {
display: flex;
align-items: center;
&.base-info {
flex: 0 0 350px;
}
&.status {
flex: 0 0 230px;
}
&.connect-info {
flex: 0 0 200px;
}
&.table-space {
flex: 0 0 130px;
}
&.memory-used {
flex: 0 0 130px;
}
}
.node-role {
height: 40px;
padding: 10px;
display: flex;
justify-content: center;
align-items: center;
background-color: var(--color-text-4);
}
.node-state-c {
width: 15px;
height: 15px;
border-radius: 50%;
}
.un-check {
background-color: gray;
}
.check-pass {
background-color: green;
}
.check-error {
background-color: red
}
.status-container {
display: flex;
align-items: center;
justify-content: flex-start;
}
.status-dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin: 0 16px 0 0;
}
.info-dot {
background-color: var(--o-color-info-secondary);
}
.error-dot {
background-color: var(--o-color-danger);
}
.success-dot {
background-color: var(--o-color-success);
}
.flex-col-start {
display: grid;
grid-auto-flow: column;
grid-auto-columns: max-content;
gap: 64px;
min-width: 70%;
padding-bottom: 8px;
}
.flex-col {
display: flex;
flex-direction: column;
align-items: center;
}
.flex-row {
display: flex;
align-items: center;
gap: 8px;
white-space: nowrap;
}
.mb-s {
margin-bottom: 8px;
}
.mr {
margin-right: 16px;
}
.mr-s {
margin-right: 8px;
}
</style>