schedule: added history page and version implementation
This commit is contained in:
parent
fb54633390
commit
3fcf291e6d
118
api/schedule_history_list.go
Normal file
118
api/schedule_history_list.go
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stephenafamo/bob"
|
||||||
|
"github.com/stephenafamo/bob/dialect/sqlite"
|
||||||
|
"github.com/stephenafamo/bob/dialect/sqlite/dialect"
|
||||||
|
"github.com/stephenafamo/bob/dialect/sqlite/sm"
|
||||||
|
"github.com/tigorlazuardi/redmage/models"
|
||||||
|
"github.com/tigorlazuardi/redmage/pkg/errs"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ScheduleHistoryListParams struct {
|
||||||
|
Subreddit string
|
||||||
|
After time.Time
|
||||||
|
Before time.Time
|
||||||
|
|
||||||
|
Limit int64
|
||||||
|
Offset int64
|
||||||
|
OrderBy string
|
||||||
|
Sort string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (params *ScheduleHistoryListParams) FillFromQuery(query Queryable) {
|
||||||
|
params.Subreddit = query.Get("subreddit")
|
||||||
|
params.Limit, _ = strconv.ParseInt(query.Get("limit"), 10, 64)
|
||||||
|
if params.Limit < 1 {
|
||||||
|
params.Limit = 100
|
||||||
|
}
|
||||||
|
if params.Limit > 1000 {
|
||||||
|
params.Limit = 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
params.Offset, _ = strconv.ParseInt(query.Get("offset"), 10, 64)
|
||||||
|
if params.Offset < 0 {
|
||||||
|
params.Offset = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
afterInt, _ := strconv.ParseInt(query.Get("after"), 10, 64)
|
||||||
|
if afterInt > 0 {
|
||||||
|
params.After = time.Unix(afterInt, 0)
|
||||||
|
} else if afterInt < 0 {
|
||||||
|
params.After = now.Add(time.Duration(afterInt) * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeInt, _ := strconv.ParseInt(query.Get("before"), 10, 64)
|
||||||
|
if beforeInt > 0 {
|
||||||
|
params.Before = time.Unix(beforeInt, 0)
|
||||||
|
} else if beforeInt < 0 {
|
||||||
|
params.Before = now.Add(time.Duration(beforeInt) * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
params.OrderBy = query.Get("order_by")
|
||||||
|
params.Sort = query.Get("sort")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (params ScheduleHistoryListParams) CountQuery() (expr []bob.Mod[*dialect.SelectQuery]) {
|
||||||
|
if params.Subreddit != "" {
|
||||||
|
expr = append(expr, models.SelectWhere.ScheduleHistories.Subreddit.EQ(params.Subreddit))
|
||||||
|
}
|
||||||
|
if !params.After.IsZero() {
|
||||||
|
expr = append(expr, models.SelectWhere.ScheduleHistories.CreatedAt.GTE(params.After.Unix()))
|
||||||
|
}
|
||||||
|
if !params.Before.IsZero() {
|
||||||
|
expr = append(expr, models.SelectWhere.ScheduleHistories.CreatedAt.LTE(params.Before.Unix()))
|
||||||
|
}
|
||||||
|
|
||||||
|
return expr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (params ScheduleHistoryListParams) Query() (expr []bob.Mod[*dialect.SelectQuery]) {
|
||||||
|
expr = append(expr, params.CountQuery()...)
|
||||||
|
if params.Limit > 0 {
|
||||||
|
expr = append(expr, sm.Limit(params.Limit))
|
||||||
|
}
|
||||||
|
if params.Offset > 0 {
|
||||||
|
expr = append(expr, sm.Offset(params.Offset))
|
||||||
|
}
|
||||||
|
if params.OrderBy != "" {
|
||||||
|
if strings.ToLower(params.Sort) == "desc" {
|
||||||
|
expr = append(expr, sm.OrderBy(sqlite.Quote(params.OrderBy)).Desc())
|
||||||
|
} else {
|
||||||
|
expr = append(expr, sm.OrderBy(sqlite.Quote(params.OrderBy)).Asc())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
expr = append(expr, sm.OrderBy(models.ScheduleHistoryColumns.CreatedAt).Desc(), sm.OrderBy(models.ScheduleHistoryColumns.Status).Desc())
|
||||||
|
}
|
||||||
|
|
||||||
|
return expr
|
||||||
|
}
|
||||||
|
|
||||||
|
type ScheduleHistoryListResult struct {
|
||||||
|
Schedules models.ScheduleHistorySlice `json:"schedules"`
|
||||||
|
Total int64 `json:"count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *API) ScheduleHistoryList(ctx context.Context, params ScheduleHistoryListParams) (result ScheduleHistoryListResult, err error) {
|
||||||
|
ctx, span := tracer.Start(ctx, "*API.ScheduleHistoryList")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
result.Schedules, err = models.ScheduleHistories.Query(ctx, api.db, params.Query()...).All()
|
||||||
|
if err != nil {
|
||||||
|
return result, errs.Wrapw(err, "failed to list schedule histories", "query", params)
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Total, err = models.ScheduleHistories.Query(ctx, api.db, params.CountQuery()...).Count()
|
||||||
|
if err != nil {
|
||||||
|
return result, errs.Wrapw(err, "failed to count schedule histories", "query", params)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
|
@ -12,16 +12,19 @@ type ScheduleStatus int8
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ScheduleStatusDisabled ScheduleStatus = iota
|
ScheduleStatusDisabled ScheduleStatus = iota
|
||||||
|
ScheduleStatusEnabled
|
||||||
ScheduleStatusStandby
|
ScheduleStatusStandby
|
||||||
ScheduleStatusError
|
|
||||||
ScheduleStatusEnqueued
|
ScheduleStatusEnqueued
|
||||||
ScheduleStatusDownloading
|
ScheduleStatusDownloading
|
||||||
|
ScheduleStatusError
|
||||||
)
|
)
|
||||||
|
|
||||||
func (ss ScheduleStatus) String() string {
|
func (ss ScheduleStatus) String() string {
|
||||||
switch ss {
|
switch ss {
|
||||||
case ScheduleStatusDisabled:
|
case ScheduleStatusDisabled:
|
||||||
return "Disabled"
|
return "Disabled"
|
||||||
|
case ScheduleStatusEnabled:
|
||||||
|
return "Enabled"
|
||||||
case ScheduleStatusStandby:
|
case ScheduleStatusStandby:
|
||||||
return "Standby"
|
return "Standby"
|
||||||
case ScheduleStatusEnqueued:
|
case ScheduleStatusEnqueued:
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package routes
|
package routes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -26,8 +25,11 @@ func (routes *Routes) PageHome(rw http.ResponseWriter, r *http.Request) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.New(ctx).Err(err).Error("failed to list subreddits")
|
log.New(ctx).Err(err).Error("failed to list subreddits")
|
||||||
code, message := errs.HTTPMessage(err)
|
code, message := errs.HTTPMessage(err)
|
||||||
|
data.Error = message
|
||||||
rw.WriteHeader(code)
|
rw.WriteHeader(code)
|
||||||
_ = json.NewEncoder(rw).Encode(map[string]string{"error": message})
|
if err := homeview.Home(vc, data).Render(ctx, rw); err != nil {
|
||||||
|
log.New(ctx).Err(err).Error("failed to render home view")
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,10 +2,13 @@ package routes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/tigorlazuardi/redmage/api"
|
||||||
|
"github.com/tigorlazuardi/redmage/pkg/errs"
|
||||||
"github.com/tigorlazuardi/redmage/pkg/log"
|
"github.com/tigorlazuardi/redmage/pkg/log"
|
||||||
"github.com/tigorlazuardi/redmage/views"
|
"github.com/tigorlazuardi/redmage/views"
|
||||||
scheduleshistoryview "github.com/tigorlazuardi/redmage/views/schedulehistoriesview"
|
"github.com/tigorlazuardi/redmage/views/schedulehistoriesview"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (routes *Routes) PageScheduleHistory(rw http.ResponseWriter, req *http.Request) {
|
func (routes *Routes) PageScheduleHistory(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
@ -14,9 +17,36 @@ func (routes *Routes) PageScheduleHistory(rw http.ResponseWriter, req *http.Requ
|
||||||
|
|
||||||
c := views.NewContext(routes.Config, req)
|
c := views.NewContext(routes.Config, req)
|
||||||
|
|
||||||
var data scheduleshistoryview.Data
|
var data schedulehistoriesview.Data
|
||||||
|
if tz := req.URL.Query().Get("tz"); tz == "" {
|
||||||
|
data.Timezone = time.Local
|
||||||
|
} else {
|
||||||
|
var err error
|
||||||
|
data.Timezone, err = time.LoadLocation(tz)
|
||||||
|
if err != nil {
|
||||||
|
data.Timezone = time.Local
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := scheduleshistoryview.ScheduleHistoriesview(c, data).Render(ctx, rw); err != nil {
|
var params api.ScheduleHistoryListParams
|
||||||
|
params.FillFromQuery(req.URL.Query())
|
||||||
|
|
||||||
|
result, err := routes.API.ScheduleHistoryList(ctx, params)
|
||||||
|
if err != nil {
|
||||||
|
log.New(ctx).Err(err).Error("Failed to list schedule histories")
|
||||||
|
code, message := errs.HTTPMessage(err)
|
||||||
|
rw.WriteHeader(code)
|
||||||
|
data.Error = message
|
||||||
|
if err := schedulehistoriesview.ScheduleHistoriesview(c, data).Render(ctx, rw); err != nil {
|
||||||
|
log.New(ctx).Err(err).Error("Failed to render schedule histories view")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data.Schedules = result.Schedules
|
||||||
|
data.Total = result.Total
|
||||||
|
|
||||||
|
if err := schedulehistoriesview.ScheduleHistoriesview(c, data).Render(ctx, rw); err != nil {
|
||||||
log.New(ctx).Err(err).Error("Failed to render schedule histories view")
|
log.New(ctx).Err(err).Error("Failed to render schedule histories view")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
17
views/components/absolute_time_text.templ
Normal file
17
views/components/absolute_time_text.templ
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
package components
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
templ AbsoluteTimeText(id string, t int64, class ...string) {
|
||||||
|
<span class={ strings.Join(class, " ") }>
|
||||||
|
{ time.Unix(t, 0).Format("Mon, _2 Jan 2006 15:04:05") }
|
||||||
|
</span>
|
||||||
|
@absoluteFromTime(id, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
script absoluteFromTime(id string, time int64) {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
const timeText = dayjs.unix(time).tz(dayjs.tz.guess()).format('ddd, D MMM YYYY HH:mm:ss');
|
||||||
|
el.textContent = timeText;
|
||||||
|
}
|
46
views/schedulehistoriesview/data.go
Normal file
46
views/schedulehistoriesview/data.go
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
package schedulehistoriesview
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/tigorlazuardi/redmage/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Data struct {
|
||||||
|
Error string
|
||||||
|
Schedules models.ScheduleHistorySlice
|
||||||
|
Total int64
|
||||||
|
Timezone *time.Location
|
||||||
|
}
|
||||||
|
|
||||||
|
func (data *Data) splitByDays() (out []*splitByDaySchedules) {
|
||||||
|
for _, schedule := range data.Schedules {
|
||||||
|
day := time.Unix(schedule.CreatedAt, 0).In(data.Timezone)
|
||||||
|
date := time.Date(day.Year(), day.Month(), day.Day(), 0, 0, 0, 0, day.Location())
|
||||||
|
|
||||||
|
var found bool
|
||||||
|
inner:
|
||||||
|
for _, split := range out {
|
||||||
|
if split.Date.Equal(date) {
|
||||||
|
found = true
|
||||||
|
split.Schedules = append(split.Schedules, schedule)
|
||||||
|
break inner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
out = append(out, &splitByDaySchedules{
|
||||||
|
Day: day,
|
||||||
|
Date: date,
|
||||||
|
Schedules: models.ScheduleHistorySlice{schedule},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
type splitByDaySchedules struct {
|
||||||
|
Day time.Time
|
||||||
|
Date time.Time
|
||||||
|
Schedules models.ScheduleHistorySlice
|
||||||
|
}
|
|
@ -1,9 +1,10 @@
|
||||||
package scheduleshistoryview
|
package schedulehistoriesview
|
||||||
|
|
||||||
import "github.com/tigorlazuardi/redmage/views"
|
import "github.com/tigorlazuardi/redmage/views"
|
||||||
import "github.com/tigorlazuardi/redmage/views/components"
|
import "github.com/tigorlazuardi/redmage/views/components"
|
||||||
|
import "time"
|
||||||
type Data struct{}
|
import "github.com/tigorlazuardi/redmage/api"
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
templ ScheduleHistoriesview(c *views.Context, data Data) {
|
templ ScheduleHistoriesview(c *views.Context, data Data) {
|
||||||
@components.Doctype() {
|
@components.Doctype() {
|
||||||
|
@ -17,10 +18,92 @@ templ ScheduleHistoriesview(c *views.Context, data Data) {
|
||||||
}
|
}
|
||||||
|
|
||||||
templ ScheduleHistoriesContent(c *views.Context, data Data) {
|
templ ScheduleHistoriesContent(c *views.Context, data Data) {
|
||||||
@components.Container() {
|
<main class="prose min-w-full">
|
||||||
<main class="prose min-w-full">
|
@components.Container() {
|
||||||
<h1>Schedule History</h1>
|
if data.Error != "" {
|
||||||
<div class="divider"></div>
|
@components.ErrorToast(data.Error)
|
||||||
</main>
|
} else {
|
||||||
}
|
<h1>Schedule History</h1>
|
||||||
|
<div class="divider"></div>
|
||||||
|
if data.Total < 1 {
|
||||||
|
<h2>There are no history schedules to be found. You can populate this page's history by manually trigger a <a href="/subreddits" class="text-primary">Subreddit</a> { "for" } downloading.</h2>
|
||||||
|
}
|
||||||
|
if data.Total > 0 {
|
||||||
|
<div class="grid">
|
||||||
|
for _, scheduleGroup := range data.splitByDays() {
|
||||||
|
<h2>{ scheduleGroup.Day.Format("Monday, _2 January 2006") }</h2>
|
||||||
|
<div class="divider"></div>
|
||||||
|
<div class="hidden sm:grid sm:grid-cols-[1fr,3fr]">
|
||||||
|
<span class="font-bold">Time</span>
|
||||||
|
<span class="font-bold">Event</span>
|
||||||
|
</div>
|
||||||
|
for _, schedule := range scheduleGroup.Schedules {
|
||||||
|
<div class="grid sm:grid-cols-[1fr,3fr] mb-4 sm:mb-2">
|
||||||
|
<span>
|
||||||
|
<div
|
||||||
|
data-name="schedule-tooltip"
|
||||||
|
class="tooltip"
|
||||||
|
data-tip={ time.Unix(schedule.CreatedAt, 10).In(time.Local).Format(time.RFC3339) }
|
||||||
|
>
|
||||||
|
<span class="font-bold">{ time.Unix(schedule.CreatedAt, 10).In(time.Local).Format("15:04:05") }</span>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
if api.ScheduleStatus(schedule.Status) == api.ScheduleStatusDisabled {
|
||||||
|
<span>
|
||||||
|
Subreddit
|
||||||
|
@subredditLink(schedule.Subreddit)
|
||||||
|
scheduler has been set to { api.ScheduleStatusDisabled.String() } status.
|
||||||
|
</span>
|
||||||
|
} else if api.ScheduleStatus(schedule.Status) == api.ScheduleStatusEnabled {
|
||||||
|
<span>
|
||||||
|
Subreddit
|
||||||
|
@subredditLink(schedule.Subreddit)
|
||||||
|
{ " " }
|
||||||
|
has been <b>{ api.ScheduleStatusEnabled.String() }</b> { "for" } automatic scheduling.
|
||||||
|
</span>
|
||||||
|
} else if api.ScheduleStatus(schedule.Status) == api.ScheduleStatusStandby {
|
||||||
|
<span>
|
||||||
|
Subreddit
|
||||||
|
@subredditLink(schedule.Subreddit)
|
||||||
|
{ " " }
|
||||||
|
has finished
|
||||||
|
<b class="text-secondary">{ api.ScheduleStatusDownloading.String() }</b>
|
||||||
|
and turned to <b>{ api.ScheduleStatusStandby.String() }</b> status.
|
||||||
|
</span>
|
||||||
|
} else if api.ScheduleStatus(schedule.Status) == api.ScheduleStatusEnqueued {
|
||||||
|
<span>
|
||||||
|
Subreddit
|
||||||
|
@subredditLink(schedule.Subreddit)
|
||||||
|
{ " " }
|
||||||
|
is <b class="text-accent">{ api.ScheduleStatusEnqueued.String() } </b> { "for" } downloading.
|
||||||
|
</span>
|
||||||
|
} else if api.ScheduleStatus(schedule.Status) == api.ScheduleStatusDownloading {
|
||||||
|
<span>
|
||||||
|
Subreddit
|
||||||
|
@subredditLink(schedule.Subreddit)
|
||||||
|
{ " " }
|
||||||
|
has started <b class="text-secondary">{ api.ScheduleStatusDownloading.String() }</b>.
|
||||||
|
</span>
|
||||||
|
} else if api.ScheduleStatus(schedule.Status) == api.ScheduleStatusError {
|
||||||
|
<span>
|
||||||
|
Subreddit
|
||||||
|
@subredditLink(schedule.Subreddit)
|
||||||
|
{ " " }
|
||||||
|
finishes <b class="text-secondary">{ api.ScheduleStatusDownloading.String() }</b>
|
||||||
|
with <b class="text-error">{ api.ScheduleStatusError.String() }</b> of <span class="text-error">"{ schedule.ErrorMessage }"</span>.
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<script> document.querySelectorAll(`[data-name="schedule-tooltip"]`).forEach((el) => el.dataset.tip = dayjs(el.dataset.tip).tz(dayjs.tz.guess()).format('ddd, D MMM YYYY HH:mm:ss Z') ) </script>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</main>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ subredditLink(subreddit string) {
|
||||||
|
<a href={ templ.URL(fmt.Sprintf("/subreddits/details/%s", subreddit)) } class="text-primary">{ subreddit }</a>
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue