Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 774551589b | |||
| dee9bc8fb9 | |||
| 6395a753d8 | |||
| 5b228fd7fa | |||
| 307cce059c | |||
| a5d240751d | |||
| d93867479f | |||
| 9f53141368 | |||
| 0413b76461 | |||
| 417d3d914a | |||
| 2a7f698c4c | |||
| 212bdb4400 | |||
| 2432756be8 | |||
| a59df66e21 | |||
| 5bec0f1af7 | |||
| 954156d5b3 |
@@ -94,6 +94,7 @@
|
||||
/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
|
||||
/apps/sdk.mk @grafana/grafana-app-platform-squad
|
||||
|
||||
@@ -19,6 +19,7 @@ updates:
|
||||
- "/apps/dashboard"
|
||||
- "/apps/folder"
|
||||
- "/apps/iam"
|
||||
- "/apps/investigations"
|
||||
- "/apps/playlist"
|
||||
- "/apps/plugins"
|
||||
- "/apps/preferences"
|
||||
|
||||
@@ -67,6 +67,14 @@ linters:
|
||||
deny:
|
||||
- pkg: github.com/grafana/grafana/pkg
|
||||
desc: apiserver is not allowed to import grafana core
|
||||
apps-investigation:
|
||||
list-mode: lax
|
||||
files:
|
||||
- ./apps/investigations/*
|
||||
- ./apps/investigations/**/*
|
||||
deny:
|
||||
- pkg: github.com/grafana/grafana/pkg
|
||||
desc: apps/investigations is not allowed to import grafana core
|
||||
apps-playlist:
|
||||
list-mode: lax
|
||||
files:
|
||||
|
||||
@@ -103,6 +103,7 @@ COPY apps/collections apps/collections
|
||||
COPY apps/provisioning apps/provisioning
|
||||
COPY apps/secret apps/secret
|
||||
COPY apps/scope apps/scope
|
||||
COPY apps/investigations apps/investigations
|
||||
COPY apps/logsdrilldown apps/logsdrilldown
|
||||
COPY apps/advisor apps/advisor
|
||||
COPY apps/dashboard apps/dashboard
|
||||
|
||||
@@ -24,6 +24,8 @@ replace github.com/grafana/grafana/apps/alerting/historian => ../alerting/histor
|
||||
|
||||
replace github.com/grafana/grafana/apps/correlations => ../correlations
|
||||
|
||||
replace github.com/grafana/grafana/apps/investigations => ../investigations
|
||||
|
||||
replace github.com/grafana/grafana/apps/logsdrilldown => ../logsdrilldown
|
||||
|
||||
replace github.com/grafana/grafana/apps/playlist => ../playlist
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
include ../sdk.mk
|
||||
|
||||
.PHONY: generate # Run Grafana App SDK code generation
|
||||
generate: install-app-sdk update-app-sdk
|
||||
@$(APP_SDK_BIN) generate \
|
||||
--source=./kinds/ \
|
||||
--gogenpath=./pkg/apis \
|
||||
--grouping=group \
|
||||
--genoperatorstate=false \
|
||||
--defencoding=none
|
||||
@@ -0,0 +1,152 @@
|
||||
[
|
||||
{
|
||||
"id": "896312ce-65b0-4b50-ade1-e7f04fa22c66",
|
||||
"title": "Thursday morning investigation",
|
||||
"hasCustomName": false,
|
||||
"isFavorite": false,
|
||||
"collectables": [
|
||||
{
|
||||
"origin": "Explore Logs",
|
||||
"type": "timeseries",
|
||||
"queries": [
|
||||
{
|
||||
"refId": "LABEL_BREAKDOWN_VALUES",
|
||||
"queryType": "range",
|
||||
"editorMode": "code",
|
||||
"supportingQueryType": "grafana-lokiexplore-app",
|
||||
"legendFormat": "{{detected_level}}",
|
||||
"expr": "sum(count_over_time({service_name=\"web_app_1\"} | detected_level != \"\"[$__auto])) by (detected_level)"
|
||||
}
|
||||
],
|
||||
"timeRange": {
|
||||
"to": "2025-02-13T11:31:20.536Z",
|
||||
"from": "2025-02-13T11:16:20.536Z",
|
||||
"raw": {
|
||||
"from": "now-15m",
|
||||
"to": "now"
|
||||
}
|
||||
},
|
||||
"datasource": {
|
||||
"uid": "fe9k7u07b1a0wc"
|
||||
},
|
||||
"url": "http://localhost:3000/a/grafana-lokiexplore-app/explore/service/web_app_1/labels?patterns=%5B%5D&from=now-15m&to=now&var-ds=fe9k7u07b1a0wc&var-filters=service_name%7C%3D%7Cweb_app_1&var-fields=&var-levels=&var-metadata=&var-patterns=&var-lineFilterV2=&var-lineFilters=&urlColumns=%5B%5D&visualizationType=%22logs%22&displayedFields=%5B%5D&timezone=browser&var-all-fields=&var-labelBy=$__all",
|
||||
"id": "LABEL_BREAKDOWN_VALUES_detected_level",
|
||||
"title": "detected_level",
|
||||
"logoPath": "public/plugins/grafana-lokiexplore-app/img/img/logo.svg",
|
||||
"createdAt": "2025-02-13T11:31:23.637Z"
|
||||
}
|
||||
],
|
||||
"createdAt": "2025-02-13T11:31:23.636Z",
|
||||
"updatedAt": "2025-02-13T11:31:23.637Z",
|
||||
"viewMode": {
|
||||
"mode": "compact",
|
||||
"showComments": true,
|
||||
"showTooltips": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "e9cf1958-d0ed-46b7-b597-9052c7648656",
|
||||
"title": "Thursday morning investigation",
|
||||
"hasCustomName": false,
|
||||
"isFavorite": false,
|
||||
"collectables": [
|
||||
{
|
||||
"origin": "Explore Logs",
|
||||
"type": "timeseries",
|
||||
"queries": [
|
||||
{
|
||||
"refId": "LABEL_BREAKDOWN_VALUES",
|
||||
"queryType": "range",
|
||||
"editorMode": "code",
|
||||
"supportingQueryType": "grafana-lokiexplore-app",
|
||||
"legendFormat": "{{detected_level}}",
|
||||
"expr": "sum(count_over_time({service_name=\"web_app_1\"} | detected_level != \"\"[$__auto])) by (detected_level)"
|
||||
}
|
||||
],
|
||||
"timeRange": {
|
||||
"to": "2025-02-13T11:31:20.536Z",
|
||||
"from": "2025-02-13T11:16:20.536Z",
|
||||
"raw": {
|
||||
"from": "now-15m",
|
||||
"to": "now"
|
||||
}
|
||||
},
|
||||
"datasource": {
|
||||
"uid": "fe9k7u07b1a0wc"
|
||||
},
|
||||
"url": "http://localhost:3000/a/grafana-lokiexplore-app/explore/service/web_app_1/labels?patterns=%5B%5D&from=now-15m&to=now&var-ds=fe9k7u07b1a0wc&var-filters=service_name%7C%3D%7Cweb_app_1&var-fields=&var-levels=&var-metadata=&var-patterns=&var-lineFilterV2=&var-lineFilters=&urlColumns=%5B%5D&visualizationType=%22logs%22&displayedFields=%5B%5D&timezone=browser&var-all-fields=&var-labelBy=$__all",
|
||||
"id": "LABEL_BREAKDOWN_VALUES_detected_level",
|
||||
"title": "detected_level",
|
||||
"logoPath": "public/plugins/grafana-lokiexplore-app/img/img/logo.svg",
|
||||
"createdAt": "2025-02-13T11:31:23.638Z"
|
||||
},
|
||||
{
|
||||
"origin": "Explore Logs",
|
||||
"type": "timeseries",
|
||||
"queries": [
|
||||
{
|
||||
"refId": "LABEL_BREAKDOWN_VALUES",
|
||||
"queryType": "range",
|
||||
"editorMode": "code",
|
||||
"supportingQueryType": "grafana-lokiexplore-app",
|
||||
"legendFormat": "{{service_name}}",
|
||||
"expr": "sum(count_over_time({service_name=\"web_app_1\",service_name != \"\"} [$__auto])) by (service_name)"
|
||||
}
|
||||
],
|
||||
"timeRange": {
|
||||
"to": "2025-02-13T11:31:20.536Z",
|
||||
"from": "2025-02-13T11:16:20.536Z",
|
||||
"raw": {
|
||||
"from": "now-15m",
|
||||
"to": "now"
|
||||
}
|
||||
},
|
||||
"datasource": {
|
||||
"uid": "fe9k7u07b1a0wc"
|
||||
},
|
||||
"url": "http://localhost:3000/a/grafana-lokiexplore-app/explore/service/web_app_1/labels?patterns=%5B%5D&from=now-15m&to=now&var-ds=fe9k7u07b1a0wc&var-filters=service_name%7C%3D%7Cweb_app_1&var-fields=&var-levels=&var-metadata=&var-patterns=&var-lineFilterV2=&var-lineFilters=&urlColumns=%5B%5D&visualizationType=%22logs%22&displayedFields=%5B%5D&timezone=browser&var-all-fields=&var-labelBy=$__all",
|
||||
"id": "LABEL_BREAKDOWN_VALUES_service_name",
|
||||
"title": "service_name",
|
||||
"logoPath": "public/plugins/grafana-lokiexplore-app/img/img/logo.svg",
|
||||
"createdAt": "2025-02-13T11:31:41.507Z"
|
||||
},
|
||||
{
|
||||
"origin": "Explore Logs",
|
||||
"type": "timeseries",
|
||||
"queries": [
|
||||
{
|
||||
"refId": "LABEL_BREAKDOWN_VALUES",
|
||||
"queryType": "range",
|
||||
"editorMode": "code",
|
||||
"supportingQueryType": "grafana-lokiexplore-app",
|
||||
"legendFormat": "{{service}}",
|
||||
"expr": "sum(count_over_time({service_name=\"web_app_1\",service != \"\"} [$__auto])) by (service)"
|
||||
}
|
||||
],
|
||||
"timeRange": {
|
||||
"to": "2025-02-13T11:31:20.536Z",
|
||||
"from": "2025-02-13T11:16:20.536Z",
|
||||
"raw": {
|
||||
"from": "now-15m",
|
||||
"to": "now"
|
||||
}
|
||||
},
|
||||
"datasource": {
|
||||
"uid": "fe9k7u07b1a0wc"
|
||||
},
|
||||
"url": "http://localhost:3000/a/grafana-lokiexplore-app/explore/service/web_app_1/labels?patterns=%5B%5D&from=now-15m&to=now&var-ds=fe9k7u07b1a0wc&var-filters=service_name%7C%3D%7Cweb_app_1&var-fields=&var-levels=&var-metadata=&var-patterns=&var-lineFilterV2=&var-lineFilters=&urlColumns=%5B%5D&visualizationType=%22logs%22&displayedFields=%5B%5D&timezone=browser&var-all-fields=&var-labelBy=$__all",
|
||||
"id": "LABEL_BREAKDOWN_VALUES_service",
|
||||
"title": "service",
|
||||
"logoPath": "public/plugins/grafana-lokiexplore-app/img/img/logo.svg",
|
||||
"createdAt": "2025-02-13T11:31:43.698Z"
|
||||
}
|
||||
],
|
||||
"createdAt": "2025-02-13T11:31:23.637Z",
|
||||
"updatedAt": "2025-02-13T11:31:43.698Z",
|
||||
"viewMode": {
|
||||
"mode": "compact",
|
||||
"showComments": true,
|
||||
"showTooltips": false
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,102 @@
|
||||
module github.com/grafana/grafana/apps/investigations
|
||||
|
||||
go 1.25.5
|
||||
|
||||
require (
|
||||
github.com/grafana/grafana-app-sdk v0.48.7
|
||||
k8s.io/apimachinery v0.34.3
|
||||
k8s.io/klog/v2 v2.130.1
|
||||
k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
|
||||
github.com/evanphx/json-patch v5.9.11+incompatible // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/getkin/kin-openapi v0.133.0 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.22.4 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.4 // indirect
|
||||
github.com/go-openapi/swag v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/cmdutils v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/conv v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/fileutils v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/jsonname v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/jsonutils v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/loading v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/mangling v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/netutils v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/stringutils v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/typeutils v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
|
||||
github.com/go-test/deep v1.1.1 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/google/gnostic-models v0.7.1 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.7 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/mailru/easyjson v0.9.1 // 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/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect
|
||||
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.22.2 // indirect
|
||||
github.com/onsi/gomega v1.36.2 // indirect
|
||||
github.com/perimeterx/marshmallow v1.1.5 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_golang v1.23.2 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.67.4 // indirect
|
||||
github.com/prometheus/procfs v0.19.2 // indirect
|
||||
github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/woodsbury/decimal128 v1.4.0 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/otel v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.39.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
golang.org/x/oauth2 v0.34.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/term v0.38.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect
|
||||
google.golang.org/grpc v1.77.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/api v0.34.3 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.34.3 // indirect
|
||||
k8s.io/client-go v0.34.3 // indirect
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect
|
||||
sigs.k8s.io/yaml v1.6.0 // indirect
|
||||
)
|
||||
@@ -0,0 +1,264 @@
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf h1:TqhNAT4zKbTdLa62d2HDBFdvgSbIGB3eJE8HqhgiL9I=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
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/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8=
|
||||
github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
||||
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/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ=
|
||||
github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
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-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4=
|
||||
github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80=
|
||||
github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8=
|
||||
github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4=
|
||||
github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU=
|
||||
github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ=
|
||||
github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4=
|
||||
github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0=
|
||||
github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4=
|
||||
github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU=
|
||||
github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y=
|
||||
github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk=
|
||||
github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI=
|
||||
github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag=
|
||||
github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA=
|
||||
github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY=
|
||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo=
|
||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM=
|
||||
github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s=
|
||||
github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE=
|
||||
github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48=
|
||||
github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg=
|
||||
github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0=
|
||||
github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg=
|
||||
github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8=
|
||||
github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0=
|
||||
github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw=
|
||||
github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE=
|
||||
github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw=
|
||||
github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc=
|
||||
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4=
|
||||
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg=
|
||||
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
|
||||
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
|
||||
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c=
|
||||
github.com/google/gnostic-models v0.7.1/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/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
|
||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grafana/grafana-app-sdk v0.48.7 h1:9mF7nqkqP0QUYYDlznoOt+GIyjzj45wGfUHB32u2ZMo=
|
||||
github.com/grafana/grafana-app-sdk v0.48.7/go.mod h1:DWsaaH39ZMHwSOSoUBaeW8paMrRaYsjRYlLwCJYd78k=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.7 h1:Oa5qg473gka5+W/WQk61Xbw4YdAv+wV2Z4bJtzeCaQw=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.7/go.mod h1:5u3KalezoBAAo2Y3ytDYDAIIPvEqFLLDSxeiK99QxDU=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
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/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
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/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8=
|
||||
github.com/mailru/easyjson v0.9.1/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/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY=
|
||||
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw=
|
||||
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c=
|
||||
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
|
||||
github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU=
|
||||
github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk=
|
||||
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
|
||||
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
|
||||
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
|
||||
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
|
||||
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/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
|
||||
github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
|
||||
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
||||
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||
github.com/puzpuzpuz/xsync/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU=
|
||||
github.com/puzpuzpuz/xsync/v2 v2.5.1/go.mod h1:gD2H2krq/w52MfPLE+Uy64TzJDVY7lP2znR9qmR35kU=
|
||||
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/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQsBgUlc=
|
||||
github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU=
|
||||
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.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 h1:Ckwye2FpXkYgiHX7fyVrN1uA/UYd9ounqqTuSNAv0k4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0/go.mod h1:teIFJh5pW2y+AN7riv6IBPX2DuesS3HgP39mwOspKwU=
|
||||
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
|
||||
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||
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.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
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/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
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/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
||||
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
||||
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.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
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/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||
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=
|
||||
gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0=
|
||||
gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 h1:7LRqPCEdE4TP4/9psdaB7F2nhZFfBiGJomA5sojLWdU=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
||||
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
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/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
|
||||
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/api v0.34.3 h1:D12sTP257/jSH2vHV2EDYrb16bS7ULlHpdNdNhEw2S4=
|
||||
k8s.io/api v0.34.3/go.mod h1:PyVQBF886Q5RSQZOim7DybQjAbVs8g7gwJNhGtY5MBk=
|
||||
k8s.io/apiextensions-apiserver v0.34.3 h1:p10fGlkDY09eWKOTeUSioxwLukJnm+KuDZdrW71y40g=
|
||||
k8s.io/apiextensions-apiserver v0.34.3/go.mod h1:aujxvqGFRdb/cmXYfcRTeppN7S2XV/t7WMEc64zB5A0=
|
||||
k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE=
|
||||
k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
|
||||
k8s.io/client-go v0.34.3 h1:wtYtpzy/OPNYf7WyNBTj3iUA0XaBHVqhv4Iv3tbrF5A=
|
||||
k8s.io/client-go v0.34.3/go.mod h1:OxxeYagaP9Kdf78UrKLa3YZixMCfP6bgPwPwNBQBzpM=
|
||||
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-20251125145642-4e65d59e963e h1:iW9ChlU0cU16w8MpVYjXk12dqQ4BPFBEgif+ap7/hqQ=
|
||||
k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/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.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.1/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,43 @@
|
||||
package investigations
|
||||
|
||||
// Collectable represents an item collected during investigation
|
||||
#Collectable: {
|
||||
id: string
|
||||
createdAt: string
|
||||
|
||||
title: string
|
||||
origin: string
|
||||
type: string
|
||||
queries: [...string] // +listType=atomic
|
||||
timeRange: #TimeRange
|
||||
datasource: #DatasourceRef
|
||||
url: string
|
||||
logoPath?: string
|
||||
|
||||
note: string
|
||||
noteUpdatedAt: string
|
||||
|
||||
fieldConfig: string
|
||||
}
|
||||
|
||||
#CollectableSummary: {
|
||||
id: string
|
||||
title: string
|
||||
logoPath: string
|
||||
origin: string
|
||||
}
|
||||
|
||||
// TimeRange represents a time range with both absolute and relative values
|
||||
#TimeRange: {
|
||||
from: string
|
||||
to: string
|
||||
raw: {
|
||||
from: string
|
||||
to: string
|
||||
}
|
||||
}
|
||||
|
||||
// DatasourceRef is a reference to a datasource
|
||||
#DatasourceRef: {
|
||||
uid: string
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
module: "github.com/grafana/grafana/apps/investigations"
|
||||
language: {
|
||||
version: "v0.11.0"
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package investigations
|
||||
|
||||
investigationV0alpha1: {
|
||||
kind: "Investigation"
|
||||
pluralName: "Investigations"
|
||||
schema: {
|
||||
spec: {
|
||||
title: string
|
||||
createdByProfile: #Person
|
||||
hasCustomName: bool
|
||||
isFavorite: bool
|
||||
overviewNote: string
|
||||
overviewNoteUpdatedAt: string
|
||||
collectables: [...#Collectable] // +listType=atomic
|
||||
viewMode: #ViewMode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Type definition for investigation summaries
|
||||
#InvestigationSummary: {
|
||||
title: string
|
||||
createdByProfile: #Person
|
||||
hasCustomName: bool
|
||||
isFavorite: bool
|
||||
overviewNote: string
|
||||
overviewNoteUpdatedAt: string
|
||||
viewMode: #ViewMode
|
||||
collectableSummaries: [...#CollectableSummary] // +listType=atomic
|
||||
}
|
||||
|
||||
// Person represents a user profile with basic information
|
||||
#Person: {
|
||||
uid: string // Unique identifier for the user
|
||||
name: string // Display name of the user
|
||||
gravatarUrl: string // URL to user's Gravatar image
|
||||
}
|
||||
|
||||
#ViewMode: {
|
||||
mode: "compact" | "full"
|
||||
showComments: bool
|
||||
showTooltips: bool
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package investigations
|
||||
|
||||
investigationIndexV0alpha1:{
|
||||
kind: "InvestigationIndex"
|
||||
pluralName: "InvestigationIndexes"
|
||||
schema: {
|
||||
spec: {
|
||||
// Title of the index, e.g. 'Favorites' or 'My Investigations'
|
||||
title: string
|
||||
|
||||
// The Person who owns this investigation index
|
||||
owner: #Person
|
||||
|
||||
// Array of investigation summaries
|
||||
investigationSummaries: [...#InvestigationSummary] // +listType=atomic
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package investigations
|
||||
|
||||
manifest: {
|
||||
appName: "investigations"
|
||||
groupOverride: "investigations.grafana.app"
|
||||
versions: {
|
||||
"v0alpha1": {
|
||||
codegen: {
|
||||
ts: {enabled: false}
|
||||
go: {enabled: true}
|
||||
}
|
||||
kinds: [
|
||||
investigationV0alpha1,
|
||||
investigationIndexV0alpha1,
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package v0alpha1
|
||||
|
||||
import "k8s.io/apimachinery/pkg/runtime/schema"
|
||||
|
||||
const (
|
||||
// APIGroup is the API group used by all kinds in this package
|
||||
APIGroup = "investigations.grafana.app"
|
||||
// APIVersion is the API version used by all kinds in this package
|
||||
APIVersion = "v0alpha1"
|
||||
)
|
||||
|
||||
var (
|
||||
// GroupVersion is a schema.GroupVersion consisting of the Group and Version constants for this package
|
||||
GroupVersion = schema.GroupVersion{
|
||||
Group: APIGroup,
|
||||
Version: APIVersion,
|
||||
}
|
||||
)
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
package v0alpha1
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/resource"
|
||||
)
|
||||
|
||||
type InvestigationClient struct {
|
||||
client *resource.TypedClient[*Investigation, *InvestigationList]
|
||||
}
|
||||
|
||||
func NewInvestigationClient(client resource.Client) *InvestigationClient {
|
||||
return &InvestigationClient{
|
||||
client: resource.NewTypedClient[*Investigation, *InvestigationList](client, InvestigationKind()),
|
||||
}
|
||||
}
|
||||
|
||||
func NewInvestigationClientFromGenerator(generator resource.ClientGenerator) (*InvestigationClient, error) {
|
||||
c, err := generator.ClientFor(InvestigationKind())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewInvestigationClient(c), nil
|
||||
}
|
||||
|
||||
func (c *InvestigationClient) Get(ctx context.Context, identifier resource.Identifier) (*Investigation, error) {
|
||||
return c.client.Get(ctx, identifier)
|
||||
}
|
||||
|
||||
func (c *InvestigationClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*InvestigationList, error) {
|
||||
return c.client.List(ctx, namespace, opts)
|
||||
}
|
||||
|
||||
func (c *InvestigationClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*InvestigationList, error) {
|
||||
resp, err := c.client.List(ctx, namespace, resource.ListOptions{
|
||||
ResourceVersion: opts.ResourceVersion,
|
||||
Limit: opts.Limit,
|
||||
LabelFilters: opts.LabelFilters,
|
||||
FieldSelectors: opts.FieldSelectors,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for resp.GetContinue() != "" {
|
||||
page, err := c.client.List(ctx, namespace, resource.ListOptions{
|
||||
Continue: resp.GetContinue(),
|
||||
ResourceVersion: opts.ResourceVersion,
|
||||
Limit: opts.Limit,
|
||||
LabelFilters: opts.LabelFilters,
|
||||
FieldSelectors: opts.FieldSelectors,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.SetContinue(page.GetContinue())
|
||||
resp.SetResourceVersion(page.GetResourceVersion())
|
||||
resp.SetItems(append(resp.GetItems(), page.GetItems()...))
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (c *InvestigationClient) Create(ctx context.Context, obj *Investigation, opts resource.CreateOptions) (*Investigation, error) {
|
||||
// Make sure apiVersion and kind are set
|
||||
obj.APIVersion = GroupVersion.Identifier()
|
||||
obj.Kind = InvestigationKind().Kind()
|
||||
return c.client.Create(ctx, obj, opts)
|
||||
}
|
||||
|
||||
func (c *InvestigationClient) Update(ctx context.Context, obj *Investigation, opts resource.UpdateOptions) (*Investigation, error) {
|
||||
return c.client.Update(ctx, obj, opts)
|
||||
}
|
||||
|
||||
func (c *InvestigationClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*Investigation, error) {
|
||||
return c.client.Patch(ctx, identifier, req, opts)
|
||||
}
|
||||
|
||||
func (c *InvestigationClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error {
|
||||
return c.client.Delete(ctx, identifier, opts)
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// Code generated by grafana-app-sdk. DO NOT EDIT.
|
||||
//
|
||||
|
||||
package v0alpha1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/resource"
|
||||
)
|
||||
|
||||
// InvestigationJSONCodec is an implementation of resource.Codec for kubernetes JSON encoding
|
||||
type InvestigationJSONCodec struct{}
|
||||
|
||||
// Read reads JSON-encoded bytes from `reader` and unmarshals them into `into`
|
||||
func (*InvestigationJSONCodec) Read(reader io.Reader, into resource.Object) error {
|
||||
return json.NewDecoder(reader).Decode(into)
|
||||
}
|
||||
|
||||
// Write writes JSON-encoded bytes into `writer` marshaled from `from`
|
||||
func (*InvestigationJSONCodec) Write(writer io.Writer, from resource.Object) error {
|
||||
return json.NewEncoder(writer).Encode(from)
|
||||
}
|
||||
|
||||
// Interface compliance checks
|
||||
var _ resource.Codec = &InvestigationJSONCodec{}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
||||
|
||||
package v0alpha1
|
||||
|
||||
import (
|
||||
time "time"
|
||||
)
|
||||
|
||||
// metadata contains embedded CommonMetadata and can be extended with custom string fields
|
||||
// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here
|
||||
// without external reference as using the CommonMetadata reference breaks thema codegen.
|
||||
type InvestigationMetadata struct {
|
||||
UpdateTimestamp time.Time `json:"updateTimestamp"`
|
||||
CreatedBy string `json:"createdBy"`
|
||||
Uid string `json:"uid"`
|
||||
CreationTimestamp time.Time `json:"creationTimestamp"`
|
||||
DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"`
|
||||
Finalizers []string `json:"finalizers"`
|
||||
ResourceVersion string `json:"resourceVersion"`
|
||||
Generation int64 `json:"generation"`
|
||||
UpdatedBy string `json:"updatedBy"`
|
||||
Labels map[string]string `json:"labels"`
|
||||
}
|
||||
|
||||
// NewInvestigationMetadata creates a new InvestigationMetadata object.
|
||||
func NewInvestigationMetadata() *InvestigationMetadata {
|
||||
return &InvestigationMetadata{
|
||||
Finalizers: []string{},
|
||||
Labels: map[string]string{},
|
||||
}
|
||||
}
|
||||
+293
@@ -0,0 +1,293 @@
|
||||
//
|
||||
// Code generated by grafana-app-sdk. DO NOT EDIT.
|
||||
//
|
||||
|
||||
package v0alpha1
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/grafana/grafana-app-sdk/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"time"
|
||||
)
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type Investigation struct {
|
||||
metav1.TypeMeta `json:",inline" yaml:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata" yaml:"metadata"`
|
||||
|
||||
// Spec is the spec of the Investigation
|
||||
Spec InvestigationSpec `json:"spec" yaml:"spec"`
|
||||
}
|
||||
|
||||
func (o *Investigation) GetSpec() any {
|
||||
return o.Spec
|
||||
}
|
||||
|
||||
func (o *Investigation) SetSpec(spec any) error {
|
||||
cast, ok := spec.(InvestigationSpec)
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot set spec type %#v, not of type Spec", spec)
|
||||
}
|
||||
o.Spec = cast
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *Investigation) GetSubresources() map[string]any {
|
||||
return map[string]any{}
|
||||
}
|
||||
|
||||
func (o *Investigation) GetSubresource(name string) (any, bool) {
|
||||
switch name {
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Investigation) SetSubresource(name string, value any) error {
|
||||
switch name {
|
||||
default:
|
||||
return fmt.Errorf("subresource '%s' does not exist", name)
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Investigation) GetStaticMetadata() resource.StaticMetadata {
|
||||
gvk := o.GroupVersionKind()
|
||||
return resource.StaticMetadata{
|
||||
Name: o.ObjectMeta.Name,
|
||||
Namespace: o.ObjectMeta.Namespace,
|
||||
Group: gvk.Group,
|
||||
Version: gvk.Version,
|
||||
Kind: gvk.Kind,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Investigation) SetStaticMetadata(metadata resource.StaticMetadata) {
|
||||
o.Name = metadata.Name
|
||||
o.Namespace = metadata.Namespace
|
||||
o.SetGroupVersionKind(schema.GroupVersionKind{
|
||||
Group: metadata.Group,
|
||||
Version: metadata.Version,
|
||||
Kind: metadata.Kind,
|
||||
})
|
||||
}
|
||||
|
||||
func (o *Investigation) GetCommonMetadata() resource.CommonMetadata {
|
||||
dt := o.DeletionTimestamp
|
||||
var deletionTimestamp *time.Time
|
||||
if dt != nil {
|
||||
deletionTimestamp = &dt.Time
|
||||
}
|
||||
// Legacy ExtraFields support
|
||||
extraFields := make(map[string]any)
|
||||
if o.Annotations != nil {
|
||||
extraFields["annotations"] = o.Annotations
|
||||
}
|
||||
if o.ManagedFields != nil {
|
||||
extraFields["managedFields"] = o.ManagedFields
|
||||
}
|
||||
if o.OwnerReferences != nil {
|
||||
extraFields["ownerReferences"] = o.OwnerReferences
|
||||
}
|
||||
return resource.CommonMetadata{
|
||||
UID: string(o.UID),
|
||||
ResourceVersion: o.ResourceVersion,
|
||||
Generation: o.Generation,
|
||||
Labels: o.Labels,
|
||||
CreationTimestamp: o.CreationTimestamp.Time,
|
||||
DeletionTimestamp: deletionTimestamp,
|
||||
Finalizers: o.Finalizers,
|
||||
UpdateTimestamp: o.GetUpdateTimestamp(),
|
||||
CreatedBy: o.GetCreatedBy(),
|
||||
UpdatedBy: o.GetUpdatedBy(),
|
||||
ExtraFields: extraFields,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Investigation) SetCommonMetadata(metadata resource.CommonMetadata) {
|
||||
o.UID = types.UID(metadata.UID)
|
||||
o.ResourceVersion = metadata.ResourceVersion
|
||||
o.Generation = metadata.Generation
|
||||
o.Labels = metadata.Labels
|
||||
o.CreationTimestamp = metav1.NewTime(metadata.CreationTimestamp)
|
||||
if metadata.DeletionTimestamp != nil {
|
||||
dt := metav1.NewTime(*metadata.DeletionTimestamp)
|
||||
o.DeletionTimestamp = &dt
|
||||
} else {
|
||||
o.DeletionTimestamp = nil
|
||||
}
|
||||
o.Finalizers = metadata.Finalizers
|
||||
if o.Annotations == nil {
|
||||
o.Annotations = make(map[string]string)
|
||||
}
|
||||
if !metadata.UpdateTimestamp.IsZero() {
|
||||
o.SetUpdateTimestamp(metadata.UpdateTimestamp)
|
||||
}
|
||||
if metadata.CreatedBy != "" {
|
||||
o.SetCreatedBy(metadata.CreatedBy)
|
||||
}
|
||||
if metadata.UpdatedBy != "" {
|
||||
o.SetUpdatedBy(metadata.UpdatedBy)
|
||||
}
|
||||
// Legacy support for setting Annotations, ManagedFields, and OwnerReferences via ExtraFields
|
||||
if metadata.ExtraFields != nil {
|
||||
if annotations, ok := metadata.ExtraFields["annotations"]; ok {
|
||||
if cast, ok := annotations.(map[string]string); ok {
|
||||
o.Annotations = cast
|
||||
}
|
||||
}
|
||||
if managedFields, ok := metadata.ExtraFields["managedFields"]; ok {
|
||||
if cast, ok := managedFields.([]metav1.ManagedFieldsEntry); ok {
|
||||
o.ManagedFields = cast
|
||||
}
|
||||
}
|
||||
if ownerReferences, ok := metadata.ExtraFields["ownerReferences"]; ok {
|
||||
if cast, ok := ownerReferences.([]metav1.OwnerReference); ok {
|
||||
o.OwnerReferences = cast
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Investigation) GetCreatedBy() string {
|
||||
if o.ObjectMeta.Annotations == nil {
|
||||
o.ObjectMeta.Annotations = make(map[string]string)
|
||||
}
|
||||
|
||||
return o.ObjectMeta.Annotations["grafana.com/createdBy"]
|
||||
}
|
||||
|
||||
func (o *Investigation) SetCreatedBy(createdBy string) {
|
||||
if o.ObjectMeta.Annotations == nil {
|
||||
o.ObjectMeta.Annotations = make(map[string]string)
|
||||
}
|
||||
|
||||
o.ObjectMeta.Annotations["grafana.com/createdBy"] = createdBy
|
||||
}
|
||||
|
||||
func (o *Investigation) GetUpdateTimestamp() time.Time {
|
||||
if o.ObjectMeta.Annotations == nil {
|
||||
o.ObjectMeta.Annotations = make(map[string]string)
|
||||
}
|
||||
|
||||
parsed, _ := time.Parse(time.RFC3339, o.ObjectMeta.Annotations["grafana.com/updateTimestamp"])
|
||||
return parsed
|
||||
}
|
||||
|
||||
func (o *Investigation) SetUpdateTimestamp(updateTimestamp time.Time) {
|
||||
if o.ObjectMeta.Annotations == nil {
|
||||
o.ObjectMeta.Annotations = make(map[string]string)
|
||||
}
|
||||
|
||||
o.ObjectMeta.Annotations["grafana.com/updateTimestamp"] = updateTimestamp.Format(time.RFC3339)
|
||||
}
|
||||
|
||||
func (o *Investigation) GetUpdatedBy() string {
|
||||
if o.ObjectMeta.Annotations == nil {
|
||||
o.ObjectMeta.Annotations = make(map[string]string)
|
||||
}
|
||||
|
||||
return o.ObjectMeta.Annotations["grafana.com/updatedBy"]
|
||||
}
|
||||
|
||||
func (o *Investigation) SetUpdatedBy(updatedBy string) {
|
||||
if o.ObjectMeta.Annotations == nil {
|
||||
o.ObjectMeta.Annotations = make(map[string]string)
|
||||
}
|
||||
|
||||
o.ObjectMeta.Annotations["grafana.com/updatedBy"] = updatedBy
|
||||
}
|
||||
|
||||
func (o *Investigation) Copy() resource.Object {
|
||||
return resource.CopyObject(o)
|
||||
}
|
||||
|
||||
func (o *Investigation) DeepCopyObject() runtime.Object {
|
||||
return o.Copy()
|
||||
}
|
||||
|
||||
func (o *Investigation) DeepCopy() *Investigation {
|
||||
cpy := &Investigation{}
|
||||
o.DeepCopyInto(cpy)
|
||||
return cpy
|
||||
}
|
||||
|
||||
func (o *Investigation) DeepCopyInto(dst *Investigation) {
|
||||
dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion
|
||||
dst.TypeMeta.Kind = o.TypeMeta.Kind
|
||||
o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta)
|
||||
o.Spec.DeepCopyInto(&dst.Spec)
|
||||
}
|
||||
|
||||
// Interface compliance compile-time check
|
||||
var _ resource.Object = &Investigation{}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type InvestigationList struct {
|
||||
metav1.TypeMeta `json:",inline" yaml:",inline"`
|
||||
metav1.ListMeta `json:"metadata" yaml:"metadata"`
|
||||
Items []Investigation `json:"items" yaml:"items"`
|
||||
}
|
||||
|
||||
func (o *InvestigationList) DeepCopyObject() runtime.Object {
|
||||
return o.Copy()
|
||||
}
|
||||
|
||||
func (o *InvestigationList) Copy() resource.ListObject {
|
||||
cpy := &InvestigationList{
|
||||
TypeMeta: o.TypeMeta,
|
||||
Items: make([]Investigation, len(o.Items)),
|
||||
}
|
||||
o.ListMeta.DeepCopyInto(&cpy.ListMeta)
|
||||
for i := 0; i < len(o.Items); i++ {
|
||||
if item, ok := o.Items[i].Copy().(*Investigation); ok {
|
||||
cpy.Items[i] = *item
|
||||
}
|
||||
}
|
||||
return cpy
|
||||
}
|
||||
|
||||
func (o *InvestigationList) GetItems() []resource.Object {
|
||||
items := make([]resource.Object, len(o.Items))
|
||||
for i := 0; i < len(o.Items); i++ {
|
||||
items[i] = &o.Items[i]
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func (o *InvestigationList) SetItems(items []resource.Object) {
|
||||
o.Items = make([]Investigation, len(items))
|
||||
for i := 0; i < len(items); i++ {
|
||||
o.Items[i] = *items[i].(*Investigation)
|
||||
}
|
||||
}
|
||||
|
||||
func (o *InvestigationList) DeepCopy() *InvestigationList {
|
||||
cpy := &InvestigationList{}
|
||||
o.DeepCopyInto(cpy)
|
||||
return cpy
|
||||
}
|
||||
|
||||
func (o *InvestigationList) DeepCopyInto(dst *InvestigationList) {
|
||||
resource.CopyObjectInto(dst, o)
|
||||
}
|
||||
|
||||
// Interface compliance compile-time check
|
||||
var _ resource.ListObject = &InvestigationList{}
|
||||
|
||||
// Copy methods for all subresource types
|
||||
|
||||
// DeepCopy creates a full deep copy of Spec
|
||||
func (s *InvestigationSpec) DeepCopy() *InvestigationSpec {
|
||||
cpy := &InvestigationSpec{}
|
||||
s.DeepCopyInto(cpy)
|
||||
return cpy
|
||||
}
|
||||
|
||||
// DeepCopyInto deep copies Spec into another Spec object
|
||||
func (s *InvestigationSpec) DeepCopyInto(dst *InvestigationSpec) {
|
||||
resource.CopyObjectInto(dst, s)
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
//
|
||||
// Code generated by grafana-app-sdk. DO NOT EDIT.
|
||||
//
|
||||
|
||||
package v0alpha1
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana-app-sdk/resource"
|
||||
)
|
||||
|
||||
// schema is unexported to prevent accidental overwrites
|
||||
var (
|
||||
schemaInvestigation = resource.NewSimpleSchema("investigations.grafana.app", "v0alpha1", &Investigation{}, &InvestigationList{}, resource.WithKind("Investigation"),
|
||||
resource.WithPlural("investigations"), resource.WithScope(resource.NamespacedScope))
|
||||
kindInvestigation = resource.Kind{
|
||||
Schema: schemaInvestigation,
|
||||
Codecs: map[resource.KindEncoding]resource.Codec{
|
||||
resource.KindEncodingJSON: &InvestigationJSONCodec{},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// Kind returns a resource.Kind for this Schema with a JSON codec
|
||||
func InvestigationKind() resource.Kind {
|
||||
return kindInvestigation
|
||||
}
|
||||
|
||||
// Schema returns a resource.SimpleSchema representation of Investigation
|
||||
func InvestigationSchema() *resource.SimpleSchema {
|
||||
return schemaInvestigation
|
||||
}
|
||||
|
||||
// Interface compliance checks
|
||||
var _ resource.Schema = kindInvestigation
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
||||
|
||||
package v0alpha1
|
||||
|
||||
// Person represents a user profile with basic information
|
||||
// +k8s:openapi-gen=true
|
||||
type InvestigationPerson struct {
|
||||
// Unique identifier for the user
|
||||
Uid string `json:"uid"`
|
||||
// Display name of the user
|
||||
Name string `json:"name"`
|
||||
// URL to user's Gravatar image
|
||||
GravatarUrl string `json:"gravatarUrl"`
|
||||
}
|
||||
|
||||
// NewInvestigationPerson creates a new InvestigationPerson object.
|
||||
func NewInvestigationPerson() *InvestigationPerson {
|
||||
return &InvestigationPerson{}
|
||||
}
|
||||
|
||||
// Collectable represents an item collected during investigation
|
||||
// +k8s:openapi-gen=true
|
||||
type InvestigationCollectable struct {
|
||||
Id string `json:"id"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
Title string `json:"title"`
|
||||
Origin string `json:"origin"`
|
||||
Type string `json:"type"`
|
||||
// +listType=atomic
|
||||
Queries []string `json:"queries"`
|
||||
TimeRange InvestigationTimeRange `json:"timeRange"`
|
||||
Datasource InvestigationDatasourceRef `json:"datasource"`
|
||||
Url string `json:"url"`
|
||||
LogoPath *string `json:"logoPath,omitempty"`
|
||||
Note string `json:"note"`
|
||||
NoteUpdatedAt string `json:"noteUpdatedAt"`
|
||||
FieldConfig string `json:"fieldConfig"`
|
||||
}
|
||||
|
||||
// NewInvestigationCollectable creates a new InvestigationCollectable object.
|
||||
func NewInvestigationCollectable() *InvestigationCollectable {
|
||||
return &InvestigationCollectable{
|
||||
Queries: []string{},
|
||||
TimeRange: *NewInvestigationTimeRange(),
|
||||
Datasource: *NewInvestigationDatasourceRef(),
|
||||
}
|
||||
}
|
||||
|
||||
// TimeRange represents a time range with both absolute and relative values
|
||||
// +k8s:openapi-gen=true
|
||||
type InvestigationTimeRange struct {
|
||||
From string `json:"from"`
|
||||
To string `json:"to"`
|
||||
Raw InvestigationV0alpha1TimeRangeRaw `json:"raw"`
|
||||
}
|
||||
|
||||
// NewInvestigationTimeRange creates a new InvestigationTimeRange object.
|
||||
func NewInvestigationTimeRange() *InvestigationTimeRange {
|
||||
return &InvestigationTimeRange{
|
||||
Raw: *NewInvestigationV0alpha1TimeRangeRaw(),
|
||||
}
|
||||
}
|
||||
|
||||
// DatasourceRef is a reference to a datasource
|
||||
// +k8s:openapi-gen=true
|
||||
type InvestigationDatasourceRef struct {
|
||||
Uid string `json:"uid"`
|
||||
}
|
||||
|
||||
// NewInvestigationDatasourceRef creates a new InvestigationDatasourceRef object.
|
||||
func NewInvestigationDatasourceRef() *InvestigationDatasourceRef {
|
||||
return &InvestigationDatasourceRef{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type InvestigationViewMode struct {
|
||||
Mode InvestigationViewModeMode `json:"mode"`
|
||||
ShowComments bool `json:"showComments"`
|
||||
ShowTooltips bool `json:"showTooltips"`
|
||||
}
|
||||
|
||||
// NewInvestigationViewMode creates a new InvestigationViewMode object.
|
||||
func NewInvestigationViewMode() *InvestigationViewMode {
|
||||
return &InvestigationViewMode{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type InvestigationSpec struct {
|
||||
Title string `json:"title"`
|
||||
CreatedByProfile InvestigationPerson `json:"createdByProfile"`
|
||||
HasCustomName bool `json:"hasCustomName"`
|
||||
IsFavorite bool `json:"isFavorite"`
|
||||
OverviewNote string `json:"overviewNote"`
|
||||
OverviewNoteUpdatedAt string `json:"overviewNoteUpdatedAt"`
|
||||
// +listType=atomic
|
||||
Collectables []InvestigationCollectable `json:"collectables"`
|
||||
ViewMode InvestigationViewMode `json:"viewMode"`
|
||||
}
|
||||
|
||||
// NewInvestigationSpec creates a new InvestigationSpec object.
|
||||
func NewInvestigationSpec() *InvestigationSpec {
|
||||
return &InvestigationSpec{
|
||||
CreatedByProfile: *NewInvestigationPerson(),
|
||||
Collectables: []InvestigationCollectable{},
|
||||
ViewMode: *NewInvestigationViewMode(),
|
||||
}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type InvestigationV0alpha1TimeRangeRaw struct {
|
||||
From string `json:"from"`
|
||||
To string `json:"to"`
|
||||
}
|
||||
|
||||
// NewInvestigationV0alpha1TimeRangeRaw creates a new InvestigationV0alpha1TimeRangeRaw object.
|
||||
func NewInvestigationV0alpha1TimeRangeRaw() *InvestigationV0alpha1TimeRangeRaw {
|
||||
return &InvestigationV0alpha1TimeRangeRaw{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type InvestigationViewModeMode string
|
||||
|
||||
const (
|
||||
InvestigationViewModeModeCompact InvestigationViewModeMode = "compact"
|
||||
InvestigationViewModeModeFull InvestigationViewModeMode = "full"
|
||||
)
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
package v0alpha1
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/resource"
|
||||
)
|
||||
|
||||
type InvestigationIndexClient struct {
|
||||
client *resource.TypedClient[*InvestigationIndex, *InvestigationIndexList]
|
||||
}
|
||||
|
||||
func NewInvestigationIndexClient(client resource.Client) *InvestigationIndexClient {
|
||||
return &InvestigationIndexClient{
|
||||
client: resource.NewTypedClient[*InvestigationIndex, *InvestigationIndexList](client, InvestigationIndexKind()),
|
||||
}
|
||||
}
|
||||
|
||||
func NewInvestigationIndexClientFromGenerator(generator resource.ClientGenerator) (*InvestigationIndexClient, error) {
|
||||
c, err := generator.ClientFor(InvestigationIndexKind())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewInvestigationIndexClient(c), nil
|
||||
}
|
||||
|
||||
func (c *InvestigationIndexClient) Get(ctx context.Context, identifier resource.Identifier) (*InvestigationIndex, error) {
|
||||
return c.client.Get(ctx, identifier)
|
||||
}
|
||||
|
||||
func (c *InvestigationIndexClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*InvestigationIndexList, error) {
|
||||
return c.client.List(ctx, namespace, opts)
|
||||
}
|
||||
|
||||
func (c *InvestigationIndexClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*InvestigationIndexList, error) {
|
||||
resp, err := c.client.List(ctx, namespace, resource.ListOptions{
|
||||
ResourceVersion: opts.ResourceVersion,
|
||||
Limit: opts.Limit,
|
||||
LabelFilters: opts.LabelFilters,
|
||||
FieldSelectors: opts.FieldSelectors,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for resp.GetContinue() != "" {
|
||||
page, err := c.client.List(ctx, namespace, resource.ListOptions{
|
||||
Continue: resp.GetContinue(),
|
||||
ResourceVersion: opts.ResourceVersion,
|
||||
Limit: opts.Limit,
|
||||
LabelFilters: opts.LabelFilters,
|
||||
FieldSelectors: opts.FieldSelectors,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.SetContinue(page.GetContinue())
|
||||
resp.SetResourceVersion(page.GetResourceVersion())
|
||||
resp.SetItems(append(resp.GetItems(), page.GetItems()...))
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (c *InvestigationIndexClient) Create(ctx context.Context, obj *InvestigationIndex, opts resource.CreateOptions) (*InvestigationIndex, error) {
|
||||
// Make sure apiVersion and kind are set
|
||||
obj.APIVersion = GroupVersion.Identifier()
|
||||
obj.Kind = InvestigationIndexKind().Kind()
|
||||
return c.client.Create(ctx, obj, opts)
|
||||
}
|
||||
|
||||
func (c *InvestigationIndexClient) Update(ctx context.Context, obj *InvestigationIndex, opts resource.UpdateOptions) (*InvestigationIndex, error) {
|
||||
return c.client.Update(ctx, obj, opts)
|
||||
}
|
||||
|
||||
func (c *InvestigationIndexClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*InvestigationIndex, error) {
|
||||
return c.client.Patch(ctx, identifier, req, opts)
|
||||
}
|
||||
|
||||
func (c *InvestigationIndexClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error {
|
||||
return c.client.Delete(ctx, identifier, opts)
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// Code generated by grafana-app-sdk. DO NOT EDIT.
|
||||
//
|
||||
|
||||
package v0alpha1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/resource"
|
||||
)
|
||||
|
||||
// InvestigationIndexJSONCodec is an implementation of resource.Codec for kubernetes JSON encoding
|
||||
type InvestigationIndexJSONCodec struct{}
|
||||
|
||||
// Read reads JSON-encoded bytes from `reader` and unmarshals them into `into`
|
||||
func (*InvestigationIndexJSONCodec) Read(reader io.Reader, into resource.Object) error {
|
||||
return json.NewDecoder(reader).Decode(into)
|
||||
}
|
||||
|
||||
// Write writes JSON-encoded bytes into `writer` marshaled from `from`
|
||||
func (*InvestigationIndexJSONCodec) Write(writer io.Writer, from resource.Object) error {
|
||||
return json.NewEncoder(writer).Encode(from)
|
||||
}
|
||||
|
||||
// Interface compliance checks
|
||||
var _ resource.Codec = &InvestigationIndexJSONCodec{}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
||||
|
||||
package v0alpha1
|
||||
|
||||
import (
|
||||
time "time"
|
||||
)
|
||||
|
||||
// metadata contains embedded CommonMetadata and can be extended with custom string fields
|
||||
// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here
|
||||
// without external reference as using the CommonMetadata reference breaks thema codegen.
|
||||
type InvestigationIndexMetadata struct {
|
||||
UpdateTimestamp time.Time `json:"updateTimestamp"`
|
||||
CreatedBy string `json:"createdBy"`
|
||||
Uid string `json:"uid"`
|
||||
CreationTimestamp time.Time `json:"creationTimestamp"`
|
||||
DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"`
|
||||
Finalizers []string `json:"finalizers"`
|
||||
ResourceVersion string `json:"resourceVersion"`
|
||||
Generation int64 `json:"generation"`
|
||||
UpdatedBy string `json:"updatedBy"`
|
||||
Labels map[string]string `json:"labels"`
|
||||
}
|
||||
|
||||
// NewInvestigationIndexMetadata creates a new InvestigationIndexMetadata object.
|
||||
func NewInvestigationIndexMetadata() *InvestigationIndexMetadata {
|
||||
return &InvestigationIndexMetadata{
|
||||
Finalizers: []string{},
|
||||
Labels: map[string]string{},
|
||||
}
|
||||
}
|
||||
+293
@@ -0,0 +1,293 @@
|
||||
//
|
||||
// Code generated by grafana-app-sdk. DO NOT EDIT.
|
||||
//
|
||||
|
||||
package v0alpha1
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/grafana/grafana-app-sdk/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"time"
|
||||
)
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type InvestigationIndex struct {
|
||||
metav1.TypeMeta `json:",inline" yaml:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata" yaml:"metadata"`
|
||||
|
||||
// Spec is the spec of the InvestigationIndex
|
||||
Spec InvestigationIndexSpec `json:"spec" yaml:"spec"`
|
||||
}
|
||||
|
||||
func (o *InvestigationIndex) GetSpec() any {
|
||||
return o.Spec
|
||||
}
|
||||
|
||||
func (o *InvestigationIndex) SetSpec(spec any) error {
|
||||
cast, ok := spec.(InvestigationIndexSpec)
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot set spec type %#v, not of type Spec", spec)
|
||||
}
|
||||
o.Spec = cast
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *InvestigationIndex) GetSubresources() map[string]any {
|
||||
return map[string]any{}
|
||||
}
|
||||
|
||||
func (o *InvestigationIndex) GetSubresource(name string) (any, bool) {
|
||||
switch name {
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
func (o *InvestigationIndex) SetSubresource(name string, value any) error {
|
||||
switch name {
|
||||
default:
|
||||
return fmt.Errorf("subresource '%s' does not exist", name)
|
||||
}
|
||||
}
|
||||
|
||||
func (o *InvestigationIndex) GetStaticMetadata() resource.StaticMetadata {
|
||||
gvk := o.GroupVersionKind()
|
||||
return resource.StaticMetadata{
|
||||
Name: o.ObjectMeta.Name,
|
||||
Namespace: o.ObjectMeta.Namespace,
|
||||
Group: gvk.Group,
|
||||
Version: gvk.Version,
|
||||
Kind: gvk.Kind,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *InvestigationIndex) SetStaticMetadata(metadata resource.StaticMetadata) {
|
||||
o.Name = metadata.Name
|
||||
o.Namespace = metadata.Namespace
|
||||
o.SetGroupVersionKind(schema.GroupVersionKind{
|
||||
Group: metadata.Group,
|
||||
Version: metadata.Version,
|
||||
Kind: metadata.Kind,
|
||||
})
|
||||
}
|
||||
|
||||
func (o *InvestigationIndex) GetCommonMetadata() resource.CommonMetadata {
|
||||
dt := o.DeletionTimestamp
|
||||
var deletionTimestamp *time.Time
|
||||
if dt != nil {
|
||||
deletionTimestamp = &dt.Time
|
||||
}
|
||||
// Legacy ExtraFields support
|
||||
extraFields := make(map[string]any)
|
||||
if o.Annotations != nil {
|
||||
extraFields["annotations"] = o.Annotations
|
||||
}
|
||||
if o.ManagedFields != nil {
|
||||
extraFields["managedFields"] = o.ManagedFields
|
||||
}
|
||||
if o.OwnerReferences != nil {
|
||||
extraFields["ownerReferences"] = o.OwnerReferences
|
||||
}
|
||||
return resource.CommonMetadata{
|
||||
UID: string(o.UID),
|
||||
ResourceVersion: o.ResourceVersion,
|
||||
Generation: o.Generation,
|
||||
Labels: o.Labels,
|
||||
CreationTimestamp: o.CreationTimestamp.Time,
|
||||
DeletionTimestamp: deletionTimestamp,
|
||||
Finalizers: o.Finalizers,
|
||||
UpdateTimestamp: o.GetUpdateTimestamp(),
|
||||
CreatedBy: o.GetCreatedBy(),
|
||||
UpdatedBy: o.GetUpdatedBy(),
|
||||
ExtraFields: extraFields,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *InvestigationIndex) SetCommonMetadata(metadata resource.CommonMetadata) {
|
||||
o.UID = types.UID(metadata.UID)
|
||||
o.ResourceVersion = metadata.ResourceVersion
|
||||
o.Generation = metadata.Generation
|
||||
o.Labels = metadata.Labels
|
||||
o.CreationTimestamp = metav1.NewTime(metadata.CreationTimestamp)
|
||||
if metadata.DeletionTimestamp != nil {
|
||||
dt := metav1.NewTime(*metadata.DeletionTimestamp)
|
||||
o.DeletionTimestamp = &dt
|
||||
} else {
|
||||
o.DeletionTimestamp = nil
|
||||
}
|
||||
o.Finalizers = metadata.Finalizers
|
||||
if o.Annotations == nil {
|
||||
o.Annotations = make(map[string]string)
|
||||
}
|
||||
if !metadata.UpdateTimestamp.IsZero() {
|
||||
o.SetUpdateTimestamp(metadata.UpdateTimestamp)
|
||||
}
|
||||
if metadata.CreatedBy != "" {
|
||||
o.SetCreatedBy(metadata.CreatedBy)
|
||||
}
|
||||
if metadata.UpdatedBy != "" {
|
||||
o.SetUpdatedBy(metadata.UpdatedBy)
|
||||
}
|
||||
// Legacy support for setting Annotations, ManagedFields, and OwnerReferences via ExtraFields
|
||||
if metadata.ExtraFields != nil {
|
||||
if annotations, ok := metadata.ExtraFields["annotations"]; ok {
|
||||
if cast, ok := annotations.(map[string]string); ok {
|
||||
o.Annotations = cast
|
||||
}
|
||||
}
|
||||
if managedFields, ok := metadata.ExtraFields["managedFields"]; ok {
|
||||
if cast, ok := managedFields.([]metav1.ManagedFieldsEntry); ok {
|
||||
o.ManagedFields = cast
|
||||
}
|
||||
}
|
||||
if ownerReferences, ok := metadata.ExtraFields["ownerReferences"]; ok {
|
||||
if cast, ok := ownerReferences.([]metav1.OwnerReference); ok {
|
||||
o.OwnerReferences = cast
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (o *InvestigationIndex) GetCreatedBy() string {
|
||||
if o.ObjectMeta.Annotations == nil {
|
||||
o.ObjectMeta.Annotations = make(map[string]string)
|
||||
}
|
||||
|
||||
return o.ObjectMeta.Annotations["grafana.com/createdBy"]
|
||||
}
|
||||
|
||||
func (o *InvestigationIndex) SetCreatedBy(createdBy string) {
|
||||
if o.ObjectMeta.Annotations == nil {
|
||||
o.ObjectMeta.Annotations = make(map[string]string)
|
||||
}
|
||||
|
||||
o.ObjectMeta.Annotations["grafana.com/createdBy"] = createdBy
|
||||
}
|
||||
|
||||
func (o *InvestigationIndex) GetUpdateTimestamp() time.Time {
|
||||
if o.ObjectMeta.Annotations == nil {
|
||||
o.ObjectMeta.Annotations = make(map[string]string)
|
||||
}
|
||||
|
||||
parsed, _ := time.Parse(time.RFC3339, o.ObjectMeta.Annotations["grafana.com/updateTimestamp"])
|
||||
return parsed
|
||||
}
|
||||
|
||||
func (o *InvestigationIndex) SetUpdateTimestamp(updateTimestamp time.Time) {
|
||||
if o.ObjectMeta.Annotations == nil {
|
||||
o.ObjectMeta.Annotations = make(map[string]string)
|
||||
}
|
||||
|
||||
o.ObjectMeta.Annotations["grafana.com/updateTimestamp"] = updateTimestamp.Format(time.RFC3339)
|
||||
}
|
||||
|
||||
func (o *InvestigationIndex) GetUpdatedBy() string {
|
||||
if o.ObjectMeta.Annotations == nil {
|
||||
o.ObjectMeta.Annotations = make(map[string]string)
|
||||
}
|
||||
|
||||
return o.ObjectMeta.Annotations["grafana.com/updatedBy"]
|
||||
}
|
||||
|
||||
func (o *InvestigationIndex) SetUpdatedBy(updatedBy string) {
|
||||
if o.ObjectMeta.Annotations == nil {
|
||||
o.ObjectMeta.Annotations = make(map[string]string)
|
||||
}
|
||||
|
||||
o.ObjectMeta.Annotations["grafana.com/updatedBy"] = updatedBy
|
||||
}
|
||||
|
||||
func (o *InvestigationIndex) Copy() resource.Object {
|
||||
return resource.CopyObject(o)
|
||||
}
|
||||
|
||||
func (o *InvestigationIndex) DeepCopyObject() runtime.Object {
|
||||
return o.Copy()
|
||||
}
|
||||
|
||||
func (o *InvestigationIndex) DeepCopy() *InvestigationIndex {
|
||||
cpy := &InvestigationIndex{}
|
||||
o.DeepCopyInto(cpy)
|
||||
return cpy
|
||||
}
|
||||
|
||||
func (o *InvestigationIndex) DeepCopyInto(dst *InvestigationIndex) {
|
||||
dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion
|
||||
dst.TypeMeta.Kind = o.TypeMeta.Kind
|
||||
o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta)
|
||||
o.Spec.DeepCopyInto(&dst.Spec)
|
||||
}
|
||||
|
||||
// Interface compliance compile-time check
|
||||
var _ resource.Object = &InvestigationIndex{}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type InvestigationIndexList struct {
|
||||
metav1.TypeMeta `json:",inline" yaml:",inline"`
|
||||
metav1.ListMeta `json:"metadata" yaml:"metadata"`
|
||||
Items []InvestigationIndex `json:"items" yaml:"items"`
|
||||
}
|
||||
|
||||
func (o *InvestigationIndexList) DeepCopyObject() runtime.Object {
|
||||
return o.Copy()
|
||||
}
|
||||
|
||||
func (o *InvestigationIndexList) Copy() resource.ListObject {
|
||||
cpy := &InvestigationIndexList{
|
||||
TypeMeta: o.TypeMeta,
|
||||
Items: make([]InvestigationIndex, len(o.Items)),
|
||||
}
|
||||
o.ListMeta.DeepCopyInto(&cpy.ListMeta)
|
||||
for i := 0; i < len(o.Items); i++ {
|
||||
if item, ok := o.Items[i].Copy().(*InvestigationIndex); ok {
|
||||
cpy.Items[i] = *item
|
||||
}
|
||||
}
|
||||
return cpy
|
||||
}
|
||||
|
||||
func (o *InvestigationIndexList) GetItems() []resource.Object {
|
||||
items := make([]resource.Object, len(o.Items))
|
||||
for i := 0; i < len(o.Items); i++ {
|
||||
items[i] = &o.Items[i]
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func (o *InvestigationIndexList) SetItems(items []resource.Object) {
|
||||
o.Items = make([]InvestigationIndex, len(items))
|
||||
for i := 0; i < len(items); i++ {
|
||||
o.Items[i] = *items[i].(*InvestigationIndex)
|
||||
}
|
||||
}
|
||||
|
||||
func (o *InvestigationIndexList) DeepCopy() *InvestigationIndexList {
|
||||
cpy := &InvestigationIndexList{}
|
||||
o.DeepCopyInto(cpy)
|
||||
return cpy
|
||||
}
|
||||
|
||||
func (o *InvestigationIndexList) DeepCopyInto(dst *InvestigationIndexList) {
|
||||
resource.CopyObjectInto(dst, o)
|
||||
}
|
||||
|
||||
// Interface compliance compile-time check
|
||||
var _ resource.ListObject = &InvestigationIndexList{}
|
||||
|
||||
// Copy methods for all subresource types
|
||||
|
||||
// DeepCopy creates a full deep copy of Spec
|
||||
func (s *InvestigationIndexSpec) DeepCopy() *InvestigationIndexSpec {
|
||||
cpy := &InvestigationIndexSpec{}
|
||||
s.DeepCopyInto(cpy)
|
||||
return cpy
|
||||
}
|
||||
|
||||
// DeepCopyInto deep copies Spec into another Spec object
|
||||
func (s *InvestigationIndexSpec) DeepCopyInto(dst *InvestigationIndexSpec) {
|
||||
resource.CopyObjectInto(dst, s)
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
//
|
||||
// Code generated by grafana-app-sdk. DO NOT EDIT.
|
||||
//
|
||||
|
||||
package v0alpha1
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana-app-sdk/resource"
|
||||
)
|
||||
|
||||
// schema is unexported to prevent accidental overwrites
|
||||
var (
|
||||
schemaInvestigationIndex = resource.NewSimpleSchema("investigations.grafana.app", "v0alpha1", &InvestigationIndex{}, &InvestigationIndexList{}, resource.WithKind("InvestigationIndex"),
|
||||
resource.WithPlural("investigationindexes"), resource.WithScope(resource.NamespacedScope))
|
||||
kindInvestigationIndex = resource.Kind{
|
||||
Schema: schemaInvestigationIndex,
|
||||
Codecs: map[resource.KindEncoding]resource.Codec{
|
||||
resource.KindEncodingJSON: &InvestigationIndexJSONCodec{},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// Kind returns a resource.Kind for this Schema with a JSON codec
|
||||
func InvestigationIndexKind() resource.Kind {
|
||||
return kindInvestigationIndex
|
||||
}
|
||||
|
||||
// Schema returns a resource.SimpleSchema representation of InvestigationIndex
|
||||
func InvestigationIndexSchema() *resource.SimpleSchema {
|
||||
return schemaInvestigationIndex
|
||||
}
|
||||
|
||||
// Interface compliance checks
|
||||
var _ resource.Schema = kindInvestigationIndex
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
||||
|
||||
package v0alpha1
|
||||
|
||||
// Person represents a user profile with basic information
|
||||
// +k8s:openapi-gen=true
|
||||
type InvestigationIndexPerson struct {
|
||||
// Unique identifier for the user
|
||||
Uid string `json:"uid"`
|
||||
// Display name of the user
|
||||
Name string `json:"name"`
|
||||
// URL to user's Gravatar image
|
||||
GravatarUrl string `json:"gravatarUrl"`
|
||||
}
|
||||
|
||||
// NewInvestigationIndexPerson creates a new InvestigationIndexPerson object.
|
||||
func NewInvestigationIndexPerson() *InvestigationIndexPerson {
|
||||
return &InvestigationIndexPerson{}
|
||||
}
|
||||
|
||||
// Type definition for investigation summaries
|
||||
// +k8s:openapi-gen=true
|
||||
type InvestigationIndexInvestigationSummary struct {
|
||||
Title string `json:"title"`
|
||||
CreatedByProfile InvestigationIndexPerson `json:"createdByProfile"`
|
||||
HasCustomName bool `json:"hasCustomName"`
|
||||
IsFavorite bool `json:"isFavorite"`
|
||||
OverviewNote string `json:"overviewNote"`
|
||||
OverviewNoteUpdatedAt string `json:"overviewNoteUpdatedAt"`
|
||||
ViewMode InvestigationIndexViewMode `json:"viewMode"`
|
||||
// +listType=atomic
|
||||
CollectableSummaries []InvestigationIndexCollectableSummary `json:"collectableSummaries"`
|
||||
}
|
||||
|
||||
// NewInvestigationIndexInvestigationSummary creates a new InvestigationIndexInvestigationSummary object.
|
||||
func NewInvestigationIndexInvestigationSummary() *InvestigationIndexInvestigationSummary {
|
||||
return &InvestigationIndexInvestigationSummary{
|
||||
CreatedByProfile: *NewInvestigationIndexPerson(),
|
||||
ViewMode: *NewInvestigationIndexViewMode(),
|
||||
CollectableSummaries: []InvestigationIndexCollectableSummary{},
|
||||
}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type InvestigationIndexViewMode struct {
|
||||
Mode InvestigationIndexViewModeMode `json:"mode"`
|
||||
ShowComments bool `json:"showComments"`
|
||||
ShowTooltips bool `json:"showTooltips"`
|
||||
}
|
||||
|
||||
// NewInvestigationIndexViewMode creates a new InvestigationIndexViewMode object.
|
||||
func NewInvestigationIndexViewMode() *InvestigationIndexViewMode {
|
||||
return &InvestigationIndexViewMode{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type InvestigationIndexCollectableSummary struct {
|
||||
Id string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
LogoPath string `json:"logoPath"`
|
||||
Origin string `json:"origin"`
|
||||
}
|
||||
|
||||
// NewInvestigationIndexCollectableSummary creates a new InvestigationIndexCollectableSummary object.
|
||||
func NewInvestigationIndexCollectableSummary() *InvestigationIndexCollectableSummary {
|
||||
return &InvestigationIndexCollectableSummary{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type InvestigationIndexSpec struct {
|
||||
// Title of the index, e.g. 'Favorites' or 'My Investigations'
|
||||
Title string `json:"title"`
|
||||
// The Person who owns this investigation index
|
||||
Owner InvestigationIndexPerson `json:"owner"`
|
||||
// Array of investigation summaries
|
||||
// +listType=atomic
|
||||
InvestigationSummaries []InvestigationIndexInvestigationSummary `json:"investigationSummaries"`
|
||||
}
|
||||
|
||||
// NewInvestigationIndexSpec creates a new InvestigationIndexSpec object.
|
||||
func NewInvestigationIndexSpec() *InvestigationIndexSpec {
|
||||
return &InvestigationIndexSpec{
|
||||
Owner: *NewInvestigationIndexPerson(),
|
||||
InvestigationSummaries: []InvestigationIndexInvestigationSummary{},
|
||||
}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type InvestigationIndexViewModeMode string
|
||||
|
||||
const (
|
||||
InvestigationIndexViewModeModeCompact InvestigationIndexViewModeMode = "compact"
|
||||
InvestigationIndexViewModeModeFull InvestigationIndexViewModeMode = "full"
|
||||
)
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
||||
|
||||
package v0alpha1
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type InvestigationIndexstatusOperatorState struct {
|
||||
// lastEvaluation is the ResourceVersion last evaluated
|
||||
LastEvaluation string `json:"lastEvaluation"`
|
||||
// state describes the state of the lastEvaluation.
|
||||
// It is limited to three possible states for machine evaluation.
|
||||
State InvestigationIndexStatusOperatorStateState `json:"state"`
|
||||
// descriptiveState is an optional more descriptive state field which has no requirements on format
|
||||
DescriptiveState *string `json:"descriptiveState,omitempty"`
|
||||
// details contains any extra information that is operator-specific
|
||||
Details map[string]interface{} `json:"details,omitempty"`
|
||||
}
|
||||
|
||||
// NewInvestigationIndexstatusOperatorState creates a new InvestigationIndexstatusOperatorState object.
|
||||
func NewInvestigationIndexstatusOperatorState() *InvestigationIndexstatusOperatorState {
|
||||
return &InvestigationIndexstatusOperatorState{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type InvestigationIndexStatus struct {
|
||||
// operatorStates is a map of operator ID to operator state evaluations.
|
||||
// Any operator which consumes this kind SHOULD add its state evaluation information to this field.
|
||||
OperatorStates map[string]InvestigationIndexstatusOperatorState `json:"operatorStates,omitempty"`
|
||||
// additionalFields is reserved for future use
|
||||
AdditionalFields map[string]interface{} `json:"additionalFields,omitempty"`
|
||||
}
|
||||
|
||||
// NewInvestigationIndexStatus creates a new InvestigationIndexStatus object.
|
||||
func NewInvestigationIndexStatus() *InvestigationIndexStatus {
|
||||
return &InvestigationIndexStatus{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type InvestigationIndexStatusOperatorStateState string
|
||||
|
||||
const (
|
||||
InvestigationIndexStatusOperatorStateStateSuccess InvestigationIndexStatusOperatorStateState = "success"
|
||||
InvestigationIndexStatusOperatorStateStateInProgress InvestigationIndexStatusOperatorStateState = "in_progress"
|
||||
InvestigationIndexStatusOperatorStateStateFailed InvestigationIndexStatusOperatorStateState = "failed"
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
+136
@@ -0,0 +1,136 @@
|
||||
//
|
||||
// This file is generated by grafana-app-sdk
|
||||
// DO NOT EDIT
|
||||
//
|
||||
|
||||
package apis
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/app"
|
||||
"github.com/grafana/grafana-app-sdk/resource"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/kube-openapi/pkg/spec3"
|
||||
"k8s.io/kube-openapi/pkg/validation/spec"
|
||||
|
||||
v0alpha1 "github.com/grafana/grafana/apps/investigations/pkg/apis/investigations/v0alpha1"
|
||||
)
|
||||
|
||||
var (
|
||||
rawSchemaInvestigationv0alpha1 = []byte(`{"Collectable":{"additionalProperties":false,"description":"Collectable represents an item collected during investigation","properties":{"createdAt":{"type":"string"},"datasource":{"$ref":"#/components/schemas/DatasourceRef"},"fieldConfig":{"type":"string"},"id":{"type":"string"},"logoPath":{"type":"string"},"note":{"type":"string"},"noteUpdatedAt":{"type":"string"},"origin":{"type":"string"},"queries":{"description":"+listType=atomic","items":{"type":"string"},"type":"array"},"timeRange":{"$ref":"#/components/schemas/TimeRange"},"title":{"type":"string"},"type":{"type":"string"},"url":{"type":"string"}},"required":["id","createdAt","title","origin","type","queries","timeRange","datasource","url","note","noteUpdatedAt","fieldConfig"],"type":"object"},"DatasourceRef":{"additionalProperties":false,"description":"DatasourceRef is a reference to a datasource","properties":{"uid":{"type":"string"}},"required":["uid"],"type":"object"},"Investigation":{"properties":{"spec":{"$ref":"#/components/schemas/spec"}},"required":["spec"]},"Person":{"additionalProperties":false,"description":"Person represents a user profile with basic information","properties":{"gravatarUrl":{"description":"URL to user's Gravatar image","type":"string"},"name":{"description":"Display name of the user","type":"string"},"uid":{"description":"Unique identifier for the user","type":"string"}},"required":["uid","name","gravatarUrl"],"type":"object"},"TimeRange":{"additionalProperties":false,"description":"TimeRange represents a time range with both absolute and relative values","properties":{"from":{"type":"string"},"raw":{"additionalProperties":false,"properties":{"from":{"type":"string"},"to":{"type":"string"}},"required":["from","to"],"type":"object"},"to":{"type":"string"}},"required":["from","to","raw"],"type":"object"},"ViewMode":{"additionalProperties":false,"properties":{"mode":{"enum":["compact","full"],"type":"string"},"showComments":{"type":"boolean"},"showTooltips":{"type":"boolean"}},"required":["mode","showComments","showTooltips"],"type":"object"},"spec":{"additionalProperties":false,"properties":{"collectables":{"description":"+listType=atomic","items":{"$ref":"#/components/schemas/Collectable"},"type":"array"},"createdByProfile":{"$ref":"#/components/schemas/Person"},"hasCustomName":{"type":"boolean"},"isFavorite":{"type":"boolean"},"overviewNote":{"type":"string"},"overviewNoteUpdatedAt":{"type":"string"},"title":{"type":"string"},"viewMode":{"$ref":"#/components/schemas/ViewMode"}},"required":["title","createdByProfile","hasCustomName","isFavorite","overviewNote","overviewNoteUpdatedAt","collectables","viewMode"],"type":"object"}}`)
|
||||
versionSchemaInvestigationv0alpha1 app.VersionSchema
|
||||
_ = json.Unmarshal(rawSchemaInvestigationv0alpha1, &versionSchemaInvestigationv0alpha1)
|
||||
rawSchemaInvestigationIndexv0alpha1 = []byte(`{"CollectableSummary":{"additionalProperties":false,"properties":{"id":{"type":"string"},"logoPath":{"type":"string"},"origin":{"type":"string"},"title":{"type":"string"}},"required":["id","title","logoPath","origin"],"type":"object"},"InvestigationIndex":{"properties":{"spec":{"$ref":"#/components/schemas/spec"}},"required":["spec"]},"InvestigationSummary":{"additionalProperties":false,"description":"Type definition for investigation summaries","properties":{"collectableSummaries":{"description":"+listType=atomic","items":{"$ref":"#/components/schemas/CollectableSummary"},"type":"array"},"createdByProfile":{"$ref":"#/components/schemas/Person"},"hasCustomName":{"type":"boolean"},"isFavorite":{"type":"boolean"},"overviewNote":{"type":"string"},"overviewNoteUpdatedAt":{"type":"string"},"title":{"type":"string"},"viewMode":{"$ref":"#/components/schemas/ViewMode"}},"required":["title","createdByProfile","hasCustomName","isFavorite","overviewNote","overviewNoteUpdatedAt","viewMode","collectableSummaries"],"type":"object"},"Person":{"additionalProperties":false,"description":"Person represents a user profile with basic information","properties":{"gravatarUrl":{"description":"URL to user's Gravatar image","type":"string"},"name":{"description":"Display name of the user","type":"string"},"uid":{"description":"Unique identifier for the user","type":"string"}},"required":["uid","name","gravatarUrl"],"type":"object"},"ViewMode":{"additionalProperties":false,"properties":{"mode":{"enum":["compact","full"],"type":"string"},"showComments":{"type":"boolean"},"showTooltips":{"type":"boolean"}},"required":["mode","showComments","showTooltips"],"type":"object"},"spec":{"additionalProperties":false,"properties":{"investigationSummaries":{"description":"Array of investigation summaries\n+listType=atomic","items":{"$ref":"#/components/schemas/InvestigationSummary"},"type":"array"},"owner":{"$ref":"#/components/schemas/Person","description":"The Person who owns this investigation index"},"title":{"description":"Title of the index, e.g. 'Favorites' or 'My Investigations'","type":"string"}},"required":["title","owner","investigationSummaries"],"type":"object"}}`)
|
||||
versionSchemaInvestigationIndexv0alpha1 app.VersionSchema
|
||||
_ = json.Unmarshal(rawSchemaInvestigationIndexv0alpha1, &versionSchemaInvestigationIndexv0alpha1)
|
||||
)
|
||||
|
||||
var appManifestData = app.ManifestData{
|
||||
AppName: "investigations",
|
||||
Group: "investigations.grafana.app",
|
||||
PreferredVersion: "v0alpha1",
|
||||
Versions: []app.ManifestVersion{
|
||||
{
|
||||
Name: "v0alpha1",
|
||||
Served: true,
|
||||
Kinds: []app.ManifestVersionKind{
|
||||
{
|
||||
Kind: "Investigation",
|
||||
Plural: "Investigations",
|
||||
Scope: "Namespaced",
|
||||
Conversion: false,
|
||||
Schema: &versionSchemaInvestigationv0alpha1,
|
||||
},
|
||||
|
||||
{
|
||||
Kind: "InvestigationIndex",
|
||||
Plural: "InvestigationIndexes",
|
||||
Scope: "Namespaced",
|
||||
Conversion: false,
|
||||
Schema: &versionSchemaInvestigationIndexv0alpha1,
|
||||
},
|
||||
},
|
||||
Routes: app.ManifestVersionRoutes{
|
||||
Namespaced: map[string]spec3.PathProps{},
|
||||
Cluster: map[string]spec3.PathProps{},
|
||||
Schemas: map[string]spec.Schema{},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func LocalManifest() app.Manifest {
|
||||
return app.NewEmbeddedManifest(appManifestData)
|
||||
}
|
||||
|
||||
func RemoteManifest() app.Manifest {
|
||||
return app.NewAPIServerManifest("investigations")
|
||||
}
|
||||
|
||||
var kindVersionToGoType = map[string]resource.Kind{
|
||||
"Investigation/v0alpha1": v0alpha1.InvestigationKind(),
|
||||
"InvestigationIndex/v0alpha1": v0alpha1.InvestigationIndexKind(),
|
||||
}
|
||||
|
||||
// ManifestGoTypeAssociator returns the associated resource.Kind instance for a given Kind and Version, if one exists.
|
||||
// If there is no association for the provided Kind and Version, exists will return false.
|
||||
func ManifestGoTypeAssociator(kind, version string) (goType resource.Kind, exists bool) {
|
||||
goType, exists = kindVersionToGoType[fmt.Sprintf("%s/%s", kind, version)]
|
||||
return goType, exists
|
||||
}
|
||||
|
||||
var customRouteToGoResponseType = map[string]any{}
|
||||
|
||||
// ManifestCustomRouteResponsesAssociator returns the associated response go type for a given kind, version, custom route path, and method, if one exists.
|
||||
// kind may be empty for custom routes which are not kind subroutes. Leading slashes are removed from subroute paths.
|
||||
// If there is no association for the provided kind, version, custom route path, and method, exists will return false.
|
||||
// Resource routes (those without a kind) should prefix their route with "<namespace>/" if the route is namespaced (otherwise the route is assumed to be cluster-scope)
|
||||
func ManifestCustomRouteResponsesAssociator(kind, version, path, verb string) (goType any, exists bool) {
|
||||
if len(path) > 0 && path[0] == '/' {
|
||||
path = path[1:]
|
||||
}
|
||||
goType, exists = customRouteToGoResponseType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))]
|
||||
return goType, exists
|
||||
}
|
||||
|
||||
var customRouteToGoParamsType = map[string]runtime.Object{}
|
||||
|
||||
func ManifestCustomRouteQueryAssociator(kind, version, path, verb string) (goType runtime.Object, exists bool) {
|
||||
if len(path) > 0 && path[0] == '/' {
|
||||
path = path[1:]
|
||||
}
|
||||
goType, exists = customRouteToGoParamsType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))]
|
||||
return goType, exists
|
||||
}
|
||||
|
||||
var customRouteToGoRequestBodyType = map[string]any{}
|
||||
|
||||
func ManifestCustomRouteRequestBodyAssociator(kind, version, path, verb string) (goType any, exists bool) {
|
||||
if len(path) > 0 && path[0] == '/' {
|
||||
path = path[1:]
|
||||
}
|
||||
goType, exists = customRouteToGoRequestBodyType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))]
|
||||
return goType, exists
|
||||
}
|
||||
|
||||
type GoTypeAssociator struct{}
|
||||
|
||||
func NewGoTypeAssociator() *GoTypeAssociator {
|
||||
return &GoTypeAssociator{}
|
||||
}
|
||||
|
||||
func (g *GoTypeAssociator) KindToGoType(kind, version string) (goType resource.Kind, exists bool) {
|
||||
return ManifestGoTypeAssociator(kind, version)
|
||||
}
|
||||
func (g *GoTypeAssociator) CustomRouteReturnGoType(kind, version, path, verb string) (goType any, exists bool) {
|
||||
return ManifestCustomRouteResponsesAssociator(kind, version, path, verb)
|
||||
}
|
||||
func (g *GoTypeAssociator) CustomRouteQueryGoType(kind, version, path, verb string) (goType runtime.Object, exists bool) {
|
||||
return ManifestCustomRouteQueryAssociator(kind, version, path, verb)
|
||||
}
|
||||
func (g *GoTypeAssociator) CustomRouteRequestBodyGoType(kind, version, path, verb string) (goType any, exists bool) {
|
||||
return ManifestCustomRouteRequestBodyAssociator(kind, version, path, verb)
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/app"
|
||||
"github.com/grafana/grafana-app-sdk/operator"
|
||||
"github.com/grafana/grafana-app-sdk/resource"
|
||||
"github.com/grafana/grafana-app-sdk/simple"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
investigationsv0alpha1 "github.com/grafana/grafana/apps/investigations/pkg/apis/investigations/v0alpha1"
|
||||
)
|
||||
|
||||
func New(cfg app.Config) (app.App, error) {
|
||||
var err error
|
||||
simpleConfig := simple.AppConfig{
|
||||
Name: "investigation",
|
||||
KubeConfig: cfg.KubeConfig,
|
||||
InformerConfig: simple.AppInformerConfig{
|
||||
InformerOptions: operator.InformerOptions{
|
||||
ErrorHandler: func(_ context.Context, err error) {
|
||||
klog.ErrorS(err, "Informer processing error")
|
||||
},
|
||||
},
|
||||
},
|
||||
ManagedKinds: []simple.AppManagedKind{
|
||||
{
|
||||
Kind: investigationsv0alpha1.InvestigationKind(),
|
||||
},
|
||||
{
|
||||
Kind: investigationsv0alpha1.InvestigationIndexKind(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
a, err := simple.NewApp(simpleConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = a.ValidateManifest(cfg.ManifestData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func GetKinds() map[schema.GroupVersion][]resource.Kind {
|
||||
gv := schema.GroupVersion{
|
||||
Group: investigationsv0alpha1.InvestigationKind().Group(),
|
||||
Version: investigationsv0alpha1.InvestigationKind().Version(),
|
||||
}
|
||||
return map[schema.GroupVersion][]resource.Kind{
|
||||
gv: {
|
||||
investigationsv0alpha1.InvestigationKind(),
|
||||
investigationsv0alpha1.InvestigationIndexKind(),
|
||||
},
|
||||
}
|
||||
}
|
||||
Generated
+49
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* This file was generated by grafana-app-sdk. DO NOT EDIT.
|
||||
*/
|
||||
import { Spec } from './types.spec.gen';
|
||||
import { Status } from './types.status.gen';
|
||||
|
||||
export interface Metadata {
|
||||
name: string;
|
||||
namespace: string;
|
||||
generateName?: string;
|
||||
selfLink?: string;
|
||||
uid?: string;
|
||||
resourceVersion?: string;
|
||||
generation?: number;
|
||||
creationTimestamp?: string;
|
||||
deletionTimestamp?: string;
|
||||
deletionGracePeriodSeconds?: number;
|
||||
labels?: Record<string, string>;
|
||||
annotations?: Record<string, string>;
|
||||
ownerReferences?: OwnerReference[];
|
||||
finalizers?: string[];
|
||||
managedFields?: ManagedFieldsEntry[];
|
||||
}
|
||||
|
||||
export interface OwnerReference {
|
||||
apiVersion: string;
|
||||
kind: string;
|
||||
name: string;
|
||||
uid: string;
|
||||
controller?: boolean;
|
||||
blockOwnerDeletion?: boolean;
|
||||
}
|
||||
|
||||
export interface ManagedFieldsEntry {
|
||||
manager?: string;
|
||||
operation?: string;
|
||||
apiVersion?: string;
|
||||
time?: string;
|
||||
fieldsType?: string;
|
||||
subresource?: string;
|
||||
}
|
||||
|
||||
export interface Investigation {
|
||||
kind: string;
|
||||
apiVersion: string;
|
||||
metadata: Metadata;
|
||||
spec: Spec;
|
||||
status: Status;
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
||||
|
||||
// metadata contains embedded CommonMetadata and can be extended with custom string fields
|
||||
// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here
|
||||
// without external reference as using the CommonMetadata reference breaks thema codegen.
|
||||
export interface Metadata {
|
||||
updateTimestamp: string;
|
||||
createdBy: string;
|
||||
uid: string;
|
||||
creationTimestamp: string;
|
||||
deletionTimestamp?: string;
|
||||
finalizers: string[];
|
||||
resourceVersion: string;
|
||||
generation: number;
|
||||
updatedBy: string;
|
||||
labels: Record<string, string>;
|
||||
}
|
||||
|
||||
export const defaultMetadata = (): Metadata => ({
|
||||
updateTimestamp: "",
|
||||
createdBy: "",
|
||||
uid: "",
|
||||
creationTimestamp: "",
|
||||
finalizers: [],
|
||||
resourceVersion: "",
|
||||
generation: 0,
|
||||
updatedBy: "",
|
||||
labels: {},
|
||||
});
|
||||
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
||||
|
||||
// Person represents a user profile with basic information
|
||||
export interface Person {
|
||||
// Unique identifier for the user
|
||||
uid: string;
|
||||
// Display name of the user
|
||||
name: string;
|
||||
// URL to user's Gravatar image
|
||||
gravatarUrl: string;
|
||||
}
|
||||
|
||||
export const defaultPerson = (): Person => ({
|
||||
uid: "",
|
||||
name: "",
|
||||
gravatarUrl: "",
|
||||
});
|
||||
|
||||
// Collectable represents an item collected during investigation
|
||||
export interface Collectable {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
title: string;
|
||||
origin: string;
|
||||
type: string;
|
||||
// +listType=atomic
|
||||
queries: string[];
|
||||
timeRange: TimeRange;
|
||||
datasource: DatasourceRef;
|
||||
url: string;
|
||||
logoPath?: string;
|
||||
note: string;
|
||||
noteUpdatedAt: string;
|
||||
fieldConfig: string;
|
||||
}
|
||||
|
||||
export const defaultCollectable = (): Collectable => ({
|
||||
id: "",
|
||||
createdAt: "",
|
||||
title: "",
|
||||
origin: "",
|
||||
type: "",
|
||||
queries: [],
|
||||
timeRange: defaultTimeRange(),
|
||||
datasource: defaultDatasourceRef(),
|
||||
url: "",
|
||||
note: "",
|
||||
noteUpdatedAt: "",
|
||||
fieldConfig: "",
|
||||
});
|
||||
|
||||
// TimeRange represents a time range with both absolute and relative values
|
||||
export interface TimeRange {
|
||||
from: string;
|
||||
to: string;
|
||||
raw: {
|
||||
from: string;
|
||||
to: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const defaultTimeRange = (): TimeRange => ({
|
||||
from: "",
|
||||
to: "",
|
||||
raw: {
|
||||
from: "",
|
||||
to: "",
|
||||
},
|
||||
});
|
||||
|
||||
// DatasourceRef is a reference to a datasource
|
||||
export interface DatasourceRef {
|
||||
uid: string;
|
||||
}
|
||||
|
||||
export const defaultDatasourceRef = (): DatasourceRef => ({
|
||||
uid: "",
|
||||
});
|
||||
|
||||
export interface ViewMode {
|
||||
mode: "compact" | "full";
|
||||
showComments: boolean;
|
||||
showTooltips: boolean;
|
||||
}
|
||||
|
||||
export const defaultViewMode = (): ViewMode => ({
|
||||
mode: "compact",
|
||||
showComments: false,
|
||||
showTooltips: false,
|
||||
});
|
||||
|
||||
// spec is the schema of our resource
|
||||
export interface Spec {
|
||||
title: string;
|
||||
createdByProfile: Person;
|
||||
hasCustomName: boolean;
|
||||
isFavorite: boolean;
|
||||
overviewNote: string;
|
||||
overviewNoteUpdatedAt: string;
|
||||
// +listType=atomic
|
||||
collectables: Collectable[];
|
||||
viewMode: ViewMode;
|
||||
}
|
||||
|
||||
export const defaultSpec = (): Spec => ({
|
||||
title: "",
|
||||
createdByProfile: defaultPerson(),
|
||||
hasCustomName: false,
|
||||
isFavorite: false,
|
||||
overviewNote: "",
|
||||
overviewNoteUpdatedAt: "",
|
||||
collectables: [],
|
||||
viewMode: defaultViewMode(),
|
||||
});
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
||||
|
||||
export interface OperatorState {
|
||||
// lastEvaluation is the ResourceVersion last evaluated
|
||||
lastEvaluation: string;
|
||||
// state describes the state of the lastEvaluation.
|
||||
// It is limited to three possible states for machine evaluation.
|
||||
state: "success" | "in_progress" | "failed";
|
||||
// descriptiveState is an optional more descriptive state field which has no requirements on format
|
||||
descriptiveState?: string;
|
||||
// details contains any extra information that is operator-specific
|
||||
details?: Record<string, any>;
|
||||
}
|
||||
|
||||
export const defaultOperatorState = (): OperatorState => ({
|
||||
lastEvaluation: "",
|
||||
state: "success",
|
||||
});
|
||||
|
||||
export interface Status {
|
||||
// operatorStates is a map of operator ID to operator state evaluations.
|
||||
// Any operator which consumes this kind SHOULD add its state evaluation information to this field.
|
||||
operatorStates?: Record<string, OperatorState>;
|
||||
// additionalFields is reserved for future use
|
||||
additionalFields?: Record<string, any>;
|
||||
}
|
||||
|
||||
export const defaultStatus = (): Status => ({
|
||||
});
|
||||
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* This file was generated by grafana-app-sdk. DO NOT EDIT.
|
||||
*/
|
||||
import { Spec } from './types.spec.gen';
|
||||
import { Status } from './types.status.gen';
|
||||
|
||||
export interface Metadata {
|
||||
name: string;
|
||||
namespace: string;
|
||||
generateName?: string;
|
||||
selfLink?: string;
|
||||
uid?: string;
|
||||
resourceVersion?: string;
|
||||
generation?: number;
|
||||
creationTimestamp?: string;
|
||||
deletionTimestamp?: string;
|
||||
deletionGracePeriodSeconds?: number;
|
||||
labels?: Record<string, string>;
|
||||
annotations?: Record<string, string>;
|
||||
ownerReferences?: OwnerReference[];
|
||||
finalizers?: string[];
|
||||
managedFields?: ManagedFieldsEntry[];
|
||||
}
|
||||
|
||||
export interface OwnerReference {
|
||||
apiVersion: string;
|
||||
kind: string;
|
||||
name: string;
|
||||
uid: string;
|
||||
controller?: boolean;
|
||||
blockOwnerDeletion?: boolean;
|
||||
}
|
||||
|
||||
export interface ManagedFieldsEntry {
|
||||
manager?: string;
|
||||
operation?: string;
|
||||
apiVersion?: string;
|
||||
time?: string;
|
||||
fieldsType?: string;
|
||||
subresource?: string;
|
||||
}
|
||||
|
||||
export interface InvestigationIndex {
|
||||
kind: string;
|
||||
apiVersion: string;
|
||||
metadata: Metadata;
|
||||
spec: Spec;
|
||||
status: Status;
|
||||
}
|
||||
Generated
+30
@@ -0,0 +1,30 @@
|
||||
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
||||
|
||||
// metadata contains embedded CommonMetadata and can be extended with custom string fields
|
||||
// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here
|
||||
// without external reference as using the CommonMetadata reference breaks thema codegen.
|
||||
export interface Metadata {
|
||||
updateTimestamp: string;
|
||||
createdBy: string;
|
||||
uid: string;
|
||||
creationTimestamp: string;
|
||||
deletionTimestamp?: string;
|
||||
finalizers: string[];
|
||||
resourceVersion: string;
|
||||
generation: number;
|
||||
updatedBy: string;
|
||||
labels: Record<string, string>;
|
||||
}
|
||||
|
||||
export const defaultMetadata = (): Metadata => ({
|
||||
updateTimestamp: "",
|
||||
createdBy: "",
|
||||
uid: "",
|
||||
creationTimestamp: "",
|
||||
finalizers: [],
|
||||
resourceVersion: "",
|
||||
generation: 0,
|
||||
updatedBy: "",
|
||||
labels: {},
|
||||
});
|
||||
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
||||
|
||||
// Person represents a user profile with basic information
|
||||
export interface Person {
|
||||
// Unique identifier for the user
|
||||
uid: string;
|
||||
// Display name of the user
|
||||
name: string;
|
||||
// URL to user's Gravatar image
|
||||
gravatarUrl: string;
|
||||
}
|
||||
|
||||
export const defaultPerson = (): Person => ({
|
||||
uid: "",
|
||||
name: "",
|
||||
gravatarUrl: "",
|
||||
});
|
||||
|
||||
// Type definition for investigation summaries
|
||||
export interface InvestigationSummary {
|
||||
title: string;
|
||||
createdByProfile: Person;
|
||||
hasCustomName: boolean;
|
||||
isFavorite: boolean;
|
||||
overviewNote: string;
|
||||
overviewNoteUpdatedAt: string;
|
||||
viewMode: ViewMode;
|
||||
// +listType=atomic
|
||||
collectableSummaries: CollectableSummary[];
|
||||
}
|
||||
|
||||
export const defaultInvestigationSummary = (): InvestigationSummary => ({
|
||||
title: "",
|
||||
createdByProfile: defaultPerson(),
|
||||
hasCustomName: false,
|
||||
isFavorite: false,
|
||||
overviewNote: "",
|
||||
overviewNoteUpdatedAt: "",
|
||||
viewMode: defaultViewMode(),
|
||||
collectableSummaries: [],
|
||||
});
|
||||
|
||||
export interface ViewMode {
|
||||
mode: "compact" | "full";
|
||||
showComments: boolean;
|
||||
showTooltips: boolean;
|
||||
}
|
||||
|
||||
export const defaultViewMode = (): ViewMode => ({
|
||||
mode: "compact",
|
||||
showComments: false,
|
||||
showTooltips: false,
|
||||
});
|
||||
|
||||
export interface CollectableSummary {
|
||||
id: string;
|
||||
title: string;
|
||||
logoPath: string;
|
||||
origin: string;
|
||||
}
|
||||
|
||||
export const defaultCollectableSummary = (): CollectableSummary => ({
|
||||
id: "",
|
||||
title: "",
|
||||
logoPath: "",
|
||||
origin: "",
|
||||
});
|
||||
|
||||
export interface Spec {
|
||||
// Title of the index, e.g. 'Favorites' or 'My Investigations'
|
||||
title: string;
|
||||
// The Person who owns this investigation index
|
||||
owner: Person;
|
||||
// Array of investigation summaries
|
||||
// +listType=atomic
|
||||
investigationSummaries: InvestigationSummary[];
|
||||
}
|
||||
|
||||
export const defaultSpec = (): Spec => ({
|
||||
title: "",
|
||||
owner: defaultPerson(),
|
||||
investigationSummaries: [],
|
||||
});
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
||||
|
||||
export interface OperatorState {
|
||||
// lastEvaluation is the ResourceVersion last evaluated
|
||||
lastEvaluation: string;
|
||||
// state describes the state of the lastEvaluation.
|
||||
// It is limited to three possible states for machine evaluation.
|
||||
state: "success" | "in_progress" | "failed";
|
||||
// descriptiveState is an optional more descriptive state field which has no requirements on format
|
||||
descriptiveState?: string;
|
||||
// details contains any extra information that is operator-specific
|
||||
details?: Record<string, any>;
|
||||
}
|
||||
|
||||
export const defaultOperatorState = (): OperatorState => ({
|
||||
lastEvaluation: "",
|
||||
state: "success",
|
||||
});
|
||||
|
||||
export interface Status {
|
||||
// operatorStates is a map of operator ID to operator state evaluations.
|
||||
// Any operator which consumes this kind SHOULD add its state evaluation information to this field.
|
||||
operatorStates?: Record<string, OperatorState>;
|
||||
// additionalFields is reserved for future use
|
||||
additionalFields?: Record<string, any>;
|
||||
}
|
||||
|
||||
export const defaultStatus = (): Status => ({
|
||||
});
|
||||
|
||||
@@ -147,8 +147,8 @@ require (
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // @grafana/alerting-backend
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // @grafana/grafana-operator-experience-squad
|
||||
github.com/olekukonko/tablewriter v0.0.5 // @grafana/grafana-backend-group
|
||||
github.com/open-feature/go-sdk v1.17.0 // @grafana/grafana-backend-group
|
||||
github.com/open-feature/go-sdk-contrib/providers/go-feature-flag v0.2.7 // @grafana/grafana-backend-group
|
||||
github.com/open-feature/go-sdk v1.16.0 // @grafana/grafana-backend-group
|
||||
github.com/open-feature/go-sdk-contrib/providers/go-feature-flag v0.2.6 // @grafana/grafana-backend-group
|
||||
github.com/open-feature/go-sdk-contrib/providers/ofrep v0.1.6 // @grafana/grafana-backend-group
|
||||
github.com/openfga/api/proto v0.0.0-20250909172242-b4b2a12f5c67 // @grafana/identity-access-team
|
||||
github.com/openfga/language/pkg/go v0.2.0-beta.2.0.20251027165255-0f8f255e5f6c // @grafana/identity-access-team
|
||||
@@ -250,6 +250,7 @@ require (
|
||||
github.com/grafana/grafana/apps/example v0.0.0-20251027162426-edef69fdc82b // @grafana/grafana-app-platform-squad
|
||||
github.com/grafana/grafana/apps/folder v0.0.0 // @grafana/grafana-search-and-storage
|
||||
github.com/grafana/grafana/apps/iam v0.0.0 // @grafana/identity-access-team
|
||||
github.com/grafana/grafana/apps/investigations v0.0.0 // @fcjack @matryer
|
||||
github.com/grafana/grafana/apps/logsdrilldown v0.0.0 // @grafana/observability-logs
|
||||
github.com/grafana/grafana/apps/playlist v0.0.0 // @grafana/grafana-app-platform-squad
|
||||
github.com/grafana/grafana/apps/plugins v0.0.0 // @grafana/plugins-platform-backend
|
||||
@@ -283,6 +284,7 @@ replace (
|
||||
github.com/grafana/grafana/apps/dashboard => ./apps/dashboard
|
||||
github.com/grafana/grafana/apps/folder => ./apps/folder
|
||||
github.com/grafana/grafana/apps/iam => ./apps/iam
|
||||
github.com/grafana/grafana/apps/investigations => ./apps/investigations
|
||||
github.com/grafana/grafana/apps/logsdrilldown => ./apps/logsdrilldown
|
||||
github.com/grafana/grafana/apps/playlist => ./apps/playlist
|
||||
github.com/grafana/grafana/apps/plugins => ./apps/plugins
|
||||
|
||||
@@ -2181,10 +2181,10 @@ github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1Cpa
|
||||
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
|
||||
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
|
||||
github.com/open-feature/go-sdk v1.17.0 h1:/OUBBw5d9D61JaNZZxb2Nnr5/EJrEpjtKCTY3rspJQk=
|
||||
github.com/open-feature/go-sdk v1.17.0/go.mod h1:lPxPSu1UnZ4E3dCxZi5gV3et2ACi8O8P+zsTGVsDZUw=
|
||||
github.com/open-feature/go-sdk-contrib/providers/go-feature-flag v0.2.7 h1:WRrqpQ3vp0f/HtGjaKKw3aqhrcTrXtOYQXP42U2/gR0=
|
||||
github.com/open-feature/go-sdk-contrib/providers/go-feature-flag v0.2.7/go.mod h1:f4wOeIW/t7HNXweyAQCIGBxICGzXgLHdE1f87Dfq1mI=
|
||||
github.com/open-feature/go-sdk v1.16.0 h1:5NCHYv5slvNBIZhYXAzAufo0OI59OACZ5tczVqSE+Tg=
|
||||
github.com/open-feature/go-sdk v1.16.0/go.mod h1:EIF40QcoYT1VbQkMPy2ZJH4kvZeY+qGUXAorzSWgKSo=
|
||||
github.com/open-feature/go-sdk-contrib/providers/go-feature-flag v0.2.6 h1:megzzlQGjsRVWDX8oJnLaa5eEcsAHekiL4Uvl3jSAcY=
|
||||
github.com/open-feature/go-sdk-contrib/providers/go-feature-flag v0.2.6/go.mod h1:K1gDKvt76CGFLSUMHUydd5ba2V5Cv69gQZsdbnXhAm8=
|
||||
github.com/open-feature/go-sdk-contrib/providers/ofrep v0.1.6 h1:WinefYxeVx5rV0uQmuWbxQf8iACu/JiRubo5w0saToc=
|
||||
github.com/open-feature/go-sdk-contrib/providers/ofrep v0.1.6/go.mod h1:Dwcaoma6lZVqYwyfVlY7eB6RXbG+Ju3b9cnpTlUN+Hc=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.124.1 h1:NrjsoVPxI6lmV8jPImDcMeqYh+97Y71f/HB5Sfpfe3I=
|
||||
|
||||
@@ -17,6 +17,7 @@ use (
|
||||
./apps/example
|
||||
./apps/folder
|
||||
./apps/iam
|
||||
./apps/investigations
|
||||
./apps/logsdrilldown
|
||||
./apps/playlist
|
||||
./apps/plugins
|
||||
|
||||
@@ -547,6 +547,11 @@ export interface FeatureToggles {
|
||||
*/
|
||||
alertingCentralAlertHistory?: boolean;
|
||||
/**
|
||||
* Enable new grouped navigation structure for Alerting
|
||||
* @default false
|
||||
*/
|
||||
alertingNavigationV2?: boolean;
|
||||
/**
|
||||
* Preserve plugin proxy trailing slash.
|
||||
* @default false
|
||||
*/
|
||||
@@ -787,6 +792,11 @@ export interface FeatureToggles {
|
||||
*/
|
||||
lokiLabelNamesQueryApi?: boolean;
|
||||
/**
|
||||
* Enable the investigations backend API
|
||||
* @default false
|
||||
*/
|
||||
investigationsBackend?: boolean;
|
||||
/**
|
||||
* Enable folder's api server counts
|
||||
* @default false
|
||||
*/
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { useId, memo, HTMLAttributes, SVGProps } from 'react';
|
||||
import { useId, memo, HTMLAttributes, ReactNode, SVGProps } from 'react';
|
||||
|
||||
import { FieldDisplay } from '@grafana/data';
|
||||
|
||||
import { RadialArcPathEndpointMarks } from './RadialArcPathEndpointMarks';
|
||||
import { getBarEndcapColors, getGradientCss } from './colors';
|
||||
import { getBarEndcapColors, getGradientCss, getEndpointMarkerColors } from './colors';
|
||||
import { RadialShape, RadialGaugeDimensions, GradientStop } from './types';
|
||||
import { drawRadialArcPath, toRad } from './utils';
|
||||
|
||||
@@ -30,6 +29,11 @@ interface RadialArcPathPropsWithGradient extends RadialArcPathPropsBase {
|
||||
|
||||
type RadialArcPathProps = RadialArcPathPropsWithColor | RadialArcPathPropsWithGradient;
|
||||
|
||||
const ENDPOINT_MARKER_MIN_ANGLE = 10;
|
||||
const DOT_OPACITY = 0.5;
|
||||
const DOT_RADIUS_FACTOR = 0.4;
|
||||
const MAX_DOT_RADIUS = 8;
|
||||
|
||||
export const RadialArcPath = memo(
|
||||
({
|
||||
arcLengthDeg,
|
||||
@@ -64,25 +68,67 @@ export const RadialArcPath = memo(
|
||||
const xEnd = centerX + radius * Math.cos(endRadians);
|
||||
const yEnd = centerY + radius * Math.sin(endRadians);
|
||||
|
||||
const dotRadius =
|
||||
endpointMarker === 'point' ? Math.min((barWidth / 2) * DOT_RADIUS_FACTOR, MAX_DOT_RADIUS) : barWidth / 2;
|
||||
|
||||
const bgDivStyle: HTMLAttributes<HTMLDivElement>['style'] = { width: boxSize, height: vizHeight, marginLeft: boxX };
|
||||
|
||||
const pathProps: SVGProps<SVGPathElement> = {};
|
||||
let barEndcapColors: [string, string] | undefined;
|
||||
let endpointMarks: ReactNode = null;
|
||||
if (isGradient) {
|
||||
bgDivStyle.backgroundImage = getGradientCss(rest.gradient, shape);
|
||||
|
||||
if (endpointMarker && (rest.gradient?.length ?? 0) > 0) {
|
||||
switch (endpointMarker) {
|
||||
case 'point':
|
||||
const [pointColorStart, pointColorEnd] = getEndpointMarkerColors(
|
||||
rest.gradient!,
|
||||
fieldDisplay.display.percent
|
||||
);
|
||||
endpointMarks = (
|
||||
<>
|
||||
{arcLengthDeg > ENDPOINT_MARKER_MIN_ANGLE && (
|
||||
<circle cx={xStart} cy={yStart} r={dotRadius} fill={pointColorStart} opacity={DOT_OPACITY} />
|
||||
)}
|
||||
<circle cx={xEnd} cy={yEnd} r={dotRadius} fill={pointColorEnd} opacity={DOT_OPACITY} />
|
||||
</>
|
||||
);
|
||||
break;
|
||||
case 'glow':
|
||||
const offsetAngle = toRad(ENDPOINT_MARKER_MIN_ANGLE);
|
||||
const xStartMark = centerX + radius * Math.cos(endRadians + offsetAngle);
|
||||
const yStartMark = centerY + radius * Math.sin(endRadians + offsetAngle);
|
||||
endpointMarks =
|
||||
arcLengthDeg > ENDPOINT_MARKER_MIN_ANGLE ? (
|
||||
<path
|
||||
d={['M', xStartMark, yStartMark, 'A', radius, radius, 0, 0, 1, xEnd, yEnd].join(' ')}
|
||||
fill="none"
|
||||
strokeWidth={barWidth}
|
||||
stroke={endpointMarkerGlowFilter}
|
||||
strokeLinecap={roundedBars ? 'round' : 'butt'}
|
||||
filter={glowFilter}
|
||||
/>
|
||||
) : null;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (barEndcaps) {
|
||||
barEndcapColors = getBarEndcapColors(rest.gradient, fieldDisplay.display.percent);
|
||||
}
|
||||
|
||||
pathProps.fill = 'none';
|
||||
pathProps.stroke = 'white';
|
||||
} else {
|
||||
bgDivStyle.backgroundColor = rest.color;
|
||||
|
||||
pathProps.fill = 'none';
|
||||
pathProps.stroke = rest.color;
|
||||
}
|
||||
|
||||
let barEndcapColors: [string, string] | undefined;
|
||||
if (barEndcaps) {
|
||||
barEndcapColors = isGradient
|
||||
? getBarEndcapColors(rest.gradient, fieldDisplay.display.percent)
|
||||
: [rest.color, rest.color];
|
||||
}
|
||||
|
||||
const pathEl = (
|
||||
<path d={path} strokeWidth={barWidth} strokeLinecap={roundedBars ? 'round' : 'butt'} {...pathProps} />
|
||||
);
|
||||
@@ -112,23 +158,7 @@ export const RadialArcPath = memo(
|
||||
)}
|
||||
</g>
|
||||
|
||||
{endpointMarker && (
|
||||
<RadialArcPathEndpointMarks
|
||||
startAngle={angle}
|
||||
arcLengthDeg={arcLengthDeg}
|
||||
dimensions={dimensions}
|
||||
endpointMarker={endpointMarker}
|
||||
fieldDisplay={fieldDisplay}
|
||||
xStart={xStart}
|
||||
xEnd={xEnd}
|
||||
yStart={yStart}
|
||||
yEnd={yEnd}
|
||||
roundedBars={roundedBars}
|
||||
endpointMarkerGlowFilter={endpointMarkerGlowFilter}
|
||||
glowFilter={glowFilter}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
{endpointMarks}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,143 +0,0 @@
|
||||
import { render, RenderResult } from '@testing-library/react';
|
||||
|
||||
import { FieldDisplay } from '@grafana/data';
|
||||
|
||||
import { RadialArcPathEndpointMarks, RadialArcPathEndpointMarksProps } from './RadialArcPathEndpointMarks';
|
||||
import { RadialGaugeDimensions } from './types';
|
||||
|
||||
const ser = new XMLSerializer();
|
||||
|
||||
const expectHTML = (result: RenderResult, expected: string) => {
|
||||
let actual = ser.serializeToString(result.asFragment()).replace(/xmlns=".*?" /g, '');
|
||||
expect(actual).toEqual(expected.replace(/^\s*|\n/gm, ''));
|
||||
};
|
||||
|
||||
describe('RadialArcPathEndpointMarks', () => {
|
||||
const defaultDimensions = Object.freeze({
|
||||
centerX: 100,
|
||||
centerY: 100,
|
||||
radius: 80,
|
||||
barWidth: 20,
|
||||
vizWidth: 200,
|
||||
vizHeight: 200,
|
||||
margin: 10,
|
||||
barIndex: 0,
|
||||
thresholdsBarRadius: 0,
|
||||
thresholdsBarWidth: 0,
|
||||
thresholdsBarSpacing: 0,
|
||||
scaleLabelsFontSize: 0,
|
||||
scaleLabelsSpacing: 0,
|
||||
scaleLabelsRadius: 0,
|
||||
gaugeBottomY: 0,
|
||||
}) satisfies RadialGaugeDimensions;
|
||||
|
||||
const defaultFieldDisplay = Object.freeze({
|
||||
name: 'Test',
|
||||
field: {},
|
||||
display: { text: '50', numeric: 50, color: '#FF0000' },
|
||||
hasLinks: false,
|
||||
}) satisfies FieldDisplay;
|
||||
|
||||
const defaultProps = Object.freeze({
|
||||
arcLengthDeg: 90,
|
||||
dimensions: defaultDimensions,
|
||||
fieldDisplay: defaultFieldDisplay,
|
||||
startAngle: 0,
|
||||
xStart: 100,
|
||||
xEnd: 150,
|
||||
yStart: 100,
|
||||
yEnd: 50,
|
||||
}) satisfies Omit<RadialArcPathEndpointMarksProps, 'color' | 'gradient' | 'endpointMarker'>;
|
||||
|
||||
it('renders the expected marks when endpointMarker is "point" w/ a static color', () => {
|
||||
expectHTML(
|
||||
render(
|
||||
<svg role="img">
|
||||
<RadialArcPathEndpointMarks {...defaultProps} endpointMarker="point" color="#FF0000" />
|
||||
</svg>
|
||||
),
|
||||
'<svg role=\"img\"><circle cx=\"100\" cy=\"100\" r=\"4\" fill=\"#111217\" opacity=\"0.5\"/><circle cx=\"150\" cy=\"50\" r=\"4\" fill=\"#111217\" opacity=\"0.5\"/></svg>'
|
||||
);
|
||||
});
|
||||
|
||||
it('renders the expected marks when endpointMarker is "point" w/ a gradient color', () => {
|
||||
expectHTML(
|
||||
render(
|
||||
<svg role="img">
|
||||
<RadialArcPathEndpointMarks
|
||||
{...defaultProps}
|
||||
endpointMarker="point"
|
||||
gradient={[
|
||||
{ color: '#00FF00', percent: 0 },
|
||||
{ color: '#0000FF', percent: 1 },
|
||||
]}
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
'<svg role=\"img\"><circle cx=\"100\" cy=\"100\" r=\"4\" fill=\"#111217\" opacity=\"0.5\"/><circle cx=\"150\" cy=\"50\" r=\"4\" fill=\"#fbfbfb\" opacity=\"0.5\"/></svg>'
|
||||
);
|
||||
});
|
||||
|
||||
it('renders the expected marks when endpointMarker is "glow" w/ a static color', () => {
|
||||
expectHTML(
|
||||
render(
|
||||
<svg role="img">
|
||||
<RadialArcPathEndpointMarks {...defaultProps} endpointMarker="glow" color="#FF0000" />
|
||||
</svg>
|
||||
),
|
||||
'<svg role=\"img\"><path d=\"M 113.89185421335443 21.215379759023364 A 80 80 0 0 1 150 50\" fill=\"none\" stroke-width=\"20\" stroke-linecap=\"butt\"/></svg>'
|
||||
);
|
||||
});
|
||||
|
||||
it('renders the expected marks when endpointMarker is "glow" w/ a gradient color', () => {
|
||||
expectHTML(
|
||||
render(
|
||||
<svg role="img">
|
||||
<RadialArcPathEndpointMarks
|
||||
{...defaultProps}
|
||||
endpointMarker="glow"
|
||||
gradient={[
|
||||
{ color: '#00FF00', percent: 0 },
|
||||
{ color: '#0000FF', percent: 1 },
|
||||
]}
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
'<svg role=\"img\"><path d=\"M 113.89185421335443 21.215379759023364 A 80 80 0 0 1 150 50\" fill=\"none\" stroke-width=\"20\" stroke-linecap=\"butt\"/></svg>'
|
||||
);
|
||||
});
|
||||
|
||||
it('does not render the start mark when arcLengthDeg is less than the minimum angle for "point" endpointMarker', () => {
|
||||
expectHTML(
|
||||
render(
|
||||
<svg role="img">
|
||||
<RadialArcPathEndpointMarks {...defaultProps} arcLengthDeg={5} endpointMarker="point" color="#FF0000" />
|
||||
</svg>
|
||||
),
|
||||
'<svg role=\"img\"><circle cx=\"150\" cy=\"50\" r=\"4\" fill=\"#111217\" opacity=\"0.5\"/></svg>'
|
||||
);
|
||||
});
|
||||
|
||||
it('does not render anything when arcLengthDeg is less than the minimum angle for "glow" endpointMarker', () => {
|
||||
expectHTML(
|
||||
render(
|
||||
<svg role="img">
|
||||
<RadialArcPathEndpointMarks {...defaultProps} arcLengthDeg={5} endpointMarker="glow" color="#FF0000" />
|
||||
</svg>
|
||||
),
|
||||
'<svg role=\"img\"/>'
|
||||
);
|
||||
});
|
||||
|
||||
it('does not render anything if endpointMarker is some other value', () => {
|
||||
expectHTML(
|
||||
render(
|
||||
<svg role="img">
|
||||
{/* @ts-ignore: confirming the component doesn't throw */}
|
||||
<RadialArcPathEndpointMarks {...defaultProps} endpointMarker="foo" />
|
||||
</svg>
|
||||
),
|
||||
'<svg role=\"img\"/>'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,98 +0,0 @@
|
||||
import { FieldDisplay } from '@grafana/data';
|
||||
|
||||
import { getEndpointMarkerColors, getGuideDotColor } from './colors';
|
||||
import { GradientStop, RadialGaugeDimensions } from './types';
|
||||
import { toRad } from './utils';
|
||||
|
||||
interface RadialArcPathEndpointMarksPropsBase {
|
||||
arcLengthDeg: number;
|
||||
dimensions: RadialGaugeDimensions;
|
||||
fieldDisplay: FieldDisplay;
|
||||
endpointMarker: 'point' | 'glow';
|
||||
roundedBars?: boolean;
|
||||
startAngle: number;
|
||||
glowFilter?: string;
|
||||
endpointMarkerGlowFilter?: string;
|
||||
xStart: number;
|
||||
xEnd: number;
|
||||
yStart: number;
|
||||
yEnd: number;
|
||||
}
|
||||
|
||||
interface RadialArcPathEndpointMarksPropsWithColor extends RadialArcPathEndpointMarksPropsBase {
|
||||
color: string;
|
||||
}
|
||||
|
||||
interface RadialArcPathEndpointMarksPropsWithGradient extends RadialArcPathEndpointMarksPropsBase {
|
||||
gradient: GradientStop[];
|
||||
}
|
||||
|
||||
export type RadialArcPathEndpointMarksProps =
|
||||
| RadialArcPathEndpointMarksPropsWithColor
|
||||
| RadialArcPathEndpointMarksPropsWithGradient;
|
||||
|
||||
const ENDPOINT_MARKER_MIN_ANGLE = 10;
|
||||
const DOT_OPACITY = 0.5;
|
||||
const DOT_RADIUS_FACTOR = 0.4;
|
||||
const MAX_DOT_RADIUS = 8;
|
||||
|
||||
export function RadialArcPathEndpointMarks({
|
||||
startAngle: angle,
|
||||
arcLengthDeg,
|
||||
dimensions,
|
||||
endpointMarker,
|
||||
fieldDisplay,
|
||||
xStart,
|
||||
xEnd,
|
||||
yStart,
|
||||
yEnd,
|
||||
roundedBars,
|
||||
endpointMarkerGlowFilter,
|
||||
glowFilter,
|
||||
...rest
|
||||
}: RadialArcPathEndpointMarksProps) {
|
||||
const isGradient = 'gradient' in rest;
|
||||
const { radius, centerX, centerY, barWidth } = dimensions;
|
||||
const endRadians = toRad(angle + arcLengthDeg);
|
||||
|
||||
switch (endpointMarker) {
|
||||
case 'point': {
|
||||
const [pointColorStart, pointColorEnd] = isGradient
|
||||
? getEndpointMarkerColors(rest.gradient, fieldDisplay.display.percent)
|
||||
: [getGuideDotColor(rest.color), getGuideDotColor(rest.color)];
|
||||
|
||||
const dotRadius =
|
||||
endpointMarker === 'point' ? Math.min((barWidth / 2) * DOT_RADIUS_FACTOR, MAX_DOT_RADIUS) : barWidth / 2;
|
||||
|
||||
return (
|
||||
<>
|
||||
{arcLengthDeg > ENDPOINT_MARKER_MIN_ANGLE && (
|
||||
<circle cx={xStart} cy={yStart} r={dotRadius} fill={pointColorStart} opacity={DOT_OPACITY} />
|
||||
)}
|
||||
<circle cx={xEnd} cy={yEnd} r={dotRadius} fill={pointColorEnd} opacity={DOT_OPACITY} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
case 'glow':
|
||||
const offsetAngle = toRad(ENDPOINT_MARKER_MIN_ANGLE);
|
||||
const xStartMark = centerX + radius * Math.cos(endRadians + offsetAngle);
|
||||
const yStartMark = centerY + radius * Math.sin(endRadians + offsetAngle);
|
||||
if (arcLengthDeg <= ENDPOINT_MARKER_MIN_ANGLE) {
|
||||
break;
|
||||
}
|
||||
return (
|
||||
<path
|
||||
d={['M', xStartMark, yStartMark, 'A', radius, radius, 0, 0, 1, xEnd, yEnd].join(' ')}
|
||||
fill="none"
|
||||
strokeWidth={barWidth}
|
||||
stroke={endpointMarkerGlowFilter}
|
||||
strokeLinecap={roundedBars ? 'round' : 'butt'}
|
||||
filter={glowFilter}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -175,7 +175,7 @@ export function getGradientCss(gradientStops: GradientStop[], shape: RadialShape
|
||||
const GRAY_05 = '#111217';
|
||||
const GRAY_90 = '#fbfbfb';
|
||||
const CONTRAST_THRESHOLD_MAX = 4.5;
|
||||
export const getGuideDotColor = (color: string): string => {
|
||||
const getGuideDotColor = (color: string): string => {
|
||||
const darkColor = GRAY_05;
|
||||
const lightColor = GRAY_90;
|
||||
return colorManipulator.getContrastRatio(darkColor, color) >= CONTRAST_THRESHOLD_MAX ? darkColor : lightColor;
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/registry/apps/annotation"
|
||||
"github.com/grafana/grafana/pkg/registry/apps/correlations"
|
||||
"github.com/grafana/grafana/pkg/registry/apps/example"
|
||||
"github.com/grafana/grafana/pkg/registry/apps/investigations"
|
||||
"github.com/grafana/grafana/pkg/registry/apps/logsdrilldown"
|
||||
"github.com/grafana/grafana/pkg/registry/apps/playlist"
|
||||
"github.com/grafana/grafana/pkg/registry/apps/plugins"
|
||||
@@ -106,6 +107,7 @@ func ProvideBuilderRunners(
|
||||
registrar builder.APIRegistrar,
|
||||
restConfigProvider apiserver.RestConfigProvider,
|
||||
features featuremgmt.FeatureToggles,
|
||||
investigationAppProvider *investigations.InvestigationsAppProvider,
|
||||
grafanaCfg *setting.Cfg,
|
||||
) (*Service, error) {
|
||||
cfgWrapper := func(ctx context.Context) (*rest.Config, error) {
|
||||
@@ -125,6 +127,11 @@ func ProvideBuilderRunners(
|
||||
var apiGroupRunner *runner.APIGroupRunner
|
||||
var err error
|
||||
providers := []app.Provider{}
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if features.IsEnabledGlobally(featuremgmt.FlagInvestigationsBackend) {
|
||||
logger.Debug("Investigations backend is enabled")
|
||||
providers = append(providers, investigationAppProvider)
|
||||
}
|
||||
apiGroupRunner, err = runner.NewAPIGroupRunner(cfg, providers...)
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package investigations
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
)
|
||||
|
||||
func GetAuthorizer() authorizer.Authorizer {
|
||||
return authorizer.AuthorizerFunc(func(
|
||||
ctx context.Context, attr authorizer.Attributes,
|
||||
) (authorized authorizer.Decision, reason string, err error) {
|
||||
if !attr.IsResourceRequest() {
|
||||
return authorizer.DecisionNoOpinion, "", nil
|
||||
}
|
||||
|
||||
u, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
return authorizer.DecisionDeny, "valid user is required", err
|
||||
}
|
||||
|
||||
p := u.GetPermissions()
|
||||
if len(p) == 0 {
|
||||
return authorizer.DecisionDeny, "no permissions", nil
|
||||
}
|
||||
|
||||
_, ok := p[accesscontrol.ActionDatasourcesExplore]
|
||||
if !ok {
|
||||
// defer to the default authorizer if datasources:explore is not present
|
||||
return authorizer.DecisionNoOpinion, "", nil
|
||||
}
|
||||
|
||||
return authorizer.DecisionAllow, "", nil
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package investigations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
)
|
||||
|
||||
func TestGetAuthorizer(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
attr authorizer.Attributes
|
||||
expectedDecision authorizer.Decision
|
||||
expectedReason string
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
name: "non-resource request",
|
||||
ctx: context.TODO(),
|
||||
attr: &mockAttributes{resourceRequest: false},
|
||||
expectedDecision: authorizer.DecisionNoOpinion,
|
||||
expectedReason: "",
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "user has datasources:explore permission",
|
||||
ctx: identity.WithRequester(context.TODO(), &mockUser{permissions: map[string][]string{accesscontrol.ActionDatasourcesExplore: {}}}),
|
||||
attr: &mockAttributes{resourceRequest: true},
|
||||
expectedDecision: authorizer.DecisionAllow,
|
||||
expectedReason: "",
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "user does not have datasources:explore permission",
|
||||
ctx: identity.WithRequester(context.TODO(), &mockUser{}),
|
||||
attr: &mockAttributes{resourceRequest: true},
|
||||
expectedDecision: authorizer.DecisionDeny,
|
||||
expectedReason: "no permissions",
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "user does not have datasources:explore permission",
|
||||
ctx: identity.WithRequester(context.TODO(), &mockUser{permissions: map[string][]string{"foo": {}}}),
|
||||
attr: &mockAttributes{resourceRequest: true},
|
||||
expectedDecision: authorizer.DecisionNoOpinion,
|
||||
expectedReason: "",
|
||||
expectedErr: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
auth := GetAuthorizer()
|
||||
decision, reason, err := auth.Authorize(tt.ctx, tt.attr)
|
||||
assert.Equal(t, tt.expectedDecision, decision)
|
||||
assert.Equal(t, tt.expectedReason, reason)
|
||||
assert.Equal(t, tt.expectedErr, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type mockAttributes struct {
|
||||
authorizer.Attributes
|
||||
resourceRequest bool
|
||||
}
|
||||
|
||||
func (m *mockAttributes) IsResourceRequest() bool {
|
||||
return m.resourceRequest
|
||||
}
|
||||
|
||||
// Implement other methods of authorizer.Attributes as needed
|
||||
|
||||
type mockUser struct {
|
||||
identity.Requester
|
||||
permissions map[string][]string
|
||||
}
|
||||
|
||||
func (m *mockUser) GetPermissions() map[string][]string {
|
||||
return m.permissions
|
||||
}
|
||||
|
||||
// Implement other methods of identity.Requester as needed
|
||||
@@ -0,0 +1,33 @@
|
||||
package investigations
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana-app-sdk/app"
|
||||
"github.com/grafana/grafana-app-sdk/simple"
|
||||
"github.com/grafana/grafana/apps/investigations/pkg/apis"
|
||||
investigationv0alpha1 "github.com/grafana/grafana/apps/investigations/pkg/apis/investigations/v0alpha1"
|
||||
investigationapp "github.com/grafana/grafana/apps/investigations/pkg/app"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/builder"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/builder/runner"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
type InvestigationsAppProvider struct {
|
||||
app.Provider
|
||||
cfg *setting.Cfg
|
||||
}
|
||||
|
||||
func RegisterApp(
|
||||
cfg *setting.Cfg,
|
||||
) *InvestigationsAppProvider {
|
||||
provider := &InvestigationsAppProvider{
|
||||
cfg: cfg,
|
||||
}
|
||||
appCfg := &runner.AppBuilderConfig{
|
||||
OpenAPIDefGetter: investigationv0alpha1.GetOpenAPIDefinitions,
|
||||
ManagedKinds: investigationapp.GetKinds(),
|
||||
Authorizer: GetAuthorizer(),
|
||||
AllowedV0Alpha1Resources: []string{builder.AllResourcesAllowed},
|
||||
}
|
||||
provider.Provider = simple.NewAppProvider(apis.LocalManifest(), appCfg, investigationapp.New)
|
||||
return provider
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/registry/apps/annotation"
|
||||
"github.com/grafana/grafana/pkg/registry/apps/correlations"
|
||||
"github.com/grafana/grafana/pkg/registry/apps/example"
|
||||
"github.com/grafana/grafana/pkg/registry/apps/investigations"
|
||||
"github.com/grafana/grafana/pkg/registry/apps/logsdrilldown"
|
||||
"github.com/grafana/grafana/pkg/registry/apps/playlist"
|
||||
"github.com/grafana/grafana/pkg/registry/apps/plugins"
|
||||
@@ -20,6 +21,7 @@ var WireSet = wire.NewSet(
|
||||
ProvideAppInstallers,
|
||||
ProvideBuilderRunners,
|
||||
playlist.RegisterAppInstaller,
|
||||
investigations.RegisterApp,
|
||||
plugins.ProvideAppInstaller,
|
||||
shorturl.RegisterAppInstaller,
|
||||
correlations.RegisterAppInstaller,
|
||||
|
||||
Generated
+5
-2
@@ -84,6 +84,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/registry/apps/annotation"
|
||||
correlations2 "github.com/grafana/grafana/pkg/registry/apps/correlations"
|
||||
"github.com/grafana/grafana/pkg/registry/apps/example"
|
||||
"github.com/grafana/grafana/pkg/registry/apps/investigations"
|
||||
"github.com/grafana/grafana/pkg/registry/apps/logsdrilldown"
|
||||
"github.com/grafana/grafana/pkg/registry/apps/playlist"
|
||||
"github.com/grafana/grafana/pkg/registry/apps/plugins"
|
||||
@@ -847,7 +848,8 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api
|
||||
return nil, err
|
||||
}
|
||||
zanzanaReconciler := dualwrite2.ProvideZanzanaReconciler(cfg, featureToggles, zanzanaClient, sqlStore, serverLockService, folderimplService, registerer)
|
||||
appregistryService, err := appregistry.ProvideBuilderRunners(apiserverService, eventualRestConfigProvider, featureToggles, cfg)
|
||||
investigationsAppProvider := investigations.RegisterApp(cfg)
|
||||
appregistryService, err := appregistry.ProvideBuilderRunners(apiserverService, eventualRestConfigProvider, featureToggles, investigationsAppProvider, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1509,7 +1511,8 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac
|
||||
return nil, err
|
||||
}
|
||||
zanzanaReconciler := dualwrite2.ProvideZanzanaReconciler(cfg, featureToggles, zanzanaClient, sqlStore, serverLockService, folderimplService, registerer)
|
||||
appregistryService, err := appregistry.ProvideBuilderRunners(apiserverService, eventualRestConfigProvider, featureToggles, cfg)
|
||||
investigationsAppProvider := investigations.RegisterApp(cfg)
|
||||
appregistryService, err := appregistry.ProvideBuilderRunners(apiserverService, eventualRestConfigProvider, featureToggles, investigationsAppProvider, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -907,6 +907,14 @@ var (
|
||||
Owner: grafanaAlertingSquad,
|
||||
FrontendOnly: false, // changes navtree from backend
|
||||
},
|
||||
{
|
||||
Name: "alertingNavigationV2",
|
||||
Description: "Enable new grouped navigation structure for Alerting",
|
||||
Stage: FeatureStageExperimental,
|
||||
Owner: grafanaAlertingSquad,
|
||||
FrontendOnly: false, // changes navtree from backend
|
||||
Expression: "false", // Off by default
|
||||
},
|
||||
{
|
||||
Name: "pluginProxyPreserveTrailingSlash",
|
||||
Description: "Preserve plugin proxy trailing slash.",
|
||||
@@ -1299,6 +1307,13 @@ var (
|
||||
Owner: grafanaObservabilityLogsSquad,
|
||||
Expression: "true",
|
||||
},
|
||||
{
|
||||
Name: "investigationsBackend",
|
||||
Description: "Enable the investigations backend API",
|
||||
Stage: FeatureStageExperimental,
|
||||
Owner: grafanaAppPlatformSquad,
|
||||
Expression: "false",
|
||||
},
|
||||
{
|
||||
Name: "k8SFolderCounts",
|
||||
Description: "Enable folder's api server counts",
|
||||
|
||||
Generated
+2
@@ -125,6 +125,7 @@ alertingSavedSearches,experimental,@grafana/alerting-squad,false,false,true
|
||||
alertingDisableSendAlertsExternal,experimental,@grafana/alerting-squad,false,false,false
|
||||
preserveDashboardStateWhenNavigating,experimental,@grafana/dashboards-squad,false,false,false
|
||||
alertingCentralAlertHistory,experimental,@grafana/alerting-squad,false,false,false
|
||||
alertingNavigationV2,experimental,@grafana/alerting-squad,false,false,false
|
||||
pluginProxyPreserveTrailingSlash,GA,@grafana/plugins-platform-backend,false,false,false
|
||||
azureMonitorPrometheusExemplars,GA,@grafana/partner-datasources,false,false,false
|
||||
authZGRPCServer,experimental,@grafana/identity-access-team,false,false,false
|
||||
@@ -179,6 +180,7 @@ unifiedStorageSearchUI,experimental,@grafana/search-and-storage,false,false,fals
|
||||
elasticsearchCrossClusterSearch,GA,@grafana/partner-datasources,false,false,false
|
||||
unifiedHistory,experimental,@grafana/grafana-search-navigate-organise,false,false,true
|
||||
lokiLabelNamesQueryApi,GA,@grafana/observability-logs,false,false,false
|
||||
investigationsBackend,experimental,@grafana/grafana-app-platform-squad,false,false,false
|
||||
k8SFolderCounts,experimental,@grafana/search-and-storage,false,false,false
|
||||
k8SFolderMove,experimental,@grafana/search-and-storage,false,false,false
|
||||
improvedExternalSessionHandlingSAML,GA,@grafana/identity-access-team,false,false,false
|
||||
|
||||
|
Generated
+8
@@ -379,6 +379,10 @@ const (
|
||||
// Enables the new central alert history.
|
||||
FlagAlertingCentralAlertHistory = "alertingCentralAlertHistory"
|
||||
|
||||
// FlagAlertingNavigationV2
|
||||
// Enable new grouped navigation structure for Alerting
|
||||
FlagAlertingNavigationV2 = "alertingNavigationV2"
|
||||
|
||||
// FlagPluginProxyPreserveTrailingSlash
|
||||
// Preserve plugin proxy trailing slash.
|
||||
FlagPluginProxyPreserveTrailingSlash = "pluginProxyPreserveTrailingSlash"
|
||||
@@ -539,6 +543,10 @@ const (
|
||||
// Defaults to using the Loki `/labels` API instead of `/series`
|
||||
FlagLokiLabelNamesQueryApi = "lokiLabelNamesQueryApi"
|
||||
|
||||
// FlagInvestigationsBackend
|
||||
// Enable the investigations backend API
|
||||
FlagInvestigationsBackend = "investigationsBackend"
|
||||
|
||||
// FlagK8SFolderCounts
|
||||
// Enable folder's api server counts
|
||||
FlagK8SFolderCounts = "k8SFolderCounts"
|
||||
|
||||
+14
-2
@@ -348,6 +348,19 @@
|
||||
"expression": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "alertingNavigationV2",
|
||||
"resourceVersion": "1767827323622",
|
||||
"creationTimestamp": "2026-01-07T23:08:43Z"
|
||||
},
|
||||
"spec": {
|
||||
"description": "Enable new grouped navigation structure for Alerting",
|
||||
"stage": "experimental",
|
||||
"codeowner": "@grafana/alerting-squad",
|
||||
"expression": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "alertingNotificationHistory",
|
||||
@@ -1793,8 +1806,7 @@
|
||||
"metadata": {
|
||||
"name": "investigationsBackend",
|
||||
"resourceVersion": "1764664939750",
|
||||
"creationTimestamp": "2024-12-18T08:31:03Z",
|
||||
"deletionTimestamp": "2025-12-16T16:06:24Z"
|
||||
"creationTimestamp": "2024-12-18T08:31:03Z"
|
||||
},
|
||||
"spec": {
|
||||
"description": "Enable the investigations backend API",
|
||||
|
||||
@@ -433,6 +433,214 @@ func (s *ServiceImpl) buildDashboardNavLinks(c *contextmodel.ReqContext) []*navt
|
||||
}
|
||||
|
||||
func (s *ServiceImpl) buildAlertNavLinks(c *contextmodel.ReqContext) *navtree.NavLink {
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if !s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingNavigationV2) {
|
||||
return s.buildAlertNavLinksLegacy(c)
|
||||
}
|
||||
|
||||
// V2 Navigation - New grouped structure
|
||||
hasAccess := ac.HasAccess(s.accessControl, c)
|
||||
var alertChildNavs []*navtree.NavLink
|
||||
|
||||
// 1. Alert activity (parent with tabs: Alerts, Active notifications)
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
var alertActivityChildren []*navtree.NavLink
|
||||
if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingTriage) {
|
||||
// Alerts tab
|
||||
if hasAccess(ac.EvalAny(ac.EvalPermission(ac.ActionAlertingRuleRead), ac.EvalPermission(ac.ActionAlertingRuleExternalRead))) {
|
||||
alertActivityChildren = append(alertActivityChildren, &navtree.NavLink{
|
||||
Text: "Alerts", SubTitle: "Visualize active and pending alerts", Id: "alert-activity-alerts", Url: s.cfg.AppSubURL + "/alerting/alerts", Icon: "bell",
|
||||
})
|
||||
}
|
||||
// Active notifications tab
|
||||
if hasAccess(ac.EvalAny(ac.EvalPermission(ac.ActionAlertingInstanceRead), ac.EvalPermission(ac.ActionAlertingInstancesExternalRead))) {
|
||||
alertActivityChildren = append(alertActivityChildren, &navtree.NavLink{
|
||||
Text: "Active notifications", SubTitle: "See grouped alerts with active notifications", Id: "alert-activity-groups", Url: s.cfg.AppSubURL + "/alerting/groups", Icon: "layer-group",
|
||||
})
|
||||
}
|
||||
if len(alertActivityChildren) > 0 {
|
||||
alertChildNavs = append(alertChildNavs, &navtree.NavLink{
|
||||
Text: "Alert activity",
|
||||
SubTitle: "Visualize active and pending alerts",
|
||||
Id: "alert-activity",
|
||||
Url: s.cfg.AppSubURL + "/alerting/alerts",
|
||||
Icon: "bell",
|
||||
IsNew: true,
|
||||
Children: alertActivityChildren,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Alert rules (parent with tabs: Alert rules, Recently deleted)
|
||||
var alertRulesChildren []*navtree.NavLink
|
||||
if hasAccess(ac.EvalAny(ac.EvalPermission(ac.ActionAlertingRuleRead), ac.EvalPermission(ac.ActionAlertingRuleExternalRead))) {
|
||||
alertRulesChildren = append(alertRulesChildren, &navtree.NavLink{
|
||||
Text: "Alert rules", SubTitle: "Rules that determine whether an alert will fire", Id: "alert-rules-list", Url: s.cfg.AppSubURL + "/alerting/list", Icon: "list-ul",
|
||||
})
|
||||
}
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if c.GetOrgRole() == org.RoleAdmin && s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertRuleRestore) && s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingRuleRecoverDeleted) {
|
||||
alertRulesChildren = append(alertRulesChildren, &navtree.NavLink{
|
||||
Text: "Recently deleted",
|
||||
SubTitle: "Any items listed here for more than 30 days will be automatically deleted.",
|
||||
Id: "alert-rules-recently-deleted",
|
||||
Url: s.cfg.AppSubURL + "/alerting/recently-deleted",
|
||||
})
|
||||
}
|
||||
if len(alertRulesChildren) > 0 {
|
||||
alertChildNavs = append(alertChildNavs, &navtree.NavLink{
|
||||
Text: "Alert rules",
|
||||
SubTitle: "Manage alert and recording rules",
|
||||
Id: "alert-rules",
|
||||
Url: s.cfg.AppSubURL + "/alerting/list",
|
||||
Icon: "list-ul",
|
||||
Children: alertRulesChildren,
|
||||
})
|
||||
}
|
||||
|
||||
// 3. Notification configuration (parent with tabs: Contact points, Notification policies, Templates, Time intervals)
|
||||
var notificationConfigChildren []*navtree.NavLink
|
||||
|
||||
contactPointsPerms := []ac.Evaluator{
|
||||
ac.EvalPermission(ac.ActionAlertingNotificationsRead),
|
||||
ac.EvalPermission(ac.ActionAlertingNotificationsExternalRead),
|
||||
ac.EvalPermission(ac.ActionAlertingReceiversRead),
|
||||
ac.EvalPermission(ac.ActionAlertingReceiversReadSecrets),
|
||||
ac.EvalPermission(ac.ActionAlertingReceiversCreate),
|
||||
ac.EvalPermission(ac.ActionAlertingNotificationsTemplatesRead),
|
||||
ac.EvalPermission(ac.ActionAlertingNotificationsTemplatesWrite),
|
||||
ac.EvalPermission(ac.ActionAlertingNotificationsTemplatesDelete),
|
||||
}
|
||||
|
||||
if hasAccess(ac.EvalAny(contactPointsPerms...)) {
|
||||
notificationConfigChildren = append(notificationConfigChildren, &navtree.NavLink{
|
||||
Text: "Contact points", SubTitle: "Choose how to notify your contact points when an alert instance fires", Id: "notification-config-contact-points", Url: s.cfg.AppSubURL + "/alerting/notifications", Icon: "comment-alt-share",
|
||||
})
|
||||
}
|
||||
|
||||
if hasAccess(ac.EvalAny(
|
||||
ac.EvalPermission(ac.ActionAlertingNotificationsRead),
|
||||
ac.EvalPermission(ac.ActionAlertingNotificationsExternalRead),
|
||||
ac.EvalPermission(ac.ActionAlertingRoutesRead),
|
||||
ac.EvalPermission(ac.ActionAlertingRoutesWrite),
|
||||
ac.EvalPermission(ac.ActionAlertingNotificationsTimeIntervalsRead),
|
||||
ac.EvalPermission(ac.ActionAlertingNotificationsTimeIntervalsWrite),
|
||||
)) {
|
||||
notificationConfigChildren = append(notificationConfigChildren, &navtree.NavLink{
|
||||
Text: "Notification policies", SubTitle: "Determine how alerts are routed to contact points", Id: "notification-config-policies", Url: s.cfg.AppSubURL + "/alerting/routes", Icon: "sitemap",
|
||||
})
|
||||
}
|
||||
|
||||
// Templates
|
||||
if hasAccess(ac.EvalAny(contactPointsPerms...)) {
|
||||
notificationConfigChildren = append(notificationConfigChildren, &navtree.NavLink{
|
||||
Text: "Notification templates", SubTitle: "Manage notification templates", Id: "notification-config-templates", Url: s.cfg.AppSubURL + "/alerting/notifications/templates", Icon: "file-alt",
|
||||
})
|
||||
}
|
||||
|
||||
// Time intervals
|
||||
if hasAccess(ac.EvalAny(
|
||||
ac.EvalPermission(ac.ActionAlertingNotificationsRead),
|
||||
ac.EvalPermission(ac.ActionAlertingNotificationsExternalRead),
|
||||
ac.EvalPermission(ac.ActionAlertingRoutesRead),
|
||||
ac.EvalPermission(ac.ActionAlertingRoutesWrite),
|
||||
ac.EvalPermission(ac.ActionAlertingNotificationsTimeIntervalsRead),
|
||||
ac.EvalPermission(ac.ActionAlertingNotificationsTimeIntervalsWrite),
|
||||
)) {
|
||||
notificationConfigChildren = append(notificationConfigChildren, &navtree.NavLink{
|
||||
Text: "Time intervals", SubTitle: "Configure time intervals for notification policies", Id: "notification-config-time-intervals", Url: s.cfg.AppSubURL + "/alerting/routes?tab=time_intervals", Icon: "clock-nine",
|
||||
})
|
||||
}
|
||||
|
||||
if len(notificationConfigChildren) > 0 {
|
||||
alertChildNavs = append(alertChildNavs, &navtree.NavLink{
|
||||
Text: "Notification configuration",
|
||||
SubTitle: "Configure how alerts are notified",
|
||||
Id: "notification-config",
|
||||
Url: s.cfg.AppSubURL + "/alerting/notifications",
|
||||
Icon: "cog",
|
||||
Children: notificationConfigChildren,
|
||||
})
|
||||
}
|
||||
|
||||
// 4. Insights (parent with tabs: System Insights, Alert state history)
|
||||
var insightsChildren []*navtree.NavLink
|
||||
|
||||
// System Insights
|
||||
if hasAccess(ac.EvalAny(ac.EvalPermission(ac.ActionAlertingRuleRead), ac.EvalPermission(ac.ActionAlertingRuleExternalRead))) {
|
||||
insightsChildren = append(insightsChildren, &navtree.NavLink{
|
||||
Text: "System Insights", SubTitle: "View system insights and analytics", Id: "insights-system", Url: s.cfg.AppSubURL + "/alerting/insights", Icon: "chart-line",
|
||||
})
|
||||
}
|
||||
|
||||
// Alert state history
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingCentralAlertHistory) {
|
||||
if hasAccess(ac.EvalAny(ac.EvalPermission(ac.ActionAlertingRuleRead))) {
|
||||
insightsChildren = append(insightsChildren, &navtree.NavLink{
|
||||
Text: "Alert state history",
|
||||
SubTitle: "View a history of all alert events generated by your Grafana-managed alert rules. All alert events are displayed regardless of whether silences or mute timings are set.",
|
||||
Id: "insights-history",
|
||||
Url: s.cfg.AppSubURL + "/alerting/history",
|
||||
Icon: "history",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(insightsChildren) > 0 {
|
||||
alertChildNavs = append(alertChildNavs, &navtree.NavLink{
|
||||
Text: "Insights",
|
||||
SubTitle: "Analytics and history for alerting",
|
||||
Id: "insights",
|
||||
Url: s.cfg.AppSubURL + "/alerting/insights",
|
||||
Icon: "chart-line",
|
||||
Children: insightsChildren,
|
||||
})
|
||||
}
|
||||
|
||||
// 5. Settings (parent with tab: Settings)
|
||||
if c.GetOrgRole() == org.RoleAdmin {
|
||||
settingsChildren := []*navtree.NavLink{
|
||||
{
|
||||
Text: "Settings", Id: "alerting-admin", Url: s.cfg.AppSubURL + "/alerting/admin", Icon: "cog",
|
||||
},
|
||||
}
|
||||
alertChildNavs = append(alertChildNavs, &navtree.NavLink{
|
||||
Text: "Settings",
|
||||
SubTitle: "Alerting configuration and administration",
|
||||
Id: "alerting-settings",
|
||||
Url: s.cfg.AppSubURL + "/alerting/admin",
|
||||
Icon: "cog",
|
||||
Children: settingsChildren,
|
||||
})
|
||||
}
|
||||
|
||||
// Create alert rule (hidden from tabs)
|
||||
if hasAccess(ac.EvalAny(ac.EvalPermission(ac.ActionAlertingRuleCreate), ac.EvalPermission(ac.ActionAlertingRuleExternalWrite))) {
|
||||
alertChildNavs = append(alertChildNavs, &navtree.NavLink{
|
||||
Text: "Create alert rule", SubTitle: "Create an alert rule", Id: "alert",
|
||||
Icon: "plus", Url: s.cfg.AppSubURL + "/alerting/new", HideFromTabs: true, IsCreateAction: true,
|
||||
})
|
||||
}
|
||||
|
||||
if len(alertChildNavs) > 0 {
|
||||
var alertNav = navtree.NavLink{
|
||||
Text: "Alerting",
|
||||
SubTitle: "Learn about problems in your systems moments after they occur",
|
||||
Id: navtree.NavIDAlerting,
|
||||
Icon: "bell",
|
||||
Children: alertChildNavs,
|
||||
SortWeight: navtree.WeightAlerting,
|
||||
Url: s.cfg.AppSubURL + "/alerting",
|
||||
}
|
||||
|
||||
return &alertNav
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ServiceImpl) buildAlertNavLinksLegacy(c *contextmodel.ReqContext) *navtree.NavLink {
|
||||
hasAccess := ac.HasAccess(s.accessControl, c)
|
||||
var alertChildNavs []*navtree.NavLink
|
||||
|
||||
@@ -440,7 +648,7 @@ func (s *ServiceImpl) buildAlertNavLinks(c *contextmodel.ReqContext) *navtree.Na
|
||||
if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingTriage) {
|
||||
if hasAccess(ac.EvalAny(ac.EvalPermission(ac.ActionAlertingRuleRead), ac.EvalPermission(ac.ActionAlertingRuleExternalRead))) {
|
||||
alertChildNavs = append(alertChildNavs, &navtree.NavLink{
|
||||
Text: "Alerts", SubTitle: "Visualize active and pending alerts", Id: "alert-alerts", Url: s.cfg.AppSubURL + "/alerting/alerts", Icon: "bell", IsNew: true,
|
||||
Text: "Alert activity", SubTitle: "Visualize active and pending alerts", Id: "alert-alerts", Url: s.cfg.AppSubURL + "/alerting/alerts", Icon: "bell", IsNew: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
package navtreeimpl
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/navtree"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
|
||||
// Test fixtures
|
||||
func setupTestContext() *contextmodel.ReqContext {
|
||||
httpReq, _ := http.NewRequest(http.MethodGet, "", nil)
|
||||
return &contextmodel.ReqContext{
|
||||
SignedInUser: &user.SignedInUser{
|
||||
UserID: 1,
|
||||
OrgID: 1,
|
||||
OrgRole: org.RoleAdmin,
|
||||
},
|
||||
Context: &web.Context{Req: httpReq},
|
||||
}
|
||||
}
|
||||
|
||||
func setupTestService(permissions []ac.Permission, featureFlags ...string) ServiceImpl {
|
||||
// Convert string slice to []any for WithFeatures
|
||||
flags := make([]any, len(featureFlags))
|
||||
for i, flag := range featureFlags {
|
||||
flags[i] = flag
|
||||
}
|
||||
return ServiceImpl{
|
||||
log: log.New("navtree"),
|
||||
cfg: setting.NewCfg(),
|
||||
accessControl: accesscontrolmock.New().WithPermissions(permissions),
|
||||
features: featuremgmt.WithFeatures(flags...),
|
||||
}
|
||||
}
|
||||
|
||||
func fullPermissions() []ac.Permission {
|
||||
return []ac.Permission{
|
||||
{Action: ac.ActionAlertingRuleRead, Scope: "*"},
|
||||
{Action: ac.ActionAlertingNotificationsRead, Scope: "*"},
|
||||
{Action: ac.ActionAlertingRoutesRead, Scope: "*"},
|
||||
{Action: ac.ActionAlertingInstanceRead, Scope: "*"},
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to find a nav link by ID
|
||||
func findNavLink(navLink *navtree.NavLink, id string) *navtree.NavLink {
|
||||
if navLink == nil {
|
||||
return nil
|
||||
}
|
||||
if navLink.Id == id {
|
||||
return navLink
|
||||
}
|
||||
for _, child := range navLink.Children {
|
||||
if found := findNavLink(child, id); found != nil {
|
||||
return found
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helper to check if a nav link has a child with given ID
|
||||
func hasChildWithId(parent *navtree.NavLink, childId string) bool {
|
||||
if parent == nil {
|
||||
return false
|
||||
}
|
||||
for _, child := range parent.Children {
|
||||
if child.Id == childId {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func TestBuildAlertNavLinks_FeatureToggle(t *testing.T) {
|
||||
reqCtx := setupTestContext()
|
||||
permissions := fullPermissions()
|
||||
|
||||
t.Run("Should use legacy navigation when flag is off", func(t *testing.T) {
|
||||
service := setupTestService(permissions) // No feature flags
|
||||
|
||||
navLink := service.buildAlertNavLinks(reqCtx)
|
||||
require.NotNil(t, navLink)
|
||||
require.Equal(t, "Alerting", navLink.Text)
|
||||
require.Equal(t, navtree.NavIDAlerting, navLink.Id)
|
||||
|
||||
// Legacy structure: flat children without nested items
|
||||
require.NotEmpty(t, navLink.Children)
|
||||
alertList := findNavLink(navLink, "alert-list")
|
||||
receivers := findNavLink(navLink, "receivers")
|
||||
|
||||
require.NotNil(t, alertList, "Should have alert-list in legacy navigation")
|
||||
require.NotNil(t, receivers, "Should have receivers in legacy navigation")
|
||||
require.Empty(t, alertList.Children, "Legacy items should not have nested children")
|
||||
require.Empty(t, receivers.Children, "Legacy items should not have nested children")
|
||||
})
|
||||
|
||||
t.Run("Should use V2 navigation when flag is on", func(t *testing.T) {
|
||||
service := setupTestService(permissions, "alertingNavigationV2")
|
||||
|
||||
navLink := service.buildAlertNavLinks(reqCtx)
|
||||
require.NotNil(t, navLink)
|
||||
require.Equal(t, "Alerting", navLink.Text)
|
||||
require.Equal(t, navtree.NavIDAlerting, navLink.Id)
|
||||
|
||||
// V2 structure: grouped parents with nested children
|
||||
require.NotEmpty(t, navLink.Children)
|
||||
|
||||
// Verify all expected parent items exist with children
|
||||
expectedParents := []string{"alert-rules", "notification-config", "insights", "alerting-settings"}
|
||||
for _, parentId := range expectedParents {
|
||||
parent := findNavLink(navLink, parentId)
|
||||
require.NotNil(t, parent, "Should have %s parent in V2 navigation", parentId)
|
||||
require.NotEmpty(t, parent.Children, "V2 parent %s should have children", parentId)
|
||||
}
|
||||
|
||||
// Verify alert-rules has expected tab
|
||||
alertRules := findNavLink(navLink, "alert-rules")
|
||||
require.True(t, hasChildWithId(alertRules, "alert-rules-list"), "Should have alert-rules-list tab")
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuildAlertNavLinks_Legacy(t *testing.T) {
|
||||
reqCtx := setupTestContext()
|
||||
|
||||
t.Run("Should include all expected items in legacy navigation", func(t *testing.T) {
|
||||
service := setupTestService(fullPermissions())
|
||||
navLink := service.buildAlertNavLinksLegacy(reqCtx)
|
||||
require.NotNil(t, navLink)
|
||||
|
||||
expectedIds := []string{"alert-list", "receivers", "am-routes", "alerting-admin"}
|
||||
for _, expectedId := range expectedIds {
|
||||
require.NotNil(t, findNavLink(navLink, expectedId), "Should have %s in legacy navigation", expectedId)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Should respect permissions in legacy navigation", func(t *testing.T) {
|
||||
limitedPermissions := []ac.Permission{
|
||||
{Action: ac.ActionAlertingRuleRead, Scope: "*"},
|
||||
}
|
||||
limitedService := setupTestService(limitedPermissions)
|
||||
|
||||
navLink := limitedService.buildAlertNavLinksLegacy(reqCtx)
|
||||
require.NotNil(t, navLink)
|
||||
|
||||
require.NotNil(t, findNavLink(navLink, "alert-list"), "Should have alert rules with read permission")
|
||||
require.Nil(t, findNavLink(navLink, "receivers"), "Should not have contact points without notification permissions")
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuildAlertNavLinks_V2(t *testing.T) {
|
||||
reqCtx := setupTestContext()
|
||||
allFeatureFlags := []string{"alertingNavigationV2", "alertingTriage", "alertingCentralAlertHistory", "alertRuleRestore", "alertingRuleRecoverDeleted"}
|
||||
service := setupTestService(fullPermissions(), allFeatureFlags...)
|
||||
|
||||
t.Run("Should have correct parent structure in V2 navigation", func(t *testing.T) {
|
||||
navLink := service.buildAlertNavLinks(reqCtx)
|
||||
require.NotNil(t, navLink)
|
||||
require.NotEmpty(t, navLink.Children)
|
||||
|
||||
// Verify all parent items exist with children
|
||||
parentIds := []string{"alert-rules", "notification-config", "insights", "alerting-settings"}
|
||||
for _, parentId := range parentIds {
|
||||
parent := findNavLink(navLink, parentId)
|
||||
require.NotNil(t, parent, "Should have parent %s in V2 navigation", parentId)
|
||||
require.NotEmpty(t, parent.Children, "Parent %s should have children", parentId)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Should have correct tabs under each parent", func(t *testing.T) {
|
||||
navLink := service.buildAlertNavLinks(reqCtx)
|
||||
require.NotNil(t, navLink)
|
||||
|
||||
// Table-driven test for tab verification
|
||||
tests := []struct {
|
||||
parentId string
|
||||
expectedTabs []string
|
||||
}{
|
||||
{"alert-rules", []string{"alert-rules-list", "alert-rules-recently-deleted"}},
|
||||
{"notification-config", []string{"notification-config-contact-points", "notification-config-policies", "notification-config-templates", "notification-config-time-intervals"}},
|
||||
{"insights", []string{"insights-system", "insights-history"}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
parent := findNavLink(navLink, tt.parentId)
|
||||
require.NotNil(t, parent, "Should have %s parent", tt.parentId)
|
||||
|
||||
for _, expectedTab := range tt.expectedTabs {
|
||||
require.True(t, hasChildWithId(parent, expectedTab), "Parent %s should have tab %s", tt.parentId, expectedTab)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Should respect permissions in V2 navigation", func(t *testing.T) {
|
||||
limitedPermissions := []ac.Permission{
|
||||
{Action: ac.ActionAlertingRuleRead, Scope: "*"},
|
||||
}
|
||||
limitedService := setupTestService(limitedPermissions, "alertingNavigationV2")
|
||||
|
||||
navLink := limitedService.buildAlertNavLinks(reqCtx)
|
||||
require.NotNil(t, navLink)
|
||||
|
||||
// Should not have notification-config without notification permissions
|
||||
require.Nil(t, findNavLink(navLink, "notification-config"), "Should not have notification-config without permissions")
|
||||
})
|
||||
|
||||
t.Run("Should exclude future items from V2 navigation", func(t *testing.T) {
|
||||
navLink := service.buildAlertNavLinks(reqCtx)
|
||||
require.NotNil(t, navLink)
|
||||
|
||||
// Verify future items are not present
|
||||
futureIds := []string{
|
||||
"alert-rules-recording-rules",
|
||||
"alert-rules-evaluation-chains",
|
||||
"insights-alert-optimizer",
|
||||
"insights-notification-history",
|
||||
}
|
||||
|
||||
for _, futureId := range futureIds {
|
||||
require.Nil(t, findNavLink(navLink, futureId), "Should not have future item %s", futureId)
|
||||
}
|
||||
})
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -30,6 +30,7 @@ func TestIntegrationOpenAPIs(t *testing.T) {
|
||||
EnableFeatureToggles: []string{
|
||||
featuremgmt.FlagQueryService, // Query Library
|
||||
featuremgmt.FlagProvisioning,
|
||||
featuremgmt.FlagInvestigationsBackend,
|
||||
featuremgmt.FlagGrafanaAdvisor,
|
||||
featuremgmt.FlagKubernetesAlertingRules,
|
||||
featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, // all datasources
|
||||
@@ -96,6 +97,9 @@ func TestIntegrationOpenAPIs(t *testing.T) {
|
||||
}, {
|
||||
Group: "iam.grafana.app",
|
||||
Version: "v0alpha1",
|
||||
}, {
|
||||
Group: "investigations.grafana.app",
|
||||
Version: "v0alpha1",
|
||||
}, {
|
||||
Group: "advisor.grafana.app",
|
||||
Version: "v0alpha1",
|
||||
|
||||
+6
-6
@@ -26,7 +26,7 @@ const mockDifferentComponent = {
|
||||
} as ExtensionInfo;
|
||||
|
||||
const mockPluginMeta = {
|
||||
pluginId: 'grafana-assistant-app',
|
||||
pluginId: 'grafana-investigations-app',
|
||||
addedComponents: [mockComponent, mockDifferentComponent],
|
||||
addedLinks: [],
|
||||
};
|
||||
@@ -187,7 +187,7 @@ describe('ExtensionSidebarProvider', () => {
|
||||
|
||||
it('should only include permitted plugins in available components', () => {
|
||||
const permittedPluginMeta = {
|
||||
pluginId: 'grafana-assistant-app',
|
||||
pluginId: 'grafana-investigations-app',
|
||||
addedComponents: [mockComponent],
|
||||
addedLinks: [],
|
||||
};
|
||||
@@ -256,7 +256,7 @@ describe('ExtensionSidebarProvider', () => {
|
||||
// Call it directly with the test event
|
||||
subscriberFn(
|
||||
new OpenExtensionSidebarEvent({
|
||||
pluginId: 'grafana-assistant-app',
|
||||
pluginId: 'grafana-investigations-app',
|
||||
componentTitle: 'Test Component',
|
||||
props: { testProp: 'test value' },
|
||||
})
|
||||
@@ -266,7 +266,7 @@ describe('ExtensionSidebarProvider', () => {
|
||||
expect(screen.getByTestId('is-open')).toHaveTextContent('true');
|
||||
expect(screen.getByTestId('props')).toHaveTextContent('{"testProp":"test value"}');
|
||||
const expectedComponentId = JSON.stringify({
|
||||
pluginId: 'grafana-assistant-app',
|
||||
pluginId: 'grafana-investigations-app',
|
||||
componentTitle: 'Test Component',
|
||||
});
|
||||
expect(screen.getByTestId('docked-component-id')).toHaveTextContent(expectedComponentId);
|
||||
@@ -381,7 +381,7 @@ describe('ExtensionSidebarProvider', () => {
|
||||
|
||||
subscriberFn(
|
||||
new ToggleExtensionSidebarEvent({
|
||||
pluginId: 'grafana-assistant-app',
|
||||
pluginId: 'grafana-investigations-app',
|
||||
componentTitle: 'Test Component',
|
||||
props: { testProp: 'test value' },
|
||||
})
|
||||
@@ -392,7 +392,7 @@ describe('ExtensionSidebarProvider', () => {
|
||||
expect(screen.getByTestId('is-open')).toHaveTextContent('true');
|
||||
expect(screen.getByTestId('props')).toHaveTextContent('{"testProp":"test value"}');
|
||||
const expectedComponentId = JSON.stringify({
|
||||
pluginId: 'grafana-assistant-app',
|
||||
pluginId: 'grafana-investigations-app',
|
||||
componentTitle: 'Test Component',
|
||||
});
|
||||
expect(screen.getByTestId('docked-component-id')).toHaveTextContent(expectedComponentId);
|
||||
|
||||
@@ -11,6 +11,7 @@ import { DEFAULT_EXTENSION_SIDEBAR_WIDTH, MAX_EXTENSION_SIDEBAR_WIDTH } from './
|
||||
export const EXTENSION_SIDEBAR_DOCKED_LOCAL_STORAGE_KEY = 'grafana.navigation.extensionSidebarDocked';
|
||||
export const EXTENSION_SIDEBAR_WIDTH_LOCAL_STORAGE_KEY = 'grafana.navigation.extensionSidebarWidth';
|
||||
const PERMITTED_EXTENSION_SIDEBAR_PLUGINS = [
|
||||
'grafana-investigations-app',
|
||||
'grafana-assistant-app',
|
||||
'grafana-dash-app',
|
||||
// The docs plugin ID is transitioning from grafana-grafanadocsplugin-app to grafana-pathfinder-app.
|
||||
|
||||
+11
-11
@@ -44,7 +44,7 @@ const mockComponent = {
|
||||
};
|
||||
|
||||
const mockPluginMeta = {
|
||||
pluginId: 'grafana-assistant-app',
|
||||
pluginId: 'grafana-investigations-app',
|
||||
addedComponents: [mockComponent],
|
||||
};
|
||||
|
||||
@@ -109,7 +109,7 @@ describe('ExtensionToolbarItem', () => {
|
||||
|
||||
it('should render a dropdown menu when multiple components are available', async () => {
|
||||
const multipleComponentsMeta = {
|
||||
pluginId: 'grafana-assistant-app',
|
||||
pluginId: 'grafana-investigations-app',
|
||||
addedComponents: [
|
||||
{ ...mockComponent, title: 'Component 1' },
|
||||
{ ...mockComponent, title: 'Component 2' },
|
||||
@@ -141,7 +141,7 @@ describe('ExtensionToolbarItem', () => {
|
||||
|
||||
it('should show menu items when clicking the dropdown button', async () => {
|
||||
const multipleComponentsMeta = {
|
||||
pluginId: 'grafana-assistant-app',
|
||||
pluginId: 'grafana-investigations-app',
|
||||
addedComponents: [
|
||||
{ ...mockComponent, title: 'Component 1' },
|
||||
{ ...mockComponent, title: 'Component 2' },
|
||||
@@ -165,7 +165,7 @@ describe('ExtensionToolbarItem', () => {
|
||||
|
||||
it('should toggle the sidebar when clicking a menu item', async () => {
|
||||
const multipleComponentsMeta = {
|
||||
pluginId: 'grafana-assistant-app',
|
||||
pluginId: 'grafana-investigations-app',
|
||||
addedComponents: [
|
||||
{ ...mockComponent, title: 'Component 1' },
|
||||
{ ...mockComponent, title: 'Component 2' },
|
||||
@@ -192,7 +192,7 @@ describe('ExtensionToolbarItem', () => {
|
||||
|
||||
it('should close the sidebar when clicking an active menu item', async () => {
|
||||
const multipleComponentsMeta = {
|
||||
pluginId: 'grafana-assistant-app',
|
||||
pluginId: 'grafana-investigations-app',
|
||||
addedComponents: [
|
||||
{ ...mockComponent, title: 'Component 1' },
|
||||
{ ...mockComponent, title: 'Component 2' },
|
||||
@@ -218,13 +218,13 @@ describe('ExtensionToolbarItem', () => {
|
||||
|
||||
it('should render individual buttons when multiple plugins are available', async () => {
|
||||
const plugin1Meta = {
|
||||
pluginId: 'grafana-assistant-app',
|
||||
addedComponents: [{ ...mockComponent, title: 'Assistant' }],
|
||||
pluginId: 'grafana-investigations-app',
|
||||
addedComponents: [{ ...mockComponent, title: 'Investigations' }],
|
||||
};
|
||||
|
||||
const plugin2Meta = {
|
||||
pluginId: 'grafana-dash-app',
|
||||
addedComponents: [{ ...mockComponent, title: 'Dash' }],
|
||||
pluginId: 'grafana-assistant-app',
|
||||
addedComponents: [{ ...mockComponent, title: 'Assistant' }],
|
||||
};
|
||||
|
||||
(usePluginLinks as jest.Mock).mockReturnValue({
|
||||
@@ -249,7 +249,7 @@ describe('ExtensionToolbarItem', () => {
|
||||
expect(buttons).toHaveLength(2);
|
||||
|
||||
// Each button should have the correct title
|
||||
expect(buttons[0]).toHaveAttribute('aria-label', 'Open Assistant');
|
||||
expect(buttons[1]).toHaveAttribute('aria-label', 'Open Dash');
|
||||
expect(buttons[0]).toHaveAttribute('aria-label', 'Open Investigations');
|
||||
expect(buttons[1]).toHaveAttribute('aria-label', 'Open Assistant');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,6 +17,8 @@ function getPluginIcon(pluginId?: string): string {
|
||||
case 'grafana-grafanadocsplugin-app':
|
||||
case 'grafana-pathfinder-app':
|
||||
return 'book';
|
||||
case 'grafana-investigations-app':
|
||||
return 'eye';
|
||||
default:
|
||||
return 'ai-sparkle';
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { reportInteraction } from '@grafana/runtime';
|
||||
import { ScrollContainer, useStyles2 } from '@grafana/ui';
|
||||
import { useGrafana } from 'app/core/context/GrafanaContext';
|
||||
import { setBookmark } from 'app/core/reducers/navBarTree';
|
||||
import { shouldUseAlertingNavigationV2 } from 'app/features/alerting/unified/featureToggles';
|
||||
import { useDispatch, useSelector } from 'app/types/store';
|
||||
|
||||
import { MegaMenuExtensionPoint } from './MegaMenuExtensionPoint';
|
||||
@@ -37,9 +38,25 @@ export const MegaMenu = memo(
|
||||
const pinnedItems = usePinnedItems();
|
||||
|
||||
// Remove profile + help from tree
|
||||
// For Alerting V2 navigation, flatten the sidebar to show only top-level items (hide nested children/tabs)
|
||||
const useV2Nav = shouldUseAlertingNavigationV2();
|
||||
const navItems = navTree
|
||||
.filter((item) => item.id !== 'profile' && item.id !== 'help')
|
||||
.map((item) => enrichWithInteractionTracking(item, state.megaMenuDocked));
|
||||
.map((item) => {
|
||||
const enriched = enrichWithInteractionTracking(item, state.megaMenuDocked);
|
||||
// If this is Alerting section and V2 navigation is enabled, flatten children for sidebar display
|
||||
// Children are still available in navIndex for breadcrumbs and page navigation
|
||||
if (useV2Nav && item.id === 'alerting' && enriched.children) {
|
||||
return {
|
||||
...enriched,
|
||||
children: enriched.children.map((child) => ({
|
||||
...child,
|
||||
children: undefined, // Remove nested children from sidebar, but keep them for page navigation
|
||||
})),
|
||||
};
|
||||
}
|
||||
return enriched;
|
||||
});
|
||||
|
||||
const bookmarksItem = navItems.find((item) => item.id === 'bookmarks');
|
||||
if (bookmarksItem) {
|
||||
|
||||
@@ -35,11 +35,18 @@ export function buildBreadcrumbs(sectionNav: NavModelItem, pageNav?: NavModelIte
|
||||
|
||||
if (shouldAddCrumb) {
|
||||
const activeChildIndex = node.children?.findIndex((child) => child.active) ?? -1;
|
||||
// Add tab to breadcrumbs if it's not the first active child
|
||||
if (activeChildIndex > 0) {
|
||||
// Add active tab to breadcrumbs if it exists and its URL is different from the node's URL
|
||||
// This ensures tabs show in breadcrumbs (including the first tab) while preventing duplication
|
||||
if (activeChildIndex >= 0) {
|
||||
const activeChild = node.children?.[activeChildIndex];
|
||||
if (activeChild) {
|
||||
crumbs.unshift({ text: activeChild.text, href: activeChild.url ?? '' });
|
||||
// Only add the active child if its URL doesn't match the node's URL
|
||||
// This prevents duplication when the pageNav is the active tab
|
||||
const nodeUrl = node.url?.split('?')[0] ?? '';
|
||||
const childUrl = activeChild.url?.split('?')[0] ?? '';
|
||||
if (nodeUrl !== childUrl) {
|
||||
crumbs.unshift({ text: activeChild.text, href: activeChild.url ?? '' });
|
||||
}
|
||||
}
|
||||
}
|
||||
crumbs.unshift({ text: node.text, href: node.url ?? '' });
|
||||
|
||||
@@ -56,6 +56,17 @@ export function getAlertingRoutes(cfg = config): RouteDescriptor[] {
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/alerting/time-intervals',
|
||||
roles: evaluateAccess([
|
||||
AccessControlAction.AlertingNotificationsRead,
|
||||
AccessControlAction.AlertingNotificationsExternalRead,
|
||||
...PERMISSIONS_TIME_INTERVALS_READ,
|
||||
]),
|
||||
component: importAlertingComponent(
|
||||
() => import(/* webpackChunkName: "TimeIntervalsPage" */ 'app/features/alerting/unified/TimeIntervalsPage')
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/alerting/routes/mute-timing/new',
|
||||
roles: evaluateAccess([
|
||||
@@ -212,6 +223,13 @@ export function getAlertingRoutes(cfg = config): RouteDescriptor[] {
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/alerting/insights',
|
||||
roles: evaluateAccess([AccessControlAction.AlertingRuleRead, AccessControlAction.AlertingRuleExternalRead]),
|
||||
component: importAlertingComponent(
|
||||
() => import(/* webpackChunkName: "InsightsPage" */ 'app/features/alerting/unified/insights/InsightsPage')
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/alerting/recently-deleted/',
|
||||
roles: () => ['Admin'],
|
||||
|
||||
@@ -14,6 +14,7 @@ import { AlertGroupFilter } from './components/alert-groups/AlertGroupFilter';
|
||||
import { useFilteredAmGroups } from './hooks/useFilteredAmGroups';
|
||||
import { useGroupedAlerts } from './hooks/useGroupedAlerts';
|
||||
import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector';
|
||||
import { useAlertActivityNav } from './navigation/useAlertActivityNav';
|
||||
import { useAlertmanager } from './state/AlertmanagerContext';
|
||||
import { fetchAlertGroupsAction } from './state/actions';
|
||||
import { NOTIFICATIONS_POLL_INTERVAL_MS } from './utils/constants';
|
||||
@@ -113,8 +114,9 @@ const AlertGroups = () => {
|
||||
};
|
||||
|
||||
function AlertGroupsPage() {
|
||||
const { navId, pageNav } = useAlertActivityNav();
|
||||
return (
|
||||
<AlertmanagerPageWrapper navId="groups" accessType="instance">
|
||||
<AlertmanagerPageWrapper navId={navId || 'groups'} pageNav={pageNav} accessType="instance">
|
||||
<AlertGroups />
|
||||
</AlertmanagerPageWrapper>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { produce } from 'immer';
|
||||
import { clickSelectOption } from 'test/helpers/selectOptionInTest';
|
||||
import { render, screen, userEvent, within } from 'test/test-utils';
|
||||
import { render, screen, testWithFeatureToggles, userEvent, within } from 'test/test-utils';
|
||||
import { byLabelText, byRole, byTestId } from 'testing-library-selector';
|
||||
|
||||
import { AppNotificationList } from 'app/core/components/AppNotifications/AppNotificationList';
|
||||
@@ -140,6 +140,39 @@ const getRootRoute = async () => {
|
||||
};
|
||||
|
||||
describe('NotificationPolicies', () => {
|
||||
describe('V2 Navigation Mode', () => {
|
||||
testWithFeatureToggles({ enable: ['alertingNavigationV2'] });
|
||||
|
||||
beforeEach(() => {
|
||||
setupDataSources(dataSources.am);
|
||||
grantUserPermissions([
|
||||
AccessControlAction.AlertingNotificationsRead,
|
||||
AccessControlAction.AlertingNotificationsWrite,
|
||||
...PERMISSIONS_NOTIFICATION_POLICIES,
|
||||
]);
|
||||
});
|
||||
|
||||
it('shows only notification policies without internal tabs', async () => {
|
||||
renderNotificationPolicies();
|
||||
|
||||
// Should show notification policies directly
|
||||
expect(await ui.rootRouteContainer.find()).toBeInTheDocument();
|
||||
|
||||
// Should not have tabs
|
||||
expect(screen.queryByRole('tab')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not show time intervals tab in V2 mode', async () => {
|
||||
renderNotificationPolicies();
|
||||
|
||||
// Should show notification policies
|
||||
expect(await ui.rootRouteContainer.find()).toBeInTheDocument();
|
||||
|
||||
// Should not show time intervals tab
|
||||
expect(screen.queryByText(/time intervals/i)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// combobox hack :/
|
||||
beforeAll(() => {
|
||||
const mockGetBoundingClientRect = jest.fn(() => ({
|
||||
|
||||
@@ -12,6 +12,8 @@ import { AlertmanagerAction, useAlertmanagerAbility } from 'app/features/alertin
|
||||
import { AlertmanagerPageWrapper } from './components/AlertingPageWrapper';
|
||||
import { GrafanaAlertmanagerWarning } from './components/GrafanaAlertmanagerWarning';
|
||||
import { TimeIntervalsTable } from './components/mute-timings/MuteTimingsTable';
|
||||
import { shouldUseAlertingNavigationV2 } from './featureToggles';
|
||||
import { useNotificationConfigNav } from './navigation/useNotificationConfigNav';
|
||||
import { useAlertmanager } from './state/AlertmanagerContext';
|
||||
import { withPageErrorBoundary } from './withPageErrorBoundary';
|
||||
|
||||
@@ -106,9 +108,32 @@ function getActiveTabFromUrl(queryParams: UrlQueryMap, defaultTab: ActiveTab): Q
|
||||
};
|
||||
}
|
||||
|
||||
function NotificationPoliciesPage() {
|
||||
const NotificationPoliciesContent = () => {
|
||||
const { selectedAlertmanager = '' } = useAlertmanager();
|
||||
return (
|
||||
<AlertmanagerPageWrapper navId="am-routes" accessType="notification">
|
||||
<>
|
||||
<GrafanaAlertmanagerWarning currentAlertmanager={selectedAlertmanager} />
|
||||
<NotificationPoliciesList />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
function NotificationPoliciesPage() {
|
||||
const useV2Nav = shouldUseAlertingNavigationV2();
|
||||
const { navId, pageNav } = useNotificationConfigNav();
|
||||
|
||||
// In V2 mode, show only notification policies (no internal tabs)
|
||||
if (useV2Nav) {
|
||||
return (
|
||||
<AlertmanagerPageWrapper navId={navId || 'am-routes'} pageNav={pageNav} accessType="notification">
|
||||
<NotificationPoliciesContent />
|
||||
</AlertmanagerPageWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
// Legacy mode: Show internal tabs (backward compatible)
|
||||
return (
|
||||
<AlertmanagerPageWrapper navId={navId || 'am-routes'} pageNav={pageNav} accessType="notification">
|
||||
<NotificationPoliciesTabs />
|
||||
</AlertmanagerPageWrapper>
|
||||
);
|
||||
|
||||
@@ -1,13 +1,56 @@
|
||||
import { Route, Routes } from 'react-router-dom-v5-compat';
|
||||
|
||||
import { Trans } from '@grafana/i18n';
|
||||
import { LinkButton, Stack, Text } from '@grafana/ui';
|
||||
|
||||
import { AlertmanagerPageWrapper } from './components/AlertingPageWrapper';
|
||||
import DuplicateMessageTemplate from './components/contact-points/DuplicateMessageTemplate';
|
||||
import EditMessageTemplate from './components/contact-points/EditMessageTemplate';
|
||||
import NewMessageTemplate from './components/contact-points/NewMessageTemplate';
|
||||
import { NotificationTemplates } from './components/contact-points/NotificationTemplates';
|
||||
import { shouldUseAlertingNavigationV2 } from './featureToggles';
|
||||
import { AlertmanagerAction, useAlertmanagerAbility } from './hooks/useAbilities';
|
||||
import { useNotificationConfigNav } from './navigation/useNotificationConfigNav';
|
||||
import { withPageErrorBoundary } from './withPageErrorBoundary';
|
||||
|
||||
function NotificationTemplates() {
|
||||
const TemplatesList = () => {
|
||||
const [createTemplateSupported, createTemplateAllowed] = useAlertmanagerAbility(
|
||||
AlertmanagerAction.CreateNotificationTemplate
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack direction="row" alignItems="center" justifyContent="space-between">
|
||||
<Text variant="body" color="secondary">
|
||||
<Trans i18nKey="alerting.notification-templates-tab.create-notification-templates-customize-notifications">
|
||||
Create notification templates to customize your notifications.
|
||||
</Trans>
|
||||
</Text>
|
||||
{createTemplateSupported && (
|
||||
<LinkButton
|
||||
icon="plus"
|
||||
variant="primary"
|
||||
href="/alerting/notifications/templates/new"
|
||||
disabled={!createTemplateAllowed}
|
||||
>
|
||||
<Trans i18nKey="alerting.notification-templates-tab.add-notification-template-group">
|
||||
Add notification template group
|
||||
</Trans>
|
||||
</LinkButton>
|
||||
)}
|
||||
</Stack>
|
||||
<NotificationTemplates />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
function NotificationTemplatesRoutes() {
|
||||
const useV2Nav = shouldUseAlertingNavigationV2();
|
||||
|
||||
return (
|
||||
<Routes>
|
||||
{/* In V2 mode, show templates list on base route */}
|
||||
{useV2Nav && <Route path="" element={<TemplatesList />} />}
|
||||
<Route path="new" element={<NewMessageTemplate />} />
|
||||
<Route path=":name/edit" element={<EditMessageTemplate />} />
|
||||
<Route path=":name/duplicate" element={<DuplicateMessageTemplate />} />
|
||||
@@ -15,4 +58,21 @@ function NotificationTemplates() {
|
||||
);
|
||||
}
|
||||
|
||||
export default withPageErrorBoundary(NotificationTemplates);
|
||||
function NotificationTemplatesPage() {
|
||||
const useV2Nav = shouldUseAlertingNavigationV2();
|
||||
const { navId, pageNav } = useNotificationConfigNav();
|
||||
|
||||
// In V2 mode, wrap with page wrapper for proper navigation
|
||||
if (useV2Nav) {
|
||||
return (
|
||||
<AlertmanagerPageWrapper navId={navId || 'receivers'} pageNav={pageNav} accessType="notification">
|
||||
<NotificationTemplatesRoutes />
|
||||
</AlertmanagerPageWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
// In legacy mode, just render routes (templates are accessed via ContactPoints page tabs)
|
||||
return <NotificationTemplatesRoutes />;
|
||||
}
|
||||
|
||||
export default withPageErrorBoundary(NotificationTemplatesPage);
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { render, screen, testWithFeatureToggles } from 'test/test-utils';
|
||||
|
||||
import { configureStore } from 'app/store/configureStore';
|
||||
import { AccessControlAction } from 'app/types/accessControl';
|
||||
|
||||
import TimeIntervalsPage from './TimeIntervalsPage';
|
||||
import { defaultConfig } from './components/mute-timings/mocks';
|
||||
import { setupMswServer } from './mockApi';
|
||||
import { grantUserPermissions, mockDataSource } from './mocks';
|
||||
import { setTimeIntervalsListEmpty } from './mocks/server/configure';
|
||||
import { setAlertmanagerConfig } from './mocks/server/entities/alertmanagers';
|
||||
import { setupDataSources } from './testSetup/datasources';
|
||||
import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
|
||||
|
||||
setupMswServer();
|
||||
|
||||
const alertManager = mockDataSource({
|
||||
name: 'Alertmanager',
|
||||
type: DataSourceType.Alertmanager,
|
||||
});
|
||||
|
||||
describe('TimeIntervalsPage', () => {
|
||||
describe('V2 Navigation Mode', () => {
|
||||
testWithFeatureToggles({ enable: ['alertingNavigationV2'] });
|
||||
|
||||
beforeEach(() => {
|
||||
setupDataSources(alertManager);
|
||||
setAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, defaultConfig);
|
||||
setTimeIntervalsListEmpty(); // Mock empty time intervals list so component renders
|
||||
grantUserPermissions([
|
||||
AccessControlAction.AlertingNotificationsRead,
|
||||
AccessControlAction.AlertingTimeIntervalsRead,
|
||||
]);
|
||||
});
|
||||
|
||||
it('renders time intervals table', async () => {
|
||||
const mockNavIndex = {
|
||||
'notification-config': {
|
||||
id: 'notification-config',
|
||||
text: 'Notification configuration',
|
||||
url: '/alerting/notifications',
|
||||
},
|
||||
'notification-config-time-intervals': {
|
||||
id: 'notification-config-time-intervals',
|
||||
text: 'Time intervals',
|
||||
url: '/alerting/time-intervals',
|
||||
},
|
||||
};
|
||||
const store = configureStore({
|
||||
navIndex: mockNavIndex,
|
||||
});
|
||||
|
||||
render(<TimeIntervalsPage />, {
|
||||
store,
|
||||
historyOptions: {
|
||||
initialEntries: ['/alerting/time-intervals'],
|
||||
},
|
||||
});
|
||||
|
||||
// Should show time intervals content
|
||||
// When empty, it shows "You haven't created any time intervals yet"
|
||||
// When loading, it shows "Loading time intervals..."
|
||||
// When error, it shows "Error loading time intervals"
|
||||
// All contain "time intervals" - use getAllByText since there are multiple matches (tab, description, empty state)
|
||||
const timeIntervalsTexts = await screen.findAllByText(/time intervals/i, {}, { timeout: 5000 });
|
||||
expect(timeIntervalsTexts.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('returns null in legacy mode', () => {
|
||||
// This test verifies that the component returns null when V2 is disabled
|
||||
// The feature toggle is controlled by testWithFeatureToggles, so we test it separately
|
||||
const { container } = render(<TimeIntervalsPage />, {
|
||||
historyOptions: {
|
||||
initialEntries: ['/alerting/time-intervals'],
|
||||
},
|
||||
});
|
||||
// In V2 mode (enabled by testWithFeatureToggles), it should render content
|
||||
expect(container).not.toBeEmptyDOMElement();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import { AlertmanagerPageWrapper } from './components/AlertingPageWrapper';
|
||||
import { GrafanaAlertmanagerWarning } from './components/GrafanaAlertmanagerWarning';
|
||||
import { TimeIntervalsTable } from './components/mute-timings/MuteTimingsTable';
|
||||
import { shouldUseAlertingNavigationV2 } from './featureToggles';
|
||||
import { useNotificationConfigNav } from './navigation/useNotificationConfigNav';
|
||||
import { useAlertmanager } from './state/AlertmanagerContext';
|
||||
import { withPageErrorBoundary } from './withPageErrorBoundary';
|
||||
|
||||
// Content component that uses AlertmanagerContext
|
||||
// This must be rendered within AlertmanagerPageWrapper
|
||||
function TimeIntervalsPageContent() {
|
||||
const { selectedAlertmanager } = useAlertmanager();
|
||||
|
||||
return (
|
||||
<>
|
||||
<GrafanaAlertmanagerWarning currentAlertmanager={selectedAlertmanager!} />
|
||||
<TimeIntervalsTable />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function TimeIntervalsPage() {
|
||||
const useV2Nav = shouldUseAlertingNavigationV2();
|
||||
const { navId, pageNav } = useNotificationConfigNav();
|
||||
|
||||
// In V2 mode, wrap with page wrapper for proper navigation
|
||||
// AlertmanagerPageWrapper provides AlertmanagerContext, so TimeIntervalsPageContent can use useAlertmanager
|
||||
if (useV2Nav) {
|
||||
return (
|
||||
<AlertmanagerPageWrapper navId={navId || 'am-routes'} pageNav={pageNav} accessType="notification">
|
||||
<TimeIntervalsPageContent />
|
||||
</AlertmanagerPageWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
// Legacy mode: not used (handled by NotificationPoliciesPage)
|
||||
return null;
|
||||
}
|
||||
|
||||
export default withPageErrorBoundary(TimeIntervalsPage);
|
||||
+33
-1
@@ -1,6 +1,14 @@
|
||||
import { MemoryHistoryBuildOptions } from 'history';
|
||||
import { ComponentProps, ReactNode } from 'react';
|
||||
import { render, screen, userEvent, waitFor, waitForElementToBeRemoved, within } from 'test/test-utils';
|
||||
import {
|
||||
render,
|
||||
screen,
|
||||
testWithFeatureToggles,
|
||||
userEvent,
|
||||
waitFor,
|
||||
waitForElementToBeRemoved,
|
||||
within,
|
||||
} from 'test/test-utils';
|
||||
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { MIMIR_DATASOURCE_UID } from 'app/features/alerting/unified/mocks/server/constants';
|
||||
@@ -170,6 +178,30 @@ describe('contact points', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('V2 Navigation Mode', () => {
|
||||
testWithFeatureToggles({ enable: ['alertingNavigationV2'] });
|
||||
|
||||
test('shows only contact points without internal tabs', async () => {
|
||||
renderWithProvider(<ContactPointsPageContents />);
|
||||
|
||||
// Should show contact points directly
|
||||
expect(await screen.findByText(/create contact point/i)).toBeInTheDocument();
|
||||
|
||||
// Should not have tabs
|
||||
expect(screen.queryByRole('tab')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('does not show templates tab in V2 mode', async () => {
|
||||
renderWithProvider(<ContactPointsPageContents />);
|
||||
|
||||
// Should show contact points
|
||||
expect(await screen.findByText(/create contact point/i)).toBeInTheDocument();
|
||||
|
||||
// Should not show templates tab
|
||||
expect(screen.queryByText(/notification templates/i)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('templates tab', () => {
|
||||
it('does not show a warning for a "misconfigured" template', async () => {
|
||||
renderWithProvider(
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import {
|
||||
Alert,
|
||||
@@ -13,15 +15,18 @@ import {
|
||||
TabContent,
|
||||
TabsBar,
|
||||
Text,
|
||||
useStyles2,
|
||||
} from '@grafana/ui';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { shouldUseK8sApi } from 'app/features/alerting/unified/utils/k8s/utils';
|
||||
import { makeAMLink, stringifyErrorLike } from 'app/features/alerting/unified/utils/misc';
|
||||
import { AccessControlAction } from 'app/types/accessControl';
|
||||
|
||||
import { shouldUseAlertingNavigationV2 } from '../../featureToggles';
|
||||
import { AlertmanagerAction, useAlertmanagerAbility } from '../../hooks/useAbilities';
|
||||
import { usePagination } from '../../hooks/usePagination';
|
||||
import { useURLSearchParams } from '../../hooks/useURLSearchParams';
|
||||
import { useNotificationConfigNav } from '../../navigation/useNotificationConfigNav';
|
||||
import { useAlertmanager } from '../../state/AlertmanagerContext';
|
||||
import { isExtraConfig } from '../../utils/alertmanager/extraConfigs';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||
@@ -99,7 +104,7 @@ const ContactPointsTab = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack direction="column" gap={1}>
|
||||
{/* TODO we can add some additional info here with a ToggleTip */}
|
||||
<Stack direction="row" alignItems="end" justifyContent="space-between">
|
||||
<ContactPointsFilter />
|
||||
@@ -148,7 +153,7 @@ const ContactPointsTab = () => {
|
||||
<GlobalConfigAlert alertManagerName={selectedAlertmanager!} />
|
||||
)}
|
||||
{ExportDrawer}
|
||||
</>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -158,7 +163,7 @@ const NotificationTemplatesTab = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack direction="column" gap={1}>
|
||||
<Stack direction="row" alignItems="center" justifyContent="space-between">
|
||||
<Text variant="body" color="secondary">
|
||||
<Trans i18nKey="alerting.notification-templates-tab.create-notification-templates-customize-notifications">
|
||||
@@ -179,7 +184,7 @@ const NotificationTemplatesTab = () => {
|
||||
)}
|
||||
</Stack>
|
||||
<NotificationTemplates />
|
||||
</>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -201,6 +206,10 @@ const useTabQueryParam = (defaultTab: ActiveTab) => {
|
||||
|
||||
export const ContactPointsPageContents = () => {
|
||||
const { selectedAlertmanager } = useAlertmanager();
|
||||
const useV2Nav = shouldUseAlertingNavigationV2();
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
// All hooks must be called unconditionally before any early returns
|
||||
const [, canViewContactPoints] = useAlertmanagerAbility(AlertmanagerAction.ViewContactPoint);
|
||||
const [, canCreateContactPoints] = useAlertmanagerAbility(AlertmanagerAction.CreateContactPoint);
|
||||
const [, showTemplatesTab] = useAlertmanagerAbility(AlertmanagerAction.ViewNotificationTemplate);
|
||||
@@ -220,6 +229,19 @@ export const ContactPointsPageContents = () => {
|
||||
alertmanager: selectedAlertmanager!,
|
||||
});
|
||||
|
||||
// In V2 navigation mode, show only contact points (no internal tabs)
|
||||
// Templates are accessible via the sidebar navigation
|
||||
if (useV2Nav) {
|
||||
return (
|
||||
<>
|
||||
<GrafanaAlertmanagerWarning currentAlertmanager={selectedAlertmanager!} />
|
||||
<ContactPointsTab />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Legacy mode: Show internal tabs (backward compatible)
|
||||
|
||||
const showingContactPoints = activeTab === ActiveTab.ContactPoints;
|
||||
const showNotificationTemplates = activeTab === ActiveTab.NotificationTemplates;
|
||||
|
||||
@@ -244,7 +266,7 @@ export const ContactPointsPageContents = () => {
|
||||
/>
|
||||
)}
|
||||
</TabsBar>
|
||||
<TabContent>
|
||||
<TabContent className={styles.tabContent}>
|
||||
<Stack direction="column">
|
||||
{showingContactPoints && <ContactPointsTab />}
|
||||
{showNotificationTemplates && <NotificationTemplatesTab />}
|
||||
@@ -281,9 +303,16 @@ const ContactPointsList = ({ contactPoints, search, pageSize = DEFAULT_PAGE_SIZE
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
tabContent: css({
|
||||
marginTop: theme.spacing(2),
|
||||
}),
|
||||
});
|
||||
|
||||
function ContactPointsPage() {
|
||||
const { navId, pageNav } = useNotificationConfigNav();
|
||||
return (
|
||||
<AlertmanagerPageWrapper navId="receivers" accessType="notification">
|
||||
<AlertmanagerPageWrapper navId={navId || 'receivers'} pageNav={pageNav} accessType="notification">
|
||||
<ContactPointsPageContents />
|
||||
</AlertmanagerPageWrapper>
|
||||
);
|
||||
|
||||
+3
-1
@@ -1,11 +1,13 @@
|
||||
import { useInsightsNav } from '../../../navigation/useInsightsNav';
|
||||
import { withPageErrorBoundary } from '../../../withPageErrorBoundary';
|
||||
import { AlertingPageWrapper } from '../../AlertingPageWrapper';
|
||||
|
||||
import { CentralAlertHistoryScene } from './CentralAlertHistoryScene';
|
||||
|
||||
function HistoryPage() {
|
||||
const { navId, pageNav } = useInsightsNav();
|
||||
return (
|
||||
<AlertingPageWrapper navId="alerts-history" isLoading={false}>
|
||||
<AlertingPageWrapper navId={navId || 'alerts-history'} pageNav={pageNav} isLoading={false}>
|
||||
<CentralAlertHistoryScene />
|
||||
</AlertingPageWrapper>
|
||||
);
|
||||
|
||||
+3
-1
@@ -3,6 +3,7 @@ import { Alert } from '@grafana/ui';
|
||||
|
||||
import { alertRuleApi } from '../../../api/alertRuleApi';
|
||||
import { GRAFANA_RULER_CONFIG } from '../../../api/featureDiscoveryApi';
|
||||
import { useAlertRulesNav } from '../../../navigation/useAlertRulesNav';
|
||||
import { stringifyErrorLike } from '../../../utils/misc';
|
||||
import { withPageErrorBoundary } from '../../../withPageErrorBoundary';
|
||||
import { AlertingPageWrapper } from '../../AlertingPageWrapper';
|
||||
@@ -18,9 +19,10 @@ function DeletedrulesPage() {
|
||||
rulerConfig: GRAFANA_RULER_CONFIG,
|
||||
filter: {}, // todo: add filters, and limit?????
|
||||
});
|
||||
const { navId, pageNav } = useAlertRulesNav();
|
||||
|
||||
return (
|
||||
<AlertingPageWrapper navId="alerts/recently-deleted" isLoading={isLoading}>
|
||||
<AlertingPageWrapper navId={navId || 'alerts/recently-deleted'} pageNav={pageNav} isLoading={isLoading}>
|
||||
<>
|
||||
{error && (
|
||||
<Alert title={t('alerting.deleted-rules.errorloading', 'Failed to load alert deleted rules')}>
|
||||
|
||||
@@ -31,3 +31,8 @@ export const shouldUseFullyCompatibleBackendFilters = () =>
|
||||
* Saved searches feature - allows users to save and apply search queries on the Alert Rules page.
|
||||
*/
|
||||
export const shouldUseSavedSearches = () => config.featureToggles.alertingSavedSearches ?? false;
|
||||
|
||||
/**
|
||||
* New grouped navigation structure for Alerting
|
||||
*/
|
||||
export const shouldUseAlertingNavigationV2 = () => config.featureToggles.alertingNavigationV2 ?? false;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import { t } from '@grafana/i18n';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { Box, Stack, Tab, TabContent, TabsBar } from '@grafana/ui';
|
||||
|
||||
import { AlertingPageWrapper } from '../components/AlertingPageWrapper';
|
||||
@@ -14,10 +15,13 @@ import { PluginIntegrations } from './PluginIntegrations';
|
||||
import SyntheticMonitoringCard from './SyntheticMonitoringCard';
|
||||
|
||||
function Home() {
|
||||
const insightsEnabled = insightsIsAvailable() || isLocalDevEnv();
|
||||
// When V2 navigation is enabled, don't show Insights tab on Home page
|
||||
// (Insights is available via the sidebar Insights menu instead)
|
||||
const insightsEnabled = (insightsIsAvailable() || isLocalDevEnv()) && !config.featureToggles.alertingNavigationV2;
|
||||
|
||||
const [activeTab, setActiveTab] = useState<'insights' | 'overview'>(insightsEnabled ? 'insights' : 'overview');
|
||||
const insightsScene = getInsightsScenes();
|
||||
// Memoize the scene so it's only created once and properly initialized
|
||||
const insightsScene = useMemo(() => getInsightsScenes(), []);
|
||||
|
||||
return (
|
||||
<AlertingPageWrapper subTitle="Learn about problems in your systems moments after they occur" navId="alerting">
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
|
||||
import { AlertingPageWrapper } from '../components/AlertingPageWrapper';
|
||||
import { getInsightsScenes, insightsIsAvailable } from '../home/Insights';
|
||||
import { useInsightsNav } from '../navigation/useInsightsNav';
|
||||
import { isLocalDevEnv } from '../utils/misc';
|
||||
import { withPageErrorBoundary } from '../withPageErrorBoundary';
|
||||
|
||||
function InsightsPage() {
|
||||
const insightsEnabled = insightsIsAvailable() || isLocalDevEnv();
|
||||
const { navId, pageNav } = useInsightsNav();
|
||||
// Memoize the scene so it's only created once and properly initialized
|
||||
const insightsScene = useMemo(() => getInsightsScenes(), []);
|
||||
|
||||
if (!insightsEnabled) {
|
||||
return (
|
||||
<AlertingPageWrapper
|
||||
navId={navId || 'insights'}
|
||||
pageNav={pageNav}
|
||||
subTitle={t('alerting.insights.subtitle', 'Analytics and history for alerting')}
|
||||
>
|
||||
<div>
|
||||
<Trans i18nKey="alerting.insights.not-available">
|
||||
Insights are not available. Please configure the required data sources.
|
||||
</Trans>
|
||||
</div>
|
||||
</AlertingPageWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AlertingPageWrapper
|
||||
navId={navId || 'insights'}
|
||||
pageNav={pageNav}
|
||||
subTitle={t('alerting.insights.subtitle', 'Analytics and history for alerting')}
|
||||
>
|
||||
<insightsScene.Component model={insightsScene} />
|
||||
</AlertingPageWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default withPageErrorBoundary(InsightsPage);
|
||||
@@ -0,0 +1,187 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { getWrapper } from 'test/test-utils';
|
||||
|
||||
import { config } from '@grafana/runtime';
|
||||
import { configureStore } from 'app/store/configureStore';
|
||||
|
||||
import { useAlertActivityNav } from './useAlertActivityNav';
|
||||
|
||||
describe('useAlertActivityNav', () => {
|
||||
const mockNavIndex = {
|
||||
'alert-activity': {
|
||||
id: 'alert-activity',
|
||||
text: 'Alert activity',
|
||||
url: '/alerting/alerts',
|
||||
},
|
||||
'alert-activity-alerts': {
|
||||
id: 'alert-activity-alerts',
|
||||
text: 'Alerts',
|
||||
url: '/alerting/alerts',
|
||||
},
|
||||
'alert-activity-groups': {
|
||||
id: 'alert-activity-groups',
|
||||
text: 'Active notifications',
|
||||
url: '/alerting/groups',
|
||||
},
|
||||
groups: {
|
||||
id: 'groups',
|
||||
text: 'Alert groups',
|
||||
url: '/alerting/groups',
|
||||
},
|
||||
'alert-alerts': {
|
||||
id: 'alert-alerts',
|
||||
text: 'Alerts',
|
||||
url: '/alerting/alerts',
|
||||
},
|
||||
};
|
||||
|
||||
const defaultPreloadedState = {
|
||||
navIndex: mockNavIndex,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
config.featureToggles.alertingNavigationV2 = false;
|
||||
});
|
||||
|
||||
it('should return legacy navId when feature flag is off for /alerting/groups', () => {
|
||||
const wrapper = getWrapper({
|
||||
preloadedState: defaultPreloadedState,
|
||||
renderWithRouter: true,
|
||||
historyOptions: {
|
||||
initialEntries: ['/alerting/groups'],
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useAlertActivityNav(), { wrapper });
|
||||
|
||||
expect(result.current.navId).toBe('groups');
|
||||
expect(result.current.pageNav).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return legacy navId when feature flag is off for /alerting/alerts', () => {
|
||||
const wrapper = getWrapper({
|
||||
preloadedState: defaultPreloadedState,
|
||||
renderWithRouter: true,
|
||||
historyOptions: {
|
||||
initialEntries: ['/alerting/alerts'],
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useAlertActivityNav(), { wrapper });
|
||||
|
||||
expect(result.current.navId).toBe('alert-alerts');
|
||||
expect(result.current.pageNav).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return V2 navigation when feature flag is on for Alerts tab', () => {
|
||||
config.featureToggles.alertingNavigationV2 = true;
|
||||
const store = configureStore(defaultPreloadedState);
|
||||
const wrapper = getWrapper({
|
||||
store,
|
||||
renderWithRouter: true,
|
||||
historyOptions: {
|
||||
initialEntries: ['/alerting/alerts'],
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useAlertActivityNav(), { wrapper });
|
||||
|
||||
expect(result.current.navId).toBe('alert-activity');
|
||||
expect(result.current.pageNav).toBeDefined();
|
||||
// eslint-disable-next-line testing-library/no-node-access
|
||||
expect(result.current.pageNav?.children).toBeDefined();
|
||||
// The pageNav should represent Alert Activity (not the active tab) for consistent title
|
||||
expect(result.current.pageNav?.text).toBe('Alert activity');
|
||||
});
|
||||
|
||||
it('should return V2 navigation when feature flag is on for Active notifications tab', () => {
|
||||
config.featureToggles.alertingNavigationV2 = true;
|
||||
const store = configureStore(defaultPreloadedState);
|
||||
const wrapper = getWrapper({
|
||||
store,
|
||||
renderWithRouter: true,
|
||||
historyOptions: {
|
||||
initialEntries: ['/alerting/groups'],
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useAlertActivityNav(), { wrapper });
|
||||
|
||||
expect(result.current.navId).toBe('alert-activity');
|
||||
expect(result.current.pageNav).toBeDefined();
|
||||
// eslint-disable-next-line testing-library/no-node-access
|
||||
expect(result.current.pageNav?.children).toBeDefined();
|
||||
// The pageNav should represent Alert Activity (not the active tab) for consistent title
|
||||
expect(result.current.pageNav?.text).toBe('Alert activity');
|
||||
});
|
||||
|
||||
it('should set active tab based on current path', () => {
|
||||
config.featureToggles.alertingNavigationV2 = true;
|
||||
const store = configureStore(defaultPreloadedState);
|
||||
const wrapper = getWrapper({
|
||||
store,
|
||||
renderWithRouter: true,
|
||||
historyOptions: {
|
||||
initialEntries: ['/alerting/groups'],
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useAlertActivityNav(), { wrapper });
|
||||
|
||||
// eslint-disable-next-line testing-library/no-node-access
|
||||
const activeNotificationsTab = result.current.pageNav?.children?.find((tab) => tab.id === 'alert-activity-groups');
|
||||
expect(activeNotificationsTab?.active).toBe(true);
|
||||
|
||||
// eslint-disable-next-line testing-library/no-node-access
|
||||
const alertsTab = result.current.pageNav?.children?.find((tab) => tab.id === 'alert-activity-alerts');
|
||||
expect(alertsTab?.active).toBe(false);
|
||||
});
|
||||
|
||||
it('should filter tabs based on permissions', () => {
|
||||
config.featureToggles.alertingNavigationV2 = true;
|
||||
const limitedNavIndex = {
|
||||
'alert-activity': mockNavIndex['alert-activity'],
|
||||
'alert-activity-alerts': mockNavIndex['alert-activity-alerts'],
|
||||
// Missing 'alert-activity-groups' - user doesn't have permission
|
||||
};
|
||||
const store = configureStore({
|
||||
navIndex: limitedNavIndex,
|
||||
});
|
||||
const wrapper = getWrapper({
|
||||
store,
|
||||
renderWithRouter: true,
|
||||
historyOptions: {
|
||||
initialEntries: ['/alerting/alerts'],
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useAlertActivityNav(), { wrapper });
|
||||
|
||||
// eslint-disable-next-line testing-library/no-node-access
|
||||
expect(result.current.pageNav?.children?.length).toBe(1);
|
||||
// eslint-disable-next-line testing-library/no-node-access
|
||||
expect(result.current.pageNav?.children?.[0].id).toBe('alert-activity-alerts');
|
||||
});
|
||||
|
||||
it('should fallback to legacy when alert-activity nav is missing', () => {
|
||||
config.featureToggles.alertingNavigationV2 = true;
|
||||
const store = configureStore({
|
||||
navIndex: {
|
||||
groups: mockNavIndex.groups,
|
||||
'alert-alerts': mockNavIndex['alert-alerts'],
|
||||
},
|
||||
});
|
||||
const wrapper = getWrapper({
|
||||
store,
|
||||
renderWithRouter: true,
|
||||
historyOptions: {
|
||||
initialEntries: ['/alerting/groups'],
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useAlertActivityNav(), { wrapper });
|
||||
|
||||
expect(result.current.navId).toBe('groups');
|
||||
expect(result.current.pageNav).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,93 @@
|
||||
import { useLocation } from 'react-router-dom-v5-compat';
|
||||
|
||||
import { NavModelItem } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { useSelector } from 'app/types/store';
|
||||
|
||||
import { shouldUseAlertingNavigationV2 } from '../featureToggles';
|
||||
|
||||
export function useAlertActivityNav() {
|
||||
const location = useLocation();
|
||||
const navIndex = useSelector((state) => state.navIndex);
|
||||
const useV2Nav = shouldUseAlertingNavigationV2();
|
||||
|
||||
// If V2 navigation is not enabled, return legacy navId
|
||||
if (!useV2Nav) {
|
||||
if (location.pathname === '/alerting/groups') {
|
||||
return {
|
||||
navId: 'groups',
|
||||
pageNav: undefined,
|
||||
};
|
||||
}
|
||||
if (location.pathname === '/alerting/alerts') {
|
||||
return {
|
||||
navId: 'alert-alerts',
|
||||
pageNav: undefined,
|
||||
};
|
||||
}
|
||||
return {
|
||||
navId: undefined,
|
||||
pageNav: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const alertActivityNav = navIndex['alert-activity'];
|
||||
if (!alertActivityNav) {
|
||||
// Fallback to legacy
|
||||
if (location.pathname === '/alerting/groups') {
|
||||
return {
|
||||
navId: 'groups',
|
||||
pageNav: undefined,
|
||||
};
|
||||
}
|
||||
if (location.pathname === '/alerting/alerts') {
|
||||
return {
|
||||
navId: 'alert-alerts',
|
||||
pageNav: undefined,
|
||||
};
|
||||
}
|
||||
return {
|
||||
navId: undefined,
|
||||
pageNav: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// All available tabs
|
||||
const allTabs = [
|
||||
{
|
||||
id: 'alert-activity-alerts',
|
||||
text: t('alerting.navigation.alerts', 'Alerts'),
|
||||
url: '/alerting/alerts',
|
||||
active: location.pathname === '/alerting/alerts',
|
||||
icon: 'bell',
|
||||
parentItem: alertActivityNav,
|
||||
},
|
||||
{
|
||||
id: 'alert-activity-groups',
|
||||
text: t('alerting.navigation.active-notifications', 'Active notifications'),
|
||||
url: '/alerting/groups',
|
||||
active: location.pathname === '/alerting/groups',
|
||||
icon: 'layer-group',
|
||||
parentItem: alertActivityNav,
|
||||
},
|
||||
].filter((tab) => {
|
||||
// Filter based on permissions - if nav item doesn't exist, user doesn't have permission
|
||||
const navItem = navIndex[tab.id];
|
||||
return navItem !== undefined;
|
||||
});
|
||||
|
||||
// Create pageNav structure following the same pattern as useNotificationConfigNav
|
||||
// Keep "Alert Activity" as the pageNav (not the active tab) so the title and subtitle stay consistent
|
||||
// The tabs are children, and the breadcrumb utility will add the active tab to breadcrumbs
|
||||
// (including the first tab, after our fix to the breadcrumb utility)
|
||||
const pageNav: NavModelItem = {
|
||||
...alertActivityNav,
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
children: allTabs as NavModelItem[],
|
||||
};
|
||||
|
||||
return {
|
||||
navId: 'alert-activity',
|
||||
pageNav,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { getWrapper } from 'test/test-utils';
|
||||
|
||||
import { config } from '@grafana/runtime';
|
||||
import { configureStore } from 'app/store/configureStore';
|
||||
|
||||
import { useAlertRulesNav } from './useAlertRulesNav';
|
||||
|
||||
describe('useAlertRulesNav', () => {
|
||||
const mockNavIndex = {
|
||||
'alert-rules': {
|
||||
id: 'alert-rules',
|
||||
text: 'Alert rules',
|
||||
url: '/alerting/list',
|
||||
icon: 'list-ul',
|
||||
},
|
||||
'alert-rules-list': {
|
||||
id: 'alert-rules-list',
|
||||
text: 'Alert rules',
|
||||
url: '/alerting/list',
|
||||
},
|
||||
'alert-rules-recently-deleted': {
|
||||
id: 'alert-rules-recently-deleted',
|
||||
text: 'Recently deleted',
|
||||
url: '/alerting/recently-deleted',
|
||||
},
|
||||
'alert-list': {
|
||||
id: 'alert-list',
|
||||
text: 'Alert rules',
|
||||
url: '/alerting/list',
|
||||
},
|
||||
};
|
||||
|
||||
const defaultPreloadedState = {
|
||||
navIndex: mockNavIndex,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
config.featureToggles.alertingNavigationV2 = false;
|
||||
});
|
||||
|
||||
it('should return legacy navId when feature flag is off', () => {
|
||||
const wrapper = getWrapper({
|
||||
preloadedState: defaultPreloadedState,
|
||||
renderWithRouter: true,
|
||||
historyOptions: {
|
||||
initialEntries: ['/alerting/list'],
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useAlertRulesNav(), { wrapper });
|
||||
|
||||
expect(result.current.navId).toBe('alert-list');
|
||||
expect(result.current.pageNav).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return V2 navigation when feature flag is on', () => {
|
||||
config.featureToggles.alertingNavigationV2 = true;
|
||||
const store = configureStore(defaultPreloadedState);
|
||||
const wrapper = getWrapper({
|
||||
store,
|
||||
renderWithRouter: true,
|
||||
historyOptions: {
|
||||
initialEntries: ['/alerting/list'],
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useAlertRulesNav(), { wrapper });
|
||||
|
||||
expect(result.current.navId).toBe('alert-rules');
|
||||
expect(result.current.pageNav).toBeDefined();
|
||||
// eslint-disable-next-line testing-library/no-node-access
|
||||
expect(result.current.pageNav?.children).toBeDefined();
|
||||
// eslint-disable-next-line testing-library/no-node-access
|
||||
expect(result.current.pageNav?.children?.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should filter tabs based on permissions', () => {
|
||||
config.featureToggles.alertingNavigationV2 = true;
|
||||
const limitedNavIndex = {
|
||||
'alert-rules': mockNavIndex['alert-rules'],
|
||||
'alert-rules-list': mockNavIndex['alert-rules-list'],
|
||||
// Missing 'alert-rules-recently-deleted' - user doesn't have permission
|
||||
};
|
||||
const store = configureStore({
|
||||
navIndex: limitedNavIndex,
|
||||
});
|
||||
const wrapper = getWrapper({
|
||||
store,
|
||||
renderWithRouter: true,
|
||||
historyOptions: {
|
||||
initialEntries: ['/alerting/list'],
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useAlertRulesNav(), { wrapper });
|
||||
|
||||
// eslint-disable-next-line testing-library/no-node-access
|
||||
expect(result.current.pageNav?.children?.length).toBe(1);
|
||||
// eslint-disable-next-line testing-library/no-node-access
|
||||
expect(result.current.pageNav?.children?.[0].id).toBe('alert-rules-list');
|
||||
});
|
||||
|
||||
it('should set active tab based on current path', () => {
|
||||
config.featureToggles.alertingNavigationV2 = true;
|
||||
const store = configureStore(defaultPreloadedState);
|
||||
const wrapper = getWrapper({
|
||||
store,
|
||||
renderWithRouter: true,
|
||||
historyOptions: {
|
||||
initialEntries: ['/alerting/recently-deleted'],
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useAlertRulesNav(), { wrapper });
|
||||
|
||||
// eslint-disable-next-line testing-library/no-node-access
|
||||
const recentlyDeletedTab = result.current.pageNav?.children?.find(
|
||||
(tab) => tab.id === 'alert-rules-recently-deleted'
|
||||
);
|
||||
expect(recentlyDeletedTab?.active).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
import { useLocation } from 'react-router-dom-v5-compat';
|
||||
|
||||
import { NavModelItem } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { useSelector } from 'app/types/store';
|
||||
|
||||
import { shouldUseAlertingNavigationV2 } from '../featureToggles';
|
||||
|
||||
export function useAlertRulesNav() {
|
||||
const location = useLocation();
|
||||
const navIndex = useSelector((state) => state.navIndex);
|
||||
const useV2Nav = shouldUseAlertingNavigationV2();
|
||||
|
||||
// If V2 navigation is not enabled, return legacy navId
|
||||
if (!useV2Nav) {
|
||||
return {
|
||||
navId: 'alert-list',
|
||||
pageNav: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const alertRulesNav = navIndex['alert-rules'];
|
||||
if (!alertRulesNav) {
|
||||
// Fallback to legacy if V2 nav doesn't exist
|
||||
return {
|
||||
navId: 'alert-list',
|
||||
pageNav: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// All available tabs
|
||||
const allTabs = [
|
||||
{
|
||||
id: 'alert-rules-list',
|
||||
text: t('alerting.navigation.alert-rules', 'Alert rules'),
|
||||
url: '/alerting/list',
|
||||
active: location.pathname === '/alerting/list',
|
||||
icon: 'list-ul',
|
||||
parentItem: alertRulesNav,
|
||||
},
|
||||
{
|
||||
id: 'alert-rules-recently-deleted',
|
||||
text: t('alerting.navigation.recently-deleted', 'Recently deleted'),
|
||||
url: '/alerting/recently-deleted',
|
||||
active: location.pathname === '/alerting/recently-deleted',
|
||||
icon: 'trash-alt',
|
||||
parentItem: alertRulesNav,
|
||||
},
|
||||
].filter((tab) => {
|
||||
// Filter based on permissions - if nav item doesn't exist, user doesn't have permission
|
||||
const navItem = navIndex[tab.id];
|
||||
return navItem !== undefined;
|
||||
});
|
||||
|
||||
// Create pageNav that represents the Alert rules page with tabs as children
|
||||
const pageNav: NavModelItem = {
|
||||
...alertRulesNav,
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
children: allTabs as NavModelItem[],
|
||||
};
|
||||
|
||||
return {
|
||||
navId: 'alert-rules',
|
||||
pageNav,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { getWrapper } from 'test/test-utils';
|
||||
|
||||
import { config } from '@grafana/runtime';
|
||||
import { configureStore } from 'app/store/configureStore';
|
||||
|
||||
import { useInsightsNav } from './useInsightsNav';
|
||||
|
||||
describe('useInsightsNav', () => {
|
||||
const mockNavIndex = {
|
||||
insights: {
|
||||
id: 'insights',
|
||||
text: 'Insights',
|
||||
url: '/alerting/insights',
|
||||
},
|
||||
'insights-system': {
|
||||
id: 'insights-system',
|
||||
text: 'System Insights',
|
||||
url: '/alerting/insights',
|
||||
},
|
||||
'insights-history': {
|
||||
id: 'insights-history',
|
||||
text: 'Alert state history',
|
||||
url: '/alerting/history',
|
||||
},
|
||||
'alerts-history': {
|
||||
id: 'alerts-history',
|
||||
text: 'History',
|
||||
url: '/alerting/history',
|
||||
},
|
||||
};
|
||||
|
||||
const defaultPreloadedState = {
|
||||
navIndex: mockNavIndex,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
config.featureToggles.alertingNavigationV2 = false;
|
||||
});
|
||||
|
||||
it('should return legacy navId when feature flag is off', () => {
|
||||
const wrapper = getWrapper({
|
||||
preloadedState: defaultPreloadedState,
|
||||
renderWithRouter: true,
|
||||
historyOptions: {
|
||||
initialEntries: ['/alerting/history'],
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useInsightsNav(), { wrapper });
|
||||
|
||||
expect(result.current.navId).toBe('alerts-history');
|
||||
expect(result.current.pageNav).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return V2 navigation when feature flag is on', () => {
|
||||
config.featureToggles.alertingNavigationV2 = true;
|
||||
const store = configureStore(defaultPreloadedState);
|
||||
const wrapper = getWrapper({
|
||||
store,
|
||||
renderWithRouter: true,
|
||||
historyOptions: {
|
||||
initialEntries: ['/alerting/insights'],
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useInsightsNav(), { wrapper });
|
||||
|
||||
expect(result.current.navId).toBe('insights');
|
||||
expect(result.current.pageNav).toBeDefined();
|
||||
// eslint-disable-next-line testing-library/no-node-access
|
||||
expect(result.current.pageNav?.children).toBeDefined();
|
||||
});
|
||||
|
||||
it('should set active tab based on current path', () => {
|
||||
config.featureToggles.alertingNavigationV2 = true;
|
||||
const store = configureStore(defaultPreloadedState);
|
||||
const wrapper = getWrapper({
|
||||
store,
|
||||
renderWithRouter: true,
|
||||
historyOptions: {
|
||||
initialEntries: ['/alerting/history'],
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useInsightsNav(), { wrapper });
|
||||
|
||||
// eslint-disable-next-line testing-library/no-node-access
|
||||
const historyTab = result.current.pageNav?.children?.find((tab) => tab.id === 'insights-history');
|
||||
expect(historyTab?.active).toBe(true);
|
||||
});
|
||||
|
||||
it('should filter tabs based on permissions', () => {
|
||||
config.featureToggles.alertingNavigationV2 = true;
|
||||
const limitedNavIndex = {
|
||||
insights: mockNavIndex.insights,
|
||||
'insights-system': mockNavIndex['insights-system'],
|
||||
// Missing 'insights-history' - user doesn't have permission
|
||||
};
|
||||
const store = configureStore({
|
||||
navIndex: limitedNavIndex,
|
||||
});
|
||||
const wrapper = getWrapper({
|
||||
store,
|
||||
renderWithRouter: true,
|
||||
historyOptions: {
|
||||
initialEntries: ['/alerting/insights'],
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useInsightsNav(), { wrapper });
|
||||
|
||||
// eslint-disable-next-line testing-library/no-node-access
|
||||
expect(result.current.pageNav?.children?.length).toBe(1);
|
||||
// eslint-disable-next-line testing-library/no-node-access
|
||||
expect(result.current.pageNav?.children?.[0].id).toBe('insights-system');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
import { useLocation } from 'react-router-dom-v5-compat';
|
||||
|
||||
import { NavModelItem } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { useSelector } from 'app/types/store';
|
||||
|
||||
import { shouldUseAlertingNavigationV2 } from '../featureToggles';
|
||||
|
||||
export function useInsightsNav() {
|
||||
const location = useLocation();
|
||||
const navIndex = useSelector((state) => state.navIndex);
|
||||
const useV2Nav = shouldUseAlertingNavigationV2();
|
||||
|
||||
// If V2 navigation is not enabled, return legacy navId
|
||||
if (!useV2Nav) {
|
||||
if (location.pathname === '/alerting/history') {
|
||||
return {
|
||||
navId: 'alerts-history',
|
||||
pageNav: undefined,
|
||||
};
|
||||
}
|
||||
// For insights page, it doesn't exist in legacy, so return undefined
|
||||
return {
|
||||
navId: undefined,
|
||||
pageNav: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const insightsNav = navIndex.insights;
|
||||
if (!insightsNav) {
|
||||
// Fallback to legacy
|
||||
if (location.pathname === '/alerting/history') {
|
||||
return {
|
||||
navId: 'alerts-history',
|
||||
pageNav: undefined,
|
||||
};
|
||||
}
|
||||
return {
|
||||
navId: undefined,
|
||||
pageNav: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// All available tabs
|
||||
const allTabs = [
|
||||
{
|
||||
id: 'insights-system',
|
||||
text: t('alerting.navigation.system-insights', 'System Insights'),
|
||||
url: '/alerting/insights',
|
||||
active: location.pathname === '/alerting/insights',
|
||||
icon: 'chart-line',
|
||||
parentItem: insightsNav,
|
||||
},
|
||||
{
|
||||
id: 'insights-history',
|
||||
text: t('alerting.navigation.alert-state-history', 'Alert state history'),
|
||||
url: '/alerting/history',
|
||||
active: location.pathname === '/alerting/history',
|
||||
icon: 'history',
|
||||
parentItem: insightsNav,
|
||||
},
|
||||
].filter((tab) => {
|
||||
// Filter based on permissions - if nav item doesn't exist, user doesn't have permission
|
||||
const navItem = navIndex[tab.id];
|
||||
return navItem !== undefined;
|
||||
});
|
||||
|
||||
// Create pageNav that represents the Insights page with tabs as children
|
||||
const pageNav: NavModelItem = {
|
||||
...insightsNav,
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
children: allTabs as NavModelItem[],
|
||||
};
|
||||
|
||||
return {
|
||||
navId: 'insights',
|
||||
pageNav,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { getWrapper } from 'test/test-utils';
|
||||
|
||||
import { config } from '@grafana/runtime';
|
||||
import { configureStore } from 'app/store/configureStore';
|
||||
|
||||
import { useNotificationConfigNav } from './useNotificationConfigNav';
|
||||
|
||||
describe('useNotificationConfigNav', () => {
|
||||
const mockNavIndex = {
|
||||
'notification-config': {
|
||||
id: 'notification-config',
|
||||
text: 'Notification configuration',
|
||||
url: '/alerting/notifications',
|
||||
},
|
||||
'notification-config-contact-points': {
|
||||
id: 'notification-config-contact-points',
|
||||
text: 'Contact points',
|
||||
url: '/alerting/notifications',
|
||||
},
|
||||
'notification-config-policies': {
|
||||
id: 'notification-config-policies',
|
||||
text: 'Notification policies',
|
||||
url: '/alerting/routes',
|
||||
},
|
||||
'notification-config-templates': {
|
||||
id: 'notification-config-templates',
|
||||
text: 'Notification templates',
|
||||
url: '/alerting/notifications/templates',
|
||||
},
|
||||
'notification-config-time-intervals': {
|
||||
id: 'notification-config-time-intervals',
|
||||
text: 'Time intervals',
|
||||
url: '/alerting/routes?tab=time_intervals',
|
||||
},
|
||||
receivers: {
|
||||
id: 'receivers',
|
||||
text: 'Contact points',
|
||||
url: '/alerting/notifications',
|
||||
},
|
||||
'am-routes': {
|
||||
id: 'am-routes',
|
||||
text: 'Notification policies',
|
||||
url: '/alerting/routes',
|
||||
},
|
||||
};
|
||||
|
||||
const defaultPreloadedState = {
|
||||
navIndex: mockNavIndex,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
config.featureToggles.alertingNavigationV2 = false;
|
||||
});
|
||||
|
||||
it('should return legacy navId when feature flag is off', () => {
|
||||
const wrapper = getWrapper({
|
||||
preloadedState: defaultPreloadedState,
|
||||
renderWithRouter: true,
|
||||
historyOptions: {
|
||||
initialEntries: ['/alerting/notifications'],
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useNotificationConfigNav(), { wrapper });
|
||||
|
||||
expect(result.current.navId).toBe('receivers');
|
||||
expect(result.current.pageNav).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return V2 navigation when feature flag is on', () => {
|
||||
config.featureToggles.alertingNavigationV2 = true;
|
||||
const store = configureStore(defaultPreloadedState);
|
||||
const wrapper = getWrapper({
|
||||
store,
|
||||
renderWithRouter: true,
|
||||
historyOptions: {
|
||||
initialEntries: ['/alerting/notifications'],
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useNotificationConfigNav(), { wrapper });
|
||||
|
||||
expect(result.current.navId).toBe('notification-config');
|
||||
expect(result.current.pageNav).toBeDefined();
|
||||
// eslint-disable-next-line testing-library/no-node-access
|
||||
expect(result.current.pageNav?.children).toBeDefined();
|
||||
});
|
||||
|
||||
it('should detect time intervals tab from V2 path', () => {
|
||||
config.featureToggles.alertingNavigationV2 = true;
|
||||
const store = configureStore(defaultPreloadedState);
|
||||
const wrapper = getWrapper({
|
||||
store,
|
||||
renderWithRouter: true,
|
||||
historyOptions: {
|
||||
initialEntries: ['/alerting/time-intervals'],
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useNotificationConfigNav(), { wrapper });
|
||||
|
||||
// eslint-disable-next-line testing-library/no-node-access
|
||||
const timeIntervalsTab = result.current.pageNav?.children?.find(
|
||||
(tab) => tab.id === 'notification-config-time-intervals'
|
||||
);
|
||||
expect(timeIntervalsTab?.active).toBe(true);
|
||||
});
|
||||
|
||||
it('should filter tabs based on permissions', () => {
|
||||
config.featureToggles.alertingNavigationV2 = true;
|
||||
const limitedNavIndex = {
|
||||
'notification-config': mockNavIndex['notification-config'],
|
||||
'notification-config-contact-points': mockNavIndex['notification-config-contact-points'],
|
||||
// Missing other tabs - user doesn't have permission
|
||||
};
|
||||
const store = configureStore({
|
||||
navIndex: limitedNavIndex,
|
||||
});
|
||||
const wrapper = getWrapper({
|
||||
store,
|
||||
renderWithRouter: true,
|
||||
historyOptions: {
|
||||
initialEntries: ['/alerting/notifications'],
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useNotificationConfigNav(), { wrapper });
|
||||
|
||||
// eslint-disable-next-line testing-library/no-node-access
|
||||
expect(result.current.pageNav?.children?.length).toBe(1);
|
||||
// eslint-disable-next-line testing-library/no-node-access
|
||||
expect(result.current.pageNav?.children?.[0].id).toBe('notification-config-contact-points');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,112 @@
|
||||
import { useLocation } from 'react-router-dom-v5-compat';
|
||||
|
||||
import { NavModelItem } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { useSelector } from 'app/types/store';
|
||||
|
||||
import { shouldUseAlertingNavigationV2 } from '../featureToggles';
|
||||
|
||||
export function useNotificationConfigNav() {
|
||||
const location = useLocation();
|
||||
const navIndex = useSelector((state) => state.navIndex);
|
||||
const useV2Nav = shouldUseAlertingNavigationV2();
|
||||
|
||||
// If V2 navigation is not enabled, return legacy navId based on current path
|
||||
if (!useV2Nav) {
|
||||
if (location.pathname.includes('/alerting/notifications/templates')) {
|
||||
return {
|
||||
navId: 'receivers',
|
||||
pageNav: undefined,
|
||||
};
|
||||
}
|
||||
if (location.pathname === '/alerting/routes') {
|
||||
return {
|
||||
navId: 'am-routes',
|
||||
pageNav: undefined,
|
||||
};
|
||||
}
|
||||
return {
|
||||
navId: 'receivers',
|
||||
pageNav: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const notificationConfigNav = navIndex['notification-config'];
|
||||
if (!notificationConfigNav) {
|
||||
// Fallback to legacy navIds
|
||||
if (location.pathname.includes('/alerting/notifications/templates')) {
|
||||
return {
|
||||
navId: 'receivers',
|
||||
pageNav: undefined,
|
||||
};
|
||||
}
|
||||
if (location.pathname === '/alerting/routes') {
|
||||
return {
|
||||
navId: 'am-routes',
|
||||
pageNav: undefined,
|
||||
};
|
||||
}
|
||||
return {
|
||||
navId: 'receivers',
|
||||
pageNav: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// Check if we're on the time intervals page
|
||||
// In V2 mode, check for dedicated route; in legacy mode, check for query param
|
||||
const isTimeIntervalsTab = useV2Nav
|
||||
? location.pathname === '/alerting/time-intervals'
|
||||
: location.pathname === '/alerting/routes' && location.search.includes('tab=time_intervals');
|
||||
|
||||
// All available tabs
|
||||
const allTabs = [
|
||||
{
|
||||
id: 'notification-config-contact-points',
|
||||
text: t('alerting.navigation.contact-points', 'Contact points'),
|
||||
url: '/alerting/notifications',
|
||||
active: location.pathname === '/alerting/notifications' && !location.pathname.includes('/templates'),
|
||||
icon: 'comment-alt-share',
|
||||
parentItem: notificationConfigNav,
|
||||
},
|
||||
{
|
||||
id: 'notification-config-policies',
|
||||
text: t('alerting.navigation.notification-policies', 'Notification policies'),
|
||||
url: '/alerting/routes',
|
||||
active: location.pathname === '/alerting/routes' && !isTimeIntervalsTab,
|
||||
icon: 'sitemap',
|
||||
parentItem: notificationConfigNav,
|
||||
},
|
||||
{
|
||||
id: 'notification-config-templates',
|
||||
text: t('alerting.navigation.notification-templates', 'Notification templates'),
|
||||
url: '/alerting/notifications/templates',
|
||||
active: location.pathname.includes('/alerting/notifications/templates'),
|
||||
icon: 'file-alt',
|
||||
parentItem: notificationConfigNav,
|
||||
},
|
||||
{
|
||||
id: 'notification-config-time-intervals',
|
||||
text: t('alerting.navigation.time-intervals', 'Time intervals'),
|
||||
url: useV2Nav ? '/alerting/time-intervals' : '/alerting/routes?tab=time_intervals',
|
||||
active: isTimeIntervalsTab,
|
||||
icon: 'clock-nine',
|
||||
parentItem: notificationConfigNav,
|
||||
},
|
||||
].filter((tab) => {
|
||||
// Filter based on permissions - if nav item doesn't exist, user doesn't have permission
|
||||
const navItem = navIndex[tab.id];
|
||||
return navItem !== undefined;
|
||||
});
|
||||
|
||||
// Create pageNav that represents the Notification configuration page with tabs as children
|
||||
const pageNav: NavModelItem = {
|
||||
...notificationConfigNav,
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
children: allTabs as NavModelItem[],
|
||||
};
|
||||
|
||||
return {
|
||||
navId: 'notification-config',
|
||||
pageNav,
|
||||
};
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import { shouldUsePrometheusRulesPrimary } from '../featureToggles';
|
||||
import { useCombinedRuleNamespaces } from '../hooks/useCombinedRuleNamespaces';
|
||||
import { useFilteredRules, useRulesFilter } from '../hooks/useFilteredRules';
|
||||
import { useUnifiedAlertingSelector } from '../hooks/useUnifiedAlertingSelector';
|
||||
import { useAlertRulesNav } from '../navigation/useAlertRulesNav';
|
||||
import { fetchAllPromAndRulerRulesAction, fetchAllPromRulesAction, fetchRulerRulesAction } from '../state/actions';
|
||||
import { RULE_LIST_POLL_INTERVAL_MS } from '../utils/constants';
|
||||
import { GRAFANA_RULES_SOURCE_NAME, getAllRulesSourceNames } from '../utils/datasource';
|
||||
@@ -115,11 +116,14 @@ const RuleListV1 = () => {
|
||||
|
||||
const combinedNamespaces: CombinedRuleNamespace[] = useCombinedRuleNamespaces();
|
||||
const filteredNamespaces = useFilteredRules(combinedNamespaces, filterState);
|
||||
const { navId, pageNav } = useAlertRulesNav();
|
||||
|
||||
return (
|
||||
// We don't want to show the Loading... indicator for the whole page.
|
||||
// We show separate indicators for Grafana-managed and Cloud rules
|
||||
<AlertingPageWrapper
|
||||
navId="alert-list"
|
||||
navId={navId}
|
||||
pageNav={pageNav}
|
||||
isLoading={false}
|
||||
renderTitle={(title) => <RuleListPageTitle title={title} />}
|
||||
actions={<RuleListActionButtons hasAlertRulesCreated={hasAlertRulesCreated} />}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { useListViewMode } from '../components/rules/Filter/RulesViewModeSelecto
|
||||
import { AIAlertRuleButtonComponent } from '../enterprise-components/AI/AIGenAlertRuleButton/addAIAlertRuleButton';
|
||||
import { AlertingAction, useAlertingAbility } from '../hooks/useAbilities';
|
||||
import { useRulesFilter } from '../hooks/useFilteredRules';
|
||||
import { useAlertRulesNav } from '../navigation/useAlertRulesNav';
|
||||
|
||||
import { FilterView } from './FilterView';
|
||||
import { GroupedView } from './GroupedView';
|
||||
@@ -119,10 +120,12 @@ export function RuleListActions() {
|
||||
|
||||
export default function RuleListPage() {
|
||||
const { isApplying } = useApplyDefaultSearch();
|
||||
const { navId, pageNav } = useAlertRulesNav();
|
||||
|
||||
return (
|
||||
<AlertingPageWrapper
|
||||
navId="alert-list"
|
||||
navId={navId}
|
||||
pageNav={pageNav}
|
||||
renderTitle={(title) => <RuleListPageTitle title={title} />}
|
||||
isLoading={isApplying}
|
||||
actions={<RuleListActions />}
|
||||
|
||||
@@ -3,21 +3,15 @@ import { UrlSyncContextProvider } from '@grafana/scenes';
|
||||
import { withErrorBoundary } from '@grafana/ui';
|
||||
|
||||
import { AlertingPageWrapper } from '../components/AlertingPageWrapper';
|
||||
import { useAlertActivityNav } from '../navigation/useAlertActivityNav';
|
||||
|
||||
import { TriageScene, triageScene } from './scene/TriageScene';
|
||||
|
||||
export const TriagePage = () => {
|
||||
const { navId, pageNav } = useAlertActivityNav();
|
||||
|
||||
return (
|
||||
<AlertingPageWrapper
|
||||
navId="alert-alerts"
|
||||
subTitle={t(
|
||||
'alerting.pages.triage.subtitle',
|
||||
'See what is currently alerting and explore historical data to investigate current or past issues.'
|
||||
)}
|
||||
pageNav={{
|
||||
text: t('alerting.pages.triage.title', 'Alerts'),
|
||||
}}
|
||||
>
|
||||
<AlertingPageWrapper navId={navId || 'alert-alerts'} pageNav={pageNav}>
|
||||
<UrlSyncContextProvider scene={triageScene} updateUrlOnInit={true} createBrowserHistorySteps={true}>
|
||||
<TriageScene key={triageScene.state.key} />
|
||||
</UrlSyncContextProvider>
|
||||
|
||||
@@ -3791,6 +3791,7 @@
|
||||
},
|
||||
"recently-viewed": {
|
||||
"clear": "",
|
||||
"empty": "",
|
||||
"error": "",
|
||||
"retry": "",
|
||||
"title": ""
|
||||
@@ -4453,7 +4454,6 @@
|
||||
},
|
||||
"no-properties-changed": "Žádné relevantní vlastnosti se nezměnily",
|
||||
"table": {
|
||||
"notes": "",
|
||||
"updated": "Datum",
|
||||
"updatedBy": "Aktualizoval uživatel",
|
||||
"version": "Verze"
|
||||
@@ -4912,8 +4912,7 @@
|
||||
"apply": "",
|
||||
"change-value": "",
|
||||
"discard": "",
|
||||
"modal-title": "",
|
||||
"values": "Hodnoty oddělené čárkou"
|
||||
"modal-title": ""
|
||||
},
|
||||
"datasource-options": {
|
||||
"name-filter": "Filtr názvu",
|
||||
@@ -6011,9 +6010,6 @@
|
||||
},
|
||||
"custom-variable-form": {
|
||||
"custom-options": "Vlastní možnosti",
|
||||
"json-values-tooltip": "",
|
||||
"name-csv-values": "",
|
||||
"name-json-values": "",
|
||||
"name-values-separated-comma": "Hodnoty oddělené čárkou",
|
||||
"selection-options": "Možnosti výběru"
|
||||
},
|
||||
@@ -6605,11 +6601,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"use-modal-editor": {
|
||||
"description": {
|
||||
"change-variable-query": ""
|
||||
}
|
||||
},
|
||||
"use-save-dashboard": {
|
||||
"message-dashboard-saved": "Nástěnka byla uložena"
|
||||
},
|
||||
@@ -6633,7 +6624,6 @@
|
||||
"label": ""
|
||||
},
|
||||
"hidden": {
|
||||
"description": "",
|
||||
"label": ""
|
||||
},
|
||||
"hidden-label": {
|
||||
@@ -6693,11 +6683,8 @@
|
||||
"tooltip-show-usages": "Zobrazit použití"
|
||||
},
|
||||
"variable-values-preview": {
|
||||
"show-more": "Zobrazit více",
|
||||
"preview-of-values_one": "",
|
||||
"preview-of-values_few": "",
|
||||
"preview-of-values_many": "",
|
||||
"preview-of-values_other": ""
|
||||
"preview-of-values": "Náhled hodnot",
|
||||
"show-more": "Zobrazit více"
|
||||
},
|
||||
"version-history": {
|
||||
"comparison": {
|
||||
|
||||
@@ -3759,6 +3759,7 @@
|
||||
},
|
||||
"recently-viewed": {
|
||||
"clear": "",
|
||||
"empty": "",
|
||||
"error": "",
|
||||
"retry": "",
|
||||
"title": ""
|
||||
@@ -4415,7 +4416,6 @@
|
||||
},
|
||||
"no-properties-changed": "Keine relevanten Eigenschaften geändert",
|
||||
"table": {
|
||||
"notes": "",
|
||||
"updated": "Datum",
|
||||
"updatedBy": "Aktualisiert von",
|
||||
"version": "Version"
|
||||
@@ -4874,8 +4874,7 @@
|
||||
"apply": "",
|
||||
"change-value": "",
|
||||
"discard": "",
|
||||
"modal-title": "",
|
||||
"values": "Werte werden durch Komma getrennt"
|
||||
"modal-title": ""
|
||||
},
|
||||
"datasource-options": {
|
||||
"name-filter": "Namensfilter",
|
||||
@@ -5969,9 +5968,6 @@
|
||||
},
|
||||
"custom-variable-form": {
|
||||
"custom-options": "Benutzerdefinierte Optionen",
|
||||
"json-values-tooltip": "",
|
||||
"name-csv-values": "",
|
||||
"name-json-values": "",
|
||||
"name-values-separated-comma": "Werte werden durch Komma getrennt",
|
||||
"selection-options": "Auswahloptionen"
|
||||
},
|
||||
@@ -6559,11 +6555,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"use-modal-editor": {
|
||||
"description": {
|
||||
"change-variable-query": ""
|
||||
}
|
||||
},
|
||||
"use-save-dashboard": {
|
||||
"message-dashboard-saved": "Dashboard gespeichert"
|
||||
},
|
||||
@@ -6587,7 +6578,6 @@
|
||||
"label": ""
|
||||
},
|
||||
"hidden": {
|
||||
"description": "",
|
||||
"label": ""
|
||||
},
|
||||
"hidden-label": {
|
||||
@@ -6647,9 +6637,8 @@
|
||||
"tooltip-show-usages": "Nutzungen anzeigen"
|
||||
},
|
||||
"variable-values-preview": {
|
||||
"show-more": "Mehr anzeigen",
|
||||
"preview-of-values_one": "",
|
||||
"preview-of-values_other": ""
|
||||
"preview-of-values": "Vorschau der Werte",
|
||||
"show-more": "Mehr anzeigen"
|
||||
},
|
||||
"version-history": {
|
||||
"comparison": {
|
||||
|
||||
@@ -3759,6 +3759,7 @@
|
||||
},
|
||||
"recently-viewed": {
|
||||
"clear": "",
|
||||
"empty": "",
|
||||
"error": "",
|
||||
"retry": "",
|
||||
"title": ""
|
||||
@@ -4415,7 +4416,6 @@
|
||||
},
|
||||
"no-properties-changed": "No se ha cambiado ninguna propiedad relevante",
|
||||
"table": {
|
||||
"notes": "",
|
||||
"updated": "Fecha",
|
||||
"updatedBy": "Actualizada por",
|
||||
"version": "Versión"
|
||||
@@ -4874,8 +4874,7 @@
|
||||
"apply": "",
|
||||
"change-value": "",
|
||||
"discard": "",
|
||||
"modal-title": "",
|
||||
"values": "Valores separados por coma"
|
||||
"modal-title": ""
|
||||
},
|
||||
"datasource-options": {
|
||||
"name-filter": "Nombrar filtro",
|
||||
@@ -5969,9 +5968,6 @@
|
||||
},
|
||||
"custom-variable-form": {
|
||||
"custom-options": "Opciones personalizadas",
|
||||
"json-values-tooltip": "",
|
||||
"name-csv-values": "",
|
||||
"name-json-values": "",
|
||||
"name-values-separated-comma": "Valores separados por comas",
|
||||
"selection-options": "Opciones de selección"
|
||||
},
|
||||
@@ -6559,11 +6555,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"use-modal-editor": {
|
||||
"description": {
|
||||
"change-variable-query": ""
|
||||
}
|
||||
},
|
||||
"use-save-dashboard": {
|
||||
"message-dashboard-saved": "Dashboard guardado"
|
||||
},
|
||||
@@ -6587,7 +6578,6 @@
|
||||
"label": ""
|
||||
},
|
||||
"hidden": {
|
||||
"description": "",
|
||||
"label": ""
|
||||
},
|
||||
"hidden-label": {
|
||||
@@ -6647,9 +6637,8 @@
|
||||
"tooltip-show-usages": "Mostrar usos"
|
||||
},
|
||||
"variable-values-preview": {
|
||||
"show-more": "Mostrar más",
|
||||
"preview-of-values_one": "",
|
||||
"preview-of-values_other": ""
|
||||
"preview-of-values": "Vista previa de los valores",
|
||||
"show-more": "Mostrar más"
|
||||
},
|
||||
"version-history": {
|
||||
"comparison": {
|
||||
|
||||
@@ -3759,6 +3759,7 @@
|
||||
},
|
||||
"recently-viewed": {
|
||||
"clear": "",
|
||||
"empty": "",
|
||||
"error": "",
|
||||
"retry": "",
|
||||
"title": ""
|
||||
@@ -4415,7 +4416,6 @@
|
||||
},
|
||||
"no-properties-changed": "Aucune propriété pertinente n’a été modifiée",
|
||||
"table": {
|
||||
"notes": "",
|
||||
"updated": "Date",
|
||||
"updatedBy": "Mis à jour par",
|
||||
"version": "Version"
|
||||
@@ -4874,8 +4874,7 @@
|
||||
"apply": "",
|
||||
"change-value": "",
|
||||
"discard": "",
|
||||
"modal-title": "",
|
||||
"values": "Valeurs séparées par une virgule"
|
||||
"modal-title": ""
|
||||
},
|
||||
"datasource-options": {
|
||||
"name-filter": "Nom du filtre",
|
||||
@@ -5969,9 +5968,6 @@
|
||||
},
|
||||
"custom-variable-form": {
|
||||
"custom-options": "Personnaliser les options",
|
||||
"json-values-tooltip": "",
|
||||
"name-csv-values": "",
|
||||
"name-json-values": "",
|
||||
"name-values-separated-comma": "Valeurs séparées par des virgules",
|
||||
"selection-options": "Options de sélection"
|
||||
},
|
||||
@@ -6559,11 +6555,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"use-modal-editor": {
|
||||
"description": {
|
||||
"change-variable-query": ""
|
||||
}
|
||||
},
|
||||
"use-save-dashboard": {
|
||||
"message-dashboard-saved": "Tableau de bord enregistré"
|
||||
},
|
||||
@@ -6587,7 +6578,6 @@
|
||||
"label": ""
|
||||
},
|
||||
"hidden": {
|
||||
"description": "",
|
||||
"label": ""
|
||||
},
|
||||
"hidden-label": {
|
||||
@@ -6647,9 +6637,8 @@
|
||||
"tooltip-show-usages": "Afficher les usages"
|
||||
},
|
||||
"variable-values-preview": {
|
||||
"show-more": "Afficher plus",
|
||||
"preview-of-values_one": "",
|
||||
"preview-of-values_other": ""
|
||||
"preview-of-values": "Aperçu des valeurs",
|
||||
"show-more": "Afficher plus"
|
||||
},
|
||||
"version-history": {
|
||||
"comparison": {
|
||||
|
||||
@@ -3759,6 +3759,7 @@
|
||||
},
|
||||
"recently-viewed": {
|
||||
"clear": "",
|
||||
"empty": "",
|
||||
"error": "",
|
||||
"retry": "",
|
||||
"title": ""
|
||||
@@ -4415,7 +4416,6 @@
|
||||
},
|
||||
"no-properties-changed": "Nem változtak meg a releváns tulajdonságok",
|
||||
"table": {
|
||||
"notes": "",
|
||||
"updated": "Dátum",
|
||||
"updatedBy": "Frissítette:",
|
||||
"version": "Verzió"
|
||||
@@ -4874,8 +4874,7 @@
|
||||
"apply": "",
|
||||
"change-value": "",
|
||||
"discard": "",
|
||||
"modal-title": "",
|
||||
"values": "Értékek vesszővel elválasztva"
|
||||
"modal-title": ""
|
||||
},
|
||||
"datasource-options": {
|
||||
"name-filter": "Névszűrő",
|
||||
@@ -5969,9 +5968,6 @@
|
||||
},
|
||||
"custom-variable-form": {
|
||||
"custom-options": "Egyéni opciók",
|
||||
"json-values-tooltip": "",
|
||||
"name-csv-values": "",
|
||||
"name-json-values": "",
|
||||
"name-values-separated-comma": "Értékek vesszővel elválasztva",
|
||||
"selection-options": "Kijelölés beállításai"
|
||||
},
|
||||
@@ -6559,11 +6555,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"use-modal-editor": {
|
||||
"description": {
|
||||
"change-variable-query": ""
|
||||
}
|
||||
},
|
||||
"use-save-dashboard": {
|
||||
"message-dashboard-saved": "Irányítópult elmentve"
|
||||
},
|
||||
@@ -6587,7 +6578,6 @@
|
||||
"label": ""
|
||||
},
|
||||
"hidden": {
|
||||
"description": "",
|
||||
"label": ""
|
||||
},
|
||||
"hidden-label": {
|
||||
@@ -6647,9 +6637,8 @@
|
||||
"tooltip-show-usages": "Használatok megjelenítése"
|
||||
},
|
||||
"variable-values-preview": {
|
||||
"show-more": "Több megjelenítése",
|
||||
"preview-of-values_one": "",
|
||||
"preview-of-values_other": ""
|
||||
"preview-of-values": "Értékek előnézete",
|
||||
"show-more": "Több megjelenítése"
|
||||
},
|
||||
"version-history": {
|
||||
"comparison": {
|
||||
|
||||
@@ -3743,6 +3743,7 @@
|
||||
},
|
||||
"recently-viewed": {
|
||||
"clear": "",
|
||||
"empty": "",
|
||||
"error": "",
|
||||
"retry": "",
|
||||
"title": ""
|
||||
@@ -4396,7 +4397,6 @@
|
||||
},
|
||||
"no-properties-changed": "Tidak ada properti yang relevan yang diubah",
|
||||
"table": {
|
||||
"notes": "",
|
||||
"updated": "Tanggal",
|
||||
"updatedBy": "Diperbarui Oleh",
|
||||
"version": "Versi"
|
||||
@@ -4855,8 +4855,7 @@
|
||||
"apply": "",
|
||||
"change-value": "",
|
||||
"discard": "",
|
||||
"modal-title": "",
|
||||
"values": "Nilai dipisahkan dengan koma"
|
||||
"modal-title": ""
|
||||
},
|
||||
"datasource-options": {
|
||||
"name-filter": "Filter nama",
|
||||
@@ -5948,9 +5947,6 @@
|
||||
},
|
||||
"custom-variable-form": {
|
||||
"custom-options": "Opsi kustom",
|
||||
"json-values-tooltip": "",
|
||||
"name-csv-values": "",
|
||||
"name-json-values": "",
|
||||
"name-values-separated-comma": "Nilai dipisahkan dengan koma",
|
||||
"selection-options": "Opsi pemilihan"
|
||||
},
|
||||
@@ -6536,11 +6532,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"use-modal-editor": {
|
||||
"description": {
|
||||
"change-variable-query": ""
|
||||
}
|
||||
},
|
||||
"use-save-dashboard": {
|
||||
"message-dashboard-saved": "Dasbor disimpan"
|
||||
},
|
||||
@@ -6564,7 +6555,6 @@
|
||||
"label": ""
|
||||
},
|
||||
"hidden": {
|
||||
"description": "",
|
||||
"label": ""
|
||||
},
|
||||
"hidden-label": {
|
||||
@@ -6624,8 +6614,8 @@
|
||||
"tooltip-show-usages": "Tampilkan penggunaan"
|
||||
},
|
||||
"variable-values-preview": {
|
||||
"show-more": "Tampilkan lebih banyak",
|
||||
"preview-of-values_other": ""
|
||||
"preview-of-values": "Pratinjau nilai",
|
||||
"show-more": "Tampilkan lebih banyak"
|
||||
},
|
||||
"version-history": {
|
||||
"comparison": {
|
||||
|
||||
@@ -3759,6 +3759,7 @@
|
||||
},
|
||||
"recently-viewed": {
|
||||
"clear": "",
|
||||
"empty": "",
|
||||
"error": "",
|
||||
"retry": "",
|
||||
"title": ""
|
||||
@@ -4415,7 +4416,6 @@
|
||||
},
|
||||
"no-properties-changed": "Nessuna proprietà rilevante modificata",
|
||||
"table": {
|
||||
"notes": "",
|
||||
"updated": "Data",
|
||||
"updatedBy": "Aggiornato da",
|
||||
"version": "Versione"
|
||||
@@ -4874,8 +4874,7 @@
|
||||
"apply": "",
|
||||
"change-value": "",
|
||||
"discard": "",
|
||||
"modal-title": "",
|
||||
"values": "Valori separati da virgola"
|
||||
"modal-title": ""
|
||||
},
|
||||
"datasource-options": {
|
||||
"name-filter": "Filtro nome",
|
||||
@@ -5969,9 +5968,6 @@
|
||||
},
|
||||
"custom-variable-form": {
|
||||
"custom-options": "Opzioni personalizzate",
|
||||
"json-values-tooltip": "",
|
||||
"name-csv-values": "",
|
||||
"name-json-values": "",
|
||||
"name-values-separated-comma": "Valori separati da virgola",
|
||||
"selection-options": "Seleziona opzioni"
|
||||
},
|
||||
@@ -6559,11 +6555,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"use-modal-editor": {
|
||||
"description": {
|
||||
"change-variable-query": ""
|
||||
}
|
||||
},
|
||||
"use-save-dashboard": {
|
||||
"message-dashboard-saved": "Dashboard salvata"
|
||||
},
|
||||
@@ -6587,7 +6578,6 @@
|
||||
"label": ""
|
||||
},
|
||||
"hidden": {
|
||||
"description": "",
|
||||
"label": ""
|
||||
},
|
||||
"hidden-label": {
|
||||
@@ -6647,9 +6637,8 @@
|
||||
"tooltip-show-usages": "Mostra utilizzi"
|
||||
},
|
||||
"variable-values-preview": {
|
||||
"show-more": "Mostra di più",
|
||||
"preview-of-values_one": "",
|
||||
"preview-of-values_other": ""
|
||||
"preview-of-values": "Anteprima dei valori",
|
||||
"show-more": "Mostra di più"
|
||||
},
|
||||
"version-history": {
|
||||
"comparison": {
|
||||
|
||||
@@ -3743,6 +3743,7 @@
|
||||
},
|
||||
"recently-viewed": {
|
||||
"clear": "",
|
||||
"empty": "",
|
||||
"error": "",
|
||||
"retry": "",
|
||||
"title": ""
|
||||
@@ -4396,7 +4397,6 @@
|
||||
},
|
||||
"no-properties-changed": "関連するプロパティは変更されていません",
|
||||
"table": {
|
||||
"notes": "",
|
||||
"updated": "日付",
|
||||
"updatedBy": "更新者",
|
||||
"version": "バージョン"
|
||||
@@ -4855,8 +4855,7 @@
|
||||
"apply": "",
|
||||
"change-value": "",
|
||||
"discard": "",
|
||||
"modal-title": "",
|
||||
"values": "カンマで区切った値"
|
||||
"modal-title": ""
|
||||
},
|
||||
"datasource-options": {
|
||||
"name-filter": "名前フィルター",
|
||||
@@ -5948,9 +5947,6 @@
|
||||
},
|
||||
"custom-variable-form": {
|
||||
"custom-options": "カスタムオプション",
|
||||
"json-values-tooltip": "",
|
||||
"name-csv-values": "",
|
||||
"name-json-values": "",
|
||||
"name-values-separated-comma": "カンマ区切りの値",
|
||||
"selection-options": "選択オプション"
|
||||
},
|
||||
@@ -6536,11 +6532,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"use-modal-editor": {
|
||||
"description": {
|
||||
"change-variable-query": ""
|
||||
}
|
||||
},
|
||||
"use-save-dashboard": {
|
||||
"message-dashboard-saved": "ダッシュボードが保存されました"
|
||||
},
|
||||
@@ -6564,7 +6555,6 @@
|
||||
"label": ""
|
||||
},
|
||||
"hidden": {
|
||||
"description": "",
|
||||
"label": ""
|
||||
},
|
||||
"hidden-label": {
|
||||
@@ -6624,8 +6614,8 @@
|
||||
"tooltip-show-usages": "使用状況を表示"
|
||||
},
|
||||
"variable-values-preview": {
|
||||
"show-more": "さらに表示",
|
||||
"preview-of-values_other": ""
|
||||
"preview-of-values": "値のプレビュー",
|
||||
"show-more": "さらに表示"
|
||||
},
|
||||
"version-history": {
|
||||
"comparison": {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user