Files
metabuilder/frontends/qt6/DatabaseManager.qml
T
git e0893c2fe3 feat(qt6): DBAL live data integration + fix deployment Python 3.9 compat
Wire up DBALProvider across all Qt6 views for live CRUD operations,
add PackageLoader, MediaServicePanel, NotificationsPanel, SettingsView,
and JSON-driven CMake config. Fix deployment helpers.py str|None syntax
for Python 3.9 compatibility via __future__ annotations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 01:43:26 +00:00

543 lines
26 KiB
QML

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QmlComponents 1.0
import "qmllib/dbal"
Rectangle {
id: root
color: Theme.background
// ── DBAL connection ──────────────────────────────────────────
DBALProvider { id: dbal }
property bool useLiveData: dbal.connected
property var mockBackends: JSON.parse(JSON.stringify(backends))
function loadAdapterStatus() {
dbal.execute("core/adapters", {}, function(result, error) {
if (!error && result && result.adapters && result.adapters.length > 0) {
var liveBackends = []
for (var i = 0; i < result.adapters.length; i++) {
var a = result.adapters[i]
liveBackends.push({
name: a.name || "",
key: a.key || "",
status: a.status || "disconnected",
description: a.description || "",
connectionString: a.connectionString || "",
records: a.records || 0,
sizeKb: a.sizeKb || 0,
lastBackup: a.lastBackup || "Never"
})
}
backends = liveBackends
if (selectedBackendIndex >= liveBackends.length)
selectedBackendIndex = 0
if (activeBackendIndex >= liveBackends.length)
activeBackendIndex = 0
}
// On error or empty result, keep existing mock backends as fallback
})
}
function testConnectionLive(index) {
var backend = backends[index]
testingIndex = index
if (useLiveData) {
dbal.execute("core/test-connection", { adapter: backend.key }, function(result, error) {
var newResults = Object.assign({}, testResults)
if (!error && result && result.success) {
newResults[index] = "success"
} else {
newResults[index] = "error"
}
testResults = newResults
testingIndex = -1
})
} else {
// Fall back to mock timer
testTimer.targetIndex = index
testTimer.start()
}
}
function checkHealth() {
if (useLiveData) {
dbal.ping()
}
}
onUseLiveDataChanged: {
if (useLiveData) loadAdapterStatus()
}
Component.onCompleted: {
loadAdapterStatus()
}
// ── State ──────────────────────────────────────────────────────────
property int selectedBackendIndex: 2
property int activeBackendIndex: 2
property string databaseUrl: "sqlite:///var/lib/dbal/metabuilder.db"
property string cacheUrl: "redis://localhost:6379/0?ttl=300&pattern=read-through"
property string searchUrl: "http://localhost:9200?index=dbal_search&refresh=true"
property int adapterPattern: 0
property bool showExportDialog: false
property bool showImportDialog: false
property var adapterPatterns: ["read-through", "write-through", "cache-aside", "dual-write"]
property var backends: [
{ name: "Memory", key: "memory", status: "connected", description: "In-memory store for testing and development", connectionString: ":memory:", records: 142, sizeKb: 56, lastBackup: "N/A (volatile)" },
{ name: "SQLite", key: "sqlite", status: "connected", description: "Embedded database, generic CRUD via Inja SQL templates", connectionString: "sqlite:///var/lib/dbal/metabuilder.db", records: 8741, sizeKb: 3200, lastBackup: "2026-03-18 02:00" },
{ name: "PostgreSQL", key: "postgres", status: "connected", description: "Direct connection, no ORM. Primary production backend", connectionString: "postgres://dbal:secret@db:5432/metabuilder", records: 54320, sizeKb: 98400, lastBackup: "2026-03-18 04:30" },
{ name: "MySQL", key: "mysql", status: "disconnected", description: "Direct connection, no ORM", connectionString: "mysql://dbal:secret@mysql:3306/metabuilder", records: 0, sizeKb: 0, lastBackup: "Never" },
{ name: "MariaDB", key: "mariadb", status: "disconnected", description: "Reuses MySQL adapter with MariaDB-specific extensions", connectionString: "mariadb://dbal:secret@mariadb:3306/metabuilder", records: 0, sizeKb: 0, lastBackup: "Never" },
{ name: "CockroachDB", key: "cockroachdb", status: "disconnected", description: "Distributed SQL, reuses PostgreSQL adapter", connectionString: "cockroachdb://root@crdb:26257/metabuilder", records: 0, sizeKb: 0, lastBackup: "Never" },
{ name: "MongoDB", key: "mongodb", status: "error", description: "Document store with JSON to BSON bridging", connectionString: "mongodb://mongo:27017/metabuilder", records: 0, sizeKb: 0, lastBackup: "Never" },
{ name: "Redis", key: "redis", status: "connected", description: "Cache layer (L1/L2 with primary DB)", connectionString: "redis://localhost:6379/0?ttl=300", records: 1205, sizeKb: 820, lastBackup: "N/A (cache)" },
{ name: "Elasticsearch", key: "elasticsearch", status: "connected", description: "Full-text search and analytics layer", connectionString: "http://localhost:9200?index=dbal_search", records: 48210, sizeKb: 62100, lastBackup: "2026-03-18 03:15" },
{ name: "Cassandra", key: "cassandra", status: "disconnected", description: "Wide-column store for high-throughput writes", connectionString: "cassandra://cassandra:9042/metabuilder", records: 0, sizeKb: 0, lastBackup: "Never" },
{ name: "SurrealDB", key: "surrealdb", status: "disconnected", description: "Multi-model database: documents, graphs, and KV", connectionString: "ws://surrealdb:8000/rpc", records: 0, sizeKb: 0, lastBackup: "Never" },
{ name: "Supabase", key: "supabase", status: "disconnected", description: "PostgreSQL + REST + Realtime + Row Level Security", connectionString: "https://project.supabase.co?apikey=...", records: 0, sizeKb: 0, lastBackup: "Never" },
{ name: "Prisma", key: "prisma", status: "disconnected", description: "ORM with HTTP bridge for schema-driven access", connectionString: "prisma://localhost:4466/metabuilder", records: 0, sizeKb: 0, lastBackup: "Never" },
{ name: "Generic", key: "generic", status: "disconnected", description: "Custom adapter via DATABASE_URL with driver query params", connectionString: "generic://localhost:9999/custom?driver=mydriver", records: 0, sizeKb: 0, lastBackup: "Never" }
]
// ── Mock testing state ─────────────────────────────────────────────
property var testingIndex: -1
property var testResults: ({})
function testConnection(index) {
testingIndex = index
testResults = Object.assign({}, testResults)
delete testResults[index]
testTimer.targetIndex = index
testTimer.start()
}
Timer {
id: testTimer
property int targetIndex: -1
interval: 1500
onTriggered: {
var backend = backends[targetIndex]
var result = (backend.status === "connected") ? "success" : (backend.status === "error" ? "error" : "warning")
var newResults = Object.assign({}, testResults)
newResults[targetIndex] = result
testResults = newResults
testingIndex = -1
}
}
function formatSize(kb) {
if (kb < 1024) return kb + " KB"
return (kb / 1024).toFixed(1) + " MB"
}
function totalRecords() {
var sum = 0
for (var i = 0; i < backends.length; i++) sum += backends[i].records
return sum
}
function totalSize() {
var sum = 0
for (var i = 0; i < backends.length; i++) sum += backends[i].sizeKb
return sum
}
function connectedCount() {
var count = 0
for (var i = 0; i < backends.length; i++)
if (backends[i].status === "connected") count++
return count
}
// ── Export Dialog ──────────────────────────────────────────────────
Dialog {
id: exportDialog
visible: showExportDialog
title: "Export Database"
modal: true
anchors.centerIn: parent
width: 420
standardButtons: Dialog.Ok | Dialog.Cancel
onAccepted: showExportDialog = false
onRejected: showExportDialog = false
ColumnLayout {
spacing: 12
width: parent.width
CText { variant: "body1"; text: "Export the active database (" + backends[activeBackendIndex].name + ") to a JSON dump file." }
CTextField { label: "Output path"; text: "/tmp/dbal-export-" + backends[activeBackendIndex].key + ".json"; Layout.fillWidth: true }
CAlert { severity: "success"; text: "Export includes all tenants and entity data (" + backends[activeBackendIndex].records + " records)." }
}
}
// ── Import Dialog ──────────────────────────────────────────────────
Dialog {
id: importDialog
visible: showImportDialog
title: "Import Database"
modal: true
anchors.centerIn: parent
width: 420
standardButtons: Dialog.Ok | Dialog.Cancel
onAccepted: showImportDialog = false
onRejected: showImportDialog = false
ColumnLayout {
spacing: 12
width: parent.width
CText { variant: "body1"; text: "Import a JSON dump into the active backend (" + backends[activeBackendIndex].name + ")." }
CTextField { label: "Import file"; placeholderText: "/path/to/dbal-export.json"; Layout.fillWidth: true }
CAlert { severity: "warning"; text: "Existing records with matching IDs will be overwritten." }
}
}
// ── Main Layout ────────────────────────────────────────────────────
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
spacing: 16
// ── Header ────────────────────────────────────────────────────
FlexRow {
Layout.fillWidth: true
spacing: 12
CText { variant: "h3"; text: "Database Manager" }
CStatusBadge { status: "success"; text: connectedCount() + " / " + backends.length + " connected" }
CBadge {
text: useLiveData ? "Connected to DBAL" : "Mock Data"
color: useLiveData ? Theme.success : Theme.warning
}
Item { Layout.fillWidth: true }
CButton { text: "Export"; variant: "ghost"; onClicked: showExportDialog = true }
CButton { text: "Import"; variant: "ghost"; onClicked: showImportDialog = true }
}
// ── Stats Row ─────────────────────────────────────────────────
FlexRow {
Layout.fillWidth: true
spacing: 12
CCard {
Layout.fillWidth: true
implicitHeight: 72
ColumnLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 4
CText { variant: "caption"; text: "Total Records" }
CText { variant: "h4"; text: totalRecords().toLocaleString() }
}
}
CCard {
Layout.fillWidth: true
implicitHeight: 72
ColumnLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 4
CText { variant: "caption"; text: "Total Size" }
CText { variant: "h4"; text: formatSize(totalSize()) }
}
}
CCard {
Layout.fillWidth: true
implicitHeight: 72
ColumnLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 4
CText { variant: "caption"; text: "Active Backend" }
CText { variant: "h4"; text: backends[activeBackendIndex].name }
}
}
CCard {
Layout.fillWidth: true
implicitHeight: 72
ColumnLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 4
CText { variant: "caption"; text: "Adapter Pattern" }
CText { variant: "h4"; text: adapterPatterns[adapterPattern] }
}
}
}
// ── Body: Sidebar + Detail ────────────────────────────────────
RowLayout {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 16
// ── Backend List (sidebar) ────────────────────────────────
CCard {
Layout.preferredWidth: 300
Layout.fillHeight: true
ColumnLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 8
CText { variant: "subtitle1"; text: "Backends (14)" }
CDivider { Layout.fillWidth: true }
ListView {
Layout.fillWidth: true
Layout.fillHeight: true
model: backends
spacing: 4
clip: true
delegate: CListItem {
width: parent ? parent.width : 268
title: modelData.name
subtitle: modelData.key
selected: index === selectedBackendIndex
leadingIcon: modelData.status === "connected" ? "check_circle" : (modelData.status === "error" ? "error" : "radio_button_unchecked")
onClicked: selectedBackendIndex = index
CStatusBadge {
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
status: modelData.status === "connected" ? "success" : (modelData.status === "error" ? "error" : "warning")
text: modelData.status
}
}
}
}
}
// ── Detail Panel ──────────────────────────────────────────
CCard {
Layout.fillWidth: true
Layout.fillHeight: true
Flickable {
anchors.fill: parent
anchors.margins: 16
contentHeight: detailColumn.implicitHeight
clip: true
ColumnLayout {
id: detailColumn
width: parent.width
spacing: 16
// ── Backend Header ────────────────────────────
FlexRow {
Layout.fillWidth: true
spacing: 12
CText { variant: "h4"; text: backends[selectedBackendIndex].name }
CStatusBadge {
status: backends[selectedBackendIndex].status === "connected" ? "success" : (backends[selectedBackendIndex].status === "error" ? "error" : "warning")
text: backends[selectedBackendIndex].status
}
Item { Layout.fillWidth: true }
CBadge {
text: selectedBackendIndex === activeBackendIndex ? "ACTIVE" : "INACTIVE"
accent: selectedBackendIndex === activeBackendIndex
}
}
CText {
variant: "body1"
text: backends[selectedBackendIndex].description
wrapMode: Text.Wrap
Layout.fillWidth: true
}
CDivider { Layout.fillWidth: true }
// ── Connection ────────────────────────────────
CText { variant: "subtitle1"; text: "Connection" }
CTextField {
label: "Connection String"
text: backends[selectedBackendIndex].connectionString
Layout.fillWidth: true
}
FlexRow {
Layout.fillWidth: true
spacing: 8
CButton {
text: testingIndex === selectedBackendIndex ? "Testing..." : "Test Connection"
variant: "primary"
enabled: testingIndex === -1
onClicked: testConnectionLive(selectedBackendIndex)
}
CButton {
text: selectedBackendIndex === activeBackendIndex ? "Active Backend" : "Set as Active"
variant: selectedBackendIndex === activeBackendIndex ? "ghost" : "primary"
enabled: selectedBackendIndex !== activeBackendIndex && backends[selectedBackendIndex].status === "connected"
onClicked: activeBackendIndex = selectedBackendIndex
}
Item { Layout.fillWidth: true }
Loader {
active: testResults[selectedBackendIndex] !== undefined
sourceComponent: CStatusBadge {
status: testResults[selectedBackendIndex] === "success" ? "success" : "error"
text: testResults[selectedBackendIndex] === "success" ? "Connection OK" : "Connection Failed"
}
}
}
CDivider { Layout.fillWidth: true }
// ── Storage Stats ─────────────────────────────
CText { variant: "subtitle1"; text: "Storage Statistics" }
FlexRow {
Layout.fillWidth: true
spacing: 12
CPaper {
Layout.fillWidth: true
implicitHeight: 60
ColumnLayout {
anchors.fill: parent
anchors.margins: 10
spacing: 2
CText { variant: "caption"; text: "Records" }
CText { variant: "h4"; text: backends[selectedBackendIndex].records.toLocaleString() }
}
}
CPaper {
Layout.fillWidth: true
implicitHeight: 60
ColumnLayout {
anchors.fill: parent
anchors.margins: 10
spacing: 2
CText { variant: "caption"; text: "Size" }
CText { variant: "h4"; text: formatSize(backends[selectedBackendIndex].sizeKb) }
}
}
CPaper {
Layout.fillWidth: true
implicitHeight: 60
ColumnLayout {
anchors.fill: parent
anchors.margins: 10
spacing: 2
CText { variant: "caption"; text: "Last Backup" }
CText { variant: "body2"; text: backends[selectedBackendIndex].lastBackup }
}
}
}
CDivider { Layout.fillWidth: true }
// ── Configuration ─────────────────────────────
CText { variant: "subtitle1"; text: "Environment Configuration" }
CTextField {
label: "DATABASE_URL"
text: databaseUrl
onTextChanged: databaseUrl = text
placeholderText: "sqlite:///path/to/db or postgres://user:pass@host/db"
Layout.fillWidth: true
}
CTextField {
label: "DBAL_CACHE_URL (Redis)"
text: cacheUrl
onTextChanged: cacheUrl = text
placeholderText: "redis://localhost:6379/0?ttl=300&pattern=read-through"
Layout.fillWidth: true
}
CTextField {
label: "DBAL_SEARCH_URL (Elasticsearch)"
text: searchUrl
onTextChanged: searchUrl = text
placeholderText: "http://localhost:9200?index=dbal_search&refresh=true"
Layout.fillWidth: true
}
CDivider { Layout.fillWidth: true }
// ── Multi-Adapter Pattern ─────────────────────
CText { variant: "subtitle1"; text: "Multi-Adapter Pattern" }
CText { variant: "body2"; text: "Select how the primary, cache, and search adapters coordinate data flow." }
FlexRow {
Layout.fillWidth: true
spacing: 8
Repeater {
model: adapterPatterns
delegate: CButton {
text: modelData
variant: index === adapterPattern ? "primary" : "ghost"
onClicked: adapterPattern = index
}
}
}
CPaper {
Layout.fillWidth: true
implicitHeight: patternDesc.implicitHeight + 24
CText {
id: patternDesc
anchors.fill: parent
anchors.margins: 12
variant: "body2"
wrapMode: Text.Wrap
text: {
var descriptions = [
"Read-through: Reads check cache first. On miss, the cache fetches from the primary DB, stores the result, and returns it. Best for read-heavy workloads.",
"Write-through: Every write goes to both cache and primary DB synchronously. Guarantees consistency at the cost of write latency.",
"Cache-aside: Application manages cache explicitly. Reads check cache, fetch from DB on miss and populate cache. Writes go directly to DB and invalidate cache.",
"Dual-write: Writes are sent to two backends simultaneously (e.g., primary DB + search index). Requires conflict resolution strategy."
]
return descriptions[adapterPattern]
}
}
}
// ── Spacer at bottom ──────────────────────────
Item { height: 16 }
}
}
}
}
}
}