## Summary Adds `tools/hangar` — a macOS desktop control panel for working on Fleet locally: branch management, `fleet serve` orchestration, log tail, dev MySQL backup/restore, `fleetctl`, GitOps, and `osquery-perf`, all in one window. Built with **Go + [Wails 3](https://v3alpha.wails.io)** — the backend is plain Go (`os/exec`, `syscall`, goroutines) so Fleet engineers can contribute to it; only the desktop shell is Wails. The `internal/` packages are pure and unit-tested. ### History note Hangar started as a Rust/Tauri app. It was ported to Go, and **the Go port is now the canonical `tools/hangar`**. The original Rust/Tauri implementation has been removed from the monorepo (preserved in a standalone repo) — so although this branch's earlier commits add and then replace the Rust app, the net diff is just the Go app at `tools/hangar`. The bundle identifier is `com.fleetdm.fleet-hangar`, matching the original app so existing settings carry over. # Checklist for submitter - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements), JS inline code is prevented especially for url redirects, and untrusted data interpolated into shell scripts/commands is validated against shell metacharacters. - [x] Timeouts are implemented and retries are limited to avoid infinite loops > No `changes/` file: `tools/` is contributor tooling, not a user-visible Fleet change. No DB migrations, no Fleet config settings, no fleetd/orbit changes. ## Testing - [x] Added/updated automated tests (Go unit tests across `internal/`, including a path-traversal regression for backup deletion) - [x] QA'd all new/changed functionality manually ## Test plan - [x] `cd tools/hangar && task dev` launches the app (live-reload) - [x] `task build` produces `bin/fleet-hangar`; `go test ./...` is green - [x] First-run gate discovers a local Fleet clone and runs dep checks - [x] Server tab can run the build chain and start `fleet serve` - [x] Git tab branch search finds an older branch (e.g. a stale `qa-*`) by name <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Introduced Fleet Hangar, a comprehensive desktop application for Fleet development workflows, providing unified controls for server/database management, git operations, configuration, logging, and troubleshooting. * Added database backup management with metadata tracking. * Integrated process orchestration for development services (Docker, ngrok, Python). <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: George Karr <georgekarrv@users.noreply.github.com>
80 lines
2.1 KiB
Go
80 lines
2.1 KiB
Go
package main
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/wailsapp/wails/v3/pkg/application"
|
|
|
|
"github.com/fleetdm/fleet/tools/hangar/internal/traymenu"
|
|
)
|
|
|
|
const mainWindowName = "main"
|
|
|
|
// trayController owns the native system tray and rebuilds its menu from
|
|
// pushed state. Ported from tray.rs.
|
|
type trayController struct {
|
|
app *application.App
|
|
tray *application.SystemTray
|
|
}
|
|
|
|
func newTrayController(app *application.App, icon []byte) *trayController {
|
|
t := app.SystemTray.New()
|
|
// Template mode renders the icon as a system-colored silhouette in the
|
|
// menu bar (alpha-only), matching the rest of the macOS menu bar.
|
|
t.SetTemplateIcon(icon)
|
|
t.SetTooltip("Fleet Hangar")
|
|
c := &trayController{app: app, tray: t}
|
|
c.update(traymenu.State{})
|
|
// Left-click opens the main window; the menu shows on right-click.
|
|
t.OnClick(func() { showMainWindow(app) })
|
|
return c
|
|
}
|
|
|
|
// update rebuilds the tray menu in place from the given state.
|
|
func (c *trayController) update(s traymenu.State) {
|
|
menu := application.NewMenu()
|
|
for _, it := range traymenu.BuildItems(s) {
|
|
if it.Separator {
|
|
menu.AddSeparator()
|
|
continue
|
|
}
|
|
item := menu.Add(it.Label)
|
|
item.SetEnabled(it.Enabled)
|
|
id := it.ID
|
|
item.OnClick(func(*application.Context) { c.handleClick(id) })
|
|
}
|
|
c.tray.SetMenu(menu)
|
|
}
|
|
|
|
// handleClick routes a menu item. show/quit are handled here; everything else
|
|
// is forwarded to the frontend, which owns the start/stop orchestration.
|
|
func (c *trayController) handleClick(id string) {
|
|
switch id {
|
|
case "tray:show":
|
|
showMainWindow(c.app)
|
|
case "tray:quit":
|
|
// Surface the window so the confirm modal is visible even if the app
|
|
// was tray-only, then let the frontend drive the quit flow.
|
|
showMainWindow(c.app)
|
|
c.app.Event.Emit("app:quit-requested")
|
|
default:
|
|
if strings.HasPrefix(id, "tray:") {
|
|
c.app.Event.Emit(id)
|
|
}
|
|
}
|
|
}
|
|
|
|
// showMainWindow brings the main window back from hidden/minimized and
|
|
// focuses it.
|
|
func showMainWindow(app *application.App) {
|
|
w, ok := app.Window.GetByName(mainWindowName)
|
|
if !ok {
|
|
return
|
|
}
|
|
if ww, ok := w.(*application.WebviewWindow); ok {
|
|
ww.UnMinimise()
|
|
ww.Show()
|
|
ww.Focus()
|
|
}
|
|
}
|