Skip to content

Commit b2ddaec

Browse files
committed
Refactor dashboard and HAProxy handlers to update redirect paths and descriptions; remove hardcoded server ID in widget definitions; add new CSS for widget palette styling; implement JavaScript for widget palette management and filtering.
1 parent 33ca67f commit b2ddaec

23 files changed

Lines changed: 954 additions & 56 deletions

File tree

TASKS.md

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -90,22 +90,40 @@ Framework = Shared services and building blocks (graphs, tables, panels, cards,
9090

9191
**Remaining Tasks:**
9292

93-
#### Task 1: Widget Palette API
93+
#### Task 1: Widget Palette API
9494

95-
- [ ] Create `/api/dashboards/widgets` endpoint in `gearbox/internal/framework/handler/dashboard.go`
96-
- [ ] Return all available widgets from **enabled plugins only** for the selected server
97-
- [ ] Support filtering by plugin name, category, search term
98-
- [ ] Register route in router setup
99-
- [ ] Test with curl/Postman
95+
- [x] Create `/api/dashboards/widgets` endpoint in `gearbox/internal/framework/handler/dashboard.go`
96+
- [x] Return all available widgets from **enabled plugins only** for the selected server
97+
- [x] Support filtering by plugin name, category, search term
98+
- [x] Register route in router setup
99+
- [x] Added `PluginName` field to all 31 widget definitions across 8 plugins
100+
- [x] Built successfully with `make templ-generate && make build`
100101

101-
#### Task 2: Widget Palette Side Panel UI
102+
**Implementation Details:**
102103

103-
- [ ] Add palette panel HTML to `gearbox/internal/framework/templates/pages/dashboard_editor.templ`
104-
- [ ] Create `gearbox/static/css/dashboard/palette.css` for styling
105-
- [ ] Create `gearbox/static/js/dashboard/palette.js` for interactions
106-
- [ ] Display widgets as cards with icon, name, description, plugin badge
107-
- [ ] Add filtering UI (search, plugin dropdown)
108-
- [ ] Make panel scrollable with fixed header
104+
- Route: `GET /api/dashboards/widgets`
105+
- Query params: `?server_id=<id>&plugin=<name>&category=<cat>&search=<term>`
106+
- Returns JSON array of widgets from enabled plugins
107+
- Core widgets always included (PluginName = "core")
108+
109+
#### Task 2: Widget Palette Side Panel UI ✅
110+
111+
- [x] Add palette panel HTML to `gearbox/internal/framework/templates/pages/dashboard_editor.templ`
112+
- [x] Create `gearbox/static/css/dashboard/palette.css` for styling
113+
- [x] Create `gearbox/static/js/dashboard/palette.js` for interactions
114+
- [x] Display widgets as cards with icon, name, description, plugin badge
115+
- [x] Add filtering UI (search, plugin dropdown)
116+
- [x] Make panel scrollable with fixed header
117+
- [x] Built successfully with `make templ-generate && make build`
118+
119+
**Implementation Details:**
120+
121+
- Enhanced widget palette panel with fixed header and scrollable list
122+
- Widget cards display icon, name, description, and color-coded plugin badges
123+
- Real-time search across name, description, and plugin
124+
- Plugin dropdown filter with all available plugins
125+
- Fetches widgets dynamically from `/api/dashboards/widgets` endpoint
126+
- Responsive design with dark mode support
109127

110128
#### Task 3: Drag-and-Drop from Palette
111129

@@ -120,6 +138,14 @@ Framework = Shared services and building blocks (graphs, tables, panels, cards,
120138
- [ ] Show plugin menu items only when enabled for server
121139
- [ ] Ensure plugin menu items update when plugins are enabled/disabled
122140

141+
#### Task 5: Left Hand Navigation bar Updates
142+
143+
- [ ] Items can be rearranged by clicking edit icon that is next to the toggle sidebar icon
144+
- [ ] When in edit mode user can drag a menu item up or down and other nav items flow around it
145+
- [ ] Edit icon becomes save icon
146+
- [ ] When a plugin is enabled it generates its default "dashboard" interface yaml file if it does not already exist. Each plugin's default dashboard has a default name and icon as designed by plugin developer. When plugins is enabled it is added to the nav bar
147+
- [ ] The rearrangement of left hand nav items will be removed from the plugin screen, all nav rearrangement is now in the nav bar only.
148+
123149
**Testing Checklist:**
124150

125151
- [ ] Enable HAProxy plugin → verify dashboard auto-created

gearbox/cmd/server/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,7 @@ func main() {
378378
dashboardStorage,
379379
widgetRegistry,
380380
dataSourceRegistry,
381+
db,
381382
logger,
382383
"", // serverID - can be set per request
383384
"", // userID - can be set per request

gearbox/internal/framework/handler/dashboard.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ import (
44
"encoding/json"
55
"log/slog"
66
"net/http"
7+
"strings"
78

89
"github.com/go-chi/chi/v5"
910
"github.com/sarg3nt/gearbox/internal/framework/auth"
1011
"github.com/sarg3nt/gearbox/internal/framework/dashboard"
12+
"github.com/sarg3nt/gearbox/internal/framework/database"
1113
"github.com/sarg3nt/gearbox/internal/framework/templates/pages"
1214
"github.com/sarg3nt/gearbox/internal/framework/widget"
1315
)
@@ -18,6 +20,7 @@ type DashboardHandler struct {
1820
renderer *dashboard.Renderer
1921
widgetRegistry *widget.Registry
2022
dataSourceRegistry *widget.DataSourceRegistry
23+
db *database.DB
2124
logger *slog.Logger
2225
serverID string
2326
userID string
@@ -28,6 +31,7 @@ func NewDashboardHandler(
2831
storage *dashboard.Storage,
2932
widgetRegistry *widget.Registry,
3033
dataSourceRegistry *widget.DataSourceRegistry,
34+
db *database.DB,
3135
logger *slog.Logger,
3236
serverID string,
3337
userID string,
@@ -43,6 +47,7 @@ func NewDashboardHandler(
4347
renderer: renderer,
4448
widgetRegistry: widgetRegistry,
4549
dataSourceRegistry: dataSourceRegistry,
50+
db: db,
4651
logger: logger,
4752
serverID: serverID,
4853
userID: userID,
@@ -66,6 +71,7 @@ func (h *DashboardHandler) RegisterRoutes(r chi.Router) {
6671
r.Put("/api/dashboards/{slug}", h.UpdateDashboard)
6772
r.Post("/api/dashboards/order", h.SaveDashboardOrder)
6873
r.Post("/api/dashboards/{slug}/toggle", h.TogglePluginDashboard)
74+
r.Get("/api/dashboards/widgets", h.GetWidgetPalette)
6975
}
7076

7177
// ListDashboards handles GET /dashboards
@@ -308,3 +314,92 @@ func (h *DashboardHandler) UpdateDashboard(w http.ResponseWriter, r *http.Reques
308314
"slug": dash.Slug,
309315
})
310316
}
317+
318+
// GetWidgetPalette handles GET /api/dashboards/widgets
319+
// Returns available widgets from enabled plugins for the selected server
320+
func (h *DashboardHandler) GetWidgetPalette(w http.ResponseWriter, r *http.Request) {
321+
// Get server ID from query parameter
322+
serverID := r.URL.Query().Get("server_id")
323+
if serverID == "" {
324+
serverID = h.serverID // Use default if not specified
325+
}
326+
327+
// Get optional filter parameters
328+
pluginFilter := r.URL.Query().Get("plugin")
329+
categoryFilter := r.URL.Query().Get("category")
330+
searchTerm := r.URL.Query().Get("search")
331+
332+
// Get enabled plugins for this server
333+
enabledPlugins, err := h.db.GetEnabledPlugins(serverID)
334+
if err != nil {
335+
h.logger.Error("failed to get enabled plugins", "server_id", serverID, "error", err)
336+
http.Error(w, "Failed to get enabled plugins", http.StatusInternalServerError)
337+
return
338+
}
339+
340+
// Get all registered widgets
341+
allWidgets := h.widgetRegistry.List()
342+
343+
// Filter widgets based on enabled plugins and query parameters
344+
var filteredWidgets []*widget.WidgetDefinition
345+
for _, w := range allWidgets {
346+
// Skip widgets from disabled plugins (unless it's a core widget)
347+
if w.PluginName != "core" {
348+
if enabled, ok := enabledPlugins[w.PluginName]; !ok || !enabled {
349+
continue
350+
}
351+
}
352+
353+
// Apply plugin filter
354+
if pluginFilter != "" && w.PluginName != pluginFilter {
355+
continue
356+
}
357+
358+
// Apply category filter
359+
if categoryFilter != "" && w.Category != categoryFilter {
360+
continue
361+
}
362+
363+
// Apply search filter (search in name and description)
364+
if searchTerm != "" {
365+
searchLower := strings.ToLower(searchTerm)
366+
nameLower := strings.ToLower(w.Name)
367+
descLower := strings.ToLower(w.Description)
368+
if !strings.Contains(nameLower, searchLower) && !strings.Contains(descLower, searchLower) {
369+
continue
370+
}
371+
}
372+
373+
filteredWidgets = append(filteredWidgets, w)
374+
}
375+
376+
// Convert to JSON-friendly format
377+
type WidgetResponse struct {
378+
Type string `json:"type"`
379+
Name string `json:"name"`
380+
Description string `json:"description"`
381+
Category string `json:"category"`
382+
PluginName string `json:"plugin_name"`
383+
Icon string `json:"icon"`
384+
}
385+
386+
response := make([]WidgetResponse, 0, len(filteredWidgets))
387+
for _, w := range filteredWidgets {
388+
response = append(response, WidgetResponse{
389+
Type: w.Type,
390+
Name: w.Name,
391+
Description: w.Description,
392+
Category: w.Category,
393+
PluginName: w.PluginName,
394+
Icon: w.Icon,
395+
})
396+
}
397+
398+
// Return JSON response
399+
w.Header().Set("Content-Type", "application/json")
400+
if err := json.NewEncoder(w).Encode(response); err != nil {
401+
h.logger.Error("failed to encode widget palette response", "error", err)
402+
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
403+
return
404+
}
405+
}

gearbox/internal/framework/handler/handler.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,20 @@ func (h *Handler) InjectIntegrationStatus(next http.Handler) http.Handler {
235235
next.ServeHTTP(w, r.WithContext(ctx))
236236
return
237237
}
238+
239+
// No server configured - explicitly disable all plugins in navigation
240+
// This provides a clean initial setup experience without plugin clutter
241+
status := map[string]bool{
242+
"metrics": false,
243+
"logs": false,
244+
"services": false,
245+
"certificates": false,
246+
"traffic": false,
247+
"alerts": false,
248+
"os_updates": false,
249+
}
250+
ctx = auth.SetPluginStatus(ctx, status)
251+
ctx = auth.SetIntegrationOrder(ctx, []auth.SidebarIntegration{})
238252
next.ServeHTTP(w, r.WithContext(ctx))
239253
})
240254
}

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

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,18 @@ func getPath(paths []string) string {
2323
return ""
2424
}
2525

26+
// shouldHideSidebar determines if the sidebar should be hidden based on context
27+
// Returns true if there are no servers configured (initial setup state)
28+
func shouldHideSidebar(ctx context.Context, currentPath string) bool {
29+
// Check if plugin status is in context - if not, we're in initial setup
30+
_, hasPluginStatus := auth.GetPluginStatusFromContext(ctx)
31+
_, hasPluginOrder := auth.GetPluginOrderFromContext(ctx)
32+
33+
// If neither plugin status nor order is set, we're in initial setup (no servers)
34+
// Hide the sidebar to provide a cleaner first-time experience
35+
return !hasPluginStatus && !hasPluginOrder
36+
}
37+
2638
templ Base(title string, user *models.User, currentPath ...string) {
2739
<!DOCTYPE html>
2840
<html lang="en" class="h-full">
@@ -683,13 +695,21 @@ templ Base(title string, user *models.User, currentPath ...string) {
683695
<body class="h-full bg-gray-100 dark:bg-slate-900">
684696
@ui.CollapsibleRestoreScript()
685697
if user != nil {
686-
@Sidebar(user, getPath(currentPath))
687-
<div id="main-content" class="main-content ml-64 min-h-screen flex flex-col pt-[57px]">
688-
@Header()
689-
<main class="flex-1 p-6">
698+
// Check if we should hide the sidebar (e.g., on initial setup pages)
699+
if shouldHideSidebar(ctx, getPath(currentPath)) {
700+
<!-- No sidebar, full-width layout for initial setup -->
701+
<main class="min-h-screen">
690702
{ children... }
691703
</main>
692-
</div>
704+
} else {
705+
@Sidebar(user, getPath(currentPath))
706+
<div id="main-content" class="main-content ml-64 min-h-screen flex flex-col pt-[57px]">
707+
@Header()
708+
<main class="flex-1 p-6">
709+
{ children... }
710+
</main>
711+
</div>
712+
}
693713
} else {
694714
<main class="min-h-screen">
695715
{ children... }
@@ -1078,7 +1098,6 @@ templ Sidebar(user *models.User, currentPath string) {
10781098
<nav class="flex-1 py-4">
10791099
<ul class="space-y-1">
10801100
@SidebarLink("/", "Dashboard", SidebarIconDashboard(), currentPath)
1081-
@SidebarLink("/status-grid", "Status Grid", SidebarIconGrid(), currentPath)
10821101
@OrderedIntegrationLinks(currentPath)
10831102
</ul>
10841103
</nav>

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

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import (
1212
// DashboardEditorPage renders the visual dashboard editor with live widget preview
1313
templ DashboardEditorPage(dash *dashboard.Dashboard, content templ.Component, widgets []*widget.WidgetDefinition, user *models.User, currentPath string) {
1414
@layouts.Base("Edit Dashboard", user, currentPath) {
15+
<!-- Widget Palette CSS -->
16+
<link rel="stylesheet" href="/static/css/dashboard/palette.css"/>
1517
<!-- Edit Mode Indicator -->
1618
<div class="mb-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-3">
1719
<div class="flex items-center justify-between">
@@ -49,16 +51,43 @@ templ DashboardEditorPage(dash *dashboard.Dashboard, content templ.Component, wi
4951

5052
<!-- Widget Palette Panel (collapsible) -->
5153
<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-
<div class="p-4 border-b border-gray-200 dark:border-slate-700 flex items-center justify-between">
53-
<h3 class="font-semibold text-gray-900 dark:text-gray-100">Available Widgets</h3>
54-
<button onclick="toggleWidgetPalette()" class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
55-
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
56-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
57-
</svg>
58-
</button>
54+
<!-- Fixed Header -->
55+
<div class="p-4 border-b border-gray-200 dark:border-slate-700 sticky top-0 bg-white dark:bg-slate-900 z-10">
56+
<div class="flex items-center justify-between mb-3">
57+
<h3 class="font-semibold text-gray-900 dark:text-gray-100">Available Widgets</h3>
58+
<button onclick="toggleWidgetPalette()" class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
59+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
60+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
61+
</svg>
62+
</button>
63+
</div>
64+
65+
<!-- Search Input -->
66+
<input
67+
type="text"
68+
id="widget-palette-search"
69+
placeholder="Search widgets..."
70+
class="w-full px-3 py-2 mb-2 text-sm border border-gray-300 dark:border-slate-600 rounded bg-white dark:bg-slate-800 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
71+
/>
72+
73+
<!-- Filter by Plugin -->
74+
<select
75+
id="widget-palette-plugin-filter"
76+
class="w-full px-3 py-2 text-sm border border-gray-300 dark:border-slate-600 rounded bg-white dark:bg-slate-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
77+
>
78+
<option value="">All Plugins</option>
79+
<!-- Populated dynamically by JavaScript -->
80+
</select>
5981
</div>
60-
<div class="p-4 max-h-96 overflow-y-auto">
61-
@WidgetPalette(widgets)
82+
83+
<!-- Scrollable Widget List -->
84+
<div id="widget-palette-list" class="p-4 max-h-96 overflow-y-auto">
85+
<div class="text-center text-gray-500 dark:text-gray-400 py-8">
86+
<svg class="w-8 h-8 mx-auto mb-2 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
87+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
88+
</svg>
89+
<p class="text-sm">Loading widgets...</p>
90+
</div>
6291
</div>
6392
</div>
6493

@@ -353,12 +382,12 @@ templ WidgetConfigModal() {
353382
</div>
354383
}
355384

356-
// DashboardEditorScript provides the editor JavaScript
357-
358385
// DashboardEditorScript provides the editor JavaScript
359386
templ DashboardEditorScript(dash *dashboard.Dashboard) {
360387
<!-- Load Sortable.js module -->
361388
<script type="module" src="/static/js/dashboard/sortable-loader.js"></script>
389+
<!-- Load widget palette logic -->
390+
<script src="/static/js/dashboard/palette.js" defer></script>
362391
<!-- Load dashboard editor logic -->
363392
<script src="/static/js/dashboard/editor.js" defer></script>
364393
}

gearbox/internal/framework/templates/pages/traffic/visualization.templ

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ templ Visualization() {
6565
</div>
6666
<div class="flex items-center gap-2">
6767
<div class="w-4 h-4 rounded-full bg-blue-500 border-2 border-blue-600"></div>
68-
<span class="text-gray-300">HAProxy (light-hugger)</span>
68+
<span class="text-gray-300">HAProxy (server)</span>
6969
</div>
7070
<div class="flex items-center gap-2">
7171
<div class="w-4 h-3 rounded-sm bg-emerald-500"></div>

gearbox/internal/framework/widget/widget.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ type WidgetDefinition struct {
2121
// Category groups widgets (data-visualization, data-display, status-metrics, etc.)
2222
Category string
2323

24+
// PluginName is the name of the plugin that provides this widget (e.g., "haproxy", "metrics")
25+
// Used to filter widgets based on enabled plugins for a server
26+
PluginName string
27+
2428
// Icon is the icon name for the widget
2529
Icon string
2630

0 commit comments

Comments
 (0)