Add templates and custom urls for monitoring views

This commit is contained in:
Lou Knauer 2021-12-08 15:50:03 +01:00
parent c79fcec3ba
commit a26d652332
11 changed files with 216 additions and 15 deletions

View File

@ -11,7 +11,7 @@ git clone --recursive git@github.com:ClusterCockpit/cc-jobarchive.git
# Prepare frontend # Prepare frontend
cd ./cc-jobarchive/frontend cd ./cc-jobarchive/frontend
yarn install yarn install
yarn build CCFRONTEND_ROLLUP_INTRO="" yarn build
cd .. cd ..
go get go get
@ -33,7 +33,16 @@ touch ./var/job.db
./cc-jobarchive --help ./cc-jobarchive --help
``` ```
### Configuration
A config file in the JSON format can be provided using `--config` to override the defaults. Loop at the beginning of `server.go` for the defaults and consequently the format of the configuration file.
### Update GraphQL schema ### Update GraphQL schema
This project uses [gqlgen](https://github.com/99designs/gqlgen) for the GraphQL API. The schema can be found in `./graph/schema.graphqls`. After changing it, you need to run `go run github.com/99designs/gqlgen` which will update `graph/model`. In case new resolvers are needed, they will be inserted into `graph/schema.resolvers.go`, where you will need to implement them. This project uses [gqlgen](https://github.com/99designs/gqlgen) for the GraphQL API. The schema can be found in `./graph/schema.graphqls`. After changing it, you need to run `go run github.com/99designs/gqlgen` which will update `graph/model`. In case new resolvers are needed, they will be inserted into `graph/schema.resolvers.go`, where you will need to implement them.
### TODO
- [ ] Documentation
- [ ] Write more TODOs

View File

@ -168,7 +168,7 @@ func Login(db *sqlx.DB) http.Handler {
if err != nil { if err != nil {
log.Printf("login failed: %s\n", err.Error()) log.Printf("login failed: %s\n", err.Error())
rw.WriteHeader(http.StatusUnauthorized) rw.WriteHeader(http.StatusUnauthorized)
templates.Render(rw, r, "login.html", &templates.Page{ templates.Render(rw, r, "login", &templates.Page{
Title: "Login failed", Title: "Login failed",
Login: &templates.LoginPage{ Login: &templates.LoginPage{
Error: "Username or password incorrect", Error: "Username or password incorrect",
@ -264,7 +264,7 @@ func Auth(next http.Handler) http.Handler {
log.Printf("authentication failed: no session or jwt found\n") log.Printf("authentication failed: no session or jwt found\n")
rw.WriteHeader(http.StatusUnauthorized) rw.WriteHeader(http.StatusUnauthorized)
templates.Render(rw, r, "login.html", &templates.Page{ templates.Render(rw, r, "login", &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",
@ -320,7 +320,7 @@ func Logout(rw http.ResponseWriter, r *http.Request) {
} }
} }
templates.Render(rw, r, "login.html", &templates.Page{ templates.Render(rw, r, "login", &templates.Page{
Title: "Logout successful", Title: "Logout successful",
Login: &templates.LoginPage{ Login: &templates.LoginPage{
Info: "Logout successful", Info: "Logout successful",

View File

@ -3,6 +3,7 @@ package main
import ( import (
"encoding/json" "encoding/json"
"flag" "flag"
"fmt"
"log" "log"
"net/http" "net/http"
"os" "os"
@ -141,11 +142,12 @@ func main() {
// Build routes... // Build routes...
graphQLEndpoint := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &graph.Resolver{DB: db}})) resolver := &graph.Resolver{DB: db}
graphQLEndpoint := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: resolver}))
graphQLPlayground := playground.Handler("GraphQL playground", "/query") graphQLPlayground := playground.Handler("GraphQL playground", "/query")
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", &templates.Page{
Title: "Login", Title: "Login",
Login: &templates.LoginPage{}, Login: &templates.LoginPage{},
}) })
@ -153,7 +155,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", &templates.Page{
Title: "Not found", Title: "Not found",
}) })
}) })
@ -170,9 +172,17 @@ func main() {
secured.Handle("/query", graphQLEndpoint) secured.Handle("/query", graphQLEndpoint)
secured.HandleFunc("/api/jobs/start_job/", startJob).Methods(http.MethodPost) secured.HandleFunc("/api/jobs/start_job/", startJob).Methods(http.MethodPost)
secured.HandleFunc("/api/jobs/stop_job/", stopJob).Methods(http.MethodPost, http.MethodPut) secured.HandleFunc("/api/jobs/stop_job/", stopJob).Methods(http.MethodPost, http.MethodPut)
secured.HandleFunc("/api/jobs/stop_job/{id}", stopJob).Methods(http.MethodPost, http.MethodPut) secured.HandleFunc("/api/jobs/stop_job/{id:[0-9]+}", stopJob).Methods(http.MethodPost, http.MethodPut)
secured.HandleFunc("/config.json", config.ServeConfig).Methods(http.MethodGet) secured.HandleFunc("/config.json", config.ServeConfig).Methods(http.MethodGet)
secured.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
templates.Render(rw, r, "home", &templates.Page{
Title: "ClusterCockpit",
})
})
monitoringRoutes(secured, resolver)
r.PathPrefix("/").Handler(http.FileServer(http.Dir(programConfig.StaticFiles))) r.PathPrefix("/").Handler(http.FileServer(http.Dir(programConfig.StaticFiles)))
handler := handlers.CORS( handler := handlers.CORS(
handlers.AllowedHeaders([]string{"X-Requested-With", "Content-Type", "Authorization"}), handlers.AllowedHeaders([]string{"X-Requested-With", "Content-Type", "Authorization"}),
@ -189,3 +199,76 @@ func main() {
} }
log.Fatal(err) log.Fatal(err)
} }
func monitoringRoutes(router *mux.Router, resolver *graph.Resolver) {
router.HandleFunc("/monitoring/jobs/", func(rw http.ResponseWriter, r *http.Request) {
conf, err := config.GetUIConfig(r)
if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
templates.Render(rw, r, "monitoring/jobs/", &templates.Page{
Title: "Jobs - ClusterCockpit",
Config: conf,
})
})
router.HandleFunc("/monitoring/job/{id:[0-9]+}", func(rw http.ResponseWriter, r *http.Request) {
conf, err := config.GetUIConfig(r)
if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
id := mux.Vars(r)["id"]
job, err := resolver.Query().Job(r.Context(), id)
if err != nil {
http.Error(rw, err.Error(), http.StatusNotFound)
return
}
templates.Render(rw, r, "monitoring/job/", &templates.Page{
Title: fmt.Sprintf("Job %s - ClusterCockpit", job.JobID),
Config: conf,
Infos: map[string]interface{}{
"id": id,
"jobId": job.JobID,
"clusterId": job.ClusterID,
},
})
})
router.HandleFunc("/monitoring/users/", func(rw http.ResponseWriter, r *http.Request) {
conf, err := config.GetUIConfig(r)
if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
templates.Render(rw, r, "monitoring/users/", &templates.Page{
Title: "Users - ClusterCockpit",
Config: conf,
})
})
router.HandleFunc("/monitoring/user/{id}", func(rw http.ResponseWriter, r *http.Request) {
conf, err := config.GetUIConfig(r)
if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
id := mux.Vars(r)["id"]
// 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.
templates.Render(rw, r, "monitoring/user/", &templates.Page{
Title: fmt.Sprintf("User %s - ClusterCockpit", id),
Config: conf,
Infos: map[string]interface{}{
"userId": id,
},
})
})
}

