mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +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. */
|
||||
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 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 {
|
||||
try (Connection connection = dataSource.getConnection()) {
|
||||
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() {
|
||||
@ -190,7 +190,7 @@ public class SystemHealthService {
|
||||
|
||||
// ==================== 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) {}
|
||||
|
||||
|
||||
@ -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: {
|
||||
title: 'System Diagnostics',
|
||||
subtitle: 'Local instance health snapshot',
|
||||
database: 'Connected database',
|
||||
checking: 'Checking...',
|
||||
allGood: 'All systems healthy',
|
||||
hasWarnings: '{count} warning(s)',
|
||||
|
||||
@ -2054,6 +2054,7 @@ export default {
|
||||
doctor: {
|
||||
title: '系统诊断',
|
||||
subtitle: '本地实例健康概览',
|
||||
database: '当前数据库',
|
||||
checking: '检查中...',
|
||||
allGood: '所有系统正常',
|
||||
hasWarnings: '{count} 个警告',
|
||||
|
||||
@ -7,6 +7,11 @@
|
||||
<div class="mc-page-kicker">{{ t('dashboard.kicker') }}</div>
|
||||
<h1 class="mc-page-title">{{ t('dashboard.title') }}</h1>
|
||||
<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 class="hero-note mc-surface-card">
|
||||
<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 { useRouter } from 'vue-router'
|
||||
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 * as echarts from 'echarts/core'
|
||||
import { LineChart } from 'echarts/charts'
|
||||
@ -219,6 +224,9 @@ const todayStats = reactive({
|
||||
// ── Model configuration card ──
|
||||
const modelProviders = ref<any[]>([])
|
||||
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(
|
||||
() => modelProviders.value.filter((p) => providerChipStatus(p) === 'ready').length,
|
||||
@ -272,6 +280,15 @@ onMounted(async () => {
|
||||
// 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
|
||||
// blanks the analytics above, and vice versa.
|
||||
try {
|
||||
@ -415,6 +432,34 @@ function calcDuration(run: any): string {
|
||||
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 {
|
||||
min-width: 220px;
|
||||
padding: 16px 18px;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user