mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2024-12-26 13:29: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()
|
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 {
|
if !config.Keys.DisableAuthentication {
|
||||||
r.Handle("/login", authentication.Login(
|
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 {
|
if flagDev {
|
||||||
@ -453,7 +521,10 @@ func main() {
|
|||||||
|
|
||||||
// Mount all /monitoring/... and /api/... routes.
|
// Mount all /monitoring/... and /api/... routes.
|
||||||
routerConfig.SetupRoutes(secured, buildInfo)
|
routerConfig.SetupRoutes(secured, buildInfo)
|
||||||
api.MountRoutes(secured)
|
api.MountApiRoutes(securedapi)
|
||||||
|
api.MountUserApiRoutes(userapi)
|
||||||
|
api.MountConfigApiRoutes(configapi)
|
||||||
|
api.MountFrontendApiRoutes(frontendapi)
|
||||||
|
|
||||||
if config.Keys.EmbedStaticFiles {
|
if config.Keys.EmbedStaticFiles {
|
||||||
if i, err := os.Stat("./var/img"); err == nil {
|
if i, err := os.Stat("./var/img"); err == nil {
|
||||||
|
@ -197,7 +197,9 @@ func TestRestApi(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
r := mux.NewRouter()
|
r := mux.NewRouter()
|
||||||
restapi.MountRoutes(r)
|
r.PathPrefix("/api").Subrouter()
|
||||||
|
r.StrictSlash(true)
|
||||||
|
restapi.MountApiRoutes(r)
|
||||||
|
|
||||||
const startJobBody string = `{
|
const startJobBody string = `{
|
||||||
"jobId": 123,
|
"jobId": 123,
|
||||||
@ -225,11 +227,22 @@ func TestRestApi(t *testing.T) {
|
|||||||
}`
|
}`
|
||||||
|
|
||||||
var dbid int64
|
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) {
|
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()
|
recorder := httptest.NewRecorder()
|
||||||
|
|
||||||
r.ServeHTTP(recorder, req)
|
ctx := context.WithValue(req.Context(), contextUserKey, contextUserValue)
|
||||||
|
|
||||||
|
r.ServeHTTP(recorder, req.WithContext(ctx))
|
||||||
response := recorder.Result()
|
response := recorder.Result()
|
||||||
if response.StatusCode != http.StatusCreated {
|
if response.StatusCode != http.StatusCreated {
|
||||||
t.Fatal(response.Status, recorder.Body.String())
|
t.Fatal(response.Status, recorder.Body.String())
|
||||||
@ -240,12 +253,12 @@ func TestRestApi(t *testing.T) {
|
|||||||
t.Fatal(err)
|
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 {
|
if err != nil {
|
||||||
t.Fatal(err)
|
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 {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -289,17 +302,19 @@ func TestRestApi(t *testing.T) {
|
|||||||
|
|
||||||
var stoppedJob *schema.Job
|
var stoppedJob *schema.Job
|
||||||
if ok := t.Run("StopJob", func(t *testing.T) {
|
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()
|
recorder := httptest.NewRecorder()
|
||||||
|
|
||||||
r.ServeHTTP(recorder, req)
|
ctx := context.WithValue(req.Context(), contextUserKey, contextUserValue)
|
||||||
|
|
||||||
|
r.ServeHTTP(recorder, req.WithContext(ctx))
|
||||||
response := recorder.Result()
|
response := recorder.Result()
|
||||||
if response.StatusCode != http.StatusOK {
|
if response.StatusCode != http.StatusOK {
|
||||||
t.Fatal(response.Status, recorder.Body.String())
|
t.Fatal(response.Status, recorder.Body.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
restapi.JobRepository.WaitForArchiving()
|
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 {
|
if err != nil {
|
||||||
t.Fatal(err)
|
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!
|
// 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)
|
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()
|
recorder := httptest.NewRecorder()
|
||||||
|
|
||||||
r.ServeHTTP(recorder, req)
|
ctx := context.WithValue(req.Context(), contextUserKey, contextUserValue)
|
||||||
|
|
||||||
|
r.ServeHTTP(recorder, req.WithContext(ctx))
|
||||||
response := recorder.Result()
|
response := recorder.Result()
|
||||||
if response.StatusCode != http.StatusUnprocessableEntity {
|
if response.StatusCode != http.StatusUnprocessableEntity {
|
||||||
t.Fatal(response.Status, recorder.Body.String())
|
t.Fatal(response.Status, recorder.Body.String())
|
||||||
@ -371,10 +388,12 @@ func TestRestApi(t *testing.T) {
|
|||||||
}`
|
}`
|
||||||
|
|
||||||
ok := t.Run("StartJobFailed", func(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()
|
recorder := httptest.NewRecorder()
|
||||||
|
|
||||||
r.ServeHTTP(recorder, req)
|
ctx := context.WithValue(req.Context(), contextUserKey, contextUserValue)
|
||||||
|
|
||||||
|
r.ServeHTTP(recorder, req.WithContext(ctx))
|
||||||
response := recorder.Result()
|
response := recorder.Result()
|
||||||
if response.StatusCode != http.StatusCreated {
|
if response.StatusCode != http.StatusCreated {
|
||||||
t.Fatal(response.Status, recorder.Body.String())
|
t.Fatal(response.Status, recorder.Body.String())
|
||||||
@ -393,10 +412,12 @@ func TestRestApi(t *testing.T) {
|
|||||||
}`
|
}`
|
||||||
|
|
||||||
ok = t.Run("StopJobFailed", func(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()
|
recorder := httptest.NewRecorder()
|
||||||
|
|
||||||
r.ServeHTTP(recorder, req)
|
ctx := context.WithValue(req.Context(), contextUserKey, contextUserValue)
|
||||||
|
|
||||||
|
r.ServeHTTP(recorder, req.WithContext(ctx))
|
||||||
response := recorder.Result()
|
response := recorder.Result()
|
||||||
if response.StatusCode != http.StatusOK {
|
if response.StatusCode != http.StatusOK {
|
||||||
t.Fatal(response.Status, recorder.Body.String())
|
t.Fatal(response.Status, recorder.Body.String())
|
||||||
|
@ -59,8 +59,7 @@ type RestApi struct {
|
|||||||
RepositoryMutex sync.Mutex
|
RepositoryMutex sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *RestApi) MountRoutes(r *mux.Router) {
|
func (api *RestApi) MountApiRoutes(r *mux.Router) {
|
||||||
r = r.PathPrefix("/api").Subrouter()
|
|
||||||
r.StrictSlash(true)
|
r.StrictSlash(true)
|
||||||
|
|
||||||
r.HandleFunc("/jobs/start_job/", api.startJob).Methods(http.MethodPost, http.MethodPut)
|
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.getMachineState).Methods(http.MethodGet)
|
||||||
r.HandleFunc("/machine_state/{cluster}/{host}", api.putMachineState).Methods(http.MethodPut, http.MethodPost)
|
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 {
|
if api.Authentication != nil {
|
||||||
r.HandleFunc("/jwt/", api.getJWT).Methods(http.MethodGet)
|
|
||||||
r.HandleFunc("/roles/", api.getRoles).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.createUser).Methods(http.MethodPost, http.MethodPut)
|
||||||
r.HandleFunc("/users/", api.getUsers).Methods(http.MethodGet)
|
r.HandleFunc("/users/", api.getUsers).Methods(http.MethodGet)
|
||||||
r.HandleFunc("/users/", api.deleteUser).Methods(http.MethodDelete)
|
r.HandleFunc("/users/", api.deleteUser).Methods(http.MethodDelete)
|
||||||
r.HandleFunc("/user/{id}", api.updateUser).Methods(http.MethodPost)
|
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("/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
|
// @security ApiKeyAuth
|
||||||
// @router /jobs/ [get]
|
// @router /jobs/ [get]
|
||||||
func (api *RestApi) getJobs(rw http.ResponseWriter, r *http.Request) {
|
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
|
withMetadata := false
|
||||||
filter := &model.JobFilter{}
|
filter := &model.JobFilter{}
|
||||||
page := &model.PageRequest{ItemsPerPage: 25, Page: 1}
|
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
|
// @summary Get job meta and optional all metric data
|
||||||
// @tags Job query
|
// @tags Job query
|
||||||
// @description Job to get is specified by database ID
|
// @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
|
// @security ApiKeyAuth
|
||||||
// @router /jobs/{id} [get]
|
// @router /jobs/{id} [get]
|
||||||
func (api *RestApi) getCompleteJobById(rw http.ResponseWriter, r *http.Request) {
|
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
|
// Fetch job from db
|
||||||
id, ok := mux.Vars(r)["id"]
|
id, ok := mux.Vars(r)["id"]
|
||||||
var job *schema.Job
|
var job *schema.Job
|
||||||
@ -471,7 +476,7 @@ func (api *RestApi) getCompleteJobById(rw http.ResponseWriter, r *http.Request)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
job, err = api.JobRepository.FindById(id)
|
job, err = api.JobRepository.FindById(r.Context(), id) // Get Job from Repo by ID
|
||||||
} else {
|
} else {
|
||||||
handleError(errors.New("the parameter 'id' is required"), http.StatusBadRequest, rw)
|
handleError(errors.New("the parameter 'id' is required"), http.StatusBadRequest, rw)
|
||||||
return
|
return
|
||||||
@ -546,14 +551,6 @@ func (api *RestApi) getCompleteJobById(rw http.ResponseWriter, r *http.Request)
|
|||||||
// @security ApiKeyAuth
|
// @security ApiKeyAuth
|
||||||
// @router /jobs/{id} [post]
|
// @router /jobs/{id} [post]
|
||||||
func (api *RestApi) getJobById(rw http.ResponseWriter, r *http.Request) {
|
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
|
// Fetch job from db
|
||||||
id, ok := mux.Vars(r)["id"]
|
id, ok := mux.Vars(r)["id"]
|
||||||
var job *schema.Job
|
var job *schema.Job
|
||||||
@ -565,7 +562,7 @@ func (api *RestApi) getJobById(rw http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
job, err = api.JobRepository.FindById(id)
|
job, err = api.JobRepository.FindById(r.Context(), id)
|
||||||
} else {
|
} else {
|
||||||
handleError(errors.New("the parameter 'id' is required"), http.StatusBadRequest, rw)
|
handleError(errors.New("the parameter 'id' is required"), http.StatusBadRequest, rw)
|
||||||
return
|
return
|
||||||
@ -651,19 +648,13 @@ func (api *RestApi) getJobById(rw http.ResponseWriter, r *http.Request) {
|
|||||||
// @security ApiKeyAuth
|
// @security ApiKeyAuth
|
||||||
// @router /jobs/edit_meta/{id} [post]
|
// @router /jobs/edit_meta/{id} [post]
|
||||||
func (api *RestApi) editMeta(rw http.ResponseWriter, r *http.Request) {
|
func (api *RestApi) editMeta(rw http.ResponseWriter, r *http.Request) {
|
||||||
if user := repository.GetUserFromContext(r.Context()); user != nil &&
|
id, err := strconv.ParseInt(mux.Vars(r)["id"], 10, 64)
|
||||||
!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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(rw, err.Error(), http.StatusBadRequest)
|
http.Error(rw, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
job, err := api.JobRepository.FindById(iid)
|
job, err := api.JobRepository.FindById(r.Context(), id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(rw, err.Error(), http.StatusNotFound)
|
http.Error(rw, err.Error(), http.StatusNotFound)
|
||||||
return
|
return
|
||||||
@ -702,20 +693,13 @@ func (api *RestApi) editMeta(rw http.ResponseWriter, r *http.Request) {
|
|||||||
// @security ApiKeyAuth
|
// @security ApiKeyAuth
|
||||||
// @router /jobs/tag_job/{id} [post]
|
// @router /jobs/tag_job/{id} [post]
|
||||||
func (api *RestApi) tagJob(rw http.ResponseWriter, r *http.Request) {
|
func (api *RestApi) tagJob(rw http.ResponseWriter, r *http.Request) {
|
||||||
if user := repository.GetUserFromContext(r.Context()); user != nil &&
|
id, err := strconv.ParseInt(mux.Vars(r)["id"], 10, 64)
|
||||||
!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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(rw, err.Error(), http.StatusBadRequest)
|
http.Error(rw, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
job, err := api.JobRepository.FindById(iid)
|
job, err := api.JobRepository.FindById(r.Context(), id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(rw, err.Error(), http.StatusNotFound)
|
http.Error(rw, err.Error(), http.StatusNotFound)
|
||||||
return
|
return
|
||||||
@ -769,13 +753,6 @@ func (api *RestApi) tagJob(rw http.ResponseWriter, r *http.Request) {
|
|||||||
// @security ApiKeyAuth
|
// @security ApiKeyAuth
|
||||||
// @router /jobs/start_job/ [post]
|
// @router /jobs/start_job/ [post]
|
||||||
func (api *RestApi) startJob(rw http.ResponseWriter, r *http.Request) {
|
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}
|
req := schema.JobMeta{BaseJob: schema.JobDefaults}
|
||||||
if err := decode(r.Body, &req); err != nil {
|
if err := decode(r.Body, &req); err != nil {
|
||||||
handleError(fmt.Errorf("parsing request body failed: %w", err), http.StatusBadRequest, rw)
|
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
|
// @security ApiKeyAuth
|
||||||
// @router /jobs/stop_job/{id} [post]
|
// @router /jobs/stop_job/{id} [post]
|
||||||
func (api *RestApi) stopJobById(rw http.ResponseWriter, r *http.Request) {
|
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
|
// Parse request body: Only StopTime and State
|
||||||
req := StopJobApiRequest{}
|
req := StopJobApiRequest{}
|
||||||
if err := decode(r.Body, &req); err != nil {
|
if err := decode(r.Body, &req); err != nil {
|
||||||
@ -877,7 +847,7 @@ func (api *RestApi) stopJobById(rw http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
job, err = api.JobRepository.FindById(id)
|
job, err = api.JobRepository.FindById(r.Context(), id)
|
||||||
} else {
|
} else {
|
||||||
handleError(errors.New("the parameter 'id' is required"), http.StatusBadRequest, rw)
|
handleError(errors.New("the parameter 'id' is required"), http.StatusBadRequest, rw)
|
||||||
return
|
return
|
||||||
@ -907,13 +877,6 @@ func (api *RestApi) stopJobById(rw http.ResponseWriter, r *http.Request) {
|
|||||||
// @security ApiKeyAuth
|
// @security ApiKeyAuth
|
||||||
// @router /jobs/stop_job/ [post]
|
// @router /jobs/stop_job/ [post]
|
||||||
func (api *RestApi) stopJobByRequest(rw http.ResponseWriter, r *http.Request) {
|
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
|
// Parse request body
|
||||||
req := StopJobApiRequest{}
|
req := StopJobApiRequest{}
|
||||||
if err := decode(r.Body, &req); err != nil {
|
if err := decode(r.Body, &req); err != nil {
|
||||||
@ -955,11 +918,6 @@ func (api *RestApi) stopJobByRequest(rw http.ResponseWriter, r *http.Request) {
|
|||||||
// @security ApiKeyAuth
|
// @security ApiKeyAuth
|
||||||
// @router /jobs/delete_job/{id} [delete]
|
// @router /jobs/delete_job/{id} [delete]
|
||||||
func (api *RestApi) deleteJobById(rw http.ResponseWriter, r *http.Request) {
|
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
|
// Fetch job (that will be stopped) from db
|
||||||
id, ok := mux.Vars(r)["id"]
|
id, ok := mux.Vars(r)["id"]
|
||||||
var err error
|
var err error
|
||||||
@ -1003,12 +961,6 @@ func (api *RestApi) deleteJobById(rw http.ResponseWriter, r *http.Request) {
|
|||||||
// @security ApiKeyAuth
|
// @security ApiKeyAuth
|
||||||
// @router /jobs/delete_job/ [delete]
|
// @router /jobs/delete_job/ [delete]
|
||||||
func (api *RestApi) deleteJobByRequest(rw http.ResponseWriter, r *http.Request) {
|
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
|
// Parse request body
|
||||||
req := DeleteJobApiRequest{}
|
req := DeleteJobApiRequest{}
|
||||||
if err := decode(r.Body, &req); err != nil {
|
if err := decode(r.Body, &req); err != nil {
|
||||||
@ -1060,11 +1012,6 @@ func (api *RestApi) deleteJobByRequest(rw http.ResponseWriter, r *http.Request)
|
|||||||
// @security ApiKeyAuth
|
// @security ApiKeyAuth
|
||||||
// @router /jobs/delete_job_before/{ts} [delete]
|
// @router /jobs/delete_job_before/{ts} [delete]
|
||||||
func (api *RestApi) deleteJobBefore(rw http.ResponseWriter, r *http.Request) {
|
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
|
var cnt int
|
||||||
// Fetch job (that will be stopped) from db
|
// Fetch job (that will be stopped) from db
|
||||||
id, ok := mux.Vars(r)["ts"]
|
id, ok := mux.Vars(r)["ts"]
|
||||||
|
@ -219,31 +219,141 @@ func (auth *Authentication) Auth(
|
|||||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
user, err := auth.JwtAuth.AuthViaJWT(rw, r)
|
user, err := auth.JwtAuth.AuthViaJWT(rw, r)
|
||||||
if err != nil {
|
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)
|
http.Error(rw, err.Error(), http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if user == nil {
|
if user == nil {
|
||||||
user, err = auth.AuthViaSession(rw, r)
|
user, err = auth.AuthViaSession(rw, r)
|
||||||
if err != nil {
|
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)
|
http.Error(rw, err.Error(), http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if user != nil {
|
if user != nil {
|
||||||
ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
|
ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
|
||||||
onsuccess.ServeHTTP(rw, r.WithContext(ctx))
|
onsuccess.ServeHTTP(rw, r.WithContext(ctx))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug("authentication failed")
|
log.Info("auth -> authentication failed")
|
||||||
onfailure(rw, r, errors.New("unauthorized (please login first)"))
|
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 {
|
func (auth *Authentication) Logout(onsuccess http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
session, err := auth.sessionStore.Get(r, "session")
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
job, err := r.Repo.FindById(numericId)
|
job, err := r.Repo.FindById(ctx, numericId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("Error while finding job by id")
|
log.Warn("Error while finding job by id")
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -86,12 +86,63 @@ func (r *JobRepository) FindAll(
|
|||||||
// The job is queried using the database id.
|
// The job is queried using the database id.
|
||||||
// It returns a pointer to a schema.Job data structure and an error variable.
|
// 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
|
// 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...).
|
q := sq.Select(jobColumns...).
|
||||||
From("job").Where("job.id = ?", jobId)
|
From("job").Where("job.id = ?", jobId)
|
||||||
return scanJob(q.RunWith(r.stmtCache).QueryRow())
|
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(
|
func (r *JobRepository) FindConcurrentJobs(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
job *schema.Job,
|
job *schema.Job,
|
||||||
|
@ -97,23 +97,25 @@ func SecurityCheck(ctx context.Context, query sq.SelectBuilder) (sq.SelectBuilde
|
|||||||
if user == nil {
|
if user == nil {
|
||||||
var qnil sq.SelectBuilder
|
var qnil sq.SelectBuilder
|
||||||
return qnil, fmt.Errorf("user context is nil")
|
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
|
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 {
|
if len(user.Projects) != 0 {
|
||||||
return query.Where(sq.Or{sq.Eq{"job.project": user.Projects}, sq.Eq{"job.user": user.Username}}), nil
|
return query.Where(sq.Or{sq.Eq{"job.project": user.Projects}, sq.Eq{"job.user": user.Username}}), nil
|
||||||
} else {
|
} else {
|
||||||
log.Debugf("Manager-User '%s' has no defined projects to lookup! Query only personal jobs ...", user.Username)
|
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
|
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
|
return query.Where("job.user = ?", user.Username), nil
|
||||||
} else {
|
default: // No known Role, return error
|
||||||
// Shortterm compatibility: Return User-Query if no roles:
|
var qnil sq.SelectBuilder
|
||||||
return query.Where("job.user = ?", user.Username), nil
|
return qnil, fmt.Errorf("user has no or unknown roles")
|
||||||
// // 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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ func TestFind(t *testing.T) {
|
|||||||
func TestFindById(t *testing.T) {
|
func TestFindById(t *testing.T) {
|
||||||
r := setup(t)
|
r := setup(t)
|
||||||
|
|
||||||
job, err := r.FindById(5)
|
job, err := r.FindById(getContext(t), 5)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -55,7 +55,7 @@ func BenchmarkDB_FindJobById(b *testing.B) {
|
|||||||
|
|
||||||
b.RunParallel(func(pb *testing.PB) {
|
b.RunParallel(func(pb *testing.PB) {
|
||||||
for pb.Next() {
|
for pb.Next() {
|
||||||
_, err := db.FindById(jobId)
|
_, err := db.FindById(getContext(b), jobId)
|
||||||
noErr(b, err)
|
noErr(b, err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -23,7 +23,7 @@ func (r *JobRepository) AddTag(job int64, tag int64) ([]*schema.Tag, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
j, err := r.FindById(job)
|
j, err := r.FindByIdDirect(job)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("Error while finding job by id")
|
log.Warn("Error while finding job by id")
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -48,7 +48,7 @@ func (r *JobRepository) RemoveTag(job, tag int64) ([]*schema.Tag, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
j, err := r.FindById(job)
|
j, err := r.FindByIdDirect(job)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("Error while finding job by id")
|
log.Warn("Error while finding job by id")
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -1,16 +1,15 @@
|
|||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte";
|
// import { init } from "./utils.js";
|
||||||
import { init } from "./utils.js";
|
|
||||||
import { Card, CardHeader, CardTitle } from "@sveltestrap/sveltestrap";
|
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";
|
import AdminSettings from "./config/AdminSettings.svelte";
|
||||||
|
|
||||||
const { query: initq } = init();
|
// const { query: initq } = init();
|
||||||
|
|
||||||
const ccconfig = getContext("cc-config");
|
|
||||||
|
|
||||||
export let isAdmin;
|
export let isAdmin;
|
||||||
|
export let isApi;
|
||||||
|
export let username;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if isAdmin == true}
|
{#if isAdmin == true}
|
||||||
@ -24,7 +23,7 @@
|
|||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle class="mb-1">Plotting Options</CardTitle>
|
<CardTitle class="mb-1">User Options</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<PlotSettings config={ccconfig} />
|
<UserSettings {username} {isApi}/>
|
||||||
</Card>
|
</Card>
|
||||||
|
@ -4,7 +4,9 @@ import Config from './Config.root.svelte'
|
|||||||
new Config({
|
new Config({
|
||||||
target: document.getElementById('svelte-app'),
|
target: document.getElementById('svelte-app'),
|
||||||
props: {
|
props: {
|
||||||
isAdmin: isAdmin
|
isAdmin: isAdmin,
|
||||||
|
isApi: isApi,
|
||||||
|
username: username
|
||||||
},
|
},
|
||||||
context: new Map([
|
context: new Map([
|
||||||
['cc-config', clusterCockpitConfig]
|
['cc-config', clusterCockpitConfig]
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
let roles = [];
|
let roles = [];
|
||||||
|
|
||||||
function getUserList() {
|
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((res) => res.json())
|
||||||
.then((usersRaw) => {
|
.then((usersRaw) => {
|
||||||
users = usersRaw;
|
users = usersRaw;
|
||||||
@ -19,7 +19,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getValidRoles() {
|
function getValidRoles() {
|
||||||
fetch("/api/roles/")
|
fetch("/config/roles/")
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then((rolesRaw) => {
|
.then((rolesRaw) => {
|
||||||
roles = 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
|
<form
|
||||||
id="create-user-form"
|
id="create-user-form"
|
||||||
method="post"
|
method="post"
|
||||||
action="/api/users/"
|
action="/config/users/"
|
||||||
class="card-body"
|
class="card-body"
|
||||||
on:submit|preventDefault={handleUserSubmit}
|
on:submit|preventDefault={handleUserSubmit}
|
||||||
>
|
>
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
formData.append("add-project", project);
|
formData.append("add-project", project);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/user/${username}`, {
|
const res = await fetch(`/config/user/${username}`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: formData,
|
body: formData,
|
||||||
});
|
});
|
||||||
@ -54,7 +54,7 @@
|
|||||||
formData.append("remove-project", project);
|
formData.append("remove-project", project);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/user/${username}`, {
|
const res = await fetch(`/config/user/${username}`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: formData,
|
body: formData,
|
||||||
});
|
});
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
formData.append("add-role", role);
|
formData.append("add-role", role);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/user/${username}`, {
|
const res = await fetch(`/config/user/${username}`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: formData,
|
body: formData,
|
||||||
});
|
});
|
||||||
@ -56,7 +56,7 @@
|
|||||||
formData.append("remove-role", role);
|
formData.append("remove-role", role);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/user/${username}`, {
|
const res = await fetch(`/config/user/${username}`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: formData,
|
body: formData,
|
||||||
});
|
});
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
if (confirm("Are you sure?")) {
|
if (confirm("Are you sure?")) {
|
||||||
let formData = new FormData();
|
let formData = new FormData();
|
||||||
formData.append("username", username);
|
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) {
|
if (res.status == 200) {
|
||||||
reloadUserList();
|
reloadUserList();
|
||||||
} else {
|
} else {
|
||||||
|
@ -1,18 +1,17 @@
|
|||||||
<script>
|
<script>
|
||||||
import { Button } from "@sveltestrap/sveltestrap";
|
import { Button } from "@sveltestrap/sveltestrap";
|
||||||
|
import { fetchJwt } from "../../utils.js"
|
||||||
|
|
||||||
export let user;
|
export let user;
|
||||||
let jwt = "";
|
|
||||||
|
|
||||||
|
let jwt = "";
|
||||||
function getUserJwt(username) {
|
function getUserJwt(username) {
|
||||||
fetch(`/api/jwt/?username=${username}`)
|
const p = fetchJwt(username);
|
||||||
.then((res) => res.text())
|
p.then((content) => {
|
||||||
.then((text) => {
|
jwt = content
|
||||||
jwt = text;
|
}).catch((error) => {
|
||||||
navigator.clipboard
|
console.error(`Could not get JWT: ${error}`);
|
||||||
.writeText(text)
|
});
|
||||||
.catch((reason) => console.error(reason));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
</script>
|
</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 {
|
try {
|
||||||
let res = await fetch(
|
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
|
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
|
// https://stackoverflow.com/questions/45309447/calculating-median-javascript
|
||||||
// function median(numbers) {
|
// function median(numbers) {
|
||||||
// const sorted = Array.from(numbers).sort((a, b) => a - b);
|
// const sorted = Array.from(numbers).sort((a, b) => a - b);
|
||||||
|
@ -8,6 +8,8 @@
|
|||||||
{{define "javascript"}}
|
{{define "javascript"}}
|
||||||
<script>
|
<script>
|
||||||
const isAdmin = {{ .User.HasRole .Roles.admin }};
|
const isAdmin = {{ .User.HasRole .Roles.admin }};
|
||||||
|
const isApi = {{ .User.HasRole .Roles.api }};
|
||||||
|
const username = {{ .User.Username }};
|
||||||
const filterPresets = {{ .FilterPresets }};
|
const filterPresets = {{ .FilterPresets }};
|
||||||
const clusterCockpitConfig = {{ .Config }};
|
const clusterCockpitConfig = {{ .Config }};
|
||||||
</script>
|
</script>
|
||||||
|
Loading…
Reference in New Issue
Block a user