365 lines
8.2 KiB
Go
365 lines
8.2 KiB
Go
package dns
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"git.juancwu.dev/juancwu/porkbacon/internal/porkbun"
|
|
"git.juancwu.dev/juancwu/porkbacon/internal/ui/defaults"
|
|
"git.juancwu.dev/juancwu/porkbacon/internal/ui/messages"
|
|
"github.com/charmbracelet/bubbles/key"
|
|
"github.com/charmbracelet/bubbles/list"
|
|
"github.com/charmbracelet/bubbles/spinner"
|
|
"github.com/charmbracelet/bubbles/textinput"
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
"github.com/charmbracelet/lipgloss"
|
|
)
|
|
|
|
type State int
|
|
|
|
const (
|
|
StateList State = iota
|
|
StateForm
|
|
StateConfirmDelete
|
|
)
|
|
|
|
type Mode int
|
|
|
|
const (
|
|
ModeCreate Mode = iota
|
|
ModeEdit
|
|
)
|
|
|
|
type item struct {
|
|
record porkbun.DNSRecord
|
|
}
|
|
|
|
func (i item) Title() string {
|
|
return fmt.Sprintf("%s %s", i.record.Type, i.record.Name)
|
|
}
|
|
|
|
func (i item) Description() string {
|
|
return i.record.Content
|
|
}
|
|
|
|
func (i item) FilterValue() string {
|
|
return i.record.Name + " " + i.record.Type + " " + i.record.Content
|
|
}
|
|
|
|
type Model struct {
|
|
state State
|
|
mode Mode
|
|
list list.Model
|
|
spinner spinner.Model
|
|
inputs []textinput.Model
|
|
focusIndex int
|
|
client *porkbun.Client
|
|
domain string
|
|
selectedRecord *porkbun.DNSRecord
|
|
loading bool
|
|
err error
|
|
statusMsg string
|
|
}
|
|
|
|
func NewListModel(client *porkbun.Client) Model {
|
|
l := list.New([]list.Item{}, list.NewDefaultDelegate(), 0, 0)
|
|
l.Title = "DNS Records"
|
|
l.AdditionalFullHelpKeys = func() []key.Binding {
|
|
return []key.Binding{
|
|
key.NewBinding(key.WithKeys("a"), key.WithHelp("a", "add record")),
|
|
key.NewBinding(key.WithKeys("e"), key.WithHelp("e", "edit record")),
|
|
key.NewBinding(key.WithKeys("d"), key.WithHelp("d", "delete record")),
|
|
key.NewBinding(key.WithKeys("r"), key.WithHelp("r", "refresh")),
|
|
}
|
|
}
|
|
l.AdditionalShortHelpKeys = func() []key.Binding {
|
|
return []key.Binding{
|
|
key.NewBinding(key.WithKeys("a"), key.WithHelp("a", "add")),
|
|
key.NewBinding(key.WithKeys("e"), key.WithHelp("e", "edit")),
|
|
key.NewBinding(key.WithKeys("d"), key.WithHelp("d", "del")),
|
|
}
|
|
}
|
|
|
|
s := spinner.New()
|
|
s.Spinner = spinner.Dot
|
|
s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
|
|
|
|
inputs := make([]textinput.Model, 5)
|
|
for i := range inputs {
|
|
inputs[i] = textinput.New()
|
|
inputs[i].Width = defaults.INPUT_WIDTH
|
|
}
|
|
inputs[0].Placeholder = "Type (required)"
|
|
inputs[1].Placeholder = "Name (leave empty for root)"
|
|
inputs[2].Placeholder = "Content (required)"
|
|
inputs[3].Placeholder = "TTL (optional)"
|
|
inputs[4].Placeholder = "Priority (optional)"
|
|
|
|
return Model{
|
|
state: StateList,
|
|
list: l,
|
|
spinner: s,
|
|
inputs: inputs,
|
|
client: client,
|
|
}
|
|
}
|
|
|
|
func (m Model) Init() tea.Cmd {
|
|
return nil
|
|
}
|
|
|
|
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
var cmd tea.Cmd
|
|
var cmds []tea.Cmd
|
|
|
|
switch msg := msg.(type) {
|
|
case messages.DomainSelectedMsg:
|
|
m.domain = msg.Domain.Domain
|
|
m.list.Title = fmt.Sprintf("DNS Records for %s", m.domain)
|
|
m.list.SetItems(nil)
|
|
m.state = StateList
|
|
|
|
case messages.SwitchPageMsg:
|
|
if msg.Page == messages.PageDNSList {
|
|
if len(m.list.Items()) == 0 {
|
|
return m.refresh()
|
|
}
|
|
}
|
|
|
|
case messages.RefreshMsg:
|
|
if m.domain != "" {
|
|
return m.refresh()
|
|
}
|
|
|
|
case *porkbun.DNSRecordsResponse:
|
|
m.loading = false
|
|
items := []list.Item{}
|
|
for _, r := range msg.Records {
|
|
items = append(items, item{record: r})
|
|
}
|
|
m.list.SetItems(items)
|
|
m.statusMsg = "Records loaded."
|
|
|
|
case messages.ErrorMsg:
|
|
m.loading = false
|
|
m.err = msg
|
|
m.statusMsg = "Error occurred."
|
|
|
|
case tea.KeyMsg:
|
|
// Global keys for this model
|
|
if m.state == StateList && !m.loading {
|
|
switch msg.String() {
|
|
case "esc":
|
|
return m, func() tea.Msg {
|
|
return messages.SwitchPageMsg{Page: messages.PageDomainMenu}
|
|
}
|
|
case "r":
|
|
return m.refresh()
|
|
case "a":
|
|
m.mode = ModeCreate
|
|
m.state = StateForm
|
|
m.resetInputs()
|
|
m.FocusAt(0)
|
|
return m, nil
|
|
case "e":
|
|
if i, ok := m.list.SelectedItem().(item); ok {
|
|
m.mode = ModeEdit
|
|
m.selectedRecord = &i.record
|
|
m.state = StateForm
|
|
m.populateInputs(i.record)
|
|
m.FocusAt(0)
|
|
return m, nil
|
|
}
|
|
case "d":
|
|
if i, ok := m.list.SelectedItem().(item); ok {
|
|
m.selectedRecord = &i.record
|
|
m.state = StateConfirmDelete
|
|
return m, nil
|
|
}
|
|
}
|
|
} else if m.state == StateConfirmDelete {
|
|
switch msg.String() {
|
|
case "y", "Y", "enter":
|
|
return m.deleteRecord()
|
|
case "n", "N", "esc":
|
|
m.state = StateList
|
|
m.statusMsg = "Deletion cancelled."
|
|
return m, nil
|
|
}
|
|
} else if m.state == StateForm {
|
|
switch msg.String() {
|
|
case "esc":
|
|
m.state = StateList
|
|
m.statusMsg = "Cancelled."
|
|
return m, nil
|
|
case "tab", "shift+tab", "enter", "up", "down":
|
|
s := msg.String()
|
|
if s == "enter" {
|
|
if m.focusIndex == len(m.inputs)-1 {
|
|
return m.submitForm()
|
|
}
|
|
m.focusIndex++
|
|
} else if s == "up" || s == "shift+tab" {
|
|
m.focusIndex--
|
|
} else if s == "down" || s == "tab" {
|
|
m.focusIndex++
|
|
}
|
|
|
|
if m.focusIndex > len(m.inputs)-1 {
|
|
m.focusIndex = 0
|
|
} else if m.focusIndex < 0 {
|
|
m.focusIndex = len(m.inputs) - 1
|
|
}
|
|
|
|
cmds := make([]tea.Cmd, len(m.inputs))
|
|
for i := 0; i <= len(m.inputs)-1; i++ {
|
|
if i == m.focusIndex {
|
|
cmds[i] = m.inputs[i].Focus()
|
|
continue
|
|
}
|
|
m.inputs[i].Blur()
|
|
}
|
|
return m, tea.Batch(cmds...)
|
|
}
|
|
}
|
|
|
|
case tea.WindowSizeMsg:
|
|
m.list.SetWidth(msg.Width)
|
|
m.list.SetHeight(msg.Height)
|
|
}
|
|
|
|
if m.loading {
|
|
m.spinner, cmd = m.spinner.Update(msg)
|
|
cmds = append(cmds, cmd)
|
|
}
|
|
|
|
if m.state == StateList {
|
|
m.list, cmd = m.list.Update(msg)
|
|
cmds = append(cmds, cmd)
|
|
} else if m.state == StateForm {
|
|
for i := range m.inputs {
|
|
m.inputs[i], cmd = m.inputs[i].Update(msg)
|
|
cmds = append(cmds, cmd)
|
|
}
|
|
}
|
|
|
|
return m, tea.Batch(cmds...)
|
|
}
|
|
|
|
func (m Model) View() string {
|
|
if m.loading {
|
|
return fmt.Sprintf("\n\n %s Processing...", m.spinner.View())
|
|
}
|
|
|
|
if m.err != nil {
|
|
return fmt.Sprintf("Error: %v\n\n(Press r to refresh, esc to go back)", m.err)
|
|
}
|
|
|
|
if m.state == StateConfirmDelete {
|
|
return fmt.Sprintf(
|
|
"Are you sure you want to delete this record?\n\n%s %s\n\n(y/n)",
|
|
m.selectedRecord.Type, m.selectedRecord.Name,
|
|
)
|
|
}
|
|
|
|
if m.state == StateForm {
|
|
var b strings.Builder
|
|
title := "Create Record"
|
|
if m.mode == ModeEdit {
|
|
title = "Edit Record"
|
|
}
|
|
b.WriteString(title + "\n\n")
|
|
|
|
labels := []string{"Type", "Name", "Content", "TTL", "Priority"}
|
|
|
|
for i := range m.inputs {
|
|
b.WriteString(fmt.Sprintf("% -10s %s\n", labels[i], m.inputs[i].View()))
|
|
}
|
|
|
|
b.WriteString("\n(Press Enter to next/submit, Esc to cancel)\n")
|
|
return b.String()
|
|
}
|
|
|
|
return m.list.View()
|
|
}
|
|
|
|
func (m *Model) refresh() (tea.Model, tea.Cmd) {
|
|
m.loading = true
|
|
m.err = nil
|
|
return m, tea.Batch(
|
|
func() tea.Msg {
|
|
resp, err := m.client.RetrieveDNSRecords(m.domain)
|
|
if err != nil {
|
|
return messages.ErrorMsg(err)
|
|
}
|
|
return resp
|
|
},
|
|
m.spinner.Tick,
|
|
)
|
|
}
|
|
|
|
func (m *Model) deleteRecord() (tea.Model, tea.Cmd) {
|
|
m.loading = true
|
|
id := m.selectedRecord.ID
|
|
return m, tea.Batch(
|
|
func() tea.Msg {
|
|
err := m.client.DeleteDNSRecord(m.domain, id)
|
|
if err != nil {
|
|
return messages.ErrorMsg(err)
|
|
}
|
|
return messages.RefreshMsg{}
|
|
},
|
|
m.spinner.Tick,
|
|
)
|
|
}
|
|
|
|
func (m *Model) submitForm() (tea.Model, tea.Cmd) {
|
|
m.loading = true
|
|
|
|
record := porkbun.DNSRecord{
|
|
Type: m.inputs[0].Value(),
|
|
Name: m.inputs[1].Value(),
|
|
Content: m.inputs[2].Value(),
|
|
TTL: porkbun.NullString{Value: m.inputs[3].Value(), Valid: true},
|
|
Priority: porkbun.NullString{Value: m.inputs[4].Value(), Valid: true},
|
|
}
|
|
|
|
return m, tea.Batch(
|
|
func() tea.Msg {
|
|
var err error
|
|
if m.mode == ModeCreate {
|
|
err = m.client.CreateDNSRecord(m.domain, record)
|
|
} else {
|
|
err = m.client.EditDNSRecord(m.domain, m.selectedRecord.ID, record)
|
|
}
|
|
|
|
if err != nil {
|
|
return messages.ErrorMsg(err)
|
|
}
|
|
return messages.RefreshMsg{}
|
|
},
|
|
m.spinner.Tick,
|
|
)
|
|
}
|
|
|
|
func (m *Model) resetInputs() {
|
|
for i := range m.inputs {
|
|
m.inputs[i].SetValue("")
|
|
}
|
|
}
|
|
|
|
func (m *Model) populateInputs(r porkbun.DNSRecord) {
|
|
m.inputs[0].SetValue(r.Type)
|
|
m.inputs[1].SetValue(r.Name)
|
|
m.inputs[2].SetValue(r.Content)
|
|
m.inputs[3].SetValue(r.TTL.String())
|
|
m.inputs[4].SetValue(r.Priority.String())
|
|
}
|
|
|
|
func (m *Model) FocusAt(index int) {
|
|
for i := range m.inputs {
|
|
m.inputs[i].Blur()
|
|
}
|
|
m.inputs[index].Focus()
|
|
m.focusIndex = index
|
|
}
|