Web edit sources

This commit is contained in:
Tim Van Baak 2025-02-12 10:04:58 -08:00
parent 7361fd4600
commit bd5737ad7a
11 changed files with 291 additions and 10 deletions

View File

@ -110,11 +110,11 @@ Parity features
* [x] crontab integration * [x] crontab integration
* [ ] source batching * [ ] source batching
* [x] add item from web * [x] add item from web
* [ ] web edit channels * [x] web edit channels
* web edit sources * web edit sources
* [ ] edit action argv * [x] edit action argv
* [ ] edit source envs * [x] edit source envs
* [ ] edit source crontab * [x] edit source crontab
* [x] Nix build * [x] Nix build
* [ ] NixOS module * [ ] NixOS module
* [ ] NixOS vm demo * [ ] NixOS vm demo

View File

@ -5,7 +5,7 @@ import (
"strings" "strings"
) )
func GetEnvs(db DB, source string) ([]string, error) { func GetEnvs(db DB, source string) (envs []string, err error) {
rows, err := db.Query(` rows, err := db.Query(`
select name, value select name, value
from envs from envs
@ -15,19 +15,43 @@ func GetEnvs(db DB, source string) ([]string, error) {
return nil, err return nil, err
} }
defer rows.Close() defer rows.Close()
var envs []string
for rows.Next() { for rows.Next() {
var name string var name string
var value string var value string
if err := rows.Scan(&name, &value); err != nil { if err = rows.Scan(&name, &value); err != nil {
return nil, err return nil, err
} }
envs = append(envs, fmt.Sprintf("%s=%s", name, value)) envs = append(envs, fmt.Sprintf("%s=%s", name, value))
} }
if err := rows.Err(); err != nil { if err = rows.Err(); err != nil {
return nil, err return nil, err
} }
return envs, nil return
}
func GetEnvsMap(db DB, source string) (envs map[string]string, err error) {
rows, err := db.Query(`
select name, value
from envs
where source = ?
`, source)
if err != nil {
return nil, err
}
defer rows.Close()
envs = make(map[string]string)
for rows.Next() {
var name string
var value string
if err = rows.Scan(&name, &value); err != nil {
return nil, err
}
envs[name] = value
}
if err = rows.Err(); err != nil {
return nil, err
}
return
} }
func SetEnvs(db DB, source string, envs []string) error { func SetEnvs(db DB, source string, envs []string) error {

View File

@ -19,7 +19,7 @@
<label for="channel">to</label> <label for="channel">to</label>
<input type="text" name="channel" list="channel-options"> <input type="text" name="channel" list="channel-options">
<button <button
hx-post="/channel/" hx-post="/channel/"
>Submit</button> >Submit</button>
</form> </form>
</p> </p>

54
web/html/editSource.html Normal file
View File

@ -0,0 +1,54 @@
{{ define "title" }}{{ .Name }} - Intake{{ end }}
{{ define "content" -}}
<nav class="center">
<span class="feed-controls">
<a href="/">Home</a>
</span>
</nav>
<nav>
<span class="feed-controls">Edit source {{ .Name }}</span>
<p>
<form method="post">
<label for="name">Name:</label>
<input type="text" name="name" placeholder="Source name" value="{{ .Name }}" readonly>
<input type="submit" value="Update" disabled>
</form>
</p>
<p>Environment:</p>
<table id="envvars">{{ range $name, $value := .Envs }}
<tr>
<td><input type="text" value="{{ $name }}" disabled></td>
<td><input type="text" value="{{ $value }}" disabled></td>
</tr>{{ end }}
<tr>
<td><input type="text" form="env" name="envName"></td>
<td><input type="text" form="env" name="envValue"></td>
<td><form id="env"><button
type="button"
hx-post="/source/{{ .Name }}/edit"
>Update</button></form></td>
</form>
</tr>
</table>
<p>Actions:</p>
<table id="actions">{{ range $name, $argv := .Actions }}
<tr>
<td><input type="text" value="{{ $name }}" disabled></td>
<td><input type="text" value="{{ $argv }}" disabled></td>
</tr>{{ end }}
<tr>
<td><input type="text" form="action" name="actionName"></td>
<td><input type="text" form="action" name="actionArgv"></td>
<td><form id="action"><button
type="button"
hx-post="/source/{{ .Name }}/edit"
>Update</button></form></td>
</form>
</tr>
</table>
</nav>
{{- end }}

View File

@ -34,6 +34,9 @@
<button type="submit">fetch</button> <button type="submit">fetch</button>
</form> </form>
</td> </td>
<td>
<a href="/source/{{ .Name }}/edit">(edit)</a>
</td>
</td> </td>
<td><a href="/source/{{ .Name }}">{{ .Name }}</a></td> <td><a href="/source/{{ .Name }}">{{ .Name }}</a></td>
</tr> </tr>

View File

@ -182,3 +182,17 @@ func EditChannels(writer io.Writer, data EditChannelsData) {
log.Printf("error: failed to render edit channels: %v", err) log.Printf("error: failed to render edit channels: %v", err)
} }
} }
var editSource = load("editSource.html")
type EditSourceData struct {
Name string
Envs map[string]string
Actions map[string]string
}
func EditSource(writer io.Writer, data EditSourceData) {
if err := editSource.Execute(writer, data); err != nil {
log.Printf("error: failed to render edit source: %v", err)
}
}

