From bf6ec1bc98cc045162b8bc816958a2a1957b4783 Mon Sep 17 00:00:00 2001 From: Christoph Kluge Date: Fri, 17 Feb 2023 10:45:27 +0100 Subject: [PATCH 1/2] Add search for user names, add names in user table - Regenerated GraphQL due to new required query 'multiUser' --- api/schema.graphqls | 2 + docs/searchbar.md | 30 ++++-- go.mod | 5 +- go.sum | 26 +++-- internal/graph/generated/generated.go | 99 +++++++++++++++++- internal/graph/model/models_gen.go | 2 + internal/graph/schema.resolvers.go | 2 +- internal/graph/stats.go | 15 ++- internal/repository/job.go | 131 ++++++++++++++++++++++-- internal/repository/query.go | 7 ++ internal/routerConfig/routes.go | 38 +++++-- web/frontend/src/Header.svelte | 2 +- web/frontend/src/List.root.svelte | 13 +++ web/frontend/src/filters/Filters.svelte | 6 ++ 14 files changed, 336 insertions(+), 42 deletions(-) diff --git a/api/schema.graphqls b/api/schema.graphqls index 957a9f5..fc6167f 100644 --- a/api/schema.graphqls +++ b/api/schema.graphqls @@ -201,6 +201,7 @@ input JobFilter { jobId: StringInput arrayJobId: Int user: StringInput + multiUser: [String] project: StringInput jobName: StringInput cluster: StringInput @@ -256,6 +257,7 @@ type HistoPoint { type JobsStatistics { 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 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 diff --git a/docs/searchbar.md b/docs/searchbar.md index 77d5f75..c80620c 100644 --- a/docs/searchbar.md +++ b/docs/searchbar.md @@ -3,14 +3,31 @@ ### Usage * Searchtags are implemented as `type:` 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` * Examples: * `jobName:myJob12` * `jobId:123456` * `username:abcd100` + * `name:Paul` * 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 ... * Best guess search fails -> 'Not Found' * Query `type` is unknown @@ -18,7 +35,8 @@ * Spaces trimmed (both for searchTag and queryString) * ` job12` == `job12` * `projectID : abcd ` == `projectId:abcd` -* jobId-Query now redirects to table - * Allows multiple jobs from different systems, but with identical job-id to be found -* jobName-Query works with a part of the jobName-String (e.g. jobName:myjob for jobName myjob_cluster1) - * JobName GQL Query is resolved as matching the query as a part of the whole metaData-JSON in the SQL DB. +* `jobName`- and `name-`queries work with a part of the target-string + * `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. diff --git a/go.mod b/go.mod index 5b327bb..2f5a989 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,8 @@ require ( github.com/influxdata/influxdb-client-go/v2 v2.10.0 github.com/jmoiron/sqlx v1.3.5 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/swaggo/http-swagger v1.3.3 github.com/swaggo/swag v1.8.5 @@ -55,9 +57,7 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // 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/common v0.37.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a // indirect @@ -69,7 +69,6 @@ require ( golang.org/x/sys v0.0.0-20220913175220-63ea55921009 // indirect golang.org/x/text v0.3.7 // indirect golang.org/x/tools v0.1.12 // indirect - golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f // indirect google.golang.org/appengine v1.6.6 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 3e69cd9..dcd7238 100644 --- a/go.sum +++ b/go.sum @@ -70,6 +70,7 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU= github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -190,6 +191,8 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +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/gops v0.3.25 h1:Pf6uw+cO6pDhc7HJ71NiG0x8dyQTeQcmg3HQFF39qVw= github.com/google/gops v0.3.25/go.mod h1:8A7ebAm0id9K3H0uOggeRVGxszSvnlURun9mg3GdYDw= @@ -205,9 +208,9 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= @@ -240,9 +243,9 @@ github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/ github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kevinmbeaulieu/eq-go v1.0.0/go.mod h1:G3S8ajA56gKBZm4UB9AOyoOS37JO3roToPzKNM8dtdM= @@ -368,13 +371,14 @@ github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shirou/gopsutil/v3 v3.22.4/go.mod h1:D01hZJ4pVHPpCTZ3m3T2+wDF2YAGfd+H4ifUguaQzHM= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -410,6 +414,9 @@ github.com/vektah/gqlparser/v2 v2.5.0/go.mod h1:mPgqFBu/woKTVYWyNk8cO3kh4S/f4aRF github.com/xlab/treeprint v1.1.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -421,6 +428,8 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -464,7 +473,6 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -528,6 +536,7 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/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-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/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -612,6 +621,7 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= @@ -646,7 +656,7 @@ golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -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.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/internal/graph/generated/generated.go b/internal/graph/generated/generated.go index edae306..0d018b3 100644 --- a/internal/graph/generated/generated.go +++ b/internal/graph/generated/generated.go @@ -131,6 +131,7 @@ type ComplexityRoot struct { HistDuration func(childComplexity int) int HistNumNodes func(childComplexity int) int ID func(childComplexity int) int + Name func(childComplexity int) int ShortJobs func(childComplexity int) int TotalCoreHours 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 + case "JobsStatistics.name": + if e.complexity.JobsStatistics.Name == nil { + break + } + + return e.complexity.JobsStatistics.Name(childComplexity), true + case "JobsStatistics.shortJobs": if e.complexity.JobsStatistics.ShortJobs == nil { break @@ -1584,6 +1592,7 @@ input JobFilter { jobId: StringInput arrayJobId: Int user: StringInput + multiUser: [String] project: StringInput jobName: StringInput cluster: StringInput @@ -1639,6 +1648,7 @@ type HistoPoint { type JobsStatistics { 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 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 @@ -4485,6 +4495,47 @@ func (ec *executionContext) fieldContext_JobsStatistics_id(ctx context.Context, 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) { fc, err := ec.fieldContext_JobsStatistics_totalJobs(ctx, field) if err != nil { @@ -6401,6 +6452,8 @@ func (ec *executionContext) fieldContext_Query_jobsStatistics(ctx context.Contex switch field.Name { case "id": return ec.fieldContext_JobsStatistics_id(ctx, field) + case "name": + return ec.fieldContext_JobsStatistics_name(ctx, field) case "totalJobs": return ec.fieldContext_JobsStatistics_totalJobs(ctx, field) case "shortJobs": @@ -10392,7 +10445,7 @@ func (ec *executionContext) unmarshalInputJobFilter(ctx context.Context, obj int asMap[k] = v } - fieldsInOrder := [...]string{"tags", "jobId", "arrayJobId", "user", "project", "jobName", "cluster", "partition", "duration", "minRunningFor", "numNodes", "numAccelerators", "numHWThreads", "startTime", "state", "flopsAnyAvg", "memBwAvg", "loadAvg", "memUsedMax"} + fieldsInOrder := [...]string{"tags", "jobId", "arrayJobId", "user", "multiUser", "project", "jobName", "cluster", "partition", "duration", "minRunningFor", "numNodes", "numAccelerators", "numHWThreads", "startTime", "state", "flopsAnyAvg", "memBwAvg", "loadAvg", "memUsedMax"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { @@ -10431,6 +10484,14 @@ func (ec *executionContext) unmarshalInputJobFilter(ctx context.Context, obj int if err != nil { return it, err } + case "multiUser": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("multiUser")) + it.MultiUser, err = ec.unmarshalOString2ᚕᚖstring(ctx, v) + if err != nil { + return it, err + } case "project": var err error @@ -11340,6 +11401,10 @@ func (ec *executionContext) _JobsStatistics(ctx context.Context, sel ast.Selecti if out.Values[i] == graphql.Null { invalids++ } + case "name": + + out.Values[i] = ec._JobsStatistics_name(ctx, field, obj) + case "totalJobs": out.Values[i] = ec._JobsStatistics_totalJobs(ctx, field, obj) @@ -14606,6 +14671,38 @@ func (ec *executionContext) marshalOString2ᚕstringᚄ(ctx context.Context, sel return ret } +func (ec *executionContext) unmarshalOString2ᚕᚖstring(ctx context.Context, v interface{}) ([]*string, error) { + if v == nil { + return nil, nil + } + var vSlice []interface{} + if v != nil { + vSlice = graphql.CoerceList(v) + } + var err error + res := make([]*string, len(vSlice)) + for i := range vSlice { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithIndex(i)) + res[i], err = ec.unmarshalOString2ᚖstring(ctx, vSlice[i]) + if err != nil { + return nil, err + } + } + return res, nil +} + +func (ec *executionContext) marshalOString2ᚕᚖstring(ctx context.Context, sel ast.SelectionSet, v []*string) graphql.Marshaler { + if v == nil { + return graphql.Null + } + ret := make(graphql.Array, len(v)) + for i := range v { + ret[i] = ec.marshalOString2ᚖstring(ctx, sel, v[i]) + } + + return ret +} + func (ec *executionContext) unmarshalOString2ᚖstring(ctx context.Context, v interface{}) (*string, error) { if v == nil { return nil, nil diff --git a/internal/graph/model/models_gen.go b/internal/graph/model/models_gen.go index 9d2f9ea..2ff3b94 100644 --- a/internal/graph/model/models_gen.go +++ b/internal/graph/model/models_gen.go @@ -41,6 +41,7 @@ type JobFilter struct { JobID *StringInput `json:"jobId"` ArrayJobID *int `json:"arrayJobId"` User *StringInput `json:"user"` + MultiUser []*string `json:"multiUser"` Project *StringInput `json:"project"` JobName *StringInput `json:"jobName"` Cluster *StringInput `json:"cluster"` @@ -72,6 +73,7 @@ type JobResultList struct { type JobsStatistics struct { ID string `json:"id"` + Name *string `json:"name"` TotalJobs int `json:"totalJobs"` ShortJobs int `json:"shortJobs"` TotalWalltime int `json:"totalWalltime"` diff --git a/internal/graph/schema.resolvers.go b/internal/graph/schema.resolvers.go index 5c3d982..d1a6371 100644 --- a/internal/graph/schema.resolvers.go +++ b/internal/graph/schema.resolvers.go @@ -16,8 +16,8 @@ import ( "github.com/ClusterCockpit/cc-backend/internal/metricdata" "github.com/ClusterCockpit/cc-backend/internal/repository" "github.com/ClusterCockpit/cc-backend/pkg/archive" - "github.com/ClusterCockpit/cc-backend/pkg/schema" "github.com/ClusterCockpit/cc-backend/pkg/log" + "github.com/ClusterCockpit/cc-backend/pkg/schema" ) // Partitions is the resolver for the partitions field. diff --git a/internal/graph/stats.go b/internal/graph/stats.go index 3678ea4..b619dc8 100644 --- a/internal/graph/stats.go +++ b/internal/graph/stats.go @@ -17,8 +17,8 @@ import ( "github.com/ClusterCockpit/cc-backend/internal/metricdata" "github.com/ClusterCockpit/cc-backend/internal/repository" "github.com/ClusterCockpit/cc-backend/pkg/archive" - "github.com/ClusterCockpit/cc-backend/pkg/schema" "github.com/ClusterCockpit/cc-backend/pkg/log" + "github.com/ClusterCockpit/cc-backend/pkg/schema" sq "github.com/Masterminds/squirrel" ) @@ -116,6 +116,7 @@ func (r *queryResolver) jobsStatistics(ctx context.Context, filter []*model.JobF for _, f := range filter { query = repository.BuildWhereClause(f, query) } + rows, err := query.RunWith(r.DB).Query() if err != nil { log.Warn("Error while querying jobs for short jobs") @@ -134,6 +135,18 @@ func (r *queryResolver) jobsStatistics(ctx context.Context, filter []*model.JobF stats[id.String].ShortJobs = int(shortJobs.Int64) } } + + if col == "job.user" { + for id, _ := range stats { + emptyDash := "-" + name, _ := repository.GetJobRepository().FindNameByUser(ctx, id) + if name != "" { + stats[id].Name = &name + } else { + stats[id].Name = &emptyDash + } + } + } } // Calculating the histogram data is expensive, so only do it if needed. diff --git a/internal/repository/job.go b/internal/repository/job.go index 535a855..4a7901f 100644 --- a/internal/repository/job.go +++ b/internal/repository/job.go @@ -44,9 +44,9 @@ func GetJobRepository() *JobRepository { db := GetConnection() jobRepoInstance = &JobRepository{ - DB: db.DB, - stmtCache: sq.NewStmtCache(db.DB), - cache: lrucache.New(1024 * 1024), + DB: db.DB, + stmtCache: sq.NewStmtCache(db.DB), + cache: lrucache.New(1024 * 1024), archiveChannel: make(chan *schema.Job, 128), } // start archiving worker @@ -67,8 +67,8 @@ func scanJob(row interface{ Scan(...interface{}) error }) (*schema.Job, error) { if err := row.Scan( &job.ID, &job.JobID, &job.User, &job.Project, &job.Cluster, &job.SubCluster, &job.StartTimeUnix, &job.Partition, &job.ArrayJobId, &job.NumNodes, &job.NumHWThreads, &job.NumAcc, &job.Exclusive, &job.MonitoringStatus, &job.SMT, &job.State, - &job.Duration, &job.Walltime, &job.RawResources, /*&job.RawMetaData*/); err != nil { - log.Warn("Error while scanning rows") + &job.Duration, &job.Walltime, &job.RawResources /*&job.RawMetaData*/); err != nil { + log.Warn("Error while scanning rows") return nil, err } @@ -417,10 +417,10 @@ func (r *JobRepository) MarkArchived( } // Archiving worker thread -func (r *JobRepository) archivingWorker(){ +func (r *JobRepository) archivingWorker() { for { select { - case job, ok := <- r.archiveChannel: + case job, ok := <-r.archiveChannel: if !ok { break } @@ -454,13 +454,13 @@ func (r *JobRepository) archivingWorker(){ } // Trigger async archiving -func (r *JobRepository) TriggerArchiving(job *schema.Job){ +func (r *JobRepository) TriggerArchiving(job *schema.Job) { r.archivePending.Add(1) r.archiveChannel <- job } // Wait for background thread to finish pending archiving operations -func (r *JobRepository) WaitForArchiving(){ +func (r *JobRepository) WaitForArchiving() { // close channel and wait for worker to process remaining jobs r.archivePending.Wait() } @@ -487,6 +487,17 @@ func (r *JobRepository) FindJobnameOrUserOrProject(ctx context.Context, searchte } else if err == 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) { @@ -502,7 +513,7 @@ func (r *JobRepository) FindJobnameOrUserOrProject(ctx context.Context, searchte // All Authorizations: If unlabeled query not username or projectId, try for jobname: Match Metadata, on hit, parent method redirects to jobName GQL query err := sq.Select("job.cluster").Distinct().From("job"). - Where("job.meta_data LIKE ?", "%" + searchterm + "%"). + Where("job.meta_data LIKE ?", "%"+searchterm+"%"). RunWith(r.stmtCache).QueryRow().Scan(&metasnip) if err != nil && err != sql.ErrNoRows { return "", "", "", err @@ -528,7 +539,105 @@ func (r *JobRepository) FindUser(ctx context.Context, searchterm string) (userna return "", ErrNotFound } 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 } } diff --git a/internal/repository/query.go b/internal/repository/query.go index 7fa34b6..d2f5c33 100644 --- a/internal/repository/query.go +++ b/internal/repository/query.go @@ -118,6 +118,13 @@ func BuildWhereClause(filter *model.JobFilter, query sq.SelectBuilder) sq.Select if filter.User != nil { query = buildStringCondition("job.user", filter.User, query) } + if filter.MultiUser != nil { + queryUsers := make([]string, len(filter.MultiUser)) + for i, val := range filter.MultiUser { + queryUsers[i] = *val + } + query = query.Where(sq.Or{sq.Eq{"job.user": queryUsers}}) + } if filter.Project != nil { query = buildStringCondition("job.project", filter.Project, query) } diff --git a/internal/routerConfig/routes.go b/internal/routerConfig/routes.go index 9663874..04b6c7a 100644 --- a/internal/routerConfig/routes.go +++ b/internal/routerConfig/routes.go @@ -12,8 +12,8 @@ import ( "strings" "time" - "github.com/ClusterCockpit/cc-backend/internal/auth" "github.com/ClusterCockpit/cc-backend/internal/api" + "github.com/ClusterCockpit/cc-backend/internal/auth" "github.com/ClusterCockpit/cc-backend/internal/graph" "github.com/ClusterCockpit/cc-backend/internal/graph/model" "github.com/ClusterCockpit/cc-backend/internal/repository" @@ -186,7 +186,10 @@ func buildFilterPresets(query url.Values) map[string]interface{} { } if query.Get("user") != "" { filterPresets["user"] = query.Get("user") - filterPresets["userMatch"] = "eq" + filterPresets["userMatch"] = "contains" + } + if len(query["multiUser"]) != 0 { + filterPresets["multiUser"] = query["multiUser"] } if len(query["state"]) != 0 { filterPresets["state"] = query["state"] @@ -303,7 +306,7 @@ func HandleSearchBar(rw http.ResponseWriter, r *http.Request, api *api.RestApi) if search := r.URL.Query().Get("searchId"); search != "" { splitSearch := strings.Split(search, ":") - if (len(splitSearch) == 2) { + if len(splitSearch) == 2 { switch strings.Trim(splitSearch[0], " ") { case "jobId": http.Redirect(rw, r, "/monitoring/jobs/?jobId="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusTemporaryRedirect) // All Users: Redirect to Tablequery @@ -320,18 +323,33 @@ 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 } case "username": - username, _ := api.JobRepository.FindUser(r.Context(), strings.Trim(splitSearch[1], " ")) // Restricted: username - if username != "" { - http.Redirect(rw, r, "/monitoring/user/"+username, http.StatusTemporaryRedirect) + usernames, _ := api.JobRepository.FindUsers(r.Context(), strings.Trim(splitSearch[1], " ")) // Restricted: usernames + if len(usernames) == 1 { + 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 } 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, "&multiUser=") + http.Redirect(rw, r, "/monitoring/users/?multiUser="+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: http.Error(rw, "'searchId' type parameter unknown", http.StatusBadRequest) } - } else if (len(splitSearch) == 1) { + } else if len(splitSearch) == 1 { jobname, username, project, err := api.JobRepository.FindJobnameOrUserOrProject(r.Context(), strings.Trim(search, " ")) // Determine Access within if err != nil { @@ -342,10 +360,10 @@ func HandleSearchBar(rw http.ResponseWriter, r *http.Request, api *api.RestApi) if username != "" { http.Redirect(rw, r, "/monitoring/user/"+username, http.StatusTemporaryRedirect) // User: Redirect to user page return - } else if (project != "") { + } else if project != "" { http.Redirect(rw, r, "/monitoring/jobs/?projectMatch=eq&project="+url.QueryEscape(strings.Trim(search, " ")), http.StatusTemporaryRedirect) // projectId (equal) return - } else if (jobname != "") { + } else if jobname != "" { http.Redirect(rw, r, "/monitoring/jobs/?jobName="+url.QueryEscape(strings.Trim(search, " ")), http.StatusTemporaryRedirect) // JobName (contains) return } else { diff --git a/web/frontend/src/Header.svelte b/web/frontend/src/Header.svelte index 96a5f06..af32a23 100644 --- a/web/frontend/src/Header.svelte +++ b/web/frontend/src/Header.svelte @@ -57,7 +57,7 @@ - + {#if username} diff --git a/web/frontend/src/List.root.svelte b/web/frontend/src/List.root.svelte index 7d973e4..d80e330 100644 --- a/web/frontend/src/List.root.svelte +++ b/web/frontend/src/List.root.svelte @@ -20,6 +20,7 @@ const stats = operationStore(`query($filter: [JobFilter!]!) { rows: jobsStatistics(filter: $filter, groupBy: ${type}) { id + name totalJobs totalWalltime totalCoreHours @@ -93,6 +94,15 @@ + {#if type == 'USER'} + + Name + + + {/if} Total Jobs