WIP delete works too now
This commit is contained in:
parent
9db30c7e80
commit
d87b197cc0
14
again.go
14
again.go
|
@ -208,21 +208,21 @@ func IsAmbiguous(st []int, tzstr string) error {
|
||||||
if nil != err {
|
if nil != err {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
m := time.Month(st[1])
|
m := time.Month(st[1])
|
||||||
t1 := time.Date(st[0], m, st[2], st[3], st[4], st[5], st[6], tz)
|
t1 := time.Date(st[0], m, st[2], st[3], st[4], st[5], st[6], tz)
|
||||||
u1 := t1.UTC()
|
u1 := t1.UTC()
|
||||||
// A better way to do this would probably be to parse the timezone database, but... yeah...
|
// Australia/Lord_Howe has a 30-minute DST
|
||||||
for _, n := range []int{ /*-120, -60,*/ 30, 60, 120} {
|
// 60-minute DST is common
|
||||||
|
// Antarctica/Troll has a 120-minute DST
|
||||||
|
for _, n := range []int{30, 60, 120} {
|
||||||
t2 := time.Date(st[0], m, st[2], st[3], st[4]+n, st[5], st[6], tz)
|
t2 := time.Date(st[0], m, st[2], st[3], st[4]+n, st[5], st[6], tz)
|
||||||
u2 := t2.UTC()
|
u2 := t2.UTC()
|
||||||
if u1.Equal(u2) {
|
if u1.Equal(u2) {
|
||||||
fmt.Println("Ambiguous Time")
|
return fmt.Errorf("Ambiguous: %s, %s, %+d\n", t1, t2, n)
|
||||||
fmt.Printf("%s, %s, %+d\n", t1, u1, n)
|
|
||||||
fmt.Printf("%s, %s, %+d\n", t2, u2, n)
|
|
||||||
return fmt.Errorf("Ambiguous")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//ta :=
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -77,6 +77,7 @@ func main() {
|
||||||
}
|
}
|
||||||
//mux.Handle("/api/", http.HandlerFunc(handleFunc))
|
//mux.Handle("/api/", http.HandlerFunc(handleFunc))
|
||||||
mux.HandleFunc("/api/v0/schedules", s.Handle)
|
mux.HandleFunc("/api/v0/schedules", s.Handle)
|
||||||
|
mux.HandleFunc("/api/v0/schedules/", s.Handle)
|
||||||
|
|
||||||
// TODO Filebox FS
|
// TODO Filebox FS
|
||||||
mux.Handle("/", http.FileServer(http.Dir("./public")))
|
mux.Handle("/", http.FileServer(http.Dir("./public")))
|
||||||
|
@ -88,6 +89,7 @@ func main() {
|
||||||
type ScheduleDB interface {
|
type ScheduleDB interface {
|
||||||
List(string) ([]*again.Schedule, error)
|
List(string) ([]*again.Schedule, error)
|
||||||
Set(again.Schedule) (*again.Schedule, error)
|
Set(again.Schedule) (*again.Schedule, error)
|
||||||
|
Delete(accessID string, id string) (*again.Schedule, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type scheduler struct {
|
type scheduler struct {
|
||||||
|
@ -107,7 +109,6 @@ func (s *scheduler) Handle(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx = context.WithValue(ctx, "token", token)
|
ctx = context.WithValue(ctx, "token", token)
|
||||||
r = r.WithContext(ctx)
|
r = r.WithContext(ctx)
|
||||||
|
|
||||||
fmt.Println("whatever", r.Method, r.URL)
|
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
s.List(w, r)
|
s.List(w, r)
|
||||||
|
@ -115,6 +116,9 @@ func (s *scheduler) Handle(w http.ResponseWriter, r *http.Request) {
|
||||||
case http.MethodPost:
|
case http.MethodPost:
|
||||||
s.Create(w, r)
|
s.Create(w, r)
|
||||||
return
|
return
|
||||||
|
case http.MethodDelete:
|
||||||
|
s.Delete(w, r)
|
||||||
|
return
|
||||||
default:
|
default:
|
||||||
http.Error(w, "Not Implemented", http.StatusNotImplemented)
|
http.Error(w, "Not Implemented", http.StatusNotImplemented)
|
||||||
return
|
return
|
||||||
|
@ -144,10 +148,8 @@ func (s *scheduler) Create(w http.ResponseWriter, r *http.Request) {
|
||||||
br, bw := io.Pipe()
|
br, bw := io.Pipe()
|
||||||
b := io.TeeReader(r.Body, bw)
|
b := io.TeeReader(r.Body, bw)
|
||||||
go func() {
|
go func() {
|
||||||
fmt.Println("reading from reader...")
|
|
||||||
x, _ := ioutil.ReadAll(b)
|
x, _ := ioutil.ReadAll(b)
|
||||||
fmt.Println("cool beans and all")
|
fmt.Println("[debug] http body", string(x))
|
||||||
fmt.Println(string(x))
|
|
||||||
bw.Close()
|
bw.Close()
|
||||||
}()
|
}()
|
||||||
decoder := json.NewDecoder(br)
|
decoder := json.NewDecoder(br)
|
||||||
|
@ -176,3 +178,29 @@ func (s *scheduler) Create(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
w.Write(buf)
|
w.Write(buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *scheduler) Delete(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// TODO validate user
|
||||||
|
accessID := r.Context().Value("token").(string)
|
||||||
|
parts := strings.Split(r.URL.Path, "/")
|
||||||
|
|
||||||
|
// ""/"api"/"v0"/"schedules"/":id"
|
||||||
|
if 5 != len(parts) {
|
||||||
|
http.Error(w, "Not Found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id := parts[4]
|
||||||
|
sched2, err := s.DB.Delete(accessID, id)
|
||||||
|
if nil != err {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := json.Marshal(sched2)
|
||||||
|
if nil != err {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Write(buf)
|
||||||
|
}
|
||||||
|
|
|
@ -1,3 +1,9 @@
|
||||||
|
// A JSON storage strategy for Go Again
|
||||||
|
// SQL would be a better choice, but... meh
|
||||||
|
//
|
||||||
|
// Note that we use mutexes instead of channels
|
||||||
|
// because everything is both synchronous and
|
||||||
|
// sequential. Meh.
|
||||||
package jsondb
|
package jsondb
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -8,16 +14,20 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
again "git.rootprojects.org/root/go-again"
|
"git.rootprojects.org/root/go-again"
|
||||||
)
|
)
|
||||||
|
|
||||||
type JSONDB struct {
|
type JSONDB struct {
|
||||||
dburl string
|
dburl string
|
||||||
path string
|
path string
|
||||||
json *dbjson
|
json *dbjson
|
||||||
|
mux sync.Mutex
|
||||||
|
fmux sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
type dbjson struct {
|
type dbjson struct {
|
||||||
|
@ -31,13 +41,9 @@ func Connect(dburl string) (*JSONDB, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// json:/abspath/to/db.json
|
// json:/abspath/to/db.json
|
||||||
fmt.Println("url.Opaque:", u.Opaque)
|
|
||||||
// json:///abspath/to/db.json
|
|
||||||
fmt.Println("url.Path:", u.Path)
|
|
||||||
fmt.Println(u)
|
|
||||||
|
|
||||||
path := u.Opaque
|
path := u.Opaque
|
||||||
if "" == path {
|
if "" == path {
|
||||||
|
// json:///abspath/to/db.json
|
||||||
path = u.Path
|
path = u.Path
|
||||||
if "" == path {
|
if "" == path {
|
||||||
// json:relpath/to/db.json
|
// json:relpath/to/db.json
|
||||||
|
@ -73,10 +79,14 @@ func Connect(dburl string) (*JSONDB, error) {
|
||||||
return nil, fmt.Errorf("Couldn't parse %q as JSON: %s", path, err)
|
return nil, fmt.Errorf("Couldn't parse %q as JSON: %s", path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
wd, _ := os.Getwd()
|
||||||
|
fmt.Println("jsondb:", filepath.Join(wd, path))
|
||||||
return &JSONDB{
|
return &JSONDB{
|
||||||
dburl: dburl,
|
dburl: dburl,
|
||||||
path: path,
|
path: path,
|
||||||
json: db,
|
json: db,
|
||||||
|
mux: sync.Mutex{},
|
||||||
|
fmux: sync.Mutex{},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,10 +102,6 @@ type Schedule struct {
|
||||||
Webhooks []again.Webhook `json:"webhooks" db"webhooks"`
|
Webhooks []again.Webhook `json:"webhooks" db"webhooks"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func ctcmp(x string, y string) bool {
|
|
||||||
return 1 == subtle.ConstantTimeCompare([]byte(x), []byte(y))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *JSONDB) List(accessID string) ([]*again.Schedule, error) {
|
func (db *JSONDB) List(accessID string) ([]*again.Schedule, error) {
|
||||||
schedules := []*again.Schedule{}
|
schedules := []*again.Schedule{}
|
||||||
for i := range db.json.Schedules {
|
for i := range db.json.Schedules {
|
||||||
|
@ -115,30 +121,11 @@ func (db *JSONDB) List(accessID string) ([]*again.Schedule, error) {
|
||||||
return schedules, nil
|
return schedules, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *JSONDB) get(id string) (int, *Schedule) {
|
|
||||||
for i := range db.json.Schedules {
|
|
||||||
schedule := db.json.Schedules[i]
|
|
||||||
if ctcmp(id, schedule.ID) {
|
|
||||||
return i, &schedule
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func genID() (string, error) {
|
|
||||||
b := make([]byte, 16)
|
|
||||||
_, err := rand.Read(b)
|
|
||||||
if nil != err {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return hex.EncodeToString(b), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *JSONDB) Set(s again.Schedule) (*again.Schedule, error) {
|
func (db *JSONDB) Set(s again.Schedule) (*again.Schedule, error) {
|
||||||
exists := false
|
exists := false
|
||||||
index := -1
|
index := -1
|
||||||
if "" == s.ID {
|
if "" == s.ID {
|
||||||
id, err := genID()
|
id, err := genID(16)
|
||||||
if nil != err {
|
if nil != err {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -147,6 +134,7 @@ func (db *JSONDB) Set(s again.Schedule) (*again.Schedule, error) {
|
||||||
i, old := db.get(s.ID)
|
i, old := db.get(s.ID)
|
||||||
index = i
|
index = i
|
||||||
exists = nil != old
|
exists = nil != old
|
||||||
|
// TODO constant time bail
|
||||||
if !exists || !ctcmp(old.AccessID, s.AccessID) {
|
if !exists || !ctcmp(old.AccessID, s.AccessID) {
|
||||||
return nil, fmt.Errorf("invalid id")
|
return nil, fmt.Errorf("invalid id")
|
||||||
}
|
}
|
||||||
|
@ -163,9 +151,13 @@ func (db *JSONDB) Set(s again.Schedule) (*again.Schedule, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if exists {
|
if exists {
|
||||||
|
db.mux.Lock()
|
||||||
db.json.Schedules[index] = schedule
|
db.json.Schedules[index] = schedule
|
||||||
|
db.mux.Unlock()
|
||||||
} else {
|
} else {
|
||||||
|
db.mux.Lock()
|
||||||
db.json.Schedules = append(db.json.Schedules, schedule)
|
db.json.Schedules = append(db.json.Schedules, schedule)
|
||||||
|
db.mux.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
err := db.save(s.AccessID)
|
err := db.save(s.AccessID)
|
||||||
|
@ -176,9 +168,73 @@ func (db *JSONDB) Set(s again.Schedule) (*again.Schedule, error) {
|
||||||
return &s, nil
|
return &s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (db *JSONDB) Delete(accessID string, id string) (*again.Schedule, error) {
|
||||||
|
_, old := db.get(id)
|
||||||
|
exists := nil != old
|
||||||
|
// TODO constant time bail
|
||||||
|
if !exists || !ctcmp(old.AccessID, accessID) {
|
||||||
|
return nil, fmt.Errorf("invalid id")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy everything we keep into its own array
|
||||||
|
newSchedules := []Schedule{}
|
||||||
|
for i := range db.json.Schedules {
|
||||||
|
schedule := db.json.Schedules[i]
|
||||||
|
if old.ID != schedule.ID {
|
||||||
|
newSchedules = append(newSchedules, schedule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
db.mux.Lock()
|
||||||
|
db.json.Schedules = newSchedules
|
||||||
|
db.mux.Unlock()
|
||||||
|
|
||||||
|
err := db.save(accessID)
|
||||||
|
if nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &again.Schedule{
|
||||||
|
ID: old.ID,
|
||||||
|
AccessID: old.AccessID,
|
||||||
|
Date: old.Date,
|
||||||
|
Time: old.Time,
|
||||||
|
TZ: old.TZ,
|
||||||
|
NextRunAt: old.NextRunAt,
|
||||||
|
Webhooks: old.Webhooks,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ctcmp(x string, y string) bool {
|
||||||
|
return 1 == subtle.ConstantTimeCompare([]byte(x), []byte(y))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *JSONDB) get(id string) (int, *Schedule) {
|
||||||
|
db.mux.Lock()
|
||||||
|
scheds := db.json.Schedules
|
||||||
|
db.mux.Unlock()
|
||||||
|
for i := range scheds {
|
||||||
|
schedule := scheds[i]
|
||||||
|
if ctcmp(id, schedule.ID) {
|
||||||
|
return i, &schedule
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func genID(n int) (string, error) {
|
||||||
|
b := make([]byte, n)
|
||||||
|
_, err := rand.Read(b)
|
||||||
|
if nil != err {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
func (db *JSONDB) save(accessID string) error {
|
func (db *JSONDB) save(accessID string) error {
|
||||||
// TODO per-user files (w/ mutex lock or channel on open and write)
|
// TODO per-user files, maybe
|
||||||
tmppath := db.path + ".tmp"
|
// or probably better to spend that time building the postgres adapter
|
||||||
|
rnd, err := genID(4)
|
||||||
|
tmppath := db.path + "." + rnd + ".tmp"
|
||||||
bakpath := db.path + ".bak"
|
bakpath := db.path + ".bak"
|
||||||
|
|
||||||
os.Remove(tmppath) // ignore error
|
os.Remove(tmppath) // ignore error
|
||||||
|
@ -194,12 +250,15 @@ func (db *JSONDB) save(accessID string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO could make async and debounce...
|
||||||
|
// or spend that time on something useful
|
||||||
|
db.fmux.Lock()
|
||||||
|
defer db.fmux.Unlock()
|
||||||
os.Remove(bakpath) // ignore error
|
os.Remove(bakpath) // ignore error
|
||||||
err = os.Rename(db.path, bakpath)
|
err = os.Rename(db.path, bakpath)
|
||||||
if nil != err {
|
if nil != err {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = os.Rename(tmppath, db.path)
|
err = os.Rename(tmppath, db.path)
|
||||||
if nil != err {
|
if nil != err {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -50,8 +50,8 @@
|
||||||
newWebhookHeader(ev.target);
|
newWebhookHeader(ev.target);
|
||||||
} else if (ev.target.matches('.js-rm-header')) {
|
} else if (ev.target.matches('.js-rm-header')) {
|
||||||
rmWebhookHeader(ev.target);
|
rmWebhookHeader(ev.target);
|
||||||
} else if (ev.target.matches('.js-delete') && ev.target.closest('.js-webhook')) {
|
} else if (ev.target.matches('.js-delete') && ev.target.closest('.js-schedule')) {
|
||||||
deleteWebhook(ev.target.closest('.js-webhook'));
|
deleteSchedule(ev.target.closest('.js-schedule'));
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -142,11 +142,12 @@
|
||||||
throw new Error('something bad happened');
|
throw new Error('something bad happened');
|
||||||
}
|
}
|
||||||
|
|
||||||
state.account.schedules.push(resp.data);
|
state.account.schedules.push(data);
|
||||||
|
|
||||||
displayAccount(state.account);
|
displayAccount(state.account);
|
||||||
})
|
})
|
||||||
.catch(function(e) {
|
.catch(function(e) {
|
||||||
|
console.error(e);
|
||||||
window.alert(e.message);
|
window.alert(e.message);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -168,9 +169,8 @@
|
||||||
var $h = $rmHeader.closest('.js-header');
|
var $h = $rmHeader.closest('.js-header');
|
||||||
$h.parentElement.removeChild($h);
|
$h.parentElement.removeChild($h);
|
||||||
}
|
}
|
||||||
function deleteWebhook($hook) {
|
function deleteSchedule($sched) {
|
||||||
var deviceId = $hook.closest('.js-schedule').querySelector('.js-id').value;
|
var schedId = $('.js-id', $sched).value;
|
||||||
var id = $('.js-id', $hook).innerText;
|
|
||||||
var opts = {
|
var opts = {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -179,27 +179,17 @@
|
||||||
},
|
},
|
||||||
cors: true
|
cors: true
|
||||||
};
|
};
|
||||||
window.fetch('/api/iot/devices/' + deviceId + '/webhooks/' + id, opts).then(function(resp) {
|
window.fetch('/api/v0/schedules/' + schedId, opts).then(function(resp) {
|
||||||
return resp.json().then(function(result) {
|
return resp.json().then(function(result) {
|
||||||
if (!result.webhook) {
|
if (!result.webhooks) {
|
||||||
console.error(result);
|
console.error(result);
|
||||||
window.alert('something went wrong: ' + JSON.stringify(result));
|
window.alert('something went wrong: ' + JSON.stringify(result));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var index = -1;
|
state.account.schedules = state.account.schedules.filter(function(g) {
|
||||||
var dev = state.account.devices.filter(function(d, i) {
|
return g.id !== result.id;
|
||||||
return d.accessToken == deviceId;
|
|
||||||
})[0];
|
|
||||||
dev.webhooks.some(function(g, i) {
|
|
||||||
if (g.id === id) {
|
|
||||||
index = i;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
if (index > -1) {
|
displayAccount(state.account);
|
||||||
dev.webhooks.splice(index, 1);
|
|
||||||
displayAccount(state.account);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -211,6 +201,7 @@
|
||||||
var $devs = $('.js-schedules');
|
var $devs = $('.js-schedules');
|
||||||
$devs.innerHTML = '';
|
$devs.innerHTML = '';
|
||||||
data.schedules.forEach(function(d) {
|
data.schedules.forEach(function(d) {
|
||||||
|
console.log('schedule', d);
|
||||||
var $dev = $.create($devTpl);
|
var $dev = $.create($devTpl);
|
||||||
$('.js-id', $dev).value = d.id;
|
$('.js-id', $dev).value = d.id;
|
||||||
$('.js-date', $dev).value = d.date;
|
$('.js-date', $dev).value = d.date;
|
||||||
|
|
|
@ -15,56 +15,62 @@
|
||||||
<button>Login</button>
|
<button>Login</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<pre><code class="js-schedules-output"> </code></pre>
|
|
||||||
|
|
||||||
<div class="js-account" hidden>
|
<div class="js-account" hidden>
|
||||||
<h2>Schedules</h2>
|
<details>
|
||||||
<div class="js-schedules">
|
<summary>Schedules</summary>
|
||||||
<div class="js-schedule">
|
<h3>Schedules</h3>
|
||||||
<input type="hidden" class="js-id" />
|
<div class="js-schedules">
|
||||||
<input type="date" class="js-date" readonly />
|
<div class="js-schedule">
|
||||||
<input type="time" class="js-time" readonly />
|
<input type="hidden" class="js-id" />
|
||||||
<input type="text" class="js-tz" readonly />
|
<input type="date" class="js-date" readonly />
|
||||||
|
<input type="time" class="js-time" readonly />
|
||||||
|
<input type="text" class="js-tz" readonly />
|
||||||
|
|
||||||
<div class="doc-webhooks-container">
|
<div class="doc-webhooks-container">
|
||||||
<div class="js-webhooks">
|
<div class="js-webhooks">
|
||||||
<div class="js-webhook">
|
<div class="js-webhook">
|
||||||
<h4><span class="js-comment"></span></h4>
|
<h4><span class="js-comment"></span></h4>
|
||||||
<span class="js-id" hidden></span>
|
<span class="js-id" hidden></span>
|
||||||
<span class="js-method"></span>
|
<span class="js-method"></span>
|
||||||
<span class="js-url"></span>
|
<span class="js-url"></span>
|
||||||
<br />
|
<br />
|
||||||
<div class="js-headers">
|
<div class="js-headers">
|
||||||
<div class="js-header">
|
<div class="js-header">
|
||||||
<span class="js-key"></span>
|
<span class="js-key"></span>
|
||||||
<span class="js-value"></span>
|
<span class="js-value"></span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<pre><code class="js-body-template"></code></pre>
|
||||||
</div>
|
</div>
|
||||||
<pre><code class="js-body-template"></code></pre>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button class="js-delete" type="button">Delete Schedule</button>
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
</div>
|
</div>
|
||||||
<button class="js-delete" type="button">Delete Schedule</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2>Add Schedule</h2>
|
|
||||||
<form class="js-new-schedule">
|
|
||||||
<label>Date: <input type="date" class="js-date" required/></label>
|
|
||||||
<label>Time: <input type="time" class="js-time" step="300" required/></label>
|
|
||||||
<!-- TODO combo box -->
|
|
||||||
<label
|
|
||||||
>Location:
|
|
||||||
<select class="js-tz">
|
|
||||||
<option value="UTC">UTC</option>
|
|
||||||
<option disabled>──────────</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<br />
|
<br />
|
||||||
|
</details>
|
||||||
|
|
||||||
<h3>Webhook</h3>
|
<details>
|
||||||
<div class="js-new-webhook">
|
<summary>Add Schedule</summary>
|
||||||
<!--
|
<h3>Add Schedule</h3>
|
||||||
|
<form class="js-new-schedule">
|
||||||
|
<label>Date: <input type="date" class="js-date" required/></label>
|
||||||
|
<label>Time: <input type="time" class="js-time" step="300" required/></label>
|
||||||
|
<!-- TODO combo box -->
|
||||||
|
<label
|
||||||
|
>Location:
|
||||||
|
<select class="js-tz">
|
||||||
|
<option value="UTC">UTC</option>
|
||||||
|
<option disabled>──────────</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<h3>Webhook</h3>
|
||||||
|
<div class="js-new-webhook">
|
||||||
|
<!--
|
||||||
<select class="js-template">
|
<select class="js-template">
|
||||||
<option value="webhook" selected>Custom Webhook</option>
|
<option value="webhook" selected>Custom Webhook</option>
|
||||||
<option value="requestbin">RequestBin</option>
|
<option value="requestbin">RequestBin</option>
|
||||||
|
@ -74,33 +80,46 @@
|
||||||
</select>
|
</select>
|
||||||
<br />
|
<br />
|
||||||
-->
|
-->
|
||||||
<input class="js-comment" type="text" placeholder="Webhook Name" required />
|
<input class="js-comment" type="text" placeholder="Webhook Name" required />
|
||||||
<br />
|
<br />
|
||||||
<select class="js-method">
|
<select class="js-method">
|
||||||
<option value="POST" selected>POST</option>
|
<option value="POST" selected>POST</option>
|
||||||
<option value="PUT">PUT</option>
|
<option value="PUT">PUT</option>
|
||||||
</select>
|
</select>
|
||||||
<input placeholder="https://example.com/api/v1/updates" class="js-url" type="url" required />
|
<input placeholder="https://example.com/api/v1/updates" class="js-url" type="url" required />
|
||||||
<div class="js-headers">
|
<div class="js-headers">
|
||||||
<div class="js-header">
|
<div class="js-header">
|
||||||
<input placeholder="Header" class="js-key" type="text" />
|
<input placeholder="Header" class="js-key" type="text" />
|
||||||
<input placeholder="Value" class="js-value" type="text" />
|
<input placeholder="Value" class="js-value" type="text" />
|
||||||
<button type="button" class="js-rm-header" hidden>[x]</button>
|
<button type="button" class="js-rm-header" hidden>[x]</button>
|
||||||
<button type="button" class="js-new-header">[+]</button>
|
<button type="button" class="js-new-header">[+]</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="js-body">
|
||||||
|
<textarea
|
||||||
|
placeholder="Body template, use '{{ keyname }}' for template values."
|
||||||
|
class="js-body-template"
|
||||||
|
></textarea>
|
||||||
|
<!-- TODO preview template -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="js-body">
|
|
||||||
<textarea
|
|
||||||
placeholder="Body template, use '{{ keyname }}' for template values."
|
|
||||||
class="js-body-template"
|
|
||||||
></textarea>
|
|
||||||
<!-- TODO preview template -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<br />
|
||||||
|
<button class="js-create">Save Schedule</button>
|
||||||
|
</form>
|
||||||
<br />
|
<br />
|
||||||
<button class="js-create">Save Schedule</button>
|
<br />
|
||||||
</form>
|
<br />
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Debug Info</summary>
|
||||||
|
<h3>Debug Info</h3>
|
||||||
|
<pre><code class="js-schedules-output"> </code></pre>
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
</details>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="./ajquery.js"></script>
|
<script src="./ajquery.js"></script>
|
||||||
|
|
Loading…
Reference in New Issue