Skip to content

Commit ad55d0d

Browse files
committed
add glance codes to intergrate with pocketdb
1 parent 495ce3d commit ad55d0d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

91 files changed

+7536
-7
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
.idea/
22
pocketbase
33
pb_data/
4-
fluent
4+
fluent
5+
public/

app/glance/cli.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package glance
2+
3+
//https://github.com/glanceapp/glance.git
4+
import (
5+
"flag"
6+
"os"
7+
)
8+
9+
type CliIntent uint8
10+
11+
const (
12+
CliIntentServe CliIntent = iota
13+
CliIntentCheckConfig = iota
14+
)
15+
16+
type CliOptions struct {
17+
Intent CliIntent
18+
ConfigPath string
19+
}
20+
21+
func ParseCliOptions() (*CliOptions, error) {
22+
flags := flag.NewFlagSet("", flag.ExitOnError)
23+
24+
checkConfig := flags.Bool("check-config", false, "Check whether the config is valid")
25+
configPath := flags.String("config", "glance.yml", "Set config path")
26+
27+
err := flags.Parse(os.Args[1:])
28+
29+
if err != nil {
30+
return nil, err
31+
}
32+
33+
intent := CliIntentServe
34+
35+
if *checkConfig {
36+
intent = CliIntentCheckConfig
37+
}
38+
39+
return &CliOptions{
40+
Intent: intent,
41+
ConfigPath: *configPath,
42+
}, nil
43+
}

app/glance/config.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package glance
2+
3+
import (
4+
"fmt"
5+
"io"
6+
7+
"gopkg.in/yaml.v3"
8+
)
9+
10+
type Config struct {
11+
Server Server `yaml:"server"`
12+
Theme Theme `yaml:"theme"`
13+
Pages []Page `yaml:"pages"`
14+
}
15+
16+
func NewConfigFromYml(contents io.Reader) (*Config, error) {
17+
config := NewConfig()
18+
19+
contentBytes, err := io.ReadAll(contents)
20+
21+
if err != nil {
22+
return nil, err
23+
}
24+
25+
err = yaml.Unmarshal(contentBytes, config)
26+
27+
if err != nil {
28+
return nil, err
29+
}
30+
31+
if err = configIsValid(config); err != nil {
32+
return nil, err
33+
}
34+
35+
return config, nil
36+
}
37+
38+
func NewConfig() *Config {
39+
config := &Config{}
40+
41+
config.Server.Host = ""
42+
config.Server.Port = 8080
43+
44+
return config
45+
}
46+
47+
func configIsValid(config *Config) error {
48+
for i := range config.Pages {
49+
if config.Pages[i].Title == "" {
50+
return fmt.Errorf("Page %d has no title", i+1)
51+
}
52+
53+
if len(config.Pages[i].Columns) == 0 {
54+
return fmt.Errorf("Page %d has no columns", i+1)
55+
}
56+
57+
if len(config.Pages[i].Columns) > 3 {
58+
return fmt.Errorf("Page %d has more than 3 columns: %d", i+1, len(config.Pages[i].Columns))
59+
}
60+
61+
columnSizesCount := make(map[string]int)
62+
63+
for j := range config.Pages[i].Columns {
64+
if config.Pages[i].Columns[j].Size != "small" && config.Pages[i].Columns[j].Size != "full" {
65+
return fmt.Errorf("Column %d of page %d: size can only be either small or full", j+1, i+1)
66+
}
67+
68+
columnSizesCount[config.Pages[i].Columns[j].Size]++
69+
}
70+
71+
full := columnSizesCount["full"]
72+
73+
if full > 2 || full == 0 {
74+
return fmt.Errorf("Page %d must have either 1 or 2 full width columns", i+1)
75+
}
76+
}
77+
78+
return nil
79+
}