View File

@ -98,3 +98,6 @@ article textarea {
span.error-message { span.error-message {
color: red; color: red;
} }
#envvars input {
font-family: monospace;
}

View File

@ -40,6 +40,8 @@ func RunServer(db core.DB, addr string, port string) {
handleFunc("GET /htmx.org@2.0.4.js", env.getScript, logged) handleFunc("GET /htmx.org@2.0.4.js", env.getScript, logged)
handleFunc("POST /login", env.login, logged) handleFunc("POST /login", env.login, logged)
handleFunc("GET /source/{source}", env.getSource, env.authed, logged) handleFunc("GET /source/{source}", env.getSource, env.authed, logged)
handleFunc("GET /source/{source}/edit", env.getEditSource, env.authed, logged)
handleFunc("POST /source/{source}/edit", env.editSource, env.authed, logged)
handleFunc("POST /source/{source}/fetch", env.fetchSource, env.authed, logged) handleFunc("POST /source/{source}/fetch", env.fetchSource, env.authed, logged)
handleFunc("GET /channel/", env.getChannels, env.authed, logged) handleFunc("GET /channel/", env.getChannels, env.authed, logged)
handleFunc("POST /channel/", env.editChannel, env.authed, logged) handleFunc("POST /channel/", env.editChannel, env.authed, logged)

44
web/shlex.go Normal file
View File

@ -0,0 +1,44 @@
package web
import (
"io"
"os/exec"
"strings"
)
func Shlex(cmd string) (argv []string, err error) {
xargs := exec.Command("xargs", "-n1")
stdin, err := xargs.StdinPipe()
if err != nil {
return
}
_, err = io.WriteString(stdin, cmd)
if err != nil {
return
}
err = stdin.Close()
if err != nil {
return
}
output, err := xargs.Output()
if err != nil {
return
}
argv = strings.Split(string(output), "\n")
if argv[len(argv)-1] == "" {
argv = argv[:len(argv)-1]
}
return
}
func Quote(argv []string) (cmd string, err error) {
argv = append([]string{"%q "}, argv...)
printf := exec.Command("printf", argv...)
output, err := printf.Output()
if err != nil {
return
}
cmd = string(output)
cmd = cmd[:len(cmd)-1] // strip off the last space from '%q '
return
}

60
web/shlex_test.go Normal file
View File

@ -0,0 +1,60 @@
package web
import "testing"
func TestShlex(t *testing.T) {
shlex := func(t *testing.T, cmd string, expected ...string) {
t.Helper()
actual, err := Shlex(cmd)
if err != nil {
t.Fatalf("shlex failed: %v", err)
}
if len(actual) != len(expected) {
t.Fatalf("expected %d args, got %d\nexpect: %v\nactual: %v", len(expected), len(actual), expected, actual)
}
for i := range len(expected) {
if actual[i] != expected[i] {
t.Fatalf("arg %d incorrect: expected %s got %s", i, expected[i], actual[i])
}
}
}
quote := func(t *testing.T, argvAndExpected ...string) {
t.Helper()
argv := argvAndExpected[:len(argvAndExpected)-1]
expected := argvAndExpected[len(argvAndExpected)-1]
actual, err := Quote(argv)
if err != nil {
t.Fatalf("quote failed: %v", err)
}
if actual != expected {
t.Fatalf("expected %q, got %q", expected, actual)
}
}
shlex(t,
`Lord "have mercy"`,
"Lord", "have mercy",
)
shlex(t,
`jq -cR '{id: .}'`,
"jq", "-cR", `{id: .}`,
)
shlex(t,
`jq -cR '{id: "hello"}'`,
"jq", "-cR", `{id: "hello"}`,
)
quote(t,
"Lord", "have mercy",
`Lord 'have mercy'`,
)
quote(t,
"jq", "-cR", `{id: .}`,
`jq -cR '{id: .}'`,
)
quote(t,
"jq", "-cR", `{id: "hello"}`,
`jq -cR '{id: "hello"}'`,
)
}

View File

@ -78,3 +78,80 @@ func (env *Env) fetchSource(writer http.ResponseWriter, req *http.Request) {
} }
html.Fetch(writer, data) html.Fetch(writer, data)
} }
func (env *Env) getEditSource(writer http.ResponseWriter, req *http.Request) {
source := req.PathValue("source")
if exists, err := core.SourceExists(env.db, source); !exists || err != nil {
http.NotFound(writer, req)
return
}
envs, err := core.GetEnvsMap(env.db, source)
if err != nil {
http.Error(writer, err.Error(), 500)
return
}
actions, _ := core.GetActionsForSource(env.db, source)
argvs := make(map[string]string)
for _, action := range actions {
argv, _ := core.GetArgvForAction(env.db, source, action)
args, _ := Quote(argv)
argvs[action] = args
}
data := html.EditSourceData{
Name: source,
Envs: envs,
Actions: argvs,
}
html.EditSource(writer, data)
}
func (env *Env) editSource(writer http.ResponseWriter, req *http.Request) {
source := req.PathValue("source")
if exists, err := core.SourceExists(env.db, source); !exists || err != nil {
http.NotFound(writer, req)
return
}
if err := req.ParseForm(); err != nil {
http.Error(writer, err.Error(), 400)
return
}
envName := req.PostForm.Get("envName")
envValue := req.PostForm.Get("envValue")
if envName != "" {
log.Printf("setting %s=%s", envName, envValue)
if err := core.SetEnvs(env.db, source, []string{fmt.Sprintf("%s=%s", envName, envValue)}); err != nil {
http.Error(writer, err.Error(), 500)
return
}
}
actionName := req.PostForm.Get("actionName")
actionArgv := req.PostForm.Get("actionArgv")
if actionName != "" {
log.Printf("setting %s -- %s", actionName, actionArgv)
if actionArgv == "" {
if err := core.DeleteAction(env.db, source, actionName); err != nil {
http.Error(writer, err.Error(), 500)
return
}
} else {
argv, err := Shlex(actionArgv)
if err != nil {
http.Error(writer, err.Error(), 400)
return
}
if err = core.SetAction(env.db, source, actionName, argv); err != nil {
http.Error(writer, err.Error(), 500)
return
}
}
}
writer.Header()["HX-Refresh"] = []string{"true"}
writer.WriteHeader(http.StatusNoContent)
}