Rename templates and port ClusterCockpit navbar + layout

This commit is contained in:
Jan Eitzinger 2022-02-01 17:48:56 +01:00
parent 3dd1d48f86
commit d24e261db2
16 changed files with 237 additions and 122 deletions

View File

@ -190,7 +190,7 @@ func Login(db *sqlx.DB) http.Handler {
if err != nil { if err != nil {
log.Warnf("login of user %#v failed: %s", username, err.Error()) log.Warnf("login of user %#v failed: %s", username, err.Error())
rw.WriteHeader(http.StatusUnauthorized) rw.WriteHeader(http.StatusUnauthorized)
templates.Render(rw, r, "login.html", &templates.Page{ templates.Render(rw, r, "login.tmpl", &templates.Page{
Title: "Login failed", Title: "Login failed",
Login: &templates.LoginPage{ Login: &templates.LoginPage{
Error: "Username or password incorrect", Error: "Username or password incorrect",
@ -291,7 +291,7 @@ func Auth(next http.Handler) http.Handler {
log.Warn("authentication failed: no session or jwt found") log.Warn("authentication failed: no session or jwt found")
rw.WriteHeader(http.StatusUnauthorized) rw.WriteHeader(http.StatusUnauthorized)
templates.Render(rw, r, "login.html", &templates.Page{ templates.Render(rw, r, "login.tmpl", &templates.Page{
Title: "Authentication failed", Title: "Authentication failed",
Login: &templates.LoginPage{ Login: &templates.LoginPage{
Error: "No valid session or JWT provided", Error: "No valid session or JWT provided",
@ -349,7 +349,7 @@ func Logout(rw http.ResponseWriter, r *http.Request) {
} }
} }
templates.Render(rw, r, "login.html", &templates.Page{ templates.Render(rw, r, "login.tmpl", &templates.Page{
Title: "Logout successful", Title: "Logout successful",
Login: &templates.LoginPage{ Login: &templates.LoginPage{
Info: "Logout successful", Info: "Logout successful",

View File

@ -268,7 +268,7 @@ func main() {
} }
handleGetLogin := func(rw http.ResponseWriter, r *http.Request) { handleGetLogin := func(rw http.ResponseWriter, r *http.Request) {
templates.Render(rw, r, "login.html", &templates.Page{ templates.Render(rw, r, "login.tmpl", &templates.Page{
Title: "Login", Title: "Login",
Login: &templates.LoginPage{}, Login: &templates.LoginPage{},
}) })
@ -276,7 +276,7 @@ func main() {
r := mux.NewRouter() r := mux.NewRouter()
r.NotFoundHandler = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { r.NotFoundHandler = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
templates.Render(rw, r, "404.html", &templates.Page{ templates.Render(rw, r, "404.tmpl", &templates.Page{
Title: "Not found", Title: "Not found",
}) })
}) })
@ -301,16 +301,17 @@ func main() {
infos := map[string]interface{}{ infos := map[string]interface{}{
"clusters": config.Clusters, "clusters": config.Clusters,
"username": "",
"admin": true,
} }
if user := auth.GetUser(r.Context()); user != nil { if user := auth.GetUser(r.Context()); user != nil {
infos["username"] = user.Username infos["username"] = user.Username
infos["admin"] = user.HasRole(auth.RoleAdmin) infos["admin"] = user.HasRole(auth.RoleAdmin)
} else {
infos["username"] = false
infos["admin"] = false
} }
templates.Render(rw, r, "home.html", &templates.Page{ templates.Render(rw, r, "home.tmpl", &templates.Page{
Title: "ClusterCockpit", Title: "ClusterCockpit",
Config: conf, Config: conf,
Infos: infos, Infos: infos,
@ -393,6 +394,27 @@ func main() {
log.Print("Gracefull shutdown completed!") log.Print("Gracefull shutdown completed!")
} }
func prepareRoute(r *http.Request) (map[string]interface{}, map[string]interface{}, error) {
conf, err := config.GetUIConfig(r)
if err != nil {
return nil, nil, err
}
infos := map[string]interface{}{
"admin": true,
}
if user := auth.GetUser(r.Context()); user != nil {
infos["username"] = user.Username
infos["admin"] = user.HasRole(auth.RoleAdmin)
} else {
infos["username"] = false
infos["admin"] = false
}
return conf, infos, nil
}
func monitoringRoutes(router *mux.Router, resolver *graph.Resolver) { func monitoringRoutes(router *mux.Router, resolver *graph.Resolver) {
buildFilterPresets := func(query url.Values) map[string]interface{} { buildFilterPresets := func(query url.Values) map[string]interface{} {
filterPresets := map[string]interface{}{} filterPresets := map[string]interface{}{}
@ -444,21 +466,22 @@ func monitoringRoutes(router *mux.Router, resolver *graph.Resolver) {
} }
router.HandleFunc("/monitoring/jobs/", func(rw http.ResponseWriter, r *http.Request) { router.HandleFunc("/monitoring/jobs/", func(rw http.ResponseWriter, r *http.Request) {
conf, err := config.GetUIConfig(r) conf, infos, err := prepareRoute(r)
if err != nil { if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError) http.Error(rw, err.Error(), http.StatusInternalServerError)
return return
} }
templates.Render(rw, r, "monitoring/jobs.html", &templates.Page{ templates.Render(rw, r, "monitoring/jobs.tmpl", &templates.Page{
Title: "Jobs - ClusterCockpit", Title: "Jobs - ClusterCockpit",
Config: conf, Config: conf,
Infos: infos,
FilterPresets: buildFilterPresets(r.URL.Query()), FilterPresets: buildFilterPresets(r.URL.Query()),
}) })
}) })
router.HandleFunc("/monitoring/job/{id:[0-9]+}", func(rw http.ResponseWriter, r *http.Request) { router.HandleFunc("/monitoring/job/{id:[0-9]+}", func(rw http.ResponseWriter, r *http.Request) {
conf, err := config.GetUIConfig(r) conf, infos, err := prepareRoute(r)
if err != nil { if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError) http.Error(rw, err.Error(), http.StatusInternalServerError)
return return
@ -471,49 +494,53 @@ func monitoringRoutes(router *mux.Router, resolver *graph.Resolver) {
return return
} }
templates.Render(rw, r, "monitoring/job.html", &templates.Page{ infos["id"] = id
infos["jobId"] = job.JobID
infos["clusterId"] = job.Cluster
templates.Render(rw, r, "monitoring/job.tmpl", &templates.Page{
Title: fmt.Sprintf("Job %d - ClusterCockpit", job.JobID), Title: fmt.Sprintf("Job %d - ClusterCockpit", job.JobID),
Config: conf, Config: conf,
Infos: map[string]interface{}{ Infos: infos,
"id": id,
"jobId": job.JobID,
"clusterId": job.Cluster,
},
}) })
}) })
router.HandleFunc("/monitoring/users/", func(rw http.ResponseWriter, r *http.Request) { router.HandleFunc("/monitoring/users/", func(rw http.ResponseWriter, r *http.Request) {
conf, err := config.GetUIConfig(r) conf, infos, err := prepareRoute(r)
if err != nil { if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError) http.Error(rw, err.Error(), http.StatusInternalServerError)
return return
} }
templates.Render(rw, r, "monitoring/list.html", &templates.Page{ infos["listType"] = "USER"
templates.Render(rw, r, "monitoring/list.tmpl", &templates.Page{
Title: "Users - ClusterCockpit", Title: "Users - ClusterCockpit",
Config: conf, Config: conf,
FilterPresets: buildFilterPresets(r.URL.Query()), FilterPresets: buildFilterPresets(r.URL.Query()),
Infos: map[string]interface{}{"listType": "USER"}, Infos: infos,
}) })
}) })
router.HandleFunc("/monitoring/projects/", func(rw http.ResponseWriter, r *http.Request) { router.HandleFunc("/monitoring/projects/", func(rw http.ResponseWriter, r *http.Request) {
conf, err := config.GetUIConfig(r) conf, infos, err := prepareRoute(r)
if err != nil { if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError) http.Error(rw, err.Error(), http.StatusInternalServerError)
return return
} }
templates.Render(rw, r, "monitoring/list.html", &templates.Page{ infos["listType"] = "PROJECT"
templates.Render(rw, r, "monitoring/list.tmpl", &templates.Page{
Title: "Projects - ClusterCockpit", Title: "Projects - ClusterCockpit",
Config: conf, Config: conf,
FilterPresets: buildFilterPresets(r.URL.Query()), FilterPresets: buildFilterPresets(r.URL.Query()),
Infos: map[string]interface{}{"listType": "PROJECT"}, Infos: infos,
}) })
}) })
router.HandleFunc("/monitoring/user/{id}", func(rw http.ResponseWriter, r *http.Request) { router.HandleFunc("/monitoring/user/{id}", func(rw http.ResponseWriter, r *http.Request) {
conf, err := config.GetUIConfig(r) conf, infos, err := prepareRoute(r)
if err != nil { if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError) http.Error(rw, err.Error(), http.StatusInternalServerError)
return return
@ -522,17 +549,18 @@ func monitoringRoutes(router *mux.Router, resolver *graph.Resolver) {
id := mux.Vars(r)["id"] id := mux.Vars(r)["id"]
// TODO: One could check if the user exists, but that would be unhelpfull if authentication // TODO: One could check if the user exists, but that would be unhelpfull if authentication
// is disabled or the user does not exist but has started jobs. // is disabled or the user does not exist but has started jobs.
infos["username"] = id
templates.Render(rw, r, "monitoring/user.html", &templates.Page{ templates.Render(rw, r, "monitoring/user.tmpl", &templates.Page{
Title: fmt.Sprintf("User %s - ClusterCockpit", id), Title: fmt.Sprintf("User %s - ClusterCockpit", id),
Config: conf, Config: conf,
Infos: map[string]interface{}{"username": id}, Infos: infos,
FilterPresets: buildFilterPresets(r.URL.Query()), FilterPresets: buildFilterPresets(r.URL.Query()),
}) })
}) })
router.HandleFunc("/monitoring/analysis/", func(rw http.ResponseWriter, r *http.Request) { router.HandleFunc("/monitoring/analysis/", func(rw http.ResponseWriter, r *http.Request) {
conf, err := config.GetUIConfig(r) conf, infos, err := prepareRoute(r)
if err != nil { if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError) http.Error(rw, err.Error(), http.StatusInternalServerError)
return return
@ -544,15 +572,16 @@ func monitoringRoutes(router *mux.Router, resolver *graph.Resolver) {
filterPresets["clusterId"] = query.Get("cluster") filterPresets["clusterId"] = query.Get("cluster")
} }
templates.Render(rw, r, "monitoring/analysis.html", &templates.Page{ templates.Render(rw, r, "monitoring/analysis.tmpl", &templates.Page{
Title: "Analysis View - ClusterCockpit", Title: "Analysis View - ClusterCockpit",
Config: conf, Config: conf,
Infos: infos,
FilterPresets: filterPresets, FilterPresets: filterPresets,
}) })
}) })
router.HandleFunc("/monitoring/systems/", func(rw http.ResponseWriter, r *http.Request) { router.HandleFunc("/monitoring/systems/", func(rw http.ResponseWriter, r *http.Request) {
conf, err := config.GetUIConfig(r) conf, infos, err := prepareRoute(r)
if err != nil { if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError) http.Error(rw, err.Error(), http.StatusInternalServerError)
return return
@ -564,28 +593,29 @@ func monitoringRoutes(router *mux.Router, resolver *graph.Resolver) {
filterPresets["clusterId"] = query.Get("cluster") filterPresets["clusterId"] = query.Get("cluster")
} }
templates.Render(rw, r, "monitoring/systems.html", &templates.Page{ templates.Render(rw, r, "monitoring/systems.tmpl", &templates.Page{
Title: "System View - ClusterCockpit", Title: "System View - ClusterCockpit",
Config: conf, Config: conf,
Infos: infos,
FilterPresets: filterPresets, FilterPresets: filterPresets,
}) })
}) })
router.HandleFunc("/monitoring/node/{clusterId}/{nodeId}", func(rw http.ResponseWriter, r *http.Request) { router.HandleFunc("/monitoring/node/{clusterId}/{nodeId}", func(rw http.ResponseWriter, r *http.Request) {
conf, err := config.GetUIConfig(r) conf, infos, err := prepareRoute(r)
if err != nil { if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError) http.Error(rw, err.Error(), http.StatusInternalServerError)
return return
} }
vars := mux.Vars(r) vars := mux.Vars(r)
templates.Render(rw, r, "monitoring/node.html", &templates.Page{ infos["nodeId"] = vars["nodeId"]
infos["clusterId"] = vars["clusterId"]
templates.Render(rw, r, "monitoring/node.tmpl", &templates.Page{
Title: fmt.Sprintf("Node %s - ClusterCockpit", vars["nodeId"]), Title: fmt.Sprintf("Node %s - ClusterCockpit", vars["nodeId"]),
Config: conf, Config: conf,
Infos: map[string]interface{}{ Infos: infos,
"nodeId": vars["nodeId"],
"clusterId": vars["clusterId"],
},
}) })
}) })
} }

View File

@ -1,4 +1,4 @@
{{template "base.html" .}} {{template "base.tmpl" .}}
{{define "content"}} {{define "content"}}
<div class="row"> <div class="row">
<div class="col"> <div class="col">

View File

@ -1,28 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width,initial-scale=1'>
<title>{{.Title}}</title>
<link rel='icon' type='image/png' href='/favicon.png'>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.1/dist/css/bootstrap.min.css" integrity="sha384-F3w7mX95PdgyTmZZMECAngseQB83DfGTowi0iMjiWaeVhAn4FJkqJByhZMI3AhiU" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.4.1/font/bootstrap-icons.css">
<link rel='stylesheet' href='/global.css'>
<link rel='stylesheet' href='/uPlot.min.css'>
{{block "stylesheets" .}}{{end}}
</head>
<body>
<div class="container">
<div class="row">
<div class="col">
{{block "content" .}}
Whoops, you should not see this...
{{end}}
</div>
</div>
</div>
{{block "javascript" .}}{{end}}
</body>
</html>

119
templates/base.tmpl Normal file
View File

@ -0,0 +1,119 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<meta name='viewport' content='width=device-width,initial-scale=1'>
<title>{{.Title}}</title>
<link rel='icon' type='image/png' href='/favicon.png'>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.1/dist/css/bootstrap.min.css" integrity="sha384-F3w7mX95PdgyTmZZMECAngseQB83DfGTowi0iMjiWaeVhAn4FJkqJByhZMI3AhiU" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.4.1/font/bootstrap-icons.css">
<link rel='stylesheet' href='/global.css'>
<link rel='stylesheet' href='/uPlot.min.css'>
{{block "stylesheets" .}}
{{end}}
</head>
<body class="Site">
<header>
<nav class="navbar navbar-expand-lg navbar-light fixed-top bg-light">
<div class="container-fluid">
<a class="navbar-brand" href="/">
{{block "brand" .}}
<img alt="ClusterCockpit Logo" src="/img/logo.png" class="d-inline-block align-top">
{{end}}
</a>
{{block "navigation" .}}
<ul class="navbar-nav mr-auto">
{{block "navitem_joblist" .}}
<li class="nav-item">
<a class="nav-link fs-5" href="/monitoring/jobs/">
<span class="cc-nav-text">Joblist</span>
<i class="bi-card-list"></i>
</a>
</li>
{{end}}
{{if .Infos.admin }}
{{block "navitem_analysis" .}}
<li class="nav-item">
<a class="nav-link fs-5" href="/monitoring/analysis/">
<span class="cc-nav-text">Analysis</span>
<i class="bi-graph-up"></i>
</a>
</li>
{{end}}
{{block "navitem_systems" .}}
<li class="nav-item">
<a class="nav-link fs-5" href="/monitoring/systems/">
<span class="cc-nav-text">Systems</span>
<i class="bi-graph-up"></i>
</a>
</li>
{{end}}
{{block "navitem_users" .}}
<li class="nav-item">
<a class="nav-link fs-5" href="/monitoring/users/">
<span class="cc-nav-text">Users</span>
<i class="bi-people-fill"></i>
</a>
</li>
{{end}}
{{else}}
{{block "navitem_stats" .}}
<li class="nav-item">
<a class="nav-link fs-5" href="/monitoring/user/admin">
<span class="cc-nav-text">Statistics</span>
<i class="bi-bar-chart-line-fill"></i>
</a>
</li>
{{end}}
{{end}}
</ul>
{{end}}
{{if .Infos.username }}
<div class="d-flex align-items-end">
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<form method="post" action="/logout">
<button type="submit" class="btn btn-link nav-link fs-5">
<span class="cc-nav-text">{{ .Infos.username }} Logout</span>
<i class="bi-box-arrow-right"></i>
</button>
</form>
</li>
</ul>
<form class="d-flex my-0" onsubmit="this.action='/search';">
{{if .Infos.admin }}
<input class="form-control me-2" type="search" name="searchId" placeholder="jobId / userId" id="searchId" aria-label="Search">
{{else}}
<input class="form-control me-2" type="search" name="searchId" placeholder="jobId" id="searchId" aria-label="Search">
{{end}}
<button class="btn btn-outline-success fs-6" type="submit">Search</button>
</form>
</div>
{{end}}
</div>
</nav>
</header>
<main class="Site-content">
<div class="container">
{{block "content" .}}
Whoops, you should not see this...
{{end}}
</div>
</main>
{{block "footer" .}}
<footer class="site-footer bg-light">
<ul class="footer-list">
<li class="footer-list-item"><a class="link-secondary fs-5" href="/imprint" title="Imprint" rel="nofollow">Imprint</a></li>
<li class="footer-list-item"><a class="link-secondary fs-5" href="/privacy" title="Privacy Policy" rel="nofollow">Privacy Policy</a></li>
</ul>
</footer>
{{end}}
{{block "javascript" .}}
{{end}}
</body>
</html>

View File

@ -1,47 +0,0 @@
{{define "content"}}
<div class="row">
<div class="col">
<h1>
ClusterCockpit Login
</h1>
</div>
</div>
<div class="row">
<div class="col">
{{if .Login.Error}}
<div class="alert alert-warning" role="alert">
{{.Login.Error}}
</div>
{{end}}
{{if .Login.Info}}
<div class="alert alert-success" role="alert">
{{.Login.Info}}
</div>
{{end}}
</div>
</div>
<div class="row">
<div class="col">
<form method="post" action="/login">
<div class="mb-3">
<label class="form-label" for="username">Username</label>
<input class="form-control" type="text" id="username" name="username">
</div>
<div class="mb-3">
<label class="form-label" for="password">Password</label>
<input class="form-control" type="password" id="password" name="password">
</div>
<button type="submit" class="btn btn-primary">Login</button>
</form>
</div>
</div>
<br/>
<div class="row">
<div class="col">
<form method="post" action="/logout">
<button type="submit" class="btn btn-primary">Logout</button>
</form>
</div>
</div>
{{end}}

41
templates/login.tmpl Normal file
View File

@ -0,0 +1,41 @@
{{define "navigation"}}
{{end}}
{{define "content"}}
<section class="content-section">
<div class="container">
<div class="row">
<div class="col-4 mx-auto">
{{if .Login.Error}}
<div class="alert alert-warning" role="alert">
{{.Login.Error}}
</div>
{{end}}
{{if .Login.Info}}
<div class="alert alert-success" role="alert">
{{.Login.Info}}
</div>
{{end}}
<div class="card">
<div class="card-header">
<h3>Login</h3>
</div>
<div class="card-body">
<form action="/login" method="post">
<div class="mb-3">
<label class="form-label" for="username">Username</label>
<input class="form-control" type="text" id="username" name="username" required autofocus/>
</div>
<div class="mb-3">
<label class="form-label" for="password">Password</label>
<input class="form-control" type="password" id="password" name="password" required/>
</div>
<button type="submit" class="btn btn-success">Login</button>
</form>
</div>
</div>
</div>
</section>
{{end}}

View File

@ -27,16 +27,16 @@ type LoginPage struct {
func init() { func init() {
templatesDir = "./templates/" templatesDir = "./templates/"
base := template.Must(template.ParseFiles(templatesDir + "base.html")) base := template.Must(template.ParseFiles(templatesDir + "base.tmpl"))
files := []string{ files := []string{
"home.html", "404.html", "login.html", "home.tmpl", "404.tmpl", "login.tmpl",
"monitoring/jobs.html", "monitoring/jobs.tmpl",
"monitoring/job.html", "monitoring/job.tmpl",
"monitoring/list.html", "monitoring/list.tmpl",
"monitoring/user.html", "monitoring/user.tmpl",
// "monitoring/analysis.html", // "monitoring/analysis.tmpl",
// "monitoring/systems.html", // "monitoring/systems.tmpl",
// "monitoring/node.html", // "monitoring/node.tmpl",
} }
for _, file := range files { for _, file := range files {
@ -51,7 +51,7 @@ func Render(rw http.ResponseWriter, r *http.Request, file string, page *Page) {
} }
if debugMode { if debugMode {
t = template.Must(template.ParseFiles(templatesDir+"base.html", templatesDir+file)) t = template.Must(template.ParseFiles(templatesDir+"base.tmpl", templatesDir+file))
} }
if err := t.Execute(rw, page); err != nil { if err := t.Execute(rw, page); err != nil {