Merge branch 'master' into sql-repository-opt

This commit is contained in:
Jan Eitzinger 2023-02-21 16:21:47 +01:00 committed by GitHub
commit 33b20620ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 270 additions and 24 deletions

View File

@ -236,6 +236,7 @@ input StringInput {
contains: String contains: String
startsWith: String startsWith: String
endsWith: String endsWith: String
in: [String!]
} }
input IntRange { from: Int!, to: Int! } input IntRange { from: Int!, to: Int! }
@ -256,6 +257,7 @@ type HistoPoint {
type JobsStatistics { type JobsStatistics {
id: ID! # If `groupBy` was used, ID of the user/project/cluster id: ID! # If `groupBy` was used, ID of the user/project/cluster
name: String # if User-Statistics: Given Name of Account (ID) Owner
totalJobs: Int! # Number of jobs that matched totalJobs: Int! # Number of jobs that matched
shortJobs: Int! # Number of jobs with a duration of less than 2 minutes shortJobs: Int! # Number of jobs with a duration of less than 2 minutes
totalWalltime: Int! # Sum of the duration of all matched jobs in hours totalWalltime: Int! # Sum of the duration of all matched jobs in hours

View File

@ -3,14 +3,31 @@
### Usage ### Usage
* Searchtags are implemented as `type:<query>` search-string * Searchtags are implemented as `type:<query>` search-string
* Types `jobId, jobName, projectId, username` for roles `admin` and `support` * Types `jobId, jobName, projectId, username, name` for roles `admin` and `support`
* `jobName` is jobName as persisted in `job.meta_data` table-column
* `username` is actual account identifier as persisted in `job.user` table-column
* `name` is account owners name as persisted in `user.name` table-column
* Types `jobId, jobName` for role `user` * Types `jobId, jobName` for role `user`
* Examples: * Examples:
* `jobName:myJob12` * `jobName:myJob12`
* `jobId:123456` * `jobId:123456`
* `username:abcd100` * `username:abcd100`
* `name:Paul`
* If no searchTag used: Best guess search with the following hierarchy * If no searchTag used: Best guess search with the following hierarchy
* `jobId -> username -> projectId -> jobName` * `jobId -> username -> name -> projectId -> jobName`
* Destinations:
* JobId: Always Job-Table (Allows multiple identical matches, e.g. JobIds from different clusters)
* JobName: Always Job-Table (Allows multiple identical matches, e.g. JobNames from different clusters)
* ProjectId: Always Job-Table
* Username
* If *one* match found: Opens detailed user-view (`/monitoring/user/$USER`)
* If *multiple* matches found: Opens user-table with matches listed (`/monitoring/users/`)
* **Please Note**: Only users with jobs will be shown in table! I.e., "multiple matches" can still be only one entry in table.
* Name
* If *one* matching username found: Opens detailed user-view (`/monitoring/user/$USER`)
* If *multiple* usernames found: Opens user-table with matches listed (`/monitoring/users/`)
* **Please Note**: Only users with jobs will be shown in table! I.e., "multiple matches" can still be only one entry in table.
* Best guess search always redirects to Job-Table or `/monitoring/user/$USER` (first username match)
* Simple HTML Error if ... * Simple HTML Error if ...
* Best guess search fails -> 'Not Found' * Best guess search fails -> 'Not Found'
* Query `type` is unknown * Query `type` is unknown
@ -18,7 +35,8 @@
* 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`
* jobId-Query now redirects to table * `jobName`- and `name-`queries work with a part of the target-string
* Allows multiple jobs from different systems, but with identical job-id to be found * `jobName:myjob` for jobName "myjob_cluster1"
* jobName-Query works with a part of the jobName-String (e.g. jobName:myjob for jobName myjob_cluster1) * `name:Paul` for name "Paul Atreides"
* JobName GQL Query is resolved as matching the query as a part of the whole metaData-JSON in the SQL DB. * JobName GQL Query is resolved as matching the query as a part of the whole metaData-JSON in the SQL DB.

4
go.mod
View File

@ -15,6 +15,8 @@ require (
github.com/influxdata/influxdb-client-go/v2 v2.10.0 github.com/influxdata/influxdb-client-go/v2 v2.10.0
github.com/jmoiron/sqlx v1.3.5 github.com/jmoiron/sqlx v1.3.5
github.com/mattn/go-sqlite3 v1.14.15 github.com/mattn/go-sqlite3 v1.14.15
github.com/prometheus/client_golang v1.14.0
github.com/prometheus/common v0.37.0
github.com/santhosh-tekuri/jsonschema/v5 v5.0.0 github.com/santhosh-tekuri/jsonschema/v5 v5.0.0
github.com/swaggo/http-swagger v1.3.3 github.com/swaggo/http-swagger v1.3.3
github.com/swaggo/swag v1.8.5 github.com/swaggo/swag v1.8.5
@ -58,9 +60,7 @@ require (
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_golang v1.14.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect
github.com/qustavo/sqlhooks/v2 v2.1.0 // indirect github.com/qustavo/sqlhooks/v2 v2.1.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect

4
go.sum
View File

@ -622,6 +622,8 @@ github.com/google/go-containerregistry v0.5.1/go.mod h1:Ct15B4yir3PLOP5jsy0GNeYV
github.com/google/go-github/v39 v39.2.0/go.mod h1:C1s8C5aCC9L+JXIYpJM5GYytdX52vC1bLvHEF1IhBrE= github.com/google/go-github/v39 v39.2.0/go.mod h1:C1s8C5aCC9L+JXIYpJM5GYytdX52vC1bLvHEF1IhBrE=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@ -1503,6 +1505,7 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180224232135-f6cff0780e54/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180224232135-f6cff0780e54/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -1743,6 +1746,7 @@ golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20= golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20=
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=

View File

@ -131,6 +131,7 @@ type ComplexityRoot struct {
HistDuration func(childComplexity int) int HistDuration func(childComplexity int) int
HistNumNodes func(childComplexity int) int HistNumNodes func(childComplexity int) int
ID func(childComplexity int) int ID func(childComplexity int) int
Name func(childComplexity int) int
ShortJobs func(childComplexity int) int ShortJobs func(childComplexity int) int
TotalCoreHours func(childComplexity int) int TotalCoreHours func(childComplexity int) int
TotalJobs func(childComplexity int) int TotalJobs func(childComplexity int) int
@ -671,6 +672,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.JobsStatistics.ID(childComplexity), true return e.complexity.JobsStatistics.ID(childComplexity), true
case "JobsStatistics.name":
if e.complexity.JobsStatistics.Name == nil {
break
}
return e.complexity.JobsStatistics.Name(childComplexity), true
case "JobsStatistics.shortJobs": case "JobsStatistics.shortJobs":
if e.complexity.JobsStatistics.ShortJobs == nil { if e.complexity.JobsStatistics.ShortJobs == nil {
break break
@ -1619,6 +1627,7 @@ input StringInput {
contains: String contains: String
startsWith: String startsWith: String
endsWith: String endsWith: String
in: [String!]
} }
input IntRange { from: Int!, to: Int! } input IntRange { from: Int!, to: Int! }
@ -1639,6 +1648,7 @@ type HistoPoint {
type JobsStatistics { type JobsStatistics {
id: ID! # If ` + "`" + `groupBy` + "`" + ` was used, ID of the user/project/cluster id: ID! # If ` + "`" + `groupBy` + "`" + ` was used, ID of the user/project/cluster
name: String # if User-Statistics: Given Name of Account (ID) Owner
totalJobs: Int! # Number of jobs that matched totalJobs: Int! # Number of jobs that matched
shortJobs: Int! # Number of jobs with a duration of less than 2 minutes shortJobs: Int! # Number of jobs with a duration of less than 2 minutes
totalWalltime: Int! # Sum of the duration of all matched jobs in hours totalWalltime: Int! # Sum of the duration of all matched jobs in hours
@ -4485,6 +4495,47 @@ func (ec *executionContext) fieldContext_JobsStatistics_id(ctx context.Context,
return fc, nil return fc, nil
} }
func (ec *executionContext) _JobsStatistics_name(ctx context.Context, field graphql.CollectedField, obj *model.JobsStatistics) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_JobsStatistics_name(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Name, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.(*string)
fc.Result = res
return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_JobsStatistics_name(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "JobsStatistics",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type String does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _JobsStatistics_totalJobs(ctx context.Context, field graphql.CollectedField, obj *model.JobsStatistics) (ret graphql.Marshaler) { func (ec *executionContext) _JobsStatistics_totalJobs(ctx context.Context, field graphql.CollectedField, obj *model.JobsStatistics) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_JobsStatistics_totalJobs(ctx, field) fc, err := ec.fieldContext_JobsStatistics_totalJobs(ctx, field)
if err != nil { if err != nil {
@ -6401,6 +6452,8 @@ func (ec *executionContext) fieldContext_Query_jobsStatistics(ctx context.Contex
switch field.Name { switch field.Name {
case "id": case "id":
return ec.fieldContext_JobsStatistics_id(ctx, field) return ec.fieldContext_JobsStatistics_id(ctx, field)
case "name":
return ec.fieldContext_JobsStatistics_name(ctx, field)
case "totalJobs": case "totalJobs":
return ec.fieldContext_JobsStatistics_totalJobs(ctx, field) return ec.fieldContext_JobsStatistics_totalJobs(ctx, field)
case "shortJobs": case "shortJobs":
@ -10640,7 +10693,7 @@ func (ec *executionContext) unmarshalInputStringInput(ctx context.Context, obj i
asMap[k] = v asMap[k] = v
} }
fieldsInOrder := [...]string{"eq", "contains", "startsWith", "endsWith"} fieldsInOrder := [...]string{"eq", "contains", "startsWith", "endsWith", "in"}
for _, k := range fieldsInOrder { for _, k := range fieldsInOrder {
v, ok := asMap[k] v, ok := asMap[k]
if !ok { if !ok {
@ -10679,6 +10732,14 @@ func (ec *executionContext) unmarshalInputStringInput(ctx context.Context, obj i
if err != nil { if err != nil {
return it, err return it, err
} }
case "in":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("in"))
it.In, err = ec.unmarshalOString2ᚕstringᚄ(ctx, v)
if err != nil {
return it, err
}
} }
} }
@ -11340,6 +11401,10 @@ func (ec *executionContext) _JobsStatistics(ctx context.Context, sel ast.Selecti
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
invalids++ invalids++
} }
case "name":
out.Values[i] = ec._JobsStatistics_name(ctx, field, obj)
case "totalJobs": case "totalJobs":
out.Values[i] = ec._JobsStatistics_totalJobs(ctx, field, obj) out.Values[i] = ec._JobsStatistics_totalJobs(ctx, field, obj)

View File

@ -72,6 +72,7 @@ type JobResultList struct {
type JobsStatistics struct { type JobsStatistics struct {
ID string `json:"id"` ID string `json:"id"`
Name *string `json:"name"`
TotalJobs int `json:"totalJobs"` TotalJobs int `json:"totalJobs"`
ShortJobs int `json:"shortJobs"` ShortJobs int `json:"shortJobs"`
TotalWalltime int `json:"totalWalltime"` TotalWalltime int `json:"totalWalltime"`
@ -106,6 +107,7 @@ type StringInput struct {
Contains *string `json:"contains"` Contains *string `json:"contains"`
StartsWith *string `json:"startsWith"` StartsWith *string `json:"startsWith"`
EndsWith *string `json:"endsWith"` EndsWith *string `json:"endsWith"`
In []string `json:"in"`
} }
type TimeRangeOutput struct { type TimeRangeOutput struct {

View File

@ -49,6 +49,7 @@ func GetJobRepository() *JobRepository {
jobRepoInstance = &JobRepository{ jobRepoInstance = &JobRepository{
DB: db.DB, DB: db.DB,
driver: db.Driver, driver: db.Driver,
stmtCache: sq.NewStmtCache(db.DB), stmtCache: sq.NewStmtCache(db.DB),
cache: lrucache.New(1024 * 1024), cache: lrucache.New(1024 * 1024),
archiveChannel: make(chan *schema.Job, 128), archiveChannel: make(chan *schema.Job, 128),
@ -501,6 +502,17 @@ func (r *JobRepository) FindJobnameOrUserOrProject(ctx context.Context, searchte
} else if err == nil { } else if err == nil {
return "", username, "", nil return "", username, "", nil
} }
if username == "" { // Try with Name2Username query
errtwo := sq.Select("user.username").Distinct().From("user").
Where("user.name LIKE ?", fmt.Sprint("%"+searchterm+"%")).
RunWith(r.stmtCache).QueryRow().Scan(&username)
if errtwo != nil && errtwo != sql.ErrNoRows {
return "", "", "", errtwo
} else if errtwo == nil {
return "", username, "", nil
}
}
} }
if user == nil || user.HasRole(auth.RoleAdmin) || user.HasRole(auth.RoleSupport) { if user == nil || user.HasRole(auth.RoleAdmin) || user.HasRole(auth.RoleSupport) {
@ -542,7 +554,105 @@ func (r *JobRepository) FindUser(ctx context.Context, searchterm string) (userna
return "", ErrNotFound return "", ErrNotFound
} else { } else {
log.Infof("Non-Admin User %s : Requested Query Username -> %s: Forbidden", user.Name, username) log.Infof("Non-Admin User %s : Requested Query Username -> %s: Forbidden", user.Name, searchterm)
return "", ErrForbidden
}
}
func (r *JobRepository) FindUserByName(ctx context.Context, searchterm string) (username string, err error) {
user := auth.GetUser(ctx)
if user == nil || user.HasRole(auth.RoleAdmin) || user.HasRole(auth.RoleSupport) {
err := sq.Select("user.username").Distinct().From("user").
Where("user.name = ?", searchterm).
RunWith(r.stmtCache).QueryRow().Scan(&username)
if err != nil && err != sql.ErrNoRows {
return "", err
} else if err == nil {
return username, nil
}
return "", ErrNotFound
} else {
log.Infof("Non-Admin User %s : Requested Query Name -> %s: Forbidden", user.Name, searchterm)
return "", ErrForbidden
}
}
func (r *JobRepository) FindUsers(ctx context.Context, searchterm string) (usernames []string, err error) {
user := auth.GetUser(ctx)
emptyResult := make([]string, 0)
if user == nil || user.HasRole(auth.RoleAdmin) || user.HasRole(auth.RoleSupport) {
rows, err := sq.Select("job.user").Distinct().From("job").
Where("job.user LIKE ?", fmt.Sprint("%", searchterm, "%")).
RunWith(r.stmtCache).Query()
if err != nil && err != sql.ErrNoRows {
return emptyResult, err
} else if err == nil {
for rows.Next() {
var name string
err := rows.Scan(&name)
if err != nil {
rows.Close()
log.Warnf("Error while scanning rows: %v", err)
return emptyResult, err
}
usernames = append(usernames, name)
}
return usernames, nil
}
return emptyResult, ErrNotFound
} else {
log.Infof("Non-Admin User %s : Requested Query Usernames -> %s: Forbidden", user.Name, searchterm)
return emptyResult, ErrForbidden
}
}
func (r *JobRepository) FindUsersByName(ctx context.Context, searchterm string) (usernames []string, err error) {
user := auth.GetUser(ctx)
emptyResult := make([]string, 0)
if user == nil || user.HasRole(auth.RoleAdmin) || user.HasRole(auth.RoleSupport) {
rows, err := sq.Select("user.username").Distinct().From("user").
Where("user.name LIKE ?", fmt.Sprint("%", searchterm, "%")).
RunWith(r.stmtCache).Query()
if err != nil && err != sql.ErrNoRows {
return emptyResult, err
} else if err == nil {
for rows.Next() {
var username string
err := rows.Scan(&username)
if err != nil {
rows.Close()
log.Warnf("Error while scanning rows: %v", err)
return emptyResult, err
}
usernames = append(usernames, username)
}
return usernames, nil
}
return emptyResult, ErrNotFound
} else {
log.Infof("Non-Admin User %s : Requested Query name -> %s: Forbidden", user.Name, searchterm)
return emptyResult, ErrForbidden
}
}
func (r *JobRepository) FindNameByUser(ctx context.Context, searchterm string) (name string, err error) {
user := auth.GetUser(ctx)
if user == nil || user.HasRole(auth.RoleAdmin) || user.HasRole(auth.RoleSupport) {
err := sq.Select("user.name").Distinct().From("user").
Where("user.username = ?", searchterm).
RunWith(r.stmtCache).QueryRow().Scan(&name)
if err != nil && err != sql.ErrNoRows {
return "", err
} else if err == nil {
return name, nil
}
return "", ErrNotFound
} else {
log.Infof("Non-Admin User %s : Requested Query Name -> %s: Forbidden", user.Name, searchterm)
return "", ErrForbidden return "", ErrForbidden
} }
} }

View File

@ -206,6 +206,13 @@ func buildStringCondition(field string, cond *model.StringInput, query sq.Select
if cond.Contains != nil { if cond.Contains != nil {
return query.Where(field+" LIKE ?", fmt.Sprint("%", *cond.Contains, "%")) return query.Where(field+" LIKE ?", fmt.Sprint("%", *cond.Contains, "%"))
} }
if cond.In != nil {
queryUsers := make([]string, len(cond.In))
for i, val := range cond.In {
queryUsers[i] = val
}
return query.Where(sq.Or{sq.Eq{"job.user": queryUsers}})
}
return query return query
} }

View File

@ -183,9 +183,14 @@ func buildFilterPresets(query url.Values) map[string]interface{} {
if query.Get("jobName") != "" { if query.Get("jobName") != "" {
filterPresets["jobName"] = query.Get("jobName") filterPresets["jobName"] = query.Get("jobName")
} }
if query.Get("user") != "" { if len(query["user"]) != 0 {
if len(query["user"]) == 1 {
filterPresets["user"] = query.Get("user") filterPresets["user"] = query.Get("user")
filterPresets["userMatch"] = "eq" filterPresets["userMatch"] = "contains"
} else {
filterPresets["user"] = query["user"]
filterPresets["userMatch"] = "in"
}
} }
if len(query["state"]) != 0 { if len(query["state"]) != 0 {
filterPresets["state"] = query["state"] filterPresets["state"] = query["state"]
@ -319,12 +324,27 @@ func HandleSearchBar(rw http.ResponseWriter, r *http.Request, api *api.RestApi)
http.Redirect(rw, r, "/monitoring/jobs/?jobId=NotFound", http.StatusTemporaryRedirect) // Workaround to display correctly empty table http.Redirect(rw, r, "/monitoring/jobs/?jobId=NotFound", http.StatusTemporaryRedirect) // Workaround to display correctly empty table
} }
case "username": case "username":
username, _ := api.JobRepository.FindUser(r.Context(), strings.Trim(splitSearch[1], " ")) // Restricted: username usernames, _ := api.JobRepository.FindUsers(r.Context(), strings.Trim(splitSearch[1], " ")) // Restricted: usernames
if username != "" { if len(usernames) == 1 {
http.Redirect(rw, r, "/monitoring/user/"+username, http.StatusTemporaryRedirect) http.Redirect(rw, r, "/monitoring/user/"+usernames[0], http.StatusTemporaryRedirect) // One Match: Redirect to User View
return
} else if len(usernames) > 1 {
http.Redirect(rw, r, "/monitoring/users/?user="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusTemporaryRedirect) // > 1 Matches: Redirect to user table
return return
} else { } else {
http.Redirect(rw, r, "/monitoring/jobs/?jobId=NotFound", http.StatusTemporaryRedirect) // Workaround to display correctly empty table http.Redirect(rw, r, "/monitoring/users/?user=NotFound", http.StatusTemporaryRedirect) // Workaround to display correctly empty table
}
case "name":
usernames, _ := api.JobRepository.FindUsersByName(r.Context(), strings.Trim(splitSearch[1], " ")) // Restricted: usernames queried by name
if len(usernames) == 1 {
http.Redirect(rw, r, "/monitoring/user/"+usernames[0], http.StatusTemporaryRedirect)
return
} else if len(usernames) > 1 {
joinedNames := strings.Join(usernames, "&user=")
http.Redirect(rw, r, "/monitoring/users/?user="+joinedNames, http.StatusTemporaryRedirect) // > 1 Matches: Redirect to user table
return
} else {
http.Redirect(rw, r, "/monitoring/users/?user=NotFound", http.StatusTemporaryRedirect) // Workaround to display correctly empty table
} }
default: default:
http.Error(rw, "'searchId' type parameter unknown", http.StatusBadRequest) http.Error(rw, "'searchId' type parameter unknown", http.StatusBadRequest)

View File

@ -57,7 +57,7 @@
<InputGroup> <InputGroup>
<Input type="text" placeholder="Search 'type:<query>' ..." name="searchId"/> <Input type="text" placeholder="Search 'type:<query>' ..." name="searchId"/>
<Button outline type="submit"><Icon name="search"/></Button> <Button outline type="submit"><Icon name="search"/></Button>
<InputGroupText style="cursor:help;" title={isAdmin ? "Example: 'projectId:a100cd', Types are: jobId | jobName | projectId | username" : "Example: 'jobName:myjob', Types are jobId | jobName"}><Icon name="info-circle"/></InputGroupText> <InputGroupText style="cursor:help;" title={isAdmin ? "Example: 'projectId:a100cd', Types are: jobId | jobName | projectId | username" | "name" : "Example: 'jobName:myjob', Types are jobId | jobName"}><Icon name="info-circle"/></InputGroupText>
</InputGroup> </InputGroup>
</form> </form>
{#if username} {#if username}

View File

@ -20,6 +20,7 @@
const stats = operationStore(`query($filter: [JobFilter!]!) { const stats = operationStore(`query($filter: [JobFilter!]!) {
rows: jobsStatistics(filter: $filter, groupBy: ${type}) { rows: jobsStatistics(filter: $filter, groupBy: ${type}) {
id id
name
totalJobs totalJobs
totalWalltime totalWalltime
totalCoreHours totalCoreHours
@ -93,6 +94,15 @@
<Icon name="sort-numeric-down" /> <Icon name="sort-numeric-down" />
</Button> </Button>
</th> </th>
{#if type == 'USER'}
<th scope="col">
Name
<Button color="{sorting.field == 'name' ? 'primary' : 'light'}"
size="sm" on:click={e => changeSorting(e, 'name')}>
<Icon name="sort-numeric-down" />
</Button>
</th>
{/if}
<th scope="col"> <th scope="col">
Total Jobs Total Jobs
<Button color="{sorting.field == 'totalJobs' ? 'primary' : 'light'}" <Button color="{sorting.field == 'totalJobs' ? 'primary' : 'light'}"
@ -137,6 +147,9 @@
{row.id} {row.id}
{/if} {/if}
</td> </td>
{#if type == 'USER'}
<td>{row?.name ? row.name : ''}</td>
{/if}
<td>{row.totalJobs}</td> <td>{row.totalJobs}</td>
<td>{row.totalWalltime}</td> <td>{row.totalWalltime}</td>
<td>{row.totalCoreHours}</td> <td>{row.totalCoreHours}</td>

View File

@ -126,8 +126,13 @@
opts.push(`numNodes=${filters.numNodes.from}-${filters.numNodes.to}`) opts.push(`numNodes=${filters.numNodes.from}-${filters.numNodes.to}`)
if (filters.numAccelerators.from && filters.numAccelerators.to) if (filters.numAccelerators.from && filters.numAccelerators.to)
opts.push(`numAccelerators=${filters.numAccelerators.from}-${filters.numAccelerators.to}`) opts.push(`numAccelerators=${filters.numAccelerators.from}-${filters.numAccelerators.to}`)
if (filters.user) if (filters.user.length != 0)
if (filters.userMatch != 'in') {
opts.push(`user=${filters.user}`) opts.push(`user=${filters.user}`)
} else {
for (let singleUser of filters.user)
opts.push(`user=${singleUser}`)
}
if (filters.userMatch != 'contains') if (filters.userMatch != 'contains')
opts.push(`userMatch=${filters.userMatch}`) opts.push(`userMatch=${filters.userMatch}`)
if (filters.project) if (filters.project)