Merge pull request #159 from ClusterCockpit/158_fix_searchbar

158 fix searchbar
This commit is contained in:
Jan Eitzinger 2023-06-23 10:31:19 +02:00 committed by GitHub
commit 76d3018b5c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 73 additions and 61 deletions

View File

@ -275,7 +275,8 @@ func main() {
rw.WriteHeader(http.StatusUnauthorized) rw.WriteHeader(http.StatusUnauthorized)
web.RenderTemplate(rw, r, "login.tmpl", &web.Page{ web.RenderTemplate(rw, r, "login.tmpl", &web.Page{
Title: "Login failed - ClusterCockpit", Title: "Login failed - ClusterCockpit",
Error: err.Error(), MsgType: "alert-warning",
Message: err.Error(),
Build: buildInfo, Build: buildInfo,
}) })
})).Methods(http.MethodPost) })).Methods(http.MethodPost)
@ -285,7 +286,8 @@ func main() {
rw.WriteHeader(http.StatusOK) rw.WriteHeader(http.StatusOK)
web.RenderTemplate(rw, r, "login.tmpl", &web.Page{ web.RenderTemplate(rw, r, "login.tmpl", &web.Page{
Title: "Bye - ClusterCockpit", Title: "Bye - ClusterCockpit",
Info: "Logout sucessful", MsgType: "alert-info",
Message: "Logout successful",
Build: buildInfo, Build: buildInfo,
}) })
}))).Methods(http.MethodPost) }))).Methods(http.MethodPost)
@ -300,7 +302,8 @@ func main() {
rw.WriteHeader(http.StatusUnauthorized) rw.WriteHeader(http.StatusUnauthorized)
web.RenderTemplate(rw, r, "login.tmpl", &web.Page{ web.RenderTemplate(rw, r, "login.tmpl", &web.Page{
Title: "Authentication failed - ClusterCockpit", Title: "Authentication failed - ClusterCockpit",
Error: err.Error(), MsgType: "alert-danger",
Message: err.Error(),
Build: buildInfo, Build: buildInfo,
}) })
}) })
@ -316,11 +319,11 @@ func main() {
// Send a searchId and then reply with a redirect to a user, or directly send query to job table for jobid and project. // Send a searchId and then reply with a redirect to a user, or directly send query to job table for jobid and project.
secured.HandleFunc("/search", func(rw http.ResponseWriter, r *http.Request) { secured.HandleFunc("/search", func(rw http.ResponseWriter, r *http.Request) {
routerConfig.HandleSearchBar(rw, r, api) routerConfig.HandleSearchBar(rw, r, buildInfo)
}) })
// Mount all /monitoring/... and /api/... routes. // Mount all /monitoring/... and /api/... routes.
routerConfig.SetupRoutes(secured, version, commit, date) routerConfig.SetupRoutes(secured, buildInfo)
api.MountRoutes(secured) api.MountRoutes(secured)
if config.Keys.EmbedStaticFiles { if config.Keys.EmbedStaticFiles {

View File

@ -20,11 +20,11 @@
* JobName: Job-Table (Allows multiple identical matches, e.g. JobNames from different clusters) * JobName: Job-Table (Allows multiple identical matches, e.g. JobNames from different clusters)
* ProjectId: Job-Table * ProjectId: Job-Table
* Username: Users-Table * Username: Users-Table
* **Please Note**: Only users with jobs will be shown in table! I.e., Users without jobs will be missing in table. * **Please Note**: Only users with jobs will be shown in table! I.e., Users without jobs will be missing in table. Also, a `Last 30 Days` is active by default and might filter out expected users.
* Name: Users-Table * Name: Users-Table
* **Please Note**: Only users with jobs will be shown in table! I.e., Users without jobs will be missing in table. * **Please Note**: Only users with jobs will be shown in table! I.e., Users without jobs will be missing in table. Also, a `Last 30 Days` is active by default and might filter out expected users.
* Best guess search always redirects to Job-Table or `/monitoring/user/$USER` (first username match) * Best guess search always redirects to Job-Table or `/monitoring/user/$USER` (first username match)
* Unprocessable queries will redirect to `/monitoring/jobs/?` * Unprocessable queries will display messages detailing the cause (Info, Warning, Error)
* Spaces trimmed (both for searchTag and queryString) * Spaces trimmed (both for searchTag and queryString)
* ` job12` == `job12` * ` job12` == `job12`
* `projectID : abcd ` == `projectId:abcd` * `projectID : abcd ` == `projectId:abcd`

View File

@ -524,11 +524,10 @@ var ErrForbidden = errors.New("not authorized")
// If query is found to be an integer (= conversion to INT datatype succeeds), skip back to parent call // If query is found to be an integer (= conversion to INT datatype succeeds), skip back to parent call
// If nothing matches the search, `ErrNotFound` is returned. // If nothing matches the search, `ErrNotFound` is returned.
func (r *JobRepository) FindUserOrProjectOrJobname(ctx context.Context, searchterm string) (username string, project string, metasnip string, err error) { func (r *JobRepository) FindUserOrProjectOrJobname(user *auth.User, searchterm string) (username string, project string, metasnip string, err error) {
if _, err := strconv.Atoi(searchterm); err == nil { // Return empty on successful conversion: parent method will redirect for integer jobId if _, err := strconv.Atoi(searchterm); err == nil { // Return empty on successful conversion: parent method will redirect for integer jobId
return "", "", "", nil return "", "", "", nil
} else { // Has to have letters and logged-in user for other guesses } else { // Has to have letters and logged-in user for other guesses
user := auth.GetUser(ctx)
if user != nil { if user != nil {
// Find username in jobs (match) // Find username in jobs (match)
uresult, _ := r.FindColumnValue(user, searchterm, "job", "user", "user", false) uresult, _ := r.FindColumnValue(user, searchterm, "job", "user", "user", false)

View File

@ -12,7 +12,6 @@ import (
"strings" "strings"
"time" "time"
"github.com/ClusterCockpit/cc-backend/internal/api"
"github.com/ClusterCockpit/cc-backend/internal/auth" "github.com/ClusterCockpit/cc-backend/internal/auth"
"github.com/ClusterCockpit/cc-backend/internal/graph/model" "github.com/ClusterCockpit/cc-backend/internal/graph/model"
"github.com/ClusterCockpit/cc-backend/internal/repository" "github.com/ClusterCockpit/cc-backend/internal/repository"
@ -230,7 +229,7 @@ func buildFilterPresets(query url.Values) map[string]interface{} {
return filterPresets return filterPresets
} }
func SetupRoutes(router *mux.Router, version string, hash string, buildTime string) { func SetupRoutes(router *mux.Router, buildInfo web.Build) {
userCfgRepo := repository.GetUserCfgRepo() userCfgRepo := repository.GetUserCfgRepo()
for _, route := range routes { for _, route := range routes {
route := route route := route
@ -256,7 +255,7 @@ func SetupRoutes(router *mux.Router, version string, hash string, buildTime stri
Title: title, Title: title,
User: *user, User: *user,
Roles: availableRoles, Roles: availableRoles,
Build: web.Build{Version: version, Hash: hash, Buildtime: buildTime}, Build: buildInfo,
Config: conf, Config: conf,
Infos: infos, Infos: infos,
} }
@ -270,66 +269,64 @@ func SetupRoutes(router *mux.Router, version string, hash string, buildTime stri
} }
} }
func HandleSearchBar(rw http.ResponseWriter, r *http.Request, api *api.RestApi) { func HandleSearchBar(rw http.ResponseWriter, r *http.Request, buildInfo web.Build) {
if search := r.URL.Query().Get("searchId"); search != "" {
user := auth.GetUser(r.Context()) user := auth.GetUser(r.Context())
availableRoles, _ := auth.GetValidRolesMap(user)
if search := r.URL.Query().Get("searchId"); search != "" {
repo := repository.GetJobRepository()
splitSearch := strings.Split(search, ":") splitSearch := strings.Split(search, ":")
if len(splitSearch) == 2 { if len(splitSearch) == 2 {
switch strings.Trim(splitSearch[0], " ") { switch strings.Trim(splitSearch[0], " ") {
case "jobId": case "jobId":
http.Redirect(rw, r, "/monitoring/jobs/?jobId="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusTemporaryRedirect) // All Users: Redirect to Tablequery http.Redirect(rw, r, "/monitoring/jobs/?jobId="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusFound) // All Users: Redirect to Tablequery
case "jobName": case "jobName":
http.Redirect(rw, r, "/monitoring/jobs/?jobName="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusTemporaryRedirect) // All Users: Redirect to Tablequery http.Redirect(rw, r, "/monitoring/jobs/?jobName="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusFound) // All Users: Redirect to Tablequery
case "projectId": case "projectId":
http.Redirect(rw, r, "/monitoring/jobs/?projectMatch=eq&project="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusTemporaryRedirect) // All Users: Redirect to Tablequery http.Redirect(rw, r, "/monitoring/jobs/?projectMatch=eq&project="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusFound) // All Users: Redirect to Tablequery
case "username": case "username":
if user.HasAnyRole([]auth.Role{auth.RoleAdmin, auth.RoleSupport, auth.RoleManager}) { if user.HasAnyRole([]auth.Role{auth.RoleAdmin, auth.RoleSupport, auth.RoleManager}) {
http.Redirect(rw, r, "/monitoring/users/?user="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusTemporaryRedirect) http.Redirect(rw, r, "/monitoring/users/?user="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusFound)
} else { } else {
http.Redirect(rw, r, "/monitoring/jobs/?", http.StatusTemporaryRedirect) // Users: Redirect to Tablequery web.RenderTemplate(rw, r, "message.tmpl", &web.Page{Title: "Error", MsgType: "alert-danger", Message: "Missing Access Rights", User: *user, Roles: availableRoles, Build: buildInfo})
} }
case "name": case "name":
usernames, _ := api.JobRepository.FindColumnValues(user, strings.Trim(splitSearch[1], " "), "user", "username", "name") usernames, _ := repo.FindColumnValues(user, strings.Trim(splitSearch[1], " "), "user", "username", "name")
if len(usernames) != 0 { if len(usernames) != 0 {
joinedNames := strings.Join(usernames, "&user=") joinedNames := strings.Join(usernames, "&user=")
http.Redirect(rw, r, "/monitoring/users/?user="+joinedNames, http.StatusTemporaryRedirect) http.Redirect(rw, r, "/monitoring/users/?user="+joinedNames, http.StatusFound)
} else { } else {
if user.HasAnyRole([]auth.Role{auth.RoleAdmin, auth.RoleSupport, auth.RoleManager}) { if user.HasAnyRole([]auth.Role{auth.RoleAdmin, auth.RoleSupport, auth.RoleManager}) {
http.Redirect(rw, r, "/monitoring/users/?user=NoUserNameFound", http.StatusTemporaryRedirect) http.Redirect(rw, r, "/monitoring/users/?user=NoUserNameFound", http.StatusPermanentRedirect)
} else { } else {
http.Redirect(rw, r, "/monitoring/jobs/?", http.StatusTemporaryRedirect) // Users: Redirect to Tablequery web.RenderTemplate(rw, r, "message.tmpl", &web.Page{Title: "Error", MsgType: "alert-danger", Message: "Missing Access Rights", User: *user, Roles: availableRoles, Build: buildInfo})
} }
} }
default: default:
log.Warnf("Searchbar type parameter '%s' unknown", strings.Trim(splitSearch[0], " ")) web.RenderTemplate(rw, r, "message.tmpl", &web.Page{Title: "Warning", MsgType: "alert-warning", Message: fmt.Sprintf("Unknown search type: %s", strings.Trim(splitSearch[0], " ")), User: *user, Roles: availableRoles, Build: buildInfo})
http.Redirect(rw, r, "/monitoring/jobs/?", http.StatusTemporaryRedirect) // Unknown: Redirect to Tablequery
} }
} else if len(splitSearch) == 1 { } else if len(splitSearch) == 1 {
username, project, jobname, err := api.JobRepository.FindUserOrProjectOrJobname(r.Context(), strings.Trim(search, " "))
username, project, jobname, err := repo.FindUserOrProjectOrJobname(user, strings.Trim(search, " "))
if err != nil { if err != nil {
log.Errorf("Error while searchbar best guess: %v", err.Error()) web.RenderTemplate(rw, r, "message.tmpl", &web.Page{Title: "Info", MsgType: "alert-info", Message: "Search without result", User: *user, Roles: availableRoles, Build: buildInfo})
http.Redirect(rw, r, "/monitoring/jobs/?", http.StatusTemporaryRedirect) // Unknown: Redirect to Tablequery return
} }
if username != "" { if username != "" {
http.Redirect(rw, r, "/monitoring/user/"+username, http.StatusTemporaryRedirect) // User: Redirect to user page http.Redirect(rw, r, "/monitoring/user/"+username, http.StatusFound) // User: Redirect to user page
} else if project != "" { } else if project != "" {
http.Redirect(rw, r, "/monitoring/jobs/?projectMatch=eq&project="+url.QueryEscape(strings.Trim(search, " ")), http.StatusTemporaryRedirect) // projectId (equal) http.Redirect(rw, r, "/monitoring/jobs/?projectMatch=eq&project="+url.QueryEscape(strings.Trim(search, " ")), http.StatusFound) // projectId (equal)
} else if jobname != "" { } else if jobname != "" {
http.Redirect(rw, r, "/monitoring/jobs/?jobName="+url.QueryEscape(strings.Trim(search, " ")), http.StatusTemporaryRedirect) // JobName (contains) http.Redirect(rw, r, "/monitoring/jobs/?jobName="+url.QueryEscape(strings.Trim(search, " ")), http.StatusFound) // JobName (contains)
} else { } else {
http.Redirect(rw, r, "/monitoring/jobs/?jobId="+url.QueryEscape(strings.Trim(search, " ")), http.StatusTemporaryRedirect) // No Result: Probably jobId http.Redirect(rw, r, "/monitoring/jobs/?jobId="+url.QueryEscape(strings.Trim(search, " ")), http.StatusFound) // No Result: Probably jobId
} }
} else { } else {
log.Warnf("Searchbar query parameters malformed: %v", search) web.RenderTemplate(rw, r, "message.tmpl", &web.Page{Title: "Error", MsgType: "alert-danger", Message: "Searchbar query parameters malformed", User: *user, Roles: availableRoles, Build: buildInfo})
http.Redirect(rw, r, "/monitoring/jobs/?", http.StatusTemporaryRedirect) // Unknown: Redirect to Tablequery
} }
} else { } else {
http.Redirect(rw, r, "/monitoring/jobs/?", http.StatusTemporaryRedirect) web.RenderTemplate(rw, r, "message.tmpl", &web.Page{Title: "Warning", MsgType: "alert-warning", Message: "Empty search", User: *user, Roles: availableRoles, Build: buildInfo})
} }
} }

View File

@ -270,6 +270,7 @@
} }
export function findThresholds(metricConfig, scope, subCluster) { export function findThresholds(metricConfig, scope, subCluster) {
// console.log('NAME ' + metricConfig.name + ' / SCOPE ' + scope + ' / SUBCLUSTER ' + subCluster.name)
if (!metricConfig || !scope || !subCluster) { if (!metricConfig || !scope || !subCluster) {
console.warn('Argument missing for findThresholds!') console.warn('Argument missing for findThresholds!')
return null return null
@ -280,8 +281,10 @@
// console.log('subClusterConfigs array empty, use metricConfig defaults') // console.log('subClusterConfigs array empty, use metricConfig defaults')
return { normal: metricConfig.normal, caution: metricConfig.caution, alert: metricConfig.alert } return { normal: metricConfig.normal, caution: metricConfig.caution, alert: metricConfig.alert }
} else if (metricConfig.subClusters && metricConfig.subClusters.length > 0) { } else if (metricConfig.subClusters && metricConfig.subClusters.length > 0) {
// console.log('subClusterConfigs found, find and use subCluster Settings') // console.log('subClusterConfigs found, use subCluster Settings if matching jobs subcluster:')
return metricConfig.subClusters.find(sc => sc.name == subCluster.name) let forSubCluster = metricConfig.subClusters.find(sc => sc.name == subCluster.name)
if (forSubCluster && forSubCluster.normal && forSubCluster.caution && forSubCluster.alert) return forSubCluster
else return { normal: metricConfig.normal, caution: metricConfig.caution, alert: metricConfig.alert }
} else { } else {
console.warn('metricConfig.subClusters not found!') console.warn('metricConfig.subClusters not found!')
return null return null

View File

@ -17,15 +17,9 @@
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-4 mx-auto"> <div class="col-4 mx-auto">
{{if .Error}} {{if .MsgType}}
<div class="alert alert-warning" role="alert"> <div class="alert {{.MsgType}}" role="alert">
{{.Error}} {{.Message}}
</div>
{{end}}
{{if .Info}}
<div class="alert alert-success" role="alert">
{{.Info}}
</div> </div>
{{end}} {{end}}

View File

@ -0,0 +1,17 @@
{{define "content"}}
<div class="row justify-content-center">
<div class="col-4">
<div class="alert {{.MsgType}} p-3 text-center fs-3" role="alert">
{{if eq .MsgType "alert-info"}}
<i class="bi-info-circle-fill me-3"></i>
{{else if eq .MsgType "alert-warning"}}
<i class="bi-question-circle-fill me-3"></i>
{{else if eq .MsgType "alert-danger"}}
<i class="bi-exclamation-circle-fill me-3"></i>
{{end}}
{{.Message}}
</div>
</div>
</div>
{{end}}

View File

@ -82,8 +82,8 @@ type Build struct {
type Page struct { type Page struct {
Title string // Page title Title string // Page title
Error string // For generic use (e.g. the exact error message on /login) MsgType string // For generic use in message boxes
Info string // For generic use (e.g. "Logout successfull" on /login) Message string // For generic use in message boxes
User auth.User // Information about the currently logged in user (Full User Info) User auth.User // Information about the currently logged in user (Full User Info)
Roles map[string]auth.Role // Available roles for frontend render checks Roles map[string]auth.Role // Available roles for frontend render checks
Build Build // Latest information about the application Build Build // Latest information about the application
@ -96,8 +96,7 @@ type Page struct {
func RenderTemplate(rw http.ResponseWriter, r *http.Request, file string, page *Page) { func RenderTemplate(rw http.ResponseWriter, r *http.Request, file string, page *Page) {
t, ok := templates[file] t, ok := templates[file]
if !ok { if !ok {
log.Fatalf("WEB/WEB > template '%s' not found", file) log.Errorf("WEB/WEB > template '%s' not found", file)
panic("template not found")
} }
if page.Clusters == nil { if page.Clusters == nil {
@ -106,7 +105,7 @@ func RenderTemplate(rw http.ResponseWriter, r *http.Request, file string, page *
} }
} }
log.Infof("Page config : %v\n", page.Config) log.Debugf("Page config : %v\n", page.Config)
if err := t.Execute(rw, page); err != nil { if err := t.Execute(rw, page); err != nil {
log.Errorf("Template error: %s", err.Error()) log.Errorf("Template error: %s", err.Error())
} }