View File

@ -9,6 +9,9 @@
<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@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="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.4.1/font/bootstrap-icons.css">
<link rel='stylesheet' href='/global.css'> <link rel='stylesheet' href='/global.css'>
<link rel='stylesheet' href='/uPlot.min.css'>
{{block "stylesheets" .}}{{end}}
</head> </head>
<body> <body>
<div class="container"> <div class="container">
@ -20,5 +23,6 @@
</div> </div>
</div> </div>
</div> </div>
{{block "javascript" .}}{{end}}
</body> </body>
</html> </html>

10
templates/home.html Normal file
View File

@ -0,0 +1,10 @@
{{define "content"}}
<div class="row">
<div class="col">
<ul>
<li><a href="/monitoring/jobs/">jobs</a></li>
<li><a href="/monitoring/users/">users</a></li>
</ul>
</div>
</div>
{{end}}

View File

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

View File

@ -0,0 +1,29 @@
{{define "content"}}
<div id="svelte-app"></div>
{{end}}
{{define "stylesheets"}}
<link rel='stylesheet' href='/build/job.css'>
{{end}}
{{define "javascript"}}
<script>
const jobInfos = {
id: "{{ .Infos.id }}",
jobId: "{{ .Infos.jobId }}",
clusterId: "{{ .Infos.clusterId }}"
};
const clusterCockpitConfigPromise = Promise.resolve({
plot_general_colorscheme: {{ .Config.plot_general_colorscheme }},
plot_general_lineWidth: {{ .Config.plot_general_lineWidth }},
plot_general_colorBackground: {{ .Config.plot_general_colorBackground }},
plot_view_showRoofline: {{ .Config.plot_view_showRoofline }},
plot_view_showPolarplot: {{ .Config.plot_view_showPolarplot }},
plot_view_showStatTable: {{ .Config.plot_view_showStatTable }},
plot_view_plotsPerRow: {{ .Config.plot_view_plotsPerRow }},
job_view_selectedMetrics: {{ .Config.job_view_selectedMetrics }},
job_view_nodestats_selectedMetrics: {{ .Config.job_view_nodestats_selectedMetrics }},
job_view_polarPlotMetrics: {{ .Config.plot_view_polarPlotMetrics }},
});
</script>
<script src='/build/job.js'></script>
{{end}}

