Web edit sources
This commit is contained in:
parent
7361fd4600
commit
bd5737ad7a
@ -110,11 +110,11 @@ Parity features
|
||||
* [x] crontab integration
|
||||
* [ ] source batching
|
||||
* [x] add item from web
|
||||
* [ ] web edit channels
|
||||
* [x] web edit channels
|
||||
* web edit sources
|
||||
* [ ] edit action argv
|
||||
* [ ] edit source envs
|
||||
* [ ] edit source crontab
|
||||
* [x] edit action argv
|
||||
* [x] edit source envs
|
||||
* [x] edit source crontab
|
||||
* [x] Nix build
|
||||
* [ ] NixOS module
|
||||
* [ ] NixOS vm demo
|
||||
|
34
core/env.go
34
core/env.go
@ -5,7 +5,7 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func GetEnvs(db DB, source string) ([]string, error) {
|
||||
func GetEnvs(db DB, source string) (envs []string, err error) {
|
||||
rows, err := db.Query(`
|
||||
select name, value
|
||||
from envs
|
||||
@ -15,19 +15,43 @@ func GetEnvs(db DB, source string) ([]string, error) {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var envs []string
|
||||
for rows.Next() {
|
||||
var name string
|
||||
var value string
|
||||
if err := rows.Scan(&name, &value); err != nil {
|
||||
if err = rows.Scan(&name, &value); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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 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 {
|
||||
|
@ -19,7 +19,7 @@
|
||||
<label for="channel">to</label>
|
||||
<input type="text" name="channel" list="channel-options">
|
||||
<button
|
||||
hx-post="/channel/"
|
||||
hx-post="/channel/"
|
||||
>Submit</button>
|
||||
</form>
|
||||
</p>
|
||||
|
54
web/html/editSource.html
Normal file
54
web/html/editSource.html
Normal 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 }}
|
@ -34,6 +34,9 @@
|
||||
<button type="submit">fetch</button>
|
||||
</form>
|
||||
</td>
|
||||
<td>
|
||||
<a href="/source/{{ .Name }}/edit">(edit)</a>
|
||||
</td>
|
||||
</td>
|
||||
<td><a href="/source/{{ .Name }}">{{ .Name }}</a></td>
|
||||
</tr>
|
||||
|
@ -182,3 +182,17 @@ func EditChannels(writer io.Writer, data EditChannelsData) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -98,3 +98,6 @@ article textarea {
|
||||
span.error-message {
|
||||
color: red;
|
||||
}
|
||||
#envvars input {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
@ -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("POST /login", env.login, 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("GET /channel/", env.getChannels, env.authed, logged)
|
||||
handleFunc("POST /channel/", env.editChannel, env.authed, logged)
|
||||
|
44
web/shlex.go
Normal file
44
web/shlex.go
Normal 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
60
web/shlex_test.go
Normal 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"}'`,
|
||||
)
|
||||
}
|
@ -78,3 +78,80 @@ func (env *Env) fetchSource(writer http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user