Web edit sources
This commit is contained in:
parent
7361fd4600
commit
bd5737ad7a
@ -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
|
||||||
|
34
core/env.go
34
core/env.go
@ -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 {
|
||||||
|
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>
|
<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>
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -98,3 +98,6 @@ article textarea {
|
|||||||
span.error-message {
|
span.error-message {
|
||||||
color: red;
|
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("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
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)
|
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