Skip to content

Commit 2a73fdd

Browse files
authored
Add request inspection (#1)
* Add request inspection * Cleanup
1 parent 50d5751 commit 2a73fdd

File tree

2 files changed

+131
-6
lines changed

2 files changed

+131
-6
lines changed

internal/funnel/ts.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,8 @@ func CreateEphemeralFunnel(name string, target string, logger *stdlog.Logger) (F
250250

251251
// ok go create the funnel
252252
tsClient := TailscaleClient{
253-
ts: localClient,
253+
ts: localClient,
254+
logger: logger,
254255
}
255256

256257
funnelID := uuid.New().String()

internal/tui/tui.go

Lines changed: 129 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
stdlog "log"
66
"os"
7+
"sort"
78
"strconv"
89
"strings"
910
"time"
@@ -115,7 +116,11 @@ Detail View:
115116
tab / → / l: Next Tab
116117
shift+tab / ← / h: Previous Tab
117118
c : Copy Public URL (Info Tab)
118-
esc / q : Back to List View
119+
enter : View Request Details (Request Log Tab)
120+
esc : Back to List View
121+
122+
Request Detail View:
123+
esc : Back to Request Log
119124
`
120125

121126
// viewState indicates which view is currently active
@@ -127,6 +132,7 @@ const (
127132
viewConfirmDelete // View for confirming deletion
128133
viewDetail // View showing details for a selected funnel
129134
viewHelp // View displaying keybindings/help
135+
viewRequestDetail // View showing details of a specific proxied request
130136
)
131137

132138
// --- Model ---
@@ -166,7 +172,8 @@ type model struct {
166172
table table.Model
167173
funnelOrder []string // Slice of funnel IDs to maintain order matching table rows
168174

169-
requestTable table.Model
175+
requestTable table.Model
176+
selectedRequest *funnel.CaptureRequestResponse // The request being inspected in viewRequestDetail
170177

171178
logger *stdlog.Logger
172179
}
@@ -236,6 +243,7 @@ func createRequestTable() table.Model {
236243
{Title: "Method"},
237244
{Title: "Status"},
238245
{Title: "URL"},
246+
{Title: "ID"}, // Hidden column for request ID
239247
}
240248

241249
t := table.New(
@@ -250,7 +258,6 @@ func createRequestTable() table.Model {
250258
t.SetStyles(s)
251259

252260
return t
253-
254261
}
255262

256263
func (m model) Init() tea.Cmd {
@@ -273,6 +280,10 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
273280
// Let the input handler process 'q'
274281
break // Fall through to view-specific handlers
275282
}
283+
// Prevent quitting globally if in request detail view (let view handler decide)
284+
if m.state == viewRequestDetail && msg.String() == "q" {
285+
break // Fall through to view-specific handlers (which will ignore 'q')
286+
}
276287
return m, tea.Quit
277288
case "?":
278289
// Don't open help from help view or create view
@@ -429,6 +440,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
429440
{Title: "Method", Width: 8},
430441
{Title: "Status", Width: 8},
431442
{Title: "URL", Width: urlWidth},
443+
{}, // Hidden column, no title or width needed
432444
}
433445
m.requestTable.SetColumns(requestColumns)
434446

@@ -447,6 +459,8 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
447459
return m.updateDetailView(msg)
448460
case viewHelp:
449461
return m.updateHelpView(msg) // Add call to new update function
462+
case viewRequestDetail:
463+
return m.updateRequestDetailView(msg)
450464
}
451465

452466
return m, nil
@@ -704,8 +718,36 @@ func (m model) updateDetailView(msg tea.Msg) (tea.Model, tea.Cmd) {
704718
}
705719
return m, nil // Do nothing if not on info tab or error
706720

707-
// Note: We don't have a default case here, so keys not handled above
708-
// will fall through to the logic below.
721+
case "enter":
722+
if m.detailTabIndex == 1 {
723+
selectedRow := m.requestTable.SelectedRow()
724+
if len(selectedRow) < 5 { // Ensure row and ID exist (index 4)
725+
return m, nil // Or handle error
726+
}
727+
selectedRequestID := selectedRow[4] // Get ID from the hidden column
728+
729+
funnel, err := m.funnelRegistry.GetFunnel(m.detailedFunnelID)
730+
if err == nil {
731+
// Find the request by ID in the linked list
732+
node := funnel.Requests.Head
733+
found := false
734+
for node != nil {
735+
if node.Request.ID == selectedRequestID {
736+
m.selectedRequest = &node.Request
737+
m.state = viewRequestDetail
738+
m.requestTable.Blur() // Unfocus table when leaving
739+
found = true
740+
break
741+
}
742+
node = node.Next
743+
}
744+
745+
if found {
746+
return m, nil
747+
}
748+
}
749+
}
750+
return m, nil
709751
}
710752
}
711753

@@ -740,6 +782,24 @@ func (m model) updateHelpView(msg tea.Msg) (tea.Model, tea.Cmd) {
740782
return m, cmd
741783
}
742784

785+
// updateRequestDetailView handles updates when the request detail view is active.
786+
func (m model) updateRequestDetailView(msg tea.Msg) (tea.Model, tea.Cmd) {
787+
switch msg := msg.(type) {
788+
case tea.KeyMsg:
789+
switch msg.String() {
790+
case "esc": // Only Esc goes back to detail view (request log tab)
791+
m.state = viewDetail
792+
m.detailTabIndex = 1 // Ensure we return to the request log tab
793+
m.selectedRequest = nil // Clear the selected request
794+
m.requestTable.Focus() // Refocus the request table
795+
return m, nil
796+
}
797+
}
798+
// Handle other message types (like window resize) if needed.
799+
// Currently, no other actions are handled in this basic version.
800+
return m, nil
801+
}
802+
743803
// View renders the TUI's UI. It's called after every Update.
744804
func (m model) View() string {
745805
// Footer (Render first to get its height)
@@ -766,6 +826,8 @@ func (m model) View() string {
766826
mainContent = m.viewDetailView(contentHeight)
767827
case viewHelp:
768828
mainContent = m.viewHelpView(contentHeight)
829+
case viewRequestDetail:
830+
mainContent = m.viewRequestDetailView(contentHeight)
769831
}
770832

771833
finalView := lipgloss.JoinVertical(lipgloss.Left,
@@ -922,6 +984,7 @@ func (m *model) populateRequestTable() {
922984
node.Request.Method(),
923985
strconv.Itoa(node.Request.StatusCode()),
924986
node.Request.Path(),
987+
node.Request.ID,
925988
})
926989
node = node.Next
927990
}
@@ -999,6 +1062,8 @@ func (m model) footerView() string {
9991062
coreHelp = "tab/←/→: tabs, c: copy, esc/q: back, ?: help"
10001063
case viewHelp: // No specific help needed when already viewing help
10011064
coreHelp = "esc/q: back, ←/→: scroll"
1065+
case viewRequestDetail:
1066+
coreHelp = "esc/q: back"
10021067
}
10031068

10041069
// Combine status message and help text
@@ -1059,3 +1124,62 @@ func (m model) viewHelpView(contentHeight int) string {
10591124

10601125
return m.renderContent("Help", m.viewport.View(), contentHeight, 1)
10611126
}
1127+
1128+
// viewRequestDetailView renders the details of a selected HTTP request.
1129+
func (m model) viewRequestDetailView(contentHeight int) string {
1130+
title := "Request Details"
1131+
if m.selectedRequest == nil {
1132+
return m.renderContent(title, "Error: No request selected.", contentHeight, 1)
1133+
}
1134+
1135+
requestInfo := fmt.Sprintf(
1136+
"URL: %s\nMethod: %s\nStatus: %d",
1137+
m.selectedRequest.Path(),
1138+
m.selectedRequest.Method(),
1139+
m.selectedRequest.StatusCode(),
1140+
)
1141+
1142+
formatHeaders := func(headers map[string]string) string {
1143+
var builder strings.Builder
1144+
if len(headers) == 0 {
1145+
builder.WriteString(" (No headers)")
1146+
} else {
1147+
// Get keys and sort them
1148+
keys := make([]string, 0, len(headers))
1149+
for k := range headers {
1150+
keys = append(keys, k)
1151+
}
1152+
sort.Strings(keys)
1153+
1154+
// Iterate over sorted keys
1155+
for _, k := range keys {
1156+
v := headers[k]
1157+
builder.WriteString(fmt.Sprintf(" %s: %s\n", k, v))
1158+
}
1159+
// Remove trailing newline
1160+
result := builder.String()
1161+
return strings.TrimSuffix(result, "\n")
1162+
}
1163+
return builder.String()
1164+
}
1165+
1166+
responseHeadersTitle := lipgloss.NewStyle().Bold(true).Render("Response Headers")
1167+
responseHeadersContent := formatHeaders(m.selectedRequest.Response.Headers)
1168+
1169+
requestHeadersTitle := lipgloss.NewStyle().Bold(true).Render("Request Headers")
1170+
requestHeadersContent := formatHeaders(m.selectedRequest.Request.Headers)
1171+
1172+
// Simple vertical layout for now
1173+
content := lipgloss.JoinVertical(lipgloss.Left,
1174+
requestInfo,
1175+
"\n", // Spacer
1176+
responseHeadersTitle,
1177+
responseHeadersContent,
1178+
"\n", // Spacer
1179+
requestHeadersTitle,
1180+
requestHeadersContent,
1181+
)
1182+
1183+
// Use the standard renderContent helper
1184+
return m.renderContent(title, content, contentHeight, 1)
1185+
}

0 commit comments

Comments
 (0)