From c952de773d09e4263ebd763014a88c10053d93fe Mon Sep 17 00:00:00 2001 From: Tania <10127682+undef1nd@users.noreply.github.com> Date: Mon, 29 Sep 2025 19:05:44 +0200 Subject: [PATCH] FeatureToggles: Removed deprecated code (#111750) --- conf/defaults.ini | 18 -- conf/sample.ini | 14 -- eslint-suppressions.json | 5 - .../src/types/featureToggles.gen.ts | 4 - pkg/api/api.go | 5 - pkg/apis/featuretoggle/v0alpha1/doc.go | 6 - pkg/apis/featuretoggle/v0alpha1/register.go | 54 ----- .../v0alpha1/zz_generated.deepcopy.go | 215 ------------------ .../v0alpha1/zz_generated.defaults.go | 19 -- ...enerated.openapi_violation_exceptions.list | 3 - .../featuremgmt/feature_toggle_api}/types.go | 8 +- pkg/services/featuremgmt/manager.go | 96 +------- pkg/services/featuremgmt/registry.go | 15 +- pkg/services/featuremgmt/service.go | 1 - pkg/services/featuremgmt/toggles_gen.csv | 1 - pkg/services/featuremgmt/toggles_gen.go | 4 - pkg/services/featuremgmt/toggles_gen.json | 1 + pkg/services/featuremgmt/toggles_gen_test.go | 2 +- pkg/services/navtree/navtreeimpl/admin.go | 9 - pkg/setting/setting.go | 4 - pkg/setting/setting_featuremgmt.go | 38 ---- .../admin/AdminFeatureTogglesAPI.test.ts | 102 --------- .../features/admin/AdminFeatureTogglesAPI.ts | 77 ------- .../admin/AdminFeatureTogglesPage.tsx | 96 -------- .../admin/AdminFeatureTogglesTable.tsx | 201 ---------------- public/app/routes/routes.tsx | 8 - public/locales/en-US/grafana.json | 19 -- 27 files changed, 13 insertions(+), 1012 deletions(-) delete mode 100644 pkg/apis/featuretoggle/v0alpha1/doc.go delete mode 100644 pkg/apis/featuretoggle/v0alpha1/register.go delete mode 100644 pkg/apis/featuretoggle/v0alpha1/zz_generated.deepcopy.go delete mode 100644 pkg/apis/featuretoggle/v0alpha1/zz_generated.defaults.go delete mode 100644 pkg/apis/featuretoggle/v0alpha1/zz_generated.openapi_violation_exceptions.list rename pkg/{apis/featuretoggle/v0alpha1 => services/featuremgmt/feature_toggle_api}/types.go (96%) delete mode 100644 pkg/setting/setting_featuremgmt.go delete mode 100644 public/app/features/admin/AdminFeatureTogglesAPI.test.ts delete mode 100644 public/app/features/admin/AdminFeatureTogglesAPI.ts delete mode 100644 public/app/features/admin/AdminFeatureTogglesPage.tsx delete mode 100644 public/app/features/admin/AdminFeatureTogglesTable.tsx diff --git a/conf/defaults.ini b/conf/defaults.ini index c7044021cd1..bc599285555 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -2148,24 +2148,6 @@ show_ui = true # Disables TLS in the secure socks proxy allow_insecure = false -################################## Feature Management ############################################## -# Options to configure the experimental Feature Toggle Admin Page feature, which is behind the `featureToggleAdminPage` feature toggle. Use at your own risk. -[feature_management] -# Allows editing of feature toggles in the feature management page -allow_editing = false - -# Allow customization of URL for the controller that manages feature toggles -update_webhook = - -# Allow configuring an auth token for feature management update requests -update_webhook_token = - -# Hides specific feature toggles from the feature management page -hidden_toggles = - -# Disables updating specific feature toggles in the feature management page -read_only_toggles = - #################################### Public Dashboards ##################################### [public_dashboards] # Set to false to disable public dashboards diff --git a/conf/sample.ini b/conf/sample.ini index eec89e05b73..964b3df5196 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -2050,20 +2050,6 @@ default_datasource_uid = ; show_ui = true ; allow_insecure = false -################################## Feature Management ############################################## -[feature_management] -# Options to configure the experimental Feature Toggle Admin Page feature, which is behind the `featureToggleAdminPage` feature toggle. Use at your own risk. -# Allow editing of feature toggles in the feature management page -;allow_editing = false -# Allow customization of URL for the controller that manages feature toggles -;update_webhook = -# Allow configuring an auth token for feature management update requests -;update_webhook_token = -# Hide specific feature toggles from the feature management page -;hidden_toggles = -# Disable updating specific feature toggles in the feature management page -;read_only_toggles = - #################################### Public Dashboards ##################################### [public_dashboards] # Set to false to disable public dashboards diff --git a/eslint-suppressions.json b/eslint-suppressions.json index a8b5d823be1..2bfa991ffaf 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -1359,11 +1359,6 @@ "count": 1 } }, - "public/app/features/admin/AdminFeatureTogglesTable.tsx": { - "no-restricted-syntax": { - "count": 3 - } - }, "public/app/features/admin/ServerStatsCard.tsx": { "no-restricted-syntax": { "count": 1 diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 06546e18d9d..2bf402c41e0 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -205,10 +205,6 @@ export interface FeatureToggles { */ grafanaAPIServerEnsureKubectlAccess?: boolean; /** - * Enable admin page for managing feature toggles from the Grafana front-end. Grafana Cloud only. - */ - featureToggleAdminPage?: boolean; - /** * Enable caching for async queries for Redshift and Athena. Requires that the datasource has caching and async query support enabled * @default true */ diff --git a/pkg/api/api.go b/pkg/api/api.go index 87cf69d966d..4710056875d 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -125,11 +125,6 @@ func (hs *HTTPServer) registerRoutes() { r.Get("/admin/migrate-to-cloud", authorize(cloudmigration.MigrationAssistantAccess), hs.Index) } - // feature toggle admin page - if hs.Features.IsEnabledGlobally(featuremgmt.FlagFeatureToggleAdminPage) { - r.Get("/admin/featuretoggles", authorize(ac.EvalPermission(ac.ActionFeatureManagementRead)), hs.Index) - } - // secrets management page if hs.Features.IsEnabledGlobally(featuremgmt.FlagSecretsManagementAppPlatform) && hs.Features.IsEnabledGlobally(featuremgmt.FlagSecretsManagementAppPlatformUI) { r.Get("/admin/secrets", authorize(ac.EvalAny( diff --git a/pkg/apis/featuretoggle/v0alpha1/doc.go b/pkg/apis/featuretoggle/v0alpha1/doc.go deleted file mode 100644 index 222ebfd88b5..00000000000 --- a/pkg/apis/featuretoggle/v0alpha1/doc.go +++ /dev/null @@ -1,6 +0,0 @@ -// +k8s:deepcopy-gen=package -// +k8s:openapi-gen=true -// +k8s:defaulter-gen=TypeMeta -// +groupName=featuretoggle.grafana.app - -package v0alpha1 // import "github.com/grafana/grafana/pkg/apis/featuretoggle/v0alpha1" diff --git a/pkg/apis/featuretoggle/v0alpha1/register.go b/pkg/apis/featuretoggle/v0alpha1/register.go deleted file mode 100644 index efcad19821b..00000000000 --- a/pkg/apis/featuretoggle/v0alpha1/register.go +++ /dev/null @@ -1,54 +0,0 @@ -package v0alpha1 - -import ( - "fmt" - - "github.com/grafana/grafana/pkg/apimachinery/utils" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - runtime "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" -) - -const ( - GROUP = "featuretoggle.grafana.app" - VERSION = "v0alpha1" - APIVERSION = GROUP + "/" + VERSION -) - -// FeatureResourceInfo represents each feature that may have a toggle -var FeatureResourceInfo = utils.NewResourceInfo(GROUP, VERSION, - "features", "feature", "Feature", - func() runtime.Object { return &Feature{} }, - func() runtime.Object { return &FeatureList{} }, - utils.TableColumns{ - Definition: []metav1.TableColumnDefinition{ - {Name: "Name", Type: "string", Format: "name"}, - {Name: "Stage", Type: "string", Format: "string", Description: "Where is the flag in the dev cycle"}, - {Name: "Owner", Type: "string", Format: "string", Description: "Which team owns the feature"}, - }, - Reader: func(obj any) ([]interface{}, error) { - r, ok := obj.(*Feature) - if ok { - return []interface{}{ - r.Name, - r.Spec.Stage, - r.Spec.Owner, - }, nil - } - return nil, fmt.Errorf("expected resource or info") - }, - }, -) - -// TogglesResourceInfo represents the actual configuration -var TogglesResourceInfo = utils.NewResourceInfo(GROUP, VERSION, - "featuretoggles", "featuretoggle", "FeatureToggles", - func() runtime.Object { return &FeatureToggles{} }, - func() runtime.Object { return &FeatureTogglesList{} }, - utils.TableColumns{}, // default table converter -) - -var ( - // SchemeGroupVersion is group version used to register these objects - SchemeGroupVersion = schema.GroupVersion{Group: GROUP, Version: VERSION} -) diff --git a/pkg/apis/featuretoggle/v0alpha1/zz_generated.deepcopy.go b/pkg/apis/featuretoggle/v0alpha1/zz_generated.deepcopy.go deleted file mode 100644 index 6b06fab2b6d..00000000000 --- a/pkg/apis/featuretoggle/v0alpha1/zz_generated.deepcopy.go +++ /dev/null @@ -1,215 +0,0 @@ -//go:build !ignore_autogenerated -// +build !ignore_autogenerated - -// SPDX-License-Identifier: AGPL-3.0-only - -// Code generated by deepcopy-gen. DO NOT EDIT. - -package v0alpha1 - -import ( - commonv0alpha1 "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" - runtime "k8s.io/apimachinery/pkg/runtime" -) - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Feature) DeepCopyInto(out *Feature) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Feature. -func (in *Feature) DeepCopy() *Feature { - if in == nil { - return nil - } - out := new(Feature) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *Feature) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *FeatureList) DeepCopyInto(out *FeatureList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]Feature, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeatureList. -func (in *FeatureList) DeepCopy() *FeatureList { - if in == nil { - return nil - } - out := new(FeatureList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *FeatureList) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *FeatureSpec) DeepCopyInto(out *FeatureSpec) { - *out = *in - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeatureSpec. -func (in *FeatureSpec) DeepCopy() *FeatureSpec { - if in == nil { - return nil - } - out := new(FeatureSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *FeatureToggles) DeepCopyInto(out *FeatureToggles) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - if in.Spec != nil { - in, out := &in.Spec, &out.Spec - *out = make(map[string]bool, len(*in)) - for key, val := range *in { - (*out)[key] = val - } - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeatureToggles. -func (in *FeatureToggles) DeepCopy() *FeatureToggles { - if in == nil { - return nil - } - out := new(FeatureToggles) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *FeatureToggles) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *FeatureTogglesList) DeepCopyInto(out *FeatureTogglesList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]FeatureToggles, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeatureTogglesList. -func (in *FeatureTogglesList) DeepCopy() *FeatureTogglesList { - if in == nil { - return nil - } - out := new(FeatureTogglesList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *FeatureTogglesList) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ResolvedToggleState) DeepCopyInto(out *ResolvedToggleState) { - *out = *in - out.TypeMeta = in.TypeMeta - if in.Enabled != nil { - in, out := &in.Enabled, &out.Enabled - *out = make(map[string]bool, len(*in)) - for key, val := range *in { - (*out)[key] = val - } - } - if in.Toggles != nil { - in, out := &in.Toggles, &out.Toggles - *out = make([]ToggleStatus, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResolvedToggleState. -func (in *ResolvedToggleState) DeepCopy() *ResolvedToggleState { - if in == nil { - return nil - } - out := new(ResolvedToggleState) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *ResolvedToggleState) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ToggleStatus) DeepCopyInto(out *ToggleStatus) { - *out = *in - if in.Source != nil { - in, out := &in.Source, &out.Source - *out = new(commonv0alpha1.ObjectReference) - **out = **in - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ToggleStatus. -func (in *ToggleStatus) DeepCopy() *ToggleStatus { - if in == nil { - return nil - } - out := new(ToggleStatus) - in.DeepCopyInto(out) - return out -} diff --git a/pkg/apis/featuretoggle/v0alpha1/zz_generated.defaults.go b/pkg/apis/featuretoggle/v0alpha1/zz_generated.defaults.go deleted file mode 100644 index 238fc2f4edc..00000000000 --- a/pkg/apis/featuretoggle/v0alpha1/zz_generated.defaults.go +++ /dev/null @@ -1,19 +0,0 @@ -//go:build !ignore_autogenerated -// +build !ignore_autogenerated - -// SPDX-License-Identifier: AGPL-3.0-only - -// Code generated by defaulter-gen. DO NOT EDIT. - -package v0alpha1 - -import ( - runtime "k8s.io/apimachinery/pkg/runtime" -) - -// RegisterDefaults adds defaulters functions to the given scheme. -// Public to allow building arbitrary schemes. -// All generated defaulters are covering - they call all nested defaulters. -func RegisterDefaults(scheme *runtime.Scheme) error { - return nil -} diff --git a/pkg/apis/featuretoggle/v0alpha1/zz_generated.openapi_violation_exceptions.list b/pkg/apis/featuretoggle/v0alpha1/zz_generated.openapi_violation_exceptions.list deleted file mode 100644 index 2a3961b3eee..00000000000 --- a/pkg/apis/featuretoggle/v0alpha1/zz_generated.openapi_violation_exceptions.list +++ /dev/null @@ -1,3 +0,0 @@ -API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/featuretoggle/v0alpha1,ResolvedToggleState,Toggles -API rule violation: names_match,github.com/grafana/grafana/pkg/apis/featuretoggle/v0alpha1,FeatureSpec,FrontendOnly -API rule violation: names_match,github.com/grafana/grafana/pkg/apis/featuretoggle/v0alpha1,FeatureSpec,Owner diff --git a/pkg/apis/featuretoggle/v0alpha1/types.go b/pkg/services/featuremgmt/feature_toggle_api/types.go similarity index 96% rename from pkg/apis/featuretoggle/v0alpha1/types.go rename to pkg/services/featuremgmt/feature_toggle_api/types.go index 664ff1adec5..6b62447fa33 100644 --- a/pkg/apis/featuretoggle/v0alpha1/types.go +++ b/pkg/services/featuremgmt/feature_toggle_api/types.go @@ -1,4 +1,4 @@ -package v0alpha1 +package feature_toggle_api import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -6,6 +6,12 @@ import ( common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" ) +const ( + GROUP = "featuretoggle.grafana.app" + VERSION = "v0alpha1" + APIVERSION = GROUP + "/" + VERSION +) + // Feature represents a feature in development and information about that feature // It does *not* know the status, only defines properties about the feature itself // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/pkg/services/featuremgmt/manager.go b/pkg/services/featuremgmt/manager.go index 457436790a5..5cfe588a518 100644 --- a/pkg/services/featuremgmt/manager.go +++ b/pkg/services/featuremgmt/manager.go @@ -6,7 +6,6 @@ import ( "reflect" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/setting" ) var ( @@ -14,10 +13,7 @@ var ( ) type FeatureManager struct { - isDevMod bool - restartRequired bool - - Settings setting.FeatureMgmtSettings + isDevMod bool flags map[string]*FeatureFlag enabled map[string]bool // only the "on" values @@ -131,66 +127,6 @@ func (fm *FeatureManager) GetFlags() []FeatureFlag { return v } -// isFeatureEditingAllowed checks if the backend is properly configured to allow feature toggle changes from the UI -func (fm *FeatureManager) IsFeatureEditingAllowed() bool { - return fm.Settings.AllowEditing && fm.Settings.UpdateWebhook != "" -} - -// indicate if a change has been made (not that accurate, but better than nothing) -func (fm *FeatureManager) IsRestartRequired() bool { - return fm.restartRequired -} - -// Flags that can be edited -func (fm *FeatureManager) IsEditableFromAdminPage(key string) bool { - flag, ok := fm.flags[key] - if !ok || - !fm.IsFeatureEditingAllowed() || - !flag.AllowSelfServe || - flag.Name == FlagFeatureToggleAdminPage { - return false - } - return flag.Stage == FeatureStageGeneralAvailability || - flag.Stage == FeatureStagePublicPreview || - flag.Stage == FeatureStageDeprecated -} - -// Flags that should not be shown in the UI (regardless of their state) -func (fm *FeatureManager) IsHiddenFromAdminPage(key string, lenient bool) bool { - _, hide := fm.Settings.HiddenToggles[key] - flag, ok := fm.flags[key] - if !ok || flag.HideFromAdminPage || hide { - return true // unknown flag (should we show it as a warning!) - } - - // Explicitly hidden from configs - _, found := fm.Settings.HiddenToggles[key] - if found { - return true - } - if lenient { - return false - } - - return flag.Stage == FeatureStageUnknown || - flag.Stage == FeatureStageExperimental || - flag.Stage == FeatureStagePrivatePreview -} - -// Get the flags that were explicitly set on startup -func (fm *FeatureManager) GetStartupFlags() map[string]bool { - return fm.startup -} - -// Perhaps expose the flag warnings -func (fm *FeatureManager) GetWarning() map[string]string { - return fm.warnings -} - -func (fm *FeatureManager) SetRestartRequired() { - fm.restartRequired = true -} - // ############# Test Functions ############# func WithFeatures(spec ...any) FeatureToggles { @@ -223,33 +159,3 @@ func WithManager(spec ...any) *FeatureManager { return &FeatureManager{enabled: enabled, flags: features, startup: enabled, warnings: map[string]string{}} } - -// WithFeatureManager is used to define feature toggle manager for testing. -// It should be used when your test feature toggles require metadata beyond `Name` and `Enabled`. -// You should provide a feature toggle Name at a minimum. -func WithFeatureManager(cfg setting.FeatureMgmtSettings, flags []*FeatureFlag, disabled ...string) *FeatureManager { - count := len(flags) - features := make(map[string]*FeatureFlag, count) - enabled := make(map[string]bool, count) - - dis := make(map[string]bool) - for _, v := range disabled { - dis[v] = true - } - - for _, f := range flags { - if f.Name == "" { - continue - } - features[f.Name] = f - enabled[f.Name] = !dis[f.Name] - } - - return &FeatureManager{ - Settings: cfg, - enabled: enabled, - flags: features, - startup: enabled, - warnings: map[string]string{}, - } -} diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index c13012b264e..5ceeb8b3166 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -11,7 +11,7 @@ import ( "embed" "encoding/json" - featuretoggle "github.com/grafana/grafana/pkg/apis/featuretoggle/v0alpha1" + featuretoggleapi "github.com/grafana/grafana/pkg/services/featuremgmt/feature_toggle_api" ) var ( @@ -332,15 +332,6 @@ var ( RequiresRestart: true, Owner: grafanaAppPlatformSquad, }, - { - Name: "featureToggleAdminPage", - Description: "Enable admin page for managing feature toggles from the Grafana front-end. Grafana Cloud only.", - Stage: FeatureStageExperimental, - FrontendOnly: false, - Owner: grafanaBackendServicesSquad, - RequiresRestart: true, - HideFromDocs: true, - }, { Name: "awsAsyncQueryCaching", Description: "Enable caching for async queries for Redshift and Athena. Requires that the datasource has caching and async query support enabled", @@ -2073,8 +2064,8 @@ var ( var f embed.FS // Get the cached feature list (exposed as a k8s resource) -func GetEmbeddedFeatureList() (featuretoggle.FeatureList, error) { - features := featuretoggle.FeatureList{} +func GetEmbeddedFeatureList() (featuretoggleapi.FeatureList, error) { + features := featuretoggleapi.FeatureList{} body, err := f.ReadFile("toggles_gen.json") if err == nil { err = json.Unmarshal(body, &features) diff --git a/pkg/services/featuremgmt/service.go b/pkg/services/featuremgmt/service.go index d1bc74b17cd..2769a75d788 100644 --- a/pkg/services/featuremgmt/service.go +++ b/pkg/services/featuremgmt/service.go @@ -27,7 +27,6 @@ func ProvideManagerService(cfg *setting.Cfg) (*FeatureManager, error) { enabled: make(map[string]bool), startup: make(map[string]bool), warnings: make(map[string]string), - Settings: cfg.FeatureManagement, log: log.New("featuremgmt"), } diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 243f0e7b37a..4a3c802aea5 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -42,7 +42,6 @@ datasourceAPIServers,experimental,@grafana/grafana-app-platform-squad,false,true grafanaAPIServerWithExperimentalAPIs,experimental,@grafana/grafana-app-platform-squad,true,true,false provisioning,experimental,@grafana/grafana-app-platform-squad,false,true,false grafanaAPIServerEnsureKubectlAccess,experimental,@grafana/grafana-app-platform-squad,true,true,false -featureToggleAdminPage,experimental,@grafana/grafana-backend-services-squad,false,true,false awsAsyncQueryCaching,GA,@grafana/aws-datasources,false,false,false queryCacheRequestDeduplication,experimental,@grafana/grafana-operator-experience-squad,false,false,false permissionsFilterRemoveSubquery,experimental,@grafana/search-and-storage,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index b189a80ead6..6f3813ead1c 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -179,10 +179,6 @@ const ( // Start an additional https handler and write kubectl options FlagGrafanaAPIServerEnsureKubectlAccess = "grafanaAPIServerEnsureKubectlAccess" - // FlagFeatureToggleAdminPage - // Enable admin page for managing feature toggles from the Grafana front-end. Grafana Cloud only. - FlagFeatureToggleAdminPage = "featureToggleAdminPage" - // FlagAwsAsyncQueryCaching // Enable caching for async queries for Redshift and Athena. Requires that the datasource has caching and async query support enabled FlagAwsAsyncQueryCaching = "awsAsyncQueryCaching" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index ec62239bd1e..cb15026bfd3 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -1577,6 +1577,7 @@ "name": "featureToggleAdminPage", "resourceVersion": "1758022099771", "creationTimestamp": "2023-07-18T20:43:32Z", + "deletionTimestamp": "2025-09-29T13:36:16Z", "annotations": { "grafana.app/updatedTimestamp": "2025-09-16 11:28:19.771156 +0000 UTC" } diff --git a/pkg/services/featuremgmt/toggles_gen_test.go b/pkg/services/featuremgmt/toggles_gen_test.go index d3c7f88f746..d6a7c66802d 100644 --- a/pkg/services/featuremgmt/toggles_gen_test.go +++ b/pkg/services/featuremgmt/toggles_gen_test.go @@ -21,7 +21,7 @@ import ( v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/grafana/grafana/pkg/apimachinery/utils" - featuretoggleapi "github.com/grafana/grafana/pkg/apis/featuretoggle/v0alpha1" + featuretoggleapi "github.com/grafana/grafana/pkg/services/featuremgmt/feature_toggle_api" "github.com/grafana/grafana/pkg/services/featuremgmt/strcase" ) diff --git a/pkg/services/navtree/navtreeimpl/admin.go b/pkg/services/navtree/navtreeimpl/admin.go index ff256522fba..8794a57c3e9 100644 --- a/pkg/services/navtree/navtreeimpl/admin.go +++ b/pkg/services/navtree/navtreeimpl/admin.go @@ -44,15 +44,6 @@ func (s *ServiceImpl) getAdminNode(c *contextmodel.ReqContext) (*navtree.NavLink Text: "Organizations", SubTitle: "Isolated instances of Grafana running on the same server", Id: "global-orgs", Url: s.cfg.AppSubURL + "/admin/orgs", Icon: "building", }) } - if s.features.IsEnabled(ctx, featuremgmt.FlagFeatureToggleAdminPage) && hasAccess(ac.EvalPermission(ac.ActionFeatureManagementRead)) { - generalNodeLinks = append(generalNodeLinks, &navtree.NavLink{ - Text: "Feature toggles", - SubTitle: "View and edit feature toggles", - Id: "feature-toggles", - Url: s.cfg.AppSubURL + "/admin/featuretoggles", - Icon: "toggle-on", - }) - } if hasAccess(cloudmigration.MigrationAssistantAccess) && s.features.IsEnabled(ctx, featuremgmt.FlagOnPremToCloudMigrations) { generalNodeLinks = append(generalNodeLinks, &navtree.NavLink{ Text: "Migrate to Grafana Cloud", diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 50cecb688bb..21f43269fc9 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -543,9 +543,6 @@ type Cfg struct { // Cloud Migration CloudMigration CloudMigrationSettings - // Feature Management Settings - FeatureManagement FeatureMgmtSettings - // Alerting AlertingEvaluationTimeout time.Duration AlertingNotificationTimeout time.Duration @@ -1429,7 +1426,6 @@ func (cfg *Cfg) parseINIFile(iniFile *ini.File) error { logSection := iniFile.Section("log") cfg.UserFacingDefaultError = logSection.Key("user_facing_default_error").MustString("please inspect Grafana server log for details") - cfg.readFeatureManagementConfig() cfg.readPublicDashboardsSettings() cfg.readCloudMigrationSettings() cfg.readSecretsManagerSettings() diff --git a/pkg/setting/setting_featuremgmt.go b/pkg/setting/setting_featuremgmt.go deleted file mode 100644 index 1887e9e0856..00000000000 --- a/pkg/setting/setting_featuremgmt.go +++ /dev/null @@ -1,38 +0,0 @@ -package setting - -import ( - "github.com/grafana/grafana/pkg/util" -) - -type FeatureMgmtSettings struct { - HiddenToggles map[string]struct{} - ReadOnlyToggles map[string]struct{} - AllowEditing bool - UpdateWebhook string - UpdateWebhookToken string -} - -func (cfg *Cfg) readFeatureManagementConfig() { - section := cfg.Raw.Section("feature_management") - - hiddenToggles := make(map[string]struct{}) - readOnlyToggles := make(map[string]struct{}) - - // parse the comma separated list in `hidden_toggles`. - hiddenTogglesStr := valueAsString(section, "hidden_toggles", "") - for _, feature := range util.SplitString(hiddenTogglesStr) { - hiddenToggles[feature] = struct{}{} - } - - // parse the comma separated list in `read_only_toggles`. - readOnlyTogglesStr := valueAsString(section, "read_only_toggles", "") - for _, feature := range util.SplitString(readOnlyTogglesStr) { - readOnlyToggles[feature] = struct{}{} - } - - cfg.FeatureManagement.HiddenToggles = hiddenToggles - cfg.FeatureManagement.ReadOnlyToggles = readOnlyToggles - cfg.FeatureManagement.AllowEditing = cfg.SectionWithEnvOverrides("feature_management").Key("allow_editing").MustBool(false) - cfg.FeatureManagement.UpdateWebhook = cfg.SectionWithEnvOverrides("feature_management").Key("update_webhook").MustString("") - cfg.FeatureManagement.UpdateWebhookToken = cfg.SectionWithEnvOverrides("feature_management").Key("update_webhook_token").MustString("") -} diff --git a/public/app/features/admin/AdminFeatureTogglesAPI.test.ts b/public/app/features/admin/AdminFeatureTogglesAPI.test.ts deleted file mode 100644 index ca6c25be525..00000000000 --- a/public/app/features/admin/AdminFeatureTogglesAPI.test.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { BackendSrvRequest, config } from '@grafana/runtime'; - -import { getTogglesAPI } from './AdminFeatureTogglesAPI'; - -// implements @grafana/runtime/BackendSrv -class MockSrv { - constructor() { - this.apiCalls = []; - } - - apiCalls: Array<{ - url: string; - method: string; - }>; - - async get( - url: string, - params?: BackendSrvRequest['params'], - requestId?: BackendSrvRequest['requestId'], - options?: Partial - ) { - this.apiCalls.push({ - url: url, - method: 'get', - }); - if (config.featureToggles.kubernetesFeatureToggles && url.indexOf('current') > -1) { - return await { toggles: [] }; - } - - return await {}; - } - - async post(url: string, data?: unknown, options?: Partial) { - this.apiCalls.push({ - url: url, - method: 'post', - }); - return await {}; - } - - async patch(url: string, data: unknown, options?: Partial) { - this.apiCalls.push({ - url: url, - method: 'patch', - }); - return await {}; - } - - // these aren't needed for this test - async put(url: string, data: unknown, options?: Partial) { - return await {}; - } - async delete(url: string, data?: unknown, options?: Partial) { - return await {}; - } -} - -const testBackendSrv = new MockSrv(); - -jest.mock('@grafana/runtime', () => ({ - ...jest.requireActual('@grafana/runtime'), - getBackendSrv: () => testBackendSrv, - config: { - featureToggles: { - kubernetesFeatureToggles: false, - grafanaAPIServerWithExperimentalAPIs: false, - }, - }, -})); - -describe('AdminFeatureTogglesApi', () => { - beforeEach(() => { - jest.clearAllMocks(); - testBackendSrv.apiCalls.length = 0; - }); - - const originalToggles = { ...config.featureToggles }; - - afterAll(() => { - config.featureToggles = originalToggles; - }); - - it('uses the k8s api when the k8s toggles are on', async () => { - config.featureToggles.kubernetesFeatureToggles = true; - config.featureToggles.grafanaAPIServerWithExperimentalAPIs = true; - - const togglesApi = getTogglesAPI(); - await togglesApi.getFeatureToggles(); - await togglesApi.updateFeatureToggles([]); - const expected = [ - { - method: 'get', - url: '/apis/featuretoggle.grafana.app/v0alpha1/current', - }, - { - method: 'patch', - url: '/apis/featuretoggle.grafana.app/v0alpha1/current', - }, - ]; - expect(testBackendSrv.apiCalls).toEqual(expect.arrayContaining(expected)); - }); -}); diff --git a/public/app/features/admin/AdminFeatureTogglesAPI.ts b/public/app/features/admin/AdminFeatureTogglesAPI.ts deleted file mode 100644 index 6ecba0c8b7d..00000000000 --- a/public/app/features/admin/AdminFeatureTogglesAPI.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { getBackendSrv } from '@grafana/runtime'; - -export type FeatureToggle = { - name: string; - description?: string; - enabled: boolean; - stage: string; - readOnly?: boolean; - hidden?: boolean; -}; - -export type CurrentTogglesState = { - restartRequired: boolean; - allowEditing: boolean; - toggles: FeatureToggle[]; -}; - -interface ResolvedToggleState { - kind: 'ResolvedToggleState'; - restartRequired?: boolean; - allowEditing?: boolean; - toggles?: K8sToggleSpec[]; // not used in patch - enabled: { [key: string]: boolean }; -} - -interface K8sToggleSpec { - name: string; - description: string; - enabled: boolean; - writeable: boolean; - source: K8sToggleSource; - stage: string; -} - -interface K8sToggleSource { - namespace: string; - name: string; -} - -interface FeatureTogglesAPI { - getFeatureToggles(): Promise; - updateFeatureToggles(toggles: FeatureToggle[]): Promise; -} - -class K8sAPI implements FeatureTogglesAPI { - baseURL = '/apis/featuretoggle.grafana.app/v0alpha1'; - - async getFeatureToggles(): Promise { - const current = await getBackendSrv().get(this.baseURL + '/current'); - return { - restartRequired: Boolean(current.restartRequired), - allowEditing: Boolean(current.allowEditing), - toggles: current.toggles!.map((t) => ({ - name: t.name, - description: t.description!, - enabled: t.enabled, - readOnly: !Boolean(t.writeable), - stage: t.stage, - hidden: false, // only return visible things - })), - }; - } - updateFeatureToggles(toggles: FeatureToggle[]): Promise { - const patchBody: ResolvedToggleState = { - kind: 'ResolvedToggleState', - enabled: {}, - }; - toggles.forEach((t) => { - patchBody.enabled[t.name] = t.enabled; - }); - return getBackendSrv().patch(this.baseURL + '/current', patchBody); - } -} - -export const getTogglesAPI = (): FeatureTogglesAPI => { - return new K8sAPI(); -}; diff --git a/public/app/features/admin/AdminFeatureTogglesPage.tsx b/public/app/features/admin/AdminFeatureTogglesPage.tsx deleted file mode 100644 index 113847983e1..00000000000 --- a/public/app/features/admin/AdminFeatureTogglesPage.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import { css } from '@emotion/css'; -import { useState } from 'react'; -import { useAsync } from 'react-use'; - -import { GrafanaTheme2 } from '@grafana/data'; -import { Trans, t } from '@grafana/i18n'; -import { useStyles2, Icon, TextLink } from '@grafana/ui'; -import { Page } from 'app/core/components/Page/Page'; - -import { getTogglesAPI } from './AdminFeatureTogglesAPI'; -import { AdminFeatureTogglesTable } from './AdminFeatureTogglesTable'; - -export default function AdminFeatureTogglesPage() { - const [reload, setReload] = useState(1); - const togglesApi = getTogglesAPI(); - const featureState = useAsync(() => togglesApi.getFeatureToggles(), [reload]); - const styles = useStyles2(getStyles); - - const handleUpdateSuccess = () => { - setReload(reload + 1); - }; - - const EditingAlert = () => { - return ( -
-
- -
- - {featureState.value?.restartRequired - ? t( - 'admin.feature-toggles.restart-pending', - 'A restart is pending for your Grafana instance to apply the latest feature toggle changes' - ) - : t( - 'admin.feature-toggles.restart-required', - 'Saving feature toggle changes will prompt a restart of the instance, which may take a few minutes' - )} - -
- ); - }; - - const subTitle = ( -
- - View and edit feature toggles. Read more about feature toggles at{' '} - - grafana.com - - . - -
- ); - - return ( - - - <> - {featureState.error?.message} - {featureState.loading && 'Fetching feature toggles'} - - - {featureState.value && ( - - )} - - - - ); -} - -function getStyles(theme: GrafanaTheme2) { - return { - warning: css({ - display: 'flex', - marginTop: theme.spacing(0.25), - marginBottom: theme.spacing(0.25), - }), - icon: css({ - color: theme.colors.warning.main, - paddingRight: theme.spacing(), - }), - message: css({ - color: theme.colors.text.secondary, - marginTop: theme.spacing(0.25), - }), - }; -} diff --git a/public/app/features/admin/AdminFeatureTogglesTable.tsx b/public/app/features/admin/AdminFeatureTogglesTable.tsx deleted file mode 100644 index 65b6112652c..00000000000 --- a/public/app/features/admin/AdminFeatureTogglesTable.tsx +++ /dev/null @@ -1,201 +0,0 @@ -import { useState, useRef } from 'react'; - -import { Trans, t } from '@grafana/i18n'; -import { Switch, InteractiveTable, Tooltip, type CellProps, Button, ConfirmModal, type SortByFn } from '@grafana/ui'; - -import { FeatureToggle, getTogglesAPI } from './AdminFeatureTogglesAPI'; - -interface Props { - featureToggles: FeatureToggle[]; - allowEditing: boolean; - onUpdateSuccess: () => void; -} - -const sortByName: SortByFn = (a, b) => { - return a.original.name.localeCompare(b.original.name); -}; - -const sortByDescription: SortByFn = (a, b) => { - if (!a.original.description && !b.original.description) { - return 0; - } else if (!a.original.description) { - return 1; - } else if (!b.original.description) { - return -1; - } - return a.original.description.localeCompare(b.original.description); -}; - -const sortByEnabled: SortByFn = (a, b) => { - return a.original.enabled === b.original.enabled ? 0 : a.original.enabled ? 1 : -1; -}; - -export function AdminFeatureTogglesTable({ featureToggles, allowEditing, onUpdateSuccess }: Props) { - // sort manually, doesn't look like it can be automatically done in the table - featureToggles.sort((a, b) => a.name.localeCompare(b.name)); - const serverToggles = useRef(featureToggles); - const [localToggles, setLocalToggles] = useState(featureToggles); - const [isSaving, setIsSaving] = useState(false); - const [showSaveModel, setShowSaveModal] = useState(false); - - const togglesApi = getTogglesAPI(); - - const handleToggleChange = (toggle: FeatureToggle, newValue: boolean) => { - const updatedToggle = { ...toggle, enabled: newValue }; - - // Update the local state - const updatedToggles = localToggles.map((t) => (t.name === toggle.name ? updatedToggle : t)); - setLocalToggles(updatedToggles); - }; - - const handleSaveChanges = async () => { - setIsSaving(true); - try { - const modifiedToggles = getModifiedToggles(); - await togglesApi.updateFeatureToggles(modifiedToggles); - // Pretend the values came from a new request - serverToggles.current = [...localToggles]; - onUpdateSuccess(); // should trigger a new get - } finally { - setIsSaving(false); - } - }; - - const saveButtonRef = useRef(null); - const showSaveChangesModal = (show: boolean) => () => { - setShowSaveModal(show); - if (!show && saveButtonRef.current) { - saveButtonRef.current.focus(); - } - }; - - const getModifiedToggles = (): FeatureToggle[] => { - return localToggles.filter((toggle, index) => toggle.enabled !== serverToggles.current[index].enabled); - }; - - const hasModifications = () => { - // Check if there are any differences between the original toggles and the local toggles - return localToggles.some((toggle, index) => toggle.enabled !== serverToggles.current[index].enabled); - }; - - const getToggleTooltipContent = (readOnlyToggle?: boolean) => { - if (!allowEditing) { - return 'Feature management is not configured for editing'; - } - if (readOnlyToggle) { - return 'This is a non-editable feature'; - } - return ''; - }; - - const getStageCell = (stage: string) => { - switch (stage) { - case 'GA': - return ( - -
- GA -
-
- ); - case 'privatePreview': - case 'preview': - case 'experimental': - return t('admin.admin-feature-toggles-table.get-stage-cell.beta', 'Beta'); - case 'deprecated': - return t('admin.admin-feature-toggles-table.get-stage-cell.deprecated', 'Deprecated'); - default: - return stage; - } - }; - - const columns = [ - { - id: 'name', - header: 'Name', - cell: ({ cell: { value } }: CellProps) =>
{value}
, - sortType: sortByName, - }, - { - id: 'description', - header: 'Description', - cell: ({ cell: { value } }: CellProps) =>
{value}
, - sortType: sortByDescription, - }, - { - id: 'stage', - header: 'Stage', - cell: ({ cell: { value } }: CellProps) =>
{getStageCell(value)}
, - }, - { - id: 'enabled', - header: 'State', - cell: ({ row }: CellProps) => { - const renderStateSwitch = ( -
- handleToggleChange(row.original, e.currentTarget.checked)} - /> -
- ); - - return row.original.readOnly ? ( - {renderStateSwitch} - ) : ( - renderStateSwitch - ); - }, - sortType: sortByEnabled, - }, - ]; - - return ( - <> - {allowEditing && ( -
- - -

- - Some features are stable (GA) and enabled by default, whereas some are currently in their - preliminary Beta phase, available for early adoption. - -

-

- - We advise understanding the implications of each feature change before making modifications. - -

-
- } - confirmText={t('admin.admin-feature-toggles-table.confirmText-save-changes', 'Save changes')} - onConfirm={async () => { - showSaveChangesModal(false)(); - handleSaveChanges(); - }} - onDismiss={showSaveChangesModal(false)} - /> - - )} - featureToggle.name} /> - - ); -} diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx index 3767ed90322..80b57083042 100644 --- a/public/app/routes/routes.tsx +++ b/public/app/routes/routes.tsx @@ -364,14 +364,6 @@ export function getAppRoutes(): RouteDescriptor[] { () => import(/* webpackChunkName: "AdminEditOrgPage" */ 'app/features/admin/AdminEditOrgPage') ), }, - { - path: '/admin/featuretoggles', - component: config.featureToggles.featureToggleAdminPage - ? SafeDynamicImport( - () => import(/* webpackChunkName: "AdminFeatureTogglesPage" */ 'app/features/admin/AdminFeatureTogglesPage') - ) - : () => , - }, { path: '/admin/stats', component: SafeDynamicImport( diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 93340dcbcc7..5015fd2da29 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -81,20 +81,6 @@ "title-access-denied": "Access denied" } }, - "admin-feature-toggles-table": { - "confirm-modal-body-1": "Some features are stable (GA) and enabled by default, whereas some are currently in their preliminary Beta phase, available for early adoption.", - "confirm-modal-body-2": "We advise understanding the implications of each feature change before making modifications.", - "confirmText-save-changes": "Save changes", - "get-stage-cell": { - "beta": "Beta", - "content-general-availability": "General availability", - "deprecated": "Deprecated", - "ga": "GA" - }, - "save-changes": "Save changes", - "saving": "Saving...", - "title-apply-feature-toggle-changes": "Apply feature toggle changes" - }, "admin-orgs-table": { "aria-label-delete-org": "Delete org", "confirmText-delete": "Delete", @@ -140,11 +126,6 @@ "title-sort-dashboards-by-popularity-in-search": "Sort dashboards by popularity in search", "title-team-sync": "Team Sync" }, - "feature-toggles": { - "restart-pending": "A restart is pending for your Grafana instance to apply the latest feature toggle changes", - "restart-required": "Saving feature toggle changes will prompt a restart of the instance, which may take a few minutes", - "sub-title": "View and edit feature toggles. Read more about feature toggles at <2>grafana.com." - }, "get-enterprise": { "contact-us": "Contact us and get a free trial", "description": "You can use the trial version for free for 30 days. We will remind you about it five days before the trial period ends.",