mirror of
https://github.com/wavetermdev/waveapps.git
synced 2026-08-05 13:48:00 -07:00
a little log viewer app. not really complete yet, just playing around
This commit is contained in:
@@ -0,0 +1,329 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
|
||||
"github.com/wavetermdev/waveterm/pkg/util/logview"
|
||||
"github.com/wavetermdev/waveterm/pkg/vdom"
|
||||
"github.com/wavetermdev/waveterm/pkg/waveapp"
|
||||
)
|
||||
|
||||
//go:embed style.css
|
||||
var styleCSS []byte
|
||||
|
||||
// CLI arguments
|
||||
var windowSize = flag.Int64("l", 20, "number of lines to display")
|
||||
var logFilePath string
|
||||
|
||||
var AppClient = waveapp.MakeClient(waveapp.AppOpts{
|
||||
CloseOnCtrlC: true,
|
||||
GlobalKeyboardEvents: true,
|
||||
GlobalStyles: styleCSS,
|
||||
})
|
||||
|
||||
// Props types
|
||||
type LogContentProps struct {
|
||||
Lines [][]byte `json:"lines"`
|
||||
Error string `json:"error"`
|
||||
LineStart int64 `json:"lineStart"`
|
||||
}
|
||||
|
||||
type FilterInputProps struct {
|
||||
Value string `json:"value"`
|
||||
OnChange func(string) `json:"onChange"`
|
||||
OnError func(string) `json:"onError"`
|
||||
ClearError func() `json:"clearError"`
|
||||
}
|
||||
|
||||
var FilterInput = waveapp.DefineComponent[FilterInputProps](AppClient, "FilterInput",
|
||||
func(ctx context.Context, props FilterInputProps) any {
|
||||
handleChange := func(e vdom.VDomEvent) {
|
||||
filter := e.TargetValue
|
||||
props.OnChange(filter)
|
||||
}
|
||||
|
||||
return vdom.H("div", map[string]any{
|
||||
"className": "filter-container",
|
||||
},
|
||||
vdom.H("input", map[string]any{
|
||||
"type": "text",
|
||||
"className": "filter-input",
|
||||
"placeholder": "Filter regexp (e.g. 'error|warn')",
|
||||
"value": props.Value,
|
||||
"onChange": handleChange,
|
||||
}),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
// LogContent component to display the log lines
|
||||
var LogContent = waveapp.DefineComponent[LogContentProps](AppClient, "LogContent",
|
||||
func(ctx context.Context, props LogContentProps) any {
|
||||
if props.Error != "" {
|
||||
return vdom.H("div", map[string]any{
|
||||
"className": "log-error",
|
||||
}, props.Error)
|
||||
}
|
||||
|
||||
return vdom.H("pre", map[string]any{
|
||||
"className": "log-content",
|
||||
},
|
||||
vdom.ForEachIdx(props.Lines, func(line []byte, idx int) any {
|
||||
lineNum := props.LineStart + int64(idx)
|
||||
return vdom.H("div", map[string]any{
|
||||
"key": idx,
|
||||
"className": "log-line",
|
||||
},
|
||||
vdom.H("span", map[string]any{
|
||||
"className": "line-number",
|
||||
}, fmt.Sprintf("%6d ", lineNum)),
|
||||
vdom.H("span", map[string]any{
|
||||
"className": "line-content",
|
||||
}, string(line)),
|
||||
)
|
||||
}),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
// Main App component
|
||||
var App = waveapp.DefineComponent(AppClient, "App",
|
||||
func(ctx context.Context, _ any) any {
|
||||
// State for storing log lines and error
|
||||
lines, setLines := vdom.UseState(ctx, [][]byte{})
|
||||
errorMsg, setErrorMsg := vdom.UseState(ctx, "")
|
||||
currentLineNum, setCurrentLineNum := vdom.UseState(ctx, int64(0))
|
||||
filterText, setFilterText := vdom.UseState(ctx, "")
|
||||
currentLinePtr := vdom.UseRef(ctx, (*logview.LinePtr)(nil))
|
||||
logViewRef := vdom.UseRef(ctx, (*logview.LogView)(nil))
|
||||
|
||||
// Handle filter changes
|
||||
handleFilterChange := func(filter string) {
|
||||
setFilterText(filter)
|
||||
|
||||
if logViewRef.Current == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// If filter is empty, clear the regexp
|
||||
if filter == "" {
|
||||
logViewRef.Current.MatchRe = nil
|
||||
} else {
|
||||
// Try to compile the regexp
|
||||
re, err := regexp.Compile(filter)
|
||||
if err != nil {
|
||||
setErrorMsg(fmt.Sprintf("Invalid regexp: %v", err))
|
||||
return
|
||||
}
|
||||
setErrorMsg("")
|
||||
logViewRef.Current.MatchRe = re
|
||||
}
|
||||
|
||||
// Reset to first matching line
|
||||
newPtr, err := logViewRef.Current.FirstLinePtr()
|
||||
if err != nil {
|
||||
setErrorMsg(fmt.Sprintf("Error finding first matching line: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if newPtr == nil {
|
||||
setLines([][]byte{})
|
||||
setCurrentLineNum(0)
|
||||
currentLinePtr.Current = nil
|
||||
return
|
||||
}
|
||||
|
||||
currentLinePtr.Current = newPtr
|
||||
setCurrentLineNum(newPtr.LineNum)
|
||||
|
||||
// Read new window of lines
|
||||
newLines, err := logViewRef.Current.ReadWindow(newPtr, int(*windowSize))
|
||||
if err != nil {
|
||||
setErrorMsg(fmt.Sprintf("Error reading lines: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
setLines(newLines)
|
||||
}
|
||||
|
||||
// Load initial log data and setup keyboard handler
|
||||
vdom.UseEffect(ctx, func() func() {
|
||||
file, err := os.Open(logFilePath)
|
||||
if err != nil {
|
||||
setErrorMsg(fmt.Sprintf("Error opening file: %v", err))
|
||||
return nil
|
||||
}
|
||||
|
||||
lv := logview.MakeLogView(file)
|
||||
logViewRef.Current = lv
|
||||
|
||||
// Setup keyboard handler
|
||||
AppClient.SetGlobalEventHandler(func(client *waveapp.Client, event vdom.VDomEvent) {
|
||||
if event.EventType != "onKeyDown" || event.KeyData == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if logViewRef.Current == nil {
|
||||
return
|
||||
}
|
||||
|
||||
key := event.KeyData.Key
|
||||
var newPtr *logview.LinePtr
|
||||
var err error
|
||||
|
||||
switch key {
|
||||
case "Home":
|
||||
newPtr, err = logViewRef.Current.FirstLinePtr()
|
||||
if err != nil {
|
||||
setErrorMsg(fmt.Sprintf("Error moving to first line: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
case "End":
|
||||
lastPtr, err := logViewRef.Current.LastLinePtr(currentLinePtr.Current)
|
||||
if err != nil {
|
||||
setErrorMsg(fmt.Sprintf("Error moving to last line: %v", err))
|
||||
return
|
||||
}
|
||||
if lastPtr == nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, backPtr, err := logViewRef.Current.Move(lastPtr, -int(*windowSize-1))
|
||||
if err != nil {
|
||||
setErrorMsg(fmt.Sprintf("Error adjusting view: %v", err))
|
||||
return
|
||||
}
|
||||
if backPtr != nil {
|
||||
newPtr = backPtr
|
||||
} else {
|
||||
newPtr, _ = logViewRef.Current.FirstLinePtr()
|
||||
}
|
||||
|
||||
case "ArrowUp":
|
||||
if currentLinePtr.Current == nil {
|
||||
newPtr, _ = logViewRef.Current.FirstLinePtr()
|
||||
} else {
|
||||
_, newPtr, _ = logViewRef.Current.Move(currentLinePtr.Current, -1)
|
||||
}
|
||||
|
||||
case "ArrowDown":
|
||||
if currentLinePtr.Current == nil {
|
||||
newPtr, _ = logViewRef.Current.FirstLinePtr()
|
||||
} else {
|
||||
_, newPtr, _ = logViewRef.Current.Move(currentLinePtr.Current, 1)
|
||||
}
|
||||
|
||||
case "PageUp":
|
||||
if currentLinePtr.Current == nil {
|
||||
newPtr, _ = logViewRef.Current.FirstLinePtr()
|
||||
} else if currentLinePtr.Current.LineNum < *windowSize {
|
||||
newPtr, _ = logViewRef.Current.FirstLinePtr()
|
||||
} else {
|
||||
_, newPtr, _ = logViewRef.Current.Move(currentLinePtr.Current, -int(*windowSize))
|
||||
}
|
||||
|
||||
case "PageDown":
|
||||
if currentLinePtr.Current == nil {
|
||||
newPtr, _ = logViewRef.Current.FirstLinePtr()
|
||||
} else {
|
||||
_, newPtr, _ = logViewRef.Current.Move(currentLinePtr.Current, int(*windowSize))
|
||||
}
|
||||
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
if newPtr == nil {
|
||||
return
|
||||
}
|
||||
|
||||
currentLinePtr.Current = newPtr
|
||||
|
||||
// Read new window of lines
|
||||
newLines, err := logViewRef.Current.ReadWindow(newPtr, int(*windowSize))
|
||||
if err != nil {
|
||||
setErrorMsg(fmt.Sprintf("Error reading lines: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
setLines(newLines)
|
||||
setCurrentLineNum(newPtr.LineNum)
|
||||
client.SendAsyncInitiation()
|
||||
})
|
||||
|
||||
// Get first line pointer
|
||||
linePtr, err := lv.FirstLinePtr()
|
||||
if err != nil {
|
||||
setErrorMsg(fmt.Sprintf("Error getting first line: %v", err))
|
||||
return nil
|
||||
}
|
||||
|
||||
if linePtr == nil {
|
||||
setLines([][]byte{})
|
||||
return nil
|
||||
}
|
||||
|
||||
currentLinePtr.Current = linePtr
|
||||
setCurrentLineNum(linePtr.LineNum)
|
||||
|
||||
// Read initial window of lines
|
||||
initialLines, err := lv.ReadWindow(linePtr, int(*windowSize))
|
||||
if err != nil {
|
||||
setErrorMsg(fmt.Sprintf("Error reading lines: %v", err))
|
||||
return nil
|
||||
}
|
||||
|
||||
setLines(initialLines)
|
||||
|
||||
// Cleanup function
|
||||
return func() {
|
||||
lv.Close()
|
||||
file.Close()
|
||||
}
|
||||
}, []any{})
|
||||
|
||||
return vdom.H("div", map[string]any{
|
||||
"className": "log-viewer",
|
||||
},
|
||||
vdom.H("div", map[string]any{
|
||||
"className": "log-header",
|
||||
},
|
||||
vdom.H("h1", nil, "Log Viewer"),
|
||||
vdom.H("div", map[string]any{
|
||||
"className": "log-info",
|
||||
},
|
||||
"Showing ", *windowSize, " lines starting at line ", currentLineNum,
|
||||
" of ", logFilePath),
|
||||
FilterInput(FilterInputProps{
|
||||
Value: filterText,
|
||||
OnChange: handleFilterChange,
|
||||
}),
|
||||
),
|
||||
LogContent(LogContentProps{
|
||||
Lines: lines,
|
||||
Error: errorMsg,
|
||||
LineStart: currentLineNum,
|
||||
}),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
func main() {
|
||||
AppClient.RegisterDefaultFlags()
|
||||
flag.Parse()
|
||||
|
||||
if flag.NArg() != 1 {
|
||||
fmt.Fprintf(os.Stderr, "Usage: logviewer [flags] <logfile>\n")
|
||||
flag.PrintDefaults()
|
||||
os.Exit(1)
|
||||
}
|
||||
logFilePath = flag.Arg(0)
|
||||
|
||||
AppClient.RunMain()
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
.log-viewer {
|
||||
padding: 1rem;
|
||||
font-family: monospace;
|
||||
height: calc(100vh - 2rem);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.log-header {
|
||||
margin-bottom: 1rem;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.log-info {
|
||||
color: #666;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.filter-container {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.filter-input {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
font-family: monospace;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid #666;
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.filter-input::placeholder {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.filter-input:focus {
|
||||
outline: none;
|
||||
border-color: #888;
|
||||
}
|
||||
|
||||
.log-content {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
margin: 0;
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.log-line {
|
||||
padding: 0.125rem 0;
|
||||
}
|
||||
|
||||
.log-line:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.line-number {
|
||||
color: #666;
|
||||
padding-right: 1rem;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.line-content {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.log-error {
|
||||
color: #ff4444;
|
||||
padding: 1rem;
|
||||
background: rgba(255, 0, 0, 0.1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
@@ -224,6 +224,8 @@ var AppClient *waveapp.Client = waveapp.MakeClient(waveapp.AppOpts{
|
||||
})
|
||||
```
|
||||
|
||||
Note that the default environment where these apps will launch is a dark mode terminal. So try to pick text colors and background colors that will work well against a dark client. Normally the background is transparent (to a dark-ish background). So either try to leave the background transparent (untouched), or if the transparency is not desirable, then choose a dark background color that would be appropriate.
|
||||
|
||||
## Component Definition Pattern
|
||||
|
||||
Create typed, reusable components using the client:
|
||||
|
||||
Reference in New Issue
Block a user