app/glance/glance.go

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
package glance
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"fmt"
7+
"github.com/fluent-qa/qgops/internal/assets"
8+
"github.com/fluent-qa/qgops/internal/widget"
9+
"log/slog"
10+
"net/http"
11+
"path/filepath"
12+
"regexp"
13+
"strings"
14+
"sync"
15+
"time"
16+
)
17+
18+
var buildVersion = "dev"
19+
20+
var sequentialWhitespacePattern = regexp.MustCompile(`\s+`)
21+
22+
type Application struct {
23+
Version string
24+
Config Config
25+
slugToPage map[string]*Page
26+
}
27+
28+
type Theme struct {
29+
BackgroundColor *widget.HSLColorField `yaml:"background-color"`
30+
PrimaryColor *widget.HSLColorField `yaml:"primary-color"`
31+
PositiveColor *widget.HSLColorField `yaml:"positive-color"`
32+
NegativeColor *widget.HSLColorField `yaml:"negative-color"`
33+
Light bool `yaml:"light"`
34+
ContrastMultiplier float32 `yaml:"contrast-multiplier"`
35+
TextSaturationMultiplier float32 `yaml:"text-saturation-multiplier"`
36+
CustomCSSFile string `yaml:"custom-css-file"`
37+
}
38+
39+
type Server struct {
40+
Host string `yaml:"host"`
41+
Port uint16 `yaml:"port"`
42+
AssetsPath string `yaml:"assets-path"`
43+
StartedAt time.Time `yaml:"-"`
44+
}
45+
46+
type Column struct {
47+
Size string `yaml:"size"`
48+
Widgets widget.Widgets `yaml:"widgets"`
49+
}
50+
51+
type templateData struct {
52+
App *Application
53+
Page *Page
54+
}
55+
56+
type Page struct {
57+
Title string `yaml:"name"`
58+
Slug string `yaml:"slug"`
59+
ShowMobileHeader bool `yaml:"show-mobile-header"`
60+
Columns []Column `yaml:"columns"`
61+
mu sync.Mutex
62+
}
63+
64+
func (p *Page) UpdateOutdatedWidgets() {
65+
now := time.Now()
66+
67+
var wg sync.WaitGroup
68+
context := context.Background()
69+
70+
for c := range p.Columns {
71+
for w := range p.Columns[c].Widgets {
72+
widget := p.Columns[c].Widgets[w]
73+
74+
if !widget.RequiresUpdate(&now) {
75+
continue
76+
}
77+
78+
wg.Add(1)
79+
go func() {
80+
defer wg.Done()
81+
widget.Update(context)
82+
}()
83+
}
84+
}
85+
86+
wg.Wait()
87+
}
88+
89+
// TODO: fix, currently very simple, lots of uncovered edge cases
90+
func titleToSlug(s string) string {
91+
s = strings.ToLower(s)
92+
s = sequentialWhitespacePattern.ReplaceAllString(s, "-")
93+
s = strings.Trim(s, "-")
94+
95+
return s
96+
}
97+
98+
func NewApplication(config *Config) (*Application, error) {
99+
if len(config.Pages) == 0 {
100+
return nil, fmt.Errorf("no pages configured")
101+
}
102+
103+
app := &Application{
104+
Version: buildVersion,
105+
Config: *config,
106+
slugToPage: make(map[string]*Page),
107+
}
108+
109+
app.slugToPage[""] = &config.Pages[0]
110+
111+
for i := range config.Pages {
112+
if config.Pages[i].Slug == "" {
113+
config.Pages[i].Slug = titleToSlug(config.Pages[i].Title)
114+
}
115+
116+
app.slugToPage[config.Pages[i].Slug] = &config.Pages[i]
117+
}
118+
119+
return app, nil
120+
}
121+
122+
func (a *Application) HandlePageRequest(w http.ResponseWriter, r *http.Request) {
123+
page, exists := a.slugToPage[r.PathValue("page")]
124+
125+
if !exists {
126+
a.HandleNotFound(w, r)
127+
return
128+
}
129+
130+
pageData := templateData{
131+
Page: page,
132+
App: a,
133+
}
134+
135+
var responseBytes bytes.Buffer
136+
err := assets.PageTemplate.Execute(&responseBytes, pageData)
137+
138+
if err != nil {
139+
w.WriteHeader(http.StatusInternalServerError)
140+
w.Write([]byte(err.Error()))
141+
return
142+
}
143+
144+
w.Write(responseBytes.Bytes())
145+
}
146+
147+
func (a *Application) HandlePageContentRequest(w http.ResponseWriter, r *http.Request) {
148+
page, exists := a.slugToPage[r.PathValue("page")]
149+
150+
if !exists {
151+
a.HandleNotFound(w, r)
152+
return
153+
}
154+
155+
pageData := templateData{
156+
Page: page,
157+
}
158+
159+
page.mu.Lock()
160+
defer page.mu.Unlock()
161+
page.UpdateOutdatedWidgets()
162+
163+
var responseBytes bytes.Buffer
164+
err := assets.PageContentTemplate.Execute(&responseBytes, pageData)
165+
166+
if err != nil {
167+
w.WriteHeader(http.StatusInternalServerError)
168+
w.Write([]byte(err.Error()))
169+
return
170+
}
171+
172+
w.Write(responseBytes.Bytes())
173+
}
174+
175+
func (a *Application) HandleNotFound(w http.ResponseWriter, r *http.Request) {
176+
// TODO: add proper not found page
177+
w.WriteHeader(http.StatusNotFound)
178+
w.Write([]byte("Page not found"))
179+
}
180+
181+
func FileServerWithCache(fs http.FileSystem, cacheDuration time.Duration) http.Handler {
182+
server := http.FileServer(fs)
183+
184+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
185+
// TODO: fix always setting cache control even if the file doesn't exist
186+
w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d", int(cacheDuration.Seconds())))
187+
server.ServeHTTP(w, r)
188+
})
189+
}
190+
191+
func (a *Application) Serve() error {
192+
// TODO: add gzip support, static files must have their gzipped contents cached
193+
// TODO: add HTTPS support
194+
mux := http.NewServeMux()
195+
196+
mux.HandleFunc("GET /{$}", a.HandlePageRequest)
197+
mux.HandleFunc("GET /{page}", a.HandlePageRequest)
198+
mux.HandleFunc("GET /api/pages/{page}/content/{$}", a.HandlePageContentRequest)
199+
mux.Handle("GET /static/{path...}", http.StripPrefix("/static/", FileServerWithCache(http.FS(assets.PublicFS), 2*time.Hour)))
200+
201+
if a.Config.Server.AssetsPath != "" {
202+
absAssetsPath, err := filepath.Abs(a.Config.Server.AssetsPath)
203+
204+
if err != nil {
205+
return fmt.Errorf("invalid assets path: %s", a.Config.Server.AssetsPath)
206+
}
207+
208+
slog.Info("Serving assets", "path", absAssetsPath)
209+
assetsFS := FileServerWithCache(http.Dir(a.Config.Server.AssetsPath), 2*time.Hour)
210+
mux.Handle("/assets/{path...}", http.StripPrefix("/assets/", assetsFS))
211+
}
212+
213+
server := http.Server{
214+
Addr: fmt.Sprintf("%s:%d", a.Config.Server.Host, a.Config.Server.Port),
215+
Handler: mux,
216+
}
217+
218+
a.Config.Server.StartedAt = time.Now()
219+
220+
slog.Info("Starting server", "host", a.Config.Server.Host, "port", a.Config.Server.Port)
221+
return server.ListenAndServe()
222+
}

0 commit comments

Comments
 (0)