Skip to content

Commit ec83ffe

Browse files
authored
Merge pull request #7 from sarg3nt/fix/default-dashboard-empty-widget
Fix default dashboard Widget Error and auto-redirect to editor
2 parents 3eb9d45 + 8e363dd commit ec83ffe

10 files changed

Lines changed: 175 additions & 79 deletions

File tree

gearbox/cmd/server/main.go

Lines changed: 42 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -623,8 +623,8 @@ func main() {
623623

624624
// Page routes (non-plugin)
625625
r.Get("/os-updates", h.OSUpdatesPage)
626-
r.Get("/box/{serverID}/frontend/{name}", h.FrontendDetailPage)
627-
r.Get("/box/{serverID}/backend/{name}", h.BackendDetailPage)
626+
r.Get("/box/{boxID}/frontend/{name}", h.FrontendDetailPage)
627+
r.Get("/box/{boxID}/backend/{name}", h.BackendDetailPage)
628628

629629
// Config editor pages
630630
r.Get("/config/haproxy/{boxID}", h.HAProxyConfigPage)
@@ -646,36 +646,36 @@ func main() {
646646
r.Get("/user/permissions", h.APIGetCurrentUserPermissions)
647647
r.Get("/user/pending-count", h.APIPendingUsersCount)
648648
r.Get("/websocket/status", h.APIWebSocketStatusHandler) // WebSocket connection status
649-
r.Get("/{serverID}/stats", h.APIStatsHandler)
650-
r.Get("/{serverID}/metadata", h.APIMetadataHandler)
651-
r.Get("/{serverID}/metrics", h.APISystemMetricsHandler)
652-
r.Get("/{serverID}/logs/{logName}", h.APILogsHandler)
653-
r.Get("/{serverID}/log-sources", h.APILogSourcesHandler) // Get enabled log sources
649+
r.Get("/{boxID}/stats", h.APIStatsHandler)
650+
r.Get("/{boxID}/metadata", h.APIMetadataHandler)
651+
r.Get("/{boxID}/metrics", h.APISystemMetricsHandler)
652+
r.Get("/{boxID}/logs/{logName}", h.APILogsHandler)
653+
r.Get("/{boxID}/log-sources", h.APILogSourcesHandler) // Get enabled log sources
654654
// History API endpoints
655-
r.Get("/{serverID}/history/stats", h.APIStatsHistoryHandler)
656-
r.Get("/{serverID}/history/metrics", h.APISystemMetricsHistoryHandler)
657-
r.Get("/{serverID}/history/backend/{backendName}", h.APIBackendHistoryHandler)
658-
r.Get("/{serverID}/incidents", h.APIIncidentsHandler)
655+
r.Get("/{boxID}/history/stats", h.APIStatsHistoryHandler)
656+
r.Get("/{boxID}/history/metrics", h.APISystemMetricsHistoryHandler)
657+
r.Get("/{boxID}/history/backend/{backendName}", h.APIBackendHistoryHandler)
658+
r.Get("/{boxID}/incidents", h.APIIncidentsHandler)
659659

660660
// Disabled entities management
661-
r.Get("/{serverID}/disabled-entities", h.APIDisabledEntitiesHandler)
662-
r.Post("/{serverID}/disable-entity", h.APIDisableEntityHandler)
663-
r.Post("/{serverID}/enable-entity", h.APIEnableEntityHandler)
661+
r.Get("/{boxID}/disabled-entities", h.APIDisabledEntitiesHandler)
662+
r.Post("/{boxID}/disable-entity", h.APIDisableEntityHandler)
663+
r.Post("/{boxID}/enable-entity", h.APIEnableEntityHandler)
664664

665665
// Certificate management
666-
r.Get("/{serverID}/certificates", h.APICertificatesHandler)
667-
r.Post("/{serverID}/certificates/{domain}/refresh", h.APICertificateRefreshHandler)
668-
r.Get("/{serverID}/certificates/{domain}/download", h.APICertificateDownloadHandler)
666+
r.Get("/{boxID}/certificates", h.APICertificatesHandler)
667+
r.Post("/{boxID}/certificates/{domain}/refresh", h.APICertificateRefreshHandler)
668+
r.Get("/{boxID}/certificates/{domain}/download", h.APICertificateDownloadHandler)
669669

670670
// Services management
671-
r.Get("/{serverID}/services-config", h.APIServicesConfigHandler)
672-
r.Get("/{serverID}/services", h.APIServicesHandler)
673-
r.Post("/{serverID}/service-control", h.APIServiceControlHandler)
671+
r.Get("/{boxID}/services-config", h.APIServicesConfigHandler)
672+
r.Get("/{boxID}/services", h.APIServicesHandler)
673+
r.Post("/{boxID}/service-control", h.APIServiceControlHandler)
674674

675675
// Traffic analysis API
676-
r.Get("/{serverID}/traffic", h.APITrafficAnalysisHandler)
677-
r.Get("/{serverID}/traffic/sources", h.APITrafficSourcesHandler)
678-
r.Get("/{serverID}/traffic/network", h.APITrafficNetworkHandler)
676+
r.Get("/{boxID}/traffic", h.APITrafficAnalysisHandler)
677+
r.Get("/{boxID}/traffic/sources", h.APITrafficSourcesHandler)
678+
r.Get("/{boxID}/traffic/network", h.APITrafficNetworkHandler)
679679

680680
// Integrations API
681681
r.Get("/plugins", h.APIPluginsHandler)
@@ -689,23 +689,23 @@ func main() {
689689
r.Get("/backup/download/{path}", h.APIDownloadBackup)
690690

691691
// Metrics storage management
692-
r.Get("/{serverID}/metrics/storage-stats", h.APIMetricsStorageStatsHandler)
693-
r.Post("/{serverID}/metrics/clear", h.APIClearMetricsDataHandler)
692+
r.Get("/{boxID}/metrics/storage-stats", h.APIMetricsStorageStatsHandler)
693+
r.Post("/{boxID}/metrics/clear", h.APIClearMetricsDataHandler)
694694

695695
// HAProxy config management
696-
r.Get("/{serverID}/haproxy/config", h.APIHAProxyConfigGet)
697-
r.Post("/{serverID}/haproxy/config", h.APIHAProxyConfigSave)
698-
r.Post("/{serverID}/haproxy/config/validate", h.APIHAProxyConfigValidate)
699-
r.Get("/{serverID}/haproxy/config/backups", h.APIHAProxyConfigBackups)
700-
r.Post("/{serverID}/haproxy/config/restore", h.APIHAProxyConfigRestore)
701-
r.Get("/{serverID}/haproxy/config/history", h.APIHAProxyConfigHistory)
696+
r.Get("/{boxID}/haproxy/config", h.APIHAProxyConfigGet)
697+
r.Post("/{boxID}/haproxy/config", h.APIHAProxyConfigSave)
698+
r.Post("/{boxID}/haproxy/config/validate", h.APIHAProxyConfigValidate)
699+
r.Get("/{boxID}/haproxy/config/backups", h.APIHAProxyConfigBackups)
700+
r.Post("/{boxID}/haproxy/config/restore", h.APIHAProxyConfigRestore)
701+
r.Get("/{boxID}/haproxy/config/history", h.APIHAProxyConfigHistory)
702702

703703
// Firewall config management
704-
r.Get("/{serverID}/firewall/config", h.APIFirewallConfigGet)
705-
r.Post("/{serverID}/firewall/config", h.APIFirewallConfigSave)
706-
r.Post("/{serverID}/firewall/config/validate", h.APIFirewallConfigValidate)
707-
r.Get("/{serverID}/firewall/config/backups", h.APIFirewallConfigBackups)
708-
r.Post("/{serverID}/firewall/config/restore", h.APIFirewallConfigRestore)
704+
r.Get("/{boxID}/firewall/config", h.APIFirewallConfigGet)
705+
r.Post("/{boxID}/firewall/config", h.APIFirewallConfigSave)
706+
r.Post("/{boxID}/firewall/config/validate", h.APIFirewallConfigValidate)
707+
r.Get("/{boxID}/firewall/config/backups", h.APIFirewallConfigBackups)
708+
r.Post("/{boxID}/firewall/config/restore", h.APIFirewallConfigRestore)
709709

710710
// User permissions API
711711
r.Route("/users", func(r chi.Router) {
@@ -716,12 +716,12 @@ func main() {
716716

717717
// Alerts API
718718
r.Get("/alerts/count", h.APIGlobalAlertCountHandler) // Global active alert count
719-
r.Get("/{serverID}/alerts", h.APIAlertsHandler)
720-
r.Get("/{serverID}/alerts/summary", h.APIAlertSummaryHandler)
721-
r.Get("/{serverID}/alerts/rules", h.APIAlertRulesHandler)
722-
r.Post("/{serverID}/alerts/rules", h.APICreateAlertRuleHandler)
723-
r.Get("/{serverID}/alerts/retention", h.APIAlertRetentionConfigHandler)
724-
r.Post("/{serverID}/alerts/retention", h.APIUpdateAlertRetentionConfigHandler)
719+
r.Get("/{boxID}/alerts", h.APIAlertsHandler)
720+
r.Get("/{boxID}/alerts/summary", h.APIAlertSummaryHandler)
721+
r.Get("/{boxID}/alerts/rules", h.APIAlertRulesHandler)
722+
r.Post("/{boxID}/alerts/rules", h.APICreateAlertRuleHandler)
723+
r.Get("/{boxID}/alerts/retention", h.APIAlertRetentionConfigHandler)
724+
r.Post("/{boxID}/alerts/retention", h.APIUpdateAlertRetentionConfigHandler)
725725
r.Put("/alerts/rules/{ruleID}", h.APIUpdateAlertRuleHandler)
726726
r.Delete("/alerts/rules/{ruleID}", h.APIDeleteAlertRuleHandler)
727727
r.Post("/alerts/{alertID}/acknowledge", h.APIAcknowledgeAlertWithNoteHandler)

gearbox/internal/framework/dashboard/storage.go

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ func (s *Storage) CreateDefaultDashboard() error {
209209
return nil
210210
}
211211

212-
// Create default dashboard
212+
// Create a default, empty, editable dashboard so users can configure it as needed.
213213
dashboard := &Dashboard{
214214
Version: "1.0",
215215
Name: "Dashboard",
@@ -221,25 +221,8 @@ func (s *Storage) CreateDefaultDashboard() error {
221221
Columns: 12,
222222
Gap: 4,
223223
},
224-
Widgets: []Widget{
225-
{
226-
ID: "welcome-1",
227-
Type: "alert-banner",
228-
Position: WidgetPosition{
229-
Row: 1,
230-
Column: 1,
231-
Width: 12,
232-
Height: "auto",
233-
},
234-
Config: map[string]interface{}{
235-
"severity": "info",
236-
"message": "Welcome to Gearbox! This is your default dashboard. You can customize it by adding widgets.",
237-
"icon": "info",
238-
"dismissible": true,
239-
},
240-
},
241-
},
242-
Slug: "dashboard",
224+
Widgets: []Widget{},
225+
Slug: "dashboard",
243226
}
244227

245228
// Save dashboard

gearbox/internal/framework/database/plugins.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ type Plugin struct {
162162
SortOrder int `json:"sort_order"` // Order for display in UI (lower = higher priority)
163163
CreatedAt time.Time `json:"created_at"`
164164
UpdatedAt time.Time `json:"updated_at"`
165-
UpdatedBy *int64 `json:"updated_by,omitempty"`
165+
UpdatedBy *string `json:"updated_by,omitempty"`
166166
}
167167

168168
// DefaultPlugins returns the default integration configurations for a server.
@@ -302,7 +302,7 @@ func (d *DB) initPluginsSchema() error {
302302
sort_order INTEGER NOT NULL DEFAULT 0,
303303
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
304304
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
305-
updated_by INTEGER,
305+
updated_by TEXT,
306306
UNIQUE(server_id, name),
307307
FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL
308308
);

gearbox/internal/framework/handler/dashboard.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,11 +169,14 @@ func (h *DashboardHandler) EditDashboardPage(w http.ResponseWriter, r *http.Requ
169169
// Get available widgets
170170
widgets := h.widgetRegistry.List()
171171

172+
// Check if palette should be auto-opened (e.g., redirected from empty dashboard)
173+
openPalette := r.URL.Query().Get("open_palette") == "1"
174+
172175
// Get user from context
173176
user, _ := auth.GetUserFromContext(r.Context())
174177

175178
// Render editor with live content
176-
component := pages.DashboardEditorPage(dash, content, widgets, user, r.URL.Path)
179+
component := pages.DashboardEditorPage(dash, content, widgets, user, r.URL.Path, openPalette)
177180
if err := component.Render(r.Context(), w); err != nil {
178181
h.logger.Error("failed to render dashboard editor", "error", err)
179182
http.Error(w, "Failed to render page", http.StatusInternalServerError)

gearbox/internal/framework/handler/haproxy_config.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"encoding/json"
55
"fmt"
66
"net/http"
7+
"net/url"
78
"strconv"
89
"strings"
910

@@ -136,8 +137,8 @@ func (h *Handler) HAProxyBoxCreatePost(w http.ResponseWriter, r *http.Request) {
136137
}
137138
}
138139

139-
// Redirect to servers list page
140-
http.Redirect(w, r, "/settings/boxes", http.StatusSeeOther)
140+
// Redirect to plugins page so user can enable plugins for this new server
141+
http.Redirect(w, r, "/settings/plugins?server="+url.QueryEscape(server.BoxID), http.StatusSeeOther)
141142
}
142143

143144
// HAProxyBoxEditPage shows the form for editing a server.

gearbox/internal/framework/templates/layouts/base.templ

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1216,7 +1216,6 @@ templ Sidebar(user *models.User, currentPath string) {
12161216
<!-- Navigation -->
12171217
<nav class="flex-1 py-4">
12181218
<ul id="sidebar-nav-list" class="space-y-1">
1219-
@SidebarLink("/", "Dashboard", SidebarIconDashboard(), currentPath)
12201219
@OrderedIntegrationLinks(currentPath)
12211220
</ul>
12221221
</nav>
@@ -1570,6 +1569,57 @@ templ SidebarLinkWithIntegration(href string, label string, icon templ.Component
15701569
}
15711570
}
15721571

1572+
// hasAnyEnabledIntegration checks if any integration (plugin) is enabled in the current context.
1573+
func hasAnyEnabledIntegration(ctx context.Context) bool {
1574+
integrations, ok := auth.GetPluginOrderFromContext(ctx)
1575+
if !ok {
1576+
return false
1577+
}
1578+
for _, integration := range integrations {
1579+
if integration.Enabled {
1580+
return true
1581+
}
1582+
}
1583+
return false
1584+
}
1585+
1586+
// integrationPath returns the URL path for a given integration name.
1587+
func integrationPath(name string) string {
1588+
switch name {
1589+
case "metrics":
1590+
return "/history"
1591+
case "logs":
1592+
return "/logs"
1593+
case "services":
1594+
return "/services"
1595+
case "certificates":
1596+
return "/certificates"
1597+
case "traffic":
1598+
return "/traffic"
1599+
case "alerts":
1600+
return "/alerts"
1601+
case "os_updates":
1602+
return "/os-updates"
1603+
default:
1604+
return "/"
1605+
}
1606+
}
1607+
1608+
// firstEnabledIntegrationPath returns the URL path for the first enabled integration.
1609+
// Returns "/" if no integrations are enabled.
1610+
func firstEnabledIntegrationPath(ctx context.Context) string {
1611+
integrations, ok := auth.GetPluginOrderFromContext(ctx)
1612+
if !ok {
1613+
return "/"
1614+
}
1615+
for _, integration := range integrations {
1616+
if integration.Enabled {
1617+
return integrationPath(integration.Name)
1618+
}
1619+
}
1620+
return "/"
1621+
}
1622+
15731623
// canViewIntegration checks if the user has permission to view an integration.
15741624
// Maps integration names to component View permissions.
15751625
func canViewIntegration(ctx context.Context, integrationName string) bool {

gearbox/internal/framework/templates/pages/dashboard_editor.templ

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
)
1111

1212
// DashboardEditorPage renders the visual dashboard editor with live widget preview
13-
templ DashboardEditorPage(dash *dashboard.Dashboard, content templ.Component, widgets []*widget.WidgetDefinition, user *models.User, currentPath string) {
13+
templ DashboardEditorPage(dash *dashboard.Dashboard, content templ.Component, widgets []*widget.WidgetDefinition, user *models.User, currentPath string, openPalette bool) {
1414
@layouts.Base("Edit Dashboard", user, currentPath) {
1515
<!-- Widget Palette CSS -->
1616
<link rel="stylesheet" href="/static/css/dashboard/palette.css"/>
@@ -49,8 +49,8 @@ templ DashboardEditorPage(dash *dashboard.Dashboard, content templ.Component, wi
4949
</div>
5050
</div>
5151

52-
<!-- Widget Palette Panel (collapsible) -->
53-
<div id="widget-palette-panel" class="hidden mb-4 bg-white dark:bg-slate-900 border border-gray-300 dark:border-slate-600 rounded-lg shadow-lg">
52+
<!-- Widget Palette Panel (collapsible, auto-opened when dashboard is empty) -->
53+
<div id="widget-palette-panel" class={ widgetPaletteClass(openPalette) }>
5454
<!-- Fixed Header -->
5555
<div class="p-4 border-b border-gray-200 dark:border-slate-700 sticky top-0 bg-white dark:bg-slate-900 z-10">
5656
<div class="flex items-center justify-between mb-3">
@@ -393,6 +393,14 @@ templ DashboardEditorScript(dash *dashboard.Dashboard) {
393393
}
394394

395395
// Helper functions
396+
397+
func widgetPaletteClass(openPalette bool) string {
398+
if openPalette {
399+
return "mb-4 bg-white dark:bg-slate-900 border border-gray-300 dark:border-slate-600 rounded-lg shadow-lg"
400+
}
401+
return "hidden mb-4 bg-white dark:bg-slate-900 border border-gray-300 dark:border-slate-600 rounded-lg shadow-lg"
402+
}
403+
396404
func getWidgetStyle(pos dashboard.WidgetPosition) string {
397405
return fmt.Sprintf("grid-column: span %d;", pos.Width)
398406
}

gearbox/internal/framework/templates/pages/plugins.templ

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,16 @@ templ PluginsPage(user *models.User, servers []models.BoxConfig, currentServerID
7272
}
7373
</div>
7474

75+
<!-- Done button to navigate to main dashboard -->
76+
<div class="mt-8 flex justify-end">
77+
<a
78+
href="/"
79+
class="px-6 py-2.5 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors"
80+
>
81+
Done
82+
</a>
83+
</div>
84+
7585
<script src="/static/js/plugins/plugins-page.js"></script>
7686
</div>
7787
}

0 commit comments

Comments
 (0)