mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2024-11-10 08:57:25 +01:00
Merge branch 'master' into sql-repository-opt
This commit is contained in:
commit
33b20620ee
@ -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
|
||||||
|
@ -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
4
go.mod
@ -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
4
go.sum
@ -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=
|
||||||
|
@ -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)
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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}
|
||||||
|
@ -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>
|
||||||
|
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user