feat(webui): bootstrap a proper UI
This commit is contained in:
parent
09885ef1e4
commit
4e029fb83a
11 changed files with 127 additions and 75 deletions
|
@ -9,11 +9,13 @@ var errorTemplates = template.Must(template.ParseFS(htmlFS, "html/base.html", "h
|
||||||
|
|
||||||
func errorResponse(w http.ResponseWriter, status int, err error) {
|
func errorResponse(w http.ResponseWriter, status int, err error) {
|
||||||
type ErrorData struct {
|
type ErrorData struct {
|
||||||
|
Page
|
||||||
Err error
|
Err error
|
||||||
Status int
|
Status int
|
||||||
StatusText string
|
StatusText string
|
||||||
}
|
}
|
||||||
render(w, errorTemplates, status, &ErrorData{
|
render(w, errorTemplates, status, &ErrorData{
|
||||||
|
Page: Page{Title: "Error", Section: "error"},
|
||||||
Err: err,
|
Err: err,
|
||||||
Status: status,
|
Status: status,
|
||||||
StatusText: http.StatusText(status),
|
StatusText: http.StatusText(status),
|
||||||
|
|
|
@ -1,28 +1,50 @@
|
||||||
|
{{ define "nav" }}
|
||||||
|
<header>
|
||||||
|
<nav>
|
||||||
|
<a href="/">
|
||||||
|
<h6>TFSTATED</h6>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
{{ if eq .Page.Section "login" }}
|
||||||
|
<a href="/login" class="active">
|
||||||
|
<i>login</i>
|
||||||
|
<span>Login</span>
|
||||||
|
</a>
|
||||||
|
{{ else }}
|
||||||
|
<a href="/states"{{ if eq .Page.Section "states" }} class="active"{{ end}}>
|
||||||
|
<i>home_storage</i>
|
||||||
|
<span>States</span>
|
||||||
|
</a>
|
||||||
|
<hr>
|
||||||
|
<a href="/logout">
|
||||||
|
<i>logout</i>
|
||||||
|
<span>Logout</span>
|
||||||
|
</a>
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="icon" href="/static/favicon.svg">
|
<link rel="icon" href="/static/favicon.svg">
|
||||||
<link rel="stylesheet" href="/static/main.css">
|
<link href="/static/main.css" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
|
<link href="https://cdn.jsdelivr.net/npm/beercss@3.8.0/dist/cdn/beer.min.css" rel="stylesheet">
|
||||||
<title>tfstated</title>
|
<title>TFSTATED - {{ .Page.Title }}</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="dark">
|
||||||
<header class="container">
|
<nav class="left drawer l">{{ template "nav" . }}</nav>
|
||||||
|
<nav class="left m">{{ template "nav" . }}</nav>
|
||||||
|
<nav class="bottom s">{{ template "nav" . }}</nav>
|
||||||
|
<header>
|
||||||
<nav>
|
<nav>
|
||||||
<ul>
|
<h5 class="max center-align">{{ .Page.Title }}</h5>
|
||||||
<li><a href="/"><strong>TFSTATED</strong></a></li>
|
|
||||||
</ul>
|
|
||||||
<ul>
|
|
||||||
<li><a href="/states">States</a></li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
<main class="container">
|
|
||||||
{{ template "main" . }}
|
{{ template "main" . }}
|
||||||
</main>
|
<footer>
|
||||||
<footer class="container">
|
|
||||||
</footer>
|
</footer>
|
||||||
|
<script type="module" src="https://cdn.jsdelivr.net/npm/beercss@3.8.0/dist/cdn/beer.min.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{{ define "main" }}
|
{{ define "main" }}
|
||||||
<article>
|
<main class="responsive">
|
||||||
<h1>{{ .Status }} - {{ .StatusText }}</h1>
|
<h5>{{ .Status }} - {{ .StatusText }}</h5>
|
||||||
<p>{{ .Err }}</p>
|
<p>{{ .Err }}</p>
|
||||||
</article>
|
</main>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
|
@ -1,29 +1,26 @@
|
||||||
{{ define "main" }}
|
{{ define "main" }}
|
||||||
{{ if .Forbidden }}
|
<main class="responsive">
|
||||||
<article>
|
|
||||||
<p class="error-message">Invalid username or password</p>
|
|
||||||
</article>
|
|
||||||
{{ end }}
|
|
||||||
<form action="/login" method="post">
|
<form action="/login" method="post">
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label>
|
<div class="field border label{{ if .Forbidden }} invalid{{ end}}">
|
||||||
Username
|
<input id="username"
|
||||||
<input type="text"
|
|
||||||
placeholder="Username"
|
|
||||||
name="username"
|
name="username"
|
||||||
|
type="text"
|
||||||
value="{{ .Username }}"
|
value="{{ .Username }}"
|
||||||
{{ if .Forbidden }}aria-invalid="true"{{ end }}
|
|
||||||
required>
|
required>
|
||||||
</label>
|
<label for="username">Username</label>
|
||||||
<label>
|
{{ if .Forbidden }}<span class="error">Invalid username or password</span>{{ end }}
|
||||||
Password
|
</div>
|
||||||
<input type="password"
|
<div class="field border label{{ if .Forbidden }} invalid{{ end}}">
|
||||||
placeholder="Password"
|
<input id="password"
|
||||||
name="password"
|
name="password"
|
||||||
{{ if .Forbidden }}aria-invalid="true"{{ end }}
|
type="password"
|
||||||
required>
|
required>
|
||||||
</label>
|
<label for="password">Password</label>
|
||||||
|
{{ if .Forbidden }}<span class="error">Invalid username or password</span>{{ end }}
|
||||||
|
</div>
|
||||||
|
<button class="small-round" type="submit" value="login">Login</button>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<button type="submit" value="login">Login</button>
|
|
||||||
</form>
|
</form>
|
||||||
|
</main>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{{ define "main" }}
|
{{ define "main" }}
|
||||||
<article>
|
<main class="responsive">
|
||||||
<p>Logout successful</p>
|
<h5>Logout successful</h5>
|
||||||
</article>
|
</main>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
|
@ -1,23 +1,32 @@
|
||||||
{{ define "main" }}
|
{{ define "main" }}
|
||||||
<h1>States</h1>
|
<main class="responsive" id="main">
|
||||||
<table class="striped">
|
<table class="clickable-rows no-space stripes">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">Path</th>
|
<th>Path</th>
|
||||||
<th scope="col">Created</th>
|
<th>Updated</th>
|
||||||
<th scope="col">Updated</th>
|
<th>Locked</th>
|
||||||
<th scope="col">Locked</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{{ range .States }}
|
{{ range .States }}
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{{ .Path }}</th>
|
<td><a href="/state/{{ .Id }}">{{ .Path }}</a></td>
|
||||||
<td>{{ .Created }}</td>
|
<td><a href="/state/{{ .Id }}">{{ .Updated }}</a></td>
|
||||||
<td>{{ .Updated }}</td>
|
<td>
|
||||||
<td>{{ .Lock }}</td>
|
<a href="/state/{{ .Id }}">
|
||||||
|
{{ if eq .Lock nil }}no{{ else }}
|
||||||
|
<span>yes</span>
|
||||||
|
<div class="tooltip left max">
|
||||||
|
<b>Lock</b>
|
||||||
|
<p>{{ .Lock }}</p>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</main>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
|
@ -5,6 +5,11 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Page struct {
|
||||||
|
Section string
|
||||||
|
Title string
|
||||||
|
}
|
||||||
|
|
||||||
func handleIndexGET() http.Handler {
|
func handleIndexGET() http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.URL.Path == "/" {
|
if r.URL.Path == "/" {
|
||||||
|
|
|
@ -14,6 +14,7 @@ import (
|
||||||
var loginTemplate = template.Must(template.ParseFS(htmlFS, "html/base.html", "html/login.html"))
|
var loginTemplate = template.Must(template.ParseFS(htmlFS, "html/base.html", "html/login.html"))
|
||||||
|
|
||||||
type loginPage struct {
|
type loginPage struct {
|
||||||
|
Page
|
||||||
Forbidden bool
|
Forbidden bool
|
||||||
Username string
|
Username string
|
||||||
}
|
}
|
||||||
|
@ -28,7 +29,9 @@ func handleLoginGET() http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
render(w, loginTemplate, http.StatusOK, loginPage{})
|
render(w, loginTemplate, http.StatusOK, loginPage{
|
||||||
|
Page: Page{Title: "Login", Section: "login"},
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,6 +39,7 @@ func handleLoginPOST(db *database.DB) http.Handler {
|
||||||
var validUsername = regexp.MustCompile(`^[a-zA-Z]\w*$`)
|
var validUsername = regexp.MustCompile(`^[a-zA-Z]\w*$`)
|
||||||
renderForbidden := func(w http.ResponseWriter, username string) {
|
renderForbidden := func(w http.ResponseWriter, username string) {
|
||||||
render(w, loginTemplate, http.StatusForbidden, loginPage{
|
render(w, loginTemplate, http.StatusForbidden, loginPage{
|
||||||
|
Page: Page{Title: "Login", Section: "login"},
|
||||||
Forbidden: true,
|
Forbidden: true,
|
||||||
Username: username,
|
Username: username,
|
||||||
})
|
})
|
||||||
|
|
|
@ -11,6 +11,9 @@ import (
|
||||||
var logoutTemplate = template.Must(template.ParseFS(htmlFS, "html/base.html", "html/logout.html"))
|
var logoutTemplate = template.Must(template.ParseFS(htmlFS, "html/base.html", "html/logout.html"))
|
||||||
|
|
||||||
func handleLogoutGET(db *database.DB) http.Handler {
|
func handleLogoutGET(db *database.DB) http.Handler {
|
||||||
|
type logoutPage struct {
|
||||||
|
Page
|
||||||
|
}
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
session := r.Context().Value(model.SessionContextKey{})
|
session := r.Context().Value(model.SessionContextKey{})
|
||||||
err := db.DeleteSession(session.(*model.Session))
|
err := db.DeleteSession(session.(*model.Session))
|
||||||
|
@ -19,6 +22,8 @@ func handleLogoutGET(db *database.DB) http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
unsetSesssionCookie(w)
|
unsetSesssionCookie(w)
|
||||||
render(w, logoutTemplate, http.StatusOK, nil)
|
render(w, logoutTemplate, http.StatusOK, logoutPage{
|
||||||
|
Page: Page{Title: "Logout", Section: "login"},
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ var statesTemplates = template.Must(template.ParseFS(htmlFS, "html/base.html", "
|
||||||
|
|
||||||
func handleStatesGET(db *database.DB) http.Handler {
|
func handleStatesGET(db *database.DB) http.Handler {
|
||||||
type StatesData struct {
|
type StatesData struct {
|
||||||
|
Page
|
||||||
States []model.State
|
States []model.State
|
||||||
}
|
}
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -23,6 +24,7 @@ func handleStatesGET(db *database.DB) http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
render(w, statesTemplates, http.StatusOK, StatesData{
|
render(w, statesTemplates, http.StatusOK, StatesData{
|
||||||
|
Page: Page{Title: "States", Section: "states"},
|
||||||
States: states,
|
States: states,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,3 +1,9 @@
|
||||||
.error-message {
|
.clickable-rows tbody a {
|
||||||
color:red;
|
display: block;
|
||||||
|
padding: 0 1em 0;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.25s ease-out;
|
||||||
|
}
|
||||||
|
.clickable-rows tbody tr:hover a {
|
||||||
|
background-color: var(--secondary-container);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue