mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-05-04 10:44:52 +00:00
e0893c2fe3
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>
543 lines
26 KiB
QML
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 }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|