diff --git a/README.md b/README.md index f57a0f2..2c941af 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ git clone --recursive git@github.com:ClusterCockpit/cc-jobarchive.git # Prepare frontend cd ./cc-jobarchive/frontend yarn install -yarn build +CCFRONTEND_ROLLUP_INTRO="" yarn build cd .. go get @@ -33,7 +33,16 @@ touch ./var/job.db ./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 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 + diff --git a/auth/auth.go b/auth/auth.go index 66fd1e9..d10ac3b 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -168,7 +168,7 @@ func Login(db *sqlx.DB) http.Handler { if err != nil { log.Printf("login failed: %s\n", err.Error()) rw.WriteHeader(http.StatusUnauthorized) - templates.Render(rw, r, "login.html", &templates.Page{ + templates.Render(rw, r, "login", &templates.Page{ Title: "Login failed", Login: &templates.LoginPage{ 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") rw.WriteHeader(http.StatusUnauthorized) - templates.Render(rw, r, "login.html", &templates.Page{ + templates.Render(rw, r, "login", &templates.Page{ Title: "Authentication failed", Login: &templates.LoginPage{ 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", Login: &templates.LoginPage{ Info: "Logout successful", diff --git a/server.go b/server.go index 58e3d02..5cd4790 100644 --- a/server.go +++ b/server.go @@ -3,6 +3,7 @@ package main import ( "encoding/json" "flag" + "fmt" "log" "net/http" "os" @@ -141,11 +142,12 @@ func main() { // 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") 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", Login: &templates.LoginPage{}, }) @@ -153,7 +155,7 @@ func main() { r := mux.NewRouter() 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", }) }) @@ -170,9 +172,17 @@ func main() { secured.Handle("/query", graphQLEndpoint) 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/{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("/", 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))) handler := handlers.CORS( handlers.AllowedHeaders([]string{"X-Requested-With", "Content-Type", "Authorization"}), @@ -189,3 +199,76 @@ func main() { } 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, + }, + }) + }) +} diff --git a/templates/base.html b/templates/base.html index 32d6f6b..4691f30 100644 --- a/templates/base.html +++ b/templates/base.html @@ -9,6 +9,9 @@ + + + {{block "stylesheets" .}}{{end}}
@@ -20,5 +23,6 @@
+ {{block "javascript" .}}{{end}} - \ No newline at end of file + diff --git a/templates/home.html b/templates/home.html new file mode 100644 index 0000000..7bbee5a --- /dev/null +++ b/templates/home.html @@ -0,0 +1,10 @@ +{{define "content"}} +
+
+ +
+
+{{end}} diff --git a/templates/login.html b/templates/login.html index 35776a4..7f6507b 100644 --- a/templates/login.html +++ b/templates/login.html @@ -1,4 +1,3 @@ -{{template "base.html" .}} {{define "content"}}
diff --git a/templates/monitoring/job.html b/templates/monitoring/job.html new file mode 100644 index 0000000..5f8b7f6 --- /dev/null +++ b/templates/monitoring/job.html @@ -0,0 +1,29 @@ +{{define "content"}} +
+{{end}} + +{{define "stylesheets"}} + +{{end}} +{{define "javascript"}} + + +{{end}} diff --git a/templates/monitoring/jobs.html b/templates/monitoring/jobs.html new file mode 100644 index 0000000..e6382c2 --- /dev/null +++ b/templates/monitoring/jobs.html @@ -0,0 +1,20 @@ +{{define "content"}} +
+{{end}} + +{{define "stylesheets"}} + +{{end}} +{{define "javascript"}} + + +{{end}} diff --git a/templates/monitoring/user.html b/templates/monitoring/user.html new file mode 100644 index 0000000..ee16cdc --- /dev/null +++ b/templates/monitoring/user.html @@ -0,0 +1,22 @@ +{{define "content"}} +
+{{end}} + +{{define "stylesheets"}} + +{{end}} +{{define "javascript"}} + + +{{end}} diff --git a/templates/monitoring/users.html b/templates/monitoring/users.html new file mode 100644 index 0000000..ff7c9d6 --- /dev/null +++ b/templates/monitoring/users.html @@ -0,0 +1,14 @@ +{{define "content"}} +
+{{end}} + +{{define "stylesheets"}} + +{{end}} +{{define "javascript"}} + + +{{end}} diff --git a/templates/templates.go b/templates/templates.go index c1c7c07..6b0a267 100644 --- a/templates/templates.go +++ b/templates/templates.go @@ -6,11 +6,13 @@ import ( "net/http" ) -var templates *template.Template +var templates map[string]*template.Template type Page struct { - Title string - Login *LoginPage + Title string + Login *LoginPage + Infos map[string]interface{} + Config map[string]interface{} } type LoginPage struct { @@ -19,11 +21,20 @@ type LoginPage struct { } 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) { - 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()) } }