|
| 1 | +// database.templ renders the admin database explorer page with migration |
| 2 | +// status cards, an interactive D3.js schema diagram, and a table detail panel. |
| 3 | + |
| 4 | +package admin |
| 5 | + |
| 6 | +import ( |
| 7 | + "fmt" |
| 8 | + "github.com/keyxmakerx/chronicle/internal/templates/layouts" |
| 9 | +) |
| 10 | + |
| 11 | +// AdminDatabasePage renders the database explorer with migration status and schema visualization. |
| 12 | +templ AdminDatabasePage(statuses []PluginMigrationStatus, tableCount int, csrfToken string) { |
| 13 | + @layouts.App("Database - Admin") { |
| 14 | + <div class="max-w-7xl mx-auto space-y-6"> |
| 15 | + <div class="flex items-center justify-between"> |
| 16 | + <div> |
| 17 | + <h1 class="text-2xl font-bold text-fg">Database Explorer</h1> |
| 18 | + <p class="text-sm text-fg-secondary mt-1"> |
| 19 | + { fmt.Sprintf("%d", tableCount) } tables · Schema visualization and migration management. |
| 20 | + </p> |
| 21 | + </div> |
| 22 | + <a href="/admin" class="text-sm text-fg-muted hover:text-accent"> |
| 23 | + <i class="fa-solid fa-arrow-left mr-1"></i> Back to Dashboard |
| 24 | + </a> |
| 25 | + </div> |
| 26 | + |
| 27 | + <!-- Migration Status --> |
| 28 | + if len(statuses) > 0 { |
| 29 | + <div> |
| 30 | + <div class="flex items-center justify-between mb-3"> |
| 31 | + <h2 class="section-header">Plugin Migrations</h2> |
| 32 | + if hasPendingMigrations(statuses) { |
| 33 | + <button |
| 34 | + hx-post="/admin/database/migrations/apply" |
| 35 | + hx-confirm="Apply all pending plugin migrations? This will create new database tables." |
| 36 | + hx-headers={ fmt.Sprintf(`{"X-CSRF-Token":"%s"}`, csrfToken) } |
| 37 | + class="btn btn-sm btn-primary" |
| 38 | + > |
| 39 | + <i class="fa-solid fa-play mr-1"></i> Apply Pending Migrations |
| 40 | + </button> |
| 41 | + } |
| 42 | + </div> |
| 43 | + <div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4"> |
| 44 | + for _, s := range statuses { |
| 45 | + @migrationStatusCard(s) |
| 46 | + } |
| 47 | + </div> |
| 48 | + </div> |
| 49 | + } |
| 50 | + |
| 51 | + <!-- Schema Diagram --> |
| 52 | + <div class="card p-0 overflow-hidden"> |
| 53 | + <div class="px-5 py-3 border-b border-edge flex items-center justify-between"> |
| 54 | + <h2 class="text-sm font-semibold text-fg">Schema Diagram</h2> |
| 55 | + <span class="text-xs text-fg-muted">Scroll to zoom · Drag to pan · Click a table for details</span> |
| 56 | + </div> |
| 57 | + <div |
| 58 | + data-widget="db-explorer" |
| 59 | + data-api-url="/admin/database/schema" |
| 60 | + style="min-height: 600px;" |
| 61 | + ></div> |
| 62 | + </div> |
| 63 | + |
| 64 | + <!-- Table Detail Panel (populated by JS on node click) --> |
| 65 | + <div id="table-detail" class="card hidden"> |
| 66 | + <p class="text-sm text-fg-muted p-6">Click a table in the diagram above to see its details.</p> |
| 67 | + </div> |
| 68 | + </div> |
| 69 | + } |
| 70 | +} |
| 71 | + |
| 72 | +// migrationStatusCard renders a card for a single plugin's migration status. |
| 73 | +templ migrationStatusCard(s PluginMigrationStatus) { |
| 74 | + <div class="card p-4"> |
| 75 | + <div class="flex items-center justify-between mb-2"> |
| 76 | + <span class="text-sm font-semibold text-fg capitalize">{ s.Slug }</span> |
| 77 | + if s.Healthy { |
| 78 | + <span class="w-2.5 h-2.5 rounded-full bg-emerald-500" title="Healthy"></span> |
| 79 | + } else { |
| 80 | + <span class="w-2.5 h-2.5 rounded-full bg-red-500" title="Unhealthy"></span> |
| 81 | + } |
| 82 | + </div> |
| 83 | + <div class="text-xs text-fg-secondary"> |
| 84 | + <span>v{ fmt.Sprintf("%d", s.CurrentVersion) }</span> |
| 85 | + <span class="text-fg-muted"> / { fmt.Sprintf("%d", s.LatestVersion) }</span> |
| 86 | + </div> |
| 87 | + if s.Pending > 0 { |
| 88 | + <div class="mt-2"> |
| 89 | + <span class="inline-flex items-center px-2 py-0.5 text-xs font-medium rounded-full bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400"> |
| 90 | + { fmt.Sprintf("%d", s.Pending) } pending |
| 91 | + </span> |
| 92 | + </div> |
| 93 | + } |
| 94 | + if s.Error != "" { |
| 95 | + <p class="text-xs text-red-500 mt-2 truncate" title={ s.Error }>{ s.Error }</p> |
| 96 | + } |
| 97 | + </div> |
| 98 | +} |
| 99 | + |
| 100 | +// hasPendingMigrations returns true if any plugin has pending migrations. |
| 101 | +func hasPendingMigrations(statuses []PluginMigrationStatus) bool { |
| 102 | + for _, s := range statuses { |
| 103 | + if s.Pending > 0 { |
| 104 | + return true |
| 105 | + } |
| 106 | + } |
| 107 | + return false |
| 108 | +} |
0 commit comments