Add initail version of a web UI for querying history from the browser

This commit is contained in:
David Dworken 2024-02-17 14:41:37 -08:00
parent 86833a6109
commit 2aecffd912
No known key found for this signature in database
6 changed files with 221 additions and 0 deletions

4
.gitignore vendored
View File

@ -5,3 +5,7 @@ postgres-data/
server
!backend/server
.DS_Store
node_modules/
package.json
package-lock.json
.prettierrc

23
client/cmd/webui.go Normal file
View File

@ -0,0 +1,23 @@
package cmd
import (
"os"
"github.com/ddworken/hishtory/client/hctx"
"github.com/ddworken/hishtory/client/lib"
"github.com/ddworken/hishtory/client/webui"
"github.com/spf13/cobra"
)
var webUiCmd = &cobra.Command{
Use: "web-ui",
Short: "Serve a basic web UI for interacting with your shell history",
Run: func(cmd *cobra.Command, args []string) {
lib.CheckFatalError(webui.StartWebUiServer(hctx.MakeContext()))
os.Exit(1)
},
}
func init() {
rootCmd.AddCommand(webUiCmd)
}

View File

@ -0,0 +1,76 @@
<div class="p-3 mb-2 bg-secondary text-white">
<div class="jumbotron jumbotron-fluid">
<div class="container">
<h1 class="display-4">hiSHtory</h1>
<p class="lead">Your shell history in context, synced, and queryable</p>
</div>
</div>
</div>
<nav class="navbar navbar-light bg-light">
<form class="form-inline my-2 my-lg-0 w-100" style="display:flex">
<input
type="search"
name="q"
id="search-input"
autocomplete="off"
placeholder="Search Query"
value="{{ .SearchQuery }}"
class="form-control mr-sm-2"
hx-get="/htmx/results-table"
hx-params="*"
hx-trigger="input changed delay:500ms, search"
hx-target="#search-results"
/>
<button
id="search-button"
class="btn btn-outline-success my-2 my-sm-0 btn-light"
type="submit"
>
Search
</button>
</form>
</nav>
<hr />
{{ block "resultsTable.html" . }}
<div id="search-results" class="table-responsive">
<table class="table">
<thead>
<tr class="table-info">
{{ range .ColumnNames }}
<th scope="col">{{ . }}</th>
{{ end }}
</tr>
</thead>
<tbody>
{{ range .SearchResults }}
<tr class="table-light">
{{ range . }}
<td>{{ . }}</td>
{{ end }}
</tr>
{{ end }}
</tbody>
</table>
</div>
{{ end }}
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN"
crossorigin="anonymous"
/>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
crossorigin="anonymous"
></script>
<script
src="https://cdn.jsdelivr.net/npm/htmx.org@1.9.10/dist/htmx.min.js"
integrity="sha256-s73PXHQYl6U2SLEgf/8EaaDWGQFCm6H26I+Y69hOZp4="
crossorigin="anonymous"
></script>

115
client/webui/webui.go Normal file
View File

@ -0,0 +1,115 @@
package webui
import (
"context"
"embed"
"fmt"
"net"
"net/http"
"net/url"
"html/template"
"github.com/ddworken/hishtory/client/data"
"github.com/ddworken/hishtory/client/hctx"
"github.com/ddworken/hishtory/client/lib"
)
//go:embed templates
var templateFiles embed.FS
type webUiData struct {
SearchQuery string
SearchResults [][]string
ColumnNames []string
}
func getTableRowsForDisplay(ctx context.Context, searchQuery string) ([][]string, error) {
results, err := lib.Search(ctx, hctx.GetDb(ctx), searchQuery, 100)
if err != nil {
panic(err)
}
return buildTableRows(ctx, results)
}
func htmx_resultsTable(w http.ResponseWriter, r *http.Request) {
searchQuery := r.URL.Query().Get("q")
tableRows, err := getTableRowsForDisplay(r.Context(), searchQuery)
if err != nil {
panic(err)
}
w.Header().Add("Content-Type", "text/html")
w.Header().Add("HX-Replace-Url", getNewUrl(r, searchQuery))
err = getTemplates().ExecuteTemplate(w, "resultsTable.html", webUiData{
SearchQuery: searchQuery,
SearchResults: tableRows,
ColumnNames: hctx.GetConf(r.Context()).DisplayedColumns,
})
if err != nil {
panic(err)
}
}
func getNewUrl(r *http.Request, searchQuery string) string {
urlStr := r.Header.Get("Hx-Current-Url")
if urlStr == "" {
// In this function we purposefully want to silence any errors since updating the URL is non-critical, so
// we always return an empty string rather than handling the error.
return ""
}
url, err := url.Parse(urlStr)
if err != nil {
return ""
}
q := url.Query()
q.Set("q", searchQuery)
url.RawQuery = q.Encode()
return url.String()
}
func webuiHandler(w http.ResponseWriter, r *http.Request) {
searchQuery := r.URL.Query().Get("q")
tableRows, err := getTableRowsForDisplay(r.Context(), searchQuery)
if err != nil {
panic(err)
}
w.Header().Add("Content-Type", "text/html")
err = getTemplates().ExecuteTemplate(w, "webui.html", webUiData{
SearchQuery: searchQuery,
SearchResults: tableRows,
ColumnNames: hctx.GetConf(r.Context()).DisplayedColumns,
})
if err != nil {
panic(err)
}
}
func getTemplates() *template.Template {
return template.Must(template.ParseFS(templateFiles, "templates/*"))
}
func buildTableRows(ctx context.Context, entries []*data.HistoryEntry) ([][]string, error) {
columnNames := hctx.GetConf(ctx).DisplayedColumns
ret := make([][]string, 0)
for _, entry := range entries {
row, err := lib.BuildTableRow(ctx, columnNames, *entry, func(s string) string { return s })
if err != nil {
return nil, err
}
ret = append(ret, row)
}
return ret, nil
}
func StartWebUiServer(ctx context.Context) error {
http.HandleFunc("/", webuiHandler)
http.HandleFunc("/htmx/results-table", htmx_resultsTable)
server := http.Server{
BaseContext: func(l net.Listener) context.Context { return ctx },
Addr: ":8080",
}
fmt.Println("Starting web server...")
return server.ListenAndServe()
}

1
go.mod
View File

@ -148,6 +148,7 @@ require (
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect
github.com/google/safehtml v0.1.0 // indirect
github.com/google/trillian v1.5.0 // indirect
github.com/googleapis/gnostic v0.5.5 // indirect
github.com/gorilla/websocket v1.4.2 // indirect

2
go.sum
View File

@ -766,6 +766,8 @@ github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/rpmpack v0.0.0-20191226140753-aa36bfddb3a0/go.mod h1:RaTPr0KUf2K7fnZYLNDrr8rxAamWs3iNywJLtQ2AzBg=
github.com/google/safehtml v0.1.0 h1:EwLKo8qawTKfsi0orxcQAZzu07cICaBeFMegAU9eaT8=
github.com/google/safehtml v0.1.0/go.mod h1:L4KWwDsUJdECRAEpZoBn3O64bQaywRscowZjJAzjHnU=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/google/trillian v1.3.14-0.20210409160123-c5ea3abd4a41/go.mod h1:1dPv0CUjNQVFEDuAUFhZql16pw/VlPgaX8qj+g5pVzQ=