Commit fda3e4e
Eric Bower
·
2025-12-20 12:11:56 -0500 EST
parent d6f553a
feat(tui): new pages section showing all sites
5 files changed,
+211,
-0
+7,
-0
| ... | ... | @@ -34,6 +34,13 @@ func NewDB(databaseUrl string, logger *slog.Logger) (*PgsPsqlDB, error) { | |
| 34 | 34 | return d, nil | |
| 35 | 35 | } | |
| 36 | 36 | ||
| 37 | + | func NewDBWithConn(db *sqlx.DB, logger *slog.Logger) *PgsPsqlDB { | |
| 38 | + | return &PgsPsqlDB{ | |
| 39 | + | Logger: logger, | |
| 40 | + | Db: db, | |
| 41 | + | } | |
| 42 | + | } | |
| 43 | + | ||
| 37 | 44 | func (me *PgsPsqlDB) Close() error { | |
| 38 | 45 | return me.Db.Close() | |
| 39 | 46 | } |
+4,
-0
| ... | ... | @@ -8,6 +8,7 @@ import ( | |
| 8 | 8 | "time" | |
| 9 | 9 | ||
| 10 | 10 | "git.sr.ht/~rockorager/vaxis" | |
| 11 | + | pgsdb "github.com/picosh/pico/pkg/apps/pgs/db" | |
| 11 | 12 | "github.com/picosh/pico/pkg/db/postgres" | |
| 12 | 13 | "github.com/picosh/pico/pkg/pssh" | |
| 13 | 14 | "github.com/picosh/pico/pkg/send/auth" |
| ... | ... | @@ -54,6 +55,8 @@ func StartSshServer() { | |
| 54 | 55 | _ = dbpool.Close() | |
| 55 | 56 | }() | |
| 56 | 57 | ||
| 58 | + | pgsDB := pgsdb.NewDBWithConn(dbpool.Db, cfg.Logger) | |
| 59 | + | ||
| 57 | 60 | handler := NewUploadHandler( | |
| 58 | 61 | dbpool, | |
| 59 | 62 | cfg, |
| ... | ... | @@ -100,6 +103,7 @@ func StartSshServer() { | |
| 100 | 103 | Session: sesh, | |
| 101 | 104 | Cfg: cfg, | |
| 102 | 105 | Dbpool: handler.DBPool, | |
| 106 | + | PgsDB: pgsDB, | |
| 103 | 107 | Logger: pssh.GetLogger(sesh), | |
| 104 | 108 | } | |
| 105 | 109 | return pssh.PtyMdw(createTui(shrd), 200*time.Millisecond)(next)(sesh) |
| ... | ... | @@ -14,6 +14,7 @@ var menuChoices = []string{ | |
| 14 | 14 | "logs", | |
| 15 | 15 | "access_logs", | |
| 16 | 16 | "analytics", | |
| 17 | + | "pages", | |
| 17 | 18 | "tuns", | |
| 18 | 19 | "pico+", | |
| 19 | 20 | "chat", |
+196,
-0
| ... | ... | @@ -0,0 +1,196 @@ | |
| 1 | + | package tui | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "fmt" | |
| 5 | + | "sort" | |
| 6 | + | "strings" | |
| 7 | + | ||
| 8 | + | "git.sr.ht/~rockorager/vaxis" | |
| 9 | + | "git.sr.ht/~rockorager/vaxis/vxfw" | |
| 10 | + | "git.sr.ht/~rockorager/vaxis/vxfw/list" | |
| 11 | + | "git.sr.ht/~rockorager/vaxis/vxfw/richtext" | |
| 12 | + | "git.sr.ht/~rockorager/vaxis/vxfw/text" | |
| 13 | + | "github.com/picosh/pico/pkg/db" | |
| 14 | + | ) | |
| 15 | + | ||
| 16 | + | type PagesLoaded struct{} | |
| 17 | + | ||
| 18 | + | type PagesPage struct { | |
| 19 | + | shared *SharedModel | |
| 20 | + | ||
| 21 | + | list *list.Dynamic | |
| 22 | + | pages []*db.Project | |
| 23 | + | loading bool | |
| 24 | + | err error | |
| 25 | + | } | |
| 26 | + | ||
| 27 | + | func NewPagesPage(shrd *SharedModel) *PagesPage { | |
| 28 | + | page := &PagesPage{ | |
| 29 | + | shared: shrd, | |
| 30 | + | } | |
| 31 | + | page.list = &list.Dynamic{Builder: page.getWidget, DrawCursor: true, Gap: 1} | |
| 32 | + | return page | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | func (m *PagesPage) Footer() []Shortcut { | |
| 36 | + | return []Shortcut{ | |
| 37 | + | {Shortcut: "c", Text: "copy url"}, | |
| 38 | + | {Shortcut: "^r", Text: "refresh"}, | |
| 39 | + | } | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | func (m *PagesPage) HandleEvent(ev vaxis.Event, phase vxfw.EventPhase) (vxfw.Command, error) { | |
| 43 | + | switch msg := ev.(type) { | |
| 44 | + | case PageIn: | |
| 45 | + | m.loading = true | |
| 46 | + | go m.fetchPages() | |
| 47 | + | return vxfw.FocusWidgetCmd(m.list), nil | |
| 48 | + | case PagesLoaded: | |
| 49 | + | return vxfw.RedrawCmd{}, nil | |
| 50 | + | case vaxis.Key: | |
| 51 | + | if msg.Matches('c') { | |
| 52 | + | cursor := m.list.Cursor() | |
| 53 | + | if int(cursor) < len(m.pages) { | |
| 54 | + | project := m.pages[cursor] | |
| 55 | + | url := fmt.Sprintf("https://%s-%s.pgs.sh", m.shared.User.Name, project.Name) | |
| 56 | + | return vxfw.CopyToClipboardCmd(url), nil | |
| 57 | + | } | |
| 58 | + | } | |
| 59 | + | if msg.Matches('r', vaxis.ModCtrl) { | |
| 60 | + | m.loading = true | |
| 61 | + | go m.fetchPages() | |
| 62 | + | return vxfw.RedrawCmd{}, nil | |
| 63 | + | } | |
| 64 | + | } | |
| 65 | + | return nil, nil | |
| 66 | + | } | |
| 67 | + | ||
| 68 | + | func (m *PagesPage) fetchPages() { | |
| 69 | + | if m.shared.User == nil { | |
| 70 | + | m.err = fmt.Errorf("no user found") | |
| 71 | + | m.loading = false | |
| 72 | + | m.shared.App.PostEvent(PagesLoaded{}) | |
| 73 | + | return | |
| 74 | + | } | |
| 75 | + | ||
| 76 | + | if m.shared.PgsDB == nil { | |
| 77 | + | m.err = fmt.Errorf("pgs database not configured") | |
| 78 | + | m.loading = false | |
| 79 | + | m.shared.App.PostEvent(PagesLoaded{}) | |
| 80 | + | return | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | pages, err := m.shared.PgsDB.FindProjectsByUser(m.shared.User.ID) | |
| 84 | + | ||
| 85 | + | m.loading = false | |
| 86 | + | if err != nil { | |
| 87 | + | m.err = err | |
| 88 | + | m.pages = []*db.Project{} | |
| 89 | + | } else { | |
| 90 | + | m.err = nil | |
| 91 | + | sort.Slice(pages, func(i, j int) bool { | |
| 92 | + | return pages[i].Name < pages[j].Name | |
| 93 | + | }) | |
| 94 | + | m.pages = pages | |
| 95 | + | } | |
| 96 | + | ||
| 97 | + | m.shared.App.PostEvent(PagesLoaded{}) | |
| 98 | + | } | |
| 99 | + | ||
| 100 | + | func (m *PagesPage) Draw(ctx vxfw.DrawContext) (vxfw.Surface, error) { | |
| 101 | + | w := ctx.Max.Width | |
| 102 | + | h := ctx.Max.Height | |
| 103 | + | root := vxfw.NewSurface(w, h, m) | |
| 104 | + | ah := 0 | |
| 105 | + | ||
| 106 | + | pagesLen := len(m.pages) | |
| 107 | + | err := m.err | |
| 108 | + | ||
| 109 | + | info := text.New("Pages deployed to pgs. Each page is accessible via https://{user}-{project}.pgs.sh. We do not have access to custom domains so those cannot be shown.") | |
| 110 | + | brd := NewBorder(info) | |
| 111 | + | brd.Label = "desc" | |
| 112 | + | brdSurf, _ := brd.Draw(ctx) | |
| 113 | + | root.AddChild(0, ah, brdSurf) | |
| 114 | + | ah += int(brdSurf.Size.Height) | |
| 115 | + | ||
| 116 | + | if err != nil { | |
| 117 | + | txt := text.New(fmt.Sprintf("Error: %s", err.Error())) | |
| 118 | + | txt.Style = vaxis.Style{Foreground: red} | |
| 119 | + | txtSurf, _ := txt.Draw(ctx) | |
| 120 | + | root.AddChild(0, ah, txtSurf) | |
| 121 | + | } else if pagesLen == 0 { | |
| 122 | + | txt := text.New("No pages found.") | |
| 123 | + | txtSurf, _ := txt.Draw(ctx) | |
| 124 | + | root.AddChild(0, ah, txtSurf) | |
| 125 | + | } else { | |
| 126 | + | listPane := NewBorder(m.list) | |
| 127 | + | listPane.Label = "pages" | |
| 128 | + | listPane.Style = vaxis.Style{Foreground: oj} | |
| 129 | + | listSurf, _ := listPane.Draw(createDrawCtx(ctx, ctx.Max.Height-uint16(ah))) | |
| 130 | + | root.AddChild(0, ah, listSurf) | |
| 131 | + | } | |
| 132 | + | ||
| 133 | + | return root, nil | |
| 134 | + | } | |
| 135 | + | ||
| 136 | + | func (m *PagesPage) getWidget(i uint, cursor uint) vxfw.Widget { | |
| 137 | + | if int(i) >= len(m.pages) { | |
| 138 | + | return nil | |
| 139 | + | } | |
| 140 | + | ||
| 141 | + | isSelected := i == cursor | |
| 142 | + | return pageToWidget(m.pages[i], m.shared.User.Name, isSelected) | |
| 143 | + | } | |
| 144 | + | ||
| 145 | + | func pageToWidget(project *db.Project, username string, isSelected bool) vxfw.Widget { | |
| 146 | + | url := fmt.Sprintf("https://%s-%s.pgs.sh", username, project.Name) | |
| 147 | + | ||
| 148 | + | updatedAt := "" | |
| 149 | + | if project.UpdatedAt != nil { | |
| 150 | + | updatedAt = project.UpdatedAt.Format("2006-01-02 15:04:05") | |
| 151 | + | } | |
| 152 | + | ||
| 153 | + | labelStyle := vaxis.Style{Foreground: grey} | |
| 154 | + | if isSelected { | |
| 155 | + | labelStyle = vaxis.Style{Foreground: fuschia} | |
| 156 | + | } | |
| 157 | + | ||
| 158 | + | segs := []vaxis.Segment{ | |
| 159 | + | {Text: "URL: ", Style: labelStyle}, | |
| 160 | + | {Text: url + "\n", Style: vaxis.Style{Foreground: green}}, | |
| 161 | + | ||
| 162 | + | {Text: "Updated: ", Style: labelStyle}, | |
| 163 | + | {Text: updatedAt}, | |
| 164 | + | } | |
| 165 | + | ||
| 166 | + | if project.ProjectDir != project.Name { | |
| 167 | + | segs = append(segs, | |
| 168 | + | vaxis.Segment{Text: "\n"}, | |
| 169 | + | vaxis.Segment{Text: "Links To: ", Style: labelStyle}, | |
| 170 | + | vaxis.Segment{Text: project.ProjectDir, Style: vaxis.Style{Foreground: purp}}, | |
| 171 | + | ) | |
| 172 | + | } | |
| 173 | + | ||
| 174 | + | if project.Acl.Type != "" && project.Acl.Type != "public" { | |
| 175 | + | aclStr := project.Acl.Type | |
| 176 | + | if len(project.Acl.Data) > 0 { | |
| 177 | + | aclStr += " (" + strings.Join(project.Acl.Data, ", ") + ")" | |
| 178 | + | } | |
| 179 | + | segs = append(segs, | |
| 180 | + | vaxis.Segment{Text: "\n"}, | |
| 181 | + | vaxis.Segment{Text: "ACL: ", Style: labelStyle}, | |
| 182 | + | vaxis.Segment{Text: aclStr, Style: vaxis.Style{Foreground: oj}}, | |
| 183 | + | ) | |
| 184 | + | } | |
| 185 | + | ||
| 186 | + | if project.Blocked != "" { | |
| 187 | + | segs = append(segs, | |
| 188 | + | vaxis.Segment{Text: "\n"}, | |
| 189 | + | vaxis.Segment{Text: "Blocked: ", Style: labelStyle}, | |
| 190 | + | vaxis.Segment{Text: project.Blocked, Style: vaxis.Style{Foreground: red}}, | |
| 191 | + | ) | |
| 192 | + | } | |
| 193 | + | ||
| 194 | + | txt := richtext.New(segs) | |
| 195 | + | return txt | |
| 196 | + | } |
+3,
-0
| ... | ... | @@ -10,6 +10,7 @@ import ( | |
| 10 | 10 | "git.sr.ht/~rockorager/vaxis" | |
| 11 | 11 | "git.sr.ht/~rockorager/vaxis/vxfw" | |
| 12 | 12 | "git.sr.ht/~rockorager/vaxis/vxfw/richtext" | |
| 13 | + | pgsdb "github.com/picosh/pico/pkg/apps/pgs/db" | |
| 13 | 14 | "github.com/picosh/pico/pkg/db" | |
| 14 | 15 | "github.com/picosh/pico/pkg/pssh" | |
| 15 | 16 | "github.com/picosh/pico/pkg/shared" |