diff --git a/.github/workflows/add-to-whats-new.yml b/.github/workflows/add-to-whats-new.yml index a6a8bb8ea2e..c388d2799a4 100644 --- a/.github/workflows/add-to-whats-new.yml +++ b/.github/workflows/add-to-whats-new.yml @@ -1,11 +1,11 @@ -name: Add comment about adding a What's new note +name: Add comment about adding a What's new note for either what's new or breaking changes on: pull_request: types: [labeled] jobs: add-comment: - if: ${{ ! github.event.pull_request.head.repo.fork && contains(github.event.pull_request.labels.*.name, 'add to what''s new') }} + if: ${{ ! github.event.pull_request.head.repo.fork && (contains(github.event.pull_request.labels.*.name, 'add to what''s new') || contains(github.event.pull_request.labels.*.name, 'breaking change') || contains(github.event.pull_request.labels.*.name, 'levitate breaking change')) }} runs-on: ubuntu-latest permissions: pull-requests: write @@ -13,4 +13,4 @@ jobs: - uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2.9.4 with: message: | - Since you've added the `Add to what's new` label, consider drafting a [What's new note](https://admin.grafana.com/content-admin/#/collections/whats-new/new) for this feature. + Since you've added the `What's New` or a breaking change label, consider drafting a [What's new note](https://admin.grafana.com/content-admin/#/collections/whats-new/new) for this feature. diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-elasticsearch/v0alpha1.elasticsearch_complex.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-elasticsearch/v0alpha1.elasticsearch_complex.v42.v2alpha1.json index 4ba14d53c0c..253b6471087 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-elasticsearch/v0alpha1.elasticsearch_complex.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-elasticsearch/v0alpha1.elasticsearch_complex.v42.v2alpha1.json @@ -7234,9 +7234,9 @@ "type": "elasticsearch", "uid": "gdev-elasticsearch" }, - "baseFilters": null, + "baseFilters": [], "filters": [], - "defaultKeys": null, + "defaultKeys": [], "hide": "dontHide", "skipUrlSync": false, "allowCustomValue": true diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-elasticsearch/v0alpha1.elasticsearch_complex.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-elasticsearch/v0alpha1.elasticsearch_complex.v42.v2beta1.json index 7de7561d8af..54edeecb540 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-elasticsearch/v0alpha1.elasticsearch_complex.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-elasticsearch/v0alpha1.elasticsearch_complex.v42.v2beta1.json @@ -7335,9 +7335,9 @@ }, "spec": { "name": "adhoc", - "baseFilters": null, + "baseFilters": [], "filters": [], - "defaultKeys": null, + "defaultKeys": [], "hide": "dontHide", "skipUrlSync": false, "allowCustomValue": true diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-influxdb/v0alpha1.influxdb-templated.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-influxdb/v0alpha1.influxdb-templated.v42.v2alpha1.json index c04c33b8edb..3b776907f83 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-influxdb/v0alpha1.influxdb-templated.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-influxdb/v0alpha1.influxdb-templated.v42.v2alpha1.json @@ -307,9 +307,9 @@ "kind": "AdhocVariable", "spec": { "name": "adhoc", - "baseFilters": null, + "baseFilters": [], "filters": [], - "defaultKeys": null, + "defaultKeys": [], "hide": "dontHide", "skipUrlSync": false, "allowCustomValue": true diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-influxdb/v0alpha1.influxdb-templated.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-influxdb/v0alpha1.influxdb-templated.v42.v2beta1.json index 731506b101b..a6ebf10b8f0 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-influxdb/v0alpha1.influxdb-templated.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-influxdb/v0alpha1.influxdb-templated.v42.v2beta1.json @@ -317,9 +317,9 @@ "datasource": {}, "spec": { "name": "adhoc", - "baseFilters": null, + "baseFilters": [], "filters": [], - "defaultKeys": null, + "defaultKeys": [], "hide": "dontHide", "skipUrlSync": false, "allowCustomValue": true diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-loki/v0alpha1.loki_fakedata.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-loki/v0alpha1.loki_fakedata.v42.v2alpha1.json index 495d5ae8ced..d8a362c5029 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-loki/v0alpha1.loki_fakedata.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-loki/v0alpha1.loki_fakedata.v42.v2alpha1.json @@ -604,9 +604,9 @@ "type": "loki", "uid": "PDDA8E780A17E7EF1" }, - "baseFilters": null, + "baseFilters": [], "filters": [], - "defaultKeys": null, + "defaultKeys": [], "label": "Ad-hoc", "hide": "dontHide", "skipUrlSync": false, diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-loki/v0alpha1.loki_fakedata.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-loki/v0alpha1.loki_fakedata.v42.v2beta1.json index 0a0142cb3ed..029c2bcf218 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-loki/v0alpha1.loki_fakedata.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/datasource-loki/v0alpha1.loki_fakedata.v42.v2beta1.json @@ -616,9 +616,9 @@ }, "spec": { "name": "adhoc", - "baseFilters": null, + "baseFilters": [], "filters": [], - "defaultKeys": null, + "defaultKeys": [], "label": "Ad-hoc", "hide": "dontHide", "skipUrlSync": false, diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_footer.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_footer.v42.v2alpha1.json index bf4adacd2bc..2697100f4cd 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_footer.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_footer.v42.v2alpha1.json @@ -1885,7 +1885,7 @@ "name": "Filters", "baseFilters": [], "filters": [], - "defaultKeys": null, + "defaultKeys": [], "hide": "dontHide", "skipUrlSync": false, "allowCustomValue": true diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_footer.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_footer.v42.v2beta1.json index 89f22b62bf0..ec3f1d1ed5c 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_footer.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_footer.v42.v2beta1.json @@ -1931,7 +1931,7 @@ "name": "Filters", "baseFilters": [], "filters": [], - "defaultKeys": null, + "defaultKeys": [], "hide": "dontHide", "skipUrlSync": false, "allowCustomValue": true diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_kitchen_sink.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_kitchen_sink.v42.v2alpha1.json index c06c7e81260..d2ec399b434 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_kitchen_sink.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_kitchen_sink.v42.v2alpha1.json @@ -2184,7 +2184,7 @@ "name": "Filters", "baseFilters": [], "filters": [], - "defaultKeys": null, + "defaultKeys": [], "hide": "dontHide", "skipUrlSync": false, "allowCustomValue": true diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_kitchen_sink.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_kitchen_sink.v42.v2beta1.json index 4b98f1b4c55..a2ea184c554 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_kitchen_sink.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_kitchen_sink.v42.v2beta1.json @@ -2219,7 +2219,7 @@ "name": "Filters", "baseFilters": [], "filters": [], - "defaultKeys": null, + "defaultKeys": [], "hide": "dontHide", "skipUrlSync": false, "allowCustomValue": true diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_v12_2_migrations.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_v12_2_migrations.v42.v2alpha1.json index de3fd99993b..b8e6725ca5a 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_v12_2_migrations.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_v12_2_migrations.v42.v2alpha1.json @@ -2499,7 +2499,7 @@ "name": "Filters", "baseFilters": [], "filters": [], - "defaultKeys": null, + "defaultKeys": [], "hide": "dontHide", "skipUrlSync": false, "allowCustomValue": true diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_v12_2_migrations.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_v12_2_migrations.v42.v2beta1.json index fb856d2bdea..4a7ea3bcf33 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_v12_2_migrations.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-table/v0alpha1.table_v12_2_migrations.v42.v2beta1.json @@ -2542,7 +2542,7 @@ "name": "Filters", "baseFilters": [], "filters": [], - "defaultKeys": null, + "defaultKeys": [], "hide": "dontHide", "skipUrlSync": false, "allowCustomValue": true diff --git a/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go b/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go index 231c0ad4131..236a4337efb 100644 --- a/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go +++ b/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go @@ -1539,26 +1539,38 @@ func buildAdhocVariable(ctx context.Context, varMap map[string]interface{}, comm }, } - // Transform baseFilters if they exist + // Transform baseFilters if they exist, otherwise default to empty array if baseFilters, exists := varMap["baseFilters"]; exists { if baseFiltersArray, ok := baseFilters.([]interface{}); ok { adhocVar.Spec.BaseFilters = transformAdHocFilters(baseFiltersArray) } } + // Ensure baseFilters is always set (default to empty array if not present or invalid) + if adhocVar.Spec.BaseFilters == nil { + adhocVar.Spec.BaseFilters = []dashv2alpha1.DashboardAdHocFilterWithLabels{} + } - // Transform filters if they exist + // Transform filters if they exist, otherwise default to empty array if filters, exists := varMap["filters"]; exists { if filtersArray, ok := filters.([]interface{}); ok { adhocVar.Spec.Filters = transformAdHocFilters(filtersArray) } } + // Ensure filters is always set (default to empty array if not present or invalid) + if adhocVar.Spec.Filters == nil { + adhocVar.Spec.Filters = []dashv2alpha1.DashboardAdHocFilterWithLabels{} + } - // Transform defaultKeys if they exist + // Transform defaultKeys if they exist, otherwise default to empty array if defaultKeys, exists := varMap["defaultKeys"]; exists { if defaultKeysArray, ok := defaultKeys.([]interface{}); ok { adhocVar.Spec.DefaultKeys = transformMetricFindValues(defaultKeysArray) } } + // Ensure defaultKeys is always set (default to empty array if not present or invalid) + if adhocVar.Spec.DefaultKeys == nil { + adhocVar.Spec.DefaultKeys = []dashv2alpha1.DashboardMetricFindValue{} + } // Only include datasource if datasourceUID exists (matching frontend behavior) if datasourceUID != "" { diff --git a/apps/iam/kinds/manifest.cue b/apps/iam/kinds/manifest.cue index bc9c3d47ad7..c6f609cbe15 100644 --- a/apps/iam/kinds/manifest.cue +++ b/apps/iam/kinds/manifest.cue @@ -22,4 +22,32 @@ v0alpha1: { serviceaccountv0alpha1, externalGroupMappingv0alpha1 ] + routes: { + namespaced: { + "/searchTeams": { + "GET": { + request: { + query: { + query?: string + } + } + response: { + #TeamHit: { + name: string + title: string + email: string + provisioned: bool + externalUID: string + } + offset: int64 + totalHits: int64 + hits: [...#TeamHit] + queryCost: float64 + maxScore: float64 + } + responseMetadata: objectMeta: false + } + } + } + } } diff --git a/apps/iam/pkg/apis/iam/v0alpha1/getsearchteams_request_params_object_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/getsearchteams_request_params_object_gen.go new file mode 100644 index 00000000000..76ba539c798 --- /dev/null +++ b/apps/iam/pkg/apis/iam/v0alpha1/getsearchteams_request_params_object_gen.go @@ -0,0 +1,33 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +import ( + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +type GetSearchTeamsRequestParamsObject struct { + metav1.TypeMeta `json:",inline"` + GetSearchTeamsRequestParams `json:",inline"` +} + +func NewGetSearchTeamsRequestParamsObject() *GetSearchTeamsRequestParamsObject { + return &GetSearchTeamsRequestParamsObject{} +} + +func (o *GetSearchTeamsRequestParamsObject) DeepCopyObject() runtime.Object { + dst := NewGetSearchTeamsRequestParamsObject() + o.DeepCopyInto(dst) + return dst +} + +func (o *GetSearchTeamsRequestParamsObject) DeepCopyInto(dst *GetSearchTeamsRequestParamsObject) { + dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion + dst.TypeMeta.Kind = o.TypeMeta.Kind + dstGetSearchTeamsRequestParams := GetSearchTeamsRequestParams{} + _ = resource.CopyObjectInto(&dstGetSearchTeamsRequestParams, &o.GetSearchTeamsRequestParams) +} + +var _ runtime.Object = NewGetSearchTeamsRequestParamsObject() diff --git a/apps/iam/pkg/apis/iam/v0alpha1/getsearchteams_request_params_types_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/getsearchteams_request_params_types_gen.go new file mode 100644 index 00000000000..ffa5067c41b --- /dev/null +++ b/apps/iam/pkg/apis/iam/v0alpha1/getsearchteams_request_params_types_gen.go @@ -0,0 +1,12 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +type GetSearchTeamsRequestParams struct { + Query *string `json:"query,omitempty"` +} + +// NewGetSearchTeamsRequestParams creates a new GetSearchTeamsRequestParams object. +func NewGetSearchTeamsRequestParams() *GetSearchTeamsRequestParams { + return &GetSearchTeamsRequestParams{} +} diff --git a/apps/iam/pkg/apis/iam/v0alpha1/getsearchteams_response_body_types_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/getsearchteams_response_body_types_gen.go new file mode 100644 index 00000000000..9fabe93260d --- /dev/null +++ b/apps/iam/pkg/apis/iam/v0alpha1/getsearchteams_response_body_types_gen.go @@ -0,0 +1,33 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +// +k8s:openapi-gen=true +type VersionsV0alpha1RoutesNamespacedSearchTeamsGETResponseTeamHit struct { + Name string `json:"name"` + Title string `json:"title"` + Email string `json:"email"` + Provisioned bool `json:"provisioned"` + ExternalUID string `json:"externalUID"` +} + +// NewVersionsV0alpha1RoutesNamespacedSearchTeamsGETResponseTeamHit creates a new VersionsV0alpha1RoutesNamespacedSearchTeamsGETResponseTeamHit object. +func NewVersionsV0alpha1RoutesNamespacedSearchTeamsGETResponseTeamHit() *VersionsV0alpha1RoutesNamespacedSearchTeamsGETResponseTeamHit { + return &VersionsV0alpha1RoutesNamespacedSearchTeamsGETResponseTeamHit{} +} + +// +k8s:openapi-gen=true +type GetSearchTeamsBody struct { + Offset int64 `json:"offset"` + TotalHits int64 `json:"totalHits"` + Hits []VersionsV0alpha1RoutesNamespacedSearchTeamsGETResponseTeamHit `json:"hits"` + QueryCost float64 `json:"queryCost"` + MaxScore float64 `json:"maxScore"` +} + +// NewGetSearchTeamsBody creates a new GetSearchTeamsBody object. +func NewGetSearchTeamsBody() *GetSearchTeamsBody { + return &GetSearchTeamsBody{ + Hits: []VersionsV0alpha1RoutesNamespacedSearchTeamsGETResponseTeamHit{}, + } +} diff --git a/apps/iam/pkg/apis/iam/v0alpha1/getsearchteams_response_object_types_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/getsearchteams_response_object_types_gen.go new file mode 100644 index 00000000000..685bd7d85be --- /dev/null +++ b/apps/iam/pkg/apis/iam/v0alpha1/getsearchteams_response_object_types_gen.go @@ -0,0 +1,37 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +import ( + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// +k8s:openapi-gen=true +type GetSearchTeams struct { + metav1.TypeMeta `json:",inline"` + GetSearchTeamsBody `json:",inline"` +} + +func NewGetSearchTeams() *GetSearchTeams { + return &GetSearchTeams{} +} + +func (t *GetSearchTeamsBody) DeepCopyInto(dst *GetSearchTeamsBody) { + _ = resource.CopyObjectInto(dst, t) +} + +func (o *GetSearchTeams) DeepCopyObject() runtime.Object { + dst := NewGetSearchTeams() + o.DeepCopyInto(dst) + return dst +} + +func (o *GetSearchTeams) DeepCopyInto(dst *GetSearchTeams) { + dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion + dst.TypeMeta.Kind = o.TypeMeta.Kind + o.GetSearchTeamsBody.DeepCopyInto(&dst.GetSearchTeamsBody) +} + +var _ runtime.Object = NewGetSearchTeams() diff --git a/apps/iam/pkg/apis/iam/v0alpha1/register.go b/apps/iam/pkg/apis/iam/v0alpha1/register.go index be70a0e1714..05dbc30e684 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/register.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/register.go @@ -317,6 +317,7 @@ func AddAuthNKnownTypes(scheme *runtime.Scheme) error { &ServiceAccountList{}, &Team{}, &TeamList{}, + &GetSearchTeams{}, &TeamBinding{}, &TeamBindingList{}, &ExternalGroupMapping{}, diff --git a/apps/iam/pkg/apis/iam/v0alpha1/team_search.go b/apps/iam/pkg/apis/iam/v0alpha1/team_search.go new file mode 100644 index 00000000000..545552a4795 --- /dev/null +++ b/apps/iam/pkg/apis/iam/v0alpha1/team_search.go @@ -0,0 +1,35 @@ +package v0alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +k8s:deepcopy-gen=true +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type TeamSearchResults struct { + metav1.TypeMeta `json:",inline"` + + // Where the query started from + Offset int64 `json:"offset,omitempty"` + + // The number of matching results + TotalHits int64 `json:"totalHits"` + + // The team body + Hits []TeamHit `json:"hits"` + + // Cost of running the query + QueryCost float64 `json:"queryCost,omitempty"` + + // Max score + MaxScore float64 `json:"maxScore,omitempty"` +} + +// +k8s:deepcopy-gen=true +type TeamHit struct { + Name string `json:"name"` + Title string `json:"title"` + Email string `json:"email,omitempty"` + Provisioned bool `json:"provisioned,omitempty"` + ExternalUID string `json:"externalUID,omitempty"` +} diff --git a/apps/iam/pkg/apis/iam/v0alpha1/zz_openapi_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/zz_openapi_gen.go index e3b024799d7..a41d51e8d70 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/zz_openapi_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/zz_openapi_gen.go @@ -24,6 +24,8 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ExternalGroupMappingTeamRef": schema_pkg_apis_iam_v0alpha1_ExternalGroupMappingTeamRef(ref), "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GetGroups": schema_pkg_apis_iam_v0alpha1_GetGroups(ref), "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GetGroupsBody": schema_pkg_apis_iam_v0alpha1_GetGroupsBody(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GetSearchTeams": schema_pkg_apis_iam_v0alpha1_GetSearchTeams(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GetSearchTeamsBody": schema_pkg_apis_iam_v0alpha1_GetSearchTeamsBody(ref), "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRole": schema_pkg_apis_iam_v0alpha1_GlobalRole(ref), "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleBinding": schema_pkg_apis_iam_v0alpha1_GlobalRoleBinding(ref), "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleBindingList": schema_pkg_apis_iam_v0alpha1_GlobalRoleBindingList(ref), @@ -80,6 +82,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserStatus": schema_pkg_apis_iam_v0alpha1_UserStatus(ref), "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserstatusOperatorState": schema_pkg_apis_iam_v0alpha1_UserstatusOperatorState(ref), "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.VersionsV0alpha1Kinds7RoutesGroupsGETResponseExternalGroupMapping": schema_pkg_apis_iam_v0alpha1_VersionsV0alpha1Kinds7RoutesGroupsGETResponseExternalGroupMapping(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.VersionsV0alpha1RoutesNamespacedSearchTeamsGETResponseTeamHit": schema_pkg_apis_iam_v0alpha1_VersionsV0alpha1RoutesNamespacedSearchTeamsGETResponseTeamHit(ref), } } @@ -564,6 +567,132 @@ func schema_pkg_apis_iam_v0alpha1_GetGroupsBody(ref common.ReferenceCallback) co } } +func schema_pkg_apis_iam_v0alpha1_GetSearchTeams(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "offset": { + SchemaProps: spec.SchemaProps{ + Default: 0, + Type: []string{"integer"}, + Format: "int64", + }, + }, + "totalHits": { + SchemaProps: spec.SchemaProps{ + Default: 0, + Type: []string{"integer"}, + Format: "int64", + }, + }, + "hits": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.VersionsV0alpha1RoutesNamespacedSearchTeamsGETResponseTeamHit"), + }, + }, + }, + }, + }, + "queryCost": { + SchemaProps: spec.SchemaProps{ + Default: 0, + Type: []string{"number"}, + Format: "double", + }, + }, + "maxScore": { + SchemaProps: spec.SchemaProps{ + Default: 0, + Type: []string{"number"}, + Format: "double", + }, + }, + }, + Required: []string{"offset", "totalHits", "hits", "queryCost", "maxScore"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.VersionsV0alpha1RoutesNamespacedSearchTeamsGETResponseTeamHit"}, + } +} + +func schema_pkg_apis_iam_v0alpha1_GetSearchTeamsBody(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "offset": { + SchemaProps: spec.SchemaProps{ + Default: 0, + Type: []string{"integer"}, + Format: "int64", + }, + }, + "totalHits": { + SchemaProps: spec.SchemaProps{ + Default: 0, + Type: []string{"integer"}, + Format: "int64", + }, + }, + "hits": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.VersionsV0alpha1RoutesNamespacedSearchTeamsGETResponseTeamHit"), + }, + }, + }, + }, + }, + "queryCost": { + SchemaProps: spec.SchemaProps{ + Default: 0, + Type: []string{"number"}, + Format: "double", + }, + }, + "maxScore": { + SchemaProps: spec.SchemaProps{ + Default: 0, + Type: []string{"number"}, + Format: "double", + }, + }, + }, + Required: []string{"offset", "totalHits", "hits", "queryCost", "maxScore"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.VersionsV0alpha1RoutesNamespacedSearchTeamsGETResponseTeamHit"}, + } +} + func schema_pkg_apis_iam_v0alpha1_GlobalRole(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -2956,3 +3085,51 @@ func schema_pkg_apis_iam_v0alpha1_VersionsV0alpha1Kinds7RoutesGroupsGETResponseE }, } } + +func schema_pkg_apis_iam_v0alpha1_VersionsV0alpha1RoutesNamespacedSearchTeamsGETResponseTeamHit(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "name": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "title": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "email": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "provisioned": { + SchemaProps: spec.SchemaProps{ + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + "externalUID": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"name", "title", "email", "provisioned", "externalUID"}, + }, + }, + } +} diff --git a/apps/iam/pkg/apis/iam_manifest.go b/apps/iam/pkg/apis/iam_manifest.go index 9fe171525b4..5c27c228ed2 100644 --- a/apps/iam/pkg/apis/iam_manifest.go +++ b/apps/iam/pkg/apis/iam_manifest.go @@ -157,9 +157,139 @@ var appManifestData = app.ManifestData{ }, }, Routes: app.ManifestVersionRoutes{ - Namespaced: map[string]spec3.PathProps{}, - Cluster: map[string]spec3.PathProps{}, - Schemas: map[string]spec.Schema{}, + Namespaced: map[string]spec3.PathProps{ + "/searchTeams": { + Get: &spec3.Operation{ + OperationProps: spec3.OperationProps{ + + OperationId: "getSearchTeams", + + Parameters: []*spec3.Parameter{ + + { + ParameterProps: spec3.ParameterProps{ + Name: "query", + In: "query", + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + }, + }, + }, + + Responses: &spec3.Responses{ + ResponsesProps: spec3.ResponsesProps{ + Default: &spec3.Response{ + ResponseProps: spec3.ResponseProps{ + Description: "Default OK response", + Content: map[string]*spec3.MediaType{ + "application/json": { + MediaTypeProps: spec3.MediaTypeProps{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + }, + }, + "hits": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + }, + }, + "kind": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + }, + }, + "maxScore": { + SchemaProps: spec.SchemaProps{ + Type: []string{"number"}, + }, + }, + "offset": { + SchemaProps: spec.SchemaProps{ + Type: []string{"integer"}, + }, + }, + "queryCost": { + SchemaProps: spec.SchemaProps{ + Type: []string{"number"}, + }, + }, + "totalHits": { + SchemaProps: spec.SchemaProps{ + Type: []string{"integer"}, + }, + }, + }, + Required: []string{ + "offset", + "totalHits", + "hits", + "queryCost", + "maxScore", + "apiVersion", + "kind", + }, + }}, + }}, + }, + }, + }, + }}, + }, + }, + }, + }, + Cluster: map[string]spec3.PathProps{}, + Schemas: map[string]spec.Schema{ + "getSearchTeamsTeamHit": { + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "email": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + "externalUID": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + "name": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + "provisioned": { + SchemaProps: spec.SchemaProps{ + Type: []string{"boolean"}, + }, + }, + "title": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + }, + Required: []string{ + "name", + "title", + "email", + "provisioned", + "externalUID", + }, + }, + }, + }, }, }, }, @@ -196,6 +326,8 @@ func ManifestGoTypeAssociator(kind, version string) (goType resource.Kind, exist var customRouteToGoResponseType = map[string]any{ "v0alpha1|Team|groups|GET": v0alpha1.GetGroups{}, + + "v0alpha1||/searchTeams|GET": v0alpha1.GetSearchTeams{}, } // ManifestCustomRouteResponsesAssociator returns the associated response go type for a given kind, version, custom route path, and method, if one exists. diff --git a/docs/sources/visualizations/dashboards/share-dashboards-panels/shared-dashboards/index.md b/docs/sources/visualizations/dashboards/share-dashboards-panels/shared-dashboards/index.md index a96efda3edf..b0dc5e723b2 100644 --- a/docs/sources/visualizations/dashboards/share-dashboards-panels/shared-dashboards/index.md +++ b/docs/sources/visualizations/dashboards/share-dashboards-panels/shared-dashboards/index.md @@ -37,6 +37,11 @@ refs: destination: /docs/grafana//introduction/grafana-enterprise/ - pattern: /docs/grafana-cloud/ destination: /docs/grafana//introduction/grafana-enterprise/ + fixed-role-definitions: + - pattern: /docs/grafana/ + destination: /docs/grafana//administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/#fixed-role-definitions + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/security-and-account-management/authentication-and-permissions/access-control/rbac-fixed-basic-role-definitions/#fixed-role-definitions --- # Externally shared dashboards @@ -49,6 +54,8 @@ Externally shared dashboards allow you to share your Grafana dashboard with anyo If you change a dashboard, ensure that you save the changes before sharing. +In order to create public dashboards, you need to be an Admin, have the `dashboards.public:write` permission, or the `fixed:dashboards.public:write` [RBAC role](ref:fixed-role-definitions). + {{< admonition type="warning" >}} Sharing your dashboard externally could result in a large number of queries to the data sources used by your dashboard. This can be mitigated by using the Enterprise [caching](ref:caching) and/or rate limiting features. diff --git a/e2e-playwright/various-suite/prometheus-variable-editor.spec.ts b/e2e-playwright/various-suite/prometheus-variable-editor.spec.ts index 16a722854aa..82b293da581 100644 --- a/e2e-playwright/various-suite/prometheus-variable-editor.spec.ts +++ b/e2e-playwright/various-suite/prometheus-variable-editor.spec.ts @@ -95,7 +95,7 @@ test.describe( await createNewPanelButton.click(); // Close the data source picker modal - const closeButton = page.getByRole('button', { name: 'Close menu' }); + const closeButton = page.getByRole('button', { name: 'Close', exact: true }); await closeButton.click({ force: true }); // Select prom data source from the data source list diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 96fa771540d..9ecdfaed054 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -710,11 +710,6 @@ "count": 1 } }, - "packages/grafana-ui/src/components/QueryField/QueryField.story.tsx": { - "no-restricted-syntax": { - "count": 1 - } - }, "packages/grafana-ui/src/components/QueryField/QueryField.tsx": { "react-prefer-function-component/react-prefer-function-component": { "count": 1 diff --git a/package.json b/package.json index 5d7935110eb..e9ce0640360 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,7 @@ "@emotion/eslint-plugin": "11.12.0", "@grafana/eslint-config": "8.2.0", "@grafana/eslint-plugin": "link:./packages/grafana-eslint-rules", - "@grafana/plugin-e2e": "^3.0.1", + "@grafana/plugin-e2e": "^3.0.3", "@grafana/test-utils": "workspace:*", "@manypkg/get-packages": "^3.0.0", "@npmcli/package-json": "^6.0.0", diff --git a/packages/grafana-api-clients/src/clients/rtkq/iam/v0alpha1/endpoints.gen.ts b/packages/grafana-api-clients/src/clients/rtkq/iam/v0alpha1/endpoints.gen.ts index 88ce5985631..8b0690f57ba 100644 --- a/packages/grafana-api-clients/src/clients/rtkq/iam/v0alpha1/endpoints.gen.ts +++ b/packages/grafana-api-clients/src/clients/rtkq/iam/v0alpha1/endpoints.gen.ts @@ -3,6 +3,7 @@ export const addTagTypes = [ 'API Discovery', 'Display', 'ExternalGroupMapping', + 'Search', 'ServiceAccount', 'SSOSetting', 'TeamBinding', @@ -152,6 +153,18 @@ const injectedRtkApi = api }), invalidatesTags: ['ExternalGroupMapping'], }), + getSearchTeams: build.query({ + query: (queryArg) => ({ + url: `/searchTeams`, + params: { + query: queryArg.query, + limit: queryArg.limit, + offset: queryArg.offset, + page: queryArg.page, + }, + }), + providesTags: ['Search'], + }), listServiceAccount: build.query({ query: (queryArg) => ({ url: `/serviceaccounts`, @@ -862,6 +875,27 @@ export type UpdateExternalGroupMappingApiArg = { force?: boolean; patch: Patch; }; +export type GetSearchTeamsApiResponse = /** status 200 undefined */ { + /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + hits: any[]; + /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + maxScore: number; + offset: number; + queryCost: number; + totalHits: number; +}; +export type GetSearchTeamsApiArg = { + /** team name query string */ + query?: string; + /** limit the number of results */ + limit?: number; + /** start the query at the given offset */ + offset?: number; + /** page number to start from */ + page?: number; +}; export type ListServiceAccountApiResponse = /** status 200 OK */ ServiceAccountList; export type ListServiceAccountApiArg = { /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ @@ -2084,6 +2118,8 @@ export const { useReplaceExternalGroupMappingMutation, useDeleteExternalGroupMappingMutation, useUpdateExternalGroupMappingMutation, + useGetSearchTeamsQuery, + useLazyGetSearchTeamsQuery, useListServiceAccountQuery, useLazyListServiceAccountQuery, useCreateServiceAccountMutation, diff --git a/packages/grafana-data/src/field/fieldColor.ts b/packages/grafana-data/src/field/fieldColor.ts index 7016c208994..78e0b632619 100644 --- a/packages/grafana-data/src/field/fieldColor.ts +++ b/packages/grafana-data/src/field/fieldColor.ts @@ -9,6 +9,8 @@ import { import stringHash from 'string-hash'; import tinycolor from 'tinycolor2'; +import { t } from '@grafana/i18n'; + import { getContrastRatio } from '../themes/colorManipulator'; import { GrafanaTheme2 } from '../themes/types'; import { reduceField } from '../transformations/fieldReducer'; @@ -30,10 +32,14 @@ export interface FieldColorMode extends RegistryItem { isContinuous?: boolean; isByValue?: boolean; useSeriesName?: boolean; + group?: string; } /** @internal */ export const fieldColorModeRegistry = new Registry(() => { + const accessibleGroup = t('grafana-data.field.fieldColor.accessibleGroup', 'Accessible'); + const otherGroup = t('grafana-data.field.fieldColor.otherGroup', 'Others'); + return [ { id: FieldColorModeId.Fixed, @@ -88,6 +94,7 @@ export const fieldColorModeRegistry = new Registry(() => { isContinuous: true, isByValue: true, interpolator: interpolateViridis, + group: accessibleGroup, }), new FieldColorSchemeMode({ id: FieldColorModeId.ContinuousMagma, @@ -95,6 +102,7 @@ export const fieldColorModeRegistry = new Registry(() => { isContinuous: true, isByValue: true, interpolator: interpolateMagma, + group: accessibleGroup, }), new FieldColorSchemeMode({ id: FieldColorModeId.ContinuousPlasma, @@ -102,6 +110,7 @@ export const fieldColorModeRegistry = new Registry(() => { isContinuous: true, isByValue: true, interpolator: interpolatePlasma, + group: accessibleGroup, }), new FieldColorSchemeMode({ id: FieldColorModeId.ContinuousInferno, @@ -109,6 +118,7 @@ export const fieldColorModeRegistry = new Registry(() => { isContinuous: true, isByValue: true, interpolator: interpolateInferno, + group: accessibleGroup, }), new FieldColorSchemeMode({ id: FieldColorModeId.ContinuousCividis, @@ -116,6 +126,7 @@ export const fieldColorModeRegistry = new Registry(() => { isContinuous: true, isByValue: true, interpolator: interpolateCividis, + group: accessibleGroup, }), new FieldColorSchemeMode({ id: FieldColorModeId.ContinuousGrYlRd, @@ -123,6 +134,7 @@ export const fieldColorModeRegistry = new Registry(() => { isContinuous: true, isByValue: true, getColors: (theme: GrafanaTheme2) => ['green', 'yellow', 'red'], + group: otherGroup, }), new FieldColorSchemeMode({ id: FieldColorModeId.ContinuousRdYlGr, @@ -130,6 +142,7 @@ export const fieldColorModeRegistry = new Registry(() => { isContinuous: true, isByValue: true, getColors: (theme: GrafanaTheme2) => ['red', 'yellow', 'green'], + group: otherGroup, }), new FieldColorSchemeMode({ id: FieldColorModeId.ContinuousBlYlRd, @@ -137,6 +150,7 @@ export const fieldColorModeRegistry = new Registry(() => { isContinuous: true, isByValue: true, getColors: (theme: GrafanaTheme2) => ['dark-blue', 'super-light-yellow', 'dark-red'], + group: otherGroup, }), new FieldColorSchemeMode({ id: FieldColorModeId.ContinuousYlRd, @@ -144,6 +158,7 @@ export const fieldColorModeRegistry = new Registry(() => { isContinuous: true, isByValue: true, getColors: (theme: GrafanaTheme2) => ['super-light-yellow', 'dark-red'], + group: otherGroup, }), new FieldColorSchemeMode({ id: FieldColorModeId.ContinuousBlPu, @@ -151,6 +166,7 @@ export const fieldColorModeRegistry = new Registry(() => { isContinuous: true, isByValue: true, getColors: (theme: GrafanaTheme2) => ['blue', 'purple'], + group: otherGroup, }), new FieldColorSchemeMode({ id: FieldColorModeId.ContinuousYlBl, @@ -158,6 +174,7 @@ export const fieldColorModeRegistry = new Registry(() => { isContinuous: true, isByValue: true, getColors: (theme: GrafanaTheme2) => ['super-light-yellow', 'dark-blue'], + group: otherGroup, }), new FieldColorSchemeMode({ id: FieldColorModeId.ContinuousBlues, @@ -165,6 +182,7 @@ export const fieldColorModeRegistry = new Registry(() => { isContinuous: true, isByValue: true, getColors: (theme: GrafanaTheme2) => ['panel-bg', 'dark-blue'], + group: otherGroup, }), new FieldColorSchemeMode({ id: FieldColorModeId.ContinuousReds, @@ -172,6 +190,7 @@ export const fieldColorModeRegistry = new Registry(() => { isContinuous: true, isByValue: true, getColors: (theme: GrafanaTheme2) => ['panel-bg', 'dark-red'], + group: otherGroup, }), new FieldColorSchemeMode({ id: FieldColorModeId.ContinuousGreens, @@ -179,6 +198,7 @@ export const fieldColorModeRegistry = new Registry(() => { isContinuous: true, isByValue: true, getColors: (theme: GrafanaTheme2) => ['panel-bg', 'dark-green'], + group: otherGroup, }), new FieldColorSchemeMode({ id: FieldColorModeId.ContinuousPurples, @@ -186,6 +206,7 @@ export const fieldColorModeRegistry = new Registry(() => { isContinuous: true, isByValue: true, getColors: (theme: GrafanaTheme2) => ['panel-bg', 'dark-purple'], + group: otherGroup, }), ]; }); @@ -197,6 +218,7 @@ interface BaseFieldColorSchemeModeOptions { isContinuous: boolean; isByValue: boolean; useSeriesName?: boolean; + group?: string; } interface FieldColorSchemeModeInterpolator extends BaseFieldColorSchemeModeOptions { @@ -222,6 +244,7 @@ export class FieldColorSchemeMode implements FieldColorMode { colorCacheTheme?: GrafanaTheme2; interpolator?: (value: number) => string; getNamedColors?: (theme: GrafanaTheme2) => string[]; + group?: string; constructor(options: FieldColorSchemeModeOptions) { this.id = options.id; @@ -232,6 +255,7 @@ export class FieldColorSchemeMode implements FieldColorMode { this.isByValue = options.isByValue; this.useSeriesName = options.useSeriesName; this.interpolator = options.interpolator; + this.group = options.group; } getColors(theme: GrafanaTheme2): string[] { diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 3fab1ac8bd5..eba76d2c198 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -377,10 +377,14 @@ export interface FeatureToggles { */ perPanelNonApplicableDrilldowns?: boolean; /** - * Enabled a group by action per panel + * Enables a group by action per panel */ panelGroupBy?: boolean; /** + * Enables filtering by grouping labels on the panel level through legend or tooltip + */ + perPanelFiltering?: boolean; + /** * Enables use of the `systemPanelFilterVar` variable to filter panels in a dashboard */ panelFilterVariable?: boolean; diff --git a/packages/grafana-data/src/types/pluginExtensions.ts b/packages/grafana-data/src/types/pluginExtensions.ts index 40d20b88c38..cc2a97b4a50 100644 --- a/packages/grafana-data/src/types/pluginExtensions.ts +++ b/packages/grafana-data/src/types/pluginExtensions.ts @@ -30,6 +30,7 @@ export type PluginExtensionLink = PluginExtensionBase & { onClick?: (event?: React.MouseEvent) => void; icon?: IconName; category?: string; + openInNewTab?: boolean; }; export type PluginExtensionComponentMeta = Omit; @@ -86,6 +87,7 @@ export type PluginExtensionAddedComponentConfig = PluginExtensionCon */ component: React.ComponentType; }; + export type PluginExtensionAddedFunctionConfig = PluginExtensionConfigBase & { /** * The target extension points where the component will be added @@ -106,6 +108,7 @@ export type PluginAddedLinksConfigureFunc = (context: Re onClick: (event: React.MouseEvent | undefined, helpers: PluginExtensionEventHelpers) => void; icon: IconName; category: string; + openInNewTab: boolean; }> | undefined; @@ -137,6 +140,10 @@ export type PluginExtensionAddedLinkConfig = Pl // (Optional) A category to be used when grouping the options in the ui category?: string; + + // (Optional) If true, opens the link in a new tab (renders with target="_blank") + // (Important: this is not guaranteed, depends on the extension point if it implements it.) + openInNewTab?: boolean; }; export type PluginExtensionExposedComponentConfig = PluginExtensionConfigBase & { diff --git a/packages/grafana-e2e-selectors/src/selectors/components.ts b/packages/grafana-e2e-selectors/src/selectors/components.ts index 47c30187175..aca18844459 100644 --- a/packages/grafana-e2e-selectors/src/selectors/components.ts +++ b/packages/grafana-e2e-selectors/src/selectors/components.ts @@ -1490,6 +1490,16 @@ export const versionedComponents = { }, }, }, + VizTooltipFooter: { + buttons: { + apply: { + ['12.1.0']: 'data-testid viz-tooltip-footer-apply-filters-button', + }, + applyInverse: { + ['12.1.0']: 'data-testid viz-tooltip-footer-apply-inverse-filters-button', + }, + }, + }, } satisfies VersionedSelectorGroup; export type VersionedComponents = typeof versionedComponents; diff --git a/packages/grafana-ui/src/components/Icon/Icon.tsx b/packages/grafana-ui/src/components/Icon/Icon.tsx index fa4e1fac6d3..fae4b284a01 100644 --- a/packages/grafana-ui/src/components/Icon/Icon.tsx +++ b/packages/grafana-ui/src/components/Icon/Icon.tsx @@ -93,7 +93,7 @@ export const Icon = React.memo( // this prevents content layout shift whilst the icon asynchronously loads // which happens even if the icon is in the cache(!) loader={ - void; + /** + * Returns filters based on existing grouping or an empty array + */ + getFiltersBasedOnGrouping?: (items: AdHocFilterItem[]) => AdHocFilterItem[]; + /** + * + * Used to apply multiple filters at once + */ + onAddAdHocFilters?: (items: AdHocFilterItem[]) => void; /** * Enables modifying thresholds directly from the panel * diff --git a/packages/grafana-ui/src/components/QueryField/QueryField.story.tsx b/packages/grafana-ui/src/components/QueryField/QueryField.story.tsx index f0f672fa120..24aaee5af3a 100644 --- a/packages/grafana-ui/src/components/QueryField/QueryField.story.tsx +++ b/packages/grafana-ui/src/components/QueryField/QueryField.story.tsx @@ -1,6 +1,9 @@ import { Meta, StoryFn } from '@storybook/react'; +import { useId } from 'react'; import { TypeaheadInput } from '../../types/completion'; +import { Field } from '../Forms/Field'; +import { Label } from '../Forms/Label'; import { QueryField, QueryFieldProps } from './QueryField'; @@ -24,8 +27,6 @@ const meta: Meta = { 'syntaxLoaded', ], }, - // TODO fix a11y issue in story and remove this - a11y: { test: 'off' }, }, argTypes: { query: { @@ -34,7 +35,16 @@ const meta: Meta = { }, }; -export const Basic: StoryFn = (args: Omit) => ; +export const Basic: StoryFn = (args: Omit) => { + const id = useId(); + // have to manually set an id on the label + // can't use htmlFor as QueryField is a contenteditable div, not an input + return ( + Query field}> + + + ); +}; Basic.args = { onTypeahead: async (_input: TypeaheadInput) => ({ diff --git a/packages/grafana-ui/src/components/QueryField/QueryField.tsx b/packages/grafana-ui/src/components/QueryField/QueryField.tsx index b9cd4fcd67c..a0583ee7121 100644 --- a/packages/grafana-ui/src/components/QueryField/QueryField.tsx +++ b/packages/grafana-ui/src/components/QueryField/QueryField.tsx @@ -25,6 +25,7 @@ import { makeValue, SCHEMA } from '../../utils/slate'; export interface QueryFieldProps extends Themeable2 { additionalPlugins?: Plugin[]; + ['aria-labelledby']?: string; cleanText?: (text: string) => string; disabled?: boolean; // We have both value and local state. This is usually an antipattern but we need to keep local state @@ -201,7 +202,7 @@ export class UnThemedQueryField extends PureComponent { this.editor = editor; }} + aria-labelledby={ariaLabelledby} schema={SCHEMA} autoCorrect={false} readOnly={this.props.disabled} diff --git a/packages/grafana-ui/src/components/ToolbarButton/ToolbarButton.tsx b/packages/grafana-ui/src/components/ToolbarButton/ToolbarButton.tsx index c1f99b0db93..2193103a90f 100644 --- a/packages/grafana-ui/src/components/ToolbarButton/ToolbarButton.tsx +++ b/packages/grafana-ui/src/components/ToolbarButton/ToolbarButton.tsx @@ -206,7 +206,7 @@ const getStyles = (theme: GrafanaTheme2) => { background: 'transparent', border: `1px solid transparent`, - '&:hover, &:focus': { + '&:hover': { color: theme.colors.text.primary, background: theme.colors.action.hover, }, diff --git a/packages/grafana-ui/src/components/VizTooltip/VizTooltipFooter.test.tsx b/packages/grafana-ui/src/components/VizTooltip/VizTooltipFooter.test.tsx index f23961ef5e4..8e48305c0a9 100644 --- a/packages/grafana-ui/src/components/VizTooltip/VizTooltipFooter.test.tsx +++ b/packages/grafana-ui/src/components/VizTooltip/VizTooltipFooter.test.tsx @@ -3,6 +3,7 @@ import userEvent from '@testing-library/user-event'; import { MemoryRouter } from 'react-router-dom-v5-compat'; import { Field, FieldType, LinkModel } from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; import { VizTooltipFooter, AdHocFilterModel } from './VizTooltipFooter'; @@ -89,4 +90,65 @@ describe('VizTooltipFooter', () => { expect(screen.queryByRole('button', { name: /filter for 'testValue'/i })).not.toBeInTheDocument(); }); + + it('should render filter by grouping buttons and fire onclick', async () => { + const onForClick = jest.fn(); + const onOutClick = jest.fn(); + + const filterByGroupedLabels = { + onFilterForGroupedLabels: onForClick, + onFilterOutGroupedLabels: onOutClick, + }; + + render( + + + + ); + + const onForButton = screen.getByRole('button', { name: /Apply as filter/i }); + expect(onForButton).toBeInTheDocument(); + + const onOutButton = screen.getByRole('button', { name: /Apply as inverse filter/i }); + expect(onOutButton).toBeInTheDocument(); + + await userEvent.click(onForButton); + expect(onForClick).toHaveBeenCalled(); + + await userEvent.click(onOutButton); + expect(onOutClick).toHaveBeenCalled(); + }); + + it('should not render filter by grouping buttons when there are one-click links', () => { + const filterByGroupedLabels = { + onFilterForGroupedLabels: jest.fn(), + onFilterOutGroupedLabels: jest.fn(), + }; + + const onClick = jest.fn(); + const field: Field = { + name: '', + type: FieldType.string, + values: [], + config: {}, + }; + + const oneClickLink: LinkModel = { + href: '#', + onClick, + title: 'One Click Link', + origin: field, + target: undefined, + oneClick: true, + }; + + render( + + + + ); + + expect(screen.queryByTestId(selectors.components.VizTooltipFooter.buttons.apply)).not.toBeInTheDocument(); + expect(screen.queryByTestId(selectors.components.VizTooltipFooter.buttons.applyInverse)).not.toBeInTheDocument(); + }); }); diff --git a/packages/grafana-ui/src/components/VizTooltip/VizTooltipFooter.tsx b/packages/grafana-ui/src/components/VizTooltip/VizTooltipFooter.tsx index b4eccaee66a..2bd324129f2 100644 --- a/packages/grafana-ui/src/components/VizTooltip/VizTooltipFooter.tsx +++ b/packages/grafana-ui/src/components/VizTooltip/VizTooltipFooter.tsx @@ -2,6 +2,7 @@ import { css } from '@emotion/css'; import { useMemo } from 'react'; import { ActionModel, Field, GrafanaTheme2, LinkModel, ThemeSpacingTokens } from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; import { Trans } from '@grafana/i18n'; import { useStyles2 } from '../../themes/ThemeContext'; @@ -17,10 +18,16 @@ export interface AdHocFilterModel extends AdHocFilterItem { onClick: () => void; } +export interface FilterByGroupedLabelsModel { + onFilterForGroupedLabels?: () => void; + onFilterOutGroupedLabels?: () => void; +} + interface VizTooltipFooterProps { dataLinks: Array>; actions?: Array>; adHocFilters?: AdHocFilterModel[]; + filterByGroupedLabels?: FilterByGroupedLabelsModel; annotate?: () => void; } @@ -85,7 +92,13 @@ const renderActions = makeRenderLinksOrActions( (item, i) => ); -export const VizTooltipFooter = ({ dataLinks, actions = [], annotate, adHocFilters = [] }: VizTooltipFooterProps) => { +export const VizTooltipFooter = ({ + dataLinks, + actions = [], + annotate, + adHocFilters = [], + filterByGroupedLabels, +}: VizTooltipFooterProps) => { const styles = useStyles2(getStyles); const hasOneClickLink = useMemo(() => dataLinks.some((link) => link.oneClick === true), [dataLinks]); const hasOneClickAction = useMemo(() => actions.some((action) => action.oneClick === true), [actions]); @@ -105,6 +118,39 @@ export const VizTooltipFooter = ({ dataLinks, actions = [], annotate, adHocFilte ))} )} + + {!hasOneClickLink && !hasOneClickAction && filterByGroupedLabels && ( +
+ + + + +
+ )} {!hasOneClickLink && !hasOneClickAction && annotate != null && (