mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2024-12-25 21:09:05 +01:00
Merge branch '264_user_api_access' into Refactor-job-footprint
This commit is contained in:
commit
a54acb8c42
@ -371,6 +371,10 @@ func main() {
|
||||
})
|
||||
|
||||
secured := r.PathPrefix("/").Subrouter()
|
||||
securedapi := r.PathPrefix("/api").Subrouter()
|
||||
userapi := r.PathPrefix("/userapi").Subrouter()
|
||||
configapi := r.PathPrefix("/config").Subrouter()
|
||||
frontendapi := r.PathPrefix("/frontend").Subrouter()
|
||||
|
||||
if !config.Keys.DisableAuthentication {
|
||||
r.Handle("/login", authentication.Login(
|
||||
@ -437,6 +441,70 @@ func main() {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
securedapi.Use(func(next http.Handler) http.Handler {
|
||||
return authentication.AuthApi(
|
||||
// On success;
|
||||
next,
|
||||
|
||||
// On failure: JSON Response
|
||||
func(rw http.ResponseWriter, r *http.Request, err error) {
|
||||
rw.Header().Add("Content-Type", "application/json")
|
||||
rw.WriteHeader(http.StatusUnauthorized)
|
||||
json.NewEncoder(rw).Encode(map[string]string{
|
||||
"status": http.StatusText(http.StatusUnauthorized),
|
||||
"error": err.Error(),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
userapi.Use(func(next http.Handler) http.Handler {
|
||||
return authentication.AuthUserApi(
|
||||
// On success;
|
||||
next,
|
||||
|
||||
// On failure: JSON Response
|
||||
func(rw http.ResponseWriter, r *http.Request, err error) {
|
||||
rw.Header().Add("Content-Type", "application/json")
|
||||
rw.WriteHeader(http.StatusUnauthorized)
|
||||
json.NewEncoder(rw).Encode(map[string]string{
|
||||
"status": http.StatusText(http.StatusUnauthorized),
|
||||
"error": err.Error(),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
configapi.Use(func(next http.Handler) http.Handler {
|
||||
return authentication.AuthConfigApi(
|
||||
// On success;
|
||||
next,
|
||||
|
||||
// On failure: JSON Response
|
||||
func(rw http.ResponseWriter, r *http.Request, err error) {
|
||||
rw.Header().Add("Content-Type", "application/json")
|
||||
rw.WriteHeader(http.StatusUnauthorized)
|
||||
json.NewEncoder(rw).Encode(map[string]string{
|
||||
"status": http.StatusText(http.StatusUnauthorized),
|
||||
"error": err.Error(),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
frontendapi.Use(func(next http.Handler) http.Handler {
|
||||
return authentication.AuthFrontendApi(
|
||||
// On success;
|
||||
next,
|
||||
|
||||
// On failure: JSON Response
|
||||
func(rw http.ResponseWriter, r *http.Request, err error) {
|
||||
rw.Header().Add("Content-Type", "application/json")
|
||||
rw.WriteHeader(http.StatusUnauthorized)
|
||||
json.NewEncoder(rw).Encode(map[string]string{
|
||||
"status": http.StatusText(http.StatusUnauthorized),
|
||||
"error": err.Error(),
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
if flagDev {
|
||||
@ -453,7 +521,10 @@ func main() {
|
||||
|
||||
// Mount all /monitoring/... and /api/... routes.
|
||||
routerConfig.SetupRoutes(secured, buildInfo)
|
||||
api.MountRoutes(secured)
|
||||
api.MountApiRoutes(securedapi)
|
||||
api.MountUserApiRoutes(userapi)
|
||||
api.MountConfigApiRoutes(configapi)
|
||||
api.MountFrontendApiRoutes(frontendapi)
|
||||
|
||||
if config.Keys.EmbedStaticFiles {
|
||||
if i, err := os.Stat("./var/img"); err == nil {
|
||||
|
@ -197,7 +197,9 @@ func TestRestApi(t *testing.T) {
|
||||
}
|
||||
|
||||
r := mux.NewRouter()
|
||||
restapi.MountRoutes(r)
|
||||
r.PathPrefix("/api").Subrouter()
|
||||
r.StrictSlash(true)
|
||||
restapi.MountApiRoutes(r)
|
||||
|
||||
const startJobBody string = `{
|
||||
"jobId": 123,
|
||||
@ -225,11 +227,22 @@ func TestRestApi(t *testing.T) {
|
||||
}`
|
||||
|
||||
var dbid int64
|
||||
const contextUserKey repository.ContextKey = "user"
|
||||
contextUserValue := &schema.User{
|
||||
Username: "testuser",
|
||||
Projects: make([]string, 0),
|
||||
Roles: []string{"user"},
|
||||
AuthType: 0,
|
||||
AuthSource: 2,
|
||||
}
|
||||
|
||||
if ok := t.Run("StartJob", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/jobs/start_job/", bytes.NewBuffer([]byte(startJobBody)))
|
||||
req := httptest.NewRequest(http.MethodPost, "/jobs/start_job/", bytes.NewBuffer([]byte(startJobBody)))
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(recorder, req)
|
||||
ctx := context.WithValue(req.Context(), contextUserKey, contextUserValue)
|
||||
|
||||
r.ServeHTTP(recorder, req.WithContext(ctx))
|
||||
response := recorder.Result()
|
||||
if response.StatusCode != http.StatusCreated {
|
||||
t.Fatal(response.Status, recorder.Body.String())
|
||||
@ -240,12 +253,12 @@ func TestRestApi(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
job, err := restapi.Resolver.Query().Job(context.Background(), strconv.Itoa(int(res.DBID)))
|
||||
job, err := restapi.Resolver.Query().Job(ctx, strconv.Itoa(int(res.DBID)))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
job.Tags, err = restapi.Resolver.Job().Tags(context.Background(), job)
|
||||
job.Tags, err = restapi.Resolver.Job().Tags(ctx, job)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -289,17 +302,19 @@ func TestRestApi(t *testing.T) {
|
||||
|
||||
var stoppedJob *schema.Job
|
||||
if ok := t.Run("StopJob", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/jobs/stop_job/", bytes.NewBuffer([]byte(stopJobBody)))
|
||||
req := httptest.NewRequest(http.MethodPost, "/jobs/stop_job/", bytes.NewBuffer([]byte(stopJobBody)))
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(recorder, req)
|
||||
ctx := context.WithValue(req.Context(), contextUserKey, contextUserValue)
|
||||
|
||||
r.ServeHTTP(recorder, req.WithContext(ctx))
|
||||
response := recorder.Result()
|
||||
if response.StatusCode != http.StatusOK {
|
||||
t.Fatal(response.Status, recorder.Body.String())
|
||||
}
|
||||
|
||||
restapi.JobRepository.WaitForArchiving()
|
||||
job, err := restapi.Resolver.Query().Job(context.Background(), strconv.Itoa(int(dbid)))
|
||||
job, err := restapi.Resolver.Query().Job(ctx, strconv.Itoa(int(dbid)))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -341,10 +356,12 @@ func TestRestApi(t *testing.T) {
|
||||
// Starting a job with the same jobId and cluster should only be allowed if the startTime is far appart!
|
||||
body := strings.Replace(startJobBody, `"startTime": 123456789`, `"startTime": 123456790`, -1)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/jobs/start_job/", bytes.NewBuffer([]byte(body)))
|
||||
req := httptest.NewRequest(http.MethodPost, "/jobs/start_job/", bytes.NewBuffer([]byte(body)))
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(recorder, req)
|
||||
ctx := context.WithValue(req.Context(), contextUserKey, contextUserValue)
|
||||
|
||||
r.ServeHTTP(recorder, req.WithContext(ctx))
|
||||
response := recorder.Result()
|
||||
if response.StatusCode != http.StatusUnprocessableEntity {
|
||||
t.Fatal(response.Status, recorder.Body.String())
|
||||
@ -371,10 +388,12 @@ func TestRestApi(t *testing.T) {
|
||||
}`
|
||||
|
||||
ok := t.Run("StartJobFailed", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/jobs/start_job/", bytes.NewBuffer([]byte(startJobBodyFailed)))
|
||||
req := httptest.NewRequest(http.MethodPost, "/jobs/start_job/", bytes.NewBuffer([]byte(startJobBodyFailed)))
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(recorder, req)
|
||||
ctx := context.WithValue(req.Context(), contextUserKey, contextUserValue)
|
||||
|
||||
r.ServeHTTP(recorder, req.WithContext(ctx))
|
||||
response := recorder.Result()
|
||||
if response.StatusCode != http.StatusCreated {
|
||||
t.Fatal(response.Status, recorder.Body.String())
|
||||
@ -393,10 +412,12 @@ func TestRestApi(t *testing.T) {
|
||||
}`
|
||||
|
||||
ok = t.Run("StopJobFailed", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/jobs/stop_job/", bytes.NewBuffer([]byte(stopJobBodyFailed)))
|
||||
req := httptest.NewRequest(http.MethodPost, "/jobs/stop_job/", bytes.NewBuffer([]byte(stopJobBodyFailed)))
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(recorder, req)
|
||||
ctx := context.WithValue(req.Context(), contextUserKey, contextUserValue)
|
||||
|
||||
r.ServeHTTP(recorder, req.WithContext(ctx))
|
||||
response := recorder.Result()
|
||||
if response.StatusCode != http.StatusOK {
|
||||
t.Fatal(response.Status, recorder.Body.String())
|
||||
|
@ -59,8 +59,7 @@ type RestApi struct {
|
||||
RepositoryMutex sync.Mutex
|
||||
}
|
||||
|
||||
func (api *RestApi) MountRoutes(r *mux.Router) {
|
||||
r = r.PathPrefix("/api").Subrouter()
|
||||
func (api *RestApi) MountApiRoutes(r *mux.Router) {
|
||||
r.StrictSlash(true)
|
||||
|
||||
r.HandleFunc("/jobs/start_job/", api.startJob).Methods(http.MethodPost, http.MethodPut)
|
||||
@ -84,15 +83,36 @@ func (api *RestApi) MountRoutes(r *mux.Router) {
|
||||
r.HandleFunc("/machine_state/{cluster}/{host}", api.getMachineState).Methods(http.MethodGet)
|
||||
r.HandleFunc("/machine_state/{cluster}/{host}", api.putMachineState).Methods(http.MethodPut, http.MethodPost)
|
||||
}
|
||||
}
|
||||
|
||||
func (api *RestApi) MountUserApiRoutes(r *mux.Router) {
|
||||
r.StrictSlash(true)
|
||||
|
||||
r.HandleFunc("/jobs/", api.getJobs).Methods(http.MethodGet)
|
||||
r.HandleFunc("/jobs/{id}", api.getJobById).Methods(http.MethodPost)
|
||||
r.HandleFunc("/jobs/{id}", api.getCompleteJobById).Methods(http.MethodGet)
|
||||
r.HandleFunc("/jobs/metrics/{id}", api.getJobMetrics).Methods(http.MethodGet)
|
||||
}
|
||||
|
||||
func (api *RestApi) MountConfigApiRoutes(r *mux.Router) {
|
||||
r.StrictSlash(true)
|
||||
|
||||
if api.Authentication != nil {
|
||||
r.HandleFunc("/jwt/", api.getJWT).Methods(http.MethodGet)
|
||||
r.HandleFunc("/roles/", api.getRoles).Methods(http.MethodGet)
|
||||
r.HandleFunc("/users/", api.createUser).Methods(http.MethodPost, http.MethodPut)
|
||||
r.HandleFunc("/users/", api.getUsers).Methods(http.MethodGet)
|
||||
r.HandleFunc("/users/", api.deleteUser).Methods(http.MethodDelete)
|
||||
r.HandleFunc("/user/{id}", api.updateUser).Methods(http.MethodPost)
|
||||
}
|
||||
}
|
||||
|
||||
func (api *RestApi) MountFrontendApiRoutes(r *mux.Router) {
|
||||
r.StrictSlash(true)
|
||||
|
||||
if api.Authentication != nil {
|
||||
r.HandleFunc("/jwt/", api.getJWT).Methods(http.MethodGet)
|
||||
r.HandleFunc("/configuration/", api.updateConfiguration).Methods(http.MethodPost)
|
||||
r.HandleFunc("/jobs/metrics/{id}", api.getJobMetrics).Methods(http.MethodGet) // Fetched in Job.svelte: Needs All-User-Access-Session-Auth
|
||||
}
|
||||
}
|
||||
|
||||
@ -311,13 +331,6 @@ func (api *RestApi) getClusters(rw http.ResponseWriter, r *http.Request) {
|
||||
// @security ApiKeyAuth
|
||||
// @router /jobs/ [get]
|
||||
func (api *RestApi) getJobs(rw http.ResponseWriter, r *http.Request) {
|
||||
if user := repository.GetUserFromContext(r.Context()); user != nil &&
|
||||
!user.HasRole(schema.RoleApi) {
|
||||
|
||||
handleError(fmt.Errorf("missing role: %v", schema.GetRoleString(schema.RoleApi)), http.StatusForbidden, rw)
|
||||
return
|
||||
}
|
||||
|
||||
withMetadata := false
|
||||
filter := &model.JobFilter{}
|
||||
page := &model.PageRequest{ItemsPerPage: 25, Page: 1}
|
||||
@ -434,7 +447,7 @@ func (api *RestApi) getJobs(rw http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// getJobById godoc
|
||||
// getCompleteJobById godoc
|
||||
// @summary Get job meta and optional all metric data
|
||||
// @tags Job query
|
||||
// @description Job to get is specified by database ID
|
||||
@ -452,14 +465,6 @@ func (api *RestApi) getJobs(rw http.ResponseWriter, r *http.Request) {
|
||||
// @security ApiKeyAuth
|
||||
// @router /jobs/{id} [get]
|
||||
func (api *RestApi) getCompleteJobById(rw http.ResponseWriter, r *http.Request) {
|
||||
if user := repository.GetUserFromContext(r.Context()); user != nil &&
|
||||
!user.HasRole(schema.RoleApi) {
|
||||
|
||||
handleError(fmt.Errorf("missing role: %v",
|
||||
schema.GetRoleString(schema.RoleApi)), http.StatusForbidden, rw)
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch job from db
|
||||
id, ok := mux.Vars(r)["id"]
|
||||
var job *schema.Job
|
||||
@ -471,7 +476,7 @@ func (api *RestApi) getCompleteJobById(rw http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
job, err = api.JobRepository.FindById(id)
|
||||
job, err = api.JobRepository.FindById(r.Context(), id) // Get Job from Repo by ID
|
||||
} else {
|
||||
handleError(errors.New("the parameter 'id' is required"), http.StatusBadRequest, rw)
|
||||
return
|
||||
@ -546,14 +551,6 @@ func (api *RestApi) getCompleteJobById(rw http.ResponseWriter, r *http.Request)
|
||||
// @security ApiKeyAuth
|
||||
// @router /jobs/{id} [post]
|
||||
func (api *RestApi) getJobById(rw http.ResponseWriter, r *http.Request) {
|
||||
if user := repository.GetUserFromContext(r.Context()); user != nil &&
|
||||
!user.HasRole(schema.RoleApi) {
|
||||
|
||||
handleError(fmt.Errorf("missing role: %v",
|
||||
schema.GetRoleString(schema.RoleApi)), http.StatusForbidden, rw)
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch job from db
|
||||
id, ok := mux.Vars(r)["id"]
|
||||
var job *schema.Job
|
||||
@ -565,7 +562,7 @@ func (api *RestApi) getJobById(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
job, err = api.JobRepository.FindById(id)
|
||||
job, err = api.JobRepository.FindById(r.Context(), id)
|
||||
} else {
|
||||
handleError(errors.New("the parameter 'id' is required"), http.StatusBadRequest, rw)
|
||||
return
|
||||
@ -651,19 +648,13 @@ func (api *RestApi) getJobById(rw http.ResponseWriter, r *http.Request) {
|
||||
// @security ApiKeyAuth
|
||||
// @router /jobs/edit_meta/{id} [post]
|
||||
func (api *RestApi) editMeta(rw http.ResponseWriter, r *http.Request) {
|
||||
if user := repository.GetUserFromContext(r.Context()); user != nil &&
|
||||
!user.HasRole(schema.RoleApi) {
|
||||
handleError(fmt.Errorf("missing role: %v", schema.GetRoleString(schema.RoleApi)), http.StatusForbidden, rw)
|
||||
return
|
||||
}
|
||||
|
||||
iid, err := strconv.ParseInt(mux.Vars(r)["id"], 10, 64)
|
||||
id, err := strconv.ParseInt(mux.Vars(r)["id"], 10, 64)
|
||||
if err != nil {
|
||||
http.Error(rw, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
job, err := api.JobRepository.FindById(iid)
|
||||
job, err := api.JobRepository.FindById(r.Context(), id)
|
||||
if err != nil {
|
||||
http.Error(rw, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
@ -702,20 +693,13 @@ func (api *RestApi) editMeta(rw http.ResponseWriter, r *http.Request) {
|
||||
// @security ApiKeyAuth
|
||||
// @router /jobs/tag_job/{id} [post]
|
||||
func (api *RestApi) tagJob(rw http.ResponseWriter, r *http.Request) {
|
||||
if user := repository.GetUserFromContext(r.Context()); user != nil &&
|
||||
!user.HasRole(schema.RoleApi) {
|
||||
|
||||
handleError(fmt.Errorf("missing role: %v", schema.GetRoleString(schema.RoleApi)), http.StatusForbidden, rw)
|
||||
return
|
||||
}
|
||||
|
||||
iid, err := strconv.ParseInt(mux.Vars(r)["id"], 10, 64)
|
||||
id, err := strconv.ParseInt(mux.Vars(r)["id"], 10, 64)
|
||||
if err != nil {
|
||||
http.Error(rw, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
job, err := api.JobRepository.FindById(iid)
|
||||
job, err := api.JobRepository.FindById(r.Context(), id)
|
||||
if err != nil {
|
||||
http.Error(rw, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
@ -769,13 +753,6 @@ func (api *RestApi) tagJob(rw http.ResponseWriter, r *http.Request) {
|
||||
// @security ApiKeyAuth
|
||||
// @router /jobs/start_job/ [post]
|
||||
func (api *RestApi) startJob(rw http.ResponseWriter, r *http.Request) {
|
||||
if user := repository.GetUserFromContext(r.Context()); user != nil &&
|
||||
!user.HasRole(schema.RoleApi) {
|
||||
|
||||
handleError(fmt.Errorf("missing role: %v", schema.GetRoleString(schema.RoleApi)), http.StatusForbidden, rw)
|
||||
return
|
||||
}
|
||||
|
||||
req := schema.JobMeta{BaseJob: schema.JobDefaults}
|
||||
if err := decode(r.Body, &req); err != nil {
|
||||
handleError(fmt.Errorf("parsing request body failed: %w", err), http.StatusBadRequest, rw)
|
||||
@ -852,13 +829,6 @@ func (api *RestApi) startJob(rw http.ResponseWriter, r *http.Request) {
|
||||
// @security ApiKeyAuth
|
||||
// @router /jobs/stop_job/{id} [post]
|
||||
func (api *RestApi) stopJobById(rw http.ResponseWriter, r *http.Request) {
|
||||
if user := repository.GetUserFromContext(r.Context()); user != nil &&
|
||||
!user.HasRole(schema.RoleApi) {
|
||||
|
||||
handleError(fmt.Errorf("missing role: %v", schema.GetRoleString(schema.RoleApi)), http.StatusForbidden, rw)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse request body: Only StopTime and State
|
||||
req := StopJobApiRequest{}
|
||||
if err := decode(r.Body, &req); err != nil {
|
||||
@ -877,7 +847,7 @@ func (api *RestApi) stopJobById(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
job, err = api.JobRepository.FindById(id)
|
||||
job, err = api.JobRepository.FindById(r.Context(), id)
|
||||
} else {
|
||||
handleError(errors.New("the parameter 'id' is required"), http.StatusBadRequest, rw)
|
||||
return
|
||||
@ -907,13 +877,6 @@ func (api *RestApi) stopJobById(rw http.ResponseWriter, r *http.Request) {
|
||||
// @security ApiKeyAuth
|
||||
// @router /jobs/stop_job/ [post]
|
||||
func (api *RestApi) stopJobByRequest(rw http.ResponseWriter, r *http.Request) {
|
||||
if user := repository.GetUserFromContext(r.Context()); user != nil &&
|
||||
!user.HasRole(schema.RoleApi) {
|
||||
|
||||
handleError(fmt.Errorf("missing role: %v", schema.GetRoleString(schema.RoleApi)), http.StatusForbidden, rw)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse request body
|
||||
req := StopJobApiRequest{}
|
||||
if err := decode(r.Body, &req); err != nil {
|
||||
@ -955,11 +918,6 @@ func (api *RestApi) stopJobByRequest(rw http.ResponseWriter, r *http.Request) {
|
||||
// @security ApiKeyAuth
|
||||
// @router /jobs/delete_job/{id} [delete]
|
||||
func (api *RestApi) deleteJobById(rw http.ResponseWriter, r *http.Request) {
|
||||
if user := repository.GetUserFromContext(r.Context()); user != nil && !user.HasRole(schema.RoleApi) {
|
||||
handleError(fmt.Errorf("missing role: %v", schema.GetRoleString(schema.RoleApi)), http.StatusForbidden, rw)
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch job (that will be stopped) from db
|
||||
id, ok := mux.Vars(r)["id"]
|
||||
var err error
|
||||
@ -1003,12 +961,6 @@ func (api *RestApi) deleteJobById(rw http.ResponseWriter, r *http.Request) {
|
||||
// @security ApiKeyAuth
|
||||
// @router /jobs/delete_job/ [delete]
|
||||
func (api *RestApi) deleteJobByRequest(rw http.ResponseWriter, r *http.Request) {
|
||||
if user := repository.GetUserFromContext(r.Context()); user != nil &&
|
||||
!user.HasRole(schema.RoleApi) {
|
||||
handleError(fmt.Errorf("missing role: %v", schema.GetRoleString(schema.RoleApi)), http.StatusForbidden, rw)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse request body
|
||||
req := DeleteJobApiRequest{}
|
||||
if err := decode(r.Body, &req); err != nil {
|
||||
@ -1060,11 +1012,6 @@ func (api *RestApi) deleteJobByRequest(rw http.ResponseWriter, r *http.Request)
|
||||
// @security ApiKeyAuth
|
||||
// @router /jobs/delete_job_before/{ts} [delete]
|
||||
func (api *RestApi) deleteJobBefore(rw http.ResponseWriter, r *http.Request) {
|
||||
if user := repository.GetUserFromContext(r.Context()); user != nil && !user.HasRole(schema.RoleApi) {
|
||||
handleError(fmt.Errorf("missing role: %v", schema.GetRoleString(schema.RoleApi)), http.StatusForbidden, rw)
|
||||
return
|
||||
}
|
||||
|
||||
var cnt int
|
||||
// Fetch job (that will be stopped) from db
|
||||
id, ok := mux.Vars(r)["ts"]
|
||||
|
@ -219,31 +219,141 @@ func (auth *Authentication) Auth(
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
user, err := auth.JwtAuth.AuthViaJWT(rw, r)
|
||||
if err != nil {
|
||||
log.Infof("authentication failed: %s", err.Error())
|
||||
log.Infof("auth -> authentication failed: %s", err.Error())
|
||||
http.Error(rw, err.Error(), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
user, err = auth.AuthViaSession(rw, r)
|
||||
if err != nil {
|
||||
log.Infof("authentication failed: %s", err.Error())
|
||||
log.Infof("auth -> authentication failed: %s", err.Error())
|
||||
http.Error(rw, err.Error(), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if user != nil {
|
||||
ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
|
||||
onsuccess.ServeHTTP(rw, r.WithContext(ctx))
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug("authentication failed")
|
||||
log.Info("auth -> authentication failed")
|
||||
onfailure(rw, r, errors.New("unauthorized (please login first)"))
|
||||
})
|
||||
}
|
||||
|
||||
func (auth *Authentication) AuthApi(
|
||||
onsuccess http.Handler,
|
||||
onfailure func(rw http.ResponseWriter, r *http.Request, authErr error),
|
||||
) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
user, err := auth.JwtAuth.AuthViaJWT(rw, r)
|
||||
if err != nil {
|
||||
log.Infof("auth api -> authentication failed: %s", err.Error())
|
||||
onfailure(rw, r, err)
|
||||
return
|
||||
}
|
||||
if user != nil {
|
||||
switch {
|
||||
case len(user.Roles) == 1:
|
||||
if user.HasRole(schema.RoleApi) {
|
||||
ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
|
||||
onsuccess.ServeHTTP(rw, r.WithContext(ctx))
|
||||
return
|
||||
}
|
||||
case len(user.Roles) >= 2:
|
||||
if user.HasAllRoles([]schema.Role{schema.RoleAdmin, schema.RoleApi}) {
|
||||
ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
|
||||
onsuccess.ServeHTTP(rw, r.WithContext(ctx))
|
||||
return
|
||||
}
|
||||
default:
|
||||
log.Info("auth api -> authentication failed: missing role")
|
||||
onfailure(rw, r, errors.New("unauthorized"))
|
||||
}
|
||||
}
|
||||
log.Info("auth api -> authentication failed: no auth")
|
||||
onfailure(rw, r, errors.New("unauthorized"))
|
||||
})
|
||||
}
|
||||
|
||||
func (auth *Authentication) AuthUserApi(
|
||||
onsuccess http.Handler,
|
||||
onfailure func(rw http.ResponseWriter, r *http.Request, authErr error),
|
||||
) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
user, err := auth.JwtAuth.AuthViaJWT(rw, r)
|
||||
if err != nil {
|
||||
log.Infof("auth user api -> authentication failed: %s", err.Error())
|
||||
onfailure(rw, r, err)
|
||||
return
|
||||
}
|
||||
if user != nil {
|
||||
switch {
|
||||
case len(user.Roles) == 1:
|
||||
if user.HasRole(schema.RoleApi) {
|
||||
ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
|
||||
onsuccess.ServeHTTP(rw, r.WithContext(ctx))
|
||||
return
|
||||
}
|
||||
case len(user.Roles) >= 2:
|
||||
if user.HasRole(schema.RoleApi) && user.HasAnyRole([]schema.Role{schema.RoleUser, schema.RoleManager, schema.RoleAdmin}) {
|
||||
ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
|
||||
onsuccess.ServeHTTP(rw, r.WithContext(ctx))
|
||||
return
|
||||
}
|
||||
default:
|
||||
log.Info("auth user api -> authentication failed: missing role")
|
||||
onfailure(rw, r, errors.New("unauthorized"))
|
||||
}
|
||||
}
|
||||
log.Info("auth user api -> authentication failed: no auth")
|
||||
onfailure(rw, r, errors.New("unauthorized"))
|
||||
})
|
||||
}
|
||||
|
||||
func (auth *Authentication) AuthConfigApi(
|
||||
onsuccess http.Handler,
|
||||
onfailure func(rw http.ResponseWriter, r *http.Request, authErr error),
|
||||
) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
user, err := auth.AuthViaSession(rw, r)
|
||||
if err != nil {
|
||||
log.Infof("auth config api -> authentication failed: %s", err.Error())
|
||||
onfailure(rw, r, err)
|
||||
return
|
||||
}
|
||||
if user != nil && user.HasRole(schema.RoleAdmin) {
|
||||
ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
|
||||
onsuccess.ServeHTTP(rw, r.WithContext(ctx))
|
||||
return
|
||||
}
|
||||
log.Info("auth config api -> authentication failed: no auth")
|
||||
onfailure(rw, r, errors.New("unauthorized"))
|
||||
})
|
||||
}
|
||||
|
||||
func (auth *Authentication) AuthFrontendApi(
|
||||
onsuccess http.Handler,
|
||||
onfailure func(rw http.ResponseWriter, r *http.Request, authErr error),
|
||||
) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
user, err := auth.AuthViaSession(rw, r)
|
||||
if err != nil {
|
||||
log.Infof("auth frontend api -> authentication failed: %s", err.Error())
|
||||
onfailure(rw, r, err)
|
||||
return
|
||||
}
|
||||
if user != nil {
|
||||
ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
|
||||
onsuccess.ServeHTTP(rw, r.WithContext(ctx))
|
||||
return
|
||||
}
|
||||
log.Info("auth frontend api -> authentication failed: no auth")
|
||||
onfailure(rw, r, errors.New("unauthorized"))
|
||||
})
|
||||
}
|
||||
|
||||
func (auth *Authentication) Logout(onsuccess http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
session, err := auth.sessionStore.Get(r, "session")
|
||||
|
@ -182,7 +182,7 @@ func (r *queryResolver) Job(ctx context.Context, id string) (*schema.Job, error)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
job, err := r.Repo.FindById(numericId)
|
||||
job, err := r.Repo.FindById(ctx, numericId)
|
||||
if err != nil {
|
||||
log.Warn("Error while finding job by id")
|
||||
return nil, err
|
||||
|
@ -86,12 +86,63 @@ func (r *JobRepository) FindAll(
|
||||
// The job is queried using the database id.
|
||||
// It returns a pointer to a schema.Job data structure and an error variable.
|
||||
// To check if no job was found test err == sql.ErrNoRows
|
||||
func (r *JobRepository) FindById(jobId int64) (*schema.Job, error) {
|
||||
func (r *JobRepository) FindById(ctx context.Context, jobId int64) (*schema.Job, error) {
|
||||
q := sq.Select(jobColumns...).
|
||||
From("job").Where("job.id = ?", jobId)
|
||||
|
||||
q, qerr := SecurityCheck(ctx, q)
|
||||
if qerr != nil {
|
||||
return nil, qerr
|
||||
}
|
||||
|
||||
return scanJob(q.RunWith(r.stmtCache).QueryRow())
|
||||
}
|
||||
|
||||
// FindByIdDirect executes a SQL query to find a specific batch job.
|
||||
// The job is queried using the database id.
|
||||
// It returns a pointer to a schema.Job data structure and an error variable.
|
||||
// To check if no job was found test err == sql.ErrNoRows
|
||||
func (r *JobRepository) FindByIdDirect(jobId int64) (*schema.Job, error) {
|
||||
q := sq.Select(jobColumns...).
|
||||
From("job").Where("job.id = ?", jobId)
|
||||
return scanJob(q.RunWith(r.stmtCache).QueryRow())
|
||||
}
|
||||
|
||||
// FindByJobId executes a SQL query to find a specific batch job.
|
||||
// The job is queried using the slurm id and the clustername.
|
||||
// It returns a pointer to a schema.Job data structure and an error variable.
|
||||
// To check if no job was found test err == sql.ErrNoRows
|
||||
func (r *JobRepository) FindByJobId(ctx context.Context, jobId int64, startTime int64, cluster string) (*schema.Job, error) {
|
||||
q := sq.Select(jobColumns...).
|
||||
From("job").
|
||||
Where("job.job_id = ?", jobId).
|
||||
Where("job.cluster = ?", cluster).
|
||||
Where("job.start_time = ?", startTime)
|
||||
|
||||
q, qerr := SecurityCheck(ctx, q)
|
||||
if qerr != nil {
|
||||
return nil, qerr
|
||||
}
|
||||
|
||||
return scanJob(q.RunWith(r.stmtCache).QueryRow())
|
||||
}
|
||||
|
||||
// IsJobOwner executes a SQL query to find a specific batch job.
|
||||
// The job is queried using the slurm id,a username and the cluster.
|
||||
// It returns a bool.
|
||||
// If job was found, user is owner: test err != sql.ErrNoRows
|
||||
func (r *JobRepository) IsJobOwner(jobId int64, startTime int64, user string, cluster string) bool {
|
||||
q := sq.Select("id").
|
||||
From("job").
|
||||
Where("job.job_id = ?", jobId).
|
||||
Where("job.user = ?", user).
|
||||
Where("job.cluster = ?", cluster).
|
||||
Where("job.start_time = ?", startTime)
|
||||
|
||||
_, err := scanJob(q.RunWith(r.stmtCache).QueryRow())
|
||||
return err != sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (r *JobRepository) FindConcurrentJobs(
|
||||
ctx context.Context,
|
||||
job *schema.Job,
|
||||
|
@ -97,23 +97,25 @@ func SecurityCheck(ctx context.Context, query sq.SelectBuilder) (sq.SelectBuilde
|
||||
if user == nil {
|
||||
var qnil sq.SelectBuilder
|
||||
return qnil, fmt.Errorf("user context is nil")
|
||||
} else if user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport, schema.RoleApi}) { // Admin & Co. : All jobs
|
||||
}
|
||||
|
||||
switch {
|
||||
case len(user.Roles) == 1 && user.HasRole(schema.RoleApi): // API-User : All jobs
|
||||
return query, nil
|
||||
} else if user.HasRole(schema.RoleManager) { // Manager : Add filter for managed projects' jobs only + personal jobs
|
||||
case user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport}): // Admin & Support : All jobs
|
||||
return query, nil
|
||||
case user.HasRole(schema.RoleManager): // Manager : Add filter for managed projects' jobs only + personal jobs
|
||||
if len(user.Projects) != 0 {
|
||||
return query.Where(sq.Or{sq.Eq{"job.project": user.Projects}, sq.Eq{"job.user": user.Username}}), nil
|
||||
} else {
|
||||
log.Debugf("Manager-User '%s' has no defined projects to lookup! Query only personal jobs ...", user.Username)
|
||||
return query.Where("job.user = ?", user.Username), nil
|
||||
}
|
||||
} else if user.HasRole(schema.RoleUser) { // User : Only personal jobs
|
||||
case user.HasRole(schema.RoleUser): // User : Only personal jobs
|
||||
return query.Where("job.user = ?", user.Username), nil
|
||||
} else {
|
||||
// Shortterm compatibility: Return User-Query if no roles:
|
||||
return query.Where("job.user = ?", user.Username), nil
|
||||
// // On the longterm: Return Error instead of fallback:
|
||||
// var qnil sq.SelectBuilder
|
||||
// return qnil, fmt.Errorf("user '%s' with unknown roles [%#v]", user.Username, user.Roles)
|
||||
default: // No known Role, return error
|
||||
var qnil sq.SelectBuilder
|
||||
return qnil, fmt.Errorf("user has no or unknown roles")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -30,7 +30,7 @@ func TestFind(t *testing.T) {
|
||||
func TestFindById(t *testing.T) {
|
||||
r := setup(t)
|
||||
|
||||
job, err := r.FindById(5)
|
||||
job, err := r.FindById(getContext(t), 5)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -55,7 +55,7 @@ func BenchmarkDB_FindJobById(b *testing.B) {
|
||||
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
_, err := db.FindById(jobId)
|
||||
_, err := db.FindById(getContext(b), jobId)
|
||||
noErr(b, err)
|
||||
}
|
||||
})
|
||||
|
@ -23,7 +23,7 @@ func (r *JobRepository) AddTag(job int64, tag int64) ([]*schema.Tag, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
j, err := r.FindById(job)
|
||||
j, err := r.FindByIdDirect(job)
|
||||
if err != nil {
|
||||
log.Warn("Error while finding job by id")
|
||||
return nil, err
|
||||
@ -48,7 +48,7 @@ func (r *JobRepository) RemoveTag(job, tag int64) ([]*schema.Tag, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
j, err := r.FindById(job)
|
||||
j, err := r.FindByIdDirect(job)
|
||||
if err != nil {
|
||||
log.Warn("Error while finding job by id")
|
||||
return nil, err
|
||||
|
@ -1,16 +1,15 @@
|
||||
<script>
|
||||
import { getContext } from "svelte";
|
||||
import { init } from "./utils.js";
|
||||
// import { init } from "./utils.js";
|
||||
import { Card, CardHeader, CardTitle } from "@sveltestrap/sveltestrap";
|
||||
|
||||
import PlotSettings from "./config/PlotSettings.svelte";
|
||||
import UserSettings from "./config/UserSettings.svelte";
|
||||
import AdminSettings from "./config/AdminSettings.svelte";
|
||||
|
||||
const { query: initq } = init();
|
||||
|
||||
const ccconfig = getContext("cc-config");
|
||||
// const { query: initq } = init();
|
||||
|
||||
export let isAdmin;
|
||||
export let isApi;
|
||||
export let username;
|
||||
</script>
|
||||
|
||||
{#if isAdmin == true}
|
||||
@ -24,7 +23,7 @@
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="mb-1">Plotting Options</CardTitle>
|
||||
<CardTitle class="mb-1">User Options</CardTitle>
|
||||
</CardHeader>
|
||||
<PlotSettings config={ccconfig} />
|
||||
<UserSettings {username} {isApi}/>
|
||||
</Card>
|
||||
|
@ -4,7 +4,9 @@ import Config from './Config.root.svelte'
|
||||
new Config({
|
||||
target: document.getElementById('svelte-app'),
|
||||
props: {
|
||||
isAdmin: isAdmin
|
||||
isAdmin: isAdmin,
|
||||
isApi: isApi,
|
||||
username: username
|
||||
},
|
||||
context: new Map([
|
||||
['cc-config', clusterCockpitConfig]
|
||||
|
@ -11,7 +11,7 @@
|
||||
let roles = [];
|
||||
|
||||
function getUserList() {
|
||||
fetch("/api/users/?via-ldap=false¬-just-user=true")
|
||||
fetch("/config/users/?via-ldap=false¬-just-user=true")
|
||||
.then((res) => res.json())
|
||||
.then((usersRaw) => {
|
||||
users = usersRaw;
|
||||
@ -19,7 +19,7 @@
|
||||
}
|
||||
|
||||
function getValidRoles() {
|
||||
fetch("/api/roles/")
|
||||
fetch("/config/roles/")
|
||||
.then((res) => res.json())
|
||||
.then((rolesRaw) => {
|
||||
roles = rolesRaw;
|
||||
|
@ -1,552 +0,0 @@
|
||||
<script>
|
||||
import {
|
||||
Button,
|
||||
Table,
|
||||
Row,
|
||||
Col,
|
||||
Card,
|
||||
CardTitle,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import { fade } from "svelte/transition";
|
||||
|
||||
export let config;
|
||||
|
||||
let message = { msg: "", target: "", color: "#d63384" };
|
||||
let displayMessage = false;
|
||||
|
||||
const colorschemes = {
|
||||
Default: [
|
||||
"#00bfff",
|
||||
"#0000ff",
|
||||
"#ff00ff",
|
||||
"#ff0000",
|
||||
"#ff8000",
|
||||
"#ffff00",
|
||||
"#80ff00",
|
||||
],
|
||||
Autumn: [
|
||||
"rgb(255,0,0)",
|
||||
"rgb(255,11,0)",
|
||||
"rgb(255,20,0)",
|
||||
"rgb(255,30,0)",
|
||||
"rgb(255,41,0)",
|
||||
"rgb(255,50,0)",
|
||||
"rgb(255,60,0)",
|
||||
"rgb(255,71,0)",
|
||||
"rgb(255,80,0)",
|
||||
"rgb(255,90,0)",
|
||||
"rgb(255,101,0)",
|
||||
"rgb(255,111,0)",
|
||||
"rgb(255,120,0)",
|
||||
"rgb(255,131,0)",
|
||||
"rgb(255,141,0)",
|
||||
"rgb(255,150,0)",
|
||||
"rgb(255,161,0)",
|
||||
"rgb(255,171,0)",
|
||||
"rgb(255,180,0)",
|
||||
"rgb(255,190,0)",
|
||||
"rgb(255,201,0)",
|
||||
"rgb(255,210,0)",
|
||||
"rgb(255,220,0)",
|
||||
"rgb(255,231,0)",
|
||||
"rgb(255,240,0)",
|
||||
"rgb(255,250,0)",
|
||||
],
|
||||
Beach: [
|
||||
"rgb(0,252,0)",
|
||||
"rgb(0,233,0)",
|
||||
"rgb(0,212,0)",
|
||||
"rgb(0,189,0)",
|
||||
"rgb(0,169,0)",
|
||||
"rgb(0,148,0)",
|
||||
"rgb(0,129,4)",
|
||||
"rgb(0,145,46)",
|
||||
"rgb(0,162,90)",
|
||||
"rgb(0,180,132)",
|
||||
"rgb(29,143,136)",
|
||||
"rgb(73,88,136)",
|
||||
"rgb(115,32,136)",
|
||||
"rgb(81,9,64)",
|
||||
"rgb(124,51,23)",
|
||||
"rgb(162,90,0)",
|
||||
"rgb(194,132,0)",
|
||||
"rgb(220,171,0)",
|
||||
"rgb(231,213,0)",
|
||||
"rgb(0,0,13)",
|
||||
"rgb(0,0,55)",
|
||||
"rgb(0,0,92)",
|
||||
"rgb(0,0,127)",
|
||||
"rgb(0,0,159)",
|
||||
"rgb(0,0,196)",
|
||||
"rgb(0,0,233)",
|
||||
],
|
||||
BlueRed: [
|
||||
"rgb(0,0,131)",
|
||||
"rgb(0,0,168)",
|
||||
"rgb(0,0,208)",
|
||||
"rgb(0,0,247)",
|
||||
"rgb(0,27,255)",
|
||||
"rgb(0,67,255)",
|
||||
"rgb(0,108,255)",
|
||||
"rgb(0,148,255)",
|
||||
"rgb(0,187,255)",
|
||||
"rgb(0,227,255)",
|
||||
"rgb(8,255,247)",
|
||||
"rgb(48,255,208)",
|
||||
"rgb(87,255,168)",
|
||||
"rgb(127,255,127)",
|
||||
"rgb(168,255,87)",
|
||||
"rgb(208,255,48)",
|
||||
"rgb(247,255,8)",
|
||||
"rgb(255,224,0)",
|
||||
"rgb(255,183,0)",
|
||||
"rgb(255,143,0)",
|
||||
"rgb(255,104,0)",
|
||||
"rgb(255,64,0)",
|
||||
"rgb(255,23,0)",
|
||||
"rgb(238,0,0)",
|
||||
"rgb(194,0,0)",
|
||||
"rgb(150,0,0)",
|
||||
],
|
||||
Rainbow: [
|
||||
"rgb(125,0,255)",
|
||||
"rgb(85,0,255)",
|
||||
"rgb(39,0,255)",
|
||||
"rgb(0,6,255)",
|
||||
"rgb(0,51,255)",
|
||||
"rgb(0,97,255)",
|
||||
"rgb(0,141,255)",
|
||||
"rgb(0,187,255)",
|
||||
"rgb(0,231,255)",
|
||||
"rgb(0,255,233)",
|
||||
"rgb(0,255,189)",
|
||||
"rgb(0,255,143)",
|
||||
"rgb(0,255,99)",
|
||||
"rgb(0,255,53)",
|
||||
"rgb(0,255,9)",
|
||||
"rgb(37,255,0)",
|
||||
"rgb(83,255,0)",
|
||||
"rgb(127,255,0)",
|
||||
"rgb(173,255,0)",
|
||||
"rgb(217,255,0)",
|
||||
"rgb(255,248,0)",
|
||||
"rgb(255,203,0)",
|
||||
"rgb(255,159,0)",
|
||||
"rgb(255,113,0)",
|
||||
"rgb(255,69,0)",
|
||||
"rgb(255,23,0)",
|
||||
],
|
||||
Binary: [
|
||||
"rgb(215,215,215)",
|
||||
"rgb(206,206,206)",
|
||||
"rgb(196,196,196)",
|
||||
"rgb(185,185,185)",
|
||||
"rgb(176,176,176)",
|
||||
"rgb(166,166,166)",
|
||||
"rgb(155,155,155)",
|
||||
"rgb(145,145,145)",
|
||||
"rgb(136,136,136)",
|
||||
"rgb(125,125,125)",
|
||||
"rgb(115,115,115)",
|
||||
"rgb(106,106,106)",
|
||||
"rgb(95,95,95)",
|
||||
"rgb(85,85,85)",
|
||||
"rgb(76,76,76)",
|
||||
"rgb(66,66,66)",
|
||||
"rgb(55,55,55)",
|
||||
"rgb(46,46,46)",
|
||||
"rgb(36,36,36)",
|
||||
"rgb(25,25,25)",
|
||||
"rgb(16,16,16)",
|
||||
"rgb(6,6,6)",
|
||||
],
|
||||
GistEarth: [
|
||||
"rgb(0,0,0)",
|
||||
"rgb(2,7,117)",
|
||||
"rgb(9,30,118)",
|
||||
"rgb(16,53,120)",
|
||||
"rgb(23,73,122)",
|
||||
"rgb(31,93,124)",
|
||||
"rgb(39,110,125)",
|
||||
"rgb(47,126,127)",
|
||||
"rgb(51,133,119)",
|
||||
"rgb(57,138,106)",
|
||||
"rgb(62,145,94)",
|
||||
"rgb(66,150,82)",
|
||||
"rgb(74,157,71)",
|
||||
"rgb(97,162,77)",
|
||||
"rgb(121,168,83)",
|
||||
"rgb(136,173,85)",
|
||||
"rgb(153,176,88)",
|
||||
"rgb(170,180,92)",
|
||||
"rgb(185,182,94)",
|
||||
"rgb(189,173,99)",
|
||||
"rgb(192,164,101)",
|
||||
"rgb(203,169,124)",
|
||||
"rgb(215,178,149)",
|
||||
"rgb(226,192,176)",
|
||||
"rgb(238,212,204)",
|
||||
"rgb(248,236,236)",
|
||||
],
|
||||
BlueWaves: [
|
||||
"rgb(83,0,215)",
|
||||
"rgb(43,6,108)",
|
||||
"rgb(9,16,16)",
|
||||
"rgb(8,32,25)",
|
||||
"rgb(0,50,8)",
|
||||
"rgb(27,64,66)",
|
||||
"rgb(69,67,178)",
|
||||
"rgb(115,62,210)",
|
||||
"rgb(155,50,104)",
|
||||
"rgb(178,43,41)",
|
||||
"rgb(180,51,34)",
|
||||
"rgb(161,78,87)",
|
||||
"rgb(124,117,187)",
|
||||
"rgb(78,155,203)",
|
||||
"rgb(34,178,85)",
|
||||
"rgb(4,176,2)",
|
||||
"rgb(9,152,27)",
|
||||
"rgb(4,118,2)",
|
||||
"rgb(34,92,85)",
|
||||
"rgb(78,92,203)",
|
||||
"rgb(124,127,187)",
|
||||
"rgb(161,187,87)",
|
||||
"rgb(180,248,34)",
|
||||
"rgb(178,220,41)",
|
||||
"rgb(155,217,104)",
|
||||
"rgb(115,254,210)",
|
||||
],
|
||||
BlueGreenRedYellow: [
|
||||
"rgb(0,0,0)",
|
||||
"rgb(0,0,20)",
|
||||
"rgb(0,0,41)",
|
||||
"rgb(0,0,62)",
|
||||
"rgb(0,25,83)",
|
||||
"rgb(0,57,101)",
|
||||
"rgb(0,87,101)",
|
||||
"rgb(0,118,101)",
|
||||
"rgb(0,150,101)",
|
||||
"rgb(0,150,69)",
|
||||
"rgb(0,148,37)",
|
||||
"rgb(0,141,6)",
|
||||
"rgb(60,120,0)",
|
||||
"rgb(131,87,0)",
|
||||
"rgb(180,25,0)",
|
||||
"rgb(203,13,0)",
|
||||
"rgb(208,36,0)",
|
||||
"rgb(213,60,0)",
|
||||
"rgb(219,83,0)",
|
||||
"rgb(224,106,0)",
|
||||
"rgb(229,129,0)",
|
||||
"rgb(233,152,0)",
|
||||
"rgb(238,176,0)",
|
||||
"rgb(243,199,0)",
|
||||
"rgb(248,222,0)",
|
||||
"rgb(254,245,0)",
|
||||
],
|
||||
};
|
||||
|
||||
async function handleSettingSubmit(selector, target) {
|
||||
let form = document.querySelector(selector);
|
||||
let formData = new FormData(form);
|
||||
try {
|
||||
const res = await fetch(form.action, { method: "POST", body: formData });
|
||||
if (res.ok) {
|
||||
let text = await res.text();
|
||||
popMessage(text, target, "#048109");
|
||||
} else {
|
||||
let text = await res.text();
|
||||
// console.log(res.statusText)
|
||||
throw new Error("Response Code " + res.status + "-> " + text);
|
||||
}
|
||||
} catch (err) {
|
||||
popMessage(err, target, "#d63384");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function popMessage(response, restarget, rescolor) {
|
||||
message = { msg: response, target: restarget, color: rescolor };
|
||||
displayMessage = true;
|
||||
setTimeout(function () {
|
||||
displayMessage = false;
|
||||
}, 3500);
|
||||
}
|
||||
</script>
|
||||
|
||||
<Row cols={4} class="p-2 g-2">
|
||||
<!-- LINE WIDTH -->
|
||||
<Col
|
||||
><Card class="h-100">
|
||||
<!-- Important: Function with arguments needs to be event-triggered like on:submit={() => functionName('Some','Args')} OR no arguments and like this: on:submit={functionName} -->
|
||||
<form
|
||||
id="line-width-form"
|
||||
method="post"
|
||||
action="/api/configuration/"
|
||||
class="card-body"
|
||||
on:submit|preventDefault={() =>
|
||||
handleSettingSubmit("#line-width-form", "lw")}
|
||||
>
|
||||
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
|
||||
<CardTitle
|
||||
style="margin-bottom: 1em; display: flex; align-items: center;"
|
||||
>
|
||||
<div>Line Width</div>
|
||||
<!-- Expand If-Clause for clarity once -->
|
||||
{#if displayMessage && message.target == "lw"}
|
||||
<div style="margin-left: auto; font-size: 0.9em;">
|
||||
<code style="color: {message.color};" out:fade>
|
||||
Update: {message.msg}
|
||||
</code>
|
||||
</div>
|
||||
{/if}
|
||||
</CardTitle>
|
||||
<input type="hidden" name="key" value="plot_general_lineWidth" />
|
||||
<div class="mb-3">
|
||||
<label for="value" class="form-label">Line Width</label>
|
||||
<input
|
||||
type="number"
|
||||
class="form-control"
|
||||
id="lwvalue"
|
||||
name="value"
|
||||
aria-describedby="lineWidthHelp"
|
||||
value={config.plot_general_lineWidth}
|
||||
min="1"
|
||||
/>
|
||||
<div id="lineWidthHelp" class="form-text">
|
||||
Width of the lines in the timeseries plots.
|
||||
</div>
|
||||
</div>
|
||||
<Button color="primary" type="submit">Submit</Button>
|
||||
</form>
|
||||
</Card></Col
|
||||
>
|
||||
|
||||
<!-- PLOTS PER ROW -->
|
||||
<Col
|
||||
><Card class="h-100">
|
||||
<form
|
||||
id="plots-per-row-form"
|
||||
method="post"
|
||||
action="/api/configuration/"
|
||||
class="card-body"
|
||||
on:submit|preventDefault={() =>
|
||||
handleSettingSubmit("#plots-per-row-form", "ppr")}
|
||||
>
|
||||
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
|
||||
<CardTitle
|
||||
style="margin-bottom: 1em; display: flex; align-items: center;"
|
||||
>
|
||||
<div>Plots per Row</div>
|
||||
{#if displayMessage && message.target == "ppr"}<div
|
||||
style="margin-left: auto; font-size: 0.9em;"
|
||||
>
|
||||
<code style="color: {message.color};" out:fade
|
||||
>Update: {message.msg}</code
|
||||
>
|
||||
</div>{/if}
|
||||
</CardTitle>
|
||||
<input type="hidden" name="key" value="plot_view_plotsPerRow" />
|
||||
<div class="mb-3">
|
||||
<label for="value" class="form-label">Plots per Row</label>
|
||||
<input
|
||||
type="number"
|
||||
class="form-control"
|
||||
id="pprvalue"
|
||||
name="value"
|
||||
aria-describedby="plotsperrowHelp"
|
||||
value={config.plot_view_plotsPerRow}
|
||||
min="1"
|
||||
/>
|
||||
<div id="plotsperrowHelp" class="form-text">
|
||||
How many plots to show next to each other on pages such as
|
||||
/monitoring/job/, /monitoring/system/...
|
||||
</div>
|
||||
</div>
|
||||
<Button color="primary" type="submit">Submit</Button>
|
||||
</form>
|
||||
</Card></Col
|
||||
>
|
||||
|
||||
<!-- BACKGROUND -->
|
||||
<Col
|
||||
><Card class="h-100">
|
||||
<form
|
||||
id="backgrounds-form"
|
||||
method="post"
|
||||
action="/api/configuration/"
|
||||
class="card-body"
|
||||
on:submit|preventDefault={() =>
|
||||
handleSettingSubmit("#backgrounds-form", "bg")}
|
||||
>
|
||||
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
|
||||
<CardTitle
|
||||
style="margin-bottom: 1em; display: flex; align-items: center;"
|
||||
>
|
||||
<div>Colored Backgrounds</div>
|
||||
{#if displayMessage && message.target == "bg"}<div
|
||||
style="margin-left: auto; font-size: 0.9em;"
|
||||
>
|
||||
<code style="color: {message.color};" out:fade
|
||||
>Update: {message.msg}</code
|
||||
>
|
||||
</div>{/if}
|
||||
</CardTitle>
|
||||
<input type="hidden" name="key" value="plot_general_colorBackground" />
|
||||
<div class="mb-3">
|
||||
<div>
|
||||
{#if config.plot_general_colorBackground}
|
||||
<input type="radio" id="true" name="value" value="true" checked />
|
||||
{:else}
|
||||
<input type="radio" id="true" name="value" value="true" />
|
||||
{/if}
|
||||
<label for="true">Yes</label>
|
||||
</div>
|
||||
<div>
|
||||
{#if config.plot_general_colorBackground}
|
||||
<input type="radio" id="false" name="value" value="false" />
|
||||
{:else}
|
||||
<input
|
||||
type="radio"
|
||||
id="false"
|
||||
name="value"
|
||||
value="false"
|
||||
checked
|
||||
/>
|
||||
{/if}
|
||||
<label for="false">No</label>
|
||||
</div>
|
||||
</div>
|
||||
<Button color="primary" type="submit">Submit</Button>
|
||||
</form>
|
||||
</Card></Col
|
||||
>
|
||||
|
||||
<!-- PAGING -->
|
||||
<Col
|
||||
><Card class="h-100">
|
||||
<form
|
||||
id="paging-form"
|
||||
method="post"
|
||||
action="/api/configuration/"
|
||||
class="card-body"
|
||||
on:submit|preventDefault={() =>
|
||||
handleSettingSubmit("#paging-form", "pag")}
|
||||
>
|
||||
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
|
||||
<CardTitle
|
||||
style="margin-bottom: 1em; display: flex; align-items: center;"
|
||||
>
|
||||
<div>Paging Type</div>
|
||||
{#if displayMessage && message.target == "pag"}<div
|
||||
style="margin-left: auto; font-size: 0.9em;"
|
||||
>
|
||||
<code style="color: {message.color};" out:fade
|
||||
>Update: {message.msg}</code
|
||||
>
|
||||
</div>{/if}
|
||||
</CardTitle>
|
||||
<input type="hidden" name="key" value="job_list_usePaging" />
|
||||
<div class="mb-3">
|
||||
<div>
|
||||
{#if config.job_list_usePaging}
|
||||
<input type="radio" id="true" name="value" value="true" checked />
|
||||
{:else}
|
||||
<input type="radio" id="true" name="value" value="true" />
|
||||
{/if}
|
||||
<label for="true">Paging with selectable count of jobs.</label>
|
||||
</div>
|
||||
<div>
|
||||
{#if config.job_list_usePaging}
|
||||
<input type="radio" id="false" name="value" value="false" />
|
||||
{:else}
|
||||
<input
|
||||
type="radio"
|
||||
id="false"
|
||||
name="value"
|
||||
value="false"
|
||||
checked
|
||||
/>
|
||||
{/if}
|
||||
<label for="false">Continuous scroll iteratively adding 10 jobs.</label>
|
||||
</div>
|
||||
</div>
|
||||
<Button color="primary" type="submit">Submit</Button>
|
||||
</form>
|
||||
</Card></Col
|
||||
>
|
||||
</Row>
|
||||
|
||||
<Row cols={1} class="p-2 g-2">
|
||||
<!-- COLORSCHEME -->
|
||||
<Col
|
||||
><Card>
|
||||
<form
|
||||
id="colorscheme-form"
|
||||
method="post"
|
||||
action="/api/configuration/"
|
||||
class="card-body"
|
||||
>
|
||||
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
|
||||
<CardTitle
|
||||
style="margin-bottom: 1em; display: flex; align-items: center;"
|
||||
>
|
||||
<div>Color Scheme for Timeseries Plots</div>
|
||||
{#if displayMessage && message.target == "cs"}<div
|
||||
style="margin-left: auto; font-size: 0.9em;"
|
||||
>
|
||||
<code style="color: {message.color};" out:fade
|
||||
>Update: {message.msg}</code
|
||||
>
|
||||
</div>{/if}
|
||||
</CardTitle>
|
||||
<input type="hidden" name="key" value="plot_general_colorscheme" />
|
||||
<Table hover>
|
||||
<tbody>
|
||||
{#each Object.entries(colorschemes) as [name, rgbrow]}
|
||||
<tr>
|
||||
<th scope="col">{name}</th>
|
||||
<td>
|
||||
{#if rgbrow.join(",") == config.plot_general_colorscheme}
|
||||
<input
|
||||
type="radio"
|
||||
name="value"
|
||||
value={JSON.stringify(rgbrow)}
|
||||
checked
|
||||
on:click={() =>
|
||||
handleSettingSubmit("#colorscheme-form", "cs")}
|
||||
/>
|
||||
{:else}
|
||||
<input
|
||||
type="radio"
|
||||
name="value"
|
||||
value={JSON.stringify(rgbrow)}
|
||||
on:click={() =>
|
||||
handleSettingSubmit("#colorscheme-form", "cs")}
|
||||
/>
|
||||
{/if}
|
||||
</td>
|
||||
<td>
|
||||
{#each rgbrow as rgb}
|
||||
<span class="color-dot" style="background-color: {rgb};"
|
||||
></span>
|
||||
{/each}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</Table>
|
||||
</form>
|
||||
</Card></Col
|
||||
>
|
||||
</Row>
|
||||
|
||||
<style>
|
||||
.color-dot {
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
47
web/frontend/src/config/UserSettings.svelte
Normal file
47
web/frontend/src/config/UserSettings.svelte
Normal file
@ -0,0 +1,47 @@
|
||||
<script>
|
||||
import { getContext } from "svelte";
|
||||
import UserOptions from "./user/UserOptions.svelte";
|
||||
import PlotRenderOptions from "./user/PlotRenderOptions.svelte";
|
||||
import PlotColorScheme from "./user/PlotColorScheme.svelte";
|
||||
|
||||
export let username
|
||||
export let isApi
|
||||
|
||||
const ccconfig = getContext("cc-config");
|
||||
let message = { msg: "", target: "", color: "#d63384" };
|
||||
let displayMessage = false;
|
||||
|
||||
async function handleSettingSubmit(event) {
|
||||
const selector = event.detail.selector
|
||||
const target = event.detail.target
|
||||
let form = document.querySelector(selector);
|
||||
let formData = new FormData(form);
|
||||
try {
|
||||
const res = await fetch(form.action, { method: "POST", body: formData });
|
||||
if (res.ok) {
|
||||
let text = await res.text();
|
||||
popMessage(text, target, "#048109");
|
||||
} else {
|
||||
let text = await res.text();
|
||||
// console.log(res.statusText)
|
||||
throw new Error("Response Code " + res.status + "-> " + text);
|
||||
}
|
||||
} catch (err) {
|
||||
popMessage(err, target, "#d63384");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function popMessage(response, restarget, rescolor) {
|
||||
message = { msg: response, target: restarget, color: rescolor };
|
||||
displayMessage = true;
|
||||
setTimeout(function () {
|
||||
displayMessage = false;
|
||||
}, 3500);
|
||||
}
|
||||
</script>
|
||||
|
||||
<UserOptions config={ccconfig} {username} {isApi} bind:message bind:displayMessage on:update={(e) => handleSettingSubmit(e)}/>
|
||||
<PlotRenderOptions config={ccconfig} bind:message bind:displayMessage on:update={(e) => handleSettingSubmit(e)}/>
|
||||
<PlotColorScheme config={ccconfig} bind:message bind:displayMessage on:update={(e) => handleSettingSubmit(e)}/>
|
@ -48,7 +48,7 @@
|
||||
<form
|
||||
id="create-user-form"
|
||||
method="post"
|
||||
action="/api/users/"
|
||||
action="/config/users/"
|
||||
class="card-body"
|
||||
on:submit|preventDefault={handleUserSubmit}
|
||||
>
|
||||
|
@ -22,7 +22,7 @@
|
||||
formData.append("add-project", project);
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/user/${username}`, {
|
||||
const res = await fetch(`/config/user/${username}`, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
@ -54,7 +54,7 @@
|
||||
formData.append("remove-project", project);
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/user/${username}`, {
|
||||
const res = await fetch(`/config/user/${username}`, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
|
@ -24,7 +24,7 @@
|
||||
formData.append("add-role", role);
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/user/${username}`, {
|
||||
const res = await fetch(`/config/user/${username}`, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
@ -56,7 +56,7 @@
|
||||
formData.append("remove-role", role);
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/user/${username}`, {
|
||||
const res = await fetch(`/config/user/${username}`, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
|
@ -20,7 +20,7 @@
|
||||
if (confirm("Are you sure?")) {
|
||||
let formData = new FormData();
|
||||
formData.append("username", username);
|
||||
fetch("/api/users/", { method: "DELETE", body: formData }).then((res) => {
|
||||
fetch("/config/users/", { method: "DELETE", body: formData }).then((res) => {
|
||||
if (res.status == 200) {
|
||||
reloadUserList();
|
||||
} else {
|
||||
|
@ -1,18 +1,17 @@
|
||||
<script>
|
||||
import { Button } from "@sveltestrap/sveltestrap";
|
||||
import { fetchJwt } from "../../utils.js"
|
||||
|
||||
export let user;
|
||||
let jwt = "";
|
||||
|
||||
let jwt = "";
|
||||
function getUserJwt(username) {
|
||||
fetch(`/api/jwt/?username=${username}`)
|
||||
.then((res) => res.text())
|
||||
.then((text) => {
|
||||
jwt = text;
|
||||
navigator.clipboard
|
||||
.writeText(text)
|
||||
.catch((reason) => console.error(reason));
|
||||
});
|
||||
const p = fetchJwt(username);
|
||||
p.then((content) => {
|
||||
jwt = content
|
||||
}).catch((error) => {
|
||||
console.error(`Could not get JWT: ${error}`);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
|
329
web/frontend/src/config/user/PlotColorScheme.svelte
Normal file
329
web/frontend/src/config/user/PlotColorScheme.svelte
Normal file
@ -0,0 +1,329 @@
|
||||
<script>
|
||||
import {
|
||||
Table,
|
||||
Row,
|
||||
Col,
|
||||
Card,
|
||||
CardTitle,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import { fade } from "svelte/transition";
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
export let config;
|
||||
export let message;
|
||||
export let displayMessage;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
function updateSetting(selector, target) {
|
||||
dispatch('update', {
|
||||
selector: selector,
|
||||
target: target
|
||||
});
|
||||
}
|
||||
|
||||
const colorschemes = {
|
||||
Default: [
|
||||
"#00bfff",
|
||||
"#0000ff",
|
||||
"#ff00ff",
|
||||
"#ff0000",
|
||||
"#ff8000",
|
||||
"#ffff00",
|
||||
"#80ff00",
|
||||
],
|
||||
Autumn: [
|
||||
"rgb(255,0,0)",
|
||||
"rgb(255,11,0)",
|
||||
"rgb(255,20,0)",
|
||||
"rgb(255,30,0)",
|
||||
"rgb(255,41,0)",
|
||||
"rgb(255,50,0)",
|
||||
"rgb(255,60,0)",
|
||||
"rgb(255,71,0)",
|
||||
"rgb(255,80,0)",
|
||||
"rgb(255,90,0)",
|
||||
"rgb(255,101,0)",
|
||||
"rgb(255,111,0)",
|
||||
"rgb(255,120,0)",
|
||||
"rgb(255,131,0)",
|
||||
"rgb(255,141,0)",
|
||||
"rgb(255,150,0)",
|
||||
"rgb(255,161,0)",
|
||||
"rgb(255,171,0)",
|
||||
"rgb(255,180,0)",
|
||||
"rgb(255,190,0)",
|
||||
"rgb(255,201,0)",
|
||||
"rgb(255,210,0)",
|
||||
"rgb(255,220,0)",
|
||||
"rgb(255,231,0)",
|
||||
"rgb(255,240,0)",
|
||||
"rgb(255,250,0)",
|
||||
],
|
||||
Beach: [
|
||||
"rgb(0,252,0)",
|
||||
"rgb(0,233,0)",
|
||||
"rgb(0,212,0)",
|
||||
"rgb(0,189,0)",
|
||||
"rgb(0,169,0)",
|
||||
"rgb(0,148,0)",
|
||||
"rgb(0,129,4)",
|
||||
"rgb(0,145,46)",
|
||||
"rgb(0,162,90)",
|
||||
"rgb(0,180,132)",
|
||||
"rgb(29,143,136)",
|
||||
"rgb(73,88,136)",
|
||||
"rgb(115,32,136)",
|
||||
"rgb(81,9,64)",
|
||||
"rgb(124,51,23)",
|
||||
"rgb(162,90,0)",
|
||||
"rgb(194,132,0)",
|
||||
"rgb(220,171,0)",
|
||||
"rgb(231,213,0)",
|
||||
"rgb(0,0,13)",
|
||||
"rgb(0,0,55)",
|
||||
"rgb(0,0,92)",
|
||||
"rgb(0,0,127)",
|
||||
"rgb(0,0,159)",
|
||||
"rgb(0,0,196)",
|
||||
"rgb(0,0,233)",
|
||||
],
|
||||
BlueRed: [
|
||||
"rgb(0,0,131)",
|
||||
"rgb(0,0,168)",
|
||||
"rgb(0,0,208)",
|
||||
"rgb(0,0,247)",
|
||||
"rgb(0,27,255)",
|
||||
"rgb(0,67,255)",
|
||||
"rgb(0,108,255)",
|
||||
"rgb(0,148,255)",
|
||||
"rgb(0,187,255)",
|
||||
"rgb(0,227,255)",
|
||||
"rgb(8,255,247)",
|
||||
"rgb(48,255,208)",
|
||||
"rgb(87,255,168)",
|
||||
"rgb(127,255,127)",
|
||||
"rgb(168,255,87)",
|
||||
"rgb(208,255,48)",
|
||||
"rgb(247,255,8)",
|
||||
"rgb(255,224,0)",
|
||||
"rgb(255,183,0)",
|
||||
"rgb(255,143,0)",
|
||||
"rgb(255,104,0)",
|
||||
"rgb(255,64,0)",
|
||||
"rgb(255,23,0)",
|
||||
"rgb(238,0,0)",
|
||||
"rgb(194,0,0)",
|
||||
"rgb(150,0,0)",
|
||||
],
|
||||
Rainbow: [
|
||||
"rgb(125,0,255)",
|
||||
"rgb(85,0,255)",
|
||||
"rgb(39,0,255)",
|
||||
"rgb(0,6,255)",
|
||||
"rgb(0,51,255)",
|
||||
"rgb(0,97,255)",
|
||||
"rgb(0,141,255)",
|
||||
"rgb(0,187,255)",
|
||||
"rgb(0,231,255)",
|
||||
"rgb(0,255,233)",
|
||||
"rgb(0,255,189)",
|
||||
"rgb(0,255,143)",
|
||||
"rgb(0,255,99)",
|
||||
"rgb(0,255,53)",
|
||||
"rgb(0,255,9)",
|
||||
"rgb(37,255,0)",
|
||||
"rgb(83,255,0)",
|
||||
"rgb(127,255,0)",
|
||||
"rgb(173,255,0)",
|
||||
"rgb(217,255,0)",
|
||||
"rgb(255,248,0)",
|
||||
"rgb(255,203,0)",
|
||||
"rgb(255,159,0)",
|
||||
"rgb(255,113,0)",
|
||||
"rgb(255,69,0)",
|
||||
"rgb(255,23,0)",
|
||||
],
|
||||
Binary: [
|
||||
"rgb(215,215,215)",
|
||||
"rgb(206,206,206)",
|
||||
"rgb(196,196,196)",
|
||||
"rgb(185,185,185)",
|
||||
"rgb(176,176,176)",
|
||||
"rgb(166,166,166)",
|
||||
"rgb(155,155,155)",
|
||||
"rgb(145,145,145)",
|
||||
"rgb(136,136,136)",
|
||||
"rgb(125,125,125)",
|
||||
"rgb(115,115,115)",
|
||||
"rgb(106,106,106)",
|
||||
"rgb(95,95,95)",
|
||||
"rgb(85,85,85)",
|
||||
"rgb(76,76,76)",
|
||||
"rgb(66,66,66)",
|
||||
"rgb(55,55,55)",
|
||||
"rgb(46,46,46)",
|
||||
"rgb(36,36,36)",
|
||||
"rgb(25,25,25)",
|
||||
"rgb(16,16,16)",
|
||||
"rgb(6,6,6)",
|
||||
],
|
||||
GistEarth: [
|
||||
"rgb(0,0,0)",
|
||||
"rgb(2,7,117)",
|
||||
"rgb(9,30,118)",
|
||||
"rgb(16,53,120)",
|
||||
"rgb(23,73,122)",
|
||||
"rgb(31,93,124)",
|
||||
"rgb(39,110,125)",
|
||||
"rgb(47,126,127)",
|
||||
"rgb(51,133,119)",
|
||||
"rgb(57,138,106)",
|
||||
"rgb(62,145,94)",
|
||||
"rgb(66,150,82)",
|
||||
"rgb(74,157,71)",
|
||||
"rgb(97,162,77)",
|
||||
"rgb(121,168,83)",
|
||||
"rgb(136,173,85)",
|
||||
"rgb(153,176,88)",
|
||||
"rgb(170,180,92)",
|
||||
"rgb(185,182,94)",
|
||||
"rgb(189,173,99)",
|
||||
"rgb(192,164,101)",
|
||||
"rgb(203,169,124)",
|
||||
"rgb(215,178,149)",
|
||||
"rgb(226,192,176)",
|
||||
"rgb(238,212,204)",
|
||||
"rgb(248,236,236)",
|
||||
],
|
||||
BlueWaves: [
|
||||
"rgb(83,0,215)",
|
||||
"rgb(43,6,108)",
|
||||
"rgb(9,16,16)",
|
||||
"rgb(8,32,25)",
|
||||
"rgb(0,50,8)",
|
||||
"rgb(27,64,66)",
|
||||
"rgb(69,67,178)",
|
||||
"rgb(115,62,210)",
|
||||
"rgb(155,50,104)",
|
||||
"rgb(178,43,41)",
|
||||
"rgb(180,51,34)",
|
||||
"rgb(161,78,87)",
|
||||
"rgb(124,117,187)",
|
||||
"rgb(78,155,203)",
|
||||
"rgb(34,178,85)",
|
||||
"rgb(4,176,2)",
|
||||
"rgb(9,152,27)",
|
||||
"rgb(4,118,2)",
|
||||
"rgb(34,92,85)",
|
||||
"rgb(78,92,203)",
|
||||
"rgb(124,127,187)",
|
||||
"rgb(161,187,87)",
|
||||
"rgb(180,248,34)",
|
||||
"rgb(178,220,41)",
|
||||
"rgb(155,217,104)",
|
||||
"rgb(115,254,210)",
|
||||
],
|
||||
BlueGreenRedYellow: [
|
||||
"rgb(0,0,0)",
|
||||
"rgb(0,0,20)",
|
||||
"rgb(0,0,41)",
|
||||
"rgb(0,0,62)",
|
||||
"rgb(0,25,83)",
|
||||
"rgb(0,57,101)",
|
||||
"rgb(0,87,101)",
|
||||
"rgb(0,118,101)",
|
||||
"rgb(0,150,101)",
|
||||
"rgb(0,150,69)",
|
||||
"rgb(0,148,37)",
|
||||
"rgb(0,141,6)",
|
||||
"rgb(60,120,0)",
|
||||
"rgb(131,87,0)",
|
||||
"rgb(180,25,0)",
|
||||
"rgb(203,13,0)",
|
||||
"rgb(208,36,0)",
|
||||
"rgb(213,60,0)",
|
||||
"rgb(219,83,0)",
|
||||
"rgb(224,106,0)",
|
||||
"rgb(229,129,0)",
|
||||
"rgb(233,152,0)",
|
||||
"rgb(238,176,0)",
|
||||
"rgb(243,199,0)",
|
||||
"rgb(248,222,0)",
|
||||
"rgb(254,245,0)",
|
||||
],
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<Row cols={1} class="p-2 g-2">
|
||||
<!-- COLORSCHEME -->
|
||||
<Col
|
||||
><Card>
|
||||
<form
|
||||
id="colorscheme-form"
|
||||
method="post"
|
||||
action="/frontend/configuration/"
|
||||
class="card-body"
|
||||
>
|
||||
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
|
||||
<CardTitle
|
||||
style="margin-bottom: 1em; display: flex; align-items: center;"
|
||||
>
|
||||
<div>Color Scheme for Timeseries Plots</div>
|
||||
{#if displayMessage && message.target == "cs"}<div
|
||||
style="margin-left: auto; font-size: 0.9em;"
|
||||
>
|
||||
<code style="color: {message.color};" out:fade
|
||||
>Update: {message.msg}</code
|
||||
>
|
||||
</div>{/if}
|
||||
</CardTitle>
|
||||
<input type="hidden" name="key" value="plot_general_colorscheme" />
|
||||
<Table hover>
|
||||
<tbody>
|
||||
{#each Object.entries(colorschemes) as [name, rgbrow]}
|
||||
<tr>
|
||||
<th scope="col">{name}</th>
|
||||
<td>
|
||||
{#if rgbrow.join(",") == config.plot_general_colorscheme}
|
||||
<input
|
||||
type="radio"
|
||||
name="value"
|
||||
value={JSON.stringify(rgbrow)}
|
||||
checked
|
||||
on:click={() =>
|
||||
updateSetting("#colorscheme-form", "cs")}
|
||||
/>
|
||||
{:else}
|
||||
<input
|
||||
type="radio"
|
||||
name="value"
|
||||
value={JSON.stringify(rgbrow)}
|
||||
on:click={() =>
|
||||
updateSetting("#colorscheme-form", "cs")}
|
||||
/>
|
||||
{/if}
|
||||
</td>
|
||||
<td>
|
||||
{#each rgbrow as rgb}
|
||||
<span class="color-dot" style="background-color: {rgb};"
|
||||
></span>
|
||||
{/each}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</Table>
|
||||
</form>
|
||||
</Card></Col
|
||||
>
|
||||
</Row>
|
||||
|
||||
<style>
|
||||
.color-dot {
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
166
web/frontend/src/config/user/PlotRenderOptions.svelte
Normal file
166
web/frontend/src/config/user/PlotRenderOptions.svelte
Normal file
@ -0,0 +1,166 @@
|
||||
<script>
|
||||
import {
|
||||
Button,
|
||||
Row,
|
||||
Col,
|
||||
Card,
|
||||
CardTitle,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import { fade } from "svelte/transition";
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
export let config;
|
||||
export let message;
|
||||
export let displayMessage;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
function updateSetting(selector, target) {
|
||||
dispatch('update', {
|
||||
selector: selector,
|
||||
target: target
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<Row cols={3} class="p-2 g-2">
|
||||
<!-- LINE WIDTH -->
|
||||
<Col
|
||||
><Card class="h-100">
|
||||
<!-- Important: Function with arguments needs to be event-triggered like on:submit={() => functionName('Some','Args')} OR no arguments and like this: on:submit={functionName} -->
|
||||
<form
|
||||
id="line-width-form"
|
||||
method="post"
|
||||
action="/frontend/configuration/"
|
||||
class="card-body"
|
||||
on:submit|preventDefault={() =>
|
||||
updateSetting("#line-width-form", "lw")}
|
||||
>
|
||||
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
|
||||
<CardTitle
|
||||
style="margin-bottom: 1em; display: flex; align-items: center;"
|
||||
>
|
||||
<div>Line Width</div>
|
||||
<!-- Expand If-Clause for clarity once -->
|
||||
{#if displayMessage && message.target == "lw"}
|
||||
<div style="margin-left: auto; font-size: 0.9em;">
|
||||
<code style="color: {message.color};" out:fade>
|
||||
Update: {message.msg}
|
||||
</code>
|
||||
</div>
|
||||
{/if}
|
||||
</CardTitle>
|
||||
<input type="hidden" name="key" value="plot_general_lineWidth" />
|
||||
<div class="mb-3">
|
||||
<label for="value" class="form-label">Line Width</label>
|
||||
<input
|
||||
type="number"
|
||||
class="form-control"
|
||||
id="lwvalue"
|
||||
name="value"
|
||||
aria-describedby="lineWidthHelp"
|
||||
value={config.plot_general_lineWidth}
|
||||
min="1"
|
||||
/>
|
||||
<div id="lineWidthHelp" class="form-text">
|
||||
Width of the lines in the timeseries plots.
|
||||
</div>
|
||||
</div>
|
||||
<Button color="primary" type="submit">Submit</Button>
|
||||
</form>
|
||||
</Card></Col
|
||||
>
|
||||
|
||||
<!-- PLOTS PER ROW -->
|
||||
<Col
|
||||
><Card class="h-100">
|
||||
<form
|
||||
id="plots-per-row-form"
|
||||
method="post"
|
||||
action="/frontend/configuration/"
|
||||
class="card-body"
|
||||
on:submit|preventDefault={() =>
|
||||
updateSetting("#plots-per-row-form", "ppr")}
|
||||
>
|
||||
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
|
||||
<CardTitle
|
||||
style="margin-bottom: 1em; display: flex; align-items: center;"
|
||||
>
|
||||
<div>Plots per Row</div>
|
||||
{#if displayMessage && message.target == "ppr"}<div
|
||||
style="margin-left: auto; font-size: 0.9em;"
|
||||
>
|
||||
<code style="color: {message.color};" out:fade
|
||||
>Update: {message.msg}</code
|
||||
>
|
||||
</div>{/if}
|
||||
</CardTitle>
|
||||
<input type="hidden" name="key" value="plot_view_plotsPerRow" />
|
||||
<div class="mb-3">
|
||||
<label for="value" class="form-label">Plots per Row</label>
|
||||
<input
|
||||
type="number"
|
||||
class="form-control"
|
||||
id="pprvalue"
|
||||
name="value"
|
||||
aria-describedby="plotsperrowHelp"
|
||||
value={config.plot_view_plotsPerRow}
|
||||
min="1"
|
||||
/>
|
||||
<div id="plotsperrowHelp" class="form-text">
|
||||
How many plots to show next to each other on pages such as
|
||||
/monitoring/job/, /monitoring/system/...
|
||||
</div>
|
||||
</div>
|
||||
<Button color="primary" type="submit">Submit</Button>
|
||||
</form>
|
||||
</Card></Col
|
||||
>
|
||||
|
||||
<!-- BACKGROUND -->
|
||||
<Col
|
||||
><Card class="h-100">
|
||||
<form
|
||||
id="backgrounds-form"
|
||||
method="post"
|
||||
action="/frontend/configuration/"
|
||||
class="card-body"
|
||||
on:submit|preventDefault={() =>
|
||||
updateSetting("#backgrounds-form", "bg")}
|
||||
>
|
||||
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
|
||||
<CardTitle
|
||||
style="margin-bottom: 1em; display: flex; align-items: center;"
|
||||
>
|
||||
<div>Colored Backgrounds</div>
|
||||
{#if displayMessage && message.target == "bg"}<div
|
||||
style="margin-left: auto; font-size: 0.9em;"
|
||||
>
|
||||
<code style="color: {message.color};" out:fade
|
||||
>Update: {message.msg}</code
|
||||
>
|
||||
</div>{/if}
|
||||
</CardTitle>
|
||||
<input type="hidden" name="key" value="plot_general_colorBackground" />
|
||||
<div class="mb-3">
|
||||
<div>
|
||||
{#if config.plot_general_colorBackground}
|
||||
<input type="radio" id="true-checked" name="value" value="true" checked />
|
||||
{:else}
|
||||
<input type="radio" id="true" name="value" value="true" />
|
||||
{/if}
|
||||
<label for="true">Yes</label>
|
||||
</div>
|
||||
<div>
|
||||
{#if config.plot_general_colorBackground}
|
||||
<input type="radio" id="false" name="value" value="false" />
|
||||
{:else}
|
||||
<input type="radio" id="false-checked" name="value" value="false" checked />
|
||||
{/if}
|
||||
<label for="false">No</label>
|
||||
</div>
|
||||
</div>
|
||||
<Button color="primary" type="submit">Submit</Button>
|
||||
</form>
|
||||
</Card></Col
|
||||
>
|
||||
</Row>
|
131
web/frontend/src/config/user/UserOptions.svelte
Normal file
131
web/frontend/src/config/user/UserOptions.svelte
Normal file
@ -0,0 +1,131 @@
|
||||
<script>
|
||||
import {
|
||||
Button,
|
||||
Row,
|
||||
Col,
|
||||
Card,
|
||||
CardTitle,
|
||||
CardBody
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import { fade } from "svelte/transition";
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { fetchJwt } from "../../utils.js";
|
||||
|
||||
export let config;
|
||||
export let message;
|
||||
export let displayMessage;
|
||||
export let username;
|
||||
export let isApi;
|
||||
|
||||
let jwt = "";
|
||||
function getUserJwt(username) {
|
||||
if (username) {
|
||||
const p = fetchJwt(username);
|
||||
p.then((content) => {
|
||||
jwt = content
|
||||
}).catch((error) => {
|
||||
console.error(`Could not get JWT: ${error}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function clipJwt() {
|
||||
navigator.clipboard
|
||||
.writeText(jwt)
|
||||
.catch((reason) => console.error(reason));
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
function updateSetting(selector, target) {
|
||||
dispatch('update', {
|
||||
selector: selector,
|
||||
target: target
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<Row cols={isApi ? 3 : 1} class="p-2 g-2">
|
||||
<!-- PAGING -->
|
||||
<Col>
|
||||
<Card class="h-100">
|
||||
<form
|
||||
id="paging-form"
|
||||
method="post"
|
||||
action="/frontend/configuration/"
|
||||
class="card-body"
|
||||
on:submit|preventDefault={() =>
|
||||
updateSetting("#paging-form", "pag")}
|
||||
>
|
||||
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
|
||||
<CardTitle
|
||||
style="margin-bottom: 1em; display: flex; align-items: center;"
|
||||
>
|
||||
<div>Paging Type</div>
|
||||
{#if displayMessage && message.target == "pag"}<div
|
||||
style="margin-left: auto; font-size: 0.9em;"
|
||||
>
|
||||
<code style="color: {message.color};" out:fade
|
||||
>Update: {message.msg}</code
|
||||
>
|
||||
</div>{/if}
|
||||
</CardTitle>
|
||||
<input type="hidden" name="key" value="job_list_usePaging" />
|
||||
<div class="mb-3">
|
||||
<div>
|
||||
{#if config.job_list_usePaging}
|
||||
<input type="radio" id="true-checked" name="value" value="true" checked />
|
||||
{:else}
|
||||
<input type="radio" id="true" name="value" value="true" />
|
||||
{/if}
|
||||
<label for="true">Paging with selectable count of jobs.</label>
|
||||
</div>
|
||||
<div>
|
||||
{#if config.job_list_usePaging}
|
||||
<input type="radio" id="false" name="value" value="false" />
|
||||
{:else}
|
||||
<input type="radio" id="false-checked" name="value" value="false" checked />
|
||||
{/if}
|
||||
<label for="false">Continuous scroll iteratively adding 10 jobs.</label>
|
||||
</div>
|
||||
</div>
|
||||
<Button color="primary" type="submit">Submit</Button>
|
||||
</form>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{#if isApi}
|
||||
<!-- USER-JWT BTN -->
|
||||
<Col>
|
||||
<Card class="h-100">
|
||||
<CardBody>
|
||||
<CardTitle>Generate JWT</CardTitle>
|
||||
{#if jwt}
|
||||
<Button color="secondary" on:click={clipJwt()}>
|
||||
Copy JWT to Clipboard
|
||||
</Button>
|
||||
<p class="mt-2">
|
||||
Your token is displayed on the right. Press this button to copy it to the clipboard.
|
||||
</p>
|
||||
{:else}
|
||||
<Button color="success" on:click={getUserJwt(username)}>
|
||||
Generate JWT for '{username}'
|
||||
</Button>
|
||||
<p class="mt-2">
|
||||
Generate a JSON Web Token for use with the ClusterCockpit REST-API endpoints.
|
||||
</p>
|
||||
{/if}
|
||||
</CardBody>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<!-- USER-JWT RES -->
|
||||
<Col>
|
||||
<Card class="h-100">
|
||||
<CardBody>
|
||||
<CardTitle>Display JWT</CardTitle>
|
||||
<textarea cols="32" rows="5" readonly>{jwt ? jwt : 'Press "Gen. JWT" to request token ...'}</textarea>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</Col>
|
||||
{/if}
|
||||
</Row>
|
@ -239,7 +239,7 @@ export async function fetchMetrics(job, metrics, scopes) {
|
||||
|
||||
try {
|
||||
let res = await fetch(
|
||||
`/api/jobs/metrics/${job.id}${query.length > 0 ? "?" : ""}${query.join(
|
||||
`/frontend/jobs/metrics/${job.id}${query.length > 0 ? "?" : ""}${query.join(
|
||||
"&"
|
||||
)}`
|
||||
);
|
||||
@ -433,6 +433,18 @@ export function transformPerNodeDataForRoofline(nodes) {
|
||||
return data
|
||||
}
|
||||
|
||||
export async function fetchJwt(username) {
|
||||
const raw = await fetch(`/frontend/jwt/?username=${username}`);
|
||||
|
||||
if (!raw.ok) {
|
||||
const message = `An error has occured: ${response.status}`;
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
const res = await raw.text();
|
||||
return res;
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/questions/45309447/calculating-median-javascript
|
||||
// function median(numbers) {
|
||||
// const sorted = Array.from(numbers).sort((a, b) => a - b);
|
||||
|
@ -8,6 +8,8 @@
|
||||
{{define "javascript"}}
|
||||
<script>
|
||||
const isAdmin = {{ .User.HasRole .Roles.admin }};
|
||||
const isApi = {{ .User.HasRole .Roles.api }};
|
||||
const username = {{ .User.Username }};
|
||||
const filterPresets = {{ .FilterPresets }};
|
||||
const clusterCockpitConfig = {{ .Config }};
|
||||
</script>
|
||||
|
Loading…
Reference in New Issue
Block a user