View File

@ -0,0 +1,20 @@
{{define "content"}}
<div id="svelte-app"></div>
{{end}}
{{define "stylesheets"}}
<link rel='stylesheet' href='/build/jobs.css'>
{{end}}
{{define "javascript"}}
<script>
const filterPresets = null;
const clusterCockpitConfigPromise = Promise.resolve({
plot_general_colorscheme: {{ .Config.plot_general_colorscheme }},
plot_general_lineWidth: {{ .Config.plot_general_lineWidth }},
plot_general_colorBackground: {{ .Config.plot_general_colorBackground }},
plot_list_selectedMetrics: {{ .Config.plot_list_selectedMetrics }},
plot_list_jobsPerPage: {{ .Config.plot_list_jobsPerPage }}
});
</script>
<script src='/build/jobs.js'></script>
{{end}}

View File

@ -0,0 +1,22 @@
{{define "content"}}
<div id="svelte-app"></div>
{{end}}
{{define "stylesheets"}}
<link rel='stylesheet' href='/build/user.css'>
{{end}}
{{define "javascript"}}
<script>
const userInfos = {
userId: "{{ .Infos.userId }}"
};
const clusterCockpitConfigPromise = Promise.resolve({
plot_general_colorscheme: {{ .Config.plot_general_colorscheme }},
plot_general_lineWidth: {{ .Config.plot_general_lineWidth }},
plot_general_colorBackground: {{ .Config.plot_general_colorBackground }},
plot_list_selectedMetrics: {{ .Config.plot_list_selectedMetrics }},
plot_list_jobsPerPage: {{ .Config.plot_list_jobsPerPage }}
});
</script>
<script src='/build/user.js'></script>
{{end}}

View File

@ -0,0 +1,14 @@
{{define "content"}}
<div id="svelte-app"></div>
{{end}}
{{define "stylesheets"}}
<link rel='stylesheet' href='/build/users.css'>
{{end}}
{{define "javascript"}}
<script>
const filterPresets = null;
const clusterCockpitConfigPromise = Promise.resolve({});
</script>
<script src='/build/users.js'></script>
{{end}}

View File

@ -6,11 +6,13 @@ import (
"net/http" "net/http"
) )
var templates *template.Template var templates map[string]*template.Template
type Page struct { type Page struct {
Title string Title string
Login *LoginPage Login *LoginPage
Infos map[string]interface{}
Config map[string]interface{}
} }
type LoginPage struct { type LoginPage struct {
@ -19,11 +21,20 @@ type LoginPage struct {
} }
func init() { func init() {
templates = template.Must(template.ParseGlob("./templates/*.html")) base := template.Must(template.ParseFiles("./templates/base.html"))
templates = map[string]*template.Template{
"home": template.Must(template.Must(base.Clone()).ParseFiles("./templates/home.html")),
"404": template.Must(template.Must(base.Clone()).ParseFiles("./templates/404.html")),
"login": template.Must(template.Must(base.Clone()).ParseFiles("./templates/login.html")),
"monitoring/jobs/": template.Must(template.Must(base.Clone()).ParseFiles("./templates/monitoring/jobs.html")),
"monitoring/job/": template.Must(template.Must(base.Clone()).ParseFiles("./templates/monitoring/job.html")),
"monitoring/users/": template.Must(template.Must(base.Clone()).ParseFiles("./templates/monitoring/users.html")),
"monitoring/user/": template.Must(template.Must(base.Clone()).ParseFiles("./templates/monitoring/user.html")),
}
} }
func Render(rw http.ResponseWriter, r *http.Request, name string, page *Page) { func Render(rw http.ResponseWriter, r *http.Request, name string, page *Page) {
if err := templates.ExecuteTemplate(rw, name, page); err != nil { if err := templates[name].Execute(rw, page); err != nil {
log.Printf("template error: %s\n", err.Error()) log.Printf("template error: %s\n", err.Error())
} }
} }