292 lines
8.4 KiB
Go
292 lines
8.4 KiB
Go
package gitctl
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/charmbracelet/bubbles/list"
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
)
|
|
|
|
type detailKind int
|
|
|
|
const (
|
|
detailRepo detailKind = iota
|
|
detailNS
|
|
detailKey
|
|
)
|
|
|
|
type detailModel struct {
|
|
kind detailKind
|
|
repo repoRow
|
|
ns nsRow
|
|
key sshKeyRow
|
|
grants []grantRow
|
|
list list.Model
|
|
loaded bool
|
|
permissions bool
|
|
err string
|
|
w, h int
|
|
}
|
|
|
|
func newNSDetail(ns nsRow, w, h int) detailModel {
|
|
d := detailModel{kind: detailNS, ns: ns, w: w, h: h}
|
|
d.list = newListModel(nil, w, listHeight(h))
|
|
return d
|
|
}
|
|
|
|
func newRepoDetail(repo repoRow, w, h int) detailModel {
|
|
d := detailModel{kind: detailRepo, repo: repo, w: w, h: h}
|
|
d.list = newListModel(nil, w, listHeight(h))
|
|
return d
|
|
}
|
|
|
|
func newKeyDetail(key sshKeyRow, w, h int) detailModel {
|
|
d := detailModel{kind: detailKey, key: key, w: w, h: h}
|
|
d.list = newListModel(nil, w, listHeight(h))
|
|
return d
|
|
}
|
|
|
|
func (d detailModel) resize(w, h int) detailModel {
|
|
d.w, d.h = w, h
|
|
d.list.SetSize(w, listHeight(h))
|
|
return d
|
|
}
|
|
|
|
func (d detailModel) withGrants(grants []grantRow, errStr string) detailModel {
|
|
d.grants = grants
|
|
d.err = errStr
|
|
d.loaded = true
|
|
d.list = newListModel(grantItems(grants), d.w, listHeight(d.h))
|
|
return d
|
|
}
|
|
|
|
func (d detailModel) selectedGrant() (grantRow, bool) {
|
|
item := d.list.SelectedItem()
|
|
if item == nil {
|
|
return grantRow{}, false
|
|
}
|
|
g, ok := item.(grantItem)
|
|
return g.g, ok
|
|
}
|
|
|
|
func (d detailModel) Update(msg tea.Msg) (detailModel, tea.Cmd) {
|
|
if d.permissions && d.list.FilterState() == list.Filtering {
|
|
var cmd tea.Cmd
|
|
d.list, cmd = d.list.Update(msg)
|
|
return d, cmd
|
|
}
|
|
k, ok := msg.(tea.KeyMsg)
|
|
if !ok {
|
|
var cmd tea.Cmd
|
|
d.list, cmd = d.list.Update(msg)
|
|
return d, cmd
|
|
}
|
|
switch k.String() {
|
|
case "ctrl+c", "q":
|
|
return d, tea.Quit
|
|
case "esc":
|
|
if d.permissions && d.list.FilterState() == list.FilterApplied {
|
|
d.list.ResetFilter()
|
|
return d, nil
|
|
}
|
|
if d.permissions && d.kind != detailKey {
|
|
d.permissions = false
|
|
return d, nil
|
|
}
|
|
return d, func() tea.Msg { return detailBackMsg{} }
|
|
}
|
|
if !d.permissions && d.kind != detailKey && (k.String() == "a" || k.String() == "enter") {
|
|
d.permissions = true
|
|
return d, nil
|
|
}
|
|
if cmd, handled := d.handleQuickKey(k); handled {
|
|
return d, cmd
|
|
}
|
|
if d.permissions || d.kind == detailKey {
|
|
var cmd tea.Cmd
|
|
d.list, cmd = d.list.Update(msg)
|
|
return d, cmd
|
|
}
|
|
return d, nil
|
|
}
|
|
|
|
func (d detailModel) handleQuickKey(k tea.KeyMsg) (tea.Cmd, bool) {
|
|
path := d.repo.Ns + "/" + d.repo.Name
|
|
switch d.kind {
|
|
case detailNS:
|
|
if !d.permissions {
|
|
switch k.String() {
|
|
case "m":
|
|
return emitOp(opRequest{a: opMoveNS(d.ns.Name), ret: "list"}), true
|
|
case "d":
|
|
return emitOp(opRequest{a: opDeleteNS(d.ns.Name), ret: "list"}), true
|
|
}
|
|
return nil, false
|
|
}
|
|
switch k.String() {
|
|
case "g":
|
|
return emitOp(opRequest{a: opGrantSSHKey(), prefill: map[string]string{"target": d.ns.Name}, focus: 1, ret: "detail"}), true
|
|
case "p":
|
|
return emitOp(opRequest{a: opGrantPAT(), prefill: map[string]string{"target": d.ns.Name}, focus: 1, ret: "detail"}), true
|
|
case "c":
|
|
return emitOp(opRequest{a: opChangePassword(d.ns.Name), ret: "detail"}), true
|
|
case "u":
|
|
if !d.ns.HasPassword {
|
|
return nil, true
|
|
}
|
|
return emitOp(opRequest{a: opRemovePassword(d.ns.Name), ret: "detail"}), true
|
|
case "x":
|
|
g, ok := d.selectedGrant()
|
|
if !ok {
|
|
return nil, true
|
|
}
|
|
return emitOp(opRequest{a: opACLDelete(g.AclID), ret: "detail"}), true
|
|
case "r":
|
|
g, ok := d.selectedGrant()
|
|
if !ok {
|
|
return nil, true
|
|
}
|
|
return emitOp(opRequest{a: opRotatePerm(g), ret: "detail"}), true
|
|
}
|
|
case detailRepo:
|
|
if !d.permissions {
|
|
switch k.String() {
|
|
case "v":
|
|
return emitOp(opRequest{a: opToggleRepoVisibility(d.repo), ret: "detail"}), true
|
|
case "d":
|
|
return emitOp(opRequest{a: opDeleteRepo(d.repo.Ns, d.repo.Name), ret: "list"}), true
|
|
case "m":
|
|
return emitOp(opRequest{a: opMoveRepo(path), focus: 0, ret: "list"}), true
|
|
}
|
|
return nil, false
|
|
}
|
|
switch k.String() {
|
|
case "g":
|
|
return emitOp(opRequest{a: opGrantSSHKey(), prefill: map[string]string{"target": path}, focus: 1, ret: "detail"}), true
|
|
case "p":
|
|
return emitOp(opRequest{a: opGrantPAT(), prefill: map[string]string{"target": path}, focus: 1, ret: "detail"}), true
|
|
case "x":
|
|
g, ok := d.selectedGrant()
|
|
if !ok {
|
|
return nil, true
|
|
}
|
|
return emitOp(opRequest{a: opACLDelete(g.AclID), ret: "detail"}), true
|
|
case "r":
|
|
g, ok := d.selectedGrant()
|
|
if !ok {
|
|
return nil, true
|
|
}
|
|
return emitOp(opRequest{a: opRotatePerm(g), ret: "detail"}), true
|
|
}
|
|
case detailKey:
|
|
switch k.String() {
|
|
case "a":
|
|
line, err := reconstructAuthorizedLine(d.key.PublicKey)
|
|
if err != nil {
|
|
return nil, true
|
|
}
|
|
return emitOp(opRequest{a: opGrantSSHKey(), prefill: map[string]string{"key": line}, focus: 0, ret: "detail"}), true
|
|
case "x":
|
|
g, ok := d.selectedGrant()
|
|
if !ok {
|
|
return nil, true
|
|
}
|
|
return emitOp(opRequest{a: opACLDelete(g.AclID), ret: "detail"}), true
|
|
case "r":
|
|
g, ok := d.selectedGrant()
|
|
if !ok {
|
|
return nil, true
|
|
}
|
|
return emitOp(opRequest{a: opRotatePerm(g), ret: "detail"}), true
|
|
}
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
func (d detailModel) View() string {
|
|
if !d.permissions && d.kind != detailKey {
|
|
return d.infoView()
|
|
}
|
|
var b strings.Builder
|
|
switch d.kind {
|
|
case detailNS:
|
|
b.WriteString(detailTitleStyle.Render("gitctl · Namespace Permissions · " + d.ns.Name))
|
|
b.WriteString("\n\n " + fieldLabelStyle.Render(fmt.Sprintf("Grants (%d):", len(d.grants))) + "\n")
|
|
case detailRepo:
|
|
b.WriteString(detailTitleStyle.Render("gitctl · Repo Permissions · " + d.repo.Ns + "/" + d.repo.Name))
|
|
b.WriteString("\n\n " + fieldLabelStyle.Render(fmt.Sprintf("Grants (%d):", len(d.grants))) + "\n")
|
|
case detailKey:
|
|
c := d.key.Comment
|
|
if c == "" {
|
|
c = "(no comment)"
|
|
}
|
|
b.WriteString(detailTitleStyle.Render("gitctl · SSH Key · " + c))
|
|
b.WriteString("\n")
|
|
last := "-"
|
|
if d.key.LastUsedAt != nil {
|
|
last = fmtTime(*d.key.LastUsedAt)
|
|
}
|
|
b.WriteString(" " + fieldLabelStyle.Render("type:") + " " + d.key.KeyType +
|
|
" " + fieldLabelStyle.Render("fingerprint:") + " " + shortFP(d.key.Fingerprint) +
|
|
" " + fieldLabelStyle.Render("last used:") + " " + last)
|
|
b.WriteString("\n\n " + fieldLabelStyle.Render(fmt.Sprintf("Grants (%d):", len(d.grants))) + "\n")
|
|
}
|
|
b.WriteString(d.grantsBody())
|
|
b.WriteString("\n\n " + hintStyle.Render(d.hintLine()))
|
|
return frame.Render(b.String())
|
|
}
|
|
|
|
func (d detailModel) infoView() string {
|
|
var b strings.Builder
|
|
switch d.kind {
|
|
case detailNS:
|
|
password := "not configured"
|
|
if d.ns.HasPassword {
|
|
password = "enabled"
|
|
}
|
|
b.WriteString(detailTitleStyle.Render("gitctl · Namespace · " + d.ns.Name))
|
|
b.WriteString("\n\n " + fieldLabelStyle.Render("repos:") + fmt.Sprintf(" %d", d.ns.RepoCount))
|
|
b.WriteString(" " + fieldLabelStyle.Render("password:") + " " + password)
|
|
b.WriteString("\n " + fieldLabelStyle.Render("created:") + " " + fmtTime(d.ns.Created))
|
|
b.WriteString(" " + fieldLabelStyle.Render("updated:") + " " + fmtTime(d.ns.Updated))
|
|
b.WriteString("\n\n " + hintStyle.Render("a/enter permissions · m rename · d delete · esc back"))
|
|
case detailRepo:
|
|
visibility := "public"
|
|
if d.repo.IsPrivate {
|
|
visibility = "private"
|
|
}
|
|
b.WriteString(detailTitleStyle.Render("gitctl · Repo · " + d.repo.Ns + "/" + d.repo.Name))
|
|
b.WriteString("\n\n " + fieldLabelStyle.Render("visibility:") + " " + visibility)
|
|
b.WriteString("\n " + fieldLabelStyle.Render("created:") + " " + fmtTime(d.repo.Created))
|
|
b.WriteString(" " + fieldLabelStyle.Render("updated:") + " " + fmtTime(d.repo.Updated))
|
|
b.WriteString("\n\n " + hintStyle.Render("a/enter permissions · v private/public · m move · d delete · esc back"))
|
|
}
|
|
return frame.Render(b.String())
|
|
}
|
|
|
|
func (d detailModel) grantsBody() string {
|
|
if d.err != "" {
|
|
return " " + errStyle.Render("✗ "+d.err)
|
|
}
|
|
if !d.loaded {
|
|
return " " + hintStyle.Render("loading…")
|
|
}
|
|
if len(d.grants) == 0 {
|
|
return " " + hintStyle.Render("(no access grants)")
|
|
}
|
|
return d.list.View()
|
|
}
|
|
|
|
func (d detailModel) hintLine() string {
|
|
switch d.kind {
|
|
case detailNS:
|
|
return "g add ssh · p add pat · c set password · u remove password\n x revoke · r permission · / search · esc details"
|
|
case detailRepo:
|
|
return "g add ssh · p add pat · x revoke · r permission · / search · esc details"
|
|
case detailKey:
|
|
return "a grant · x revoke · r permission · / search · esc back"
|
|
}
|
|
return "esc back"
|
|
}
|