e04cfcf433
Release / build (amd64, darwin) (push) Failing after 10s
Release / build (amd64, linux) (push) Failing after 9s
Release / build (arm64, darwin) (push) Failing after 11s
Release / build (arm64, linux) (push) Failing after 9s
Release / release (push) Has been skipped
The alert list showed only Started, which comes from Prometheus and never changes for the lifetime of an alert instance. A firing alert that started 12 days ago looked identical whether Alertmanager refreshed it 30 seconds ago or went silent a week ago. terdut-server already tracks this: the webhook upsert sets received_at on every accepted payload, including the periodic re-sends issued at repeat_interval, and its archiver treats the field as a liveness heartbeat. The field was already decoded into api.Alert.ReceivedAt and simply never rendered. Add a Last Seen column to the alert tables, rendered with the existing humanAgo helper. The Alerts and Archived tabs share alertColumns/alertRows, so both pick it up. The width budget is re-derived for five columns; the slack constant now accounts for all of bubbles' per-cell padding, so the table lands exactly on the terminal width instead of overflowing by two columns as it did with four. The detail view gains a matching Last Seen line, with the timeline labels widened to keep values aligned. Since received_at stops advancing once an alert resolves, also pull through the server's resolution_source and show it in the status header (RESOLVED · alertmanager vs RESOLVED · expiry) so a frozen timestamp is explained.
760 lines
19 KiB
Go
760 lines
19 KiB
Go
package tui
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/charmbracelet/bubbles/help"
|
|
"github.com/charmbracelet/bubbles/table"
|
|
"github.com/charmbracelet/bubbles/textinput"
|
|
"github.com/charmbracelet/bubbles/viewport"
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
"github.com/charmbracelet/lipgloss"
|
|
"github.com/yeniklas/terdut-tui/internal/api"
|
|
)
|
|
|
|
// ── Enums ──────────────────────────────────────────────────────────────────
|
|
|
|
type section int
|
|
|
|
const (
|
|
sectionAlerts section = iota
|
|
sectionArchived
|
|
sectionSchedule
|
|
sectionUsers
|
|
)
|
|
|
|
type mode int
|
|
|
|
const (
|
|
modeDashboard mode = iota
|
|
modeDetail
|
|
modeComment
|
|
modeConfirmDelete
|
|
modeStats
|
|
modeScheduleUserPicker
|
|
modeUserCreate
|
|
modeAPIKeyMenu
|
|
modeAPIKeyCreate
|
|
modeAPIKeyReveal
|
|
modeAPIKeyRevokeByID
|
|
)
|
|
|
|
type confirmTarget int
|
|
|
|
const (
|
|
confirmDeleteComment confirmTarget = iota
|
|
confirmDeleteScheduleEntry
|
|
confirmDeleteUser
|
|
)
|
|
|
|
// ── Messages ───────────────────────────────────────────────────────────────
|
|
|
|
// dashboard
|
|
type connectedMsg struct{}
|
|
type connectErrMsg struct{ err error }
|
|
type alertsFetchedMsg struct{ alerts []api.Alert }
|
|
type archivedAlertsFetchedMsg struct{ alerts []api.Alert }
|
|
type alertArchivedMsg struct{ alerts []api.Alert }
|
|
type alertUnarchivedMsg struct{ alerts []api.Alert }
|
|
type statsFetchedMsg struct{ stats api.AlertStats }
|
|
type fetchDataErrMsg struct{ err error }
|
|
type tickMsg time.Time
|
|
type clearStatusMsg struct{}
|
|
|
|
// detail
|
|
type alertDetailFetchedMsg struct {
|
|
alert api.Alert
|
|
comments []api.Comment
|
|
}
|
|
type alertDetailErrMsg struct{ err error }
|
|
type actionErrMsg struct{ err error }
|
|
type detailStatsFetchedMsg struct {
|
|
top []api.TopAlert
|
|
byHour []api.HourStat
|
|
byDay []api.DayStat
|
|
}
|
|
type detailStatsErrMsg struct{ err error }
|
|
|
|
// schedule
|
|
type scheduleFetchedMsg struct {
|
|
entries []api.ScheduleEntry
|
|
current *api.ScheduleEntry
|
|
}
|
|
type scheduleFetchErrMsg struct{ err error }
|
|
type scheduleActionErrMsg struct{ err error }
|
|
type usersFetchedMsg struct{ users []api.User }
|
|
|
|
// user management
|
|
type apiKeyCreatedMsg struct{ key api.APIKey }
|
|
type apiKeyRevokedMsg struct{}
|
|
type userActionErrMsg struct{ err error }
|
|
|
|
// ── Model ──────────────────────────────────────────────────────────────────
|
|
|
|
type scheduleDay struct {
|
|
date time.Time
|
|
entry *api.ScheduleEntry
|
|
}
|
|
|
|
type Model struct {
|
|
client *api.Client
|
|
serverURL string
|
|
refreshInterval time.Duration
|
|
|
|
activeSection section
|
|
mode mode
|
|
width int
|
|
height int
|
|
|
|
// Connection & dashboard
|
|
connected bool
|
|
loading bool
|
|
err error
|
|
statusMsg string
|
|
alerts []api.Alert
|
|
stats *api.AlertStats
|
|
filterStatus string
|
|
alertTable table.Model
|
|
|
|
// Archived alerts
|
|
archivedAlerts []api.Alert
|
|
archivedLoading bool
|
|
archivedTable table.Model
|
|
|
|
// Detail
|
|
selectedAlert api.Alert
|
|
comments []api.Comment
|
|
commentCursor int
|
|
detailLoading bool
|
|
detailViewport viewport.Model
|
|
|
|
// Comment compose
|
|
commentInput textinput.Model
|
|
|
|
// Confirm delete
|
|
confirmTarget confirmTarget
|
|
pendingDeleteID int64 // comment ID
|
|
pendingDeleteEntry *api.ScheduleEntry
|
|
|
|
// Stats
|
|
topAlerts []api.TopAlert
|
|
hourStats []api.HourStat
|
|
dayStats []api.DayStat
|
|
statsLoading bool
|
|
statsViewport viewport.Model
|
|
|
|
// Schedule
|
|
scheduleWindow time.Time
|
|
scheduleEntries []api.ScheduleEntry
|
|
scheduleDays []scheduleDay
|
|
currentOnCall *api.ScheduleEntry
|
|
scheduleLoading bool
|
|
scheduleTable table.Model
|
|
|
|
// User picker (schedule assignment)
|
|
users []api.User
|
|
usersLoading bool
|
|
userPickerTable table.Model
|
|
pickerAssignWeek bool
|
|
|
|
// User management section
|
|
userManageTable table.Model
|
|
selectedUser api.User
|
|
userFormInputs [2]textinput.Model
|
|
userFormFocus int
|
|
apiKeyNameInput textinput.Model
|
|
apiKeyRevokeInput textinput.Model
|
|
revealedAPIKey api.APIKey
|
|
|
|
help help.Model
|
|
keys keyMap
|
|
}
|
|
|
|
func NewModel(client *api.Client, serverURL string, refreshInterval time.Duration) Model {
|
|
ts := defaultTableStyles()
|
|
|
|
alertT := table.New(table.WithFocused(true))
|
|
alertT.SetStyles(ts)
|
|
|
|
archivedT := table.New(table.WithFocused(true))
|
|
archivedT.SetStyles(ts)
|
|
|
|
schedT := table.New(table.WithFocused(true))
|
|
schedT.SetStyles(ts)
|
|
|
|
pickerT := table.New(table.WithFocused(true))
|
|
pickerT.SetStyles(ts)
|
|
|
|
manageT := table.New(table.WithFocused(true))
|
|
manageT.SetStyles(ts)
|
|
|
|
ti := textinput.New()
|
|
ti.Placeholder = "type your comment…"
|
|
ti.CharLimit = 1000
|
|
|
|
usernameIn := textinput.New()
|
|
usernameIn.Placeholder = "username"
|
|
usernameIn.CharLimit = 64
|
|
|
|
emailIn := textinput.New()
|
|
emailIn.Placeholder = "email"
|
|
emailIn.CharLimit = 128
|
|
|
|
keyNameIn := textinput.New()
|
|
keyNameIn.Placeholder = "key name (e.g. laptop)"
|
|
keyNameIn.CharLimit = 64
|
|
|
|
revokeIn := textinput.New()
|
|
revokeIn.Placeholder = "integer key ID"
|
|
revokeIn.CharLimit = 20
|
|
|
|
now := time.Now().UTC()
|
|
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC)
|
|
weekday := int(today.Weekday())
|
|
if weekday == 0 {
|
|
weekday = 7 // ISO: Sunday = 7
|
|
}
|
|
window := today.AddDate(0, 0, -(weekday - 1))
|
|
|
|
return Model{
|
|
client: client,
|
|
serverURL: serverURL,
|
|
refreshInterval: refreshInterval,
|
|
activeSection: sectionAlerts,
|
|
mode: modeDashboard,
|
|
loading: true,
|
|
filterStatus: "firing",
|
|
commentCursor: -1,
|
|
alertTable: alertT,
|
|
archivedTable: archivedT,
|
|
commentInput: ti,
|
|
scheduleWindow: window,
|
|
scheduleTable: schedT,
|
|
userPickerTable: pickerT,
|
|
userManageTable: manageT,
|
|
userFormInputs: [2]textinput.Model{usernameIn, emailIn},
|
|
apiKeyNameInput: keyNameIn,
|
|
apiKeyRevokeInput: revokeIn,
|
|
help: help.New(),
|
|
keys: keys,
|
|
}
|
|
}
|
|
|
|
func (m Model) Init() tea.Cmd {
|
|
return connectCmd(m.client)
|
|
}
|
|
|
|
// ── Table rebuilders ───────────────────────────────────────────────────────
|
|
|
|
func defaultTableStyles() table.Styles {
|
|
s := table.DefaultStyles()
|
|
s.Header = s.Header.Bold(true)
|
|
s.Selected = s.Selected.
|
|
Foreground(lipgloss.Color("0")).
|
|
Background(colorPrimary).
|
|
Bold(true)
|
|
return s
|
|
}
|
|
|
|
func (m *Model) rebuildTable() {
|
|
m.alertTable.SetColumns(alertColumns(m.width))
|
|
m.alertTable.SetRows(alertRows(m.alerts))
|
|
h := m.height - 8
|
|
if h < 1 {
|
|
h = 1
|
|
}
|
|
m.alertTable.SetHeight(h)
|
|
}
|
|
|
|
func (m *Model) rebuildArchivedTable() {
|
|
m.archivedTable.SetColumns(alertColumns(m.width))
|
|
m.archivedTable.SetRows(alertRows(m.archivedAlerts))
|
|
h := m.height - 8
|
|
if h < 1 {
|
|
h = 1
|
|
}
|
|
m.archivedTable.SetHeight(h)
|
|
}
|
|
|
|
func (m *Model) rebuildScheduleTable() {
|
|
m.scheduleTable.SetColumns(scheduleColumns(m.width))
|
|
m.scheduleTable.SetRows(scheduleRows(m.scheduleDays))
|
|
h := m.height - 10
|
|
if h < 1 {
|
|
h = 1
|
|
}
|
|
m.scheduleTable.SetHeight(h)
|
|
}
|
|
|
|
func (m *Model) rebuildUserPickerTable() {
|
|
m.userPickerTable.SetColumns(userPickerColumns(m.width))
|
|
rows := make([]table.Row, len(m.users))
|
|
for i, u := range m.users {
|
|
rows[i] = table.Row{u.Username, u.Email}
|
|
}
|
|
m.userPickerTable.SetRows(rows)
|
|
h := m.height - 10
|
|
if h < 1 {
|
|
h = 1
|
|
}
|
|
m.userPickerTable.SetHeight(h)
|
|
}
|
|
|
|
func (m *Model) rebuildUserManageTable() {
|
|
m.userManageTable.SetColumns(userManageColumns(m.width))
|
|
rows := make([]table.Row, len(m.users))
|
|
for i, u := range m.users {
|
|
rows[i] = table.Row{u.Username, u.Email, u.CreatedAt.UTC().Format("2006-01-02")}
|
|
}
|
|
m.userManageTable.SetRows(rows)
|
|
h := m.height - 10
|
|
if h < 1 {
|
|
h = 1
|
|
}
|
|
m.userManageTable.SetHeight(h)
|
|
}
|
|
|
|
func (m *Model) refreshDetailContent() {
|
|
if m.width == 0 {
|
|
return
|
|
}
|
|
m.detailViewport.SetContent(buildDetailContent(m.selectedAlert, m.comments, m.commentCursor, m.width))
|
|
}
|
|
|
|
func (m *Model) refreshStatsContent() {
|
|
m.statsViewport.SetContent(buildStatsContent(m.topAlerts, m.hourStats, m.dayStats, m.width))
|
|
}
|
|
|
|
func (m Model) detailViewportHeight() int {
|
|
h := m.height - 5
|
|
if m.mode == modeComment {
|
|
h -= 2
|
|
}
|
|
if h < 1 {
|
|
h = 1
|
|
}
|
|
return h
|
|
}
|
|
|
|
// ── Column definitions ─────────────────────────────────────────────────────
|
|
|
|
func alertColumns(width int) []table.Column {
|
|
const statusW, timeW = 10, 12
|
|
nameW := width/2 - 14
|
|
if nameW < 20 {
|
|
nameW = 20
|
|
}
|
|
// 10 = bubbles' Padding(0, 1) on each of the five cells.
|
|
ackW := width - nameW - statusW - 2*timeW - 10
|
|
if ackW < 8 {
|
|
ackW = 8
|
|
}
|
|
return []table.Column{
|
|
{Title: "Name", Width: nameW},
|
|
{Title: "Status", Width: statusW},
|
|
{Title: "Started", Width: timeW},
|
|
{Title: "Last Seen", Width: timeW},
|
|
{Title: "Ack By", Width: ackW},
|
|
}
|
|
}
|
|
|
|
func scheduleColumns(width int) []table.Column {
|
|
dateW := 18
|
|
onCallW := width - dateW - 6
|
|
if onCallW < 15 {
|
|
onCallW = 15
|
|
}
|
|
return []table.Column{
|
|
{Title: "Date", Width: dateW},
|
|
{Title: "On-Call", Width: onCallW},
|
|
}
|
|
}
|
|
|
|
func userPickerColumns(width int) []table.Column {
|
|
usernameW := 25
|
|
emailW := width - usernameW - 6
|
|
if emailW < 15 {
|
|
emailW = 15
|
|
}
|
|
return []table.Column{
|
|
{Title: "Username", Width: usernameW},
|
|
{Title: "Email", Width: emailW},
|
|
}
|
|
}
|
|
|
|
func userManageColumns(width int) []table.Column {
|
|
createdW := 12
|
|
usernameW := 25
|
|
emailW := width - usernameW - createdW - 8
|
|
if emailW < 15 {
|
|
emailW = 15
|
|
}
|
|
return []table.Column{
|
|
{Title: "Username", Width: usernameW},
|
|
{Title: "Email", Width: emailW},
|
|
{Title: "Created", Width: createdW},
|
|
}
|
|
}
|
|
|
|
// ── Row builders ───────────────────────────────────────────────────────────
|
|
|
|
func alertRows(alerts []api.Alert) []table.Row {
|
|
now := time.Now()
|
|
rows := make([]table.Row, len(alerts))
|
|
for i, a := range alerts {
|
|
rows[i] = table.Row{a.Name, a.Status, humanAgo(now, a.StartsAt), humanAgo(now, a.ReceivedAt), a.AcknowledgedBy}
|
|
}
|
|
return rows
|
|
}
|
|
|
|
func scheduleRows(days []scheduleDay) []table.Row {
|
|
today := time.Now().UTC().Format("2006-01-02")
|
|
rows := make([]table.Row, len(days))
|
|
for i, d := range days {
|
|
dateStr := d.date.Format("2006-01-02")
|
|
showWeek := i == 0 || d.date.Weekday() == time.Monday
|
|
_, week := d.date.ISOWeek()
|
|
weekPrefix := " "
|
|
if showWeek {
|
|
weekPrefix = fmt.Sprintf("W%02d ", week)
|
|
}
|
|
label := weekPrefix + d.date.Format("Jan 02 Mon")
|
|
if dateStr == today {
|
|
label = weekPrefix + "Today " + d.date.Format("Mon")
|
|
}
|
|
onCall := "—"
|
|
if d.entry != nil {
|
|
onCall = d.entry.Username
|
|
}
|
|
rows[i] = table.Row{label, onCall}
|
|
}
|
|
return rows
|
|
}
|
|
|
|
func buildScheduleDays(window time.Time, entries []api.ScheduleEntry) []scheduleDay {
|
|
entryMap := make(map[string]api.ScheduleEntry, len(entries))
|
|
for _, e := range entries {
|
|
entryMap[e.Date] = e
|
|
}
|
|
days := make([]scheduleDay, 7)
|
|
for i := range days {
|
|
date := window.AddDate(0, 0, i)
|
|
day := scheduleDay{date: date}
|
|
if e, ok := entryMap[date.Format("2006-01-02")]; ok {
|
|
e2 := e
|
|
day.entry = &e2
|
|
}
|
|
days[i] = day
|
|
}
|
|
return days
|
|
}
|
|
|
|
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
|
|
func humanAgo(now, t time.Time) string {
|
|
d := now.Sub(t)
|
|
if d < 0 {
|
|
d = 0
|
|
}
|
|
switch {
|
|
case d < time.Minute:
|
|
return "just now"
|
|
case d < time.Hour:
|
|
return fmt.Sprintf("%dm ago", int(d.Minutes()))
|
|
case d < 24*time.Hour:
|
|
h := int(d.Hours())
|
|
m := int(d.Minutes()) % 60
|
|
if m == 0 {
|
|
return fmt.Sprintf("%dh ago", h)
|
|
}
|
|
return fmt.Sprintf("%dh %dm ago", h, m)
|
|
default:
|
|
days := int(d.Hours()) / 24
|
|
h := int(d.Hours()) % 24
|
|
if h == 0 {
|
|
return fmt.Sprintf("%dd ago", days)
|
|
}
|
|
return fmt.Sprintf("%dd %dh ago", days, h)
|
|
}
|
|
}
|
|
|
|
// ── Commands ───────────────────────────────────────────────────────────────
|
|
|
|
func connectCmd(client *api.Client) tea.Cmd {
|
|
return func() tea.Msg {
|
|
if err := client.HealthCheck(); err != nil {
|
|
return connectErrMsg{err}
|
|
}
|
|
return connectedMsg{}
|
|
}
|
|
}
|
|
|
|
func fetchAlertsCmd(client *api.Client, status string) tea.Cmd {
|
|
return func() tea.Msg {
|
|
alerts, err := client.ListAlerts(status, false, 500)
|
|
if err != nil {
|
|
return fetchDataErrMsg{err}
|
|
}
|
|
return alertsFetchedMsg{alerts}
|
|
}
|
|
}
|
|
|
|
func fetchArchivedAlertsCmd(client *api.Client) tea.Cmd {
|
|
return func() tea.Msg {
|
|
alerts, err := client.ListAlerts("", true, 500)
|
|
if err != nil {
|
|
return fetchDataErrMsg{err}
|
|
}
|
|
return archivedAlertsFetchedMsg{alerts}
|
|
}
|
|
}
|
|
|
|
func archiveAlertCmd(client *api.Client, alertID int64, filterStatus string) tea.Cmd {
|
|
return func() tea.Msg {
|
|
if _, err := client.ArchiveAlert(alertID); err != nil {
|
|
return actionErrMsg{err}
|
|
}
|
|
alerts, err := client.ListAlerts(filterStatus, false, 500)
|
|
if err != nil {
|
|
return actionErrMsg{err}
|
|
}
|
|
return alertArchivedMsg{alerts}
|
|
}
|
|
}
|
|
|
|
func unarchiveAlertCmd(client *api.Client, alertID int64) tea.Cmd {
|
|
return func() tea.Msg {
|
|
if err := client.UnarchiveAlert(alertID); err != nil {
|
|
return actionErrMsg{err}
|
|
}
|
|
alerts, err := client.ListAlerts("", true, 500)
|
|
if err != nil {
|
|
return actionErrMsg{err}
|
|
}
|
|
return alertUnarchivedMsg{alerts}
|
|
}
|
|
}
|
|
|
|
func fetchStatsCmd(client *api.Client) tea.Cmd {
|
|
return func() tea.Msg {
|
|
stats, err := client.GetAlertStats()
|
|
if err != nil {
|
|
return fetchDataErrMsg{err}
|
|
}
|
|
return statsFetchedMsg{*stats}
|
|
}
|
|
}
|
|
|
|
func fetchAlertDetailCmd(client *api.Client, alertID int64) tea.Cmd {
|
|
return func() tea.Msg {
|
|
alert, err := client.GetAlert(alertID)
|
|
if err != nil {
|
|
return alertDetailErrMsg{err}
|
|
}
|
|
comments, err := client.GetComments(alertID)
|
|
if err != nil {
|
|
return alertDetailErrMsg{err}
|
|
}
|
|
return alertDetailFetchedMsg{alert: *alert, comments: comments}
|
|
}
|
|
}
|
|
|
|
func acknowledgeCmd(client *api.Client, alertID int64) tea.Cmd {
|
|
return func() tea.Msg {
|
|
alert, err := client.AcknowledgeAlert(alertID)
|
|
if err != nil {
|
|
return actionErrMsg{err}
|
|
}
|
|
comments, err := client.GetComments(alertID)
|
|
if err != nil {
|
|
return actionErrMsg{err}
|
|
}
|
|
return alertDetailFetchedMsg{alert: *alert, comments: comments}
|
|
}
|
|
}
|
|
|
|
func unacknowledgeCmd(client *api.Client, alertID int64) tea.Cmd {
|
|
return func() tea.Msg {
|
|
if err := client.UnacknowledgeAlert(alertID); err != nil {
|
|
return actionErrMsg{err}
|
|
}
|
|
alert, err := client.GetAlert(alertID)
|
|
if err != nil {
|
|
return actionErrMsg{err}
|
|
}
|
|
comments, err := client.GetComments(alertID)
|
|
if err != nil {
|
|
return actionErrMsg{err}
|
|
}
|
|
return alertDetailFetchedMsg{alert: *alert, comments: comments}
|
|
}
|
|
}
|
|
|
|
func addCommentCmd(client *api.Client, alertID int64, content string) tea.Cmd {
|
|
return func() tea.Msg {
|
|
if _, err := client.AddComment(alertID, content); err != nil {
|
|
return actionErrMsg{err}
|
|
}
|
|
alert, err := client.GetAlert(alertID)
|
|
if err != nil {
|
|
return actionErrMsg{err}
|
|
}
|
|
comments, err := client.GetComments(alertID)
|
|
if err != nil {
|
|
return actionErrMsg{err}
|
|
}
|
|
return alertDetailFetchedMsg{alert: *alert, comments: comments}
|
|
}
|
|
}
|
|
|
|
func deleteCommentCmd(client *api.Client, alertID, commentID int64) tea.Cmd {
|
|
return func() tea.Msg {
|
|
if err := client.DeleteComment(alertID, commentID); err != nil {
|
|
return actionErrMsg{err}
|
|
}
|
|
alert, err := client.GetAlert(alertID)
|
|
if err != nil {
|
|
return actionErrMsg{err}
|
|
}
|
|
comments, err := client.GetComments(alertID)
|
|
if err != nil {
|
|
return actionErrMsg{err}
|
|
}
|
|
return alertDetailFetchedMsg{alert: *alert, comments: comments}
|
|
}
|
|
}
|
|
|
|
func fetchDetailStatsCmd(client *api.Client) tea.Cmd {
|
|
return func() tea.Msg {
|
|
top, err := client.GetTopAlerts(10)
|
|
if err != nil {
|
|
return detailStatsErrMsg{err}
|
|
}
|
|
byHour, err := client.GetStatsByHour()
|
|
if err != nil {
|
|
return detailStatsErrMsg{err}
|
|
}
|
|
byDay, err := client.GetStatsByDay()
|
|
if err != nil {
|
|
return detailStatsErrMsg{err}
|
|
}
|
|
return detailStatsFetchedMsg{top: top, byHour: byHour, byDay: byDay}
|
|
}
|
|
}
|
|
|
|
func fetchScheduleCmd(client *api.Client, from, to time.Time) tea.Cmd {
|
|
return func() tea.Msg {
|
|
entries, err := client.GetSchedule(from.Format("2006-01-02"), to.Format("2006-01-02"))
|
|
if err != nil {
|
|
return scheduleFetchErrMsg{err}
|
|
}
|
|
current, err := client.GetCurrentOnCall()
|
|
if err != nil {
|
|
return scheduleFetchErrMsg{err}
|
|
}
|
|
return scheduleFetchedMsg{entries: entries, current: current}
|
|
}
|
|
}
|
|
|
|
func assignScheduleCmd(client *api.Client, userID int64, dates []string, from, to time.Time) tea.Cmd {
|
|
return func() tea.Msg {
|
|
if _, err := client.AssignSchedule(userID, dates); err != nil {
|
|
return scheduleActionErrMsg{err}
|
|
}
|
|
entries, err := client.GetSchedule(from.Format("2006-01-02"), to.Format("2006-01-02"))
|
|
if err != nil {
|
|
return scheduleActionErrMsg{err}
|
|
}
|
|
current, err := client.GetCurrentOnCall()
|
|
if err != nil {
|
|
return scheduleActionErrMsg{err}
|
|
}
|
|
return scheduleFetchedMsg{entries: entries, current: current}
|
|
}
|
|
}
|
|
|
|
func deleteScheduleEntryCmd(client *api.Client, entryID int64, from, to time.Time) tea.Cmd {
|
|
return func() tea.Msg {
|
|
if err := client.DeleteScheduleEntry(entryID); err != nil {
|
|
return scheduleActionErrMsg{err}
|
|
}
|
|
entries, err := client.GetSchedule(from.Format("2006-01-02"), to.Format("2006-01-02"))
|
|
if err != nil {
|
|
return scheduleActionErrMsg{err}
|
|
}
|
|
current, err := client.GetCurrentOnCall()
|
|
if err != nil {
|
|
return scheduleActionErrMsg{err}
|
|
}
|
|
return scheduleFetchedMsg{entries: entries, current: current}
|
|
}
|
|
}
|
|
|
|
func fetchUsersCmd(client *api.Client) tea.Cmd {
|
|
return func() tea.Msg {
|
|
users, err := client.ListUsers()
|
|
if err != nil {
|
|
return usersFetchedMsg{} // empty on error, statusMsg set elsewhere
|
|
}
|
|
return usersFetchedMsg{users: users}
|
|
}
|
|
}
|
|
|
|
func createUserCmd(client *api.Client, username, email string) tea.Cmd {
|
|
return func() tea.Msg {
|
|
if _, err := client.CreateUser(username, email); err != nil {
|
|
return userActionErrMsg{err}
|
|
}
|
|
users, err := client.ListUsers()
|
|
if err != nil {
|
|
return userActionErrMsg{err}
|
|
}
|
|
return usersFetchedMsg{users: users}
|
|
}
|
|
}
|
|
|
|
func deleteUserCmd(client *api.Client, userID int64) tea.Cmd {
|
|
return func() tea.Msg {
|
|
if err := client.DeleteUser(userID); err != nil {
|
|
return userActionErrMsg{err}
|
|
}
|
|
users, err := client.ListUsers()
|
|
if err != nil {
|
|
return userActionErrMsg{err}
|
|
}
|
|
return usersFetchedMsg{users: users}
|
|
}
|
|
}
|
|
|
|
func createAPIKeyCmd(client *api.Client, userID int64, name string) tea.Cmd {
|
|
return func() tea.Msg {
|
|
key, err := client.CreateAPIKey(userID, name)
|
|
if err != nil {
|
|
return userActionErrMsg{err}
|
|
}
|
|
return apiKeyCreatedMsg{key: *key}
|
|
}
|
|
}
|
|
|
|
func deleteAPIKeyCmd(client *api.Client, userID, keyID int64) tea.Cmd {
|
|
return func() tea.Msg {
|
|
if err := client.DeleteAPIKey(userID, keyID); err != nil {
|
|
return userActionErrMsg{err}
|
|
}
|
|
return apiKeyRevokedMsg{}
|
|
}
|
|
}
|
|
|
|
func tickCmd(interval time.Duration) tea.Cmd {
|
|
return tea.Tick(interval, func(t time.Time) tea.Msg {
|
|
return tickMsg(t)
|
|
})
|
|
}
|
|
|
|
func clearStatusCmd() tea.Cmd {
|
|
return tea.Tick(3*time.Second, func(time.Time) tea.Msg {
|
|
return clearStatusMsg{}
|
|
})
|
|
}
|