mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 11:58:34 +08:00
feat(dashboard): show the connected database on the dashboard
Surface the connected database product as a subtle chip in the Dashboard header. SystemHealthService now reports a database label on /system/health (reused by the front-end — no extra request), derived from a new DatabaseBootstrapRunner.getDatabaseLabel() that reads the JDBC product name once and normalizes it to a canonical label (MySQL / MariaDB / PostgreSQL / H2, and 人大金仓 for the KingbaseES family), collapsing driver version noise.
This commit is contained in:
parent
710b756281
commit
4cbd2b50f3
@ -47,6 +47,9 @@ public class DatabaseBootstrapRunner implements ApplicationRunner {
|
|||||||
/** Cached flag: true when running on PostgreSQL. */
|
/** Cached flag: true when running on PostgreSQL. */
|
||||||
private volatile Boolean isPostgres;
|
private volatile Boolean isPostgres;
|
||||||
|
|
||||||
|
/** Cached human-readable label of the connected database, e.g. "MySQL" / "H2" / "PostgreSQL". */
|
||||||
|
private volatile String databaseLabel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When true, wait for Desktop splash screen to call /setup/init with chosen language.
|
* When true, wait for Desktop splash screen to call /setup/init with chosen language.
|
||||||
* When false (default), auto-initialize immediately on startup.
|
* When false (default), auto-initialize immediately on startup.
|
||||||
@ -150,6 +153,57 @@ public class DatabaseBootstrapRunner implements ApplicationRunner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Friendly product name of the currently connected database, e.g. {@code "MySQL"},
|
||||||
|
* {@code "PostgreSQL"}, {@code "H2"} or {@code "KingbaseES"}. Read once from JDBC
|
||||||
|
* metadata and cached — the connected database never changes at runtime.
|
||||||
|
*
|
||||||
|
* @return the product name, or {@code "Unknown"} if metadata is unavailable.
|
||||||
|
*/
|
||||||
|
public String getDatabaseLabel() {
|
||||||
|
if (databaseLabel == null) {
|
||||||
|
try (Connection connection = dataSource.getConnection()) {
|
||||||
|
databaseLabel = normalizeDatabaseLabel(connection.getMetaData().getDatabaseProductName());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("Failed to read database product name: {}", e.getMessage());
|
||||||
|
databaseLabel = "Unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return databaseLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps a raw JDBC product name to a clean, canonical label. Some drivers append
|
||||||
|
* version noise to the product name (e.g. KingbaseES reports "KingbaseES V008R006");
|
||||||
|
* collapsing on a keyword keeps the displayed label stable across driver versions
|
||||||
|
* and consistent with the dialect this runner detects for DDL.
|
||||||
|
*
|
||||||
|
* @return a canonical label, or {@code "Unknown"} when the product name is absent.
|
||||||
|
*/
|
||||||
|
static String normalizeDatabaseLabel(String product) {
|
||||||
|
if (product == null || product.isBlank()) {
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
String lower = product.toLowerCase();
|
||||||
|
if (lower.contains("kingbase")) {
|
||||||
|
// KingbaseES is the product name; show the vendor's Chinese brand name.
|
||||||
|
return "人大金仓";
|
||||||
|
}
|
||||||
|
if (lower.contains("mariadb")) {
|
||||||
|
return "MariaDB";
|
||||||
|
}
|
||||||
|
if (lower.contains("mysql")) {
|
||||||
|
return "MySQL";
|
||||||
|
}
|
||||||
|
if (lower.contains("postgresql")) {
|
||||||
|
return "PostgreSQL";
|
||||||
|
}
|
||||||
|
if (lower.contains("h2")) {
|
||||||
|
return "H2";
|
||||||
|
}
|
||||||
|
return product.trim();
|
||||||
|
}
|
||||||
|
|
||||||
private boolean tableExists(String tableName) throws Exception {
|
private boolean tableExists(String tableName) throws Exception {
|
||||||
try (Connection connection = dataSource.getConnection()) {
|
try (Connection connection = dataSource.getConnection()) {
|
||||||
DatabaseMetaData metaData = connection.getMetaData();
|
DatabaseMetaData metaData = connection.getMetaData();
|
||||||
|
|||||||
@ -67,7 +67,7 @@ public class SystemHealthService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new HealthResponse(overall, checks);
|
return new HealthResponse(overall, checks, bootstrapRunner.getDatabaseLabel());
|
||||||
}
|
}
|
||||||
|
|
||||||
private HealthCheck checkDefaultModel() {
|
private HealthCheck checkDefaultModel() {
|
||||||
@ -190,7 +190,7 @@ public class SystemHealthService {
|
|||||||
|
|
||||||
// ==================== Response Records ====================
|
// ==================== Response Records ====================
|
||||||
|
|
||||||
public record HealthResponse(String overall, List<HealthCheck> checks) {}
|
public record HealthResponse(String overall, List<HealthCheck> checks, String database) {}
|
||||||
|
|
||||||
public record HealthCheck(String name, String status, String message, HealthAction action) {}
|
public record HealthCheck(String name, String status, String message, HealthAction action) {}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,44 @@
|
|||||||
|
package vip.mate.config;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit tests for {@link DatabaseBootstrapRunner#normalizeDatabaseLabel(String)}.
|
||||||
|
* Verifies that raw JDBC product names — including version-suffixed ones from the
|
||||||
|
* KingbaseES / PostgreSQL family — collapse to a clean, canonical label.
|
||||||
|
*/
|
||||||
|
class DatabaseBootstrapRunnerLabelTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("KingbaseES product name (with version noise) → '人大金仓'")
|
||||||
|
void kingbaseNormalizes() {
|
||||||
|
assertEquals("人大金仓", DatabaseBootstrapRunner.normalizeDatabaseLabel("KingbaseES"));
|
||||||
|
assertEquals("人大金仓", DatabaseBootstrapRunner.normalizeDatabaseLabel("KingbaseES V008R006"));
|
||||||
|
assertEquals("人大金仓", DatabaseBootstrapRunner.normalizeDatabaseLabel("kingbasees"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("MySQL / MariaDB → canonical labels")
|
||||||
|
void mysqlFamilyNormalizes() {
|
||||||
|
assertEquals("MySQL", DatabaseBootstrapRunner.normalizeDatabaseLabel("MySQL"));
|
||||||
|
assertEquals("MariaDB", DatabaseBootstrapRunner.normalizeDatabaseLabel("MariaDB"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("PostgreSQL and H2 → canonical labels")
|
||||||
|
void postgresAndH2Normalize() {
|
||||||
|
assertEquals("PostgreSQL", DatabaseBootstrapRunner.normalizeDatabaseLabel("PostgreSQL"));
|
||||||
|
assertEquals("H2", DatabaseBootstrapRunner.normalizeDatabaseLabel("H2"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Unknown / blank product name → 'Unknown'; unrecognized name passes through trimmed")
|
||||||
|
void fallbacks() {
|
||||||
|
assertEquals("Unknown", DatabaseBootstrapRunner.normalizeDatabaseLabel(null));
|
||||||
|
assertEquals("Unknown", DatabaseBootstrapRunner.normalizeDatabaseLabel(" "));
|
||||||
|
assertEquals("Oracle", DatabaseBootstrapRunner.normalizeDatabaseLabel(" Oracle "));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -573,6 +573,7 @@ export default {
|
|||||||
doctor: {
|
doctor: {
|
||||||
title: 'System Diagnostics',
|
title: 'System Diagnostics',
|
||||||
subtitle: 'Local instance health snapshot',
|
subtitle: 'Local instance health snapshot',
|
||||||
|
database: 'Connected database',
|
||||||
checking: 'Checking...',
|
checking: 'Checking...',
|
||||||
allGood: 'All systems healthy',
|
allGood: 'All systems healthy',
|
||||||
hasWarnings: '{count} warning(s)',
|
hasWarnings: '{count} warning(s)',
|
||||||
|
|||||||
@ -2054,6 +2054,7 @@ export default {
|
|||||||
doctor: {
|
doctor: {
|
||||||
title: '系统诊断',
|
title: '系统诊断',
|
||||||
subtitle: '本地实例健康概览',
|
subtitle: '本地实例健康概览',
|
||||||
|
database: '当前数据库',
|
||||||
checking: '检查中...',
|
checking: '检查中...',
|
||||||
allGood: '所有系统正常',
|
allGood: '所有系统正常',
|
||||||
hasWarnings: '{count} 个警告',
|
hasWarnings: '{count} 个警告',
|
||||||
|
|||||||
@ -7,6 +7,11 @@
|
|||||||
<div class="mc-page-kicker">{{ t('dashboard.kicker') }}</div>
|
<div class="mc-page-kicker">{{ t('dashboard.kicker') }}</div>
|
||||||
<h1 class="mc-page-title">{{ t('dashboard.title') }}</h1>
|
<h1 class="mc-page-title">{{ t('dashboard.title') }}</h1>
|
||||||
<p class="mc-page-desc">{{ t('dashboard.desc') }}</p>
|
<p class="mc-page-desc">{{ t('dashboard.desc') }}</p>
|
||||||
|
<div v-if="dbLabel" class="db-chip" :title="t('doctor.database')">
|
||||||
|
<svg class="db-chip__icon" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5v14a9 3 0 0 0 18 0V5"/><path d="M3 12a9 3 0 0 0 18 0"/></svg>
|
||||||
|
<span class="db-chip__label">{{ t('doctor.database') }}</span>
|
||||||
|
<span class="db-chip__value">{{ dbLabel }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="hero-note mc-surface-card">
|
<div class="hero-note mc-surface-card">
|
||||||
<div class="hero-note__label">{{ t('dashboard.periods.today') }}</div>
|
<div class="hero-note__label">{{ t('dashboard.periods.today') }}</div>
|
||||||
@ -190,7 +195,7 @@ import { ref, reactive, computed, onMounted, onUnmounted, nextTick, watch } from
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { ArrowRight, ChatDotRound, DataLine, Document, Tools } from '@element-plus/icons-vue'
|
import { ArrowRight, ChatDotRound, DataLine, Document, Tools } from '@element-plus/icons-vue'
|
||||||
import { dashboardApi, modelApi } from '@/api'
|
import { dashboardApi, modelApi, http } from '@/api'
|
||||||
import { getProviderIcon, onProviderIconError } from '@/utils/providerIcons'
|
import { getProviderIcon, onProviderIconError } from '@/utils/providerIcons'
|
||||||
import * as echarts from 'echarts/core'
|
import * as echarts from 'echarts/core'
|
||||||
import { LineChart } from 'echarts/charts'
|
import { LineChart } from 'echarts/charts'
|
||||||
@ -219,6 +224,9 @@ const todayStats = reactive({
|
|||||||
// ── Model configuration card ──
|
// ── Model configuration card ──
|
||||||
const modelProviders = ref<any[]>([])
|
const modelProviders = ref<any[]>([])
|
||||||
const activeModel = ref<{ providerId: string; model: string } | null>(null)
|
const activeModel = ref<{ providerId: string; model: string } | null>(null)
|
||||||
|
// Connected database product name (e.g. "MySQL" / "H2" / "PostgreSQL"), surfaced
|
||||||
|
// as a subtle line in the page header. Empty string hides it when unavailable.
|
||||||
|
const dbLabel = ref('')
|
||||||
|
|
||||||
const readyProviderCount = computed(
|
const readyProviderCount = computed(
|
||||||
() => modelProviders.value.filter((p) => providerChipStatus(p) === 'ready').length,
|
() => modelProviders.value.filter((p) => providerChipStatus(p) === 'ready').length,
|
||||||
@ -272,6 +280,15 @@ onMounted(async () => {
|
|||||||
// Dashboard data is non-critical
|
// Dashboard data is non-critical
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Connected database label — independent and non-critical. Reuses the
|
||||||
|
// existing system health endpoint, which already reports the product name.
|
||||||
|
try {
|
||||||
|
const healthRes: any = await http.get('/system/health')
|
||||||
|
dbLabel.value = (healthRes?.data || healthRes)?.database || ''
|
||||||
|
} catch {
|
||||||
|
dbLabel.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
// Model configuration card — loaded independently so a failure here never
|
// Model configuration card — loaded independently so a failure here never
|
||||||
// blanks the analytics above, and vice versa.
|
// blanks the analytics above, and vice versa.
|
||||||
try {
|
try {
|
||||||
@ -415,6 +432,34 @@ function calcDuration(run: any): string {
|
|||||||
padding-right: 4px;
|
padding-right: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.db-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 14px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border: 1px solid var(--mc-border);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--mc-bg-sunken);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--mc-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.db-chip__icon {
|
||||||
|
color: var(--mc-text-tertiary);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.db-chip__label {
|
||||||
|
color: var(--mc-text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.db-chip__value {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--mc-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
.hero-note {
|
.hero-note {
|
||||||
min-width: 220px;
|
min-width: 220px;
|
||||||
padding: 16px 18px;
|
padding: 16px 18px;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user