From 26e10a9399bd6185741d0912ffa54c807ffef671 Mon Sep 17 00:00:00 2001
From: Julien Dessaux
Date: Mon, 27 Jan 2025 22:00:51 +0100
Subject: [PATCH] feat(webui): implement state versions list
---
pkg/database/accounts.go | 25 ++++++++++++++++++
pkg/database/states.go | 26 ++++++++++++++++--
pkg/database/versions.go | 35 ++++++++++++++++++++++++
pkg/model/version.go | 14 ++++++++++
pkg/webui/html/base.html | 8 ++++--
pkg/webui/html/state.html | 30 +++++++++++++++++++++
pkg/webui/html/states.html | 2 +-
pkg/webui/index.go | 5 ++--
pkg/webui/routes.go | 1 +
pkg/webui/state.go | 54 ++++++++++++++++++++++++++++++++++++++
pkg/webui/states.go | 2 +-
pkg/webui/static/main.css | 3 +--
12 files changed, 195 insertions(+), 10 deletions(-)
create mode 100644 pkg/database/versions.go
create mode 100644 pkg/model/version.go
create mode 100644 pkg/webui/html/state.html
create mode 100644 pkg/webui/state.go
diff --git a/pkg/database/accounts.go b/pkg/database/accounts.go
index 377ca80..9adb32d 100644
--- a/pkg/database/accounts.go
+++ b/pkg/database/accounts.go
@@ -47,6 +47,31 @@ func (db *DB) InitAdminAccount() error {
})
}
+func (db *DB) LoadAccountUsernames() (map[int]string, error) {
+ rows, err := db.Query(
+ `SELECT id, username FROM accounts;`)
+ if err != nil {
+ return nil, fmt.Errorf("failed to load accounts from database: %w", err)
+ }
+ defer rows.Close()
+ accounts := make(map[int]string)
+ for rows.Next() {
+ var (
+ id int
+ username string
+ )
+ err = rows.Scan(&id, &username)
+ if err != nil {
+ return nil, fmt.Errorf("failed to load account from row: %w", err)
+ }
+ accounts[id] = username
+ }
+ if err := rows.Err(); err != nil {
+ return nil, fmt.Errorf("failed to load accounts from rows: %w", err)
+ }
+ return accounts, nil
+}
+
func (db *DB) LoadAccountById(id int) (*model.Account, error) {
account := model.Account{
Id: id,
diff --git a/pkg/database/states.go b/pkg/database/states.go
index 1bd3d76..d65dd7c 100644
--- a/pkg/database/states.go
+++ b/pkg/database/states.go
@@ -45,7 +45,29 @@ func (db *DB) GetState(path string) ([]byte, error) {
return db.dataEncryptionKey.DecryptAES256(encryptedData)
}
-func (db *DB) LoadStatesByPath() ([]model.State, error) {
+func (db *DB) LoadStateById(stateId int) (*model.State, error) {
+ state := model.State{
+ Id: stateId,
+ }
+ var (
+ created int64
+ updated int64
+ )
+ err := db.QueryRow(
+ `SELECT created, lock, path, updated FROM states WHERE id = ?;`,
+ stateId).Scan(&created, &state.Lock, &state.Path, &updated)
+ if err != nil {
+ if errors.Is(err, sql.ErrNoRows) {
+ return nil, nil
+ }
+ return nil, fmt.Errorf("failed to load state id %s from database: %w", stateId, err)
+ }
+ state.Created = time.Unix(created, 0)
+ state.Updated = time.Unix(updated, 0)
+ return &state, nil
+}
+
+func (db *DB) LoadStates() ([]model.State, error) {
rows, err := db.Query(
`SELECT created, id, lock, path, updated FROM states;`)
if err != nil {
@@ -61,7 +83,7 @@ func (db *DB) LoadStatesByPath() ([]model.State, error) {
)
err = rows.Scan(&created, &state.Id, &state.Lock, &state.Path, &updated)
if err != nil {
- return nil, fmt.Errorf("failed to load states from row: %w", err)
+ return nil, fmt.Errorf("failed to load state from row: %w", err)
}
state.Created = time.Unix(created, 0)
state.Updated = time.Unix(updated, 0)
diff --git a/pkg/database/versions.go b/pkg/database/versions.go
new file mode 100644
index 0000000..5a6f11f
--- /dev/null
+++ b/pkg/database/versions.go
@@ -0,0 +1,35 @@
+package database
+
+import (
+ "fmt"
+ "time"
+
+ "git.adyxax.org/adyxax/tfstated/pkg/model"
+)
+
+func (db *DB) LoadVersionsByState(state *model.State) ([]model.Version, error) {
+ rows, err := db.Query(
+ `SELECT account_id, created, data, id, lock
+ FROM versions
+ WHERE state_id = ?
+ ORDER BY id DESC;`, state.Id)
+ if err != nil {
+ return nil, fmt.Errorf("failed to load versions from database: %w", err)
+ }
+ defer rows.Close()
+ versions := make([]model.Version, 0)
+ for rows.Next() {
+ var version model.Version
+ var created int64
+ err = rows.Scan(&version.AccountId, &created, &version.Data, &version.Id, &version.Lock)
+ if err != nil {
+ return nil, fmt.Errorf("failed to load version from row: %w", err)
+ }
+ version.Created = time.Unix(created, 0)
+ versions = append(versions, version)
+ }
+ if err := rows.Err(); err != nil {
+ return nil, fmt.Errorf("failed to load versions from rows: %w", err)
+ }
+ return versions, nil
+}
diff --git a/pkg/model/version.go b/pkg/model/version.go
new file mode 100644
index 0000000..d8c3603
--- /dev/null
+++ b/pkg/model/version.go
@@ -0,0 +1,14 @@
+package model
+
+import (
+ "time"
+)
+
+type Version struct {
+ AccountId int
+ Created time.Time
+ Data []byte
+ Id int
+ Lock *string
+ StateId int
+}
diff --git a/pkg/webui/html/base.html b/pkg/webui/html/base.html
index 06328e4..c0138ac 100644
--- a/pkg/webui/html/base.html
+++ b/pkg/webui/html/base.html
@@ -12,11 +12,10 @@
Login
{{ else }}
-
+
home_storage
States
-
logout
Logout
@@ -39,6 +38,11 @@
diff --git a/pkg/webui/html/state.html b/pkg/webui/html/state.html
new file mode 100644
index 0000000..4439d9e
--- /dev/null
+++ b/pkg/webui/html/state.html
@@ -0,0 +1,30 @@
+{{ define "main" }}
+
+
+ Locked:
+ {{ if eq .State.Lock nil }}no{{ else }}
+ yes
+
+ {{ end }}
+
+
+
+{{ end }}
diff --git a/pkg/webui/html/states.html b/pkg/webui/html/states.html
index f61b87c..787ac73 100644
--- a/pkg/webui/html/states.html
+++ b/pkg/webui/html/states.html
@@ -1,6 +1,6 @@
{{ define "main" }}
-
+
Path |
diff --git a/pkg/webui/index.go b/pkg/webui/index.go
index deea5d1..89e5ad6 100644
--- a/pkg/webui/index.go
+++ b/pkg/webui/index.go
@@ -6,8 +6,9 @@ import (
)
type Page struct {
- Section string
- Title string
+ Precedent string
+ Section string
+ Title string
}
func handleIndexGET() http.Handler {
diff --git a/pkg/webui/routes.go b/pkg/webui/routes.go
index 9bf04ec..4330630 100644
--- a/pkg/webui/routes.go
+++ b/pkg/webui/routes.go
@@ -17,6 +17,7 @@ func addRoutes(
mux.Handle("POST /login", requireSession(handleLoginPOST(db)))
mux.Handle("GET /logout", requireLogin(handleLogoutGET(db)))
mux.Handle("GET /states", requireLogin(handleStatesGET(db)))
+ mux.Handle("GET /state/{id}", requireLogin(handleStateGET(db)))
mux.Handle("GET /static/", cache(http.FileServer(http.FS(staticFS))))
mux.Handle("GET /", requireLogin(handleIndexGET()))
}
diff --git a/pkg/webui/state.go b/pkg/webui/state.go
new file mode 100644
index 0000000..c7a6aaf
--- /dev/null
+++ b/pkg/webui/state.go
@@ -0,0 +1,54 @@
+package webui
+
+import (
+ "html/template"
+ "net/http"
+ "strconv"
+
+ "git.adyxax.org/adyxax/tfstated/pkg/database"
+ "git.adyxax.org/adyxax/tfstated/pkg/model"
+)
+
+var stateTemplate = template.Must(template.ParseFS(htmlFS, "html/base.html", "html/state.html"))
+
+func handleStateGET(db *database.DB) http.Handler {
+ type StatesData struct {
+ Page
+ State *model.State
+ Usernames map[int]string
+ Versions []model.Version
+ }
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ stateIdStr := r.PathValue("id")
+ stateId, err := strconv.Atoi(stateIdStr)
+ if err != nil {
+ errorResponse(w, http.StatusBadRequest, err)
+ return
+ }
+ state, err := db.LoadStateById(stateId)
+ if err != nil {
+ errorResponse(w, http.StatusInternalServerError, err)
+ return
+ }
+ versions, err := db.LoadVersionsByState(state)
+ if err != nil {
+ errorResponse(w, http.StatusInternalServerError, err)
+ return
+ }
+ usernames, err := db.LoadAccountUsernames()
+ if err != nil {
+ errorResponse(w, http.StatusInternalServerError, err)
+ return
+ }
+ render(w, stateTemplate, http.StatusOK, StatesData{
+ Page: Page{
+ Precedent: "/states",
+ Section: "states",
+ Title: state.Path,
+ },
+ State: state,
+ Usernames: usernames,
+ Versions: versions,
+ })
+ })
+}
diff --git a/pkg/webui/states.go b/pkg/webui/states.go
index 0e956fb..d99d310 100644
--- a/pkg/webui/states.go
+++ b/pkg/webui/states.go
@@ -16,7 +16,7 @@ func handleStatesGET(db *database.DB) http.Handler {
States []model.State
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- states, err := db.LoadStatesByPath()
+ states, err := db.LoadStates()
if err != nil {
errorResponse(w, http.StatusInternalServerError, err)
return
diff --git a/pkg/webui/static/main.css b/pkg/webui/static/main.css
index f0fe350..ae56cd0 100644
--- a/pkg/webui/static/main.css
+++ b/pkg/webui/static/main.css
@@ -1,6 +1,5 @@
-.clickable-rows tbody a {
+table tbody a {
display: block;
- padding: 0 1em 0;
text-decoration: none;
transition: all 0.25s ease-out;
}