diff options
author | Julien Dessaux | 2025-01-27 22:00:51 +0100 |
---|---|---|
committer | Julien Dessaux | 2025-01-27 22:00:51 +0100 |
commit | 26e10a9399bd6185741d0912ffa54c807ffef671 (patch) | |
tree | 0cf9e81c6880b07d9c029e3abd6ee03b6a2acdb1 | |
parent | chore(webui): remove redundant set of the Cache-Control header in state get h... (diff) | |
download | tfstated-26e10a9399bd6185741d0912ffa54c807ffef671.tar.gz tfstated-26e10a9399bd6185741d0912ffa54c807ffef671.tar.bz2 tfstated-26e10a9399bd6185741d0912ffa54c807ffef671.zip |
feat(webui): implement state versions list
-rw-r--r-- | pkg/database/accounts.go | 25 | ||||
-rw-r--r-- | pkg/database/states.go | 26 | ||||
-rw-r--r-- | pkg/database/versions.go | 35 | ||||
-rw-r--r-- | pkg/model/version.go | 14 | ||||
-rw-r--r-- | pkg/webui/html/base.html | 8 | ||||
-rw-r--r-- | pkg/webui/html/state.html | 30 | ||||
-rw-r--r-- | pkg/webui/html/states.html | 2 | ||||
-rw-r--r-- | pkg/webui/index.go | 5 | ||||
-rw-r--r-- | pkg/webui/routes.go | 1 | ||||
-rw-r--r-- | pkg/webui/state.go | 54 | ||||
-rw-r--r-- | pkg/webui/states.go | 2 | ||||
-rw-r--r-- | pkg/webui/static/main.css | 3 |
12 files changed, 195 insertions, 10 deletions
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 @@ <span>Login</span> </a> {{ else }} -<a href="/states"{{ if eq .Page.Section "states" }} class="active"{{ end}}> +<a href="/states"{{ if eq .Page.Section "states" }} class="fill"{{ end}}> <i>home_storage</i> <span>States</span> </a> -<hr> <a href="/logout"> <i>logout</i> <span>Logout</span> @@ -39,6 +38,11 @@ <nav class="bottom s">{{ template "nav" . }}</nav> <header> <nav> + {{ if ne .Page.Precedent "" }} + <a href="{{ .Page.Precedent }}" class="button circle chip"> + <i>arrow_back</i> + </a> + {{ end }} <h5 class="max center-align">{{ .Page.Title }}</h5> </nav> </header> 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" }} +<main class="responsive" id="main"> + <p> + Locked: + {{ if eq .State.Lock nil }}no{{ else }} + <span>yes</span> + <div class="tooltip left max"> + <b>Lock</b> + <p>{{ .State.Lock }}</p> + </div> + {{ end }} + </p> + <table class="clickable-rows no-space"> + <thead> + <tr> + <th>By</th> + <th>Created</th> + </tr> + </thead> + <tbody> + {{ range .Versions }} + <tr> + <td><a href="/version/{{ .Id }}">{{ index $.Usernames .AccountId }}</a></td> + <td><a href="/version/{{ .Id }}">{{ .Created }}</a></td> + </tr> + {{ end }} + </tbody> + </table> +</main> +{{ 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" }} <main class="responsive" id="main"> - <table class="clickable-rows no-space stripes"> + <table class="clickable-rows no-space"> <thead> <tr> <th>Path</th> 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; } |