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) {
3434 return d, nil
3535 }
3636
37+func NewDBWithConn(db *sqlx.DB, logger *slog.Logger) *PgsPsqlDB {
38+ return &PgsPsqlDB{
39+ Logger: logger,
40+ Db: db,
41+ }
42+}
43+
3744 func (me *PgsPsqlDB) Close() error {
3845 return me.Db.Close()
3946 }
+4, -0
......@@ -8,6 +8,7 @@ import (
88 "time"
99
1010 "git.sr.ht/~rockorager/vaxis"
11+ pgsdb "github.com/picosh/pico/pkg/apps/pgs/db"
1112 "github.com/picosh/pico/pkg/db/postgres"
1213 "github.com/picosh/pico/pkg/pssh"
1314 "github.com/picosh/pico/pkg/send/auth"
......@@ -54,6 +55,8 @@ func StartSshServer() {
5455 _ = dbpool.Close()
5556 }()
5657
58+ pgsDB := pgsdb.NewDBWithConn(dbpool.Db, cfg.Logger)
59+
5760 handler := NewUploadHandler(
5861 dbpool,
5962 cfg,
......@@ -100,6 +103,7 @@ func StartSshServer() {
100103 Session: sesh,
101104 Cfg: cfg,
102105 Dbpool: handler.DBPool,
106+ PgsDB: pgsDB,
103107 Logger: pssh.GetLogger(sesh),
104108 }
105109 return pssh.PtyMdw(createTui(shrd), 200*time.Millisecond)(next)(sesh)
+1, -0
......@@ -14,6 +14,7 @@ var menuChoices = []string{
1414 "logs",
1515 "access_logs",
1616 "analytics",
17+ "pages",
1718 "tuns",
1819 "pico+",
1920 "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 (
1010 "git.sr.ht/~rockorager/vaxis"
1111 "git.sr.ht/~rockorager/vaxis/vxfw"
1212 "git.sr.ht/~rockorager/vaxis/vxfw/richtext"
13+ pgsdb "github.com/picosh/pico/pkg/apps/pgs/db"
1314 "github.com/picosh/pico/pkg/db"
1415 "github.com/picosh/pico/pkg/pssh"
1516 "github.com/picosh/pico/pkg/shared"
......@@ -26,6 +27,7 @@ type SharedModel struct {
2627 Session *pssh.SSHServerConnSession
2728 Cfg *shared.ConfigSite
2829 Dbpool db.DB
30+ PgsDB pgsdb.PgsDB
2931 User *db.User
3032 PlusFeatureFlag *db.FeatureFlag
3133 BouncerFeatureFlag *db.FeatureFlag
......@@ -355,6 +357,7 @@ func NewTui(opts vaxis.Options, shrd *SharedModel) error {
355357 "chat": NewChatPage(shrd),
356358 "tuns": NewTunsPage(shrd),
357359 "access_logs": NewAccessLogsPage(shrd),
360+ "pages": NewPagesPage(shrd),
358361 }
359362 root := &App{
360363 shared: shrd,