mirror of https://github.com/grafana/grafana.git
Merge branch 'main' into hackathon14/api-clients
CodeQL checks / Detect whether code changed (push) Waiting to run
Details
CodeQL checks / Analyze (actions) (push) Blocked by required conditions
Details
CodeQL checks / Analyze (go) (push) Blocked by required conditions
Details
CodeQL checks / Analyze (javascript) (push) Blocked by required conditions
Details
CodeQL checks / Detect whether code changed (push) Waiting to run
Details
CodeQL checks / Analyze (actions) (push) Blocked by required conditions
Details
CodeQL checks / Analyze (go) (push) Blocked by required conditions
Details
CodeQL checks / Analyze (javascript) (push) Blocked by required conditions
Details
This commit is contained in:
commit
6d9d48ccfd
|
@ -88,6 +88,7 @@
|
|||
/apps/preferences/ @grafana/grafana-app-platform-squad @grafana/grafana-frontend-platform
|
||||
/apps/shorturl/ @grafana/sharing-squad
|
||||
/apps/secret/ @grafana/grafana-operator-experience-squad
|
||||
/apps/scope/ @grafana/grafana-operator-experience-squad
|
||||
/apps/investigations/ @fcjack @matryer @svennergr
|
||||
/apps/advisor/ @grafana/plugins-platform-backend
|
||||
/apps/iam/ @grafana/access-squad
|
||||
|
|
|
@ -99,6 +99,7 @@ COPY apps/correlations apps/correlations
|
|||
COPY apps/preferences apps/preferences
|
||||
COPY apps/provisioning apps/provisioning
|
||||
COPY apps/secret apps/secret
|
||||
COPY apps/scope apps/scope
|
||||
COPY apps/investigations apps/investigations
|
||||
COPY apps/advisor apps/advisor
|
||||
COPY apps/dashboard apps/dashboard
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
module github.com/grafana/grafana/apps/scope
|
||||
|
||||
go 1.24.6
|
||||
|
||||
require (
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20251007081214-26e147d01f0a
|
||||
k8s.io/apimachinery v0.34.1
|
||||
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/google/gnostic-models v0.7.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/mailru/easyjson v0.9.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/stretchr/testify v1.11.1 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/net v0.44.0 // indirect
|
||||
golang.org/x/text v0.29.0 // indirect
|
||||
google.golang.org/protobuf v1.36.9 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
|
||||
)
|
|
@ -0,0 +1,118 @@
|
|||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes=
|
||||
github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
||||
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
||||
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
|
||||
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
|
||||
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
||||
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
|
||||
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20251007081214-26e147d01f0a h1:L7xgV9mP6MRF3L2/vDOjNR7heaBPbXPMGTDN9/jXSFQ=
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20251007081214-26e147d01f0a/go.mod h1:OK8NwS87D5YphchOcAsiIWk/feMZ0EzfAGME1Kff860=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
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/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
|
||||
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
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/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
|
||||
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4=
|
||||
k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
|
||||
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA=
|
||||
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts=
|
||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y=
|
||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
|
||||
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
|
||||
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
|
||||
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
|
||||
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
|
||||
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
|
|
@ -0,0 +1,6 @@
|
|||
// +k8s:deepcopy-gen=package
|
||||
// +k8s:openapi-gen=true
|
||||
// +k8s:defaulter-gen=TypeMeta
|
||||
// +groupName=scope.grafana.app
|
||||
|
||||
package v0alpha1 // import "github.com/grafana/grafana/apps/pkg/apis/scope/v0alpha1"
|
|
@ -0,0 +1,168 @@
|
|||
package v0alpha1
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
const (
|
||||
GROUP = "scope.grafana.app"
|
||||
VERSION = "v0alpha1"
|
||||
APIVERSION = GROUP + "/" + VERSION
|
||||
)
|
||||
|
||||
var ScopeResourceInfo = utils.NewResourceInfo(GROUP, VERSION,
|
||||
"scopes", "scope", "Scope",
|
||||
func() runtime.Object { return &Scope{} },
|
||||
func() runtime.Object { return &ScopeList{} },
|
||||
utils.TableColumns{
|
||||
Definition: []metav1.TableColumnDefinition{
|
||||
{Name: "Name", Type: "string", Format: "name"},
|
||||
{Name: "Created At", Type: "date"},
|
||||
{Name: "Title", Type: "string"},
|
||||
{Name: "Filters", Type: "array"},
|
||||
},
|
||||
Reader: func(obj any) ([]interface{}, error) {
|
||||
m, ok := obj.(*Scope)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expected scope")
|
||||
}
|
||||
return []interface{}{
|
||||
m.Name,
|
||||
m.CreationTimestamp.UTC().Format(time.RFC3339),
|
||||
m.Spec.Title,
|
||||
m.Spec.Filters,
|
||||
}, nil
|
||||
},
|
||||
}, // default table converter
|
||||
)
|
||||
|
||||
var ScopeDashboardBindingResourceInfo = utils.NewResourceInfo(GROUP, VERSION,
|
||||
"scopedashboardbindings", "scopedashboardbinding", "ScopeDashboardBinding",
|
||||
func() runtime.Object { return &ScopeDashboardBinding{} },
|
||||
func() runtime.Object { return &ScopeDashboardBindingList{} },
|
||||
utils.TableColumns{
|
||||
Definition: []metav1.TableColumnDefinition{
|
||||
{Name: "Name", Type: "string", Format: "name"},
|
||||
{Name: "Created At", Type: "date"},
|
||||
{Name: "Dashboard", Type: "string"},
|
||||
{Name: "Scope", Type: "string"},
|
||||
},
|
||||
Reader: func(obj any) ([]interface{}, error) {
|
||||
m, ok := obj.(*ScopeDashboardBinding)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expected scope dashboard binding")
|
||||
}
|
||||
return []interface{}{
|
||||
m.Name,
|
||||
m.CreationTimestamp.UTC().Format(time.RFC3339),
|
||||
m.Spec.Dashboard,
|
||||
m.Spec.Scope,
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
var ScopeNavigationResourceInfo = utils.NewResourceInfo(GROUP, VERSION,
|
||||
"scopenavigations", "scopenavigation", "ScopeNavigation",
|
||||
func() runtime.Object { return &ScopeNavigation{} },
|
||||
func() runtime.Object { return &ScopeNavigationList{} },
|
||||
utils.TableColumns{
|
||||
Definition: []metav1.TableColumnDefinition{
|
||||
{Name: "Name", Type: "string", Format: "name"},
|
||||
{Name: "Created At", Type: "date"},
|
||||
{Name: "URL", Type: "string"},
|
||||
{Name: "Scope", Type: "string"},
|
||||
},
|
||||
Reader: func(obj any) ([]interface{}, error) {
|
||||
m, ok := obj.(*ScopeNavigation)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expected scope navigation")
|
||||
}
|
||||
return []interface{}{
|
||||
m.Name,
|
||||
m.CreationTimestamp.UTC().Format(time.RFC3339),
|
||||
m.Spec.URL,
|
||||
m.Spec.Scope,
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
var ScopeNodeResourceInfo = utils.NewResourceInfo(GROUP, VERSION,
|
||||
"scopenodes", "scopenode", "ScopeNode",
|
||||
func() runtime.Object { return &ScopeNode{} },
|
||||
func() runtime.Object { return &ScopeNodeList{} },
|
||||
utils.TableColumns{
|
||||
Definition: []metav1.TableColumnDefinition{
|
||||
{Name: "Name", Type: "string", Format: "name"},
|
||||
{Name: "Created At", Type: "date"},
|
||||
{Name: "Title", Type: "string"},
|
||||
{Name: "Parent Name", Type: "string"},
|
||||
{Name: "Node Type", Type: "string"},
|
||||
{Name: "Link Type", Type: "string"},
|
||||
{Name: "Link ID", Type: "string"},
|
||||
},
|
||||
Reader: func(obj any) ([]interface{}, error) {
|
||||
m, ok := obj.(*ScopeNode)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expected scope node")
|
||||
}
|
||||
return []interface{}{
|
||||
m.Name,
|
||||
m.CreationTimestamp.UTC().Format(time.RFC3339),
|
||||
m.Spec.Title,
|
||||
m.Spec.ParentName,
|
||||
m.Spec.NodeType,
|
||||
m.Spec.LinkType,
|
||||
m.Spec.LinkID,
|
||||
}, nil
|
||||
},
|
||||
}, // default table converter
|
||||
)
|
||||
|
||||
var (
|
||||
// SchemeGroupVersion is group version used to register these objects
|
||||
SchemeGroupVersion = schema.GroupVersion{Group: GROUP, Version: VERSION}
|
||||
InternalGroupVersion = schema.GroupVersion{Group: GROUP, Version: runtime.APIVersionInternal}
|
||||
|
||||
// SchemaBuilder is used by standard codegen
|
||||
SchemeBuilder runtime.SchemeBuilder
|
||||
localSchemeBuilder = &SchemeBuilder
|
||||
AddToScheme = localSchemeBuilder.AddToScheme
|
||||
)
|
||||
|
||||
func init() {
|
||||
localSchemeBuilder.Register(func(s *runtime.Scheme) error {
|
||||
return AddKnownTypes(SchemeGroupVersion, s)
|
||||
})
|
||||
}
|
||||
|
||||
// Adds the list of known types to the given scheme.
|
||||
func AddKnownTypes(gv schema.GroupVersion, scheme *runtime.Scheme) error {
|
||||
scheme.AddKnownTypes(gv,
|
||||
&Scope{},
|
||||
&ScopeList{},
|
||||
&ScopeDashboardBinding{},
|
||||
&ScopeDashboardBindingList{},
|
||||
&ScopeNode{},
|
||||
&ScopeNodeList{},
|
||||
&FindScopeNodeChildrenResults{},
|
||||
&FindScopeDashboardBindingsResults{},
|
||||
&ScopeNavigation{},
|
||||
&ScopeNavigationList{},
|
||||
&FindScopeNavigationsResults{},
|
||||
)
|
||||
//metav1.AddToGroupVersion(scheme, gv)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Resource takes an unqualified resource and returns a Group qualified GroupResource
|
||||
func Resource(resource string) schema.GroupResource {
|
||||
return SchemeGroupVersion.WithResource(resource).GroupResource()
|
||||
}
|
|
@ -0,0 +1,238 @@
|
|||
package v0alpha1
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
/*
|
||||
Please keep pkg/promlib/models/query.go and pkg/promlib/models/scope.go in sync
|
||||
with this file until this package is out of the grafana/grafana module.
|
||||
*/
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type Scope struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec ScopeSpec `json:"spec,omitempty"`
|
||||
}
|
||||
|
||||
type ScopeSpec struct {
|
||||
Title string `json:"title"`
|
||||
// Provides a default path for the scope. This refers to a list of nodes in the selector. This is used to display the title next to the selected scope and expand the selector to the proper path.
|
||||
// This will override whichever is selected from in the selector.
|
||||
// The path is a list of node ids, starting at the direct parent of the selected node towards the root.
|
||||
// +listType=atomic
|
||||
DefaultPath []string `json:"defaultPath,omitempty"`
|
||||
|
||||
// +listType=atomic
|
||||
Filters []ScopeFilter `json:"filters,omitempty"`
|
||||
}
|
||||
|
||||
type ScopeFilter struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
// Values is used for operators that require multiple values (e.g. one-of and not-one-of).
|
||||
// +listType=atomic
|
||||
Values []string `json:"values,omitempty"`
|
||||
Operator FilterOperator `json:"operator"`
|
||||
}
|
||||
|
||||
// Type of the filter operator.
|
||||
// +enum
|
||||
type FilterOperator string
|
||||
|
||||
// Defines values for FilterOperator.
|
||||
const (
|
||||
FilterOperatorEquals FilterOperator = "equals"
|
||||
FilterOperatorNotEquals FilterOperator = "not-equals"
|
||||
FilterOperatorRegexMatch FilterOperator = "regex-match"
|
||||
FilterOperatorRegexNotMatch FilterOperator = "regex-not-match"
|
||||
FilterOperatorOneOf FilterOperator = "one-of"
|
||||
FilterOperatorNotOneOf FilterOperator = "not-one-of"
|
||||
)
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type ScopeList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
|
||||
Items []Scope `json:"items,omitempty"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type ScopeDashboardBinding struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec ScopeDashboardBindingSpec `json:"spec,omitempty"`
|
||||
Status ScopeDashboardBindingStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type ScopeDashboardBindingList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
|
||||
Items []ScopeDashboardBinding `json:"items,omitempty"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type FindScopeDashboardBindingsResults struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
|
||||
Items []ScopeDashboardBinding `json:"items,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type ScopeNode struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec ScopeNodeSpec `json:"spec,omitempty"`
|
||||
}
|
||||
|
||||
type ScopeDashboardBindingSpec struct {
|
||||
Dashboard string `json:"dashboard"`
|
||||
Scope string `json:"scope"`
|
||||
}
|
||||
|
||||
// Type of the item.
|
||||
// +enum
|
||||
// ScopeDashboardBindingStatus contains derived information about a ScopeDashboardBinding.
|
||||
type ScopeDashboardBindingStatus struct {
|
||||
// DashboardTitle should be populated and update from the dashboard
|
||||
DashboardTitle string `json:"dashboardTitle"`
|
||||
|
||||
// Groups is used for the grouping of dashboards that are suggested based
|
||||
// on a scope. The source of truth for this information has not been
|
||||
// determined yet.
|
||||
Groups []string `json:"groups,omitempty"`
|
||||
|
||||
// DashboardTitleConditions is a list of conditions that are used to determine if the dashboard title is valid.
|
||||
// +optional
|
||||
// +listType=map
|
||||
// +listMapKey=type
|
||||
DashboardTitleConditions []metav1.Condition `json:"dashboardTitleConditions,omitempty"`
|
||||
|
||||
// DashboardTitleConditions is a list of conditions that are used to determine if the list of groups is valid.
|
||||
// +optional
|
||||
// +listType=map
|
||||
// +listMapKey=type
|
||||
GroupsConditions []metav1.Condition `json:"groupsConditions,omitempty"`
|
||||
}
|
||||
|
||||
type NodeType string
|
||||
|
||||
// Defines values for ItemType.
|
||||
const (
|
||||
NodeTypeContainer NodeType = "container"
|
||||
NodeTypeLeaf NodeType = "leaf"
|
||||
)
|
||||
|
||||
// Type of the item.
|
||||
// +enum
|
||||
type LinkType string
|
||||
|
||||
// Defines values for ItemType.
|
||||
const (
|
||||
LinkTypeScope LinkType = "scope"
|
||||
)
|
||||
|
||||
type ScopeNodeSpec struct {
|
||||
//+optional
|
||||
ParentName string `json:"parentName,omitempty"`
|
||||
|
||||
NodeType NodeType `json:"nodeType"` // container | leaf
|
||||
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description,omitempty"`
|
||||
DisableMultiSelect bool `json:"disableMultiSelect"`
|
||||
|
||||
LinkType LinkType `json:"linkType,omitempty"` // scope (later more things)
|
||||
LinkID string `json:"linkId,omitempty"` // the k8s name
|
||||
// ?? should this be a slice of links
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type ScopeNodeList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
|
||||
Items []ScopeNode `json:"items,omitempty"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type FindScopeNodeChildrenResults struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
|
||||
Items []ScopeNode `json:"items,omitempty"`
|
||||
}
|
||||
|
||||
// Scoped navigation types
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type ScopeNavigation struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec ScopeNavigationSpec `json:"spec,omitempty"`
|
||||
Status ScopeNavigationStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type ScopeNavigationList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
|
||||
Items []ScopeNavigation `json:"items,omitempty"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type FindScopeNavigationsResults struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
|
||||
Items []ScopeNavigation `json:"items,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
type ScopeNavigationSpec struct {
|
||||
URL string `json:"url"`
|
||||
Scope string `json:"scope"`
|
||||
}
|
||||
|
||||
// Type of the item.
|
||||
// +enum
|
||||
// ScopeNavigationStatus contains derived information about a ScopeNavigation.
|
||||
type ScopeNavigationStatus struct {
|
||||
// Title should be populated and update from the dashboard
|
||||
Title string `json:"title"`
|
||||
|
||||
// Groups is used for the grouping of dashboards that are suggested based
|
||||
// on a scope. The source of truth for this information has not been
|
||||
// determined yet.
|
||||
Groups []string `json:"groups,omitempty"`
|
||||
|
||||
// TitleConditions is a list of conditions that are used to determine if the title is valid.
|
||||
// +optional
|
||||
// +listType=map
|
||||
// +listMapKey=type
|
||||
TitleConditions []metav1.Condition `json:"titleConditions,omitempty"`
|
||||
|
||||
// GroupsConditions is a list of conditions that are used to determine if the list of groups is valid.
|
||||
// +optional
|
||||
// +listType=map
|
||||
// +listMapKey=type
|
||||
GroupsConditions []metav1.Condition `json:"groupsConditions,omitempty"`
|
||||
}
|
||||
|
||||
// Type of the filter operator.
|
||||
// +enum
|
||||
type ScopeNavigationLinkType string
|
||||
|
||||
// Defines values for FilterOperator.
|
||||
const (
|
||||
ScopeNavigationLinkTypeURL ScopeNavigationLinkType = "url"
|
||||
)
|
|
@ -0,0 +1,519 @@
|
|||
//go:build !ignore_autogenerated
|
||||
// +build !ignore_autogenerated
|
||||
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
// Code generated by deepcopy-gen. DO NOT EDIT.
|
||||
|
||||
package v0alpha1
|
||||
|
||||
import (
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *FindScopeDashboardBindingsResults) DeepCopyInto(out *FindScopeDashboardBindingsResults) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]ScopeDashboardBinding, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FindScopeDashboardBindingsResults.
|
||||
func (in *FindScopeDashboardBindingsResults) DeepCopy() *FindScopeDashboardBindingsResults {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(FindScopeDashboardBindingsResults)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *FindScopeDashboardBindingsResults) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *FindScopeNavigationsResults) DeepCopyInto(out *FindScopeNavigationsResults) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]ScopeNavigation, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FindScopeNavigationsResults.
|
||||
func (in *FindScopeNavigationsResults) DeepCopy() *FindScopeNavigationsResults {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(FindScopeNavigationsResults)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *FindScopeNavigationsResults) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *FindScopeNodeChildrenResults) DeepCopyInto(out *FindScopeNodeChildrenResults) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]ScopeNode, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FindScopeNodeChildrenResults.
|
||||
func (in *FindScopeNodeChildrenResults) DeepCopy() *FindScopeNodeChildrenResults {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(FindScopeNodeChildrenResults)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *FindScopeNodeChildrenResults) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Scope) DeepCopyInto(out *Scope) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Scope.
|
||||
func (in *Scope) DeepCopy() *Scope {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Scope)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *Scope) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ScopeDashboardBinding) DeepCopyInto(out *ScopeDashboardBinding) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
out.Spec = in.Spec
|
||||
in.Status.DeepCopyInto(&out.Status)
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScopeDashboardBinding.
|
||||
func (in *ScopeDashboardBinding) DeepCopy() *ScopeDashboardBinding {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ScopeDashboardBinding)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *ScopeDashboardBinding) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ScopeDashboardBindingList) DeepCopyInto(out *ScopeDashboardBindingList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]ScopeDashboardBinding, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScopeDashboardBindingList.
|
||||
func (in *ScopeDashboardBindingList) DeepCopy() *ScopeDashboardBindingList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ScopeDashboardBindingList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *ScopeDashboardBindingList) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ScopeDashboardBindingSpec) DeepCopyInto(out *ScopeDashboardBindingSpec) {
|
||||
*out = *in
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScopeDashboardBindingSpec.
|
||||
func (in *ScopeDashboardBindingSpec) DeepCopy() *ScopeDashboardBindingSpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ScopeDashboardBindingSpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ScopeDashboardBindingStatus) DeepCopyInto(out *ScopeDashboardBindingStatus) {
|
||||
*out = *in
|
||||
if in.Groups != nil {
|
||||
in, out := &in.Groups, &out.Groups
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.DashboardTitleConditions != nil {
|
||||
in, out := &in.DashboardTitleConditions, &out.DashboardTitleConditions
|
||||
*out = make([]v1.Condition, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.GroupsConditions != nil {
|
||||
in, out := &in.GroupsConditions, &out.GroupsConditions
|
||||
*out = make([]v1.Condition, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScopeDashboardBindingStatus.
|
||||
func (in *ScopeDashboardBindingStatus) DeepCopy() *ScopeDashboardBindingStatus {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ScopeDashboardBindingStatus)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ScopeFilter) DeepCopyInto(out *ScopeFilter) {
|
||||
*out = *in
|
||||
if in.Values != nil {
|
||||
in, out := &in.Values, &out.Values
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScopeFilter.
|
||||
func (in *ScopeFilter) DeepCopy() *ScopeFilter {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ScopeFilter)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ScopeList) DeepCopyInto(out *ScopeList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]Scope, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScopeList.
|
||||
func (in *ScopeList) DeepCopy() *ScopeList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ScopeList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *ScopeList) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ScopeNavigation) DeepCopyInto(out *ScopeNavigation) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
out.Spec = in.Spec
|
||||
in.Status.DeepCopyInto(&out.Status)
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScopeNavigation.
|
||||
func (in *ScopeNavigation) DeepCopy() *ScopeNavigation {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ScopeNavigation)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *ScopeNavigation) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ScopeNavigationList) DeepCopyInto(out *ScopeNavigationList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]ScopeNavigation, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScopeNavigationList.
|
||||
func (in *ScopeNavigationList) DeepCopy() *ScopeNavigationList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ScopeNavigationList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *ScopeNavigationList) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ScopeNavigationSpec) DeepCopyInto(out *ScopeNavigationSpec) {
|
||||
*out = *in
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScopeNavigationSpec.
|
||||
func (in *ScopeNavigationSpec) DeepCopy() *ScopeNavigationSpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ScopeNavigationSpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ScopeNavigationStatus) DeepCopyInto(out *ScopeNavigationStatus) {
|
||||
*out = *in
|
||||
if in.Groups != nil {
|
||||
in, out := &in.Groups, &out.Groups
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.TitleConditions != nil {
|
||||
in, out := &in.TitleConditions, &out.TitleConditions
|
||||
*out = make([]v1.Condition, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.GroupsConditions != nil {
|
||||
in, out := &in.GroupsConditions, &out.GroupsConditions
|
||||
*out = make([]v1.Condition, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScopeNavigationStatus.
|
||||
func (in *ScopeNavigationStatus) DeepCopy() *ScopeNavigationStatus {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ScopeNavigationStatus)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ScopeNode) DeepCopyInto(out *ScopeNode) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
out.Spec = in.Spec
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScopeNode.
|
||||
func (in *ScopeNode) DeepCopy() *ScopeNode {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ScopeNode)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *ScopeNode) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ScopeNodeList) DeepCopyInto(out *ScopeNodeList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]ScopeNode, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScopeNodeList.
|
||||
func (in *ScopeNodeList) DeepCopy() *ScopeNodeList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ScopeNodeList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *ScopeNodeList) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ScopeNodeSpec) DeepCopyInto(out *ScopeNodeSpec) {
|
||||
*out = *in
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScopeNodeSpec.
|
||||
func (in *ScopeNodeSpec) DeepCopy() *ScopeNodeSpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ScopeNodeSpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ScopeSpec) DeepCopyInto(out *ScopeSpec) {
|
||||
*out = *in
|
||||
if in.DefaultPath != nil {
|
||||
in, out := &in.DefaultPath, &out.DefaultPath
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Filters != nil {
|
||||
in, out := &in.Filters, &out.Filters
|
||||
*out = make([]ScopeFilter, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScopeSpec.
|
||||
func (in *ScopeSpec) DeepCopy() *ScopeSpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ScopeSpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
//go:build !ignore_autogenerated
|
||||
// +build !ignore_autogenerated
|
||||
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
// Code generated by defaulter-gen. DO NOT EDIT.
|
||||
|
||||
package v0alpha1
|
||||
|
||||
import (
|
||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// RegisterDefaults adds defaulters functions to the given scheme.
|
||||
// Public to allow building arbitrary schemes.
|
||||
// All generated defaulters are covering - they call all nested defaulters.
|
||||
func RegisterDefaults(scheme *runtime.Scheme) error {
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,934 @@
|
|||
//go:build !ignore_autogenerated
|
||||
// +build !ignore_autogenerated
|
||||
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
// Code generated by openapi-gen. DO NOT EDIT.
|
||||
|
||||
package v0alpha1
|
||||
|
||||
import (
|
||||
common "k8s.io/kube-openapi/pkg/common"
|
||||
spec "k8s.io/kube-openapi/pkg/validation/spec"
|
||||
)
|
||||
|
||||
func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition {
|
||||
return map[string]common.OpenAPIDefinition{
|
||||
"github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1.FindScopeDashboardBindingsResults": schema_pkg_apis_scope_v0alpha1_FindScopeDashboardBindingsResults(ref),
|
||||
"github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1.FindScopeNavigationsResults": schema_pkg_apis_scope_v0alpha1_FindScopeNavigationsResults(ref),
|
||||
"github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1.FindScopeNodeChildrenResults": schema_pkg_apis_scope_v0alpha1_FindScopeNodeChildrenResults(ref),
|
||||
"github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1.Scope": schema_pkg_apis_scope_v0alpha1_Scope(ref),
|
||||
"github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1.ScopeDashboardBinding": schema_pkg_apis_scope_v0alpha1_ScopeDashboardBinding(ref),
|
||||
"github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1.ScopeDashboardBindingList": schema_pkg_apis_scope_v0alpha1_ScopeDashboardBindingList(ref),
|
||||
"github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1.ScopeDashboardBindingSpec": schema_pkg_apis_scope_v0alpha1_ScopeDashboardBindingSpec(ref),
|
||||
"github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1.ScopeDashboardBindingStatus": schema_pkg_apis_scope_v0alpha1_ScopeDashboardBindingStatus(ref),
|
||||
"github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1.ScopeFilter": schema_pkg_apis_scope_v0alpha1_ScopeFilter(ref),
|
||||
"github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1.ScopeList": schema_pkg_apis_scope_v0alpha1_ScopeList(ref),
|
||||
"github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1.ScopeNavigation": schema_pkg_apis_scope_v0alpha1_ScopeNavigation(ref),
|
||||
"github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1.ScopeNavigationList": schema_pkg_apis_scope_v0alpha1_ScopeNavigationList(ref),
|
||||
"github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1.ScopeNavigationSpec": schema_pkg_apis_scope_v0alpha1_ScopeNavigationSpec(ref),
|
||||
"github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1.ScopeNavigationStatus": schema_pkg_apis_scope_v0alpha1_ScopeNavigationStatus(ref),
|
||||
"github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1.ScopeNode": schema_pkg_apis_scope_v0alpha1_ScopeNode(ref),
|
||||
"github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1.ScopeNodeList": schema_pkg_apis_scope_v0alpha1_ScopeNodeList(ref),
|
||||
"github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1.ScopeNodeSpec": schema_pkg_apis_scope_v0alpha1_ScopeNodeSpec(ref),
|
||||
"github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1.ScopeSpec": schema_pkg_apis_scope_v0alpha1_ScopeSpec(ref),
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_scope_v0alpha1_FindScopeDashboardBindingsResults(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"kind": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"apiVersion": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"items": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"array"},
|
||||
Items: &spec.SchemaOrArray{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1.ScopeDashboardBinding"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"message": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
"github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1.ScopeDashboardBinding"},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_scope_v0alpha1_FindScopeNavigationsResults(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"kind": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"apiVersion": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"items": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"array"},
|
||||
Items: &spec.SchemaOrArray{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1.ScopeNavigation"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"message": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
"github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1.ScopeNavigation"},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_scope_v0alpha1_FindScopeNodeChildrenResults(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"kind": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"apiVersion": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"metadata": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"),
|
||||
},
|
||||
},
|
||||
"items": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"array"},
|
||||
Items: &spec.SchemaOrArray{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1.ScopeNode"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
"github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1.ScopeNode", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_scope_v0alpha1_Scope(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"kind": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"apiVersion": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"metadata": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"),
|
||||
},
|
||||
},
|
||||
"spec": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1.ScopeSpec"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
"github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1.ScopeSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_scope_v0alpha1_ScopeDashboardBinding(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"kind": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"apiVersion": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"metadata": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"),
|
||||
},
|
||||
},
|
||||
"spec": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1.ScopeDashboardBindingSpec"),
|
||||
},
|
||||
},
|
||||
"status": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1.ScopeDashboardBindingStatus"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
"github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1.ScopeDashboardBindingSpec", "github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1.ScopeDashboardBindingStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_scope_v0alpha1_ScopeDashboardBindingList(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"kind": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"apiVersion": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"metadata": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"),
|
||||
},
|
||||
},
|
||||
"items": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"array"},
|
||||
Items: &spec.SchemaOrArray{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1.ScopeDashboardBinding"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
"github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1.ScopeDashboardBinding", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_scope_v0alpha1_ScopeDashboardBindingSpec(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"dashboard": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"scope": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"dashboard", "scope"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_scope_v0alpha1_ScopeDashboardBindingStatus(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Type of the item. ScopeDashboardBindingStatus contains derived information about a ScopeDashboardBinding.",
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"dashboardTitle": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "DashboardTitle should be populated and update from the dashboard",
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"groups": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Groups is used for the grouping of dashboards that are suggested based on a scope. The source of truth for this information has not been determined yet.",
|
||||
Type: []string{"array"},
|
||||
Items: &spec.SchemaOrArray{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"dashboardTitleConditions": {
|
||||
VendorExtensible: spec.VendorExtensible{
|
||||
Extensions: spec.Extensions{
|
||||
"x-kubernetes-list-map-keys": []interface{}{
|
||||
"type",
|
||||
},
|
||||
"x-kubernetes-list-type": "map",
|
||||
},
|
||||
},
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "DashboardTitleConditions is a list of conditions that are used to determine if the dashboard title is valid.",
|
||||
Type: []string{"array"},
|
||||
Items: &spec.SchemaOrArray{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Condition"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"groupsConditions": {
|
||||
VendorExtensible: spec.VendorExtensible{
|
||||
Extensions: spec.Extensions{
|
||||
"x-kubernetes-list-map-keys": []interface{}{
|
||||
"type",
|
||||
},
|
||||
"x-kubernetes-list-type": "map",
|
||||
},
|
||||
},
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "DashboardTitleConditions is a list of conditions that are used to determine if the list of groups is valid.",
|
||||
Type: []string{"array"},
|
||||
Items: &spec.SchemaOrArray{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Condition"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"dashboardTitle"},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.Condition"},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_scope_v0alpha1_ScopeFilter(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"key": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"value": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"values": {
|
||||
VendorExtensible: spec.VendorExtensible{
|
||||
Extensions: spec.Extensions{
|
||||
"x-kubernetes-list-type": "atomic",
|
||||
},
|
||||
},
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Values is used for operators that require multiple values (e.g. one-of and not-one-of).",
|
||||
Type: []string{"array"},
|
||||
Items: &spec.SchemaOrArray{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"operator": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Possible enum values:\n - `\"equals\"`\n - `\"not-equals\"`\n - `\"not-one-of\"`\n - `\"one-of\"`\n - `\"regex-match\"`\n - `\"regex-not-match\"`",
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
Enum: []interface{}{"equals", "not-equals", "not-one-of", "one-of", "regex-match", "regex-not-match"},
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"key", "value", "operator"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_scope_v0alpha1_ScopeList(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"kind": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"apiVersion": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"metadata": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"),
|
||||
},
|
||||
},
|
||||
"items": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"array"},
|
||||
Items: &spec.SchemaOrArray{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1.Scope"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
"github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1.Scope", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_scope_v0alpha1_ScopeNavigation(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"kind": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"apiVersion": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"metadata": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"),
|
||||
},
|
||||
},
|
||||
"spec": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1.ScopeNavigationSpec"),
|
||||
},
|
||||
},
|
||||
"status": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1.ScopeNavigationStatus"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
"github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1.ScopeNavigationSpec", "github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1.ScopeNavigationStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_scope_v0alpha1_ScopeNavigationList(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"kind": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"apiVersion": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"metadata": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"),
|
||||
},
|
||||
},
|
||||
"items": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"array"},
|
||||
Items: &spec.SchemaOrArray{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1.ScopeNavigation"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
"github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1.ScopeNavigation", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_scope_v0alpha1_ScopeNavigationSpec(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"url": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"scope": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"url", "scope"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_scope_v0alpha1_ScopeNavigationStatus(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Type of the item. ScopeNavigationStatus contains derived information about a ScopeNavigation.",
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"title": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Title should be populated and update from the dashboard",
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"groups": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Groups is used for the grouping of dashboards that are suggested based on a scope. The source of truth for this information has not been determined yet.",
|
||||
Type: []string{"array"},
|
||||
Items: &spec.SchemaOrArray{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"titleConditions": {
|
||||
VendorExtensible: spec.VendorExtensible{
|
||||
Extensions: spec.Extensions{
|
||||
"x-kubernetes-list-map-keys": []interface{}{
|
||||
"type",
|
||||
},
|
||||
"x-kubernetes-list-type": "map",
|
||||
},
|
||||
},
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "TitleConditions is a list of conditions that are used to determine if the title is valid.",
|
||||
Type: []string{"array"},
|
||||
Items: &spec.SchemaOrArray{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Condition"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"groupsConditions": {
|
||||
VendorExtensible: spec.VendorExtensible{
|
||||
Extensions: spec.Extensions{
|
||||
"x-kubernetes-list-map-keys": []interface{}{
|
||||
"type",
|
||||
},
|
||||
"x-kubernetes-list-type": "map",
|
||||
},
|
||||
},
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "GroupsConditions is a list of conditions that are used to determine if the list of groups is valid.",
|
||||
Type: []string{"array"},
|
||||
Items: &spec.SchemaOrArray{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Condition"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"title"},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.Condition"},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_scope_v0alpha1_ScopeNode(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"kind": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"apiVersion": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"metadata": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"),
|
||||
},
|
||||
},
|
||||
"spec": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1.ScopeNodeSpec"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
"github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1.ScopeNodeSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_scope_v0alpha1_ScopeNodeList(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"kind": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"apiVersion": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"metadata": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"),
|
||||
},
|
||||
},
|
||||
"items": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"array"},
|
||||
Items: &spec.SchemaOrArray{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1.ScopeNode"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
"github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1.ScopeNode", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_scope_v0alpha1_ScopeNodeSpec(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"parentName": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"nodeType": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"title": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"description": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"disableMultiSelect": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: false,
|
||||
Type: []string{"boolean"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"linkType": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Possible enum values:\n - `\"scope\"`",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
Enum: []interface{}{"scope"},
|
||||
},
|
||||
},
|
||||
"linkId": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "scope (later more things)",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"nodeType", "title", "disableMultiSelect"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_scope_v0alpha1_ScopeSpec(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"title": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"defaultPath": {
|
||||
VendorExtensible: spec.VendorExtensible{
|
||||
Extensions: spec.Extensions{
|
||||
"x-kubernetes-list-type": "atomic",
|
||||
},
|
||||
},
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Provides a default path for the scope. This refers to a list of nodes in the selector. This is used to display the title next to the selected scope and expand the selector to the proper path. This will override whichever is selected from in the selector. The path is a list of node ids, starting at the direct parent of the selected node towards the root.",
|
||||
Type: []string{"array"},
|
||||
Items: &spec.SchemaOrArray{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"filters": {
|
||||
VendorExtensible: spec.VendorExtensible{
|
||||
Extensions: spec.Extensions{
|
||||
"x-kubernetes-list-type": "atomic",
|
||||
},
|
||||
},
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"array"},
|
||||
Items: &spec.SchemaOrArray{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1.ScopeFilter"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"title"},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
"github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1.ScopeFilter"},
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
API rule violation: list_type_missing,github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1,FindScopeDashboardBindingsResults,Items
|
||||
API rule violation: list_type_missing,github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1,FindScopeNavigationsResults,Items
|
||||
API rule violation: list_type_missing,github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1,ScopeDashboardBindingStatus,Groups
|
||||
API rule violation: list_type_missing,github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1,ScopeNavigationStatus,Groups
|
||||
API rule violation: names_match,github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1,ScopeNodeSpec,LinkID
|
||||
API rule violation: streaming_list_type_json_tags,github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1,FindScopeNodeChildrenResults,Items
|
||||
API rule violation: streaming_list_type_json_tags,github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1,ScopeDashboardBindingList,Items
|
||||
API rule violation: streaming_list_type_json_tags,github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1,ScopeList,Items
|
||||
API rule violation: streaming_list_type_json_tags,github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1,ScopeNavigationList,Items
|
||||
API rule violation: streaming_list_type_json_tags,github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1,ScopeNodeList,Items
|
|
@ -1856,11 +1856,6 @@
|
|||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/commandPalette/actions/recentScopesActions.ts": {
|
||||
"react-hooks/rules-of-hooks": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/commandPalette/actions/scopeActions.tsx": {
|
||||
"react-hooks/rules-of-hooks": {
|
||||
"count": 4
|
||||
|
@ -4542,11 +4537,6 @@
|
|||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/panel/logs/types.ts": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/plugins/panel/nodeGraph/Edge.tsx": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 1
|
||||
|
|
2
go.mod
2
go.mod
|
@ -246,6 +246,7 @@ require (
|
|||
github.com/grafana/grafana/apps/plugins v0.0.0 // @grafana/plugins-platform-backend
|
||||
github.com/grafana/grafana/apps/preferences v0.0.0 // @grafana/grafana-app-platform-squad
|
||||
github.com/grafana/grafana/apps/provisioning v0.0.0 // @grafana/grafana-app-platform-squad
|
||||
github.com/grafana/grafana/apps/scope v0.0.0 // @grafana/grafana-operator-experience-squad
|
||||
github.com/grafana/grafana/apps/secret v0.0.0 // @grafana/grafana-operator-experience-squad
|
||||
github.com/grafana/grafana/apps/shorturl v0.0.0 // @grafana/sharing-squad
|
||||
github.com/grafana/grafana/pkg/aggregator v0.0.0 // @grafana/grafana-app-platform-squad
|
||||
|
@ -274,6 +275,7 @@ replace (
|
|||
github.com/grafana/grafana/apps/plugins => ./apps/plugins
|
||||
github.com/grafana/grafana/apps/preferences => ./apps/preferences
|
||||
github.com/grafana/grafana/apps/provisioning => ./apps/provisioning
|
||||
github.com/grafana/grafana/apps/scope => ./apps/scope
|
||||
github.com/grafana/grafana/apps/secret => ./apps/secret
|
||||
github.com/grafana/grafana/apps/shorturl => ./apps/shorturl
|
||||
|
||||
|
|
1
go.work
1
go.work
|
@ -18,6 +18,7 @@ use (
|
|||
./apps/plugins
|
||||
./apps/preferences
|
||||
./apps/provisioning
|
||||
./apps/scope
|
||||
./apps/secret
|
||||
./apps/shorturl
|
||||
./pkg/aggregator
|
||||
|
|
|
@ -90,6 +90,7 @@ grafana::codegen:run apps/dashboard/pkg
|
|||
grafana::codegen:run apps/provisioning/pkg
|
||||
grafana::codegen:run apps/folder/pkg
|
||||
grafana::codegen:run apps/preferences/pkg
|
||||
grafana::codegen:run apps/scope/pkg
|
||||
grafana::codegen:run apps/alerting/alertenrichment/pkg
|
||||
|
||||
if [ -d "pkg/extensions/apis" ]; then
|
||||
|
|
|
@ -1,5 +1,12 @@
|
|||
import { ThemeColors } from './createColors';
|
||||
import { ThemeShadows } from './createShadows';
|
||||
import type { Radii } from './createShape';
|
||||
import type { ThemeSpacingTokens } from './createSpacing';
|
||||
|
||||
interface MenuComponentTokens {
|
||||
borderRadius: keyof Radii;
|
||||
padding: ThemeSpacingTokens;
|
||||
}
|
||||
|
||||
/** @beta */
|
||||
export interface ThemeComponents {
|
||||
|
@ -53,6 +60,7 @@ export interface ThemeComponents {
|
|||
rowHoverBackground: string;
|
||||
rowSelected: string;
|
||||
};
|
||||
menu: MenuComponentTokens;
|
||||
}
|
||||
|
||||
export function createComponents(colors: ThemeColors, shadows: ThemeShadows): ThemeComponents {
|
||||
|
@ -71,6 +79,11 @@ export function createComponents(colors: ThemeColors, shadows: ThemeShadows): Th
|
|||
background: colors.mode === 'dark' ? colors.background.canvas : colors.background.primary,
|
||||
};
|
||||
|
||||
const menu: MenuComponentTokens = {
|
||||
borderRadius: 'default',
|
||||
padding: 0.5,
|
||||
};
|
||||
|
||||
return {
|
||||
height: {
|
||||
sm: 3,
|
||||
|
@ -114,5 +127,6 @@ export function createComponents(colors: ThemeColors, shadows: ThemeShadows): Th
|
|||
rowHoverBackground: colors.action.hover,
|
||||
rowSelected: colors.action.selected,
|
||||
},
|
||||
menu,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -165,6 +165,8 @@ export type PluginExtensionOpenModalOptions = {
|
|||
|
||||
export type PluginExtensionEventHelpers<Context extends object = object> = {
|
||||
context?: Readonly<Context>;
|
||||
// The ID of the extension point that triggered this event
|
||||
extensionPointId: string;
|
||||
// Opens a modal dialog and renders the provided React component inside it
|
||||
openModal: (options: PluginExtensionOpenModalOptions) => void;
|
||||
/**
|
||||
|
|
|
@ -41,6 +41,7 @@ export interface Options {
|
|||
showCommonLabels: boolean;
|
||||
showControls?: boolean;
|
||||
showLabels: boolean;
|
||||
showLogAttributes?: boolean;
|
||||
showLogContextToggle: boolean;
|
||||
showTime: boolean;
|
||||
sortOrder: common.LogsSortOrder;
|
||||
|
|
|
@ -37,7 +37,7 @@ const listFoldersHandler = () =>
|
|||
const limit = parseInt(url.searchParams.get('limit') ?? '1000', 10);
|
||||
const page = parseInt(url.searchParams.get('page') ?? '1', 10);
|
||||
|
||||
const tree = permission === 'Edit' ? mockTreeThatViewersCanEdit : mockTree;
|
||||
const tree = permission?.toLowerCase() === 'edit' ? mockTreeThatViewersCanEdit : mockTree;
|
||||
|
||||
// reconstruct a folder API response from the flat tree fixture
|
||||
const folders = tree
|
||||
|
|
|
@ -14,6 +14,9 @@ export interface ErrorBoundaryApi {
|
|||
}
|
||||
|
||||
interface Props {
|
||||
/** Name of the error boundary. Used when reporting errors in Faro. */
|
||||
boundaryName?: string;
|
||||
|
||||
children: (r: ErrorBoundaryApi) => ReactNode;
|
||||
/** Will re-render children after error if recover values changes */
|
||||
dependencies?: unknown[];
|
||||
|
@ -37,10 +40,15 @@ export class ErrorBoundary extends PureComponent<Props, State> {
|
|||
};
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||||
const logger = this.props.errorLogger ?? faro?.api?.pushError;
|
||||
|
||||
if (logger) {
|
||||
logger(error);
|
||||
if (this.props.errorLogger) {
|
||||
this.props.errorLogger(error);
|
||||
} else {
|
||||
faro?.api?.pushError(error, {
|
||||
type: 'boundary',
|
||||
context: {
|
||||
source: this.props.boundaryName ?? 'unknown',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({ error, errorInfo });
|
||||
|
@ -85,6 +93,9 @@ export class ErrorBoundary extends PureComponent<Props, State> {
|
|||
* @public
|
||||
*/
|
||||
export interface ErrorBoundaryAlertProps {
|
||||
/** Name of the error boundary. Used when reporting errors in Faro. */
|
||||
boundaryName?: string;
|
||||
|
||||
/** Title for the error boundary alert */
|
||||
title?: string;
|
||||
|
||||
|
@ -107,10 +118,10 @@ export class ErrorBoundaryAlert extends PureComponent<ErrorBoundaryAlertProps> {
|
|||
};
|
||||
|
||||
render() {
|
||||
const { title, children, style, dependencies, errorLogger } = this.props;
|
||||
const { title, children, style, dependencies, errorLogger, boundaryName } = this.props;
|
||||
|
||||
return (
|
||||
<ErrorBoundary dependencies={dependencies} errorLogger={errorLogger}>
|
||||
<ErrorBoundary dependencies={dependencies} errorLogger={errorLogger} boundaryName={boundaryName}>
|
||||
{({ error, errorInfo }) => {
|
||||
if (!errorInfo) {
|
||||
return children;
|
||||
|
|
|
@ -25,6 +25,7 @@ export interface MenuProps extends React.HTMLAttributes<HTMLDivElement> {
|
|||
const MenuComp = React.forwardRef<HTMLDivElement, MenuProps>(
|
||||
({ header, children, ariaLabel, onOpen, onClose, onKeyDown, ...otherProps }, forwardedRef) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const componentTokens = useComponentTokens();
|
||||
|
||||
const localRef = useRef<HTMLDivElement>(null);
|
||||
useImperativeHandle(forwardedRef, () => localRef.current!);
|
||||
|
@ -36,12 +37,11 @@ const MenuComp = React.forwardRef<HTMLDivElement, MenuProps>(
|
|||
{...otherProps}
|
||||
aria-label={ariaLabel}
|
||||
backgroundColor="elevated"
|
||||
borderRadius="default"
|
||||
borderRadius={componentTokens.borderRadius}
|
||||
boxShadow="z3"
|
||||
display="inline-block"
|
||||
onKeyDown={handleKeys}
|
||||
paddingX={0.5}
|
||||
paddingY={0.5}
|
||||
padding={componentTokens.padding}
|
||||
ref={localRef}
|
||||
role="menu"
|
||||
tabIndex={-1}
|
||||
|
@ -70,6 +70,18 @@ export const Menu = Object.assign(MenuComp, {
|
|||
Group: MenuGroup,
|
||||
});
|
||||
|
||||
const useComponentTokens = () =>
|
||||
useStyles2((theme: GrafanaTheme2) => {
|
||||
const {
|
||||
components: { menu },
|
||||
} = theme;
|
||||
|
||||
return {
|
||||
padding: menu.padding,
|
||||
borderRadius: menu.borderRadius,
|
||||
};
|
||||
});
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
header: css({
|
||||
|
|
|
@ -6,7 +6,7 @@ import { GrafanaTheme2, LinkTarget } from '@grafana/data';
|
|||
import { t } from '@grafana/i18n';
|
||||
|
||||
import { useStyles2 } from '../../themes/ThemeContext';
|
||||
import { getFocusStyles } from '../../themes/mixins';
|
||||
import { getFocusStyles, getInternalRadius } from '../../themes/mixins';
|
||||
import { IconName } from '../../types/icon';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
import { Stack } from '../Layout/Stack/Stack';
|
||||
|
@ -213,6 +213,8 @@ export const MenuItem = React.memo(
|
|||
MenuItem.displayName = 'MenuItem';
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
const menuPadding = theme.components.menu.padding * theme.spacing.gridSize;
|
||||
|
||||
return {
|
||||
item: css({
|
||||
background: 'none',
|
||||
|
@ -225,7 +227,7 @@ const getStyles = (theme: GrafanaTheme2) => {
|
|||
justifyContent: 'center',
|
||||
padding: theme.spacing(0.5, 1.5),
|
||||
minHeight: theme.spacing(4),
|
||||
borderRadius: theme.shape.radius.default,
|
||||
borderRadius: getInternalRadius(theme, menuPadding, { parentBorderWidth: 0 }),
|
||||
margin: 0,
|
||||
border: 'none',
|
||||
width: '100%',
|
||||
|
|
|
@ -57,4 +57,5 @@ import (
|
|||
_ "github.com/grafana/tempo/pkg/traceql"
|
||||
|
||||
_ "github.com/grafana/grafana/apps/alerting/alertenrichment/pkg/apis/alertenrichment/v1beta1"
|
||||
_ "github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1"
|
||||
)
|
||||
|
|
|
@ -56,6 +56,8 @@ func (s *store) Get(ctx context.Context, ID int64) (*auth.ExternalSession, error
|
|||
return externalSession, nil
|
||||
}
|
||||
|
||||
// List returns a list of external sessions that match the given query.
|
||||
// If the result set contains more than one entry, the entries are sorted by ID in descending order.
|
||||
func (s *store) List(ctx context.Context, query *auth.ListExternalSessionQuery) ([]*auth.ExternalSession, error) {
|
||||
ctx, span := s.tracer.Start(ctx, "externalsession.List")
|
||||
defer span.End()
|
||||
|
@ -65,6 +67,10 @@ func (s *store) List(ctx context.Context, query *auth.ListExternalSessionQuery)
|
|||
externalSession.ID = query.ID
|
||||
}
|
||||
|
||||
if query.UserID != 0 {
|
||||
externalSession.UserID = query.UserID
|
||||
}
|
||||
|
||||
hash := sha256.New()
|
||||
|
||||
if query.SessionID != "" {
|
||||
|
@ -80,7 +86,7 @@ func (s *store) List(ctx context.Context, query *auth.ListExternalSessionQuery)
|
|||
|
||||
queryResult := make([]*auth.ExternalSession, 0)
|
||||
err := s.sqlStore.WithDbSession(ctx, func(sess *db.Session) error {
|
||||
return sess.Find(&queryResult, externalSession)
|
||||
return sess.Desc("id").Find(&queryResult, externalSession)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
@ -51,6 +51,7 @@ type UpdateExternalSessionCommand struct {
|
|||
|
||||
type ListExternalSessionQuery struct {
|
||||
ID int64
|
||||
UserID int64
|
||||
NameID string
|
||||
SessionID string
|
||||
}
|
||||
|
|
|
@ -93,7 +93,11 @@ func (s *OAuthTokenSync) SyncOauthTokenHook(ctx context.Context, id *authn.Ident
|
|||
updateCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
token, refreshErr := s.service.TryTokenRefresh(updateCtx, id, id.SessionToken)
|
||||
token, refreshErr := s.service.TryTokenRefresh(updateCtx, id, &oauthtoken.TokenRefreshMetadata{
|
||||
ExternalSessionID: id.SessionToken.ExternalSessionId,
|
||||
AuthModule: id.GetAuthenticatedBy(),
|
||||
AuthID: id.GetAuthID(),
|
||||
})
|
||||
if refreshErr != nil {
|
||||
if errors.Is(refreshErr, context.Canceled) {
|
||||
return nil, nil
|
||||
|
@ -107,7 +111,7 @@ func (s *OAuthTokenSync) SyncOauthTokenHook(ctx context.Context, id *authn.Ident
|
|||
ctxLogger.Error("Failed to refresh OAuth access token", "id", id.ID, "error", refreshErr)
|
||||
|
||||
// log the user out
|
||||
if err := s.sessionService.RevokeToken(ctx, id.SessionToken, false); err != nil {
|
||||
if err := s.sessionService.RevokeToken(ctx, id.SessionToken, false); err != nil && !errors.Is(err, auth.ErrUserTokenNotFound) {
|
||||
ctxLogger.Warn("Failed to revoke session token", "id", id.ID, "tokenId", id.SessionToken.Id, "error", err)
|
||||
}
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ import (
|
|||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/login"
|
||||
"github.com/grafana/grafana/pkg/services/oauthtoken"
|
||||
"github.com/grafana/grafana/pkg/services/oauthtoken/oauthtokentest"
|
||||
)
|
||||
|
||||
|
@ -77,6 +78,14 @@ func TestOAuthTokenSync_SyncOAuthTokenHook(t *testing.T) {
|
|||
expectRevokeTokenCalled: false,
|
||||
expectToken: &login.UserAuth{OAuthExpiry: time.Now().Add(10 * time.Minute)},
|
||||
},
|
||||
{
|
||||
desc: "should not invalidate session if token refresh fails with no refresh token",
|
||||
identity: &authn.Identity{ID: "1", Type: claims.TypeUser, SessionToken: &auth.UserToken{}, AuthenticatedBy: login.AzureADAuthModule},
|
||||
expectedTryRefreshErr: oauthtoken.ErrNoRefreshTokenFound,
|
||||
expectTryRefreshTokenCalled: true,
|
||||
expectRevokeTokenCalled: true,
|
||||
expectedErr: oauthtoken.ErrNoRefreshTokenFound,
|
||||
},
|
||||
|
||||
// TODO: address coverage of oauthtoken sync
|
||||
}
|
||||
|
@ -89,7 +98,7 @@ func TestOAuthTokenSync_SyncOAuthTokenHook(t *testing.T) {
|
|||
)
|
||||
|
||||
service := &oauthtokentest.MockOauthTokenService{
|
||||
TryTokenRefreshFunc: func(ctx context.Context, usr identity.Requester, _ *auth.UserToken) (*oauth2.Token, error) {
|
||||
TryTokenRefreshFunc: func(ctx context.Context, usr identity.Requester, _ *oauthtoken.TokenRefreshMetadata) (*oauth2.Token, error) {
|
||||
tryRefreshCalled = true
|
||||
return nil, tt.expectedTryRefreshErr
|
||||
},
|
||||
|
|
|
@ -297,7 +297,9 @@ func (c *OAuth) Logout(ctx context.Context, user identity.Requester, sessionToke
|
|||
|
||||
ctxLogger := c.log.FromContext(ctx).New("userID", userID)
|
||||
|
||||
if err := c.oauthService.InvalidateOAuthTokens(ctx, user, sessionToken); err != nil {
|
||||
if err := c.oauthService.InvalidateOAuthTokens(ctx, user, &oauthtoken.TokenRefreshMetadata{
|
||||
ExternalSessionID: sessionToken.ExternalSessionId,
|
||||
AuthModule: user.GetAuthenticatedBy()}); err != nil {
|
||||
ctxLogger.Error("Failed to invalidate tokens", "error", err)
|
||||
}
|
||||
|
||||
|
|
|
@ -19,10 +19,12 @@ import (
|
|||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/login/social"
|
||||
"github.com/grafana/grafana/pkg/login/social/socialtest"
|
||||
"github.com/grafana/grafana/pkg/models/usertoken"
|
||||
"github.com/grafana/grafana/pkg/services/auth"
|
||||
"github.com/grafana/grafana/pkg/services/authn"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/login"
|
||||
"github.com/grafana/grafana/pkg/services/oauthtoken"
|
||||
"github.com/grafana/grafana/pkg/services/oauthtoken/oauthtokentest"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
|
@ -481,7 +483,7 @@ func TestOAuth_Logout(t *testing.T) {
|
|||
"id_token": "some.id.token",
|
||||
})
|
||||
},
|
||||
InvalidateOAuthTokensFunc: func(_ context.Context, _ identity.Requester, _ *auth.UserToken) error {
|
||||
InvalidateOAuthTokensFunc: func(_ context.Context, _ identity.Requester, _ *oauthtoken.TokenRefreshMetadata) error {
|
||||
invalidateTokenCalled = true
|
||||
return nil
|
||||
},
|
||||
|
@ -492,7 +494,7 @@ func TestOAuth_Logout(t *testing.T) {
|
|||
}
|
||||
c := ProvideOAuth(authn.ClientWithPrefix("azuread"), tt.cfg, mockService, fakeSocialSvc, &setting.OSSImpl{Cfg: tt.cfg}, featuremgmt.WithFeatures(), tracing.InitializeTracerForTest())
|
||||
|
||||
redirect, ok := c.Logout(context.Background(), &authn.Identity{ID: "1", Type: claims.TypeUser}, nil)
|
||||
redirect, ok := c.Logout(context.Background(), &authn.Identity{ID: "1", Type: claims.TypeUser}, &usertoken.UserToken{})
|
||||
|
||||
assert.Equal(t, tt.expectedOK, ok)
|
||||
if tt.expectedOK {
|
||||
|
|
|
@ -760,12 +760,10 @@ func (s *Service) listPermission(ctx context.Context, scopeMap map[string]bool,
|
|||
cacheHit := false
|
||||
if t.HasFolderSupport() {
|
||||
var err error
|
||||
ok = false
|
||||
if !req.Options.SkipCache {
|
||||
tree, ok = s.getCachedFolderTree(ctx, req.Namespace)
|
||||
cacheHit = true
|
||||
tree, cacheHit = s.getCachedFolderTree(ctx, req.Namespace)
|
||||
}
|
||||
if !ok {
|
||||
if !cacheHit {
|
||||
tree, err = s.buildFolderTree(ctx, req.Namespace)
|
||||
if err != nil {
|
||||
ctxLogger.Error("could not build folder and dashboard tree", "error", err)
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"strings"
|
||||
)
|
||||
|
||||
//go:generate mockery --name AuthInfoService --structname MockAuthInfoService --outpkg authinfotest --filename auth_info_service_mock.go --output ./authinfotest/
|
||||
type AuthInfoService interface {
|
||||
GetAuthInfo(ctx context.Context, query *GetAuthInfoQuery) (*UserAuth, error)
|
||||
GetUserLabels(ctx context.Context, query GetUserLabelsQuery) (map[int64]string, error)
|
||||
|
|
|
@ -0,0 +1,765 @@
|
|||
// Code generated by mockery; DO NOT EDIT.
|
||||
// github.com/vektra/mockery
|
||||
// template: testify
|
||||
|
||||
package authinfotest
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/login"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// NewMockAuthInfoService creates a new instance of MockAuthInfoService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewMockAuthInfoService(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *MockAuthInfoService {
|
||||
mock := &MockAuthInfoService{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
|
||||
// MockAuthInfoService is an autogenerated mock type for the AuthInfoService type
|
||||
type MockAuthInfoService struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type MockAuthInfoService_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *MockAuthInfoService) EXPECT() *MockAuthInfoService_Expecter {
|
||||
return &MockAuthInfoService_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// DeleteUserAuthInfo provides a mock function for the type MockAuthInfoService
|
||||
func (_mock *MockAuthInfoService) DeleteUserAuthInfo(ctx context.Context, userID int64) error {
|
||||
ret := _mock.Called(ctx, userID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for DeleteUserAuthInfo")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, int64) error); ok {
|
||||
r0 = returnFunc(ctx, userID)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockAuthInfoService_DeleteUserAuthInfo_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteUserAuthInfo'
|
||||
type MockAuthInfoService_DeleteUserAuthInfo_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// DeleteUserAuthInfo is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - userID int64
|
||||
func (_e *MockAuthInfoService_Expecter) DeleteUserAuthInfo(ctx interface{}, userID interface{}) *MockAuthInfoService_DeleteUserAuthInfo_Call {
|
||||
return &MockAuthInfoService_DeleteUserAuthInfo_Call{Call: _e.mock.On("DeleteUserAuthInfo", ctx, userID)}
|
||||
}
|
||||
|
||||
func (_c *MockAuthInfoService_DeleteUserAuthInfo_Call) Run(run func(ctx context.Context, userID int64)) *MockAuthInfoService_DeleteUserAuthInfo_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 int64
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(int64)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockAuthInfoService_DeleteUserAuthInfo_Call) Return(err error) *MockAuthInfoService_DeleteUserAuthInfo_Call {
|
||||
_c.Call.Return(err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockAuthInfoService_DeleteUserAuthInfo_Call) RunAndReturn(run func(ctx context.Context, userID int64) error) *MockAuthInfoService_DeleteUserAuthInfo_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetAuthInfo provides a mock function for the type MockAuthInfoService
|
||||
func (_mock *MockAuthInfoService) GetAuthInfo(ctx context.Context, query *login.GetAuthInfoQuery) (*login.UserAuth, error) {
|
||||
ret := _mock.Called(ctx, query)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetAuthInfo")
|
||||
}
|
||||
|
||||
var r0 *login.UserAuth
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, *login.GetAuthInfoQuery) (*login.UserAuth, error)); ok {
|
||||
return returnFunc(ctx, query)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, *login.GetAuthInfoQuery) *login.UserAuth); ok {
|
||||
r0 = returnFunc(ctx, query)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*login.UserAuth)
|
||||
}
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, *login.GetAuthInfoQuery) error); ok {
|
||||
r1 = returnFunc(ctx, query)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockAuthInfoService_GetAuthInfo_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAuthInfo'
|
||||
type MockAuthInfoService_GetAuthInfo_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetAuthInfo is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - query *login.GetAuthInfoQuery
|
||||
func (_e *MockAuthInfoService_Expecter) GetAuthInfo(ctx interface{}, query interface{}) *MockAuthInfoService_GetAuthInfo_Call {
|
||||
return &MockAuthInfoService_GetAuthInfo_Call{Call: _e.mock.On("GetAuthInfo", ctx, query)}
|
||||
}
|
||||
|
||||
func (_c *MockAuthInfoService_GetAuthInfo_Call) Run(run func(ctx context.Context, query *login.GetAuthInfoQuery)) *MockAuthInfoService_GetAuthInfo_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 *login.GetAuthInfoQuery
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(*login.GetAuthInfoQuery)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockAuthInfoService_GetAuthInfo_Call) Return(userAuth *login.UserAuth, err error) *MockAuthInfoService_GetAuthInfo_Call {
|
||||
_c.Call.Return(userAuth, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockAuthInfoService_GetAuthInfo_Call) RunAndReturn(run func(ctx context.Context, query *login.GetAuthInfoQuery) (*login.UserAuth, error)) *MockAuthInfoService_GetAuthInfo_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetUserLabels provides a mock function for the type MockAuthInfoService
|
||||
func (_mock *MockAuthInfoService) GetUserLabels(ctx context.Context, query login.GetUserLabelsQuery) (map[int64]string, error) {
|
||||
ret := _mock.Called(ctx, query)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetUserLabels")
|
||||
}
|
||||
|
||||
var r0 map[int64]string
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, login.GetUserLabelsQuery) (map[int64]string, error)); ok {
|
||||
return returnFunc(ctx, query)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, login.GetUserLabelsQuery) map[int64]string); ok {
|
||||
r0 = returnFunc(ctx, query)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(map[int64]string)
|
||||
}
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, login.GetUserLabelsQuery) error); ok {
|
||||
r1 = returnFunc(ctx, query)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockAuthInfoService_GetUserLabels_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetUserLabels'
|
||||
type MockAuthInfoService_GetUserLabels_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetUserLabels is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - query login.GetUserLabelsQuery
|
||||
func (_e *MockAuthInfoService_Expecter) GetUserLabels(ctx interface{}, query interface{}) *MockAuthInfoService_GetUserLabels_Call {
|
||||
return &MockAuthInfoService_GetUserLabels_Call{Call: _e.mock.On("GetUserLabels", ctx, query)}
|
||||
}
|
||||
|
||||
func (_c *MockAuthInfoService_GetUserLabels_Call) Run(run func(ctx context.Context, query login.GetUserLabelsQuery)) *MockAuthInfoService_GetUserLabels_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 login.GetUserLabelsQuery
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(login.GetUserLabelsQuery)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockAuthInfoService_GetUserLabels_Call) Return(int64ToString map[int64]string, err error) *MockAuthInfoService_GetUserLabels_Call {
|
||||
_c.Call.Return(int64ToString, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockAuthInfoService_GetUserLabels_Call) RunAndReturn(run func(ctx context.Context, query login.GetUserLabelsQuery) (map[int64]string, error)) *MockAuthInfoService_GetUserLabels_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetAuthInfo provides a mock function for the type MockAuthInfoService
|
||||
func (_mock *MockAuthInfoService) SetAuthInfo(ctx context.Context, cmd *login.SetAuthInfoCommand) error {
|
||||
ret := _mock.Called(ctx, cmd)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for SetAuthInfo")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, *login.SetAuthInfoCommand) error); ok {
|
||||
r0 = returnFunc(ctx, cmd)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockAuthInfoService_SetAuthInfo_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetAuthInfo'
|
||||
type MockAuthInfoService_SetAuthInfo_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// SetAuthInfo is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - cmd *login.SetAuthInfoCommand
|
||||
func (_e *MockAuthInfoService_Expecter) SetAuthInfo(ctx interface{}, cmd interface{}) *MockAuthInfoService_SetAuthInfo_Call {
|
||||
return &MockAuthInfoService_SetAuthInfo_Call{Call: _e.mock.On("SetAuthInfo", ctx, cmd)}
|
||||
}
|
||||
|
||||
func (_c *MockAuthInfoService_SetAuthInfo_Call) Run(run func(ctx context.Context, cmd *login.SetAuthInfoCommand)) *MockAuthInfoService_SetAuthInfo_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 *login.SetAuthInfoCommand
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(*login.SetAuthInfoCommand)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockAuthInfoService_SetAuthInfo_Call) Return(err error) *MockAuthInfoService_SetAuthInfo_Call {
|
||||
_c.Call.Return(err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockAuthInfoService_SetAuthInfo_Call) RunAndReturn(run func(ctx context.Context, cmd *login.SetAuthInfoCommand) error) *MockAuthInfoService_SetAuthInfo_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// UpdateAuthInfo provides a mock function for the type MockAuthInfoService
|
||||
func (_mock *MockAuthInfoService) UpdateAuthInfo(ctx context.Context, cmd *login.UpdateAuthInfoCommand) error {
|
||||
ret := _mock.Called(ctx, cmd)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for UpdateAuthInfo")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, *login.UpdateAuthInfoCommand) error); ok {
|
||||
r0 = returnFunc(ctx, cmd)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockAuthInfoService_UpdateAuthInfo_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateAuthInfo'
|
||||
type MockAuthInfoService_UpdateAuthInfo_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// UpdateAuthInfo is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - cmd *login.UpdateAuthInfoCommand
|
||||
func (_e *MockAuthInfoService_Expecter) UpdateAuthInfo(ctx interface{}, cmd interface{}) *MockAuthInfoService_UpdateAuthInfo_Call {
|
||||
return &MockAuthInfoService_UpdateAuthInfo_Call{Call: _e.mock.On("UpdateAuthInfo", ctx, cmd)}
|
||||
}
|
||||
|
||||
func (_c *MockAuthInfoService_UpdateAuthInfo_Call) Run(run func(ctx context.Context, cmd *login.UpdateAuthInfoCommand)) *MockAuthInfoService_UpdateAuthInfo_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 *login.UpdateAuthInfoCommand
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(*login.UpdateAuthInfoCommand)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockAuthInfoService_UpdateAuthInfo_Call) Return(err error) *MockAuthInfoService_UpdateAuthInfo_Call {
|
||||
_c.Call.Return(err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockAuthInfoService_UpdateAuthInfo_Call) RunAndReturn(run func(ctx context.Context, cmd *login.UpdateAuthInfoCommand) error) *MockAuthInfoService_UpdateAuthInfo_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NewMockStore creates a new instance of MockStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewMockStore(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *MockStore {
|
||||
mock := &MockStore{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
|
||||
// MockStore is an autogenerated mock type for the Store type
|
||||
type MockStore struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type MockStore_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *MockStore) EXPECT() *MockStore_Expecter {
|
||||
return &MockStore_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// DeleteUserAuthInfo provides a mock function for the type MockStore
|
||||
func (_mock *MockStore) DeleteUserAuthInfo(ctx context.Context, userID int64) error {
|
||||
ret := _mock.Called(ctx, userID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for DeleteUserAuthInfo")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, int64) error); ok {
|
||||
r0 = returnFunc(ctx, userID)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockStore_DeleteUserAuthInfo_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteUserAuthInfo'
|
||||
type MockStore_DeleteUserAuthInfo_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// DeleteUserAuthInfo is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - userID int64
|
||||
func (_e *MockStore_Expecter) DeleteUserAuthInfo(ctx interface{}, userID interface{}) *MockStore_DeleteUserAuthInfo_Call {
|
||||
return &MockStore_DeleteUserAuthInfo_Call{Call: _e.mock.On("DeleteUserAuthInfo", ctx, userID)}
|
||||
}
|
||||
|
||||
func (_c *MockStore_DeleteUserAuthInfo_Call) Run(run func(ctx context.Context, userID int64)) *MockStore_DeleteUserAuthInfo_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 int64
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(int64)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockStore_DeleteUserAuthInfo_Call) Return(err error) *MockStore_DeleteUserAuthInfo_Call {
|
||||
_c.Call.Return(err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockStore_DeleteUserAuthInfo_Call) RunAndReturn(run func(ctx context.Context, userID int64) error) *MockStore_DeleteUserAuthInfo_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetAuthInfo provides a mock function for the type MockStore
|
||||
func (_mock *MockStore) GetAuthInfo(ctx context.Context, query *login.GetAuthInfoQuery) (*login.UserAuth, error) {
|
||||
ret := _mock.Called(ctx, query)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetAuthInfo")
|
||||
}
|
||||
|
||||
var r0 *login.UserAuth
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, *login.GetAuthInfoQuery) (*login.UserAuth, error)); ok {
|
||||
return returnFunc(ctx, query)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, *login.GetAuthInfoQuery) *login.UserAuth); ok {
|
||||
r0 = returnFunc(ctx, query)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*login.UserAuth)
|
||||
}
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, *login.GetAuthInfoQuery) error); ok {
|
||||
r1 = returnFunc(ctx, query)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockStore_GetAuthInfo_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAuthInfo'
|
||||
type MockStore_GetAuthInfo_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetAuthInfo is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - query *login.GetAuthInfoQuery
|
||||
func (_e *MockStore_Expecter) GetAuthInfo(ctx interface{}, query interface{}) *MockStore_GetAuthInfo_Call {
|
||||
return &MockStore_GetAuthInfo_Call{Call: _e.mock.On("GetAuthInfo", ctx, query)}
|
||||
}
|
||||
|
||||
func (_c *MockStore_GetAuthInfo_Call) Run(run func(ctx context.Context, query *login.GetAuthInfoQuery)) *MockStore_GetAuthInfo_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 *login.GetAuthInfoQuery
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(*login.GetAuthInfoQuery)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockStore_GetAuthInfo_Call) Return(userAuth *login.UserAuth, err error) *MockStore_GetAuthInfo_Call {
|
||||
_c.Call.Return(userAuth, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockStore_GetAuthInfo_Call) RunAndReturn(run func(ctx context.Context, query *login.GetAuthInfoQuery) (*login.UserAuth, error)) *MockStore_GetAuthInfo_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetUserLabels provides a mock function for the type MockStore
|
||||
func (_mock *MockStore) GetUserLabels(ctx context.Context, query login.GetUserLabelsQuery) (map[int64]string, error) {
|
||||
ret := _mock.Called(ctx, query)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetUserLabels")
|
||||
}
|
||||
|
||||
var r0 map[int64]string
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, login.GetUserLabelsQuery) (map[int64]string, error)); ok {
|
||||
return returnFunc(ctx, query)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, login.GetUserLabelsQuery) map[int64]string); ok {
|
||||
r0 = returnFunc(ctx, query)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(map[int64]string)
|
||||
}
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, login.GetUserLabelsQuery) error); ok {
|
||||
r1 = returnFunc(ctx, query)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockStore_GetUserLabels_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetUserLabels'
|
||||
type MockStore_GetUserLabels_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetUserLabels is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - query login.GetUserLabelsQuery
|
||||
func (_e *MockStore_Expecter) GetUserLabels(ctx interface{}, query interface{}) *MockStore_GetUserLabels_Call {
|
||||
return &MockStore_GetUserLabels_Call{Call: _e.mock.On("GetUserLabels", ctx, query)}
|
||||
}
|
||||
|
||||
func (_c *MockStore_GetUserLabels_Call) Run(run func(ctx context.Context, query login.GetUserLabelsQuery)) *MockStore_GetUserLabels_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 login.GetUserLabelsQuery
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(login.GetUserLabelsQuery)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockStore_GetUserLabels_Call) Return(int64ToString map[int64]string, err error) *MockStore_GetUserLabels_Call {
|
||||
_c.Call.Return(int64ToString, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockStore_GetUserLabels_Call) RunAndReturn(run func(ctx context.Context, query login.GetUserLabelsQuery) (map[int64]string, error)) *MockStore_GetUserLabels_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetAuthInfo provides a mock function for the type MockStore
|
||||
func (_mock *MockStore) SetAuthInfo(ctx context.Context, cmd *login.SetAuthInfoCommand) error {
|
||||
ret := _mock.Called(ctx, cmd)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for SetAuthInfo")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, *login.SetAuthInfoCommand) error); ok {
|
||||
r0 = returnFunc(ctx, cmd)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockStore_SetAuthInfo_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetAuthInfo'
|
||||
type MockStore_SetAuthInfo_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// SetAuthInfo is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - cmd *login.SetAuthInfoCommand
|
||||
func (_e *MockStore_Expecter) SetAuthInfo(ctx interface{}, cmd interface{}) *MockStore_SetAuthInfo_Call {
|
||||
return &MockStore_SetAuthInfo_Call{Call: _e.mock.On("SetAuthInfo", ctx, cmd)}
|
||||
}
|
||||
|
||||
func (_c *MockStore_SetAuthInfo_Call) Run(run func(ctx context.Context, cmd *login.SetAuthInfoCommand)) *MockStore_SetAuthInfo_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 *login.SetAuthInfoCommand
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(*login.SetAuthInfoCommand)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockStore_SetAuthInfo_Call) Return(err error) *MockStore_SetAuthInfo_Call {
|
||||
_c.Call.Return(err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockStore_SetAuthInfo_Call) RunAndReturn(run func(ctx context.Context, cmd *login.SetAuthInfoCommand) error) *MockStore_SetAuthInfo_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// UpdateAuthInfo provides a mock function for the type MockStore
|
||||
func (_mock *MockStore) UpdateAuthInfo(ctx context.Context, cmd *login.UpdateAuthInfoCommand) error {
|
||||
ret := _mock.Called(ctx, cmd)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for UpdateAuthInfo")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, *login.UpdateAuthInfoCommand) error); ok {
|
||||
r0 = returnFunc(ctx, cmd)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockStore_UpdateAuthInfo_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateAuthInfo'
|
||||
type MockStore_UpdateAuthInfo_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// UpdateAuthInfo is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - cmd *login.UpdateAuthInfoCommand
|
||||
func (_e *MockStore_Expecter) UpdateAuthInfo(ctx interface{}, cmd interface{}) *MockStore_UpdateAuthInfo_Call {
|
||||
return &MockStore_UpdateAuthInfo_Call{Call: _e.mock.On("UpdateAuthInfo", ctx, cmd)}
|
||||
}
|
||||
|
||||
func (_c *MockStore_UpdateAuthInfo_Call) Run(run func(ctx context.Context, cmd *login.UpdateAuthInfoCommand)) *MockStore_UpdateAuthInfo_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 *login.UpdateAuthInfoCommand
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(*login.UpdateAuthInfoCommand)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockStore_UpdateAuthInfo_Call) Return(err error) *MockStore_UpdateAuthInfo_Call {
|
||||
_c.Call.Return(err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockStore_UpdateAuthInfo_Call) RunAndReturn(run func(ctx context.Context, cmd *login.UpdateAuthInfoCommand) error) *MockStore_UpdateAuthInfo_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NewMockUserProtectionService creates a new instance of MockUserProtectionService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewMockUserProtectionService(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *MockUserProtectionService {
|
||||
mock := &MockUserProtectionService{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
|
||||
// MockUserProtectionService is an autogenerated mock type for the UserProtectionService type
|
||||
type MockUserProtectionService struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type MockUserProtectionService_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *MockUserProtectionService) EXPECT() *MockUserProtectionService_Expecter {
|
||||
return &MockUserProtectionService_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// AllowUserMapping provides a mock function for the type MockUserProtectionService
|
||||
func (_mock *MockUserProtectionService) AllowUserMapping(user1 *user.User, authModule string) error {
|
||||
ret := _mock.Called(user1, authModule)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for AllowUserMapping")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if returnFunc, ok := ret.Get(0).(func(*user.User, string) error); ok {
|
||||
r0 = returnFunc(user1, authModule)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockUserProtectionService_AllowUserMapping_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AllowUserMapping'
|
||||
type MockUserProtectionService_AllowUserMapping_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// AllowUserMapping is a helper method to define mock.On call
|
||||
// - user1 *user.User
|
||||
// - authModule string
|
||||
func (_e *MockUserProtectionService_Expecter) AllowUserMapping(user1 interface{}, authModule interface{}) *MockUserProtectionService_AllowUserMapping_Call {
|
||||
return &MockUserProtectionService_AllowUserMapping_Call{Call: _e.mock.On("AllowUserMapping", user1, authModule)}
|
||||
}
|
||||
|
||||
func (_c *MockUserProtectionService_AllowUserMapping_Call) Run(run func(user1 *user.User, authModule string)) *MockUserProtectionService_AllowUserMapping_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 *user.User
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(*user.User)
|
||||
}
|
||||
var arg1 string
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(string)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockUserProtectionService_AllowUserMapping_Call) Return(err error) *MockUserProtectionService_AllowUserMapping_Call {
|
||||
_c.Call.Return(err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockUserProtectionService_AllowUserMapping_Call) RunAndReturn(run func(user1 *user.User, authModule string) error) *MockUserProtectionService_AllowUserMapping_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
|
@ -11,6 +11,7 @@ import (
|
|||
"github.com/go-jose/go-jose/v4/jwt"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
|
@ -57,8 +58,14 @@ var _ OAuthTokenService = (*Service)(nil)
|
|||
type OAuthTokenService interface {
|
||||
GetCurrentOAuthToken(context.Context, identity.Requester, *auth.UserToken) *oauth2.Token
|
||||
IsOAuthPassThruEnabled(*datasources.DataSource) bool
|
||||
TryTokenRefresh(context.Context, identity.Requester, *auth.UserToken) (*oauth2.Token, error)
|
||||
InvalidateOAuthTokens(context.Context, identity.Requester, *auth.UserToken) error
|
||||
TryTokenRefresh(context.Context, identity.Requester, *TokenRefreshMetadata) (*oauth2.Token, error)
|
||||
InvalidateOAuthTokens(context.Context, identity.Requester, *TokenRefreshMetadata) error
|
||||
}
|
||||
|
||||
type TokenRefreshMetadata struct {
|
||||
ExternalSessionID int64
|
||||
AuthModule string
|
||||
AuthID string
|
||||
}
|
||||
|
||||
func ProvideService(socialService social.Service, authInfoService login.AuthInfoService, cfg *setting.Cfg, registerer prometheus.Registerer,
|
||||
|
@ -102,51 +109,71 @@ func (o *Service) GetCurrentOAuthToken(ctx context.Context, usr identity.Request
|
|||
|
||||
ctxLogger = ctxLogger.New("userID", userID)
|
||||
|
||||
if !strings.HasPrefix(usr.GetAuthenticatedBy(), "oauth_") {
|
||||
ctxLogger.Warn("The specified user's auth provider is not oauth",
|
||||
"authmodule", usr.GetAuthenticatedBy())
|
||||
tokenRefreshMetadata := &TokenRefreshMetadata{
|
||||
ExternalSessionID: 0,
|
||||
}
|
||||
var persistedToken *oauth2.Token
|
||||
// Find the external session associated with the user and session token
|
||||
// regardless of the improvedExternalSessionHandling feature toggle,
|
||||
// because Grafana writes and updates both tables to make the switch
|
||||
// to the new session handling smoother.
|
||||
externalSession, err := o.getExternalSession(ctx, usr, userID, sessionToken)
|
||||
if err != nil && !errors.Is(err, auth.ErrExternalSessionNotFound) {
|
||||
ctxLogger.Error("Failed to get external session", "error", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// If the feature toggle is enabled, an external session is required.
|
||||
if o.features.IsEnabledGlobally(featuremgmt.FlagImprovedExternalSessionHandling) && (externalSession == nil || errors.Is(err, auth.ErrExternalSessionNotFound)) {
|
||||
ctxLogger.Error("No external session found for user", "userID", userID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// externalSession can be nil if Grafana was updated from a version where the
|
||||
// external session table was not used yet (did not exist) and the user has not logged in since
|
||||
// the version update (therefore no external session was created for the user yet).
|
||||
if externalSession != nil {
|
||||
tokenRefreshMetadata.ExternalSessionID = externalSession.ID
|
||||
}
|
||||
|
||||
authInfo, err := o.AuthInfoService.GetAuthInfo(ctx, &login.GetAuthInfoQuery{
|
||||
UserId: userID,
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, user.ErrUserNotFound) {
|
||||
ctxLogger.Warn("No AuthInfo found for user", "userID", userID)
|
||||
return nil
|
||||
}
|
||||
|
||||
ctxLogger.Error("Failed to fetch AuthInfo for user", "userID", userID, "error", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
tokenRefreshMetadata.AuthID = authInfo.AuthId
|
||||
tokenRefreshMetadata.AuthModule = authInfo.AuthModule
|
||||
|
||||
if !strings.HasPrefix(tokenRefreshMetadata.AuthModule, "oauth_") {
|
||||
ctxLogger.Warn("The specified user's auth provider is not oauth",
|
||||
"authmodule", tokenRefreshMetadata.AuthModule)
|
||||
return nil
|
||||
}
|
||||
|
||||
var persistedToken *oauth2.Token
|
||||
if o.features.IsEnabledGlobally(featuremgmt.FlagImprovedExternalSessionHandling) {
|
||||
externalSession, err := o.sessionService.GetExternalSession(ctx, sessionToken.ExternalSessionId)
|
||||
if err != nil {
|
||||
if errors.Is(err, auth.ErrExternalSessionNotFound) {
|
||||
return nil
|
||||
}
|
||||
ctxLogger.Error("Failed to fetch external session", "error", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
persistedToken = buildOAuthTokenFromExternalSession(externalSession)
|
||||
|
||||
if persistedToken.RefreshToken == "" {
|
||||
return persistedToken
|
||||
}
|
||||
} else {
|
||||
authInfo, ok, _ := o.hasOAuthEntry(ctx, usr)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := checkOAuthRefreshToken(authInfo); err != nil {
|
||||
if errors.Is(err, ErrNoRefreshTokenFound) {
|
||||
return buildOAuthTokenFromAuthInfo(authInfo)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
persistedToken = buildOAuthTokenFromAuthInfo(authInfo)
|
||||
}
|
||||
|
||||
if persistedToken.RefreshToken == "" {
|
||||
return persistedToken
|
||||
}
|
||||
|
||||
refreshNeeded := needTokenRefresh(ctx, persistedToken)
|
||||
if !refreshNeeded {
|
||||
return persistedToken
|
||||
}
|
||||
|
||||
token, err := o.TryTokenRefresh(ctx, usr, sessionToken)
|
||||
token, err := o.TryTokenRefresh(ctx, usr, tokenRefreshMetadata)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrNoRefreshTokenFound) {
|
||||
return persistedToken
|
||||
|
@ -214,7 +241,7 @@ func (o *Service) hasOAuthEntry(ctx context.Context, usr identity.Requester) (*l
|
|||
|
||||
// TryTokenRefresh returns an error in case the OAuth token refresh was unsuccessful
|
||||
// It uses a server lock to prevent getting the Refresh Token multiple times for a given User
|
||||
func (o *Service) TryTokenRefresh(ctx context.Context, usr identity.Requester, sessionToken *auth.UserToken) (*oauth2.Token, error) {
|
||||
func (o *Service) TryTokenRefresh(ctx context.Context, usr identity.Requester, tokenRefreshMetadata *TokenRefreshMetadata) (*oauth2.Token, error) {
|
||||
ctx, span := o.tracer.Start(ctx, "oauthtoken.TryTokenRefresh")
|
||||
defer span.End()
|
||||
|
||||
|
@ -239,14 +266,13 @@ func (o *Service) TryTokenRefresh(ctx context.Context, usr identity.Requester, s
|
|||
|
||||
ctxLogger = ctxLogger.New("userID", userID)
|
||||
|
||||
// get the token's auth provider (f.e. azuread)
|
||||
currAuthenticator := usr.GetAuthenticatedBy()
|
||||
if !strings.HasPrefix(currAuthenticator, "oauth") {
|
||||
ctxLogger.Warn("The specified user's auth provider is not OAuth", "authmodule", currAuthenticator)
|
||||
if !strings.HasPrefix(tokenRefreshMetadata.AuthModule, "oauth_") {
|
||||
ctxLogger.Warn("The specified user's auth provider is not oauth",
|
||||
"authmodule", tokenRefreshMetadata.AuthModule)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
provider := strings.TrimPrefix(currAuthenticator, "oauth_")
|
||||
provider := strings.TrimPrefix(tokenRefreshMetadata.AuthModule, "oauth_")
|
||||
currentOAuthInfo := o.SocialService.GetOAuthInfoProvider(provider)
|
||||
if currentOAuthInfo == nil {
|
||||
ctxLogger.Warn("OAuth provider not found", "provider", provider)
|
||||
|
@ -261,7 +287,7 @@ func (o *Service) TryTokenRefresh(ctx context.Context, usr identity.Requester, s
|
|||
|
||||
lockKey := fmt.Sprintf("oauth-refresh-token-%d", userID)
|
||||
if o.features.IsEnabledGlobally(featuremgmt.FlagImprovedExternalSessionHandling) {
|
||||
lockKey = fmt.Sprintf("oauth-refresh-token-%d-%d", userID, sessionToken.ExternalSessionId)
|
||||
lockKey = fmt.Sprintf("oauth-refresh-token-%d-%d", userID, tokenRefreshMetadata.ExternalSessionID)
|
||||
}
|
||||
|
||||
lockTimeConfig := serverlock.LockTimeConfig{
|
||||
|
@ -290,7 +316,7 @@ func (o *Service) TryTokenRefresh(ctx context.Context, usr identity.Requester, s
|
|||
var persistedToken *oauth2.Token
|
||||
var externalSession *auth.ExternalSession
|
||||
if o.features.IsEnabledGlobally(featuremgmt.FlagImprovedExternalSessionHandling) {
|
||||
externalSession, err = o.sessionService.GetExternalSession(ctx, sessionToken.ExternalSessionId)
|
||||
externalSession, err = o.sessionService.GetExternalSession(ctx, tokenRefreshMetadata.ExternalSessionID)
|
||||
if err != nil {
|
||||
if errors.Is(err, auth.ErrExternalSessionNotFound) {
|
||||
ctxLogger.Error("External session was not found for user", "error", err)
|
||||
|
@ -321,7 +347,7 @@ func (o *Service) TryTokenRefresh(ctx context.Context, usr identity.Requester, s
|
|||
return
|
||||
}
|
||||
|
||||
newToken, cmdErr = o.tryGetOrRefreshOAuthToken(ctx, persistedToken, usr, sessionToken)
|
||||
newToken, cmdErr = o.tryGetOrRefreshOAuthToken(ctx, persistedToken, usr, tokenRefreshMetadata)
|
||||
}, retryOpt)
|
||||
if lockErr != nil {
|
||||
ctxLogger.Error("Failed to obtain token refresh lock", "error", lockErr)
|
||||
|
@ -330,14 +356,14 @@ func (o *Service) TryTokenRefresh(ctx context.Context, usr identity.Requester, s
|
|||
|
||||
// Silence ErrNoRefreshTokenFound
|
||||
if errors.Is(cmdErr, ErrNoRefreshTokenFound) {
|
||||
return nil, nil
|
||||
return nil, ErrNoRefreshTokenFound
|
||||
}
|
||||
|
||||
return newToken, cmdErr
|
||||
}
|
||||
|
||||
// InvalidateOAuthTokens invalidates the OAuth tokens (access_token, refresh_token) and sets the Expiry to default/zero
|
||||
func (o *Service) InvalidateOAuthTokens(ctx context.Context, usr identity.Requester, sessionToken *auth.UserToken) error {
|
||||
func (o *Service) InvalidateOAuthTokens(ctx context.Context, usr identity.Requester, tokenRefreshMetadata *TokenRefreshMetadata) error {
|
||||
userID, err := usr.GetInternalID()
|
||||
if err != nil {
|
||||
logger.Error("Failed to convert user id to int", "id", usr.GetID(), "error", err)
|
||||
|
@ -347,7 +373,7 @@ func (o *Service) InvalidateOAuthTokens(ctx context.Context, usr identity.Reques
|
|||
ctxLogger := logger.FromContext(ctx).New("userID", userID)
|
||||
|
||||
if o.features.IsEnabledGlobally(featuremgmt.FlagImprovedExternalSessionHandling) {
|
||||
err := o.sessionService.UpdateExternalSession(ctx, sessionToken.ExternalSessionId, &auth.UpdateExternalSessionCommand{
|
||||
err := o.sessionService.UpdateExternalSession(ctx, tokenRefreshMetadata.ExternalSessionID, &auth.UpdateExternalSessionCommand{
|
||||
Token: &oauth2.Token{},
|
||||
})
|
||||
if err != nil {
|
||||
|
@ -358,8 +384,8 @@ func (o *Service) InvalidateOAuthTokens(ctx context.Context, usr identity.Reques
|
|||
|
||||
return o.AuthInfoService.UpdateAuthInfo(ctx, &login.UpdateAuthInfoCommand{
|
||||
UserId: userID,
|
||||
AuthModule: usr.GetAuthenticatedBy(),
|
||||
AuthId: usr.GetAuthID(),
|
||||
AuthModule: tokenRefreshMetadata.AuthModule,
|
||||
AuthId: tokenRefreshMetadata.AuthID,
|
||||
OAuthToken: &oauth2.Token{
|
||||
AccessToken: "",
|
||||
RefreshToken: "",
|
||||
|
@ -368,13 +394,14 @@ func (o *Service) InvalidateOAuthTokens(ctx context.Context, usr identity.Reques
|
|||
})
|
||||
}
|
||||
|
||||
func (o *Service) tryGetOrRefreshOAuthToken(ctx context.Context, persistedToken *oauth2.Token, usr identity.Requester, sessionToken *auth.UserToken) (*oauth2.Token, error) {
|
||||
func (o *Service) tryGetOrRefreshOAuthToken(ctx context.Context, persistedToken *oauth2.Token, usr identity.Requester, tokenRefreshMetadata *TokenRefreshMetadata) (*oauth2.Token, error) {
|
||||
ctx, span := o.tracer.Start(ctx, "oauthtoken.tryGetOrRefreshOAuthToken")
|
||||
defer span.End()
|
||||
|
||||
userID, err := usr.GetInternalID()
|
||||
if err != nil {
|
||||
logger.Error("Failed to convert user id to int", "id", usr.GetID(), "error", err)
|
||||
span.SetStatus(codes.Error, "Failed to convert user id to int")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
@ -382,8 +409,11 @@ func (o *Service) tryGetOrRefreshOAuthToken(ctx context.Context, persistedToken
|
|||
|
||||
ctxLogger := logger.FromContext(ctx).New("userID", userID)
|
||||
|
||||
// tryGetOrRefreshOAuthToken assumes that the AuthModule has RefreshToken enabled
|
||||
// which is checked by the caller (TryTokenRefresh)
|
||||
if persistedToken.RefreshToken == "" {
|
||||
ctxLogger.Warn("No refresh token available", "authmodule", usr.GetAuthenticatedBy())
|
||||
ctxLogger.Error("No refresh token available", "authmodule", tokenRefreshMetadata.AuthModule)
|
||||
span.SetStatus(codes.Error, ErrNoRefreshTokenFound.Error())
|
||||
return nil, ErrNoRefreshTokenFound
|
||||
}
|
||||
|
||||
|
@ -392,50 +422,44 @@ func (o *Service) tryGetOrRefreshOAuthToken(ctx context.Context, persistedToken
|
|||
return persistedToken, nil
|
||||
}
|
||||
|
||||
authProvider := usr.GetAuthenticatedBy()
|
||||
connect, err := o.SocialService.GetConnector(authProvider)
|
||||
connect, err := o.SocialService.GetConnector(tokenRefreshMetadata.AuthModule)
|
||||
if err != nil {
|
||||
ctxLogger.Error("Failed to get oauth connector", "provider", authProvider, "error", err)
|
||||
ctxLogger.Error("Failed to get oauth connector", "provider", tokenRefreshMetadata.AuthModule, "error", err)
|
||||
span.SetStatus(codes.Error, "Failed to get oauth connector: "+err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client, err := o.SocialService.GetOAuthHttpClient(authProvider)
|
||||
client, err := o.SocialService.GetOAuthHttpClient(tokenRefreshMetadata.AuthModule)
|
||||
if err != nil {
|
||||
ctxLogger.Error("Failed to get oauth http client", "provider", authProvider, "error", err)
|
||||
ctxLogger.Error("Failed to get oauth http client", "provider", tokenRefreshMetadata.AuthModule, "error", err)
|
||||
span.SetStatus(codes.Error, "Failed to get oauth http client")
|
||||
return nil, err
|
||||
}
|
||||
ctx = context.WithValue(ctx, oauth2.HTTPClient, client)
|
||||
|
||||
start := time.Now()
|
||||
// TokenSource handles refreshing the token if it has expired
|
||||
token, err := connect.TokenSource(ctx, persistedToken).Token()
|
||||
token, refreshErr := connect.TokenSource(ctx, persistedToken).Token()
|
||||
duration := time.Since(start)
|
||||
o.tokenRefreshDuration.WithLabelValues(authProvider, fmt.Sprintf("%t", err == nil)).Observe(duration.Seconds())
|
||||
o.tokenRefreshDuration.WithLabelValues(tokenRefreshMetadata.AuthModule, fmt.Sprintf("%t", err == nil)).Observe(duration.Seconds())
|
||||
|
||||
if err != nil {
|
||||
if refreshErr != nil {
|
||||
span.SetAttributes(attribute.Bool("token_refreshed", false))
|
||||
ctxLogger.Error("Failed to retrieve oauth access token",
|
||||
"provider", usr.GetAuthenticatedBy(), "error", err)
|
||||
"provider", tokenRefreshMetadata.AuthModule, "error", refreshErr)
|
||||
|
||||
// token refresh failed, invalidate the old token
|
||||
if err := o.InvalidateOAuthTokens(ctx, usr, sessionToken); err != nil {
|
||||
ctxLogger.Warn("Failed to invalidate OAuth tokens", "authID", usr.GetAuthID(), "error", err)
|
||||
if err := o.InvalidateOAuthTokens(ctx, usr, tokenRefreshMetadata); err != nil {
|
||||
ctxLogger.Warn("Failed to invalidate OAuth tokens", "authID", tokenRefreshMetadata.AuthID, "error", err)
|
||||
}
|
||||
|
||||
return nil, err
|
||||
return nil, refreshErr
|
||||
}
|
||||
|
||||
span.SetAttributes(attribute.Bool("token_refreshed", true))
|
||||
|
||||
// If the tokens are not the same, update the entry in the DB
|
||||
if !tokensEq(persistedToken, token) {
|
||||
updateAuthCommand := &login.UpdateAuthInfoCommand{
|
||||
UserId: userID,
|
||||
AuthModule: usr.GetAuthenticatedBy(),
|
||||
AuthId: usr.GetAuthID(),
|
||||
OAuthToken: token,
|
||||
}
|
||||
|
||||
if o.Cfg.Env == setting.Dev {
|
||||
ctxLogger.Debug("Oauth got token",
|
||||
"auth_module", usr.GetAuthenticatedBy(),
|
||||
|
@ -446,17 +470,32 @@ func (o *Service) tryGetOrRefreshOAuthToken(ctx context.Context, persistedToken
|
|||
}
|
||||
|
||||
if !o.features.IsEnabledGlobally(featuremgmt.FlagImprovedExternalSessionHandling) {
|
||||
updateAuthCommand := &login.UpdateAuthInfoCommand{
|
||||
UserId: userID,
|
||||
AuthModule: tokenRefreshMetadata.AuthModule,
|
||||
AuthId: tokenRefreshMetadata.AuthID,
|
||||
OAuthToken: token,
|
||||
}
|
||||
if err := o.AuthInfoService.UpdateAuthInfo(ctx, updateAuthCommand); err != nil {
|
||||
ctxLogger.Error("Failed to update auth info during token refresh", "authID", usr.GetAuthID(), "error", err)
|
||||
ctxLogger.Error("Failed to update auth info during token refresh", "authID", tokenRefreshMetadata.AuthID, "error", err)
|
||||
span.SetStatus(codes.Error, "Failed to update auth info during token refresh")
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := o.sessionService.UpdateExternalSession(ctx, sessionToken.ExternalSessionId, &auth.UpdateExternalSessionCommand{
|
||||
Token: token,
|
||||
}); err != nil {
|
||||
ctxLogger.Error("Failed to update external session during token refresh", "error", err)
|
||||
return nil, err
|
||||
// Update the external session with the new token if we the user has an external session,
|
||||
// regardless of the feature flag state to keep the `user_external_session` table in sync.
|
||||
// ExternalSessionID should always be set except for some edge cases:
|
||||
// - when Grafana was updated to a version where the `improvedExternalSessionHandling` feature flag
|
||||
// was enabled after the user logged in
|
||||
if tokenRefreshMetadata.ExternalSessionID != 0 {
|
||||
if err := o.sessionService.UpdateExternalSession(ctx, tokenRefreshMetadata.ExternalSessionID, &auth.UpdateExternalSessionCommand{
|
||||
Token: token,
|
||||
}); err != nil {
|
||||
ctxLogger.Error("Failed to update external session during token refresh", "error", err)
|
||||
span.SetStatus(codes.Error, "Failed to update external session during token refresh")
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
ctxLogger.Debug("Updated oauth info for user")
|
||||
|
@ -502,6 +541,11 @@ func needTokenRefresh(ctx context.Context, persistedToken *oauth2.Token) bool {
|
|||
|
||||
ctxLogger := logger.FromContext(ctx)
|
||||
|
||||
if persistedToken.AccessToken == "" {
|
||||
ctxLogger.Debug("Access token has been cleared, need to refresh")
|
||||
return true
|
||||
}
|
||||
|
||||
idTokenExp, err := GetIDTokenExpiry(persistedToken)
|
||||
if err != nil {
|
||||
ctxLogger.Warn("Could not get ID Token expiry", "error", err)
|
||||
|
@ -552,22 +596,6 @@ func buildOAuthTokenFromExternalSession(externalSession *auth.ExternalSession) *
|
|||
return token
|
||||
}
|
||||
|
||||
func checkOAuthRefreshToken(authInfo *login.UserAuth) error {
|
||||
if !strings.Contains(authInfo.AuthModule, "oauth") {
|
||||
logger.Warn("The specified user's auth provider is not oauth",
|
||||
"authmodule", authInfo.AuthModule, "userid", authInfo.UserId)
|
||||
return ErrNotAnOAuthProvider
|
||||
}
|
||||
|
||||
if authInfo.OAuthRefreshToken == "" {
|
||||
logger.Warn("No refresh token available",
|
||||
"authmodule", authInfo.AuthModule, "userid", authInfo.UserId)
|
||||
return ErrNoRefreshTokenFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetIDTokenExpiry extracts the expiry time from the ID token
|
||||
func GetIDTokenExpiry(token *oauth2.Token) (time.Time, error) {
|
||||
idToken, ok := token.Extra("id_token").(string)
|
||||
|
@ -601,3 +629,28 @@ func getExpiryWithSkew(expiry time.Time) (adjustedExpiry time.Time, hasTokenExpi
|
|||
hasTokenExpired = adjustedExpiry.Before(time.Now())
|
||||
return
|
||||
}
|
||||
|
||||
// getExternalSession fetches the external session based on the user and session token.
|
||||
// When using the render module, it fetches the most recent external session for the user
|
||||
// since the session token ID is not available.
|
||||
// For regular users, it uses the session token ID to fetch the external session.
|
||||
func (o *Service) getExternalSession(ctx context.Context, usr identity.Requester, userID int64, sessionToken *auth.UserToken) (*auth.ExternalSession, error) {
|
||||
if usr.GetAuthenticatedBy() == login.RenderModule {
|
||||
// When using render module, we don't have the session token ID, so we need to fetch the most recent session
|
||||
// entry for the user (as it is done with the old flow).
|
||||
// In the future, we might want to consider passing the session token ID to the render module to make this more robust.
|
||||
externalSessions, err := o.sessionService.FindExternalSessions(ctx, &auth.ListExternalSessionQuery{UserID: userID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(externalSessions) == 0 || externalSessions[0] == nil {
|
||||
return nil, auth.ErrExternalSessionNotFound
|
||||
}
|
||||
|
||||
return externalSessions[0], nil
|
||||
}
|
||||
|
||||
// For regular users, we use the session token ID to fetch the external session
|
||||
return o.sessionService.GetExternalSession(ctx, sessionToken.ExternalSessionId)
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -8,13 +8,14 @@ import (
|
|||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/services/auth"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/oauthtoken"
|
||||
)
|
||||
|
||||
type MockOauthTokenService struct {
|
||||
GetCurrentOauthTokenFunc func(ctx context.Context, usr identity.Requester, sessionToken *auth.UserToken) *oauth2.Token
|
||||
IsOAuthPassThruEnabledFunc func(ds *datasources.DataSource) bool
|
||||
InvalidateOAuthTokensFunc func(ctx context.Context, usr identity.Requester, sessionToken *auth.UserToken) error
|
||||
TryTokenRefreshFunc func(ctx context.Context, usr identity.Requester, sessionToken *auth.UserToken) (*oauth2.Token, error)
|
||||
InvalidateOAuthTokensFunc func(ctx context.Context, usr identity.Requester, metadata *oauthtoken.TokenRefreshMetadata) error
|
||||
TryTokenRefreshFunc func(ctx context.Context, usr identity.Requester, metadata *oauthtoken.TokenRefreshMetadata) (*oauth2.Token, error)
|
||||
}
|
||||
|
||||
func (m *MockOauthTokenService) GetCurrentOAuthToken(ctx context.Context, usr identity.Requester, sessionToken *auth.UserToken) *oauth2.Token {
|
||||
|
@ -31,16 +32,16 @@ func (m *MockOauthTokenService) IsOAuthPassThruEnabled(ds *datasources.DataSourc
|
|||
return false
|
||||
}
|
||||
|
||||
func (m *MockOauthTokenService) InvalidateOAuthTokens(ctx context.Context, usr identity.Requester, sessionToken *auth.UserToken) error {
|
||||
func (m *MockOauthTokenService) InvalidateOAuthTokens(ctx context.Context, usr identity.Requester, metadata *oauthtoken.TokenRefreshMetadata) error {
|
||||
if m.InvalidateOAuthTokensFunc != nil {
|
||||
return m.InvalidateOAuthTokensFunc(ctx, usr, sessionToken)
|
||||
return m.InvalidateOAuthTokensFunc(ctx, usr, metadata)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockOauthTokenService) TryTokenRefresh(ctx context.Context, usr identity.Requester, sessionToken *auth.UserToken) (*oauth2.Token, error) {
|
||||
func (m *MockOauthTokenService) TryTokenRefresh(ctx context.Context, usr identity.Requester, metadata *oauthtoken.TokenRefreshMetadata) (*oauth2.Token, error) {
|
||||
if m.TryTokenRefreshFunc != nil {
|
||||
return m.TryTokenRefreshFunc(ctx, usr, sessionToken)
|
||||
return m.TryTokenRefreshFunc(ctx, usr, metadata)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
|
|
@ -29,10 +29,10 @@ func (s *Service) IsOAuthPassThruEnabled(ds *datasources.DataSource) bool {
|
|||
return oauthtoken.IsOAuthPassThruEnabled(ds)
|
||||
}
|
||||
|
||||
func (s *Service) TryTokenRefresh(context.Context, identity.Requester, *auth.UserToken) (*oauth2.Token, error) {
|
||||
func (s *Service) TryTokenRefresh(context.Context, identity.Requester, *oauthtoken.TokenRefreshMetadata) (*oauth2.Token, error) {
|
||||
return s.Token, nil
|
||||
}
|
||||
|
||||
func (s *Service) InvalidateOAuthTokens(context.Context, identity.Requester, *auth.UserToken) error {
|
||||
func (s *Service) InvalidateOAuthTokens(context.Context, identity.Requester, *oauthtoken.TokenRefreshMetadata) error {
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@ import { config } from 'app/core/config';
|
|||
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||
import { getGrafanaSearcher } from 'app/features/search/service/searcher';
|
||||
import { DashboardSearchItemType } from 'app/features/search/types';
|
||||
import { PermissionLevelString } from 'app/types/acl';
|
||||
import { FolderInfo } from 'app/types/folders';
|
||||
|
||||
export interface FolderFilterProps {
|
||||
|
@ -69,7 +68,7 @@ async function getFoldersAsOptions(
|
|||
query: searchString,
|
||||
kind: ['folder'],
|
||||
limit: 100,
|
||||
permission: PermissionLevelString.View,
|
||||
permission: 'view',
|
||||
});
|
||||
|
||||
const options = queryResponse.view.map((item) => ({
|
||||
|
@ -89,7 +88,7 @@ async function getFoldersAsOptions(
|
|||
const params = {
|
||||
query: searchString,
|
||||
type: DashboardSearchItemType.DashFolder,
|
||||
permission: PermissionLevelString.View,
|
||||
permission: 'view',
|
||||
};
|
||||
|
||||
const searchHits = await getBackendSrv().search(params);
|
||||
|
|
|
@ -14,7 +14,7 @@ import { getGrafanaSearcher } from 'app/features/search/service/searcher';
|
|||
import { QueryResponse } from 'app/features/search/service/types';
|
||||
import { queryResultToViewItem } from 'app/features/search/service/utils';
|
||||
import { DashboardViewItem } from 'app/features/search/types';
|
||||
import { PermissionLevelString } from 'app/types/acl';
|
||||
import { PermissionLevel } from 'app/types/acl';
|
||||
|
||||
import { FolderRepo } from './FolderRepo';
|
||||
import { getDOMId, NestedFolderList } from './NestedFolderList';
|
||||
|
@ -57,7 +57,7 @@ export interface NestedFolderPickerProps {
|
|||
|
||||
const debouncedSearch = debounce(getSearchResults, 300);
|
||||
|
||||
async function getSearchResults(searchQuery: string, permission?: PermissionLevelString) {
|
||||
async function getSearchResults(searchQuery: string, permission?: PermissionLevel) {
|
||||
const queryResponse = await getGrafanaSearcher().search({
|
||||
query: searchQuery,
|
||||
kind: ['folder'],
|
||||
|
@ -98,17 +98,6 @@ export function NestedFolderPicker({
|
|||
const [error] = useState<Error | undefined>(undefined); // TODO: error not populated anymore
|
||||
const lastSearchTimestamp = useRef<number>(0);
|
||||
|
||||
// Map the permission string union to enum value for compatibility
|
||||
const permissionLevel = useMemo(() => {
|
||||
if (permission === 'view') {
|
||||
return PermissionLevelString.View;
|
||||
} else if (permission === 'edit') {
|
||||
return PermissionLevelString.Edit;
|
||||
}
|
||||
|
||||
throw new Error('Invalid permission');
|
||||
}, [permission]);
|
||||
|
||||
const isBrowsing = Boolean(overlayOpen && !(search && searchResults));
|
||||
const {
|
||||
emptyFolders,
|
||||
|
@ -118,7 +107,7 @@ export function NestedFolderPicker({
|
|||
} = useFoldersQuery({
|
||||
isBrowsing,
|
||||
openFolders: foldersOpenState,
|
||||
permission: permissionLevel,
|
||||
permission,
|
||||
rootFolderUID,
|
||||
rootFolderItem,
|
||||
});
|
||||
|
@ -132,7 +121,7 @@ export function NestedFolderPicker({
|
|||
const timestamp = Date.now();
|
||||
setIsFetchingSearchResults(true);
|
||||
|
||||
debouncedSearch(search, permissionLevel).then((queryResponse) => {
|
||||
debouncedSearch(search, permission).then((queryResponse) => {
|
||||
// Only keep the results if it's was issued after the most recently resolved search.
|
||||
// This prevents results showing out of order if first request is slower than later ones.
|
||||
// We don't need to worry about clearing the isFetching state either - if there's a later
|
||||
|
@ -144,7 +133,7 @@ export function NestedFolderPicker({
|
|||
lastSearchTimestamp.current = timestamp;
|
||||
}
|
||||
});
|
||||
}, [search, permissionLevel]);
|
||||
}, [search, permission]);
|
||||
|
||||
// the order of middleware is important!
|
||||
const middleware = [
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { config } from '@grafana/runtime';
|
||||
import { DashboardsTreeItem } from 'app/features/browse-dashboards/types';
|
||||
import { PermissionLevelString } from 'app/types/acl';
|
||||
import { PermissionLevel } from 'app/types/acl';
|
||||
|
||||
import { useFoldersQueryAppPlatform } from './useFoldersQueryAppPlatform';
|
||||
import { useFoldersQueryLegacy } from './useFoldersQueryLegacy';
|
||||
|
@ -8,7 +8,7 @@ import { useFoldersQueryLegacy } from './useFoldersQueryLegacy';
|
|||
export interface UseFoldersQueryProps {
|
||||
isBrowsing: boolean;
|
||||
openFolders: Record<string, boolean>;
|
||||
permission?: PermissionLevelString;
|
||||
permission?: PermissionLevel;
|
||||
rootFolderUID?: string;
|
||||
rootFolderItem?: DashboardsTreeItem;
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ import { Navigate } from 'react-router-dom-v5-compat';
|
|||
import { SafeDynamicImport } from 'app/core/components/DynamicImports/SafeDynamicImport';
|
||||
import { config } from 'app/core/config';
|
||||
import { GrafanaRouteComponent, RouteDescriptor } from 'app/core/navigation/types';
|
||||
import { AlertingPageWrapper } from 'app/features/alerting/unified/components/AlertingPageWrapper';
|
||||
import { AccessControlAction } from 'app/types/accessControl';
|
||||
|
||||
import { PERMISSIONS_CONTACT_POINTS } from './unified/components/contact-points/permissions';
|
||||
|
@ -338,7 +337,9 @@ export function getAlertingRoutes(cfg = config): RouteDescriptor[] {
|
|||
routes.push({
|
||||
path: '/alerting/triage',
|
||||
roles: evaluateAccess([AccessControlAction.AlertingRuleRead, AccessControlAction.AlertingRuleExternalRead]),
|
||||
component: () => <AlertingPageWrapper />,
|
||||
component: importAlertingComponent(
|
||||
() => import(/* webpackChunkName: "AlertingTriage" */ 'app/features/alerting/unified/triage/Triage')
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
import { css } from '@emotion/css';
|
||||
import * as React from 'react';
|
||||
import { type MergeExclusive } from 'type-fest';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Label, Stack, useStyles2 } from '@grafana/ui';
|
||||
|
||||
interface BaseProps {
|
||||
id?: string;
|
||||
}
|
||||
|
||||
interface ChildrenProps extends BaseProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
interface LabelActionsProps extends BaseProps {
|
||||
label: string;
|
||||
actions?: React.ReactNode;
|
||||
}
|
||||
|
||||
type Props = MergeExclusive<ChildrenProps, LabelActionsProps>;
|
||||
|
||||
export function EditorColumnHeader({ label, actions, id, children }: Props) {
|
||||
const styles = useStyles2(editorColumnStyles);
|
||||
|
||||
if (children) {
|
||||
return <div className={styles.container}>{children}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Label className={styles.label} id={id}>
|
||||
{label}
|
||||
</Label>
|
||||
{actions && (
|
||||
<Stack direction="row" gap={1}>
|
||||
{actions}
|
||||
</Stack>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const editorColumnStyles = (theme: GrafanaTheme2) => ({
|
||||
container: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: theme.spacing(1, 2),
|
||||
backgroundColor: theme.colors.background.secondary,
|
||||
border: `1px solid ${theme.colors.border.medium}`,
|
||||
borderTopLeftRadius: theme.shape.radius.default,
|
||||
borderTopRightRadius: theme.shape.radius.default,
|
||||
}),
|
||||
label: css({
|
||||
margin: 0,
|
||||
}),
|
||||
});
|
|
@ -1,37 +0,0 @@
|
|||
import { css } from '@emotion/css';
|
||||
import * as React from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Label, Stack, useStyles2 } from '@grafana/ui';
|
||||
|
||||
type Props = { label: string; actions?: React.ReactNode; id?: string };
|
||||
|
||||
export function EditorColumnHeader({ label, actions, id }: Props) {
|
||||
const styles = useStyles2(editorColumnStyles);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Label className={styles.label} id={id}>
|
||||
{label}
|
||||
</Label>
|
||||
<Stack direction="row" gap={1}>
|
||||
{actions}
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const editorColumnStyles = (theme: GrafanaTheme2) => ({
|
||||
container: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: theme.spacing(1, 2),
|
||||
backgroundColor: theme.colors.background.secondary,
|
||||
borderBottom: `1px solid ${theme.colors.border.medium}`,
|
||||
}),
|
||||
label: css({
|
||||
margin: 0,
|
||||
}),
|
||||
});
|
|
@ -8,7 +8,7 @@ import { Trans, t } from '@grafana/i18n';
|
|||
import { Button, CodeEditor, Dropdown, Menu, Stack, Toggletip, useStyles2 } from '@grafana/ui';
|
||||
import { TestTemplateAlert } from 'app/plugins/datasource/alertmanager/types';
|
||||
|
||||
import { EditorColumnHeader } from '../contact-points/templates/EditorColumnHeader';
|
||||
import { EditorColumnHeader } from '../EditorColumnHeader';
|
||||
|
||||
import { AlertInstanceModalSelector } from './AlertInstanceModalSelector';
|
||||
import { AlertTemplatePreviewData } from './TemplateData';
|
||||
|
|
|
@ -32,9 +32,9 @@ import { TestTemplateAlert } from 'app/plugins/datasource/alertmanager/types';
|
|||
import { AITemplateButtonComponent } from '../../enterprise-components/AI/AIGenTemplateButton/addAITemplateButton';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||
import { makeAMLink, stringifyErrorLike } from '../../utils/misc';
|
||||
import { EditorColumnHeader } from '../EditorColumnHeader';
|
||||
import { ProvisionedResource, ProvisioningAlert } from '../Provisioning';
|
||||
import { Spacer } from '../Spacer';
|
||||
import { EditorColumnHeader } from '../contact-points/templates/EditorColumnHeader';
|
||||
import {
|
||||
NotificationTemplate,
|
||||
useCreateNotificationTemplate,
|
||||
|
|
|
@ -10,7 +10,7 @@ import { Alert, Box, Button, CodeEditor, useStyles2 } from '@grafana/ui';
|
|||
import { TemplatePreviewErrors, TemplatePreviewResponse, TemplatePreviewResult } from '../../api/templateApi';
|
||||
import { AIFeedbackButtonComponent } from '../../enterprise-components/AI/addAIFeedbackButton';
|
||||
import { stringifyErrorLike } from '../../utils/misc';
|
||||
import { EditorColumnHeader } from '../contact-points/templates/EditorColumnHeader';
|
||||
import { EditorColumnHeader } from '../EditorColumnHeader';
|
||||
|
||||
import { usePreviewTemplate } from './usePreviewTemplate';
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ import { Box, useStyles2 } from '@grafana/ui';
|
|||
import { useAlertmanager } from 'app/features/alerting/unified/state/AlertmanagerContext';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
|
||||
|
||||
import { EditorColumnHeader } from '../../../contact-points/templates/EditorColumnHeader';
|
||||
import { EditorColumnHeader } from '../../../EditorColumnHeader';
|
||||
import { TemplateEditor } from '../../TemplateEditor';
|
||||
import { TemplatePreview } from '../../TemplatePreview';
|
||||
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
import { scaleTime } from 'd3-scale';
|
||||
import { useMemo } from 'react';
|
||||
import { useMeasure } from 'react-use';
|
||||
|
||||
import { Stack, Text } from '@grafana/ui';
|
||||
|
||||
import { Domain } from './types';
|
||||
|
||||
interface TimelineProps {
|
||||
domain: Domain;
|
||||
}
|
||||
|
||||
export const TimelineHeader = ({ domain }: TimelineProps) => {
|
||||
const [ref, { width }] = useMeasure<HTMLDivElement>();
|
||||
|
||||
const ticks = useMemo(() => {
|
||||
const xScale = scaleTime().domain(domain).range([0, width]).nice(0);
|
||||
const tickFormatter = xScale.tickFormat();
|
||||
|
||||
return xScale.ticks(5).map((value) => ({
|
||||
value: tickFormatter(value),
|
||||
xOffset: xScale(value),
|
||||
}));
|
||||
}, [domain, width]);
|
||||
|
||||
return (
|
||||
<div ref={ref} style={{ width: '100%' }}>
|
||||
<Stack flex={1} direction="row" justifyContent="space-between">
|
||||
{ticks.map((tick) => (
|
||||
<Text key={`${tick.value}-${tick.xOffset}`} variant="bodySmall" color="secondary">
|
||||
{tick.value}
|
||||
</Text>
|
||||
))}
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,19 @@
|
|||
# Triage view
|
||||
|
||||
The triage view should serve several purposes and be a central place for users to manage their alert instances.
|
||||
|
||||
## Goals
|
||||
|
||||
- Observe the current state of their system
|
||||
- Help correlate alerts with each other
|
||||
- Be a launchpad for further investigation
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Managing alert rules
|
||||
|
||||
## Technical goals
|
||||
|
||||
- Build re-usable components that can be used in other parts of Grafana and plugins
|
||||
- These should be a mix of presentation components and data components
|
||||
- Eventually most of this should live in the Grafana Alerting package
|
|
@ -0,0 +1,25 @@
|
|||
import { t } from '@grafana/i18n';
|
||||
import { UrlSyncContextProvider } from '@grafana/scenes';
|
||||
import { withErrorBoundary } from '@grafana/ui';
|
||||
|
||||
import { AlertingPageWrapper } from '../components/AlertingPageWrapper';
|
||||
|
||||
import { TriageScene, triageScene } from './scene/TriageScene';
|
||||
|
||||
export const TriagePage = () => {
|
||||
return (
|
||||
<AlertingPageWrapper
|
||||
navId="alerting"
|
||||
subTitle={t('alerting.pages.triage.subtitle', 'Learn about problems in your systems moments after they occur')}
|
||||
pageNav={{
|
||||
text: t('alerting.pages.triage.title', 'Triage'),
|
||||
}}
|
||||
>
|
||||
<UrlSyncContextProvider scene={triageScene} updateUrlOnInit={true} createBrowserHistorySteps={true}>
|
||||
<TriageScene key={triageScene.state.key} />
|
||||
</UrlSyncContextProvider>
|
||||
</AlertingPageWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default withErrorBoundary(TriagePage);
|
|
@ -0,0 +1,211 @@
|
|||
import { css, cx } from '@emotion/css';
|
||||
import { take } from 'lodash';
|
||||
import { useState } from 'react';
|
||||
import { useMeasure } from 'react-use';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { SceneQueryRunner } from '@grafana/scenes';
|
||||
import { ScrollContainer, useSplitter, useStyles2 } from '@grafana/ui';
|
||||
import { DEFAULT_PER_PAGE_PAGINATION } from 'app/core/constants';
|
||||
|
||||
import { EditorColumnHeader } from '../components/EditorColumnHeader';
|
||||
import LoadMoreHelper from '../rule-list/LoadMoreHelper';
|
||||
|
||||
import { TimelineHeader } from './Timeline';
|
||||
import { WorkbenchProvider } from './WorkbenchContext';
|
||||
import { AlertRuleRow } from './rows/AlertRuleRow';
|
||||
import { FolderGroupRow } from './rows/FolderGroupRow';
|
||||
import { GroupRow } from './rows/GroupRow';
|
||||
import { generateRowKey } from './rows/utils';
|
||||
import { GenericRowSkeleton } from './scene/AlertRuleInstances';
|
||||
import { SummaryChartReact } from './scene/SummaryChart';
|
||||
import { SummaryStatsReact } from './scene/SummaryStats';
|
||||
import { Domain, Filter, WorkbenchRow } from './types';
|
||||
|
||||
type WorkbenchProps = {
|
||||
domain: Domain;
|
||||
data: WorkbenchRow[];
|
||||
groupBy?: string[]; // @TODO proper type
|
||||
filterBy?: Filter[];
|
||||
queryRunner: SceneQueryRunner;
|
||||
};
|
||||
|
||||
const initialSize = 1 / 3;
|
||||
|
||||
// Helper function to recursively render WorkbenchRow items with children pattern
|
||||
function renderWorkbenchRow(
|
||||
row: WorkbenchRow,
|
||||
leftColumnWidth: number,
|
||||
domain: Domain,
|
||||
key: React.Key,
|
||||
depth = 0
|
||||
): React.ReactElement {
|
||||
if (row.type === 'alertRule') {
|
||||
return <AlertRuleRow key={key} row={row} leftColumnWidth={leftColumnWidth} rowKey={key} depth={depth} />;
|
||||
} else {
|
||||
const children = row.rows.map((childRow, childIndex) =>
|
||||
renderWorkbenchRow(childRow, leftColumnWidth, domain, `${key}-${generateRowKey(childRow, childIndex)}`, depth + 1)
|
||||
);
|
||||
|
||||
// Check if this is a grafana_folder group and use FolderGroupRow
|
||||
if (row.metadata.label === 'grafana_folder') {
|
||||
return (
|
||||
<FolderGroupRow key={key} row={row} leftColumnWidth={leftColumnWidth} rowKey={key} depth={depth}>
|
||||
{children}
|
||||
</FolderGroupRow>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<GroupRow key={key} row={row} leftColumnWidth={leftColumnWidth} rowKey={key} depth={depth}>
|
||||
{children}
|
||||
</GroupRow>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The workbench displays groups of alerts, each group containing metadata and a chart.
|
||||
* Alerts can be arbitrarily grouped by any number of labels. By default all instances are grouped by alertname.
|
||||
*
|
||||
* The page consist of a left column with metadata for the row and a right column with charts.
|
||||
* Below is a rough layout of the page:
|
||||
*
|
||||
* The page is divided into two columns, the size of these columns is determined by the splitter.
|
||||
* There is a useMeasure hook to measure the size of the left column, which is used to set the width of the group items.
|
||||
* We do this because each row needs to be a flex container such that if the height of the left colorn changes, the
|
||||
* right column will also change its height accordingly. This would not be possible if we used a simplified column layout.
|
||||
*
|
||||
* This also means we draw the rows _on top_ of the splitter, in other words the contents of the splitter are empty
|
||||
* and we only use it to determine the width of the left column of the rows that are overlayed on top.
|
||||
*
|
||||
* Each group is a row with a left and a right column. Each row consists of two cells (the left and the right cell).
|
||||
* The left cell contains the metadata for the group, the right cell contains the chart.
|
||||
┌─────────────────────────┐ ┌───────────────────────────────────┐
|
||||
│┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│─│─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐│
|
||||
│ │
|
||||
││ Row ││
|
||||
│ │
|
||||
│└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│─│─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘│
|
||||
│┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│─│─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐│
|
||||
│ ┌──────────────────────┐ ┌───────────────────────────────┐ │
|
||||
│││ Cell │ │ Cell │││
|
||||
│ └──────────────────────┘ └───────────────────────────────┘ │
|
||||
│└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│─│─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘│
|
||||
│ │ │ │
|
||||
│ │││ │
|
||||
│ │││ │
|
||||
│ │││ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
└─────────────────────────┘ └───────────────────────────────────┘
|
||||
*/
|
||||
export function Workbench({ domain, data, queryRunner }: WorkbenchProps) {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const isLoading = !queryRunner.isDataReadyToDisplay();
|
||||
const [pageIndex, setPageIndex] = useState<number>(1);
|
||||
// splitter for template and payload editor
|
||||
const splitter = useSplitter({
|
||||
direction: 'row',
|
||||
// if Grafana Alertmanager, split 50/50, otherwise 100/0 because there is no payload editor
|
||||
initialSize: initialSize,
|
||||
dragPosition: 'middle',
|
||||
});
|
||||
|
||||
// this will measure the size of the left most column of the splitter, so we can use it to set the width of the group items
|
||||
const [ref, rect] = useMeasure<HTMLDivElement>();
|
||||
const leftColumnWidth = rect.width;
|
||||
|
||||
const itemsToRender = pageIndex * DEFAULT_PER_PAGE_PAGINATION;
|
||||
const dataSlice = take(data, itemsToRender);
|
||||
const hasMore = data.length > itemsToRender;
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative', display: 'flex', flexGrow: 1, width: '100%', height: '100%' }}>
|
||||
{/* dummy splitter to handle flex width of group items */}
|
||||
<div {...splitter.containerProps}>
|
||||
<div {...splitter.primaryProps}>
|
||||
<div ref={ref} className={cx(styles.flexFull, styles.minColumnWidth)} />
|
||||
</div>
|
||||
<div {...splitter.splitterProps} />
|
||||
<div {...splitter.secondaryProps}>
|
||||
<div className={cx(styles.flexFull, styles.minColumnWidth)} />
|
||||
</div>
|
||||
</div>
|
||||
{/* content goes here */}
|
||||
<div data-testid="groups-container" className={cx(splitter.containerProps.className, styles.groupsContainer)}>
|
||||
<div className={cx(styles.groupItemWrapper(leftColumnWidth), styles.summaryContainer)}>
|
||||
<SummaryStatsReact />
|
||||
<SummaryChartReact />
|
||||
</div>
|
||||
<div className={cx(styles.groupItemWrapper(leftColumnWidth), styles.headerContainer)}>
|
||||
<EditorColumnHeader label={t('alerting.left-column.label-instances', 'Instances')} />
|
||||
<EditorColumnHeader>
|
||||
<TimelineHeader domain={domain} />
|
||||
</EditorColumnHeader>
|
||||
</div>
|
||||
{/* Render actual data */}
|
||||
<div className={styles.virtualizedContainer}>
|
||||
<WorkbenchProvider leftColumnWidth={leftColumnWidth} domain={domain} queryRunner={queryRunner}>
|
||||
<ScrollContainer height="100%" width="100%" scrollbarWidth="none" showScrollIndicators>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<GenericRowSkeleton key="skeleton-1" width={leftColumnWidth} depth={0} />
|
||||
<GenericRowSkeleton key="skeleton-2" width={leftColumnWidth} depth={0} />
|
||||
<GenericRowSkeleton key="skeleton-3" width={leftColumnWidth} depth={0} />
|
||||
</>
|
||||
) : (
|
||||
dataSlice.map((row, index) => {
|
||||
const rowKey = generateRowKey(row, index);
|
||||
return renderWorkbenchRow(row, leftColumnWidth, domain, rowKey);
|
||||
})
|
||||
)}
|
||||
{hasMore && <LoadMoreHelper handleLoad={() => setPageIndex((prevIndex) => prevIndex + 1)} />}
|
||||
</ScrollContainer>
|
||||
</WorkbenchProvider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const getStyles = (theme: GrafanaTheme2) => {
|
||||
const summaryHeight = 200;
|
||||
return {
|
||||
groupsContainer: css({
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}),
|
||||
groupItemWrapper: (width: number) =>
|
||||
css({
|
||||
display: 'grid',
|
||||
gridTemplateColumns: `${width}px auto`,
|
||||
gap: theme.spacing(2),
|
||||
}),
|
||||
virtualizedContainer: css({
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
overflow: 'hidden', // Let AutoSizer handle the overflow
|
||||
}),
|
||||
summaryContainer: css({
|
||||
gridTemplateRows: summaryHeight,
|
||||
marginBottom: theme.spacing(2),
|
||||
}),
|
||||
headerContainer: css({
|
||||
top: summaryHeight,
|
||||
}),
|
||||
flexFull: css({
|
||||
flex: 1,
|
||||
}),
|
||||
minColumnWidth: css({
|
||||
minWidth: 300,
|
||||
}),
|
||||
};
|
||||
};
|
|
@ -0,0 +1,34 @@
|
|||
import React, { createContext, useContext } from 'react';
|
||||
|
||||
import { SceneQueryRunner } from '@grafana/scenes';
|
||||
|
||||
import { Domain } from './types';
|
||||
|
||||
interface WorkbenchContextValue {
|
||||
leftColumnWidth: number;
|
||||
domain: Domain;
|
||||
queryRunner: SceneQueryRunner;
|
||||
}
|
||||
|
||||
const WorkbenchContext = createContext<WorkbenchContextValue | undefined>(undefined);
|
||||
|
||||
export function useWorkbenchContext(): WorkbenchContextValue {
|
||||
const context = useContext(WorkbenchContext);
|
||||
if (!context) {
|
||||
throw new Error('useWorkbenchContext must be used within a WorkbenchProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
interface WorkbenchProviderProps {
|
||||
leftColumnWidth: number;
|
||||
domain: Domain;
|
||||
queryRunner: SceneQueryRunner;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function WorkbenchProvider({ leftColumnWidth, domain, queryRunner, children }: WorkbenchProviderProps) {
|
||||
return (
|
||||
<WorkbenchContext.Provider value={{ leftColumnWidth, domain, queryRunner }}>{children}</WorkbenchContext.Provider>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import { config } from '@grafana/runtime';
|
||||
|
||||
export const VARIABLES = {
|
||||
groupBy: 'groupBy',
|
||||
filters: 'filters',
|
||||
};
|
||||
|
||||
export const DATASOURCE_UID = config.unifiedAlerting.stateHistory?.prometheusTargetDatasourceUID;
|
||||
export const METRIC_NAME = config.unifiedAlerting.stateHistory?.prometheusMetricName ?? 'GRAFANA_ALERTS';
|
||||
export const DEFAULT_FIELDS = ['alertname', 'grafana_folder', 'grafana_rule_uid', 'alertstate'] as const;
|
|
@ -0,0 +1,55 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Stack, Text, TextLink } from '@grafana/ui';
|
||||
|
||||
import { MetaText } from '../../components/MetaText';
|
||||
import { WithReturnButton } from '../../components/WithReturnButton';
|
||||
import { rulesNav } from '../../utils/navigation';
|
||||
import { AlertRuleInstances } from '../scene/AlertRuleInstances';
|
||||
import { AlertRuleSummary } from '../scene/AlertRuleSummary';
|
||||
import { AlertRuleRow as AlertRuleRowType } from '../types';
|
||||
|
||||
import { GenericRow } from './GenericRow';
|
||||
|
||||
interface AlertRuleRowProps {
|
||||
row: AlertRuleRowType;
|
||||
leftColumnWidth: number;
|
||||
rowKey: React.Key;
|
||||
depth?: number;
|
||||
}
|
||||
|
||||
export const AlertRuleRow = ({ row, leftColumnWidth, rowKey, depth = 0 }: AlertRuleRowProps) => {
|
||||
return (
|
||||
<GenericRow
|
||||
key={rowKey}
|
||||
width={leftColumnWidth}
|
||||
title={
|
||||
<WithReturnButton
|
||||
component={
|
||||
<TextLink
|
||||
inline={false}
|
||||
href={rulesNav.detailsPageLink('grafana', {
|
||||
ruleSourceName: 'grafana',
|
||||
uid: row.metadata.ruleUID,
|
||||
})}
|
||||
>
|
||||
{row.metadata.title}
|
||||
</TextLink>
|
||||
}
|
||||
/>
|
||||
}
|
||||
metadata={
|
||||
<Stack direction="row" gap={0.5} alignItems="center">
|
||||
<MetaText icon="folder" />
|
||||
<Text variant="bodySmall" color="secondary">
|
||||
{row.metadata.folder}
|
||||
</Text>
|
||||
</Stack>
|
||||
}
|
||||
content={<AlertRuleSummary ruleUID={row.metadata.ruleUID} />}
|
||||
depth={depth}
|
||||
>
|
||||
<AlertRuleInstances ruleUID={row.metadata.ruleUID} depth={depth + 1} />
|
||||
</GenericRow>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,47 @@
|
|||
import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Stack, Text, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { MetaText } from '../../components/MetaText';
|
||||
import { GenericGroupedRow } from '../types';
|
||||
|
||||
import { GenericRow } from './GenericRow';
|
||||
|
||||
interface FolderGroupRowProps {
|
||||
row: GenericGroupedRow;
|
||||
leftColumnWidth: number;
|
||||
rowKey: React.Key;
|
||||
depth?: number;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const FolderGroupRow = ({ row, leftColumnWidth, rowKey, depth = 0, children }: FolderGroupRowProps) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<GenericRow
|
||||
key={rowKey}
|
||||
width={leftColumnWidth}
|
||||
title={
|
||||
<Stack direction="row" gap={0.5} alignItems="center">
|
||||
<MetaText icon="folder" />
|
||||
<Text color="primary">{row.metadata.value}</Text>
|
||||
</Stack>
|
||||
}
|
||||
isOpenByDefault={true}
|
||||
leftColumnClassName={styles.folderGroupRow}
|
||||
rightColumnClassName={styles.folderGroupRow}
|
||||
depth={depth}
|
||||
>
|
||||
{children}
|
||||
</GenericRow>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
folderGroupRow: css({
|
||||
backgroundColor: theme.colors.background.secondary,
|
||||
}),
|
||||
});
|
|
@ -0,0 +1,132 @@
|
|||
import { css, cx } from '@emotion/css';
|
||||
import { ReactNode } from 'react';
|
||||
import { useToggle } from 'react-use';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { IconButton, Stack, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { Spacer } from '../../components/Spacer';
|
||||
|
||||
interface GenericRowProps {
|
||||
width: number;
|
||||
title: ReactNode;
|
||||
metadata?: ReactNode;
|
||||
actions?: ReactNode;
|
||||
content?: ReactNode;
|
||||
isOpenByDefault?: boolean;
|
||||
children?: ReactNode;
|
||||
// allow overriding / adding styles for the row
|
||||
leftColumnClassName?: string;
|
||||
rightColumnClassName?: string;
|
||||
depth?: number; // for indentation of nested rows
|
||||
}
|
||||
|
||||
export const GenericRow = ({
|
||||
width,
|
||||
title,
|
||||
metadata,
|
||||
actions,
|
||||
content,
|
||||
isOpenByDefault = false,
|
||||
children,
|
||||
leftColumnClassName,
|
||||
rightColumnClassName,
|
||||
depth = 0,
|
||||
}: GenericRowProps) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const [isOpen, handleToggle] = useToggle(isOpenByDefault);
|
||||
|
||||
const hasChildren = Boolean(children);
|
||||
const showChildContent = isOpen && hasChildren;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.groupItemWrapper(width)}>
|
||||
<div className={cx(styles.leftColumn, styles.column, leftColumnClassName)}>
|
||||
<div className={styles.columnContent(depth)}>
|
||||
<LeftCell
|
||||
title={title}
|
||||
metadata={metadata}
|
||||
actions={actions}
|
||||
isOpen={isOpen}
|
||||
onToggle={hasChildren ? handleToggle : undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ minWidth: 'min-content', flexGrow: 1 }} className={cx(styles.column, rightColumnClassName)}>
|
||||
{content && <div className={styles.columnContent()}>{content}</div>}
|
||||
</div>
|
||||
</div>
|
||||
{showChildContent ? children : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface LeftCellProps {
|
||||
title: ReactNode;
|
||||
metadata?: ReactNode;
|
||||
actions?: ReactNode;
|
||||
isOpen?: boolean;
|
||||
onToggle?: () => void;
|
||||
}
|
||||
|
||||
const LeftCell = ({ title, metadata = null, actions = null, isOpen = true, onToggle }: LeftCellProps) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<Stack direction="row" alignItems="center" gap={0.5}>
|
||||
{onToggle && (
|
||||
<IconButton
|
||||
name={isOpen ? 'angle-down' : 'angle-right'}
|
||||
onClick={() => onToggle()}
|
||||
className={styles.dropdownIcon}
|
||||
variant="secondary"
|
||||
size="md"
|
||||
aria-label={t('alerting.group-wrapper.toggle', 'Toggle group')}
|
||||
/>
|
||||
)}
|
||||
<Stack direction="column" alignItems="flex-start" gap={0} flex={1}>
|
||||
<Stack direction="row" alignItems="center" gap={1} width="100%">
|
||||
{title}
|
||||
{actions && <Spacer />}
|
||||
{actions}
|
||||
</Stack>
|
||||
{metadata}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
dropdownIcon: css({
|
||||
alignSelf: 'flex-start',
|
||||
marginTop: theme.spacing(0.5),
|
||||
}),
|
||||
column: css({
|
||||
display: 'flex',
|
||||
position: 'relative',
|
||||
flexBasis: 0,
|
||||
border: 'solid 1px transparent',
|
||||
borderBottom: `1px solid ${theme.colors.border.medium}`,
|
||||
borderLeft: `1px solid ${theme.colors.border.medium}`,
|
||||
borderRight: `1px solid ${theme.colors.border.medium}`,
|
||||
}),
|
||||
leftColumn: css({
|
||||
overflow: 'hidden',
|
||||
}),
|
||||
columnContent: (depth?: number) =>
|
||||
css({
|
||||
padding: 5,
|
||||
width: '100%',
|
||||
paddingLeft: depth ? `calc(${theme.spacing(depth)} + 5px)` : 5,
|
||||
}),
|
||||
groupItemWrapper: (width: number) =>
|
||||
css({
|
||||
display: 'grid',
|
||||
gridTemplateColumns: `${width}px auto`,
|
||||
gap: theme.spacing(2),
|
||||
}),
|
||||
};
|
||||
};
|
|
@ -0,0 +1,42 @@
|
|||
import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
|
||||
import { AlertLabel } from '@grafana/alerting/unstable';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { GenericGroupedRow } from '../types';
|
||||
|
||||
import { GenericRow } from './GenericRow';
|
||||
|
||||
interface GroupRowProps {
|
||||
row: GenericGroupedRow;
|
||||
leftColumnWidth: number;
|
||||
rowKey: React.Key;
|
||||
depth?: number;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const GroupRow = ({ row, leftColumnWidth, rowKey, depth = 0, children }: GroupRowProps) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<GenericRow
|
||||
key={rowKey}
|
||||
width={leftColumnWidth}
|
||||
title={<AlertLabel size="sm" labelKey={row.metadata.label} value={row.metadata.value} colorBy="key" />}
|
||||
isOpenByDefault={true}
|
||||
leftColumnClassName={styles.groupRow}
|
||||
rightColumnClassName={styles.groupRow}
|
||||
depth={depth}
|
||||
>
|
||||
{children}
|
||||
</GenericRow>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
groupRow: css({
|
||||
backgroundColor: theme.colors.background.secondary,
|
||||
}),
|
||||
});
|
|
@ -0,0 +1,114 @@
|
|||
import { css } from '@emotion/css';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { AlertLabels } from '@grafana/alerting/unstable';
|
||||
import { DataFrame, GrafanaTheme2, Labels, LoadingState, TimeRange } from '@grafana/data';
|
||||
import { Trans } from '@grafana/i18n';
|
||||
import { SceneDataNode, VizConfigBuilders } from '@grafana/scenes';
|
||||
import { VizPanel } from '@grafana/scenes-react';
|
||||
import { GraphDrawStyle, VisibilityMode } from '@grafana/schema';
|
||||
import {
|
||||
AxisPlacement,
|
||||
BarAlignment,
|
||||
LegendDisplayMode,
|
||||
StackingMode,
|
||||
Text,
|
||||
TooltipDisplayMode,
|
||||
useStyles2,
|
||||
} from '@grafana/ui';
|
||||
|
||||
import { overrideToFixedColor } from '../../home/Insights';
|
||||
|
||||
import { GenericRow } from './GenericRow';
|
||||
|
||||
interface Instance {
|
||||
labels: Labels;
|
||||
series: DataFrame[];
|
||||
}
|
||||
|
||||
interface InstanceRowProps {
|
||||
instance: Instance;
|
||||
commonLabels: Labels;
|
||||
leftColumnWidth: number;
|
||||
timeRange: TimeRange;
|
||||
depth?: number;
|
||||
}
|
||||
|
||||
const chartConfig = VizConfigBuilders.timeseries()
|
||||
.setCustomFieldConfig('drawStyle', GraphDrawStyle.Bars)
|
||||
.setCustomFieldConfig('barWidthFactor', 1)
|
||||
.setCustomFieldConfig('barAlignment', BarAlignment.After)
|
||||
.setCustomFieldConfig('showPoints', VisibilityMode.Never)
|
||||
.setCustomFieldConfig('fillOpacity', 60)
|
||||
.setCustomFieldConfig('lineWidth', 0)
|
||||
.setCustomFieldConfig('stacking', { mode: StackingMode.None })
|
||||
.setCustomFieldConfig('axisPlacement', AxisPlacement.Hidden)
|
||||
.setCustomFieldConfig('axisGridShow', false)
|
||||
.setOption('tooltip', { mode: TooltipDisplayMode.Multi })
|
||||
.setOption('legend', {
|
||||
showLegend: false,
|
||||
displayMode: LegendDisplayMode.Hidden,
|
||||
})
|
||||
.setMin(0)
|
||||
.setMax(1)
|
||||
.setOverrides((builder) =>
|
||||
builder
|
||||
.matchFieldsWithName('firing')
|
||||
.overrideColor(overrideToFixedColor('firing'))
|
||||
.matchFieldsWithName('pending')
|
||||
.overrideColor(overrideToFixedColor('pending'))
|
||||
)
|
||||
.build();
|
||||
|
||||
export function InstanceRow({ instance, commonLabels, leftColumnWidth, timeRange, depth = 0 }: InstanceRowProps) {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const dataProvider = useMemo(
|
||||
() =>
|
||||
new SceneDataNode({
|
||||
data: {
|
||||
series: instance.series,
|
||||
state: LoadingState.Done,
|
||||
timeRange,
|
||||
},
|
||||
}),
|
||||
[instance, timeRange]
|
||||
);
|
||||
|
||||
return (
|
||||
<GenericRow
|
||||
width={leftColumnWidth}
|
||||
title={
|
||||
isEmpty(instance.labels) ? (
|
||||
<div className={styles.wrapper}>
|
||||
<Text color="secondary" variant="bodySmall">
|
||||
<Trans i18nKey="alerting.triage.no-labels">No labels</Trans>
|
||||
</Text>
|
||||
</div>
|
||||
) : (
|
||||
<AlertLabels
|
||||
labels={instance.labels}
|
||||
displayCommonLabels={true}
|
||||
labelSets={[instance.labels, commonLabels]}
|
||||
size="xs"
|
||||
/>
|
||||
)
|
||||
}
|
||||
content={
|
||||
<VizPanel title="" hoverHeader={true} viz={chartConfig} dataProvider={dataProvider} displayMode="transparent" />
|
||||
}
|
||||
depth={depth}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
wrapper: css({
|
||||
minHeight: theme.spacing(2.5),
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}),
|
||||
};
|
||||
};
|
|
@ -0,0 +1,13 @@
|
|||
import { WorkbenchRow } from '../types';
|
||||
|
||||
// Generate unique keys for WorkbenchRow items
|
||||
export function generateRowKey(row: WorkbenchRow, fallbackIndex: number): string {
|
||||
if (row.type === 'alertRule') {
|
||||
// Use ruleUID as primary key for AlertRuleRow
|
||||
return `alert-${row.metadata.ruleUID}`;
|
||||
} else {
|
||||
// For GenericGroupedRow, create key from label and value
|
||||
const groupedRow = row;
|
||||
return `group-${groupedRow.metadata.label}-${groupedRow.metadata.value}`;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
import { omit } from 'lodash';
|
||||
import { useMemo } from 'react';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
|
||||
import { DataFrame, Labels, findCommonLabels } from '@grafana/data';
|
||||
import { Trans } from '@grafana/i18n';
|
||||
import { useQueryRunner, useTimeRange } from '@grafana/scenes-react';
|
||||
import { Box } from '@grafana/ui';
|
||||
|
||||
import { useWorkbenchContext } from '../WorkbenchContext';
|
||||
import { METRIC_NAME } from '../constants';
|
||||
import { GenericRow } from '../rows/GenericRow';
|
||||
import { InstanceRow } from '../rows/InstanceRow';
|
||||
|
||||
import { getDataQuery } from './utils';
|
||||
|
||||
function extractInstancesFromData(series: DataFrame[] | undefined) {
|
||||
if (!series) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 1. Group series by labels, ignoring alertstate
|
||||
const groups = new Map<string, { labels: Labels; series: DataFrame[] }>();
|
||||
series.forEach((series) => {
|
||||
const valueField = series.fields.find((f) => f.type !== 'time');
|
||||
if (!valueField) {
|
||||
return;
|
||||
}
|
||||
|
||||
const keyLabels = omit(valueField.labels ?? {}, 'alertstate');
|
||||
const key = JSON.stringify(keyLabels);
|
||||
|
||||
if (!groups.has(key)) {
|
||||
groups.set(key, { labels: keyLabels, series: [] });
|
||||
}
|
||||
groups.get(key)!.series.push(series);
|
||||
});
|
||||
|
||||
return Array.from(groups.values());
|
||||
}
|
||||
|
||||
type AlertRuleInstancesProps = {
|
||||
ruleUID: string;
|
||||
depth?: number;
|
||||
};
|
||||
|
||||
export function AlertRuleInstances({ ruleUID, depth = 0 }: AlertRuleInstancesProps) {
|
||||
const { leftColumnWidth } = useWorkbenchContext();
|
||||
const [timeRange] = useTimeRange();
|
||||
|
||||
const query = getDataQuery(
|
||||
`count without (alertname, grafana_alertstate, grafana_folder, grafana_rule_uid) (${METRIC_NAME}{grafana_rule_uid="${ruleUID}"})`,
|
||||
{ format: 'timeseries', legendFormat: '{{alertstate}}' }
|
||||
);
|
||||
|
||||
const queryRunner = useQueryRunner({ queries: [query] });
|
||||
|
||||
const isLoading = !queryRunner.isDataReadyToDisplay();
|
||||
const { data } = queryRunner.useState();
|
||||
|
||||
const instances = useMemo(() => extractInstancesFromData(data?.series), [data]);
|
||||
|
||||
if (isLoading) {
|
||||
return <GenericRowSkeleton width={leftColumnWidth} depth={depth} />;
|
||||
}
|
||||
|
||||
if (!instances.length && !isLoading) {
|
||||
return (
|
||||
<GenericRow
|
||||
width={leftColumnWidth}
|
||||
title={<Trans i18nKey="alerting.triage.alert-instances">Alert instances</Trans>}
|
||||
depth={depth}
|
||||
>
|
||||
<div>
|
||||
<Trans i18nKey="alerting.triage.no-instances-found">No alert instances found for rule: {ruleUID}</Trans>
|
||||
</div>
|
||||
</GenericRow>
|
||||
);
|
||||
}
|
||||
|
||||
const allSeriesLabels: Labels[] = instances.map((instance) => instance.labels);
|
||||
const commonLabels = allSeriesLabels.length === 1 ? {} : findCommonLabels(allSeriesLabels);
|
||||
|
||||
return (
|
||||
<>
|
||||
{instances.map((instance) => (
|
||||
<InstanceRow
|
||||
key={JSON.stringify(instance.labels)}
|
||||
instance={instance}
|
||||
commonLabels={commonLabels}
|
||||
leftColumnWidth={leftColumnWidth}
|
||||
timeRange={timeRange}
|
||||
depth={depth}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function GenericRowSkeleton({ width, depth }: { width: number; depth: number }) {
|
||||
return (
|
||||
<GenericRow
|
||||
width={width}
|
||||
title={
|
||||
<Box flex={1}>
|
||||
<Skeleton width="100%" />
|
||||
</Box>
|
||||
}
|
||||
depth={depth}
|
||||
content={<Skeleton width="100%" />}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
import { VizConfigBuilders } from '@grafana/scenes';
|
||||
import { VizPanel, useDataTransformer } from '@grafana/scenes-react';
|
||||
import {
|
||||
AxisPlacement,
|
||||
BarAlignment,
|
||||
GraphDrawStyle,
|
||||
LegendDisplayMode,
|
||||
StackingMode,
|
||||
TooltipDisplayMode,
|
||||
VisibilityMode,
|
||||
} from '@grafana/schema';
|
||||
|
||||
import { overrideToFixedColor } from '../../home/Insights';
|
||||
import { useWorkbenchContext } from '../WorkbenchContext';
|
||||
|
||||
/**
|
||||
* Viz config for the alert rule summary chart - used by the React component
|
||||
*/
|
||||
export const alertRuleSummaryVizConfig = VizConfigBuilders.timeseries()
|
||||
.setCustomFieldConfig('drawStyle', GraphDrawStyle.Bars)
|
||||
.setCustomFieldConfig('barWidthFactor', 1)
|
||||
.setCustomFieldConfig('barAlignment', BarAlignment.After)
|
||||
.setCustomFieldConfig('showPoints', VisibilityMode.Never)
|
||||
.setCustomFieldConfig('fillOpacity', 60)
|
||||
.setCustomFieldConfig('lineWidth', 0)
|
||||
.setCustomFieldConfig('stacking', { mode: StackingMode.None })
|
||||
.setCustomFieldConfig('axisPlacement', AxisPlacement.Hidden)
|
||||
.setCustomFieldConfig('axisGridShow', false)
|
||||
.setMin(0)
|
||||
.setOption('tooltip', { mode: TooltipDisplayMode.Multi })
|
||||
.setOption('legend', {
|
||||
showLegend: false,
|
||||
displayMode: LegendDisplayMode.Hidden,
|
||||
})
|
||||
.setOverrides((builder) =>
|
||||
builder
|
||||
.matchFieldsWithName('firing')
|
||||
.overrideColor(overrideToFixedColor('firing'))
|
||||
.matchFieldsWithName('pending')
|
||||
.overrideColor(overrideToFixedColor('pending'))
|
||||
)
|
||||
.build();
|
||||
|
||||
export function AlertRuleSummary({ ruleUID }: { ruleUID: string }) {
|
||||
// Use WorkbenchContext to access the parent query runner and reuse its data
|
||||
const { queryRunner } = useWorkbenchContext();
|
||||
|
||||
// Transform parent data to filter by this specific rule and partition by alert state
|
||||
const transformedData = useDataTransformer({
|
||||
data: queryRunner,
|
||||
transformations: [
|
||||
{
|
||||
id: 'filterByValue',
|
||||
options: {
|
||||
filters: [
|
||||
{
|
||||
config: {
|
||||
id: 'equal',
|
||||
options: {
|
||||
value: ruleUID,
|
||||
},
|
||||
},
|
||||
fieldName: 'grafana_rule_uid',
|
||||
},
|
||||
],
|
||||
match: 'any',
|
||||
type: 'include',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'partitionByValues',
|
||||
options: {
|
||||
fields: ['alertstate'],
|
||||
keepFields: false,
|
||||
naming: {
|
||||
asLabels: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return (
|
||||
<VizPanel
|
||||
title=""
|
||||
viz={alertRuleSummaryVizConfig}
|
||||
dataProvider={transformedData}
|
||||
hoverHeader={true}
|
||||
displayMode="transparent"
|
||||
collapsible={false}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
import { SceneObjectBase, SceneObjectState, VizConfigBuilders } from '@grafana/scenes';
|
||||
import { VizPanel, useQueryRunner } from '@grafana/scenes-react';
|
||||
import { BarAlignment, GraphDrawStyle, VisibilityMode } from '@grafana/schema';
|
||||
import { LegendDisplayMode, StackingMode, TooltipDisplayMode } from '@grafana/ui';
|
||||
|
||||
import { overrideToFixedColor } from '../../home/Insights';
|
||||
import { METRIC_NAME } from '../constants';
|
||||
|
||||
import { getDataQuery, useQueryFilter } from './utils';
|
||||
|
||||
/**
|
||||
* Viz config for the summary chart - used by the React component
|
||||
*/
|
||||
export const summaryChartVizConfig = VizConfigBuilders.timeseries()
|
||||
.setCustomFieldConfig('drawStyle', GraphDrawStyle.Bars)
|
||||
.setCustomFieldConfig('barWidthFactor', 1)
|
||||
.setCustomFieldConfig('barAlignment', BarAlignment.Center)
|
||||
.setCustomFieldConfig('fillOpacity', 60)
|
||||
.setCustomFieldConfig('lineWidth', 0)
|
||||
.setCustomFieldConfig('stacking', { mode: StackingMode.None })
|
||||
.setCustomFieldConfig('showPoints', VisibilityMode.Never)
|
||||
.setOption('legend', {
|
||||
showLegend: false,
|
||||
displayMode: LegendDisplayMode.Hidden,
|
||||
})
|
||||
.setOption('tooltip', { mode: TooltipDisplayMode.Multi })
|
||||
.setMin(0)
|
||||
.setOverrides((builder) =>
|
||||
builder
|
||||
.matchFieldsWithName('firing')
|
||||
.overrideColor(overrideToFixedColor('firing'))
|
||||
.matchFieldsWithName('pending')
|
||||
.overrideColor(overrideToFixedColor('pending'))
|
||||
)
|
||||
.build();
|
||||
|
||||
export function SummaryChartReact() {
|
||||
const filter = useQueryFilter();
|
||||
|
||||
const dataProvider = useQueryRunner({
|
||||
queries: [
|
||||
getDataQuery(`count by (alertstate) (${METRIC_NAME}{${filter}})`, {
|
||||
legendFormat: '{{alertstate}}', // we need this so we can map states to the correct color in the vizConfig
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
return <VizPanel title="" viz={summaryChartVizConfig} dataProvider={dataProvider} hoverHeader={true} />;
|
||||
}
|
||||
|
||||
// simple wrapper so we can render the Chart using a Scene parent
|
||||
export class SummaryChartScene extends SceneObjectBase<SceneObjectState> {
|
||||
static Component = SummaryChartReact;
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
import { DataFrameView } from '@grafana/data';
|
||||
import { Trans } from '@grafana/i18n';
|
||||
import { SceneObjectBase, SceneObjectState } from '@grafana/scenes';
|
||||
import { useQueryRunner } from '@grafana/scenes-react';
|
||||
import { Stack, Text } from '@grafana/ui';
|
||||
|
||||
import { Spacer } from '../../components/Spacer';
|
||||
import { METRIC_NAME } from '../constants';
|
||||
|
||||
import { getDataQuery, useQueryFilter } from './utils';
|
||||
|
||||
interface Frame {
|
||||
alertstate: 'firing' | 'pending';
|
||||
Value: number;
|
||||
}
|
||||
|
||||
export function SummaryStatsReact() {
|
||||
const filter = useQueryFilter();
|
||||
|
||||
const dataProvider = useQueryRunner({
|
||||
queries: [
|
||||
getDataQuery(`count by (alertstate) (${METRIC_NAME}{${filter}})`, {
|
||||
instant: true,
|
||||
exemplar: false,
|
||||
format: 'table',
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const isLoading = !dataProvider.isDataReadyToDisplay;
|
||||
const data = dataProvider.useState().data;
|
||||
const firstFrame = data?.series?.at(0);
|
||||
|
||||
if (isLoading || !firstFrame) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const dfv = new DataFrameView<Frame>(firstFrame);
|
||||
if (dfv.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const firingIndex = dfv.fields.alertstate.values.findIndex((state) => state === 'firing');
|
||||
const firingCount = dfv.fields.Value.values[firingIndex] ?? 0;
|
||||
|
||||
const pendingIndex = dfv.fields.alertstate.values.findIndex((state) => state === 'pending');
|
||||
const pendingCount = dfv.fields.Value.values[pendingIndex] ?? 0;
|
||||
|
||||
return (
|
||||
<Stack direction="column" alignItems="flex-end" gap={0}>
|
||||
<Spacer />
|
||||
<Text color="error">
|
||||
<Trans i18nKey="alerting.triage.firing-instances-count">{{ firingCount }} firing instances</Trans>
|
||||
</Text>
|
||||
<Text color="warning">
|
||||
<Trans i18nKey="alerting.triage.pending-instances-count">{{ pendingCount }} pending instances</Trans>
|
||||
</Text>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
// simple wrapper so we can render the Chart using a Scene parent
|
||||
export class SummaryStatsScene extends SceneObjectBase<SceneObjectState> {
|
||||
static Component = SummaryStatsReact;
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
import { DashboardCursorSync } from '@grafana/data';
|
||||
import {
|
||||
AdHocFiltersVariable,
|
||||
GroupByVariable,
|
||||
SceneControlsSpacer,
|
||||
SceneFlexLayout,
|
||||
SceneRefreshPicker,
|
||||
SceneTimePicker,
|
||||
SceneTimeRange,
|
||||
SceneVariableSet,
|
||||
VariableValueSelectors,
|
||||
behaviors,
|
||||
} from '@grafana/scenes';
|
||||
import { EmbeddedSceneWithContext } from '@grafana/scenes-react';
|
||||
|
||||
import { DATASOURCE_UID } from '../constants';
|
||||
|
||||
import { WorkbenchSceneObject } from './Workbench';
|
||||
import { defaultTimeRange } from './utils';
|
||||
|
||||
const cursorSync = new behaviors.CursorSync({ key: 'triage-cursor-sync', sync: DashboardCursorSync.Crosshair });
|
||||
|
||||
export const triageScene = new EmbeddedSceneWithContext({
|
||||
// this will allow us to share the cursor between all vizualizations
|
||||
$behaviors: [cursorSync],
|
||||
controls: [
|
||||
new VariableValueSelectors({}),
|
||||
new SceneControlsSpacer(),
|
||||
new SceneTimePicker({}),
|
||||
new SceneRefreshPicker({}),
|
||||
],
|
||||
$timeRange: new SceneTimeRange(defaultTimeRange),
|
||||
$variables: new SceneVariableSet({
|
||||
variables: [
|
||||
new GroupByVariable({
|
||||
name: 'groupBy',
|
||||
label: 'Group by',
|
||||
datasource: {
|
||||
type: 'prometheus',
|
||||
uid: DATASOURCE_UID,
|
||||
},
|
||||
allowCustomValue: true,
|
||||
applyMode: 'manual',
|
||||
value: 'grafana_folder',
|
||||
}),
|
||||
new AdHocFiltersVariable({
|
||||
name: 'filters',
|
||||
label: 'Filters',
|
||||
datasource: {
|
||||
type: 'prometheus',
|
||||
uid: DATASOURCE_UID,
|
||||
},
|
||||
applyMode: 'manual', // we will construct the label matchers for the PromQL queries ourselves
|
||||
allowCustomValue: true,
|
||||
useQueriesAsFilterForOptions: true,
|
||||
supportsMultiValueOperators: true,
|
||||
filters: [],
|
||||
baseFilters: [],
|
||||
layout: 'combobox',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
body: new SceneFlexLayout({
|
||||
direction: 'column',
|
||||
children: [new WorkbenchSceneObject({})],
|
||||
}),
|
||||
});
|
||||
|
||||
export const TriageScene = () => <triageScene.Component model={triageScene} />;
|
|
@ -0,0 +1,133 @@
|
|||
import { ArrayValues } from 'type-fest';
|
||||
|
||||
import { DataFrame, PanelData } from '@grafana/data';
|
||||
import { SceneObjectBase, SceneObjectState } from '@grafana/scenes';
|
||||
import { useQueryRunner, useTimeRange, useVariableValues } from '@grafana/scenes-react';
|
||||
|
||||
import { Workbench } from '../Workbench';
|
||||
import { DEFAULT_FIELDS, METRIC_NAME, VARIABLES } from '../constants';
|
||||
import { AlertRuleRow, GenericGroupedRow, WorkbenchRow } from '../types';
|
||||
|
||||
import { convertTimeRangeToDomain, getDataQuery, useQueryFilter } from './utils';
|
||||
|
||||
export class WorkbenchSceneObject extends SceneObjectBase<SceneObjectState> {
|
||||
public static Component = WorkbenchRenderer;
|
||||
}
|
||||
|
||||
export function WorkbenchRenderer() {
|
||||
const [timeRange] = useTimeRange();
|
||||
const domain = convertTimeRangeToDomain(timeRange);
|
||||
|
||||
const [groupByKeys = []] = useVariableValues<string>(VARIABLES.groupBy);
|
||||
|
||||
const countBy = [...DEFAULT_FIELDS, ...groupByKeys].join(',');
|
||||
const queryFilter = useQueryFilter();
|
||||
|
||||
const runner = useQueryRunner({
|
||||
queries: [
|
||||
getDataQuery(`count by (${countBy}) (${METRIC_NAME}{${queryFilter}})`, {
|
||||
format: 'table',
|
||||
}),
|
||||
],
|
||||
});
|
||||
const { data } = runner.useState();
|
||||
const rows = data ? convertToWorkbenchRows(data, groupByKeys) : [];
|
||||
|
||||
return <Workbench data={rows} domain={domain} queryRunner={runner} />;
|
||||
}
|
||||
|
||||
type DataPoint = Record<ArrayValues<typeof DEFAULT_FIELDS>, string> & Record<string, string | undefined>;
|
||||
|
||||
function createAlertRuleRows(dataPoints: DataPoint[]): AlertRuleRow[] {
|
||||
const rules = new Map<
|
||||
string,
|
||||
{
|
||||
alertname: string;
|
||||
folder: string;
|
||||
ruleUID: string;
|
||||
}
|
||||
>();
|
||||
|
||||
for (const dp of dataPoints) {
|
||||
const ruleUID = dp.grafana_rule_uid;
|
||||
if (!rules.has(ruleUID)) {
|
||||
rules.set(ruleUID, {
|
||||
alertname: dp.alertname,
|
||||
folder: dp.grafana_folder,
|
||||
ruleUID: ruleUID,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const result: AlertRuleRow[] = [];
|
||||
for (const rule of rules.values()) {
|
||||
result.push({
|
||||
type: 'alertRule',
|
||||
metadata: {
|
||||
title: rule.alertname,
|
||||
folder: rule.folder,
|
||||
ruleUID: rule.ruleUID,
|
||||
},
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function groupData(dataPoints: DataPoint[], groupBy: string[], depth: number): WorkbenchRow[] {
|
||||
if (depth >= groupBy.length) {
|
||||
return createAlertRuleRows(dataPoints);
|
||||
}
|
||||
|
||||
const groupByKey = groupBy[depth];
|
||||
const grouped = new Map<string, DataPoint[]>();
|
||||
|
||||
for (const dp of dataPoints) {
|
||||
const key = String(dp[groupByKey] ?? 'undefined');
|
||||
if (!grouped.has(key)) {
|
||||
grouped.set(key, []);
|
||||
}
|
||||
grouped.get(key)?.push(dp);
|
||||
}
|
||||
|
||||
const result: GenericGroupedRow[] = [];
|
||||
for (const [value, rows] of grouped.entries()) {
|
||||
result.push({
|
||||
type: 'group',
|
||||
metadata: {
|
||||
label: groupByKey,
|
||||
value: value,
|
||||
},
|
||||
rows: groupData(rows, groupBy, depth + 1),
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// @TODO narrower types for PanelData! (if possible)
|
||||
export function convertToWorkbenchRows(data: PanelData, groupBy: string[] = []): WorkbenchRow[] {
|
||||
if (!data.series.at(0)?.fields.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const frame = data.series[0];
|
||||
if (!isValidFrame(frame)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const allDataPoints = Array.from({ length: frame.length }, (_, i) => {
|
||||
const dataPoint: DataPoint = Object.create(null);
|
||||
frame.fields.forEach((field) => {
|
||||
dataPoint[field.name] = field.values[i];
|
||||
});
|
||||
return dataPoint;
|
||||
});
|
||||
|
||||
return groupData(allDataPoints, groupBy, 0);
|
||||
}
|
||||
|
||||
function isValidFrame(frame: DataFrame) {
|
||||
const requiredFieldNames = ['Time', ...DEFAULT_FIELDS];
|
||||
const fieldNames = new Set(frame.fields.map((f) => f.name));
|
||||
return requiredFieldNames.every((name) => fieldNames.has(name));
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
import { TimeRange } from '@grafana/data';
|
||||
import { SceneDataQuery } from '@grafana/scenes';
|
||||
import { useVariableValue, useVariableValues } from '@grafana/scenes-react';
|
||||
import { DataSourceRef } from '@grafana/schema';
|
||||
|
||||
import { DATASOURCE_UID, VARIABLES } from '../constants';
|
||||
import { Domain } from '../types';
|
||||
|
||||
export function getDataQuery(expression: string, options?: Partial<SceneDataQuery>): SceneDataQuery {
|
||||
const datasourceRef: DataSourceRef = {
|
||||
type: 'prometheus',
|
||||
uid: DATASOURCE_UID,
|
||||
};
|
||||
|
||||
const query: SceneDataQuery = {
|
||||
refId: 'query',
|
||||
expr: expression,
|
||||
instant: false,
|
||||
datasource: datasourceRef,
|
||||
...options,
|
||||
};
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns an array of "groupBy" keys into a Prometheus matcher such as key!="",key2!="" .
|
||||
* This way we can show only instances that have a label that was grouped on.
|
||||
*/
|
||||
export function stringifyGroupFilter(groupBy: string[]) {
|
||||
return groupBy.map((key) => `${key}!=""`).join(',');
|
||||
}
|
||||
|
||||
export const defaultTimeRange = {
|
||||
from: 'now-4h',
|
||||
to: 'now',
|
||||
} as const;
|
||||
|
||||
export function convertTimeRangeToDomain(timeRange: TimeRange): Domain {
|
||||
return [timeRange.from.toDate(), timeRange.to.toDate()];
|
||||
}
|
||||
|
||||
/**
|
||||
* This hook will create a Prometheus label matcher string from the "groupBy" and "filters" variables
|
||||
*/
|
||||
export function useQueryFilter(): string {
|
||||
const [groupBy = []] = useVariableValues<string>(VARIABLES.groupBy);
|
||||
const [filters = ''] = useVariableValue<string>(VARIABLES.filters);
|
||||
|
||||
const groupByFilter = stringifyGroupFilter(groupBy);
|
||||
const queryFilter = [groupByFilter, filters].filter((s) => Boolean(s)).join(',');
|
||||
|
||||
return queryFilter;
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
export type Domain = [Date, Date];
|
||||
export type Filter = [key: string, operator: '=' | '=!', value: string];
|
||||
|
||||
export type WorkbenchRow = GenericGroupedRow | AlertRuleRow;
|
||||
|
||||
export type TimelineEntry = [timestamp: number, state: 'firing' | 'pending'];
|
||||
|
||||
export interface AlertRuleRow {
|
||||
type: 'alertRule';
|
||||
metadata: {
|
||||
title: string;
|
||||
folder: string;
|
||||
ruleUID: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GenericGroupedRow {
|
||||
type: 'group';
|
||||
metadata: {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
rows: WorkbenchRow[];
|
||||
}
|
|
@ -15,7 +15,7 @@ import { isDashboardV2Resource, isV1DashboardCommand, isV2DashboardCommand } fro
|
|||
import { SaveDashboardCommand } from 'app/features/dashboard/components/SaveDashboard/types';
|
||||
import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher';
|
||||
import { dispatch } from 'app/store/store';
|
||||
import { PermissionLevelString } from 'app/types/acl';
|
||||
import { PermissionLevel } from 'app/types/acl';
|
||||
import { SaveDashboardResponseDTO, ImportDashboardResponseDTO } from 'app/types/dashboard';
|
||||
import { FolderListItemDTO, FolderDTO, DescendantCount, DescendantCountDTO } from 'app/types/folders';
|
||||
|
||||
|
@ -71,7 +71,7 @@ export interface ListFolderQueryArgs {
|
|||
page: number;
|
||||
parentUid: string | undefined;
|
||||
limit: number;
|
||||
permission?: PermissionLevelString;
|
||||
permission?: PermissionLevel;
|
||||
}
|
||||
|
||||
export const browseDashboardsAPI = createApi({
|
||||
|
|
|
@ -5,7 +5,7 @@ import { useScopesServices } from 'app/features/scopes/ScopesContextProvider';
|
|||
import { CommandPaletteAction } from '../types';
|
||||
import { RECENT_SCOPES_PRIORITY } from '../values';
|
||||
|
||||
export function getRecentScopesActions(): CommandPaletteAction[] {
|
||||
export function useRecentScopesActions(): CommandPaletteAction[] {
|
||||
const services = useScopesServices();
|
||||
|
||||
if (!(config.featureToggles.scopeFilters && services)) {
|
||||
|
|
|
@ -7,7 +7,7 @@ import { config } from '@grafana/runtime';
|
|||
import { ScopesRow } from '../ScopesRow';
|
||||
import { CommandPaletteAction } from '../types';
|
||||
|
||||
import { getRecentScopesActions } from './recentScopesActions';
|
||||
import { useRecentScopesActions } from './recentScopesActions';
|
||||
import {
|
||||
getScopesParentAction,
|
||||
mapScopeNodeToAction,
|
||||
|
@ -16,7 +16,7 @@ import {
|
|||
} from './scopesUtils';
|
||||
|
||||
export function useRegisterRecentScopesActions() {
|
||||
const recentScopesActions = getRecentScopesActions();
|
||||
const recentScopesActions = useRecentScopesActions();
|
||||
useRegisterActions(recentScopesActions, [recentScopesActions]);
|
||||
}
|
||||
|
||||
|
|
|
@ -30,6 +30,7 @@ import {
|
|||
serializeStateToUrlParam,
|
||||
urlUtil,
|
||||
LogLevel,
|
||||
shallowCompare,
|
||||
} from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { config, reportInteraction } from '@grafana/runtime';
|
||||
|
@ -53,7 +54,7 @@ import { InfiniteScroll } from 'app/features/logs/components/InfiniteScroll';
|
|||
import { LogRows } from 'app/features/logs/components/LogRows';
|
||||
import { LogRowContextModal } from 'app/features/logs/components/log-context/LogRowContextModal';
|
||||
import { LogLineContext } from 'app/features/logs/components/panel/LogLineContext';
|
||||
import { LogList, LogListControlOptions } from 'app/features/logs/components/panel/LogList';
|
||||
import { LogList, LogListOptions } from 'app/features/logs/components/panel/LogList';
|
||||
import { isDedupStrategy, isLogsSortOrder } from 'app/features/logs/components/panel/LogListContext';
|
||||
import { LogLevelColor, dedupLogRows } from 'app/features/logs/logsModel';
|
||||
import { getLogLevelFromKey, getLogLevelInfo } from 'app/features/logs/utils';
|
||||
|
@ -205,7 +206,8 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
|
|||
store.get(SETTINGS_KEYS.logsSortOrder) || LogsSortOrder.Descending
|
||||
);
|
||||
const [isFlipping, setIsFlipping] = useState<boolean>(false);
|
||||
const [displayedFields, setDisplayedFields] = useState<string[]>([]);
|
||||
const [displayedFields, setDisplayedFields] = useState<string[]>(panelState?.logs?.displayedFields ?? []);
|
||||
const [defaultDisplayedFields, setDefaultDisplayedFields] = useState<string[]>([]);
|
||||
const [contextOpen, setContextOpen] = useState<boolean>(false);
|
||||
const [contextRow, setContextRow] = useState<LogRowModel | undefined>(undefined);
|
||||
const [pinLineButtonTooltipTitle, setPinLineButtonTooltipTitle] = useState<PopoverContent>(PINNED_LOGS_MESSAGE);
|
||||
|
@ -280,16 +282,6 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
|
|||
store.set(visualisationTypeKey, visualisationType);
|
||||
}, [panelState?.logs?.visualisationType]);
|
||||
|
||||
useEffect(() => {
|
||||
let displayedFields: string[] = [];
|
||||
if (Array.isArray(panelState?.logs?.displayedFields)) {
|
||||
displayedFields = panelState?.logs?.displayedFields;
|
||||
} else if (panelState?.logs?.displayedFields && typeof panelState?.logs?.displayedFields === 'object') {
|
||||
displayedFields = Object.values(panelState?.logs?.displayedFields);
|
||||
}
|
||||
setDisplayedFields(displayedFields);
|
||||
}, [panelState?.logs?.displayedFields]);
|
||||
|
||||
useUnmount(() => {
|
||||
if (flipOrderTimer) {
|
||||
window.clearTimeout(flipOrderTimer.current);
|
||||
|
@ -346,6 +338,15 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
|
|||
]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!shallowCompare(displayedFields, panelState?.logs?.displayedFields ?? [])) {
|
||||
updatePanelState({
|
||||
...panelState?.logs,
|
||||
displayedFields,
|
||||
});
|
||||
}
|
||||
}, [displayedFields, panelState?.logs, updatePanelState]);
|
||||
|
||||
// actions
|
||||
const onLogRowHover = useCallback(
|
||||
(row?: LogRowModel) => {
|
||||
|
@ -544,13 +545,9 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
|
|||
if (index === -1) {
|
||||
const updatedDisplayedFields = displayedFields.concat(key);
|
||||
setDisplayedFields(updatedDisplayedFields);
|
||||
updatePanelState({
|
||||
...panelState?.logs,
|
||||
displayedFields: updatedDisplayedFields,
|
||||
});
|
||||
}
|
||||
},
|
||||
[displayedFields, panelState?.logs, updatePanelState]
|
||||
[displayedFields]
|
||||
);
|
||||
|
||||
const hideField = useCallback(
|
||||
|
@ -559,22 +556,14 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
|
|||
if (index > -1) {
|
||||
const updatedDisplayedFields = displayedFields.filter((k) => key !== k);
|
||||
setDisplayedFields(updatedDisplayedFields);
|
||||
updatePanelState({
|
||||
...panelState?.logs,
|
||||
displayedFields: updatedDisplayedFields,
|
||||
});
|
||||
}
|
||||
},
|
||||
[displayedFields, panelState?.logs, updatePanelState]
|
||||
[displayedFields]
|
||||
);
|
||||
|
||||
const clearDetectedFields = useCallback(() => {
|
||||
updatePanelState({
|
||||
...panelState?.logs,
|
||||
displayedFields: [],
|
||||
});
|
||||
const clearDisplayedFields = useCallback(() => {
|
||||
setDisplayedFields([]);
|
||||
}, [panelState?.logs, updatePanelState]);
|
||||
}, []);
|
||||
|
||||
const onCloseCallbackRef = useRef<() => void>(() => {});
|
||||
|
||||
|
@ -703,7 +692,7 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
|
|||
|
||||
const visibilityChangedRef = useRef(true);
|
||||
const onLogOptionsChange = useCallback(
|
||||
(option: LogListControlOptions, value: string | string[] | boolean) => {
|
||||
(option: LogListOptions, value: string | string[] | boolean) => {
|
||||
if (option === 'sortOrder' && isLogsSortOrder(value)) {
|
||||
sortOrderChanged(value);
|
||||
} else if (option === 'dedupStrategy' && isDedupStrategy(value)) {
|
||||
|
@ -757,6 +746,8 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
|
|||
|
||||
return newLevels;
|
||||
});
|
||||
} else if (option === 'defaultDisplayedFields' && Array.isArray(value)) {
|
||||
setDefaultDisplayedFields(value);
|
||||
}
|
||||
},
|
||||
[logsVolumeData?.data, logsVolumeEnabled, sortOrderChanged]
|
||||
|
@ -985,7 +976,8 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
|
|||
dedupStrategy={dedupStrategy}
|
||||
dedupCount={dedupCount}
|
||||
displayedFields={displayedFields}
|
||||
clearDetectedFields={clearDetectedFields}
|
||||
clearDisplayedFields={clearDisplayedFields}
|
||||
defaultDisplayedFields={defaultDisplayedFields}
|
||||
/>
|
||||
</div>
|
||||
<div className={cx(styles.logsSection, visualisationType === 'table' ? styles.logsTable : undefined)}>
|
||||
|
|
|
@ -26,7 +26,8 @@ const defaultProps: LogsMetaRowProps = {
|
|||
dedupCount: 0,
|
||||
displayedFields: [],
|
||||
logRows: [],
|
||||
clearDetectedFields: jest.fn(),
|
||||
clearDisplayedFields: jest.fn(),
|
||||
defaultDisplayedFields: [],
|
||||
};
|
||||
|
||||
const setup = (propOverrides?: object, disableDownload = false) => {
|
||||
|
@ -61,7 +62,7 @@ describe('LogsMetaRow', () => {
|
|||
|
||||
it('renders a button to clear displayedfields', () => {
|
||||
const clearSpy = jest.fn();
|
||||
setup({ displayedFields: ['testField1234'], clearDetectedFields: clearSpy });
|
||||
setup({ displayedFields: ['testField1234'], clearDisplayedFields: clearSpy });
|
||||
fireEvent(
|
||||
screen.getByRole('button', {
|
||||
name: 'Show original line',
|
||||
|
|
|
@ -1,7 +1,16 @@
|
|||
import { css } from '@emotion/css';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { LogsDedupStrategy, LogsMetaItem, LogsMetaKind, LogRowModel, CoreApp, Labels, store } from '@grafana/data';
|
||||
import {
|
||||
LogsDedupStrategy,
|
||||
LogsMetaItem,
|
||||
LogsMetaKind,
|
||||
LogRowModel,
|
||||
CoreApp,
|
||||
Labels,
|
||||
store,
|
||||
shallowCompare,
|
||||
} from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { config, reportInteraction } from '@grafana/runtime';
|
||||
import { Button, Dropdown, Menu, ToolbarButton, useStyles2 } from '@grafana/ui';
|
||||
|
@ -30,11 +39,20 @@ export type Props = {
|
|||
dedupCount: number;
|
||||
displayedFields: string[];
|
||||
logRows: LogRowModel[];
|
||||
clearDetectedFields: () => void;
|
||||
clearDisplayedFields: () => void;
|
||||
defaultDisplayedFields: string[];
|
||||
};
|
||||
|
||||
export const LogsMetaRow = memo(
|
||||
({ meta, dedupStrategy, dedupCount, displayedFields, clearDetectedFields, logRows }: Props) => {
|
||||
({
|
||||
meta,
|
||||
dedupStrategy,
|
||||
dedupCount,
|
||||
displayedFields,
|
||||
clearDisplayedFields,
|
||||
logRows,
|
||||
defaultDisplayedFields,
|
||||
}: Props) => {
|
||||
const style = useStyles2(getStyles);
|
||||
|
||||
const logsMetaItem: Array<LogsMetaItem | MetaItemProps> = [...meta];
|
||||
|
@ -49,7 +67,7 @@ export const LogsMetaRow = memo(
|
|||
}
|
||||
|
||||
// Add detected fields info
|
||||
if (displayedFields?.length > 0) {
|
||||
if (displayedFields?.length > 0 && shallowCompare(displayedFields, defaultDisplayedFields) === false) {
|
||||
logsMetaItem.push(
|
||||
{
|
||||
label: t('explore.logs-meta-row.label.showing-only-selected-fields', 'Showing only selected fields'),
|
||||
|
@ -58,8 +76,8 @@ export const LogsMetaRow = memo(
|
|||
{
|
||||
label: '',
|
||||
value: (
|
||||
<Button variant="primary" fill="outline" size="sm" onClick={clearDetectedFields}>
|
||||
<Trans i18nKey="explore.logs-meta-row.show-original-line">Show original line</Trans>
|
||||
<Button variant="primary" fill="outline" size="sm" onClick={clearDisplayedFields}>
|
||||
{t('explore.logs-meta-row.show-original-line', 'Show original line')}
|
||||
</Button>
|
||||
),
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { t } from '@grafana/i18n';
|
||||
import { useTheme2 } from '@grafana/ui';
|
||||
import { OTEL_LOG_LINE_ATTRIBUTES_FIELD_NAME } from 'app/features/logs/components/otel/formats';
|
||||
|
||||
import { getLogsFieldsStyles } from './LogsTableActiveFields';
|
||||
import { LogsTableEmptyFields } from './LogsTableEmptyFields';
|
||||
|
@ -36,7 +37,9 @@ export const LogsTableAvailableFields = (props: {
|
|||
const theme = useTheme2();
|
||||
|
||||
const styles = getLogsFieldsStyles(theme);
|
||||
const labelKeys = Object.keys(labels).filter((labelName) => valueFilter(labelName));
|
||||
const labelKeys = Object.keys(labels)
|
||||
.filter((labelName) => labelName !== OTEL_LOG_LINE_ATTRIBUTES_FIELD_NAME)
|
||||
.filter((labelName) => valueFilter(labelName));
|
||||
if (labelKeys.length) {
|
||||
// Otherwise show list with a hardcoded order
|
||||
return (
|
||||
|
|
|
@ -20,7 +20,7 @@ import { LogsVisualisationType } from '../../explore/Logs/Logs';
|
|||
import { ControlledLogsTable } from './ControlledLogsTable';
|
||||
import { InfiniteScroll } from './InfiniteScroll';
|
||||
import { LogRows, Props } from './LogRows';
|
||||
import { LogListControlOptions } from './panel/LogList';
|
||||
import { LogListOptions } from './panel/LogList';
|
||||
import { LogListContextProvider, useLogListContext } from './panel/LogListContext';
|
||||
import { LogListControls } from './panel/LogListControls';
|
||||
import { ScrollToLogsEvent } from './panel/virtualization';
|
||||
|
@ -30,7 +30,7 @@ export interface ControlledLogRowsProps extends Omit<Props, 'scrollElement'> {
|
|||
logsMeta?: LogsMetaItem[];
|
||||
loadMoreLogs?: (range: AbsoluteTimeRange) => void;
|
||||
logOptionsStorageKey?: string;
|
||||
onLogOptionsChange?: (option: LogListControlOptions, value: string | boolean | string[]) => void;
|
||||
onLogOptionsChange?: (option: LogListOptions, value: string | boolean | string[]) => void;
|
||||
range: TimeRange;
|
||||
filterLevels?: LogLevel[];
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import userEvent from '@testing-library/user-event';
|
|||
|
||||
import { LOG_LINE_BODY_FIELD_NAME } from './LogDetailsBody';
|
||||
import { LogLabels, LogLabelsList } from './LogLabels';
|
||||
import { getNormalizedFieldName } from './panel/processing';
|
||||
|
||||
describe('<LogLabels />', () => {
|
||||
it('renders notice when no labels are found', () => {
|
||||
|
@ -96,6 +97,6 @@ describe('<LogLabelsList />', () => {
|
|||
render(<LogLabelsList labels={['bar', '42', LOG_LINE_BODY_FIELD_NAME]} />);
|
||||
expect(screen.queryByText('bar')).toBeInTheDocument();
|
||||
expect(screen.queryByText('42')).toBeInTheDocument();
|
||||
expect(screen.queryByText('log line')).toBeInTheDocument();
|
||||
expect(screen.queryByText(getNormalizedFieldName(LOG_LINE_BODY_FIELD_NAME))).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,7 +5,7 @@ import { GrafanaTheme2, Labels } from '@grafana/data';
|
|||
import { t } from '@grafana/i18n';
|
||||
import { Button, Icon, Tooltip, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { LOG_LINE_BODY_FIELD_NAME } from './LogDetailsBody';
|
||||
import { getNormalizedFieldName } from './panel/processing';
|
||||
|
||||
// Levels are already encoded in color, filename is a Loki-ism
|
||||
const HIDDEN_LABELS = ['detected_level', 'level', 'lvl', 'filename'];
|
||||
|
@ -111,7 +111,7 @@ export const LogLabelsList = memo(({ labels }: LogLabelsArrayProps) => {
|
|||
<span className={styles.logsLabels}>
|
||||
{labels.map((label) => (
|
||||
<LogLabel key={label} styles={styles} tooltip={label}>
|
||||
{label === LOG_LINE_BODY_FIELD_NAME ? t('logs.log-labels-list.log-line', 'log line') : label}
|
||||
{getNormalizedFieldName(label)}
|
||||
</LogLabel>
|
||||
))}
|
||||
</span>
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
import { LOG_LINE_BODY_FIELD_NAME } from '../LogDetailsBody';
|
||||
import { createLogLine } from '../mocks/logRow';
|
||||
|
||||
import { getDisplayedFieldsForLogs, getOtelFormattedBody, OTEL_PROBE_FIELD } from './formats';
|
||||
import {
|
||||
getDisplayedFieldsForLogs,
|
||||
getOtelAttributesField,
|
||||
OTEL_LOG_LINE_ATTRIBUTES_FIELD_NAME,
|
||||
OTEL_PROBE_FIELD,
|
||||
} from './formats';
|
||||
|
||||
describe('getDisplayedFieldsForLogs', () => {
|
||||
test('Does not return displayed fields if not an OTel log line', () => {
|
||||
|
@ -18,43 +23,97 @@ describe('getDisplayedFieldsForLogs', () => {
|
|||
|
||||
test('Returns displayed fields if the OTel probe field is present', () => {
|
||||
const log = createLogLine({
|
||||
labels: { [OTEL_PROBE_FIELD]: '1', telemetry_sdk_language: 'php', scope_name: 'scope' },
|
||||
labels: { [OTEL_PROBE_FIELD]: '1', telemetry_sdk_language: 'php', thread_name: 'John' },
|
||||
entry: `place="luna" 1ms 3 KB`,
|
||||
});
|
||||
|
||||
expect(getDisplayedFieldsForLogs([log])).toEqual(['scope_name', LOG_LINE_BODY_FIELD_NAME]);
|
||||
expect(getDisplayedFieldsForLogs([log])).toEqual([
|
||||
'thread_name',
|
||||
LOG_LINE_BODY_FIELD_NAME,
|
||||
OTEL_LOG_LINE_ATTRIBUTES_FIELD_NAME,
|
||||
]);
|
||||
expect(log.otelLanguage).toBe('php');
|
||||
});
|
||||
|
||||
test('Returns displayed fields if the OTel probe field is present and the language unknown', () => {
|
||||
const log = createLogLine({
|
||||
labels: { [OTEL_PROBE_FIELD]: '1', scope_name: 'scope' },
|
||||
labels: { [OTEL_PROBE_FIELD]: '1', exception_type: 'fatal', exception_message: 'message' },
|
||||
entry: `place="luna" 1ms 3 KB`,
|
||||
});
|
||||
|
||||
expect(getDisplayedFieldsForLogs([log])).toEqual(['scope_name', LOG_LINE_BODY_FIELD_NAME]);
|
||||
expect(getDisplayedFieldsForLogs([log])).toEqual([
|
||||
'exception_type',
|
||||
'exception_message',
|
||||
LOG_LINE_BODY_FIELD_NAME,
|
||||
OTEL_LOG_LINE_ATTRIBUTES_FIELD_NAME,
|
||||
]);
|
||||
expect(log.otelLanguage).toBe('unknown');
|
||||
});
|
||||
|
||||
test('Returns the minimal displayed fields if others are not present', () => {
|
||||
const log = createLogLine({
|
||||
labels: { [OTEL_PROBE_FIELD]: '1' },
|
||||
entry: `place="luna" 1ms 3 KB`,
|
||||
});
|
||||
|
||||
expect(getDisplayedFieldsForLogs([log])).toEqual([LOG_LINE_BODY_FIELD_NAME, OTEL_LOG_LINE_ATTRIBUTES_FIELD_NAME]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOtelFormattedBody', () => {
|
||||
test('Does not modify non OTel logs', () => {
|
||||
const log = createLogLine({ labels: { place: 'luna' }, entry: `place="luna" 1ms 3 KB` });
|
||||
expect(getOtelFormattedBody(log)).toEqual(`place="luna" 1ms 3 KB`);
|
||||
});
|
||||
|
||||
test('Returns an OTel augmented log line body', () => {
|
||||
describe('getOtelAttributesField', () => {
|
||||
test('Builds the OTel attributes fields from the log line fields including and excluding fields', () => {
|
||||
const log = createLogLine({
|
||||
labels: {
|
||||
severity_number: '1',
|
||||
telemetry_sdk_language: 'php',
|
||||
scope_name: 'scope',
|
||||
aws_ignore: 'ignored',
|
||||
key: 'value',
|
||||
otel: 'otel',
|
||||
aws_something: 'nope',
|
||||
k8s_something: 'nope',
|
||||
cluster: 'nope',
|
||||
namespace: 'nope',
|
||||
pod: 'nope',
|
||||
vcs_ref_head_name: 'main',
|
||||
field: 'value',
|
||||
},
|
||||
entry: `place="luna" 1ms 3 KB`,
|
||||
});
|
||||
expect(getOtelFormattedBody(log)).toEqual(`place="luna" 1ms 3 KB key=value otel=otel`);
|
||||
|
||||
expect(getOtelAttributesField(log, true)).toEqual('vcs_ref_head_name=main field=value');
|
||||
});
|
||||
|
||||
test('Correctly matches excluded labels', () => {
|
||||
const log = createLogLine({
|
||||
labels: {
|
||||
aws_something: 'nope',
|
||||
k8s_something: 'nope',
|
||||
cluster: 'nope',
|
||||
namespace: 'nope',
|
||||
pod: 'nope',
|
||||
cluster_1: 'yes',
|
||||
namespace_2: 'yes',
|
||||
pod_3: 'yes',
|
||||
vcs_ref_head_name: 'main',
|
||||
field: 'value',
|
||||
},
|
||||
entry: `place="luna" 1ms 3 KB`,
|
||||
});
|
||||
|
||||
expect(getOtelAttributesField(log, true)).toEqual(
|
||||
'cluster_1=yes namespace_2=yes pod_3=yes vcs_ref_head_name=main field=value'
|
||||
);
|
||||
});
|
||||
|
||||
test('Removes new lines when wrapping is disabled', () => {
|
||||
const log = createLogLine({
|
||||
labels: {
|
||||
aws_something: 'nope',
|
||||
k8s_something: 'nope',
|
||||
cluster: 'nope',
|
||||
namespace: 'nope',
|
||||
pod: 'nope',
|
||||
vcs_ref_head_name: 'ma\nin',
|
||||
field: 'val\nue',
|
||||
},
|
||||
entry: `place="luna" 1ms 3 KB`,
|
||||
});
|
||||
|
||||
expect(getOtelAttributesField(log, false)).toEqual('vcs_ref_head_name=main field=value');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
import { LogRowModel } from '@grafana/data';
|
||||
|
||||
import { LOG_LINE_BODY_FIELD_NAME } from '../LogDetailsBody';
|
||||
import { LogListModel } from '../panel/processing';
|
||||
import { LogListModel, NEWLINES_REGEX } from '../panel/processing';
|
||||
|
||||
/**
|
||||
* The presence of this field along log fields determines OTel origin.
|
||||
*/
|
||||
export const OTEL_PROBE_FIELD = 'severity_number';
|
||||
const OTEL_LANGUAGE_UNKNOWN = 'unknown';
|
||||
export function identifyOTelLanguages(logs: LogListModel[] | LogRowModel[]): string[] {
|
||||
|
||||
function identifyOTelLanguages(logs: LogListModel[] | LogRowModel[]): string[] {
|
||||
const languagesSet = new Set<string>();
|
||||
logs.forEach((log) => {
|
||||
const lang = identifyOTelLanguage(log);
|
||||
|
@ -28,7 +29,7 @@ export function identifyOTelLanguage(log: LogListModel | LogRowModel): string |
|
|||
: undefined;
|
||||
}
|
||||
|
||||
export function getDisplayedFieldsForLanguages(logs: LogListModel[] | LogRowModel[], languages: string[]) {
|
||||
function getDisplayedFieldsForLanguages(logs: LogListModel[] | LogRowModel[], languages: string[]) {
|
||||
const displayedFields: string[] = [];
|
||||
|
||||
languages.forEach((language) => {
|
||||
|
@ -41,7 +42,10 @@ export function getDisplayedFieldsForLanguages(logs: LogListModel[] | LogRowMode
|
|||
});
|
||||
|
||||
return displayedFields.filter(
|
||||
(field) => field === LOG_LINE_BODY_FIELD_NAME || logs.some((log) => log.labels[field] !== undefined)
|
||||
(field) =>
|
||||
field === LOG_LINE_BODY_FIELD_NAME ||
|
||||
field === OTEL_LOG_LINE_ATTRIBUTES_FIELD_NAME ||
|
||||
logs.some((log) => log.labels[field] !== undefined)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -55,24 +59,34 @@ export function getDisplayFormatForLanguage(language: string) {
|
|||
}
|
||||
|
||||
export function getDefaultOTelDisplayFormat() {
|
||||
return ['scope_name', 'thread_name', 'exception_type', 'exception_message', LOG_LINE_BODY_FIELD_NAME];
|
||||
return [
|
||||
'thread_name',
|
||||
'exception_type',
|
||||
'exception_message',
|
||||
LOG_LINE_BODY_FIELD_NAME,
|
||||
OTEL_LOG_LINE_ATTRIBUTES_FIELD_NAME,
|
||||
];
|
||||
}
|
||||
|
||||
const OTEL_RESOURCE_ATTRS_REGEX =
|
||||
/^(aws_|cloud_|cloudfoundry_|container_|deployment_|faas_|gcp_|host_|k8s_|os_|process_|service_|telemetry_)/;
|
||||
/^(aws_|cloud_|cloudfoundry_|container_|deployment_|faas_|gcp_|host_|k8s_|os_|process_|service_|telemetry_|cluster$|namespace$|pod$)/;
|
||||
const OTEL_LOG_FIELDS_REGEX =
|
||||
/^(flags|observed_timestamp|scope_name|severity_number|severity_text|span_id|trace_id|detected_level)$/;
|
||||
/^(flags|observed_timestamp|severity_number|severity_text|span_id|trace_id|detected_level)$/;
|
||||
|
||||
export function getOtelFormattedBody(log: LogListModel) {
|
||||
if (!log.otelLanguage) {
|
||||
return log.raw;
|
||||
}
|
||||
export const OTEL_LOG_LINE_ATTRIBUTES_FIELD_NAME = '___OTEL_LOG_ATTRIBUTES___';
|
||||
|
||||
export function getOtelAttributesField(log: LogListModel, wrapLogMessage: boolean) {
|
||||
const additionalFields = Object.keys(log.labels).filter(
|
||||
(label) => !OTEL_RESOURCE_ATTRS_REGEX.test(label) && !OTEL_LOG_FIELDS_REGEX.test(label)
|
||||
);
|
||||
return (
|
||||
log.raw +
|
||||
' ' +
|
||||
additionalFields.map((field) => (log.labels[field] ? `${field}=${log.labels[field]}` : '')).join(' ')
|
||||
(label) =>
|
||||
!OTEL_RESOURCE_ATTRS_REGEX.test(label) &&
|
||||
!OTEL_LOG_FIELDS_REGEX.test(label) &&
|
||||
label !== OTEL_LOG_LINE_ATTRIBUTES_FIELD_NAME
|
||||
);
|
||||
const attributes = additionalFields
|
||||
.map((field) => (log.labels[field] ? `${field}=${log.labels[field]}` : ''))
|
||||
.join(' ');
|
||||
if (!wrapLogMessage) {
|
||||
return attributes.replace(NEWLINES_REGEX, '');
|
||||
}
|
||||
return attributes;
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ describe('HighlightedLogRenderer', () => {
|
|||
}
|
||||
);
|
||||
|
||||
const { container } = render(<HighlightedLogRenderer log={log} />);
|
||||
const { container } = render(<HighlightedLogRenderer tokens={log.highlightedBodyTokens} />);
|
||||
|
||||
expect(container.innerHTML).toEqual(log.highlightedBody);
|
||||
});
|
||||
|
@ -177,7 +177,7 @@ describe('HighlightedLogRenderer', () => {
|
|||
}
|
||||
);
|
||||
|
||||
const { container } = render(<HighlightedLogRenderer log={log} />);
|
||||
const { container } = render(<HighlightedLogRenderer tokens={log.highlightedBodyTokens} />);
|
||||
|
||||
expect(container.innerHTML).toEqual(log.highlightedBody);
|
||||
});
|
||||
|
@ -201,7 +201,7 @@ describe('HighlightedLogRenderer', () => {
|
|||
}
|
||||
);
|
||||
|
||||
const { container } = render(<HighlightedLogRenderer log={log} />);
|
||||
const { container } = render(<HighlightedLogRenderer tokens={log.highlightedBodyTokens} />);
|
||||
|
||||
expect(container.innerHTML).toEqual(log.highlightedBody);
|
||||
});
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
import { Token } from 'prismjs';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { LogListModel } from './processing';
|
||||
|
||||
export const HighlightedLogRenderer = ({ log }: { log: LogListModel }) => {
|
||||
export const HighlightedLogRenderer = memo(({ tokens }: { tokens: Array<string | Token> }) => {
|
||||
return (
|
||||
<>
|
||||
{log.highlightedBodyTokens.map((token, i) => (
|
||||
{tokens.map((token, i) => (
|
||||
<LogToken token={token} key={i} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
HighlightedLogRenderer.displayName = 'HighlightedLogRenderer';
|
||||
|
||||
const LogToken = ({ token }: { token: Token | string }) => {
|
||||
const LogToken = memo(({ token }: { token: Token | string }) => {
|
||||
if (typeof token === 'string') {
|
||||
return token;
|
||||
}
|
||||
|
@ -30,4 +30,5 @@ const LogToken = ({ token }: { token: Token | string }) => {
|
|||
{typeof token.content === 'string' ? token.content : <LogToken token={token.content} />}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
});
|
||||
LogToken.displayName = 'LogToken';
|
||||
|
|
|
@ -2,9 +2,11 @@ import { render, screen } from '@testing-library/react';
|
|||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { CoreApp, createTheme, getDefaultTimeRange, LogsDedupStrategy, LogsSortOrder } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
import { LOG_LINE_BODY_FIELD_NAME } from '../LogDetailsBody';
|
||||
import { createLogLine } from '../mocks/logRow';
|
||||
import { getDisplayedFieldsForLogs, OTEL_PROBE_FIELD } from '../otel/formats';
|
||||
|
||||
import { getGridTemplateColumns, getStyles, LogLine, Props } from './LogLine';
|
||||
import { LogListFontSize } from './LogList';
|
||||
|
@ -270,6 +272,55 @@ describe.each(fontSizes)('LogLine', (fontSize: LogListFontSize) => {
|
|||
expect(screen.getByTestId('ansiLogLine')).toBeInTheDocument();
|
||||
expect(screen.queryByText(log.entry)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Highlights the OTel attributes field when rendered', () => {
|
||||
const originalState = config.featureToggles.otelLogsFormatting;
|
||||
config.featureToggles.otelLogsFormatting = true;
|
||||
log = createLogLine({
|
||||
labels: { [OTEL_PROBE_FIELD]: '1', service: 'some service' },
|
||||
entry: `place="luna" 1ms 3 KB`,
|
||||
});
|
||||
const displayedFields = getDisplayedFieldsForLogs([log]);
|
||||
|
||||
render(
|
||||
<LogListContextProvider {...contextProps} displayedFields={displayedFields}>
|
||||
<LogLine {...defaultProps} displayedFields={displayedFields} log={log} />
|
||||
</LogListContextProvider>
|
||||
);
|
||||
expect(screen.getByText('service=')).toBeInTheDocument();
|
||||
expect(screen.getByText('some service')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('place')).toBeInTheDocument();
|
||||
expect(screen.getByText('1ms')).toBeInTheDocument();
|
||||
expect(screen.getByText('3 KB')).toBeInTheDocument();
|
||||
expect(screen.queryByText(`place="luna" 1ms 3 KB`)).not.toBeInTheDocument();
|
||||
|
||||
config.featureToggles.otelLogsFormatting = originalState;
|
||||
});
|
||||
|
||||
test('OTel attributes field is not present when the flag is disabled', () => {
|
||||
const originalState = config.featureToggles.otelLogsFormatting;
|
||||
config.featureToggles.otelLogsFormatting = false;
|
||||
log = createLogLine({
|
||||
labels: { [OTEL_PROBE_FIELD]: '1', service: 'some service' },
|
||||
entry: `place="luna" 1ms 3 KB`,
|
||||
});
|
||||
|
||||
render(
|
||||
<LogListContextProvider {...contextProps}>
|
||||
<LogLine {...defaultProps} log={log} />
|
||||
</LogListContextProvider>
|
||||
);
|
||||
expect(screen.queryByText('service')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('some service')).not.toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('place')).toBeInTheDocument();
|
||||
expect(screen.getByText('1ms')).toBeInTheDocument();
|
||||
expect(screen.getByText('3 KB')).toBeInTheDocument();
|
||||
expect(screen.queryByText(`place="luna" 1ms 3 KB`)).not.toBeInTheDocument();
|
||||
|
||||
config.featureToggles.otelLogsFormatting = originalState;
|
||||
});
|
||||
});
|
||||
|
||||
describe('Collapsible log lines', () => {
|
||||
|
|
|
@ -20,13 +20,14 @@ import { Button, Icon, Tooltip } from '@grafana/ui';
|
|||
import { LOG_LINE_BODY_FIELD_NAME } from '../LogDetailsBody';
|
||||
import { LogLabels } from '../LogLabels';
|
||||
import { LogMessageAnsi } from '../LogMessageAnsi';
|
||||
import { OTEL_LOG_LINE_ATTRIBUTES_FIELD_NAME } from '../otel/formats';
|
||||
|
||||
import { HighlightedLogRenderer } from './HighlightedLogRenderer';
|
||||
import { InlineLogLineDetails } from './LogLineDetails';
|
||||
import { LogLineMenu } from './LogLineMenu';
|
||||
import { useLogIsPermalinked, useLogIsPinned, useLogListContext } from './LogListContext';
|
||||
import { useLogListSearchContext } from './LogListSearchContext';
|
||||
import { LogListModel } from './processing';
|
||||
import { getNormalizedFieldName, LogListModel } from './processing';
|
||||
import {
|
||||
FIELD_GAP_MULTIPLIER,
|
||||
getLogLineDOMHeight,
|
||||
|
@ -374,6 +375,7 @@ const DisplayedFields = ({
|
|||
styles: LogLineStyles;
|
||||
}) => {
|
||||
const { matchingUids, search } = useLogListSearchContext();
|
||||
const { syntaxHighlighting } = useLogListContext();
|
||||
|
||||
const searchWords = useMemo(() => {
|
||||
const searchWords = log.searchWords && log.searchWords[0] ? log.searchWords.slice() : [];
|
||||
|
@ -386,11 +388,19 @@ const DisplayedFields = ({
|
|||
return searchWords;
|
||||
}, [log.searchWords, log.uid, matchingUids, search]);
|
||||
|
||||
return displayedFields.map((field) =>
|
||||
field === LOG_LINE_BODY_FIELD_NAME ? (
|
||||
<LogLineBody log={log} key={field} styles={styles} />
|
||||
) : (
|
||||
<span className="field" title={field} key={field}>
|
||||
return displayedFields.map((field) => {
|
||||
if (field === LOG_LINE_BODY_FIELD_NAME) {
|
||||
return <LogLineBody log={log} key={field} styles={styles} />;
|
||||
}
|
||||
if (field === OTEL_LOG_LINE_ATTRIBUTES_FIELD_NAME && syntaxHighlighting) {
|
||||
return (
|
||||
<span className="field log-syntax-highlight" title={getNormalizedFieldName(field)} key={field}>
|
||||
<HighlightedLogRenderer tokens={log.highlightedLogAttributesTokens} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span className="field" title={getNormalizedFieldName(field)} key={field}>
|
||||
{searchWords ? (
|
||||
<Highlighter
|
||||
textToHighlight={log.getDisplayedFieldValue(field)}
|
||||
|
@ -402,8 +412,8 @@ const DisplayedFields = ({
|
|||
log.getDisplayedFieldValue(field)
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
);
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const LogLineBody = ({ log, styles }: { log: LogListModel; styles: LogLineStyles }) => {
|
||||
|
@ -444,7 +454,7 @@ const LogLineBody = ({ log, styles }: { log: LogListModel; styles: LogLineStyles
|
|||
|
||||
return (
|
||||
<span className="field log-syntax-highlight">
|
||||
<HighlightedLogRenderer log={log} />
|
||||
<HighlightedLogRenderer tokens={log.highlightedBodyTokens} />
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -6,11 +6,10 @@ import { GrafanaTheme2 } from '@grafana/data';
|
|||
import { t } from '@grafana/i18n';
|
||||
import { Card, IconButton, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { LOG_LINE_BODY_FIELD_NAME } from '../LogDetailsBody';
|
||||
|
||||
import { LogLineDetailsMode } from './LogLineDetails';
|
||||
import { useLogListContext } from './LogListContext';
|
||||
import { reportInteractionOnce } from './analytics';
|
||||
import { getNormalizedFieldName } from './processing';
|
||||
|
||||
export const LogLineDetailsDisplayedFields = () => {
|
||||
const { displayedFields, setDisplayedFields } = useLogListContext();
|
||||
|
@ -98,9 +97,7 @@ const DisplayedField = ({
|
|||
<div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
|
||||
<Card noMargin className={styles.fieldCard}>
|
||||
<div className={styles.fieldWrapper}>
|
||||
<div className={styles.field}>
|
||||
{field === LOG_LINE_BODY_FIELD_NAME ? t('logs.log-line-details.log-line-field', 'Log line') : field}
|
||||
</div>
|
||||
<div className={styles.field}>{getNormalizedFieldName(field)}</div>
|
||||
{displayedFields.length > 1 && (
|
||||
<>
|
||||
<IconButton
|
||||
|
|
|
@ -12,9 +12,10 @@ import { logRowToSingleRowDataFrame } from '../../logsModel';
|
|||
import { calculateLogsLabelStats, calculateStats } from '../../utils';
|
||||
import { LogLabelStats } from '../LogLabelStats';
|
||||
import { FieldDef } from '../logParser';
|
||||
import { OTEL_LOG_LINE_ATTRIBUTES_FIELD_NAME } from '../otel/formats';
|
||||
|
||||
import { useLogListContext } from './LogListContext';
|
||||
import { LogListModel } from './processing';
|
||||
import { LogListModel, getNormalizedFieldName } from './processing';
|
||||
|
||||
interface LogLineDetailsFieldsProps {
|
||||
disableActions?: boolean;
|
||||
|
@ -258,12 +259,14 @@ export const LogLineDetailsField = ({
|
|||
const singleKey = keys.length === 1;
|
||||
const singleValue = values.length === 1;
|
||||
|
||||
const fieldSupportsFilters = keys[0] !== OTEL_LOG_LINE_ATTRIBUTES_FIELD_NAME;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.row}>
|
||||
{!disableActions && (
|
||||
<div className={styles.actions}>
|
||||
{onClickFilterLabel && (
|
||||
{onClickFilterLabel && fieldSupportsFilters && (
|
||||
<AsyncIconButton
|
||||
name="search-plus"
|
||||
onClick={filterLabel}
|
||||
|
@ -272,7 +275,7 @@ export const LogLineDetailsField = ({
|
|||
tooltipSuffix={refIdTooltip}
|
||||
/>
|
||||
)}
|
||||
{onClickFilterOutLabel && (
|
||||
{onClickFilterOutLabel && fieldSupportsFilters && (
|
||||
<IconButton
|
||||
name="search-minus"
|
||||
tooltip={
|
||||
|
@ -313,7 +316,9 @@ export const LogLineDetailsField = ({
|
|||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.label}>{singleKey ? keys[0] : <MultipleValue values={keys} />}</div>
|
||||
<div className={styles.label}>
|
||||
{singleKey ? getNormalizedFieldName(keys[0]) : <MultipleValue values={keys} />}
|
||||
</div>
|
||||
<div className={styles.value}>
|
||||
<div className={styles.valueContainer}>
|
||||
{singleValue ? (
|
||||
|
|
|
@ -33,7 +33,9 @@ export const LogLineDetailsLog = memo(({ log: originalLog, syntaxHighlighting }:
|
|||
<>
|
||||
{!syntaxHighlighting && <div className="field no-highlighting">{log.body}</div>}
|
||||
{syntaxHighlighting && (
|
||||
<div className="field log-syntax-highlight">{<HighlightedLogRenderer log={log} />}</div>
|
||||
<div className="field log-syntax-highlight">
|
||||
{<HighlightedLogRenderer tokens={log.highlightedBodyTokens} />}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
|
|
@ -13,7 +13,9 @@ import {
|
|||
import { config, reportInteraction } from '@grafana/runtime';
|
||||
|
||||
import { disablePopoverMenu, enablePopoverMenu, isPopoverMenuDisabled } from '../../utils';
|
||||
import { LOG_LINE_BODY_FIELD_NAME } from '../LogDetailsBody';
|
||||
import { createLogRow } from '../mocks/logRow';
|
||||
import { OTEL_LOG_LINE_ATTRIBUTES_FIELD_NAME, OTEL_PROBE_FIELD } from '../otel/formats';
|
||||
|
||||
import { LogList, Props } from './LogList';
|
||||
|
||||
|
@ -223,6 +225,67 @@ describe('LogList', () => {
|
|||
expect(screen.getByText('debug')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('OTel log lines', () => {
|
||||
const originalState = config.featureToggles.otelLogsFormatting;
|
||||
|
||||
test('Does not perform OTel-related actions when the flag is disabled', () => {
|
||||
config.featureToggles.otelLogsFormatting = false;
|
||||
const onLogOptionsChange = jest.fn();
|
||||
const setDisplayedFields = jest.fn();
|
||||
|
||||
render(
|
||||
<LogList {...defaultProps} onLogOptionsChange={onLogOptionsChange} setDisplayedFields={setDisplayedFields} />
|
||||
);
|
||||
expect(screen.getByText('log message 1')).toBeInTheDocument();
|
||||
expect(onLogOptionsChange).not.toHaveBeenCalled();
|
||||
expect(setDisplayedFields).not.toHaveBeenCalled();
|
||||
|
||||
config.featureToggles.otelLogsFormatting = originalState;
|
||||
});
|
||||
|
||||
test('Reports the default displayed fields for non-OTel logs', () => {
|
||||
config.featureToggles.otelLogsFormatting = true;
|
||||
const onLogOptionsChange = jest.fn();
|
||||
const setDisplayedFields = jest.fn();
|
||||
|
||||
render(
|
||||
<LogList {...defaultProps} onLogOptionsChange={onLogOptionsChange} setDisplayedFields={setDisplayedFields} />
|
||||
);
|
||||
expect(screen.getByText('log message 1')).toBeInTheDocument();
|
||||
expect(onLogOptionsChange).toHaveBeenCalledWith('defaultDisplayedFields', []);
|
||||
|
||||
// No fields to display, no call
|
||||
expect(setDisplayedFields).not.toHaveBeenCalled();
|
||||
|
||||
config.featureToggles.otelLogsFormatting = originalState;
|
||||
});
|
||||
|
||||
test('Reports the default OTel displayed fields', () => {
|
||||
config.featureToggles.otelLogsFormatting = true;
|
||||
const onLogOptionsChange = jest.fn();
|
||||
const setDisplayedFields = jest.fn();
|
||||
|
||||
const logs = [createLogRow({ uid: '1', labels: { [OTEL_PROBE_FIELD]: '1' } })];
|
||||
|
||||
render(
|
||||
<LogList
|
||||
{...defaultProps}
|
||||
logs={logs}
|
||||
onLogOptionsChange={onLogOptionsChange}
|
||||
setDisplayedFields={setDisplayedFields}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText('log message 1')).toBeInTheDocument();
|
||||
expect(onLogOptionsChange).toHaveBeenCalledWith('defaultDisplayedFields', [
|
||||
LOG_LINE_BODY_FIELD_NAME,
|
||||
OTEL_LOG_LINE_ATTRIBUTES_FIELD_NAME,
|
||||
]);
|
||||
expect(setDisplayedFields).toHaveBeenCalledWith([LOG_LINE_BODY_FIELD_NAME, OTEL_LOG_LINE_ATTRIBUTES_FIELD_NAME]);
|
||||
|
||||
config.featureToggles.otelLogsFormatting = originalState;
|
||||
});
|
||||
});
|
||||
|
||||
describe('Popover menu', () => {
|
||||
function setup(overrides: Partial<Props> = {}) {
|
||||
return render(
|
||||
|
|
|
@ -66,7 +66,7 @@ export interface Props {
|
|||
onClickFilterOutString?: (value: string, refId?: string) => void;
|
||||
onClickShowField?: (key: string) => void;
|
||||
onClickHideField?: (key: string) => void;
|
||||
onLogOptionsChange?: (option: LogListControlOptions, value: string | boolean | string[]) => void;
|
||||
onLogOptionsChange?: (option: LogListOptions, value: string | boolean | string[]) => void;
|
||||
onLogLineHover?: (row?: LogRowModel) => void;
|
||||
onPermalinkClick?: (row: LogRowModel) => Promise<void>;
|
||||
onPinLine?: (row: LogRowModel) => void;
|
||||
|
@ -78,6 +78,11 @@ export interface Props {
|
|||
prettifyJSON?: boolean;
|
||||
setDisplayedFields?: (displayedFields: string[]) => void;
|
||||
showControls: boolean;
|
||||
/**
|
||||
* Experimental. When OTel logs are displayed, add an extra displayed field with relevant key-value pairs from labels and metadata
|
||||
* @alpha
|
||||
*/
|
||||
showLogAttributes?: boolean;
|
||||
showTime: boolean;
|
||||
showUniqueLabels?: boolean;
|
||||
sortOrder: LogsSortOrder;
|
||||
|
@ -90,7 +95,7 @@ export interface Props {
|
|||
|
||||
export type LogListFontSize = 'default' | 'small';
|
||||
|
||||
export type LogListControlOptions = keyof LogListState | 'wrapLogMessage' | 'prettifyLogMessage';
|
||||
export type LogListOptions = keyof LogListState | 'wrapLogMessage' | 'prettifyLogMessage' | 'defaultDisplayedFields';
|
||||
|
||||
type LogListComponentProps = Omit<
|
||||
Props,
|
||||
|
@ -148,6 +153,7 @@ export const LogList = ({
|
|||
prettifyJSON = logOptionsStorageKey ? store.getBool(`${logOptionsStorageKey}.prettifyLogMessage`, true) : true,
|
||||
setDisplayedFields,
|
||||
showControls,
|
||||
showLogAttributes,
|
||||
showTime,
|
||||
showUniqueLabels,
|
||||
sortOrder,
|
||||
|
@ -193,6 +199,7 @@ export const LogList = ({
|
|||
prettifyJSON={prettifyJSON}
|
||||
setDisplayedFields={setDisplayedFields}
|
||||
showControls={showControls}
|
||||
showLogAttributes={showLogAttributes}
|
||||
showTime={showTime}
|
||||
showUniqueLabels={showUniqueLabels}
|
||||
sortOrder={sortOrder}
|
||||
|
|
|
@ -33,7 +33,7 @@ import { getDisplayedFieldsForLogs } from '../otel/formats';
|
|||
import { LogLineTimestampResolution } from './LogLine';
|
||||
import { LogLineDetailsMode } from './LogLineDetails';
|
||||
import { GetRowContextQueryFn, LogLineMenuCustomItem } from './LogLineMenu';
|
||||
import { LogListControlOptions, LogListFontSize } from './LogList';
|
||||
import { LogListOptions, LogListFontSize } from './LogList';
|
||||
import { reportInteractionOnce } from './analytics';
|
||||
import { LogListModel } from './processing';
|
||||
import { getScrollbarWidth, LOG_LIST_CONTROLS_WIDTH, LOG_LIST_MIN_WIDTH } from './virtualization';
|
||||
|
@ -176,7 +176,7 @@ export interface Props {
|
|||
onClickFilterOutString?: (value: string, refId?: string) => void;
|
||||
onClickShowField?: (key: string) => void;
|
||||
onClickHideField?: (key: string) => void;
|
||||
onLogOptionsChange?: (option: LogListControlOptions, value: string | boolean | string[]) => void;
|
||||
onLogOptionsChange?: (option: LogListOptions, value: string | boolean | string[]) => void;
|
||||
onLogLineHover?: (row?: LogRowModel) => void;
|
||||
onPermalinkClick?: (row: LogRowModel) => Promise<void>;
|
||||
onPinLine?: (row: LogRowModel) => void;
|
||||
|
@ -188,6 +188,7 @@ export interface Props {
|
|||
prettifyJSON?: boolean;
|
||||
setDisplayedFields?: (displayedFields: string[]) => void;
|
||||
showControls: boolean;
|
||||
showLogAttributes?: boolean;
|
||||
showUniqueLabels?: boolean;
|
||||
showTime: boolean;
|
||||
sortOrder: LogsSortOrder;
|
||||
|
@ -234,6 +235,7 @@ export const LogListContextProvider = ({
|
|||
prettifyJSON: prettifyJSONProp,
|
||||
setDisplayedFields,
|
||||
showControls,
|
||||
showLogAttributes,
|
||||
showTime,
|
||||
showUniqueLabels,
|
||||
sortOrder,
|
||||
|
@ -289,16 +291,28 @@ export const LogListContextProvider = ({
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const otelDisplayedFields = useMemo(() => {
|
||||
if (!config.featureToggles.otelLogsFormatting || !setDisplayedFields || showLogAttributes === false) {
|
||||
return [];
|
||||
}
|
||||
return getDisplayedFieldsForLogs(logs);
|
||||
}, [logs, setDisplayedFields, showLogAttributes]);
|
||||
|
||||
// OTel displayed fields
|
||||
useEffect(() => {
|
||||
if (displayedFields.length > 0 || !config.featureToggles.otelLogsFormatting || !setDisplayedFields) {
|
||||
if (config.featureToggles.otelLogsFormatting && showLogAttributes !== false) {
|
||||
onLogOptionsChange?.('defaultDisplayedFields', otelDisplayedFields);
|
||||
}
|
||||
}, [onLogOptionsChange, otelDisplayedFields, showLogAttributes]);
|
||||
|
||||
useEffect(() => {
|
||||
if (displayedFields.length > 0 || !setDisplayedFields) {
|
||||
return;
|
||||
}
|
||||
const otelDisplayedFields = getDisplayedFieldsForLogs(logs);
|
||||
if (otelDisplayedFields.length) {
|
||||
setDisplayedFields(otelDisplayedFields);
|
||||
}
|
||||
}, [displayedFields.length, logs, setDisplayedFields]);
|
||||
}, [displayedFields.length, otelDisplayedFields, setDisplayedFields]);
|
||||
|
||||
// Sync state
|
||||
useEffect(() => {
|
||||
|
@ -404,6 +418,13 @@ export const LogListContextProvider = ({
|
|||
}));
|
||||
}, [timestampResolution]);
|
||||
|
||||
// Sync showLogAttributes
|
||||
useEffect(() => {
|
||||
if (showLogAttributes === false && setDisplayedFields) {
|
||||
setDisplayedFields([]);
|
||||
}
|
||||
}, [setDisplayedFields, showLogAttributes]);
|
||||
|
||||
const controlsExpandedFromStore = store.getBool(
|
||||
`${logOptionsStorageKey}.controlsExpanded`,
|
||||
getDefaultControlsExpandedMode(containerElement ?? null)
|
||||
|
|
|
@ -3,6 +3,7 @@ import { config } from '@grafana/runtime';
|
|||
|
||||
import { LOG_LINE_BODY_FIELD_NAME } from '../LogDetailsBody';
|
||||
import { createLogLine, createLogRow } from '../mocks/logRow';
|
||||
import { OTEL_LOG_LINE_ATTRIBUTES_FIELD_NAME, OTEL_PROBE_FIELD } from '../otel/formats';
|
||||
|
||||
import { LogListFontSize } from './LogList';
|
||||
import { LogListModel, preProcessLogs } from './processing';
|
||||
|
@ -237,6 +238,67 @@ describe('preProcessLogs', () => {
|
|||
expect(logListModel.body).toBeDefined(); // Triggers parsing
|
||||
expect(logListModel.isJSON).toBe(false);
|
||||
});
|
||||
|
||||
describe('OTel logs', () => {
|
||||
const originalState = config.featureToggles.otelLogsFormatting;
|
||||
|
||||
test('Does not create the OTel attribute field when not enabled', () => {
|
||||
config.featureToggles.otelLogsFormatting = false;
|
||||
|
||||
const logListModel = createLogLine(
|
||||
{ entry: 'the log' },
|
||||
{
|
||||
escape: false,
|
||||
order: LogsSortOrder.Descending,
|
||||
timeZone: 'browser',
|
||||
wrapLogMessage: true, // wrapped
|
||||
prettifyJSON: true,
|
||||
}
|
||||
);
|
||||
expect(logListModel.labels[OTEL_LOG_LINE_ATTRIBUTES_FIELD_NAME]).toBeUndefined();
|
||||
expect(logListModel.highlightedLogAttributesTokens).toHaveLength(0);
|
||||
|
||||
config.featureToggles.otelLogsFormatting = originalState;
|
||||
});
|
||||
|
||||
test('Does not create the OTel attribute field when is not an OTel log', () => {
|
||||
config.featureToggles.otelLogsFormatting = false;
|
||||
|
||||
const logListModel = createLogLine(
|
||||
{ entry: 'the log', labels: {} },
|
||||
{
|
||||
escape: false,
|
||||
order: LogsSortOrder.Descending,
|
||||
timeZone: 'browser',
|
||||
wrapLogMessage: true, // wrapped
|
||||
prettifyJSON: true,
|
||||
}
|
||||
);
|
||||
expect(logListModel.labels[OTEL_LOG_LINE_ATTRIBUTES_FIELD_NAME]).toBeUndefined();
|
||||
expect(logListModel.highlightedLogAttributesTokens).toHaveLength(0);
|
||||
|
||||
config.featureToggles.otelLogsFormatting = originalState;
|
||||
});
|
||||
|
||||
test('Generates and highlights an OTel log line attributes field', () => {
|
||||
config.featureToggles.otelLogsFormatting = true;
|
||||
|
||||
const logListModel = createLogLine(
|
||||
{ entry: 'the log', labels: { [OTEL_PROBE_FIELD]: '1', field: 'value' } },
|
||||
{
|
||||
escape: false,
|
||||
order: LogsSortOrder.Descending,
|
||||
timeZone: 'browser',
|
||||
wrapLogMessage: true, // wrapped
|
||||
prettifyJSON: true,
|
||||
}
|
||||
);
|
||||
expect(logListModel.labels[OTEL_LOG_LINE_ATTRIBUTES_FIELD_NAME]).toEqual('field=value');
|
||||
expect(logListModel.highlightedLogAttributesTokens).toHaveLength(2);
|
||||
|
||||
config.featureToggles.otelLogsFormatting = originalState;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('Orders logs', () => {
|
||||
|
@ -440,43 +502,3 @@ describe('preProcessLogs', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('OTel logs', () => {
|
||||
let originalOtelLogsFormatting = config.featureToggles.otelLogsFormatting;
|
||||
afterAll(() => {
|
||||
config.featureToggles.otelLogsFormatting = originalOtelLogsFormatting;
|
||||
});
|
||||
|
||||
test('Requires a feature flag', () => {
|
||||
const log = createLogLine({
|
||||
labels: {
|
||||
severity_number: '1',
|
||||
telemetry_sdk_language: 'php',
|
||||
scope_name: 'scope',
|
||||
aws_ignore: 'ignored',
|
||||
key: 'value',
|
||||
otel: 'otel',
|
||||
},
|
||||
entry: `place="luna" 1ms 3 KB`,
|
||||
});
|
||||
expect(log.otelLanguage).toBeDefined();
|
||||
expect(log.body).toEqual(`place="luna" 1ms 3 KB`);
|
||||
});
|
||||
|
||||
test('Augments OTel log lines', () => {
|
||||
config.featureToggles.otelLogsFormatting = true;
|
||||
const log = createLogLine({
|
||||
labels: {
|
||||
severity_number: '1',
|
||||
telemetry_sdk_language: 'php',
|
||||
scope_name: 'scope',
|
||||
aws_ignore: 'ignored',
|
||||
key: 'value',
|
||||
otel: 'otel',
|
||||
},
|
||||
entry: `place="luna" 1ms 3 KB`,
|
||||
});
|
||||
expect(log.otelLanguage).toBeDefined();
|
||||
expect(log.body).toEqual(`place="luna" 1ms 3 KB key=value otel=otel`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,19 +11,20 @@ import {
|
|||
LogsSortOrder,
|
||||
systemDateFormats,
|
||||
} from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { GetFieldLinksFn } from 'app/plugins/panel/logs/types';
|
||||
|
||||
import { checkLogsError, checkLogsSampled, escapeUnescapedString, sortLogRows } from '../../utils';
|
||||
import { LOG_LINE_BODY_FIELD_NAME } from '../LogDetailsBody';
|
||||
import { FieldDef, getAllFields } from '../logParser';
|
||||
import { identifyOTelLanguage, getOtelFormattedBody } from '../otel/formats';
|
||||
import { identifyOTelLanguage, getOtelAttributesField, OTEL_LOG_LINE_ATTRIBUTES_FIELD_NAME } from '../otel/formats';
|
||||
|
||||
import { generateLogGrammar, generateTextMatchGrammar } from './grammar';
|
||||
import { LogLineVirtualization } from './virtualization';
|
||||
|
||||
const TRUNCATION_DEFAULT_LENGTH = 50000;
|
||||
const NEWLINES_REGEX = /(\r\n|\n|\r)/g;
|
||||
export const NEWLINES_REGEX = /(\r\n|\n|\r)/g;
|
||||
|
||||
export class LogListModel implements LogRowModel {
|
||||
collapsed: boolean | undefined = undefined;
|
||||
|
@ -59,6 +60,7 @@ export class LogListModel implements LogRowModel {
|
|||
private _currentSearch: string | undefined = undefined;
|
||||
private _grammar?: Grammar;
|
||||
private _highlightedBody: string | undefined = undefined;
|
||||
private _highlightedLogAttributesTokens: Array<string | Token> | undefined = undefined;
|
||||
private _highlightTokens: Array<string | Token> | undefined = undefined;
|
||||
private _fields: FieldDef[] | undefined = undefined;
|
||||
private _getFieldLinks: GetFieldLinksFn | undefined = undefined;
|
||||
|
@ -114,6 +116,10 @@ export class LogListModel implements LogRowModel {
|
|||
raw = escapeUnescapedString(raw);
|
||||
}
|
||||
this.raw = raw;
|
||||
|
||||
if (config.featureToggles.otelLogsFormatting && this.otelLanguage) {
|
||||
this.labels[OTEL_LOG_LINE_ATTRIBUTES_FIELD_NAME] = getOtelAttributesField(this, wrapLogMessage);
|
||||
}
|
||||
}
|
||||
|
||||
clone() {
|
||||
|
@ -137,7 +143,7 @@ export class LogListModel implements LogRowModel {
|
|||
this.raw = reStringified;
|
||||
}
|
||||
} catch (error) {}
|
||||
const raw = config.featureToggles.otelLogsFormatting && this.otelLanguage ? getOtelFormattedBody(this) : this.raw;
|
||||
const raw = this.raw;
|
||||
this._body = this.collapsed
|
||||
? raw.substring(0, this._virtualization?.getTruncationLength(null) ?? TRUNCATION_DEFAULT_LENGTH)
|
||||
: raw;
|
||||
|
@ -181,6 +187,19 @@ export class LogListModel implements LogRowModel {
|
|||
return this._highlightTokens;
|
||||
}
|
||||
|
||||
get highlightedLogAttributesTokens() {
|
||||
if (this._highlightedLogAttributesTokens === undefined) {
|
||||
const attributes = this.labels[OTEL_LOG_LINE_ATTRIBUTES_FIELD_NAME] ?? '';
|
||||
if (!attributes) {
|
||||
return [];
|
||||
}
|
||||
this._grammar = this._grammar ?? generateLogGrammar(this);
|
||||
const extraGrammar = generateTextMatchGrammar(this.searchWords, this._currentSearch);
|
||||
this._highlightedLogAttributesTokens = Prism.tokenize(attributes, { ...extraGrammar, ...this._grammar });
|
||||
}
|
||||
return this._highlightedLogAttributesTokens;
|
||||
}
|
||||
|
||||
get isJSON() {
|
||||
return this._json;
|
||||
}
|
||||
|
@ -250,6 +269,7 @@ export class LogListModel implements LogRowModel {
|
|||
setCurrentSearch(search: string | undefined) {
|
||||
this._currentSearch = search;
|
||||
this._highlightTokens = undefined;
|
||||
this._highlightedLogAttributesTokens = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -335,3 +355,12 @@ export function getLevelsFromLogs(logs: LogListModel[]) {
|
|||
}
|
||||
return Array.from(levels).filter((level) => level != null);
|
||||
}
|
||||
|
||||
export function getNormalizedFieldName(field: string) {
|
||||
if (field === LOG_LINE_BODY_FIELD_NAME) {
|
||||
return t('logs.log-line-details.log-line-field', 'Log line');
|
||||
} else if (field === OTEL_LOG_LINE_ATTRIBUTES_FIELD_NAME) {
|
||||
return t('logs.log-line-details.log-attributes-field', 'OTel attributes');
|
||||
}
|
||||
return field;
|
||||
}
|
||||
|
|
|
@ -405,6 +405,7 @@ describe('getPluginExtensions()', () => {
|
|||
expect.objectContaining({
|
||||
context,
|
||||
openModal: expect.any(Function),
|
||||
extensionPointId: extensionPoint2,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
|
|
@ -537,6 +537,7 @@ export function getLinkExtensionOnClick(
|
|||
|
||||
const helpers: PluginExtensionEventHelpers = {
|
||||
context,
|
||||
extensionPointId,
|
||||
openModal: createOpenModalFunction(config),
|
||||
openSidebar: (componentTitle, context) => {
|
||||
appEvents.publish(
|
||||
|
|
|
@ -2,7 +2,7 @@ import { DataFrame, DataFrameView, FieldType, getDisplayProcessor, SelectableVal
|
|||
import { config } from '@grafana/runtime';
|
||||
import { TermCount } from 'app/core/components/TagFilter/TagFilter';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
import { PermissionLevelString } from 'app/types/acl';
|
||||
import { PermissionLevel } from 'app/types/acl';
|
||||
|
||||
import { DEFAULT_MAX_VALUES, GENERAL_FOLDER_UID, TYPE_KIND_MAP } from '../constants';
|
||||
import { DashboardSearchHit, DashboardSearchItemType } from '../types';
|
||||
|
@ -21,7 +21,7 @@ interface APIQuery {
|
|||
folderUIDs?: string[];
|
||||
sort?: string;
|
||||
starred?: boolean;
|
||||
permission?: PermissionLevelString;
|
||||
permission?: PermissionLevel;
|
||||
deleted?: boolean;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { DataFrameView, SelectableValue } from '@grafana/data';
|
||||
import { TermCount } from 'app/core/components/TagFilter/TagFilter';
|
||||
import { PermissionLevelString } from 'app/types/acl';
|
||||
import { PermissionLevel } from 'app/types/acl';
|
||||
|
||||
import { ManagerKind } from '../../apiserver/types';
|
||||
|
||||
|
@ -39,7 +39,7 @@ export interface SearchQuery {
|
|||
limit?: number;
|
||||
from?: number;
|
||||
starred?: boolean;
|
||||
permission?: PermissionLevelString;
|
||||
permission?: PermissionLevel;
|
||||
deleted?: boolean;
|
||||
offset?: number;
|
||||
}
|
||||
|
|
|
@ -47,6 +47,7 @@ import { LogLabels } from '../../../features/logs/components/LogLabels';
|
|||
import { LogRows } from '../../../features/logs/components/LogRows';
|
||||
import { COMMON_LABELS, dataFrameToLogsModel, dedupLogRows } from '../../../features/logs/logsModel';
|
||||
|
||||
import type { Options } from './panelcfg.gen';
|
||||
import {
|
||||
GetFieldLinksFn,
|
||||
isCoreApp,
|
||||
|
@ -63,7 +64,6 @@ import {
|
|||
isReactNodeArray,
|
||||
isSetDisplayedFields,
|
||||
onNewLogsReceivedType,
|
||||
Options,
|
||||
} from './types';
|
||||
import { useDatasourcesFromTargets } from './useDatasourcesFromTargets';
|
||||
|
||||
|
@ -114,7 +114,7 @@ interface LogsPanelProps extends PanelProps<Options> {
|
|||
* controlsStorageKey?: string
|
||||
*
|
||||
* If controls are enabled, this function is called when a change is made in one of the options from the controls.
|
||||
* onLogOptionsChange?: (option: LogListControlOptions, value: string | boolean | string[]) => void;
|
||||
* onLogOptionsChange?: (option: LogListOptions, value: string | boolean | string[]) => void;
|
||||
*
|
||||
* When the feature toggle newLogsPanel is enabled, you can pass extra options to the LogLineMenu component.
|
||||
* These options are an array of items with { label, onClick } or { divider: true } for dividers.
|
||||
|
@ -128,6 +128,11 @@ interface LogsPanelProps extends PanelProps<Options> {
|
|||
*
|
||||
* When showing timestamps, toggle between showing nanoseconds or milliseconds.
|
||||
* timestampResolution?: 'ms' | 'ns'
|
||||
*
|
||||
* Experimental. When OTel logs are displayed, add an extra displayed field with relevant key-value pairs from labels and metadata.
|
||||
* Requires the `otelLogsFormatting`.
|
||||
* @alpha
|
||||
* showLogAttributes?: boolean
|
||||
*/
|
||||
}
|
||||
interface LogsPermalinkUrlState {
|
||||
|
@ -170,6 +175,7 @@ export const LogsPanel = ({
|
|||
detailsMode: detailsModeProp,
|
||||
noInteractions,
|
||||
timestampResolution,
|
||||
showLogAttributes,
|
||||
...options
|
||||
},
|
||||
height,
|
||||
|
@ -609,6 +615,7 @@ export const LogsPanel = ({
|
|||
prettifyJSON={prettifyLogMessage}
|
||||
setDisplayedFields={setDisplayedFieldsFn}
|
||||
showControls={Boolean(showControls)}
|
||||
showLogAttributes={showLogAttributes}
|
||||
showTime={showTime}
|
||||
showUniqueLabels={showLabels}
|
||||
sortOrder={sortOrder}
|
||||
|
|
|
@ -119,6 +119,19 @@ export const plugin = new PanelPlugin<Options>(LogsPanel)
|
|||
defaultValue: false,
|
||||
});
|
||||
|
||||
if (config.featureToggles.otelLogsFormatting) {
|
||||
builder.addBooleanSwitch({
|
||||
path: 'showLogAttributes',
|
||||
name: t('logs.show-log-attributes', 'Display log attributes for OTel logs'),
|
||||
category,
|
||||
description: t(
|
||||
'logs.description-show-log-attributes',
|
||||
'Experimental. When OTel logs are displayed, add an extra displayed field with relevant key-value pairs from labels and metadata.'
|
||||
),
|
||||
defaultValue: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (config.featureToggles.newLogsPanel) {
|
||||
builder
|
||||
.addBooleanSwitch({
|
||||
|
|
|
@ -40,6 +40,7 @@ composableKinds: PanelCfg: {
|
|||
dedupStrategy: common.LogsDedupStrategy
|
||||
enableInfiniteScrolling?: bool
|
||||
noInteractions?: bool
|
||||
showLogAttributes?: bool
|
||||
fontSize?: "default" | "small" @cuetsy(kind="enum", memberNames="default|small")
|
||||
detailsMode?: "inline" | "sidebar" @cuetsy(kind="enum", memberNames="inline|sidebar")
|
||||
timestampResolution?: "ms" | "ns" @cuetsy(kind="enum", memberNames="ms|ns")
|
||||
|
|
|
@ -39,6 +39,7 @@ export interface Options {
|
|||
showCommonLabels: boolean;
|
||||
showControls?: boolean;
|
||||
showLabels: boolean;
|
||||
showLogAttributes?: boolean;
|
||||
showLogContextToggle: boolean;
|
||||
showTime: boolean;
|
||||
sortOrder: common.LogsSortOrder;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { VisualizationSuggestionsBuilder, VisualizationSuggestionScore } from '@grafana/data';
|
||||
import { SuggestionName } from 'app/types/suggestions';
|
||||
|
||||
import { Options } from './types';
|
||||
import { Options } from './panelcfg.gen';
|
||||
|
||||
export class LogsPanelSuggestionsSupplier {
|
||||
getSuggestionsForData(builder: VisualizationSuggestionsBuilder) {
|
||||
|
|
|
@ -2,9 +2,7 @@ import React, { ReactNode } from 'react';
|
|||
|
||||
import { CoreApp, DataFrame, Field, LinkModel, ScopedVars } from '@grafana/data';
|
||||
import { LogLineMenuCustomItem } from 'app/features/logs/components/panel/LogLineMenu';
|
||||
import { LogListControlOptions } from 'app/features/logs/components/panel/LogList';
|
||||
|
||||
export type { Options } from './panelcfg.gen';
|
||||
import { LogListOptions } from 'app/features/logs/components/panel/LogList';
|
||||
|
||||
type onClickFilterLabelType = (key: string, value: string, frame?: DataFrame) => void;
|
||||
type onClickFilterOutLabelType = (key: string, value: string, frame?: DataFrame) => void;
|
||||
|
@ -14,7 +12,7 @@ type filterLabelActiveType = (key: string, value: string, refId?: string) => Pro
|
|||
type onClickShowFieldType = (value: string) => void;
|
||||
type onClickHideFieldType = (value: string) => void;
|
||||
export type onNewLogsReceivedType = (allLogs: DataFrame[], newLogs: DataFrame[]) => void;
|
||||
type onLogOptionsChangeType = (option: LogListControlOptions, value: string | boolean | string[]) => void;
|
||||
type onLogOptionsChangeType = (option: LogListOptions, value: string | boolean | string[]) => void;
|
||||
type setDisplayedFieldsType = (fields: string[]) => void;
|
||||
|
||||
export type GetFieldLinksFn = (
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue