251 lines
7.1 KiB
Go
251 lines
7.1 KiB
Go
package ui
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/charmbracelet/bubbles/list"
|
|
"github.com/charmbracelet/bubbles/textinput"
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
|
|
"ssh-access-tui/internal/model"
|
|
)
|
|
|
|
func (m *Model) refreshGroupsList() {
|
|
st := m.loadState()
|
|
items := make([]list.Item, 0, len(st.Groups))
|
|
for _, g := range st.Groups {
|
|
desc := fmt.Sprintf("%d хостов", len(g.Hosts))
|
|
if g.Description != "" {
|
|
desc += " — " + g.Description
|
|
}
|
|
items = append(items, simpleItem{id: g.Name, title: g.Name, desc: desc})
|
|
}
|
|
m.groupsList = newList(items,
|
|
"Группы [n]новая [e]изменить [enter]хосты группы [d]удалить [esc]назад",
|
|
m.groupsList.Width(), m.groupsList.Height())
|
|
}
|
|
|
|
func (m Model) updateGroups(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
if key, ok := msg.(tea.KeyMsg); ok {
|
|
switch key.String() {
|
|
case "esc":
|
|
m.screen = scrMenu
|
|
m.err, m.status = "", ""
|
|
return m, nil
|
|
case "n":
|
|
m.groupFormEditTarget = ""
|
|
for i := range m.groupForm {
|
|
m.groupForm[i] = textinput.New()
|
|
}
|
|
m.groupForm[0].Placeholder = "имя группы, например team-billing"
|
|
m.groupForm[1].Placeholder = "описание (необязательно)"
|
|
m.groupFormFocus = 0
|
|
m.groupForm[0].Focus()
|
|
m.err = ""
|
|
m.screen = scrGroupForm
|
|
return m, nil
|
|
case "e":
|
|
sel, ok := m.groupsList.SelectedItem().(simpleItem)
|
|
if !ok {
|
|
return m, nil
|
|
}
|
|
st := m.loadState()
|
|
g := st.FindGroup(sel.id)
|
|
if g == nil {
|
|
return m, nil
|
|
}
|
|
m.groupFormEditTarget = g.Name
|
|
for i := range m.groupForm {
|
|
m.groupForm[i] = textinput.New()
|
|
}
|
|
m.groupForm[0].SetValue(g.Name)
|
|
m.groupForm[1].SetValue(g.Description)
|
|
m.groupFormFocus = 0
|
|
m.groupForm[0].Focus()
|
|
m.err = ""
|
|
m.screen = scrGroupForm
|
|
return m, nil
|
|
case "enter":
|
|
sel, ok := m.groupsList.SelectedItem().(simpleItem)
|
|
if !ok {
|
|
return m, nil
|
|
}
|
|
m.selectedGroup = sel.id
|
|
st := m.loadState()
|
|
g := st.FindGroup(sel.id)
|
|
m.groupHostsPickSel = map[string]bool{}
|
|
if g != nil {
|
|
for _, hn := range g.Hosts {
|
|
m.groupHostsPickSel[hn] = true
|
|
}
|
|
}
|
|
m.groupHostsPickOrder = nil
|
|
for _, h := range st.Hosts {
|
|
m.groupHostsPickOrder = append(m.groupHostsPickOrder, h.Name)
|
|
}
|
|
m.groupHostsPickCursor = 0
|
|
m.screen = scrGroupHostsPick
|
|
return m, nil
|
|
case "d":
|
|
sel, ok := m.groupsList.SelectedItem().(simpleItem)
|
|
if !ok {
|
|
return m, nil
|
|
}
|
|
st := m.loadState()
|
|
affected := st.UsernamesInGroup(sel.id)
|
|
msg := fmt.Sprintf(
|
|
"Удалить группу %q?\n\n"+
|
|
"Это затронет пользователей: %d — группа будет снята с них, и они\n"+
|
|
"немедленно потеряют доступ ко всем сервисам, которые давала только эта\n"+
|
|
"группа.",
|
|
sel.id, len(affected))
|
|
m = m.startConfirm(confirmDeleteGroup, sel.id, msg, scrGroups)
|
|
return m, nil
|
|
}
|
|
}
|
|
var cmd tea.Cmd
|
|
m.groupsList, cmd = m.groupsList.Update(msg)
|
|
return m, cmd
|
|
}
|
|
|
|
func (m Model) viewGroups() string {
|
|
return borderStyle.Render(m.groupsList.View())
|
|
}
|
|
|
|
func (m Model) updateGroupForm(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
if key, ok := msg.(tea.KeyMsg); ok {
|
|
switch key.String() {
|
|
case "esc":
|
|
m.screen = scrGroups
|
|
return m, nil
|
|
case "tab", "down":
|
|
m.groupForm[m.groupFormFocus].Blur()
|
|
m.groupFormFocus = (m.groupFormFocus + 1) % len(m.groupForm)
|
|
m.groupForm[m.groupFormFocus].Focus()
|
|
return m, nil
|
|
case "enter":
|
|
name := m.groupForm[0].Value()
|
|
desc := m.groupForm[1].Value()
|
|
if name == "" {
|
|
m.err = "имя группы не может быть пустым"
|
|
return m, nil
|
|
}
|
|
if !model.ValidName(name) {
|
|
m.err = validNameErr("группа", name)
|
|
return m, nil
|
|
}
|
|
target := m.groupFormEditTarget
|
|
m.err = ""
|
|
if target == "" {
|
|
m.mutate("создана группа "+name, func(st *model.State) error {
|
|
if st.FindGroup(name) != nil {
|
|
return fmt.Errorf("группа %q уже существует", name)
|
|
}
|
|
st.Groups = append(st.Groups, model.Group{Name: name, Description: desc})
|
|
return nil
|
|
})
|
|
} else {
|
|
m.mutate("изменена группа "+target, func(st *model.State) error {
|
|
if name != target && st.FindGroup(name) != nil {
|
|
return fmt.Errorf("группа %q уже существует", name)
|
|
}
|
|
if name != target {
|
|
st.RenameGroup(target, name)
|
|
}
|
|
g := st.FindGroup(name)
|
|
if g == nil {
|
|
return fmt.Errorf("group %s not found", name)
|
|
}
|
|
g.Description = desc
|
|
return nil
|
|
})
|
|
}
|
|
m.screen = scrGroups
|
|
m.refreshGroupsList()
|
|
return m, nil
|
|
}
|
|
}
|
|
var cmd tea.Cmd
|
|
m.groupForm[m.groupFormFocus], cmd = m.groupForm[m.groupFormFocus].Update(msg)
|
|
return m, cmd
|
|
}
|
|
|
|
func (m Model) viewGroupForm() string {
|
|
title := "Новая группа"
|
|
if m.groupFormEditTarget != "" {
|
|
title = fmt.Sprintf("Изменить группу %q", m.groupFormEditTarget)
|
|
}
|
|
body := fmt.Sprintf("%s\n\nИмя: %s\nОписание: %s\n\n[tab] след. поле [enter] сохранить [esc] отмена",
|
|
title, m.groupForm[0].View(), m.groupForm[1].View())
|
|
return borderStyle.Render(body)
|
|
}
|
|
|
|
// --- pick which catalog hosts belong to the selected group ---
|
|
|
|
func (m Model) updateGroupHostsPick(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
if key, ok := msg.(tea.KeyMsg); ok {
|
|
switch key.String() {
|
|
case "esc":
|
|
m.screen = scrGroups
|
|
m.refreshGroupsList()
|
|
return m, nil
|
|
case "up", "k":
|
|
if m.groupHostsPickCursor > 0 {
|
|
m.groupHostsPickCursor--
|
|
}
|
|
return m, nil
|
|
case "down", "j":
|
|
if m.groupHostsPickCursor < len(m.groupHostsPickOrder)-1 {
|
|
m.groupHostsPickCursor++
|
|
}
|
|
return m, nil
|
|
case " ":
|
|
if len(m.groupHostsPickOrder) > 0 {
|
|
h := m.groupHostsPickOrder[m.groupHostsPickCursor]
|
|
m.groupHostsPickSel[h] = !m.groupHostsPickSel[h]
|
|
}
|
|
return m, nil
|
|
case "enter":
|
|
var hosts []string
|
|
for _, h := range m.groupHostsPickOrder {
|
|
if m.groupHostsPickSel[h] {
|
|
hosts = append(hosts, h)
|
|
}
|
|
}
|
|
groupName := m.selectedGroup
|
|
m.mutate("изменён список хостов группы "+groupName, func(st *model.State) error {
|
|
g := st.FindGroup(groupName)
|
|
if g == nil {
|
|
return fmt.Errorf("group %s not found", groupName)
|
|
}
|
|
g.Hosts = hosts
|
|
return resyncGroupUsers(st, groupName)
|
|
})
|
|
m.screen = scrGroups
|
|
m.refreshGroupsList()
|
|
return m, nil
|
|
}
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
func (m Model) viewGroupHostsPick() string {
|
|
body := fmt.Sprintf("Какие хосты каталога входят в группу %q\n\n", m.selectedGroup)
|
|
if len(m.groupHostsPickOrder) == 0 {
|
|
body += warnStyle.Render("В каталоге ещё нет хостов — сначала добавьте их в разделе «Хосты и сервисы».") + "\n"
|
|
}
|
|
for i, h := range m.groupHostsPickOrder {
|
|
cursor := " "
|
|
if i == m.groupHostsPickCursor {
|
|
cursor = "> "
|
|
}
|
|
check := "[ ]"
|
|
if m.groupHostsPickSel[h] {
|
|
check = "[x]"
|
|
}
|
|
body += fmt.Sprintf("%s%s %s\n", cursor, check, h)
|
|
}
|
|
body += "\n[space] отметить [enter] сохранить [esc] назад"
|
|
return borderStyle.Render(body)
|
|
}
|